diff options
-rw-r--r-- | lib/exirc/client.ex | 22 | ||||
-rw-r--r-- | lib/exirc/commands.ex | 45 | ||||
-rw-r--r-- | lib/exirc/example_handler.ex | 4 | ||||
-rw-r--r-- | lib/exirc/utils.ex | 17 | ||||
-rw-r--r-- | test/commands_test.exs | 59 | ||||
-rw-r--r-- | test/utils_test.exs | 17 |
6 files changed, 111 insertions, 53 deletions
diff --git a/lib/exirc/client.ex b/lib/exirc/client.ex index 0cd92fd..d569486 100644 --- a/lib/exirc/client.ex +++ b/lib/exirc/client.ex @@ -104,6 +104,13 @@ defmodule ExIrc.Client do :gen_server.call(client, {:msg, type, nick, msg}, :infinity) end @doc """ + Send an action message, i.e. (/me slaps someone with a big trout) + """ + @spec me(client :: pid, channel :: binary, msg :: binary) :: :ok | {:error, atom} + def me(client, channel, msg) do + :gen_server.call(client, {:me, channel, msg}, :infinity) + end + @doc """ Change the client's nick """ @spec nick(client :: pid, new_nick :: binary) :: :ok | {:error, atom} @@ -330,6 +337,12 @@ defmodule ExIrc.Client do send! state.socket, data {:reply, :ok, state} end + # Handle /me messages + def handle_call({:me, channel, msg}, _from, state) do + data = me!(channel, msg) + send! state.socket, data + {:reply, :ok, state} + end # Handles call to join a channel def handle_call({:join, channel, key}, _from, state) do send!(state.socket, join!(channel, key)); {:reply, :ok, state} end # Handles a call to leave a channel @@ -418,10 +431,9 @@ defmodule ExIrc.Client do debug? = state.debug? case Utils.parse(data) do %IrcMessage{:ctcp => true} = msg -> - send_event msg, state + handle_data msg, state {:noreply, state} %IrcMessage{:ctcp => false} = msg -> - send_event msg, state handle_data msg, state %IrcMessage{:ctcp => :invalid} = msg when debug? -> send_event msg, state @@ -610,6 +622,12 @@ defmodule ExIrc.Client do end {:noreply, state} end + # Called when someone uses ACTION, i.e. `/me dies` + def handle_data(%IrcMessage{:nick => from, :cmd => "ACTION", :args => [channel, message]} = _msg, state) do + if state.debug?, do: debug "* #{from} #{message} in #{channel}" + send_event {:me, message, from, channel}, state + {:noreply, state} + end # Called any time we receive an unrecognized message def handle_data(msg, state) do if state.debug? do debug "UNRECOGNIZED MSG: #{msg.cmd}"; IO.inspect(msg) end diff --git a/lib/exirc/commands.ex b/lib/exirc/commands.ex index 0d3862e..6f2d385 100644 --- a/lib/exirc/commands.ex +++ b/lib/exirc/commands.ex @@ -165,6 +165,7 @@ defmodule Irc.Commands do ############ # Helpers ############ + @ctcp_delimiter <<0x01>> @doc """ Send data to a TCP socket. @@ -181,64 +182,72 @@ defmodule Irc.Commands do @doc """ Builds a valid IRC command. """ - def command!(cmd) when is_list(cmd), do: [cmd, '\r\n'] - def command!(cmd) when is_binary(cmd), do: command! String.to_char_list(cmd) + def command!(cmd), do: [cmd, '\r\n'] @doc """ Builds a valid CTCP command. """ - def ctcp!(cmd), do: [1, '#{cmd}', 1] + def ctcp!(cmd), do: command! [@ctcp_delimiter, cmd, @ctcp_delimiter] + def ctcp!(cmd, args) do + expanded = args |> Enum.intersperse(' ') + command! [@ctcp_delimiter, cmd, expanded, @ctcp_delimiter] + end # IRC Commands @doc """ Send password to server """ - def pass!(pwd), do: command! ['PASS ', '#{pwd}'] + def pass!(pwd), do: command! ['PASS ', pwd] @doc """ Send nick to server. (Changes or sets your nick) """ - def nick!(nick), do: command! ['NICK ', '#{nick}'] + def nick!(nick), do: command! ['NICK ', nick] @doc """ Send username to server. (Changes or sets your username) """ def user!(user, name) do - command! ['USER ', '#{user}', ' 0 * :', '#{name}'] + command! ['USER ', user, ' 0 * :', name] end @doc """ Send PONG in response to PING """ - def pong1!(nick), do: command! ['PONG ', '#{nick}'] + def pong1!(nick), do: command! ['PONG ', nick] @doc """ Send a targeted PONG in response to PING """ - def pong2!(nick, to), do: command! ['PONG ', '#{nick}', ' ', '#{to}'] + def pong2!(nick, to), do: command! ['PONG ', nick, ' ', to] @doc """ Send message to channel or user """ - def privmsg!(nick, msg), do: command! ['PRIVMSG ', '#{nick}', ' :', '#{msg}'] + def privmsg!(nick, msg), do: command! ['PRIVMSG ', nick, ' :', msg] + @doc """ + Send a `/me <msg>` CTCP command to t + """ + def me!(channel, msg), do: command! ['PRIVMSG ', channel, ' :', @ctcp_delimiter, 'ACTION ', msg, @ctcp_delimiter] @doc """ Send notice to channel or user """ - def notice!(nick, msg), do: command! ['NOTICE ', '#{nick}', ' :', '#{msg}'] + def notice!(nick, msg), do: command! ['NOTICE ', nick, ' :', msg] @doc """ Send join command to server (join a channel) """ - def join!(channel, key \\ ""), do: command! ['JOIN ', '#{channel}', ' ', '#{key}'] + def join!(channel), do: command! ['JOIN ', channel] + def join!(channel, key), do: command! ['JOIN ', channel, ' ', key] @doc """ Send part command to server (leave a channel) """ - def part!(channel), do: command! ['PART ', '#{channel}'] + def part!(channel), do: command! ['PART ', channel] @doc """ Send quit command to server (disconnect from server) """ - def quit!(msg \\ "Leaving"), do: command! ['QUIT :', '#{msg}'] + def quit!(msg \\ "Leaving"), do: command! ['QUIT :', msg] @doc """ Send kick command to server """ def kick!(channel, nick, message \\ "") do case "#{message}" |> String.length do - 0 -> command! ['KICK ', '#{channel}', ' ', '#{nick}'] - _ -> command! ['KICK ', '#{channel}', ' ', '#{nick}', ' ', '#{message}'] + 0 -> command! ['KICK ', channel, ' ', nick] + _ -> command! ['KICK ', channel, ' ', nick, ' ', message] end end @doc """ @@ -248,15 +257,15 @@ defmodule Irc.Commands do """ def mode!(channel_or_nick, flags, args \\ "") do case "#{args}" |> String.length do - 0 -> command! ['MODE ', '#{channel_or_nick}', ' ', '#{flags}'] - _ -> command! ['MODE ', '#{channel_or_nick}', ' ', '#{flags}', ' ', '#{args}'] + 0 -> command! ['MODE ', channel_or_nick, ' ', flags] + _ -> command! ['MODE ', channel_or_nick, ' ', flags, ' ', args] end end @doc """ Send an invite command """ def invite!(nick, channel) do - command! ['INVITE ', '#{nick}', ' ', '#{channel}'] + command! ['INVITE ', nick, ' ', channel] end end diff --git a/lib/exirc/example_handler.ex b/lib/exirc/example_handler.ex index bd3f142..4bc8e8c 100644 --- a/lib/exirc/example_handler.ex +++ b/lib/exirc/example_handler.ex @@ -99,6 +99,10 @@ defmodule ExampleHandler do debug "#{from} mentioned us in #{channel}: #{message}" {:noreply, nil} end + def handle_info({:me, message, from, channel}, _state) do + debug "* #{from} #{message} in #{channel}" + {:noreply, nil} + end # This is an example of how you can manually catch commands if ExIrc.Client doesn't send a specific message for it def handle_info(%IrcMessage{:nick => from, :cmd => "PRIVMSG", :args => ["testnick", msg]}, _state) do debug "Received a private message from #{from}: #{msg}" diff --git a/lib/exirc/utils.ex b/lib/exirc/utils.ex index c46f648..3221956 100644 --- a/lib/exirc/utils.ex +++ b/lib/exirc/utils.ex @@ -44,14 +44,17 @@ defmodule ExIrc.Utils 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 ++ for arg <- restargs, do: ' ' ++ arg - |> Enum.flatten - |> Enum.reverse + 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 != ?\001 end)) + |> Enum.map(&List.to_string/1) case args do - [1 | ctcp_rev] -> - [ctcp_cmd | args] = ctcp_rev |> Enum.reverse |> :string.tokens(' ') - %{msg | :cmd => to_string(ctcp_cmd), :args => args, :ctcp => true} + 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 diff --git a/test/commands_test.exs b/test/commands_test.exs index 950e559..c84fce3 100644 --- a/test/commands_test.exs +++ b/test/commands_test.exs @@ -4,24 +4,45 @@ defmodule ExIrc.CommandsTest do use Irc.Commands test "Commands are formatted properly" do - assert [1, 'TESTCMD', 1] == ctcp! "TESTCMD" - assert [['PASS ', 'testpass'], '\r\n'] == pass! "testpass" - assert [['NICK ', 'testnick'], '\r\n'] == nick! "testnick" - assert [['USER ', 'testuser', ' 0 * :', 'Test User'], '\r\n'] == user! "testuser", "Test User" - assert [['PONG ', 'testnick'], '\r\n'] == pong1! "testnick" - assert [['PONG ', 'testnick', ' ', 'othernick'], '\r\n'] == pong2! "testnick", "othernick" - assert [['PRIVMSG ', 'testnick', ' :', 'Test message!'], '\r\n'] == privmsg! "testnick", "Test message!" - assert [['NOTICE ', 'testnick', ' :', 'Test notice!'], '\r\n'] == notice! "testnick", "Test notice!" - assert [['JOIN ', 'testchan', ' ', ''], '\r\n'] == join! "testchan" - assert [['JOIN ', 'testchan', ' ', 'chanpass'], '\r\n'] == join! "testchan", "chanpass" - assert [['PART ', 'testchan'], '\r\n'] == part! "testchan" - assert [['QUIT :', 'Leaving'], '\r\n'] == quit! - assert [['QUIT :', 'Goodbye, cruel world.'], '\r\n'] == quit! "Goodbye, cruel world." - assert [['KICK ', '#testchan', ' ', 'testuser'], '\r\n'] == kick! "#testchan", "testuser" - assert [['KICK ', '#testchan', ' ', 'testuser', ' ', 'Get outta here!'], '\r\n'] == kick! "#testchan", "testuser", "Get outta here!" - assert [['MODE ', 'testuser', ' ', '-o'], '\r\n'] == mode! "testuser", "-o" - assert [['MODE ', '#testchan', ' ', '+im'], '\r\n'] == mode! "#testchan", "+im" - assert [['MODE ', '#testchan', ' ', '+o', ' ', 'testuser'], '\r\n'] = mode! "#testchan", "+o", "testuser" - assert [['INVITE ', 'testuser', ' ', '#testchan'], '\r\n'] = invite! "testuser", "#testchan" + expected = <<1, "TESTCMD", 1, ?\r, ?\n>> + assert expected == ctcp!("TESTCMD") |> IO.iodata_to_binary + expected = <<"PRIVMSG #testchan :", 0x01, "ACTION mind explodes!!", 0x01, ?\r, ?\n>> + assert expected == me!("#testchan", "mind explodes!!") |> IO.iodata_to_binary + expected = <<"PASS testpass", ?\r, ?\n>> + assert expected == pass!("testpass") |> IO.iodata_to_binary + expected = <<"NICK testnick", ?\r, ?\n>> + assert expected == nick!("testnick") |> IO.iodata_to_binary + expected = <<"USER testuser 0 * :Test User", ?\r, ?\n>> + assert expected == user!("testuser", "Test User") |> IO.iodata_to_binary + expected = <<"PONG testnick", ?\r, ?\n>> + assert expected == pong1!("testnick") |> IO.iodata_to_binary + expected = <<"PONG testnick othernick", ?\r, ?\n>> + assert expected == pong2!("testnick", "othernick") |> IO.iodata_to_binary + expected = <<"PRIVMSG testnick :Test message!", ?\r, ?\n>> + assert expected == privmsg!("testnick", "Test message!") |> IO.iodata_to_binary + expected = <<"NOTICE testnick :Test notice!", ?\r, ?\n>> + assert expected == notice!("testnick", "Test notice!") |> IO.iodata_to_binary + expected = <<"JOIN testchan", ?\r, ?\n>> + assert expected == join!("testchan") |> IO.iodata_to_binary + expected = <<"JOIN testchan chanpass", ?\r, ?\n>> + assert expected == join!("testchan", "chanpass") |> IO.iodata_to_binary + expected = <<"PART testchan", ?\r, ?\n>> + assert expected == part!("testchan") |> IO.iodata_to_binary + expected = <<"QUIT :Leaving", ?\r, ?\n>> + assert expected == quit! |> IO.iodata_to_binary + expected = <<"QUIT :Goodbye, cruel world.", ?\r, ?\n>> + assert expected == quit!("Goodbye, cruel world.") |> IO.iodata_to_binary + expected = <<"KICK #testchan testuser", ?\r, ?\n>> + assert expected == kick!("#testchan", "testuser") |> IO.iodata_to_binary + expected = <<"KICK #testchan testuser Get outta here!", ?\r, ?\n>> + assert expected == kick!("#testchan", "testuser", "Get outta here!") |> IO.iodata_to_binary + expected = <<"MODE testuser -o", ?\r, ?\n>> + assert expected == mode!("testuser", "-o") |> IO.iodata_to_binary + expected = <<"MODE #testchan +im", ?\r, ?\n>> + assert expected == mode!("#testchan", "+im") |> IO.iodata_to_binary + expected = <<"MODE #testchan +o testuser", ?\r, ?\n>> + assert expected == mode!("#testchan", "+o", "testuser") |> IO.iodata_to_binary + expected = <<"INVITE testuser #testchan", ?\r, ?\n>> + assert expected == invite!("testuser", "#testchan") |> IO.iodata_to_binary end end
\ No newline at end of file diff --git a/test/utils_test.exs b/test/utils_test.exs index 34d4e2d..e079661 100644 --- a/test/utils_test.exs +++ b/test/utils_test.exs @@ -13,13 +13,16 @@ defmodule ExIrc.UtilsTest do 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", - :cmd => @rpl_isupport, - :args => ["nick", "NETWORK=Freenode", "PREFIX=(ov)@+", "CHANTYPES=#&"] - } = Utils.parse(message) + test "Can parse a CTCP command" do + message = ':pschoenf NOTICE #testchan :\001ACTION mind explodes!!\001' + expected = %IrcMessage{ + nick: "pschoenf", + cmd: "ACTION", + ctcp: true, + args: ["#testchan", "mind explodes!!"] + } + result = Utils.parse(message) + assert expected == result end test "Parse INVITE message" do |