Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F51325
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
12 KB
Subscribers
None
View Options
diff --git a/lib/exirc/utils.ex b/lib/exirc/utils.ex
index b2a79cd..2573cc3 100644
--- a/lib/exirc/utils.ex
+++ b/lib/exirc/utils.ex
@@ -1,182 +1,182 @@
defmodule ExIrc.Utils do
######################
# IRC Message Parsing
######################
@doc """
Parse an IRC message
Example:
data = ':irc.example.org 005 nick NETWORK=Freenode PREFIX=(ov)@+ CHANTYPES=#&'
message = ExIrc.Utils.parse data
assert "irc.example.org" = message.server
"""
@spec parse(raw_data :: char_list) :: IrcMessage.t
def parse(raw_data) do
data = :string.substr(raw_data, 1, length(raw_data))
case data do
[?:|_] ->
[[?:|from]|rest] = :string.tokens(data, ' ')
get_cmd(rest, parse_from(from, %IrcMessage{ctcp: false}))
data ->
get_cmd(:string.tokens(data, ' '), %IrcMessage{ctcp: false})
end
end
- @prefix_pattern ~r/^(?<nick>[^!]+)(?:!(?:(?<user>[^@ ]+)@)?(?:(?<host>[\w.:-]+)))?$/
+ @prefix_pattern ~r/^(?<nick>[^!\s]+)(?:!(?:(?<user>[^@\s]+)@)?(?:(?<host>[\S]+)))?$/
defp parse_from(from, msg) do
from_str = IO.iodata_to_binary(from)
parts = Regex.run(@prefix_pattern, from_str, capture: :all_but_first)
case parts do
[nick, user, host] ->
%{msg | nick: nick, user: user, host: host}
[nick, host] ->
%{msg | nick: nick, host: host}
[nick] ->
if String.contains?(nick, ".") do
%{msg | server: nick}
else
%{msg | nick: nick}
end
end
end
# Parse command from message
defp get_cmd([cmd, arg1, [?:, 1 | ctcp_trail] | restargs], msg) when cmd == 'PRIVMSG' or cmd == 'NOTICE' do
get_cmd([cmd, arg1, [1 | ctcp_trail] | restargs], msg)
end
defp get_cmd([cmd, target, [1 | ctcp_cmd] | cmd_args], msg) when cmd == 'PRIVMSG' or cmd == 'NOTICE' do
args = cmd_args
|> Enum.map(&Enum.take_while(&1, fn c -> c != 0o001 end))
|> Enum.map(&List.to_string/1)
case args do
args when args != [] ->
%{msg |
cmd: to_string(ctcp_cmd),
args: [to_string(target), args |> Enum.join(" ")],
ctcp: true
}
_ ->
%{msg | cmd: to_string(cmd), ctcp: :invalid}
end
end
defp get_cmd([cmd | rest], msg) do
get_args(rest, %{msg | cmd: to_string(cmd)})
end
# Parse command args from message
defp get_args([], msg) do
args = msg.args
|> Enum.reverse
|> Enum.filter(fn arg -> arg != [] end)
|> Enum.map(&trim_crlf/1)
|> Enum.map(&:binary.list_to_bin/1)
|> Enum.map(&:unicode.characters_to_binary/1)
post_process(%{msg | args: args})
end
defp get_args([[?: | first_arg] | rest], msg) do
args = (for arg <- [first_arg | rest], do: ' ' ++ trim_crlf(arg)) |> List.flatten
case args do
[_] ->
get_args([], %{msg | args: msg.args})
[_ | full_trail] ->
get_args([], %{msg | args: [full_trail | msg.args]})
end
end
defp get_args([arg | rest], msg) do
get_args(rest, %{msg | args: [arg | msg.args]})
end
# This function allows us to handle special case messages which are not RFC
# compliant, before passing it to the client.
defp post_process(%IrcMessage{cmd: "332", args: [nick, channel]} = msg) do
# Handle malformed RPL_TOPIC messages which contain no topic
%{msg | :cmd => "331", :args => [channel, "No topic is set"], :nick => nick}
end
defp post_process(msg), do: msg
############################
# Parse RPL_ISUPPORT (005)
############################
@doc """
Parse RPL_ISUPPORT message.
If an empty list is provided, do nothing, otherwise parse CHANTYPES,
NETWORK, and PREFIX parameters for relevant data.
"""
@spec isup(parameters :: list(binary), state :: ExIrc.Client.ClientState.t) :: ExIrc.Client.ClientState.t
def isup([], state), do: state
def isup([param | rest], state) do
try do
isup(rest, isup_param(param, state))
rescue
_ -> isup(rest, state)
end
end
defp isup_param("CHANTYPES=" <> channel_prefixes, state) do
prefixes = channel_prefixes |> String.split("", trim: true)
%{state | channel_prefixes: prefixes}
end
defp isup_param("NETWORK=" <> network, state) do
%{state | network: network}
end
defp isup_param("PREFIX=" <> user_prefixes, state) do
prefixes = Regex.run(~r/\((.*)\)(.*)/, user_prefixes, capture: :all_but_first)
|> Enum.map(&String.to_char_list/1)
|> List.zip
%{state | user_prefixes: prefixes}
end
defp isup_param(_, state) do
state
end
###################
# Helper Functions
###################
@days_of_week ['Mon','Tue','Wed','Thu','Fri','Sat','Sun']
@months_of_year ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']
@doc """
Get CTCP formatted time from a tuple representing the current calendar time:
Example:
iex> local_time = {{2013,12,6},{14,5,0}}
{{2013,12,6},{14,5,0}}
iex> ExIrc.Utils.ctcp_time local_time
"Fri Dec 06 14:05:00 2013"
"""
@spec ctcp_time(datetime :: {{integer, integer, integer}, {integer, integer, integer}}) :: binary
def ctcp_time({{y, m, d}, {h, n, s}} = _datetime) do
[:lists.nth(:calendar.day_of_the_week(y,m,d), @days_of_week),
' ',
:lists.nth(m, @months_of_year),
' ',
:io_lib.format("~2..0s", [Integer.to_char_list(d)]),
' ',
:io_lib.format("~2..0s", [Integer.to_char_list(h)]),
':',
:io_lib.format("~2..0s", [Integer.to_char_list(n)]),
':',
:io_lib.format("~2..0s", [Integer.to_char_list(s)]),
' ',
Integer.to_char_list(y)] |> List.flatten |> List.to_string
end
defp trim_crlf(charlist) do
case Enum.reverse(charlist) do
[?\n, ?\r | text] -> Enum.reverse(text)
_ -> charlist
end
end
end
diff --git a/test/utils_test.exs b/test/utils_test.exs
index 33f6907..dce38c0 100644
--- a/test/utils_test.exs
+++ b/test/utils_test.exs
@@ -1,164 +1,192 @@
defmodule ExIrc.UtilsTest do
use ExUnit.Case, async: true
use Irc.Commands
alias ExIrc.Utils, as: Utils
alias ExIrc.Client.ClientState, as: ClientState
doctest ExIrc.Utils
test "Given a local date/time as a tuple, can retrieve get the CTCP formatted time" do
local_time = {{2013,12,6},{14,5,0}} # Mimics output of :calendar.local_time()
assert Utils.ctcp_time(local_time) == "Fri Dec 06 14:05:00 2013"
end
test "Can parse a CTCP command" do
message = ':pschoenf NOTICE #testchan :' ++ '#{<<0o001>>}' ++ 'ACTION mind explodes!!' ++ '#{<<0o001>>}'
expected = %IrcMessage{
nick: "pschoenf",
cmd: "ACTION",
ctcp: true,
args: ["#testchan", "mind explodes!!"]
}
result = Utils.parse(message)
assert expected == result
end
+ test "Parse cloaked user" do
+ message = ':foo!foo@unaffiliated/foo PRIVMSG #bar Hiya.'
+ expected = %IrcMessage{
+ nick: "foo",
+ cmd: "PRIVMSG",
+ host: "unaffiliated/foo",
+ ctcp: false,
+ user: "foo",
+ args: ["#bar", "Hiya."]
+ }
+ result = Utils.parse(message)
+ assert expected == result
+ end
+
+ test "Parse uncloaked (normal) user" do
+ message = ':foo!foo@80.21.56.43 PRIVMSG #bar Hiya.'
+ expected = %IrcMessage{
+ nick: "foo",
+ cmd: "PRIVMSG",
+ host: "80.21.56.43",
+ ctcp: false,
+ user: "foo",
+ args: ["#bar", "Hiya."]
+ }
+ result = Utils.parse(message)
+ assert expected == result
+ end
+
test "Parse INVITE message" do
message = ':pschoenf INVITE testuser #awesomechan'
assert %IrcMessage{
:nick => "pschoenf",
:cmd => "INVITE",
:args => ["testuser", "#awesomechan"]
} = Utils.parse(message)
end
test "Parse KICK message" do
message = ':pschoenf KICK #testchan lameuser'
assert %IrcMessage{
:nick => "pschoenf",
:cmd => "KICK",
:args => ["#testchan", "lameuser"]
} = Utils.parse(message)
end
test "Can parse RPL_ISUPPORT commands" do
message = ':irc.example.org 005 nick NETWORK=Freenode PREFIX=(ov)@+ CHANTYPES=#&'
parsed = Utils.parse(message)
state = %ClientState{}
assert %ClientState{
:channel_prefixes => ["#", "&"],
:user_prefixes => [{?o, ?@}, {?v, ?+}],
:network => "Freenode"
} = Utils.isup(parsed.args, state)
end
test "Can parse full prefix in messages" do
assert %IrcMessage{
nick: "WiZ",
user: "jto",
host: "tolsun.oulu.fi",
} = Utils.parse(':WiZ!jto@tolsun.oulu.fi NICK Kilroy')
end
test "Can parse prefix with only hostname in messages" do
assert %IrcMessage{
nick: "WiZ",
host: "tolsun.oulu.fi",
} = Utils.parse(':WiZ!tolsun.oulu.fi NICK Kilroy')
end
test "Can parse reduced prefix in messages" do
assert %IrcMessage{
nick: "Trillian",
} = Utils.parse(':Trillian SQUIT cm22.eng.umd.edu :Server out of control')
end
test "Can parse server-only prefix in messages" do
assert %IrcMessage{
server: "ircd.stealth.net"
} = Utils.parse(':ircd.stealth.net 302 yournick :syrk=+syrk@millennium.stealth.net')
end
test "Can parse FULL STOP in username in prefixes" do
assert %IrcMessage{
nick: "nick",
user: "user.name",
host: "irc.example.org"
} = Utils.parse(':nick!user.name@irc.example.org PART #channel')
end
test "Can parse EXCLAMATION MARK in username in prefixes" do
assert %IrcMessage{
nick: "nick",
user: "user!name",
host: "irc.example.org"
} = Utils.parse(':nick!user!name@irc.example.org PART #channel')
end
test "parse join message" do
message = ':pschoenf JOIN #elixir-lang'
assert %IrcMessage{
nick: "pschoenf",
cmd: "JOIN",
args: ["#elixir-lang"]
} = Utils.parse(message)
end
test "Parse Slack's inappropriate RPL_TOPIC message as if it were an RPL_NOTOPIC" do
# NOTE: This is not a valid message per the RFC. If there's no topic
# (which is the case for Slack in this instance), they should instead send
# us a RPL_NOTOPIC (331).
#
# Two things:
#
# 1) Bad slack! Read your RFCs! (because my code has never had bugs yup obv)
# 2) Don't care, still want to talk to them without falling over dead!
#
# Parsing this as if it were actually an RPL_NOTOPIC (331) seems especially like
# a good idea when I realized that there's nothing in ExIRc that does anything
# with 331 at all - they just fall on the floor, no crashes to be seen (ideally)
message = ':irc.tinyspeck.com 332 jadams #elm-playground-news :'
assert %IrcMessage{
nick: "jadams",
cmd: "331",
args: ["#elm-playground-news", "No topic is set"]
} = Utils.parse(message)
end
test "Can parse simple unicode" do
# ':foo!~user@172.17.0.1 PRIVMSG #bar :éáçíóö\r\n'
message = [58, 102, 111, 111, 33, 126, 117, 115, 101, 114, 64, 49, 55, 50,
46, 49, 55, 46, 48, 46, 49, 32, 80, 82, 73, 86, 77, 83, 71, 32,
35, 98, 97, 114, 32, 58, 195, 169, 195, 161, 195, 167, 195, 173,
195, 179, 195, 182, 13, 10]
assert %IrcMessage{
args: ["#bar", "éáçíóö"],
cmd: "PRIVMSG",
ctcp: false,
host: "172.17.0.1",
nick: "foo",
server: [],
user: "~user"
} = Utils.parse(message)
end
test "Can parse complex unicode" do
# ':foo!~user@172.17.0.1 PRIVMSG #bar :Ĥélłø 차\r\n'
message = [58, 102, 111, 111, 33, 126, 117, 115, 101, 114, 64, 49, 55, 50,
46, 49, 55, 46, 48, 46, 49, 32, 80, 82, 73, 86, 77, 83, 71, 32,
35, 98, 97, 114, 32, 58, 196, 164, 195, 169, 108, 197, 130, 195,
184, 32, 236, 176, 168, 13, 10]
assert %IrcMessage{
args: ["#bar", "Ĥélłø 차"],
cmd: "PRIVMSG",
ctcp: false,
host: "172.17.0.1",
nick: "foo",
server: [],
user: "~user"
} = Utils.parse(message)
end
end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Mar 14, 5:55 PM (1 d, 9 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
33660
Default Alt Text
(12 KB)
Attached To
rEXIRC ExIRC Fork
Event Timeline
Log In to Comment