Page MenuHomePhabricator

No OneTemporary

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

Mime Type
text/x-diff
Expires
Fri, Mar 14, 5:55 PM (1 d, 14 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
33660
Default Alt Text
(12 KB)

Event Timeline