diff --git a/lib/exirc/channels.ex b/lib/exirc/channels.ex index 19b98b9..c74a987 100644 --- a/lib/exirc/channels.ex +++ b/lib/exirc/channels.ex @@ -1,167 +1,167 @@ defmodule ExIrc.Channels do @moduledoc """ Responsible for managing channel state """ use Irc.Commands import String, only: [downcase: 1] defrecord Channel, name: '', topic: '', users: [], modes: '', type: '' def init() do :gb_trees.empty() end ################## # Self JOIN/PART ################## def join(channel_tree, channel_name) do name = downcase(channel_name) case :gb_trees.lookup(name, channel_tree) do {:value, _} -> channel_tree :none -> :gb_trees.insert(name, Channel.new(name: name), channel_tree) end end def part(channel_tree, channel_name) do name = downcase(channel_name) case :gb_trees.lookup(name, channel_tree) do {:value, _} -> :gb_trees.delete(name, channel_tree) :none -> channel_tree end end ########################### # Channel Modes/Attributes ########################### def set_topic(channel_tree, channel_name, topic) do name = downcase(channel_name) case :gb_trees.lookup(name, channel_tree) do {:value, channel} -> :gb_trees.enter(name, channel.topic(topic), channel_tree) :none -> channel_tree end end def set_type(channel_tree, channel_name, channel_type) when is_binary(channel_type) do set_type(channel_tree, channel_name, String.to_char_list!(channel_type)) end def set_type(channel_tree, channel_name, channel_type) do name = downcase(channel_name) case :gb_trees.lookup(name, channel_tree) do {:value, channel} -> type = case channel_type do '@' -> :secret '*' -> :private '=' -> :public end :gb_trees.enter(name, channel.type(type), channel_tree) :none -> channel_tree end end #################################### # Users JOIN/PART/AKAs(namechange) #################################### def user_join(channel_tree, channel_name, nick) when not is_list(nick) do users_join(channel_tree, channel_name, [nick]) end def users_join(channel_tree, channel_name, nicks) do pnicks = strip_rank(nicks) manipfn = fn(channel_nicks) -> :lists.usort(channel_nicks ++ pnicks) end users_manip(channel_tree, channel_name, manipfn) end def user_part(channel_tree, channel_name, nick) do pnick = strip_rank([nick]) manipfn = fn(channel_nicks) -> :lists.usort(channel_nicks -- pnick) end users_manip(channel_tree, channel_name, manipfn) end def user_rename(channel_tree, nick, new_nick) do manipfn = fn(channel_nicks) -> case Enum.member?(channel_nicks, nick) do true -> [new_nick | channel_nicks -- [nick]] |> Enum.uniq |> Enum.sort false -> channel_nicks end end foldl = fn(channel_name, new_channel_tree) -> name = downcase(channel_name) users_manip(new_channel_tree, name, manipfn) end :lists.foldl(foldl, channel_tree, channels(channel_tree)) end ################ # Introspection ################ def channels(channel_tree) do (lc {channel_name, _chan} inlist :gb_trees.to_list(channel_tree), do: channel_name) |> Enum.reverse end - def chan_users(channel_tree, channel_name) do + def channel_users(channel_tree, channel_name) do get_attr(channel_tree, channel_name, fn(Channel[users: users]) -> users end) |> Enum.reverse end - def chan_topic(channel_tree, channel_name) do + def channel_topic(channel_tree, channel_name) do get_attr(channel_tree, channel_name, fn(Channel[topic: topic]) -> topic end) end - def chan_type(channel_tree, channel_name) do + def channel_type(channel_tree, channel_name) do get_attr(channel_tree, channel_name, fn(Channel[type: type]) -> type end) end - def chan_has_user?(channel_tree, channel_name, nick) do + def channel_has_user?(channel_tree, channel_name, nick) do get_attr(channel_tree, channel_name, fn(Channel[users: users]) -> :lists.member(nick, users) end) end def to_proplist(channel_tree) do (lc {channel_name, chan} inlist :gb_trees.to_list(channel_tree), do: { channel_name, [users: chan.users, topic: chan.topic, type: chan.type] }) |> Enum.reverse end #################### # Internal API #################### defp users_manip(channel_tree, channel_name, manipfn) do name = downcase(channel_name) case :gb_trees.lookup(name, channel_tree) do {:value, channel} -> channel_list = manipfn.(channel.users) :gb_trees.enter(channel_name, channel.users(channel_list), channel_tree) :none -> channel_tree end end defp strip_rank(nicks) do nicks |> Enum.map(fn(n) -> case n do [?@ | nick] -> nick [?+ | nick] -> nick nick -> nick end end) end defp get_attr(channel_tree, channel_name, getfn) do name = downcase(channel_name) case :gb_trees.lookup(name, channel_tree) do {:value, channel} -> getfn.(channel) :none -> {:error, :no_such_channel} end end end \ No newline at end of file diff --git a/lib/exirc/utils.ex b/lib/exirc/utils.ex index 3a3eb11..7669a90 100644 --- a/lib/exirc/utils.ex +++ b/lib/exirc/utils.ex @@ -1,138 +1,157 @@ defmodule ExIrc.Utils do alias ExIrc.Client.IrcMessage, as: IrcMessage + import String, only: [from_char_list!: 1] + ###################### # IRC Message Parsing ###################### @doc """ Parse an IRC message """ def parse(raw_data) do - [[?: | from] | rest] = :string.tokens(raw_data, ' ') - get_cmd rest, parse_from(from, IrcMessage.new(ctcp: false)) + 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 defp parse_from(from, msg) do case Regex.split(%r/(!|@|\.)/, from) do [nick, '!', user, '@', host | host_rest] -> - msg.nick(nick).user(user).host(host ++ host_rest) + msg.nick(from_char_list!(nick)).user(from_char_list!(user)).host(from_char_list!(host ++ host_rest)) [nick, '@', host | host_rest] -> - msg.nick(nick).host(host ++ host_rest) + msg.nick(from_char_list!(nick)).host(from_char_list!(host ++ host_rest)) [_, '.' | _] -> # from is probably a server name - msg.server(from) + msg.server(from_char_list!(from)) [nick] -> - msg.nick(nick) + msg.nick(from_char_list!(nick)) 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, _arg1, [1 | ctcp_trail] | restargs], msg) when cmd == 'PRIVMSG' or cmd == 'NOTICE' do args = ctcp_trail ++ lc arg inlist restargs, do: ' ' ++ arg |> Enum.flatten |> Enum.reverse case args do [1 | ctcp_rev] -> [ctcp_cmd | args] = ctcp_rev |> Enum.reverse |> :string.tokens(' ') - msg.cmd(ctcp_cmd).args(args).ctcp(true) + msg.cmd(from_char_list!(ctcp_cmd)).args(args).ctcp(true) _ -> - msg.cmd(cmd).ctcp(:invalid) + msg.cmd(from_char_list!(cmd)).ctcp(:invalid) end end defp get_cmd([cmd | rest], msg) do - get_args(rest, msg.cmd(cmd)) + get_args(rest, msg.cmd(from_char_list!(cmd))) end # Parse command args from message defp get_args([], msg) do msg.args |> Enum.reverse |> Enum.filter(fn(arg) -> arg != [] end) + |> Enum.map(&String.from_char_list!/1) |> msg.args end - defp get_args([[':' | first_arg] | rest], msg) do - args = lc arg inlist [first_arg | rest], do: ' ' ++ arg |> Enum.flatten + defp get_args([[?: | first_arg] | rest], msg) do + args = (lc arg inlist [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 | []], msg) do get_args([], msg.args([arg | msg.args])) end defp get_args([arg | rest], msg) do get_args(rest, msg.args([arg | msg.args])) end ############################ # Parse RPL_ISUPPORT (005) ############################ 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 - state.channel_prefixes(channel_prefixes) + 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 + 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) |> List.zip + 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,00}} {{2013,12,6},{14,5,00}} iex> ExIrc.Utils.ctcp_time local_time - 'Fri Dec 06 14:05:00 2013' + "Fri Dec 06 14:05:00 2013" """ def ctcp_time({{y, m, d}, {h, n, s}}) 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_list(d)]), ' ', :io_lib.format("~2..0s", [integer_to_list(h)]), ':', :io_lib.format("~2..0s", [integer_to_list(n)]), ':', :io_lib.format("~2..0s", [integer_to_list(s)]), ' ', - integer_to_list(y)] |> List.flatten + integer_to_list(y)] |> List.flatten |> String.from_char_list! + end + + def trim_crlf(charlist) do + case Enum.reverse(charlist) do + [?\n, ?\r | text] -> Enum.reverse(text) + _ -> charlist + end end end \ No newline at end of file diff --git a/test/channels_test.exs b/test/channels_test.exs index 927a76a..1939ea6 100644 --- a/test/channels_test.exs +++ b/test/channels_test.exs @@ -1,130 +1,130 @@ defmodule ExIrc.ChannelsTest do use ExUnit.Case, async: true alias ExIrc.Channels, as: Channels test "Joining a channel adds it to the tree of currently joined channels" do channels = Channels.init() |> Channels.join("#testchannel") |> Channels.channels assert Enum.member?(channels, "#testchannel") end test "The channel name is downcased when joining" do channels = Channels.init() |> Channels.join("#TestChannel") |> Channels.channels assert Enum.member?(channels, "#testchannel") end test "Joining the same channel twice is a noop" do channels = Channels.init() |> Channels.join("#TestChannel") |> Channels.join("#testchannel") |> Channels.channels assert 1 == Enum.count(channels) end test "Parting a channel removes it from the tree of currently joined channels" do tree = Channels.init() |> Channels.join("#testchannel") assert Enum.member?(Channels.channels(tree), "#testchannel") tree = Channels.part(tree, "#testchannel") refute Enum.member?(Channels.channels(tree), "#testchannel") end test "Parting a channel not in the tree is a noop" do tree = Channels.init() {count, _} = Channels.part(tree, "#testchannel") assert 0 == count end test "Can set the topic for a channel" do channels = Channels.init() |> Channels.join("#testchannel") |> Channels.set_topic("#testchannel", "Welcome to Test Channel!") - assert "Welcome to Test Channel!" == Channels.chan_topic(channels, "#testchannel") + assert "Welcome to Test Channel!" == Channels.channel_topic(channels, "#testchannel") end test "Setting the topic for a channel we haven't joined returns :error" do channels = Channels.init() |> Channels.set_topic("#testchannel", "Welcome to Test Channel!") - assert {:error, :no_such_channel} == Channels.chan_topic(channels, "#testchannel") + assert {:error, :no_such_channel} == Channels.channel_topic(channels, "#testchannel") end test "Can set the channel type" do channels = Channels.init() |> Channels.join("#testchannel") |> Channels.set_type("#testchannel", "@") - assert :secret == Channels.chan_type(channels, "#testchannel") + assert :secret == Channels.channel_type(channels, "#testchannel") channels = Channels.set_type(channels, "#testchannel", "*") - assert :private == Channels.chan_type(channels, "#testchannel") + assert :private == Channels.channel_type(channels, "#testchannel") channels = Channels.set_type(channels, "#testchannel", "=") - assert :public == Channels.chan_type(channels, "#testchannel") + assert :public == Channels.channel_type(channels, "#testchannel") end test "Setting the channel type for a channel we haven't joined returns :error" do channels = Channels.init() |> Channels.set_type("#testchannel", "@") - assert {:error, :no_such_channel} == Channels.chan_type(channels, "#testchannel") + assert {:error, :no_such_channel} == Channels.channel_type(channels, "#testchannel") end test "Setting an invalid channel type raises CaseClauseError" do assert_raise CaseClauseError, "no case clause matching: '!'", fn -> Channels.init() |> Channels.join("#testchannel") |> Channels.set_type("#testchannel", "!") end end test "Can join a user to a channel" do channels = Channels.init() |> Channels.join("#testchannel") |> Channels.user_join("#testchannel", "testnick") - assert Channels.chan_has_user?(channels, "#testchannel", "testnick") + assert Channels.channel_has_user?(channels, "#testchannel", "testnick") end test "Can join multiple users to a channel" do channels = Channels.init() |> Channels.join("#testchannel") |> Channels.users_join("#testchannel", ["testnick", "anothernick"]) - assert Channels.chan_has_user?(channels, "#testchannel", "testnick") - assert Channels.chan_has_user?(channels, "#testchannel", "anothernick") + assert Channels.channel_has_user?(channels, "#testchannel", "testnick") + assert Channels.channel_has_user?(channels, "#testchannel", "anothernick") end test "Joining a users to a channel we aren't in is a noop" do channels = Channels.init() |> Channels.user_join("#testchannel", "testnick") - assert {:error, :no_such_channel} == Channels.chan_has_user?(channels, "#testchannel", "testnick") + assert {:error, :no_such_channel} == Channels.channel_has_user?(channels, "#testchannel", "testnick") channels = Channels.init() |> Channels.users_join("#testchannel", ["testnick", "anothernick"]) - assert {:error, :no_such_channel} == Channels.chan_has_user?(channels, "#testchannel", "testnick") + assert {:error, :no_such_channel} == Channels.channel_has_user?(channels, "#testchannel", "testnick") end test "Can part a user from a channel" do channels = Channels.init() |> Channels.join("#testchannel") |> Channels.user_join("#testchannel", "testnick") - assert Channels.chan_has_user?(channels, "#testchannel", "testnick") + assert Channels.channel_has_user?(channels, "#testchannel", "testnick") channels = channels |> Channels.user_part("#testchannel", "testnick") - refute Channels.chan_has_user?(channels, "#testchannel", "testnick") + refute Channels.channel_has_user?(channels, "#testchannel", "testnick") end test "Parting a user from a channel we aren't in is a noop" do channels = Channels.init() |> Channels.user_part("#testchannel", "testnick") - assert {:error, :no_such_channel} == Channels.chan_has_user?(channels, "#testchannel", "testnick") + assert {:error, :no_such_channel} == Channels.channel_has_user?(channels, "#testchannel", "testnick") end test "Can rename a user" do channels = Channels.init() |> Channels.join("#testchannel") |> Channels.join("#anotherchan") |> Channels.user_join("#testchannel", "testnick") |> Channels.user_join("#anotherchan", "testnick") - assert Channels.chan_has_user?(channels, "#testchannel", "testnick") - assert Channels.chan_has_user?(channels, "#anotherchan", "testnick") + assert Channels.channel_has_user?(channels, "#testchannel", "testnick") + assert Channels.channel_has_user?(channels, "#anotherchan", "testnick") channels = Channels.user_rename(channels, "testnick", "newnick") - refute Channels.chan_has_user?(channels, "#testchannel", "testnick") - refute Channels.chan_has_user?(channels, "#anotherchan", "testnick") - assert Channels.chan_has_user?(channels, "#testchannel", "newnick") - assert Channels.chan_has_user?(channels, "#anotherchan", "newnick") + refute Channels.channel_has_user?(channels, "#testchannel", "testnick") + refute Channels.channel_has_user?(channels, "#anotherchan", "testnick") + assert Channels.channel_has_user?(channels, "#testchannel", "newnick") + assert Channels.channel_has_user?(channels, "#anotherchan", "newnick") end test "Renaming a user that doesn't exist is a noop" do channels = Channels.init() |> Channels.join("#testchannel") |> Channels.user_rename("testnick", "newnick") - refute Channels.chan_has_user?(channels, "#testchannel", "testnick") - refute Channels.chan_has_user?(channels, "#testchannel", "newnick") + refute Channels.channel_has_user?(channels, "#testchannel", "testnick") + refute Channels.channel_has_user?(channels, "#testchannel", "newnick") end test "Can get the current set of channel data as a tuple of the channel name and it's data as a proplist" do channels = Channels.init() |> Channels.join("#testchannel") |> Channels.set_type("#testchannel", "@") |> Channels.set_topic("#testchannel", "Welcome to Test!") |> Channels.join("#anotherchan") |> Channels.set_type("#anotherchan", "=") |> Channels.set_topic("#anotherchan", "Welcome to Another Channel!") |> Channels.user_join("#testchannel", "testnick") |> Channels.user_join("#anotherchan", "testnick") |> Channels.to_proplist testchannel = {"#testchannel", [users: ["testnick"], topic: "Welcome to Test!", type: :secret]} anotherchan = {"#anotherchan", [users: ["testnick"], topic: "Welcome to Another Channel!", type: :public]} assert [testchannel, anotherchan] == channels end end \ No newline at end of file diff --git a/test/utils_test.exs b/test/utils_test.exs index 9add674..eb83e6f 100644 --- a/test/utils_test.exs +++ b/test/utils_test.exs @@ -1,37 +1,37 @@ defmodule ExIrc.UtilsTest do use ExUnit.Case, async: true use Irc.Commands alias ExIrc.Utils, as: Utils alias ExIrc.Client.IrcMessage, as: IrcMessage 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,00}} # Mimics output of :calendar.local_time() - assert Utils.ctcp_time(local_time) == 'Fri Dec 06 14:05:00 2013' + assert Utils.ctcp_time(local_time) == "Fri Dec 06 14:05:00 2013" end test "Can parse an IRC message" do message = ':irc.example.org 005 nick NETWORK=Freenode PREFIX=(ov)@+ CHANTYPES=#&' assert IrcMessage[ - server: 'irc.example.org', + server: "irc.example.org", cmd: @rpl_isupport, - args: ['nick', 'NETWORK=Freenode', 'PREFIX=(ov)@+', 'CHANTYPES=#&'] + args: ["nick", "NETWORK=Freenode", "PREFIX=(ov)@+", "CHANTYPES=#&"] ] = 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.new() assert ClientState[ - channel_prefixes: [?#, ?&], + channel_prefixes: ["#", "&"], user_prefixes: [{?o, ?@}, {?v, ?+}], - network: 'Freenode' + network: "Freenode" ] = Utils.isup(parsed.args, state) end end