diff --git a/README.md b/README.md index 0453184..2c33671 100644 --- a/README.md +++ b/README.md @@ -1,191 +1,191 @@ -# ExIrc +# ExIRC [![Build Status](https://travis-ci.org/bitwalker/exirc.svg?branch=master)](https://travis-ci.org/bitwalker/exirc) [![Hex.pm Version](http://img.shields.io/hexpm/v/exirc.svg?style=flat)](https://hex.pm/packages/exirc) -ExIrc is a IRC client library for Elixir projects. It aims to have a clear, well +ExIRC is a IRC client library for Elixir projects. It aims to have a clear, well documented API, with the minimal amount of code necessary to allow you to connect and communicate with IRC servers effectively. It aims to implement the full RFC2812 protocol, and relevant parts of RFC1459. ## Getting Started -Add ExIrc as a dependency to your project in mix.exs, and add it as an application: +Add ExIRC as a dependency to your project in mix.exs, and add it as an application: ```elixir defp deps do [{:exirc, "~> x.x.x"}] end defp application do [applications: [:exirc], ...] end ``` Then fetch it using `mix deps.get`. -To use ExIrc, you need to start a new client process, and add event handlers. An example event handler module +To use ExIRC, you need to start a new client process, and add event handlers. An example event handler module is located in `lib/exirc/example_handler.ex`. **The example handler is kept up to date with all events you can expect to receive from the client**. A simple module is defined below as an example of how you might -use ExIrc in practice. ExampleHandler here is the one that comes bundled with ExIrc. +use ExIRC in practice. ExampleHandler here is the one that comes bundled with ExIRC. There is also a variety of examples in `examples`, the most up to date of which is `examples/bot`. ```elixir defmodule ExampleSupervisor do defmodule State do defstruct host: "chat.freenode.net", port: 6667, pass: "", nick: "bitwalker", user: "bitwalker", name: "Paul Schoenfelder", client: nil, handlers: [] end def start_link(_) do :gen_server.start_link(__MODULE__, [%State{}]) end def init(state) do - # Start the client and handler processes, the ExIrc supervisor is automatically started when your app runs - {:ok, client} = ExIrc.start_link!() + # Start the client and handler processes, the ExIRC supervisor is automatically started when your app runs + {:ok, client} = ExIRC.start_link!() {:ok, handler} = ExampleHandler.start_link(nil) - # Register the event handler with ExIrc - ExIrc.Client.add_handler client, handler + # Register the event handler with ExIRC + ExIRC.Client.add_handler client, handler # Connect and logon to a server, join a channel and send a simple message - ExIrc.Client.connect! client, state.host, state.port - ExIrc.Client.logon client, state.pass, state.nick, state.user, state.name - ExIrc.Client.join client, "#elixir-lang" - ExIrc.Client.msg client, :privmsg, "#elixir-lang", "Hello world!" + ExIRC.Client.connect! client, state.host, state.port + ExIRC.Client.logon client, state.pass, state.nick, state.user, state.name + ExIRC.Client.join client, "#elixir-lang" + ExIRC.Client.msg client, :privmsg, "#elixir-lang", "Hello world!" {:ok, %{state | :client => client, :handlers => [handler]}} end def terminate(_, state) do # Quit the channel and close the underlying client connection when the process is terminating - ExIrc.Client.quit state.client, "Goodbye, cruel world." - ExIrc.Client.stop! state.client + ExIRC.Client.quit state.client, "Goodbye, cruel world." + ExIRC.Client.stop! state.client :ok end end ``` A more robust example usage will wait until connected before it attempts to logon and then wait until logged on until it attempts to join a channel. Please see the `examples` directory for more in-depth examples cases. ```elixir defmodule ExampleApplication do use Application # See http://elixir-lang.org/docs/stable/elixir/Application.html # for more information on OTP Applications def start(_type, _args) do import Supervisor.Spec, warn: false - {:ok, client} = ExIrc.start_link! + {:ok, client} = ExIRC.start_link! children = [ # Define workers and child supervisors to be supervised worker(ExampleConnectionHandler, [client]), # here's where we specify the channels to join: worker(ExampleLoginHandler, [client, ["#ohaibot-testing"]]) ] # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html # for other strategies and supported options opts = [strategy: :one_for_one, name: ExampleApplication.Supervisor] Supervisor.start_link(children, opts) end end defmodule ExampleConnectionHandler do defmodule State do defstruct host: "chat.freenode.net", port: 6667, pass: "", nick: "bitwalker", user: "bitwalker", name: "Paul Schoenfelder", client: nil end def start_link(client, state \\ %State{}) do GenServer.start_link(__MODULE__, [%{state | client: client}]) end def init([state]) do - ExIrc.Client.add_handler state.client, self - ExIrc.Client.connect! state.client, state.host, state.port + ExIRC.Client.add_handler state.client, self + ExIRC.Client.connect! state.client, state.host, state.port {:ok, state} end def handle_info({:connected, server, port}, state) do debug "Connected to #{server}:#{port}" - ExIrc.Client.logon state.client, state.pass, state.nick, state.user, state.name + ExIRC.Client.logon state.client, state.pass, state.nick, state.user, state.name {:noreply, state} end # Catch-all for messages you don't care about def handle_info(msg, state) do debug "Received unknown messsage:" IO.inspect msg {:noreply, state} end defp debug(msg) do IO.puts IO.ANSI.yellow() <> msg <> IO.ANSI.reset() end end defmodule ExampleLoginHandler do @moduledoc """ This is an example event handler that listens for login events and then joins the appropriate channels. We actually need this because we can't join channels until we've waited for login to complete. We could just attempt to sleep until login is complete, but that's just hacky. This as an event handler is a far more elegant solution. """ def start_link(client, channels) do GenServer.start_link(__MODULE__, [client, channels]) end def init([client, channels]) do - ExIrc.Client.add_handler client, self + ExIRC.Client.add_handler client, self {:ok, {client, channels}} end def handle_info(:logged_in, state = {client, channels}) do debug "Logged in to server" - channels |> Enum.map(&ExIrc.Client.join client, &1) + channels |> Enum.map(&ExIRC.Client.join client, &1) {:noreply, state} end # Catch-all for messages you don't care about def handle_info(_msg, state) do {:noreply, state} end defp debug(msg) do IO.puts IO.ANSI.yellow() <> msg <> IO.ANSI.reset() end end ``` -## Projects using ExIrc (in the wild!) +## Projects using ExIRC (in the wild!) Below is a list of projects that we know of (if we've missed anything, -send a PR!) that use ExIrc in the wild. +send a PR!) that use ExIRC in the wild. - [Kuma][kuma] by @ryanwinchester - [Offension][offension] by @shymega - [hedwig_irc][hedwig_irc] by @jeffweiss [kuma]: https://github.com/ryanwinchester/kuma [offension]: https://github.com/shymega/offension [hedwig_irc]: https://github.com/jeffweiss/hedwig_irc diff --git a/examples/bot/config/config.exs b/examples/bot/config/config.exs index e0d35fe..50f23ee 100644 --- a/examples/bot/config/config.exs +++ b/examples/bot/config/config.exs @@ -1,36 +1,36 @@ # This file is responsible for configuring your application # and its dependencies with the aid of the Mix.Config module. use Mix.Config config :exirc_example, bots: [ %{:server => "chat.freenode.net", :port => 6667, - :nick => "exirc-example", :user => "exirc-example", :name => "ExIrc Example Bot", + :nick => "exirc-example", :user => "exirc-example", :name => "ExIRC Example Bot", :channel => "##exirc-test"} ] # This configuration is loaded before any dependency and is restricted # to this project. If another project depends on this project, this # file won't be loaded nor affect the parent project. For this reason, # if you want to provide default values for your application for # 3rd-party users, it should be done in your "mix.exs" file. # You can configure for your application as: # # config :exirc_example, key: :value # # And access this configuration in your application as: # # Application.get_env(:exirc_example, :key) # # Or configure a 3rd-party app: # # config :logger, level: :info # # It is also possible to import configuration files, relative to this # directory. For example, you can emulate configuration per environment # by uncommenting the line below and defining dev.exs, test.exs and such. # Configuration from the imported file will override the ones defined # here (which is why it is important to import them last). # # import_config "#{Mix.env}.exs" diff --git a/examples/bot/lib/bot.ex b/examples/bot/lib/bot.ex index 12aab5d..453b9e8 100644 --- a/examples/bot/lib/bot.ex +++ b/examples/bot/lib/bot.ex @@ -1,108 +1,108 @@ defmodule Example.Bot do use GenServer require Logger defmodule Config do defstruct server: nil, port: nil, pass: nil, nick: nil, user: nil, name: nil, channel: nil, client: nil def from_params(params) when is_map(params) do Enum.reduce(params, %Config{}, fn {k, v}, acc -> case Map.has_key?(acc, k) do true -> Map.put(acc, k, v) false -> acc end end) end end - alias ExIrc.Client - alias ExIrc.SenderInfo + alias ExIRC.Client + alias ExIRC.SenderInfo def start_link(%{:nick => nick} = params) when is_map(params) do config = Config.from_params(params) GenServer.start_link(__MODULE__, [config], name: String.to_atom(nick)) end def init([config]) do - # Start the client and handler processes, the ExIrc supervisor is automatically started when your app runs - {:ok, client} = ExIrc.start_link!() + # Start the client and handler processes, the ExIRC supervisor is automatically started when your app runs + {:ok, client} = ExIRC.start_link!() - # Register the event handler with ExIrc + # Register the event handler with ExIRC Client.add_handler client, self() # Connect and logon to a server, join a channel and send a simple message Logger.debug "Connecting to #{config.server}:#{config.port}" Client.connect! client, config.server, config.port {:ok, %Config{config | :client => client}} end def handle_info({:connected, server, port}, config) do Logger.debug "Connected to #{server}:#{port}" Logger.debug "Logging to #{server}:#{port} as #{config.nick}.." Client.logon config.client, config.pass, config.nick, config.user, config.name {:noreply, config} end def handle_info(:logged_in, config) do Logger.debug "Logged in to #{config.server}:#{config.port}" Logger.debug "Joining #{config.channel}.." Client.join config.client, config.channel {:noreply, config} end def handle_info(:disconnected, config) do Logger.debug "Disconnected from #{config.server}:#{config.port}" {:stop, :normal, config} end def handle_info({:joined, channel}, config) do Logger.debug "Joined #{channel}" Client.msg config.client, :privmsg, config.channel, "Hello world!" {:noreply, config} end def handle_info({:names_list, channel, names_list}, config) do names = String.split(names_list, " ", trim: true) |> Enum.map(fn name -> " #{name}\n" end) Logger.info "Users logged in to #{channel}:\n#{names}" {:noreply, config} end def handle_info({:received, msg, %SenderInfo{:nick => nick}, channel}, config) do Logger.info "#{nick} from #{channel}: #{msg}" {:noreply, config} end def handle_info({:mentioned, msg, %SenderInfo{:nick => nick}, channel}, config) do Logger.warn "#{nick} mentioned you in #{channel}" case String.contains?(msg, "hi") do true -> reply = "Hi #{nick}!" Client.msg config.client, :privmsg, config.channel, reply Logger.info "Sent #{reply} to #{config.channel}" false -> :ok end {:noreply, config} end def handle_info({:received, msg, %SenderInfo{:nick => nick}}, config) do Logger.warn "#{nick}: #{msg}" reply = "Hi!" Client.msg config.client, :privmsg, nick, reply Logger.info "Sent #{reply} to #{nick}" {:noreply, config} end # Catch-all for messages you don't care about def handle_info(_msg, config) do {:noreply, config} end def terminate(_, state) do # Quit the channel and close the underlying client connection when the process is terminating Client.quit state.client, "Goodbye, cruel world." Client.stop! state.client :ok end end diff --git a/examples/ohai/connection_handler.ex b/examples/ohai/connection_handler.ex index 2ba5aad..6d0de47 100644 --- a/examples/ohai/connection_handler.ex +++ b/examples/ohai/connection_handler.ex @@ -1,38 +1,38 @@ defmodule ConnectionHandler do defmodule State do defstruct host: "chat.freenode.net", port: 6667, pass: "ohaipassword", nick: "ohaibot", user: "ohaibot", name: "ohaibot welcomes you", client: nil end def start_link(client, state \\ %State{}) do GenServer.start_link(__MODULE__, [%{state | client: client}]) end def init([state]) do - ExIrc.Client.add_handler state.client, self - ExIrc.Client.connect! state.client, state.host, state.port + ExIRC.Client.add_handler state.client, self + ExIRC.Client.connect! state.client, state.host, state.port {:ok, state} end def handle_info({:connected, server, port}, state) do debug "Connected to #{server}:#{port}" - ExIrc.Client.logon state.client, state.pass, state.nick, state.user, state.name + ExIRC.Client.logon state.client, state.pass, state.nick, state.user, state.name {:noreply, state} end # Catch-all for messages you don't care about def handle_info(msg, state) do debug "Received unknown messsage:" IO.inspect msg {:noreply, state} end defp debug(msg) do IO.puts IO.ANSI.yellow() <> msg <> IO.ANSI.reset() end end diff --git a/examples/ohai/login_handler.ex b/examples/ohai/login_handler.ex index 159e191..f7f7d1f 100644 --- a/examples/ohai/login_handler.ex +++ b/examples/ohai/login_handler.ex @@ -1,32 +1,32 @@ defmodule LoginHandler do @moduledoc """ This is an example event handler that listens for login events and then joins the appropriate channels. We actually need this because we can't join channels until we've waited for login to complete. We could just attempt to sleep until login is complete, but that's just hacky. This as an event handler is a far more elegant solution. """ def start_link(client, channels) do GenServer.start_link(__MODULE__, [client, channels]) end def init([client, channels]) do - ExIrc.Client.add_handler client, self + ExIRC.Client.add_handler client, self {:ok, {client, channels}} end def handle_info(:logged_in, state = {client, channels}) do debug "Logged in to server" - channels |> Enum.map(&ExIrc.Client.join client, &1) + channels |> Enum.map(&ExIRC.Client.join client, &1) {:noreply, state} end # Catch-all for messages you don't care about def handle_info(_msg, state) do {:noreply, state} end defp debug(msg) do IO.puts IO.ANSI.yellow() <> msg <> IO.ANSI.reset() end end diff --git a/examples/ohai/ohai_handler.ex b/examples/ohai/ohai_handler.ex index 3d411f0..fe066b3 100644 --- a/examples/ohai/ohai_handler.ex +++ b/examples/ohai/ohai_handler.ex @@ -1,37 +1,37 @@ defmodule OhaiHandler do @moduledoc """ This is an example event handler that greets users when they join a channel """ def start_link(client) do GenServer.start_link(__MODULE__, [client]) end def init([client]) do - ExIrc.Client.add_handler client, self + ExIRC.Client.add_handler client, self {:ok, client} end def handle_info({:joined, channel}, client) do debug "Joined #{channel}" {:noreply, client} end def handle_info({:joined, channel, user}, client) do - # ExIrc currently has a bug that doesn't remove the \r\n from the end + # ExIRC currently has a bug that doesn't remove the \r\n from the end # of the channel name with it sends along this kind of message # so we ensure any trailing or leading whitespace is explicitly removed channel = String.strip(channel) debug "#{user} joined #{channel}" - ExIrc.Client.msg(client, :privmsg, channel, "ohai #{user}") + ExIRC.Client.msg(client, :privmsg, channel, "ohai #{user}") {:noreply, client} end # Catch-all for messages you don't care about def handle_info(_msg, state) do {:noreply, state} end defp debug(msg) do IO.puts IO.ANSI.yellow() <> msg <> IO.ANSI.reset() end end diff --git a/examples/ohai/ohai_irc.ex b/examples/ohai/ohai_irc.ex index 27c05a2..83d5d56 100644 --- a/examples/ohai/ohai_irc.ex +++ b/examples/ohai/ohai_irc.ex @@ -1,23 +1,23 @@ defmodule OhaiIrc do use Application # See http://elixir-lang.org/docs/stable/elixir/Application.html # for more information on OTP Applications def start(_type, _args) do import Supervisor.Spec, warn: false - {:ok, client} = ExIrc.start_client! + {:ok, client} = ExIRC.start_client! children = [ # Define workers and child supervisors to be supervised worker(ConnectionHandler, [client]), worker(LoginHandler, [client, ["#ohaibot-testing"]]), worker(OhaiHandler, [client]) ] # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html # for other strategies and supported options opts = [strategy: :one_for_one, name: OhaiIrc.Supervisor] Supervisor.start_link(children, opts) end end diff --git a/lib/app.ex b/lib/app.ex index 489008a..4cb5959 100644 --- a/lib/app.ex +++ b/lib/app.ex @@ -1,10 +1,10 @@ -defmodule ExIrc.App do +defmodule ExIRC.App do @moduledoc """ - Entry point for the ExIrc application. + Entry point for the ExIRC application. """ use Application def start(_type, _args) do - ExIrc.start! + ExIRC.start! end end diff --git a/lib/exirc/channels.ex b/lib/exirc/channels.ex index f274ef1..b60e0d8 100644 --- a/lib/exirc/channels.ex +++ b/lib/exirc/channels.ex @@ -1,240 +1,240 @@ -defmodule ExIrc.Channels do +defmodule ExIRC.Channels do @moduledoc """ Responsible for managing channel state """ - use Irc.Commands + use ExIRC.Commands import String, only: [downcase: 1] defmodule Channel do defstruct name: '', topic: '', users: [], modes: '', type: '' end @doc """ Initialize a new Channels data store """ def init() do :gb_trees.empty() end ################## # Self JOIN/PART ################## @doc """ Add a channel to the data store when joining a channel """ 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{name: name}, channel_tree) end end @doc """ Remove a channel from the data store when leaving a channel """ 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 ########################### @doc """ Update the topic for a tracked channel when it changes """ 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 @doc """ Update the type of a tracked channel when it changes """ def set_type(channel_tree, channel_name, channel_type) when is_binary(channel_type) do set_type(channel_tree, channel_name, String.to_charlist(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) #################################### @doc """ Add a user to a tracked channel when they join """ def user_join(channel_tree, channel_name, nick) when not is_list(nick) do users_join(channel_tree, channel_name, [nick]) end @doc """ Add multiple users to a tracked channel (used primarily in conjunction with the NAMES command) """ def users_join(channel_tree, channel_name, nicks) do pnicks = trim_rank(nicks) manipfn = fn(channel_nicks) -> :lists.usort(channel_nicks ++ pnicks) end users_manip(channel_tree, channel_name, manipfn) end @doc """ Remove a user from a tracked channel when they leave """ def user_part(channel_tree, channel_name, nick) do pnick = trim_rank([nick]) manipfn = fn(channel_nicks) -> :lists.usort(channel_nicks -- pnick) end users_manip(channel_tree, channel_name, manipfn) end def user_quit(channel_tree, nick) do pnick = trim_rank([nick]) manipfn = fn(channel_nicks) -> :lists.usort(channel_nicks -- pnick) 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 @doc """ Update the nick of a user in a tracked channel when they change their nick """ 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 ################ @doc """ Get a list of all currently tracked channels """ def channels(channel_tree) do (for {channel_name, _chan} <- :gb_trees.to_list(channel_tree), do: channel_name) |> Enum.reverse end @doc """ Get a list of all users in a tracked channel """ def channel_users(channel_tree, channel_name) do get_attr(channel_tree, channel_name, fn(%Channel{users: users}) -> users end) |> Enum.reverse end @doc """ Get the current topic for a tracked channel """ def channel_topic(channel_tree, channel_name) do case get_attr(channel_tree, channel_name, fn(%Channel{topic: topic}) -> topic end) do [] -> "No topic" topic -> topic end end @doc """ Get the type of a tracked channel """ def channel_type(channel_tree, channel_name) do case get_attr(channel_tree, channel_name, fn(%Channel{type: type}) -> type end) do [] -> :unknown type -> type end end @doc """ Determine if a user is present in a tracked channel """ 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 @doc """ Get all channel data as a tuple of the channel name and a proplist of metadata. Example Result: [{"#testchannel", [users: ["userA", "userB"], topic: "Just a test channel.", type: :public] }] """ def to_proplist(channel_tree) do for {channel_name, chan} <- :gb_trees.to_list(channel_tree) do {channel_name, [users: chan.users, topic: chan.topic, type: chan.type]} end |> 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 trim_rank(nicks) do nicks |> Enum.map(fn(n) -> case n do << "@", nick :: binary >> -> nick << "+", nick :: binary >> -> nick << "%", nick :: binary >> -> nick << "&", nick :: binary >> -> nick << "~", nick :: binary >> -> 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 diff --git a/lib/exirc/client.ex b/lib/exirc/client.ex index 4c49106..4dd4d25 100644 --- a/lib/exirc/client.ex +++ b/lib/exirc/client.ex @@ -1,862 +1,879 @@ -defmodule ExIrc.Client do +defmodule ExIRC.Client do @moduledoc """ Maintains the state and behaviour for individual IRC client connections """ - use Irc.Commands + use ExIRC.Commands use GenServer - import ExIrc.Logger + import ExIRC.Logger - alias ExIrc.Channels - alias ExIrc.Utils - alias ExIrc.SenderInfo - alias ExIrc.Client.Transport + alias ExIRC.Channels + alias ExIRC.Utils + alias ExIRC.SenderInfo + alias ExIRC.Client.Transport # Client internal state defmodule ClientState do defstruct event_handlers: [], server: "localhost", port: 6667, socket: nil, nick: "", pass: "", user: "", name: "", ssl?: false, connected?: false, logged_on?: false, autoping: true, channel_prefixes: "", network: "", user_prefixes: "", login_time: "", channels: [], debug?: false, retries: 0, inet: :inet, owner: nil, whois_buffers: %{} end ################# # External API ################# @doc """ Start a new IRC client process Returns either {:ok, pid} or {:error, reason} """ @spec start!(options :: list() | nil) :: {:ok, pid} | {:error, term} def start!(options \\ []) do start_link(options) end @doc """ Start a new IRC client process. Returns either {:ok, pid} or {:error, reason} """ @spec start_link(options :: list() | nil, process_opts :: list() | nil) :: {:ok, pid} | {:error, term} def start_link(options \\ [], process_opts \\ []) do options = Keyword.put_new(options, :owner, self()) GenServer.start_link(__MODULE__, options, process_opts) end @doc """ Stop the IRC client process """ @spec stop!(client :: pid) :: {:stop, :normal, :ok, ClientState.t} def stop!(client) do GenServer.call(client, :stop) end @doc """ Connect to a server with the provided server and port Example: Client.connect! pid, "localhost", 6667 """ @spec connect!(client :: pid, server :: binary, port :: non_neg_integer, options :: list() | nil) :: :ok def connect!(client, server, port, options \\ []) do GenServer.call(client, {:connect, server, port, options, false}, :infinity) end @doc """ Connect to a server with the provided server and port via SSL Example: Client.connect! pid, "localhost", 6697 """ @spec connect_ssl!(client :: pid, server :: binary, port :: non_neg_integer, options :: list() | nil) :: :ok def connect_ssl!(client, server, port, options \\ []) do GenServer.call(client, {:connect, server, port, options, true}, :infinity) end @doc """ Determine if the provided client process has an open connection to a server """ @spec is_connected?(client :: pid) :: true | false def is_connected?(client) do GenServer.call(client, :is_connected?) end @doc """ Logon to a server Example: Client.logon pid, "password", "mynick", "username", "My Name" """ @spec logon(client :: pid, pass :: binary, nick :: binary, user :: binary, name :: binary) :: :ok | {:error, :not_connected} def logon(client, pass, nick, user, name) do GenServer.call(client, {:logon, pass, nick, user, name}, :infinity) end @doc """ Determine if the provided client is logged on to a server """ @spec is_logged_on?(client :: pid) :: true | false def is_logged_on?(client) do GenServer.call(client, :is_logged_on?) end @doc """ Send a message to a nick or channel Message types are: :privmsg :notice :ctcp """ @spec msg(client :: pid, type :: atom, nick :: binary, msg :: binary) :: :ok | {:error, atom} def msg(client, type, nick, msg) do GenServer.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 GenServer.call(client, {:me, channel, msg}, :infinity) end @doc """ Change the client's nick """ @spec nick(client :: pid, new_nick :: binary) :: :ok | {:error, atom} def nick(client, new_nick) do GenServer.call(client, {:nick, new_nick}, :infinity) end @doc """ Send a raw IRC command """ @spec cmd(client :: pid, raw_cmd :: binary) :: :ok | {:error, atom} def cmd(client, raw_cmd) do GenServer.call(client, {:cmd, raw_cmd}) end @doc """ Join a channel, with an optional password """ @spec join(client :: pid, channel :: binary, key :: binary | nil) :: :ok | {:error, atom} def join(client, channel, key \\ "") do GenServer.call(client, {:join, channel, key}, :infinity) end @doc """ Leave a channel """ @spec part(client :: pid, channel :: binary) :: :ok | {:error, atom} def part(client, channel) do GenServer.call(client, {:part, channel}, :infinity) end @doc """ Kick a user from a channel """ @spec kick(client :: pid, channel :: binary, nick :: binary, message :: binary | nil) :: :ok | {:error, atom} def kick(client, channel, nick, message \\ "") do GenServer.call(client, {:kick, channel, nick, message}, :infinity) end @spec names(client :: pid, channel :: binary) :: :ok | {:error, atom} def names(client, channel) do GenServer.call(client, {:names, channel}, :infinity) end @doc """ Ask the server for the user's informations. """ @spec whois(client :: pid, user :: binary) :: :ok | {:error, atom()} def whois(client, user) do GenServer.call(client, {:whois, user}, :infinity) end @doc """ Change mode for a user or channel """ @spec mode(client :: pid, channel_or_nick :: binary, flags :: binary, args :: binary | nil) :: :ok | {:error, atom} def mode(client, channel_or_nick, flags, args \\ "") do GenServer.call(client, {:mode, channel_or_nick, flags, args}, :infinity) end @doc """ Invite a user to a channel """ @spec invite(client :: pid, nick :: binary, channel :: binary) :: :ok | {:error, atom} def invite(client, nick, channel) do GenServer.call(client, {:invite, nick, channel}, :infinity) end @doc """ Quit the server, with an optional part message """ @spec quit(client :: pid, msg :: binary | nil) :: :ok | {:error, atom} def quit(client, msg \\ "Leaving..") do GenServer.call(client, {:quit, msg}, :infinity) end @doc """ Get details about each of the client's currently joined channels """ @spec channels(client :: pid) :: list(binary) | [] | {:error, atom} def channels(client) do GenServer.call(client, :channels) end @doc """ Get a list of users in the provided channel """ @spec channel_users(client :: pid, channel :: binary) :: list(binary) | [] | {:error, atom} def channel_users(client, channel) do GenServer.call(client, {:channel_users, channel}) end @doc """ Get the topic of the provided channel """ @spec channel_topic(client :: pid, channel :: binary) :: binary | {:error, atom} def channel_topic(client, channel) do GenServer.call(client, {:channel_topic, channel}) end @doc """ Get the channel type of the provided channel """ @spec channel_type(client :: pid, channel :: binary) :: atom | {:error, atom} def channel_type(client, channel) do GenServer.call(client, {:channel_type, channel}) end @doc """ Determine if a nick is present in the provided channel """ @spec channel_has_user?(client :: pid, channel :: binary, nick :: binary) :: true | false | {:error, atom} def channel_has_user?(client, channel, nick) do GenServer.call(client, {:channel_has_user?, channel, nick}) end @doc """ Add a new event handler process """ @spec add_handler(client :: pid, pid) :: :ok def add_handler(client, pid) do GenServer.call(client, {:add_handler, pid}) end @doc """ Add a new event handler process, asynchronously """ @spec add_handler_async(client :: pid, pid) :: :ok def add_handler_async(client, pid) do GenServer.cast(client, {:add_handler, pid}) end @doc """ Remove an event handler process """ @spec remove_handler(client :: pid, pid) :: :ok def remove_handler(client, pid) do GenServer.call(client, {:remove_handler, pid}) end @doc """ Remove an event handler process, asynchronously """ @spec remove_handler_async(client :: pid, pid) :: :ok def remove_handler_async(client, pid) do GenServer.cast(client, {:remove_handler, pid}) end @doc """ Get the current state of the provided client """ @spec state(client :: pid) :: [{atom, any}] def state(client) do state = GenServer.call(client, :state) [server: state.server, port: state.port, nick: state.nick, pass: state.pass, user: state.user, name: state.name, autoping: state.autoping, ssl?: state.ssl?, connected?: state.connected?, logged_on?: state.logged_on?, channel_prefixes: state.channel_prefixes, user_prefixes: state.user_prefixes, channels: Channels.to_proplist(state.channels), network: state.network, login_time: state.login_time, debug?: state.debug?, event_handlers: state.event_handlers] end ############### # GenServer API ############### @doc """ Called when GenServer initializes the client """ @spec init(list(any) | []) :: {:ok, ClientState.t} def init(options \\ []) do autoping = Keyword.get(options, :autoping, true) debug = Keyword.get(options, :debug, false) owner = Keyword.fetch!(options, :owner) # Add event handlers handlers = Keyword.get(options, :event_handlers, []) |> List.foldl([], &do_add_handler/2) ref = Process.monitor(owner) # Return initial state {:ok, %ClientState{ event_handlers: handlers, autoping: autoping, logged_on?: false, debug?: debug, - channels: ExIrc.Channels.init(), + channels: ExIRC.Channels.init(), owner: {owner, ref}}} end @doc """ Handle calls from the external API. It is not recommended to call these directly. """ # Handle call to get the current state of the client process def handle_call(:state, _from, state), do: {:reply, state, state} # Handle call to stop the current client process def handle_call(:stop, _from, state) do # Ensure the socket connection is closed if stop is called while still connected to the server if state.connected?, do: Transport.close(state) {:stop, :normal, :ok, %{state | connected?: false, logged_on?: false, socket: nil}} end # Handles call to add a new event handler process def handle_call({:add_handler, pid}, _from, state) do handlers = do_add_handler(pid, state.event_handlers) {:reply, :ok, %{state | event_handlers: handlers}} end # Handles call to remove an event handler process def handle_call({:remove_handler, pid}, _from, state) do handlers = do_remove_handler(pid, state.event_handlers) {:reply, :ok, %{state | event_handlers: handlers}} end # Handle call to connect to an IRC server def handle_call({:connect, server, port, options, ssl}, _from, state) do # If there is an open connection already, close it. if state.socket != nil, do: Transport.close(state) # Set SSL mode state = %{state | ssl?: ssl} # Open a new connection case Transport.connect(state, String.to_charlist(server), port, [:list, {:packet, :line}, {:keepalive, true}] ++ options) do {:ok, socket} -> send_event {:connected, server, port}, state {:reply, :ok, %{state | connected?: true, server: server, port: port, socket: socket}} error -> {:reply, error, state} end end # Handle call to determine if the client is connected def handle_call(:is_connected?, _from, state), do: {:reply, state.connected?, state} # Prevents any of the following messages from being handled if the client is not connected to a server. # Instead, it returns {:error, :not_connected}. def handle_call(_, _from, %ClientState{connected?: false} = state), do: {:reply, {:error, :not_connected}, state} # Handle call to login to the connected IRC server def handle_call({:logon, pass, nick, user, name}, _from, %ClientState{logged_on?: false} = state) do Transport.send state, pass!(pass) Transport.send state, nick!(nick) Transport.send state, user!(user, name) {:reply, :ok, %{state | pass: pass, nick: nick, user: user, name: name} } end # Handles call to change the client's nick. def handle_call({:nick, new_nick}, _from, %ClientState{logged_on?: false} = state) do Transport.send state, nick!(new_nick) # Since we've not yet logged on, we won't get a nick change message, so we have to remember the nick here. {:reply, :ok, %{state | nick: new_nick}} end # Handle call to determine if client is logged on to a server def handle_call(:is_logged_on?, _from, state), do: {:reply, state.logged_on?, state} # Prevents any of the following messages from being handled if the client is not logged on to a server. # Instead, it returns {:error, :not_logged_in}. def handle_call(_, _from, %ClientState{logged_on?: false} = state), do: {:reply, {:error, :not_logged_in}, state} # Handles call to send a message def handle_call({:msg, type, nick, msg}, _from, state) do data = case type do :privmsg -> privmsg!(nick, msg) :notice -> notice!(nick, msg) :ctcp -> notice!(nick, ctcp!(msg)) end Transport.send state, data {:reply, :ok, state} end # Handle /me messages def handle_call({:me, channel, msg}, _from, state) do data = me!(channel, msg) Transport.send state, data {:reply, :ok, state} end # Handles call to join a channel def handle_call({:join, channel, key}, _from, state) do Transport.send(state, join!(channel, key)) {:reply, :ok, state} end # Handles a call to leave a channel def handle_call({:part, channel}, _from, state) do Transport.send(state, part!(channel)) {:reply, :ok, state} end # Handles a call to kick a client def handle_call({:kick, channel, nick, message}, _from, state) do Transport.send(state, kick!(channel, nick, message)) {:reply, :ok, state} end # Handles a call to send the NAMES command to the server def handle_call({:names, channel}, _from, state) do Transport.send(state, names!(channel)) {:reply, :ok, state} end def handle_call({:whois, user}, _from, state) do Transport.send(state, whois!(user)) {:reply, :ok, state} end # Handles a call to change mode for a user or channel def handle_call({:mode, channel_or_nick, flags, args}, _from, state) do Transport.send(state, mode!(channel_or_nick, flags, args)) {:reply, :ok, state} end # Handle call to invite a user to a channel def handle_call({:invite, nick, channel}, _from, state) do Transport.send(state, invite!(nick, channel)) {:reply, :ok, state} end # Handle call to quit the server and close the socket connection def handle_call({:quit, msg}, _from, state) do if state.connected? do Transport.send state, quit!(msg) send_event(:disconnected, state) Transport.close state end {:reply, :ok, %{state | connected?: false, logged_on?: false, socket: nil}} end # Handles call to change the client's nick def handle_call({:nick, new_nick}, _from, state) do Transport.send(state, nick!(new_nick)); {:reply, :ok, state} end # Handles call to send a raw command to the IRC server def handle_call({:cmd, raw_cmd}, _from, state) do Transport.send(state, command!(raw_cmd)); {:reply, :ok, state} end # Handles call to return the client's channel data def handle_call(:channels, _from, state), do: {:reply, Channels.channels(state.channels), state} # Handles call to return a list of users for a given channel def handle_call({:channel_users, channel}, _from, state), do: {:reply, Channels.channel_users(state.channels, channel), state} # Handles call to return the given channel's topic def handle_call({:channel_topic, channel}, _from, state), do: {:reply, Channels.channel_topic(state.channels, channel), state} # Handles call to return the type of the given channel def handle_call({:channel_type, channel}, _from, state), do: {:reply, Channels.channel_type(state.channels, channel), state} # Handles call to determine if a nick is present in the given channel def handle_call({:channel_has_user?, channel, nick}, _from, state) do {:reply, Channels.channel_has_user?(state.channels, channel, nick), state} end # Handles message to add a new event handler process asynchronously def handle_cast({:add_handler, pid}, state) do handlers = do_add_handler(pid, state.event_handlers) {:noreply, %{state | event_handlers: handlers}} end @doc """ Handles asynchronous messages from the external API. Not recommended to call these directly. """ # Handles message to remove an event handler process asynchronously def handle_cast({:remove_handler, pid}, state) do handlers = do_remove_handler(pid, state.event_handlers) {:noreply, %{state | event_handlers: handlers}} end @doc """ Handle messages from the TCP socket connection. """ # Handles the client's socket connection 'closed' event def handle_info({:tcp_closed, _socket}, %ClientState{server: server, port: port} = state) do info "Connection to #{server}:#{port} closed!" send_event :disconnected, state new_state = %{state | socket: nil, connected?: false, logged_on?: false, channels: Channels.init() } {:noreply, new_state} end @doc """ Handle messages from the SSL socket connection. """ # Handles the client's socket connection 'closed' event def handle_info({:ssl_closed, socket}, state) do handle_info({:tcp_closed, socket}, state) end # Handles any TCP errors in the client's socket connection def handle_info({:tcp_error, socket, reason}, %ClientState{server: server, port: port} = state) do error "TCP error in connection to #{server}:#{port}:\r\n#{reason}\r\nClient connection closed." new_state = %{state | socket: nil, connected?: false, logged_on?: false, channels: Channels.init() } {:stop, {:tcp_error, socket}, new_state} end # Handles any SSL errors in the client's socket connection def handle_info({:ssl_error, socket, reason}, state) do handle_info({:tcp_error, socket, reason}, state) end # General handler for messages from the IRC server def handle_info({:tcp, _, data}, state) do debug? = state.debug? case Utils.parse(data) do - %IrcMessage{ctcp: true} = msg -> + %ExIRC.Message{ctcp: true} = msg -> handle_data msg, state {:noreply, state} - %IrcMessage{ctcp: false} = msg -> + %ExIRC.Message{ctcp: false} = msg -> handle_data msg, state - %IrcMessage{ctcp: :invalid} = msg when debug? -> + %ExIRC.Message{ctcp: :invalid} = msg when debug? -> send_event msg, state {:noreply, state} _ -> {:noreply, state} end end # Wrapper for SSL socket messages def handle_info({:ssl, socket, data}, state) do handle_info({:tcp, socket, data}, state) end # If the owner process dies, we should die as well def handle_info({:DOWN, ref, _, pid, reason}, %{owner: {pid, ref}} = state) do {:stop, reason, state} end # If an event handler process dies, remove it from the list of event handlers def handle_info({:DOWN, _, _, pid, _}, state) do handlers = do_remove_handler(pid, state.event_handlers) {:noreply, %{state | event_handlers: handlers}} end # Catch-all for unrecognized messages (do nothing) def handle_info(_, state) do {:noreply, state} end @doc """ Handle termination """ def terminate(_reason, state) do if state.socket != nil do Transport.close state %{state | socket: nil} end :ok end @doc """ Transform state for hot upgrades/downgrades """ def code_change(_old, state, _extra), do: {:ok, state} ################ # Data handling ################ @doc """ - Handle IrcMessages received from the server. + Handle ExIRC.Messages received from the server. """ # Called upon successful login - def handle_data(%IrcMessage{cmd: @rpl_welcome}, %ClientState{logged_on?: false} = state) do + def handle_data(%ExIRC.Message{cmd: @rpl_welcome}, %ClientState{logged_on?: false} = state) do if state.debug?, do: debug "SUCCESFULLY LOGGED ON" new_state = %{state | logged_on?: true, login_time: :erlang.timestamp()} send_event :logged_in, new_state {:noreply, new_state} end # Called when the server sends it's current capabilities - def handle_data(%IrcMessage{cmd: @rpl_isupport} = msg, state) do + def handle_data(%ExIRC.Message{cmd: @rpl_isupport} = msg, state) do if state.debug?, do: debug "RECEIVING SERVER CAPABILITIES" {:noreply, Utils.isup(msg.args, state)} end # Called when the client enters a channel - def handle_data(%IrcMessage{nick: nick, cmd: "JOIN"} = msg, %ClientState{nick: nick} = state) do + + def handle_data(%ExIRC.Message{nick: nick, cmd: "JOIN"} = msg, %ClientState{nick: nick} = state) do channel = msg.args |> List.first |> String.trim if state.debug?, do: debug "JOINED A CHANNEL #{channel}" channels = Channels.join(state.channels, channel) new_state = %{state | channels: channels} send_event {:joined, channel}, new_state {:noreply, new_state} end # Called when another user joins a channel the client is in - def handle_data(%IrcMessage{nick: user_nick, cmd: "JOIN", host: host, user: user} = msg, state) do + def handle_data(%ExIRC.Message{nick: user_nick, cmd: "JOIN", host: host, user: user} = msg, state) do sender = %SenderInfo{nick: user_nick, host: host, user: user} channel = msg.args |> List.first |> String.trim if state.debug?, do: debug "ANOTHER USER JOINED A CHANNEL: #{channel} - #{user_nick}" channels = Channels.user_join(state.channels, channel, user_nick) new_state = %{state | channels: channels} send_event {:joined, channel, sender}, new_state {:noreply, new_state} end # Called on joining a channel, to tell us the channel topic # Message with three arguments is not RFC compliant but very common # Message with two arguments is RFC compliant # Message with a single argument is not RFC compliant, but is present # to handle poorly written IRC servers which send RPL_TOPIC with an empty # topic (such as Slack's IRC bridge), when they should be sending RPL_NOTOPIC - def handle_data(%IrcMessage{cmd: @rpl_topic} = msg, state) do + def handle_data(%ExIRC.Message{cmd: @rpl_topic} = msg, state) do {channel, topic} = case msg.args do [_nick, channel, topic] -> {channel, topic} [channel, topic] -> {channel, topic} [channel] -> {channel, "No topic is set"} end if state.debug? do debug "INITIAL TOPIC MSG" debug "1. TOPIC SET FOR #{channel} TO #{topic}" end channels = Channels.set_topic(state.channels, channel, topic) new_state = %{state | channels: channels} send_event {:topic_changed, channel, topic}, new_state {:noreply, new_state} end ## WHOIS - def handle_data(%IrcMessage{cmd: @rpl_whoisuser, args: [_sender, nickname, username, hostname, _, realname]}, state) do + + def handle_data(%ExIRC.Message{cmd: @rpl_whoisuser, args: [_sender, nickname, username, hostname, _, realname]}, state) do + user = %{nickname: nickname, username: username, hostname: hostname, realname: realname} {:noreply, %ClientState{state|whois_buffers: Map.put(state.whois_buffers, nickname, user)}} end - def handle_data(%IrcMessage{cmd: @rpl_whoiscertfp, args: [_sender, nickname, "has client certificate fingerprint "<> fingerprint]}, state) do + def handle_data(%ExIRC.Message{cmd: @rpl_whoiscertfp, args: [_sender, nickname, "has client certificate fingerprint "<> fingerprint]}, state) do {:noreply, %ClientState{state|whois_buffers: put_in(state.whois_buffers, [nickname, :certfp], fingerprint)}} end - def handle_data(%IrcMessage{cmd: @rpl_whoisregnick, args: [_sender, nickname, _message]}, state) do + def handle_data(%ExIRC.Message{cmd: @rpl_whoisregnick, args: [_sender, nickname, _message]}, state) do {:noreply, %ClientState{state|whois_buffers: put_in(state.whois_buffers, [nickname, :registered_nick?], true)}} end - def handle_data(%IrcMessage{cmd: @rpl_whoishelpop, args: [_sender, nickname, _message]}, state) do + def handle_data(%ExIRC.Message{cmd: @rpl_whoishelpop, args: [_sender, nickname, _message]}, state) do {:noreply, %ClientState{state|whois_buffers: put_in(state.whois_buffers, [nickname, :helpop?], true)}} end - def handle_data(%IrcMessage{cmd: @rpl_whoischannels, args: [_sender, nickname, channels]}, state) do + def handle_data(%ExIRC.Message{cmd: @rpl_whoischannels, args: [_sender, nickname, channels]}, state) do + chans = String.split(channels, " ") {:noreply, %ClientState{state|whois_buffers: put_in(state.whois_buffers, [nickname, :channels], chans)}} end - def handle_data(%IrcMessage{cmd: @rpl_whoisserver, args: [_sender, nickname, server_addr, server_name]}, state) do + + def handle_data(%ExIRC.Message{cmd: @rpl_whoisserver, args: [_sender, nickname, server_addr, server_name]}, state) do + new_buffer = state.whois_buffers |> put_in([nickname, :server_name], server_name) |> put_in([nickname, :server_address], server_addr) {:noreply, %ClientState{state|whois_buffers: new_buffer}} end - def handle_data(%IrcMessage{cmd: @rpl_whoisoperator, args: [_sender, nickname, _message]}, state) do + def handle_data(%ExIRC.Message{cmd: @rpl_whoisoperator, args: [_sender, nickname, _message]}, state) do {:noreply, %ClientState{state|whois_buffers: put_in(state.whois_buffers, [nickname, :ircop?], true)}} end - def handle_data(%IrcMessage{cmd: @rpl_whoisaccount, args: [_sender, nickname, account_name, _message]}, state) do + def handle_data(%ExIRC.Message{cmd: @rpl_whoisaccount, args: [_sender, nickname, account_name, _message]}, state) do {:noreply, %ClientState{state|whois_buffers: put_in(state.whois_buffers, [nickname, :account_name], account_name)}} end - def handle_data(%IrcMessage{cmd: @rpl_whoissecure, args: [_sender, nickname, _message]}, state) do + def handle_data(%ExIRC.Message{cmd: @rpl_whoissecure, args: [_sender, nickname, _message]}, state) do {:noreply, %ClientState{state|whois_buffers: put_in(state.whois_buffers, [nickname, :tls?], true)}} end - def handle_data(%IrcMessage{cmd: @rpl_whoisidle, args: [_sender, nickname, idling_time, signon_time, _message]}, state) do + def handle_data(%ExIRC.Message{cmd: @rpl_whoisidle, args: [_sender, nickname, idling_time, signon_time, _message]}, state) do + new_buffer = state.whois_buffers |> put_in([nickname, :idling_time], idling_time) |> put_in([nickname, :signon_time], signon_time) {:noreply, %ClientState{state|whois_buffers: new_buffer}} end - def handle_data(%IrcMessage{cmd: @rpl_endofwhois, args: [_sender, nickname, _message]}, state) do - buffer = struct(Irc.Whois, state.whois_buffers[nickname]) + def handle_data(%ExIRC.Message{cmd: @rpl_endofwhois, args: [_sender, nickname, _message]}, state) do + buffer = struct(ExIRC.Whois, state.whois_buffers[nickname]) + send_event {:whois, buffer}, state {:noreply, %ClientState{state|whois_buffers: Map.delete(state.whois_buffers, nickname)}} end - def handle_data(%IrcMessage{cmd: @rpl_notopic, args: [channel]}, state) do + def handle_data(%ExIRC.Message{cmd: @rpl_notopic, args: [channel]}, state) do + if state.debug? do debug "INITIAL TOPIC MSG" debug "1. NO TOPIC SET FOR #{channel}}" end channels = Channels.set_topic(state.channels, channel, "No topic is set") new_state = %{state | channels: channels} {:noreply, new_state} end # Called when the topic changes while we're in the channel - def handle_data(%IrcMessage{cmd: "TOPIC", args: [channel, topic]}, state) do + def handle_data(%ExIRC.Message{cmd: "TOPIC", args: [channel, topic]}, state) do if state.debug?, do: debug "TOPIC CHANGED FOR #{channel} TO #{topic}" channels = Channels.set_topic(state.channels, channel, topic) new_state = %{state | channels: channels} send_event {:topic_changed, channel, topic}, new_state {:noreply, new_state} end # Called when joining a channel with the list of current users in that channel, or when the NAMES command is sent - def handle_data(%IrcMessage{cmd: @rpl_namereply} = msg, state) do + def handle_data(%ExIRC.Message{cmd: @rpl_namereply} = msg, state) do if state.debug?, do: debug "NAMES LIST RECEIVED" {_nick, channel_type, channel, names} = case msg.args do [nick, channel_type, channel, names] -> {nick, channel_type, channel, names} [channel_type, channel, names] -> {nil, channel_type, channel, names} end channels = Channels.set_type( Channels.users_join(state.channels, channel, String.split(names, " ", trim: true)), channel, channel_type) send_event({:names_list, channel, names}, state) {:noreply, %{state | channels: channels}} end # Called when our nick has succesfully changed - def handle_data(%IrcMessage{cmd: "NICK", nick: nick, args: [new_nick]}, %ClientState{nick: nick} = state) do + def handle_data(%ExIRC.Message{cmd: "NICK", nick: nick, args: [new_nick]}, %ClientState{nick: nick} = state) do if state.debug?, do: debug "NICK CHANGED FROM #{nick} TO #{new_nick}" new_state = %{state | nick: new_nick} send_event {:nick_changed, new_nick}, new_state {:noreply, new_state} end # Called when someone visible to us changes their nick - def handle_data(%IrcMessage{cmd: "NICK", nick: nick, args: [new_nick]}, state) do + def handle_data(%ExIRC.Message{cmd: "NICK", nick: nick, args: [new_nick]}, state) do if state.debug?, do: debug "#{nick} CHANGED THEIR NICK TO #{new_nick}" channels = Channels.user_rename(state.channels, nick, new_nick) new_state = %{state | channels: channels} send_event {:nick_changed, nick, new_nick}, new_state {:noreply, new_state} end # Called upon mode change - def handle_data(%IrcMessage{cmd: "MODE", args: [channel, op, user]}, state) do + def handle_data(%ExIRC.Message{cmd: "MODE", args: [channel, op, user]}, state) do if state.debug?, do: debug "MODE #{channel} #{op} #{user}" send_event {:mode, [channel, op, user]}, state {:noreply, state} end # Called when we leave a channel - def handle_data(%IrcMessage{cmd: "PART", nick: nick} = msg, %ClientState{nick: nick} = state) do + + def handle_data(%ExIRC.Message{cmd: "PART", nick: nick} = msg, %ClientState{nick: nick} = state) do + channel = msg.args |> List.first |> String.trim if state.debug?, do: debug "WE LEFT A CHANNEL: #{channel}" channels = Channels.part(state.channels, channel) new_state = %{state | channels: channels} send_event {:parted, channel}, new_state {:noreply, new_state} end # Called when someone else in our channel leaves - def handle_data(%IrcMessage{cmd: "PART", nick: from, host: host, user: user} = msg, state) do + def handle_data(%ExIRC.Message{cmd: "PART", nick: from, host: host, user: user} = msg, state) do sender = %SenderInfo{nick: from, host: host, user: user} channel = msg.args |> List.first |> String.trim if state.debug?, do: debug "#{from} LEFT A CHANNEL: #{channel}" channels = Channels.user_part(state.channels, channel, from) new_state = %{state | channels: channels} send_event {:parted, channel, sender}, new_state {:noreply, new_state} end - def handle_data(%IrcMessage{cmd: "QUIT", nick: from, host: host, user: user} = msg, state) do + def handle_data(%ExIRC.Message{cmd: "QUIT", nick: from, host: host, user: user} = msg, state) do sender = %SenderInfo{nick: from, host: host, user: user} reason = msg.args |> List.first if state.debug?, do: debug "#{from} QUIT" channels = Channels.user_quit(state.channels, from) new_state = %{state | channels: channels} send_event {:quit, reason, sender}, new_state {:noreply, new_state} end # Called when we receive a PING - def handle_data(%IrcMessage{cmd: "PING"} = msg, %ClientState{autoping: true} = state) do + def handle_data(%ExIRC.Message{cmd: "PING"} = msg, %ClientState{autoping: true} = state) do if state.debug?, do: debug "RECEIVED A PING!" case msg do - %IrcMessage{args: [from]} -> + %ExIRC.Message{args: [from]} -> if state.debug?, do: debug("SENT PONG2") Transport.send(state, pong2!(from, msg.server)) _ -> if state.debug?, do: debug("SENT PONG1") Transport.send(state, pong1!(state.nick)) end {:noreply, state}; end # Called when we are invited to a channel - def handle_data(%IrcMessage{cmd: "INVITE", args: [nick, channel], nick: by, host: host, user: user} = msg, %ClientState{nick: nick} = state) do + def handle_data(%ExIRC.Message{cmd: "INVITE", args: [nick, channel], nick: by, host: host, user: user} = msg, %ClientState{nick: nick} = state) do sender = %SenderInfo{nick: by, host: host, user: user} if state.debug?, do: debug "RECEIVED AN INVITE: #{msg.args |> Enum.join(" ")}" send_event {:invited, sender, channel}, state {:noreply, state} end # Called when we are kicked from a channel - def handle_data(%IrcMessage{cmd: "KICK", args: [channel, nick, reason], nick: by, host: host, user: user} = _msg, %ClientState{nick: nick} = state) do + + def handle_data(%ExIRC.Message{cmd: "KICK", args: [channel, nick, reason], nick: by, host: host, user: user} = _msg, %ClientState{nick: nick} = state) do + sender = %SenderInfo{nick: by, host: host, user: user} if state.debug?, do: debug "WE WERE KICKED FROM #{channel} BY #{by}" send_event {:kicked, sender, channel, reason}, state {:noreply, state} end # Called when someone else was kicked from a channel - def handle_data(%IrcMessage{cmd: "KICK", args: [channel, nick, reason], nick: by, host: host, user: user} = _msg, state) do + + def handle_data(%ExIRC.Message{cmd: "KICK", args: [channel, nick, reason], nick: by, host: host, user: user} = _msg, state) do + sender = %SenderInfo{nick: by, host: host, user: user} if state.debug?, do: debug "#{nick} WAS KICKED FROM #{channel} BY #{by}" send_event {:kicked, nick, sender, channel, reason}, state {:noreply, state} end # Called when someone sends us a message - def handle_data(%IrcMessage{nick: from, cmd: "PRIVMSG", args: [nick, message], host: host, user: user} = _msg, %ClientState{nick: nick} = state) do + def handle_data(%ExIRC.Message{nick: from, cmd: "PRIVMSG", args: [nick, message], host: host, user: user} = _msg, %ClientState{nick: nick} = state) do sender = %SenderInfo{nick: from, host: host, user: user} if state.debug?, do: debug "#{from} SENT US #{message}" send_event {:received, message, sender}, state {:noreply, state} end # Called when someone sends a message to a channel we're in, or a list of users - def handle_data(%IrcMessage{nick: from, cmd: "PRIVMSG", args: [to, message], host: host, user: user} = _msg, %ClientState{nick: nick} = state) do + def handle_data(%ExIRC.Message{nick: from, cmd: "PRIVMSG", args: [to, message], host: host, user: user} = _msg, %ClientState{nick: nick} = state) do sender = %SenderInfo{nick: from, host: host, user: user} if state.debug?, do: debug "#{from} SENT #{message} TO #{to}" send_event {:received, message, sender, to}, state # If we were mentioned, fire that event as well if String.contains?(message, nick), do: send_event({:mentioned, message, sender, to}, state) {:noreply, state} end # Called when someone uses ACTION, i.e. `/me dies` - def handle_data(%IrcMessage{nick: from, cmd: "ACTION", args: [channel, message], host: host, user: user} = _msg, state) do + def handle_data(%ExIRC.Message{nick: from, cmd: "ACTION", args: [channel, message], host: host, user: user} = _msg, state) do sender = %SenderInfo{nick: from, host: host, user: user} if state.debug?, do: debug "* #{from} #{message} in #{channel}" send_event {:me, message, sender, channel}, state {:noreply, state} end + # Called when a NOTICE is received by the client. - def handle_data(%IrcMessage{nick: from, cmd: "NOTICE", args: [_target, message], host: host, user: user} = _msg, state) do + def handle_data(%ExIRC.Message{nick: from, cmd: "NOTICE", args: [_target, message], host: host, user: user} = _msg, state) do + sender = %SenderInfo{nick: from, host: host, user: user} if String.contains?(message, "identify") do if state.debug?, do: debug("* Told to identify by #{from}: #{message}") send_event({:identify, message, sender}, state) else if state.debug?, do: debug("* #{message} from #{sender}") send_event({:notice, message, sender}, state) end {: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 send_event {:unrecognized, msg.cmd, msg}, state {:noreply, state} end ############### # Internal API ############### defp send_event(msg, %ClientState{event_handlers: handlers}) when is_list(handlers) do Enum.each(handlers, fn({pid, _}) -> Kernel.send(pid, msg) end) end defp do_add_handler(pid, handlers) do case Enum.member?(handlers, pid) do false -> ref = Process.monitor(pid) [{pid, ref} | handlers] true -> handlers end end defp do_remove_handler(pid, handlers) do case List.keyfind(handlers, pid, 0) do {pid, ref} -> Process.demonitor(ref) List.keydelete(handlers, pid, 0) nil -> handlers end end defp debug(msg) do IO.puts(IO.ANSI.green() <> msg <> IO.ANSI.reset()) end end diff --git a/lib/exirc/commands.ex b/lib/exirc/commands.ex index 0d959c0..c4833d5 100644 --- a/lib/exirc/commands.ex +++ b/lib/exirc/commands.ex @@ -1,288 +1,288 @@ -defmodule Irc.Commands do +defmodule ExIRC.Commands do @moduledoc """ Defines IRC command constants, and methods for generating valid commands to send to an IRC server. """ defmacro __using__(_) do quote do - import Irc.Commands + import ExIRC.Commands #################### # IRC Numeric Codes #################### @rpl_welcome "001" @rpl_yourhost "002" @rpl_created "003" @rpl_myinfo "004" @rpl_isupport "005" # Defacto standard for server support @rpl_bounce "010" # Defacto replacement of "005" in RFC2812 @rpl_statsdline "250" #@doc """ #":There are users and invisible on servers" #""" @rpl_luserclient "251" #@doc """ # " :operator(s) online" #""" @rpl_luserop "252" #@doc """ #" :unknown connection(s)" #""" @rpl_luserunknown "253" #@doc """ #" :channels formed" #""" @rpl_luserchannels "254" #@doc """ #":I have clients and servers" #""" @rpl_luserme "255" #@doc """ #Local/Global user stats #""" @rpl_localusers "265" @rpl_globalusers "266" #@doc """ #When sending a TOPIC message to determine the channel topic, #one of two replies is sent. If the topic is set, RPL_TOPIC is sent back else #RPL_NOTOPIC. #""" @rpl_whoiscertfp "276" @rpl_whoisregnick "307" @rpl_whoishelpop "310" @rpl_whoisuser "311" @rpl_whoisserver "312" @rpl_whoisoperator "313" @rpl_whoisidle "317" @rpl_endofwhois "318" @rpl_whoischannels "319" @rpl_whoisaccount "330" @rpl_notopic "331" @rpl_topic "332" #@doc """ #To reply to a NAMES message, a reply pair consisting #of RPL_NAMREPLY and RPL_ENDOFNAMES is sent by the #server back to the client. If there is no channel #found as in the query, then only RPL_ENDOFNAMES is #returned. The exception to this is when a NAMES #message is sent with no parameters and all visible #channels and contents are sent back in a series of #RPL_NAMEREPLY messages with a RPL_ENDOFNAMES to mark #the end. #Format: " :[[@|+] [[@|+] [...]]]" #""" @rpl_namereply "353" @rpl_endofnames "366" #@doc """ #When responding to the MOTD message and the MOTD file #is found, the file is displayed line by line, with #each line no longer than 80 characters, using #RPL_MOTD format replies. These should be surrounded #by a RPL_MOTDSTART (before the RPL_MOTDs) and an #RPL_ENDOFMOTD (after). #""" @rpl_motd "372" @rpl_motdstart "375" @rpl_endofmotd "376" @rpl_whoishost "378" @rpl_whoismodes "379" ################ # Error Codes ################ #@doc """ #Used to indicate the nickname parameter supplied to a command is currently unused. #""" @err_no_such_nick "401" #@doc """ #Used to indicate the server name given currently doesn"t exist. #""" @err_no_such_server "402" #@doc """ #Used to indicate the given channel name is invalid. #""" @err_no_such_channel "403" #@doc """ #Sent to a user who is either (a) not on a channel which is mode +n or (b), #not a chanop (or mode +v) on a channel which has mode +m set, and is trying #to send a PRIVMSG message to that channel. #""" @err_cannot_send_to_chan "404" #@doc """ #Sent to a user when they have joined the maximum number of allowed channels #and they try to join another channel. #""" @err_too_many_channels "405" #@doc """ #Returned to a registered client to indicate that the command sent is unknown by the server. #""" @err_unknown_command "421" #@doc """ #Returned when a nickname parameter expected for a command and isn"t found. #""" @err_no_nickname_given "431" #@doc """ #Returned after receiving a NICK message which contains characters which do not fall in the defined set. #""" @err_erroneus_nickname "432" #@doc """ #Returned when a NICK message is processed that results in an attempt to #change to a currently existing nickname. #""" @err_nickname_in_use "433" #@doc """ #Returned by a server to a client when it detects a nickname collision #(registered of a NICK that already exists by another server). #""" @err_nick_collision "436" #@doc """ #""" @err_unavail_resource "437" #@doc """ #Returned by the server to indicate that the client must be registered before #the server will allow it to be parsed in detail. #""" @err_not_registered "451" #""" # Returned by the server by numerous commands to indicate to the client that # it didn"t supply enough parameters. #""" @err_need_more_params "461" #@doc """ #Returned by the server to any link which tries to change part of the registered #details (such as password or user details from second USER message). #""" @err_already_registered "462" #@doc """ #Returned by the server to the client when the issued command is restricted #""" @err_restricted "484" @rpl_whoissecure "671" ############### # Code groups ############### @logon_errors [ @err_no_nickname_given, @err_erroneus_nickname, @err_nickname_in_use, @err_nick_collision, @err_unavail_resource, @err_need_more_params, @err_already_registered, @err_restricted ] @whois_rpls [ @rpl_whoisuser, @rpl_whoishost, @rpl_whoishost, @rpl_whoisserver, @rpl_whoismodes, @rpl_whoisidle, @rpl_endofwhois ] end end ############ # Helpers ############ @ctcp_delimiter 0o001 @doc """ Builds a valid IRC command. """ def command!(cmd), do: [cmd, '\r\n'] @doc """ Builds a valid CTCP command. """ 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 a WHOIS request about a user """ def whois!(user), do: command! ['WHOIS ', user] @doc """ Send password to server """ def pass!(pwd), do: command! ['PASS ', pwd] @doc """ Send nick to server. (Changes or sets your 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] end @doc """ Send PONG in response to PING """ 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] @doc """ Send message to channel or user """ def privmsg!(nick, msg), do: command! ['PRIVMSG ', nick, ' :', msg] @doc """ Send a `/me ` CTCP command to t """ def me!(channel, msg), do: command! ['PRIVMSG ', channel, ' :', @ctcp_delimiter, 'ACTION ', msg, @ctcp_delimiter] @doc """ Sends a command to the server to get the list of names back """ def names!(_channel), do: command! ['NAMES'] @doc """ Send notice to channel or user """ def notice!(nick, msg), do: command! ['NOTICE ', nick, ' :', msg] @doc """ Send join command to server (join a channel) """ 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] @doc """ Send quit command to server (disconnect from server) """ 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] end end @doc """ Send mode command to server MODE MODE [] """ 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] end end @doc """ Send an invite command """ def invite!(nick, channel) do command! ['INVITE ', nick, ' ', channel] end end diff --git a/lib/exirc/example_handler.ex b/lib/exirc/example_handler.ex index 48774fd..1701729 100644 --- a/lib/exirc/example_handler.ex +++ b/lib/exirc/example_handler.ex @@ -1,132 +1,132 @@ defmodule ExampleHandler do @moduledoc """ This is an example event handler that you can attach to the client using `add_handler` or `add_handler_async`. To remove, call `remove_handler` or `remove_handler_async` with the pid of the handler process. """ def start! do start_link([]) end def start_link(_) do GenServer.start_link(__MODULE__, nil, []) end def init(_) do {:ok, nil} end @doc """ Handle messages from the client Examples: def handle_info({:connected, server, port}, _state) do IO.puts "Connected to \#{server}:\#{port}" end def handle_info(:logged_in, _state) do IO.puts "Logged in!" end - def handle_info(%IrcMessage{nick: from, cmd: "PRIVMSG", args: ["mynick", msg]}, _state) do + def handle_info(%ExIRC.Message{nick: from, cmd: "PRIVMSG", args: ["mynick", msg]}, _state) do IO.puts "Received a private message from \#{from}: \#{msg}" end - def handle_info(%IrcMessage{nick: from, cmd: "PRIVMSG", args: [to, msg]}, _state) do + def handle_info(%ExIRC.Message{nick: from, cmd: "PRIVMSG", args: [to, msg]}, _state) do IO.puts "Received a message in \#{to} from \#{from}: \#{msg}" end """ def handle_info({:connected, server, port}, _state) do debug "Connected to #{server}:#{port}" {:noreply, nil} end def handle_info(:logged_in, _state) do debug "Logged in to server" {:noreply, nil} end def handle_info(:disconnected, _state) do debug "Disconnected from server" {:noreply, nil} end def handle_info({:joined, channel}, _state) do debug "Joined #{channel}" {:noreply, nil} end def handle_info({:joined, channel, user}, _state) do debug "#{user} joined #{channel}" {:noreply, nil} end def handle_info({:topic_changed, channel, topic}, _state) do debug "#{channel} topic changed to #{topic}" {:noreply, nil} end def handle_info({:nick_changed, nick}, _state) do debug "We changed our nick to #{nick}" {:noreply, nil} end def handle_info({:nick_changed, old_nick, new_nick}, _state) do debug "#{old_nick} changed their nick to #{new_nick}" {:noreply, nil} end def handle_info({:parted, channel}, _state) do debug "We left #{channel}" {:noreply, nil} end def handle_info({:parted, channel, sender}, _state) do nick = sender.nick debug "#{nick} left #{channel}" {:noreply, nil} end def handle_info({:invited, sender, channel}, _state) do by = sender.nick debug "#{by} invited us to #{channel}" {:noreply, nil} end def handle_info({:kicked, sender, channel}, _state) do by = sender.nick debug "We were kicked from #{channel} by #{by}" {:noreply, nil} end def handle_info({:kicked, nick, sender, channel}, _state) do by = sender.nick debug "#{nick} was kicked from #{channel} by #{by}" {:noreply, nil} end def handle_info({:received, message, sender}, _state) do from = sender.nick debug "#{from} sent us a private message: #{message}" {:noreply, nil} end def handle_info({:received, message, sender, channel}, _state) do from = sender.nick debug "#{from} sent a message to #{channel}: #{message}" {:noreply, nil} end def handle_info({:mentioned, message, sender, channel}, _state) do from = sender.nick debug "#{from} mentioned us in #{channel}: #{message}" {:noreply, nil} end def handle_info({:me, message, sender, channel}, _state) do from = sender.nick 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 + # 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(%ExIRC.Message{nick: from, cmd: "PRIVMSG", args: ["testnick", msg]}, _state) do debug "Received a private message from #{from}: #{msg}" {:noreply, nil} end - def handle_info(%IrcMessage{nick: from, cmd: "PRIVMSG", args: [to, msg]}, _state) do + def handle_info(%ExIRC.Message{nick: from, cmd: "PRIVMSG", args: [to, msg]}, _state) do debug "Received a message in #{to} from #{from}: #{msg}" {:noreply, nil} end # Catch-all for messages you don't care about def handle_info(msg, _state) do - debug "Received IrcMessage:" + debug "Received ExIRC.Message:" IO.inspect msg {:noreply, nil} end defp debug(msg) do IO.puts IO.ANSI.yellow() <> msg <> IO.ANSI.reset() end end diff --git a/lib/exirc/exirc.ex b/lib/exirc/exirc.ex index c36e6e4..41d105a 100644 --- a/lib/exirc/exirc.ex +++ b/lib/exirc/exirc.ex @@ -1,75 +1,75 @@ -defmodule ExIrc do +defmodule ExIRC do @moduledoc """ Supervises IRC client processes Usage: - # Start the supervisor (started automatically when ExIrc is run as an application) - ExIrc.start_link + # Start the supervisor (started automatically when ExIRC is run as an application) + ExIRC.start_link # Start a new IRC client - {:ok, client} = ExIrc.start_client! + {:ok, client} = ExIRC.start_client! # Connect to an IRC server - ExIrc.Client.connect! client, "localhost", 6667 + ExIRC.Client.connect! client, "localhost", 6667 # Logon - ExIrc.Client.logon client, "password", "nick", "user", "name" + ExIRC.Client.logon client, "password", "nick", "user", "name" # Join a channel (password is optional) - ExIrc.Client.join client, "#channel", "password" + ExIRC.Client.join client, "#channel", "password" # Send a message - ExIrc.Client.msg client, :privmsg, "#channel", "Hello world!" + ExIRC.Client.msg client, :privmsg, "#channel", "Hello world!" # Quit (message is optional) - ExIrc.Client.quit client, "message" + ExIRC.Client.quit client, "message" # Stop and close the client connection - ExIrc.Client.stop! client + ExIRC.Client.stop! client """ use Supervisor import Supervisor.Spec ############## # Public API ############## @doc """ - Start the ExIrc supervisor. + Start the ExIRC supervisor. """ @spec start! :: {:ok, pid} | {:error, term} def start! do Supervisor.start_link(__MODULE__, [], name: :exirc) end @doc """ - Start a new ExIrc client under the ExIrc supervisor + Start a new ExIRC client under the ExIRC supervisor """ @spec start_client! :: {:ok, pid} | {:error, term} def start_client! do # Start the client worker Supervisor.start_child(:exirc, [[owner: self()]]) end @doc """ - Start a new ExIrc client + Start a new ExIRC client """ def start_link! do - ExIrc.Client.start!([owner: self()]) + ExIRC.Client.start!([owner: self()]) end ############## # Supervisor API ############## @spec init(any) :: {:ok, pid} | {:error, term} def init(_) do children = [ - worker(ExIrc.Client, [], restart: :temporary) + worker(ExIRC.Client, [], restart: :temporary) ] supervise children, strategy: :simple_one_for_one end end diff --git a/lib/exirc/irc_message.ex b/lib/exirc/irc_message.ex index ad865fc..2ea26e5 100644 --- a/lib/exirc/irc_message.ex +++ b/lib/exirc/irc_message.ex @@ -1,9 +1,9 @@ -defmodule IrcMessage do +defmodule ExIRC.Message do defstruct server: '', nick: '', user: '', host: '', ctcp: nil, cmd: '', args: [] end diff --git a/lib/exirc/logger.ex b/lib/exirc/logger.ex index 65ae980..9cee4bf 100644 --- a/lib/exirc/logger.ex +++ b/lib/exirc/logger.ex @@ -1,29 +1,29 @@ -defmodule ExIrc.Logger do +defmodule ExIRC.Logger do @moduledoc """ A simple abstraction of :error_logger """ @doc """ Log an informational message report """ @spec info(binary) :: :ok def info(msg) do :error_logger.info_report String.to_charlist(msg) end @doc """ Log a warning message report """ @spec warning(binary) :: :ok def warning(msg) do :error_logger.warning_report String.to_charlist("#{IO.ANSI.yellow()}#{msg}#{IO.ANSI.reset()}") end @doc """ Log an error message report """ @spec error(binary) :: :ok def error(msg) do :error_logger.error_report String.to_charlist("#{IO.ANSI.red()}#{msg}#{IO.ANSI.reset()}") end end \ No newline at end of file diff --git a/lib/exirc/sender_info.ex b/lib/exirc/sender_info.ex index b468f64..eda901b 100644 --- a/lib/exirc/sender_info.ex +++ b/lib/exirc/sender_info.ex @@ -1,8 +1,8 @@ -defmodule ExIrc.SenderInfo do +defmodule ExIRC.SenderInfo do @moduledoc """ This struct represents information available about the sender of a message. """ defstruct nick: nil, host: nil, user: nil end diff --git a/lib/exirc/transport.ex b/lib/exirc/transport.ex index 4c9456b..dedd8ce 100644 --- a/lib/exirc/transport.ex +++ b/lib/exirc/transport.ex @@ -1,22 +1,22 @@ -defmodule ExIrc.Client.Transport do +defmodule ExIRC.Client.Transport do def connect(%{ssl?: false}, host, port, options) do :gen_tcp.connect(host, port, options) end def connect(%{ssl?: true}, host, port, options) do :ssl.connect(host, port, options) end def send(%{ssl?: false, socket: socket}, data) do :gen_tcp.send(socket, data) end def send(%{ssl?: true, socket: socket}, data) do :ssl.send(socket, data) end def close(%{ssl?: false, socket: socket}) do :gen_tcp.close(socket) end def close(%{ssl?: true, socket: socket}) do :ssl.close(socket) end end diff --git a/lib/exirc/utils.ex b/lib/exirc/utils.ex index d79b326..428379d 100644 --- a/lib/exirc/utils.ex +++ b/lib/exirc/utils.ex @@ -1,188 +1,190 @@ -defmodule ExIrc.Utils do +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 + message = ExIRC.Utils.parse data assert "irc.example.org" = message.server """ - @spec parse(raw_data :: charlist) :: IrcMessage.t + + @spec parse(raw_data :: charlist) :: ExIRC.Message.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})) + get_cmd(rest, parse_from(from, %ExIRC.Message{ctcp: false})) data -> - get_cmd(:string.tokens(data, ' '), %IrcMessage{ctcp: false}) + get_cmd(:string.tokens(data, ' '), %ExIRC.Message{ctcp: false}) end end @prefix_pattern ~r/^(?[^!\s]+)(?:!(?:(?[^@\s]+)@)?(?:(?[\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(fn(s) -> case String.valid?(s) do true -> :unicode.characters_to_binary(s) false -> :unicode.characters_to_binary(s, :latin1, :unicode) end end) 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 + defp post_process(%ExIRC.Message{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 + @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_charlist/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 + 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_charlist(d)]), ' ', :io_lib.format("~2..0s", [Integer.to_charlist(h)]), ':', :io_lib.format("~2..0s", [Integer.to_charlist(n)]), ':', :io_lib.format("~2..0s", [Integer.to_charlist(s)]), ' ', Integer.to_charlist(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/lib/exirc/whois.ex b/lib/exirc/whois.ex index fb14e63..3970214 100644 --- a/lib/exirc/whois.ex +++ b/lib/exirc/whois.ex @@ -1,18 +1,19 @@ -defmodule Irc.Whois do +defmodule ExIRC.Whois do + defstruct [account_name: nil, channels: [], helpop?: false, hostname: nil, idling_time: 0, ircop?: false, nickname: nil, realname: nil, registered_nick?: false, server_address: nil, server_name: nil, signon_time: 0, tls?: false, username: nil, ] end diff --git a/mix.exs b/mix.exs index 63f24a6..00a1cb2 100644 --- a/mix.exs +++ b/mix.exs @@ -1,36 +1,36 @@ -defmodule ExIrc.Mixfile do +defmodule ExIRC.Mixfile do use Mix.Project def project do [app: :exirc, version: "1.0.1", elixir: "~> 1.0", description: "An IRC client library for Elixir.", package: package(), test_coverage: [tool: ExCoveralls], preferred_cli_env: ["coveralls": :test, "coveralls.detail": :test, "coveralls.html": :test, "coveralls.post": :test], deps: deps()] end # Configuration for the OTP application def application do - [mod: {ExIrc.App, []}, + [mod: {ExIRC.App, []}, applications: [:ssl, :crypto, :inets]] end defp package do [ files: ["lib", "mix.exs", "README.md", "LICENSE"], maintainers: ["Paul Schoenfelder"], licenses: ["MIT"], links: %{ "GitHub" => "https://github.com/bitwalker/exirc", "Home Page" => "http://bitwalker.org/exirc"} ] end defp deps do [ {:ex_doc, "~> 0.14", only: :dev}, {:excoveralls, "~> 0.5", only: :test}, ] end end diff --git a/test/channels_test.exs b/test/channels_test.exs index a1549ca..b29551b 100644 --- a/test/channels_test.exs +++ b/test/channels_test.exs @@ -1,154 +1,154 @@ -defmodule ExIrc.ChannelsTest do +defmodule ExIRC.ChannelsTest do use ExUnit.Case, async: true - alias ExIrc.Channels, as: Channels + 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.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.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.channel_type(channels, "#testchannel") channels = Channels.set_type(channels, "#testchannel", "*") assert :private == Channels.channel_type(channels, "#testchannel") channels = Channels.set_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.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.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.channel_has_user?(channels, "#testchannel", "testnick") assert Channels.channel_has_user?(channels, "#testchannel", "anothernick") end test "Strips rank designations from nicks" do channels = Channels.init() |> Channels.join("#testchannel") |> Channels.users_join("#testchannel", ["+testnick", "@anothernick", "&athirdnick", "%somanynicks", "~onemorenick"]) assert Channels.channel_has_user?(channels, "#testchannel", "testnick") assert Channels.channel_has_user?(channels, "#testchannel", "anothernick") assert Channels.channel_has_user?(channels, "#testchannel", "athirdnick") assert Channels.channel_has_user?(channels, "#testchannel", "somanynicks") assert Channels.channel_has_user?(channels, "#testchannel", "onemorenick") 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.channel_has_user?(channels, "#testchannel", "testnick") channels = Channels.init() |> Channels.users_join("#testchannel", ["testnick", "anothernick"]) 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.channel_has_user?(channels, "#testchannel", "testnick") channels = channels |> Channels.user_part("#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.channel_has_user?(channels, "#testchannel", "testnick") end test "Can quit a user from all channels" do channels = Channels.init() |> Channels.join("#testchannel") |> Channels.user_join("#testchannel", "testnick") |> Channels.join("#anotherchannel") |> Channels.user_join("#anotherchannel", "testnick") |> Channels.user_join("#anotherchannel", "secondnick") assert Channels.channel_has_user?(channels, "#testchannel", "testnick") channels = channels |> Channels.user_quit("testnick") refute Channels.channel_has_user?(channels, "#testchannel", "testnick") refute Channels.channel_has_user?(channels, "#anotherchannel", "testnick") assert Channels.channel_has_user?(channels, "#anotherchannel", "secondnick") 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.channel_has_user?(channels, "#testchannel", "testnick") assert Channels.channel_has_user?(channels, "#anotherchan", "testnick") channels = Channels.user_rename(channels, "testnick", "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.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 diff --git a/test/client_test.exs b/test/client_test.exs index b7277b6..1788dd3 100644 --- a/test/client_test.exs +++ b/test/client_test.exs @@ -1,30 +1,30 @@ -defmodule ExIrc.ClientTest do +defmodule ExIRC.ClientTest do use ExUnit.Case test "start multiple clients" do - assert {:ok, pid} = ExIrc.start_client! - assert {:ok, pid2} = ExIrc.start_client! + assert {:ok, pid} = ExIRC.start_client! + assert {:ok, pid2} = ExIRC.start_client! assert pid != pid2 end test "client dies if owner process dies" do test_pid = self() pid = spawn_link(fn -> - assert {:ok, pid} = ExIrc.start_client! + assert {:ok, pid} = ExIRC.start_client! send(test_pid, {:client, pid}) receive do :stop -> :ok end end) client_pid = receive do {:client, pid} -> pid end assert Process.alive?(client_pid) send(pid, :stop) :timer.sleep(1) refute Process.alive?(client_pid) end end diff --git a/test/commands_test.exs b/test/commands_test.exs index e9bdaef..38c2e90 100644 --- a/test/commands_test.exs +++ b/test/commands_test.exs @@ -1,48 +1,48 @@ -defmodule ExIrc.CommandsTest do +defmodule ExIRC.CommandsTest do use ExUnit.Case, async: true - use Irc.Commands + use ExIRC.Commands test "Commands are formatted properly" do expected = <<0o001, "TESTCMD", 0o001, ?\r, ?\n>> assert expected == ctcp!("TESTCMD") |> IO.iodata_to_binary expected = <<"PRIVMSG #testchan :", 0o001, "ACTION mind explodes!!", 0o001, ?\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 +end diff --git a/test/utils_test.exs b/test/utils_test.exs index 3beb447..0370a82 100644 --- a/test/utils_test.exs +++ b/test/utils_test.exs @@ -1,209 +1,209 @@ -defmodule ExIrc.UtilsTest do +defmodule ExIRC.UtilsTest do use ExUnit.Case, async: true - use Irc.Commands + use ExIRC.Commands - alias ExIrc.Utils, as: Utils - alias ExIrc.Client.ClientState, as: ClientState + alias ExIRC.Utils, as: Utils + alias ExIRC.Client.ClientState, as: ClientState - doctest ExIrc.Utils + 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{ + expected = %ExIRC.Message{ 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{ + expected = %ExIRC.Message{ 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{ + expected = %ExIRC.Message{ 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{ + assert %ExIRC.Message{ :nick => "pschoenf", :cmd => "INVITE", :args => ["testuser", "#awesomechan"] } = Utils.parse(message) end test "Parse KICK message" do message = ':pschoenf KICK #testchan lameuser' - assert %IrcMessage{ + assert %ExIRC.Message{ :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{ + assert %ExIRC.Message{ 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{ + assert %ExIRC.Message{ 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{ + assert %ExIRC.Message{ 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{ + assert %ExIRC.Message{ 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{ + assert %ExIRC.Message{ 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{ + assert %ExIRC.Message{ 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{ + assert %ExIRC.Message{ 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{ + assert %ExIRC.Message{ 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{ + assert %ExIRC.Message{ 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{ + assert %ExIRC.Message{ args: ["#bar", "Ĥélłø 차"], cmd: "PRIVMSG", ctcp: false, host: "172.17.0.1", nick: "foo", server: [], user: "~user" } = Utils.parse(message) end test "Can parse latin1" 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, 233, 233, 233, 13, 10] - assert %IrcMessage{ + assert %ExIRC.Message{ args: ["#bar", "ééé"], cmd: "PRIVMSG", ctcp: false, host: "172.17.0.1", nick: "foo", server: [], user: "~user" } = Utils.parse(message) end end