diff --git a/README.md b/README.md index f517cdf..04eea27 100644 --- a/README.md +++ b/README.md @@ -1,178 +1,178 @@ # 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 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: ```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 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. 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_client!() + {:ok, client} = ExIrc.start_link!() {:ok, handler} = ExampleHandler.start_link(nil) # 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!" {: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 :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_client! + {: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 {: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 {: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 {: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) {: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/bot/lib/bot.ex b/examples/bot/lib/bot.ex index 46dd6b6..85d3ffc 100644 --- a/examples/bot/lib/bot.ex +++ b/examples/bot/lib/bot.ex @@ -1,107 +1,107 @@ 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 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_client!() + {:ok, client} = ExIrc.start_link!() # 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 #{server}:#{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/lib/exirc/exirc.ex b/lib/exirc/exirc.ex index b32af54..c36e6e4 100644 --- a/lib/exirc/exirc.ex +++ b/lib/exirc/exirc.ex @@ -1,68 +1,75 @@ 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 a new IRC client {:ok, client} = ExIrc.start_client! # Connect to an IRC server ExIrc.Client.connect! client, "localhost", 6667 # Logon ExIrc.Client.logon client, "password", "nick", "user", "name" # Join a channel (password is optional) ExIrc.Client.join client, "#channel", "password" # Send a message ExIrc.Client.msg client, :privmsg, "#channel", "Hello world!" # Quit (message is optional) ExIrc.Client.quit client, "message" # Stop and close the client connection ExIrc.Client.stop! client """ use Supervisor import Supervisor.Spec ############## # Public API ############## @doc """ 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 + 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 + """ + def start_link! do + ExIrc.Client.start!([owner: self()]) + end + ############## # Supervisor API ############## @spec init(any) :: {:ok, pid} | {:error, term} def init(_) do children = [ worker(ExIrc.Client, [], restart: :temporary) ] supervise children, strategy: :simple_one_for_one end end diff --git a/mix.lock b/mix.lock index 83b9d0f..01b6bab 100644 --- a/mix.lock +++ b/mix.lock @@ -1,11 +1,11 @@ %{"certifi": {:hex, :certifi, "0.4.0", "a7966efb868b179023618d29a407548f70c52466bf1849b9e8ebd0e34b7ea11f", [:rebar3], []}, "earmark": {:hex, :earmark, "1.0.2", "a0b0904d74ecc14da8bd2e6e0248e1a409a2bc91aade75fcf428125603de3853", [:mix], []}, "ex_doc": {:hex, :ex_doc, "0.14.3", "e61cec6cf9731d7d23d254266ab06ac1decbb7651c3d1568402ec535d387b6f7", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}, "excoveralls": {:hex, :excoveralls, "0.5.7", "5d26e4a7cdf08294217594a1b0643636accc2ad30e984d62f1d166f70629ff50", [:mix], [{:exjsx, "~> 3.0", [hex: :exjsx, optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, optional: false]}]}, "exjsx": {:hex, :exjsx, "3.2.1", "1bc5bf1e4fd249104178f0885030bcd75a4526f4d2a1e976f4b428d347614f0f", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, optional: false]}]}, "hackney": {:hex, :hackney, "1.6.1", "ddd22d42db2b50e6a155439c8811b8f6df61a4395de10509714ad2751c6da817", [:rebar3], [{:certifi, "0.4.0", [hex: :certifi, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.0", [hex: :ssl_verify_fun, optional: false]}]}, "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []}, "jsx": {:hex, :jsx, "2.8.0", "749bec6d205c694ae1786d62cea6cc45a390437e24835fd16d12d74f07097727", [:mix, :rebar], []}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.0", "edee20847c42e379bf91261db474ffbe373f8acb56e9079acb6038d4e0bf414f", [:rebar, :make], []}} + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.0", "edee20847c42e379bf91261db474ffbe373f8acb56e9079acb6038d4e0bf414f", [:make, :rebar], []}}