summaryrefslogtreecommitdiff
path: root/lib/irc/base_client.ex
diff options
context:
space:
mode:
Diffstat (limited to 'lib/irc/base_client.ex')
-rw-r--r--lib/irc/base_client.ex170
1 files changed, 170 insertions, 0 deletions
diff --git a/lib/irc/base_client.ex b/lib/irc/base_client.ex
new file mode 100644
index 0000000..8da45be
--- /dev/null
+++ b/lib/irc/base_client.ex
@@ -0,0 +1,170 @@
+defmodule Irc.BaseClient do
+ @behaviour :gen_statem
+ require Logger
+ alias Irc.Parser.Line
+ alias Irc.Connection
+ require Irc.Parser.Numeric
+ import Irc.Parser.Numeric
+
+ @moduledoc """
+ Extensible fully-featured IRC client.
+
+ ## Behaviour
+
+ The BaseClient requires a callback module. You can check out `Irc.Client`.
+
+ ## Capabs
+
+ IRCv3 Capabs are handled by separates modules.
+ """
+
+ @internal {:next_event, :internal, nil}
+
+ defstruct [:conn, :conn_mon, :module, :modstate, :args, :nick, :modes, :info, :error]
+
+ def callback_mode, do: [:state_functions, :state_enter]
+
+ @type event ::event_connected | event_nick | event_modes | event_line | event_down
+
+ @type event_connected :: {:connected, Connection.Info.t}
+ @type event_nick :: {:nick, prev_nick :: String.t, new_nick :: String.t}
+ @type event_modes :: {:modes, changes :: String.t, prev :: list, modes :: list}
+ @type event_line :: {:line, line :: Irc.Parser.Line.t}
+ @type event_down :: {:down, Connection.error, delay :: integer}
+
+ @type modstate :: any
+ @type handle_return :: {:ok, modstate()}
+ @type handle_call_return :: {:reply, any(), modstate()} | {:noreply, modstate()}
+
+ @callback init(Keyword.t) :: {:ok, modstate} | any
+ @callback handle_event(event(), modstate()) :: handle_return()
+ @callback handle_info(any(), modstate()) :: handle_return()
+ @callback handle_cast(any(), modstate()) :: handle_return()
+ @callback handle_call(any(), reference(), modstate()) :: handle_call_return()
+ @callback stop(Connection.error, modstate) :: any()
+
+ @type start_opt :: Connection.start_opt
+ | {:module, module()}
+ | {:conn_statem_opts, [:gen_statem.start_opt]}
+ @type start_ret :: {:ok, pid} | {:error, Connection.error} | :gen_statem.start_ret
+
+ @spec start(Connection.nick, Connection.host, [start_opt], [:gen_statem.start_opt]) :: start_ret
+ def start(nick, host, opts \\ [], start_opts \\ []) do
+ :gen_statem.start(__MODULE__, [nick, host, prepare_args(opts)], start_opts)
+ end
+
+ @spec start_link(Connection.nick, Connection.host, [start_opt], [:gen_statem.start_opt]) :: start_ret
+ def start_link(nick, host, opts \\ [], start_opts \\ []) do
+ :gen_statem.start_link(__MODULE__, [nick, host, prepare_args(opts)], start_opts)
+ end
+
+
+ @doc false
+ def init([args]) do
+ capabs = ["account-notify", "away-notify"]
+ args = args
+ |> Keyword.put(:capabs, capabs)
+ with \
+ {:ok, conn} <- Connection.start(Keyword.get(args, :nick), Keyword.get(args, :host), args, Keyword.get(args, :conn_statem_opts, [])),
+ conn_mon <- Process.monitor(conn),
+ {:ok, module} <- Keyword.fetch(args, :module),
+ {:ok, modstate} <- module.init(args)
+ do
+ data = %__MODULE__{
+ conn: conn, conn_mon: conn_mon,
+ module: module, modstate: modstate,
+ args: args
+ }
+ {:ok, :disconnected, data}
+ else
+ error -> {:stop, error}
+ end
+ end
+
+ def disconnected(:enter, :connected, data) do
+ Logger.debug "#{inspect data} disconnected: #{inspect data.error}"
+ :keep_state_and_data
+ end
+
+ def disconnected(:info, msg = {:irc_conn_up, _, info}, data) do
+ data = run_handler_event({:connected, info}, data)
+ {:next_state, :connected, %__MODULE__{data | nick: info.nick, info: info, modes: info.modes, error: nil}}
+ end
+
+ def disconnected(type, content, data) do
+ handle_common(:disconnected, type, content, data)
+ end
+
+ def connected(:enter, _, data) do
+ Logger.debug "#{inspect data} UP"
+ :keep_state_and_data
+ end
+
+ def connected(:info, msg = {:irc_conn_nick, _, prev_nick, new_nick}, data) do
+ data = run_handler_event({:nick, prev_nick, new_nick}, data)
+ {:keep_state, %__MODULE__{data | nick: new_nick}}
+ end
+
+ def connected(:info, msg = {:irc_conn_modes, _, changes, prev, modes}, data) do
+ data = run_handler_event({:modes, changes, prev, modes}, data)
+ {:keep_state, %__MODULE__{data | modes: modes}}
+ end
+
+ def connected(:info, {:irc_conn_down, _, reason, delay}, data) do
+ data = run_handler_event({:down, reason, delay}, data)
+ {:next_state, :disconnected, %__MODULE__{data | error: reason}}
+ end
+
+ # TODO: REMOVE?
+ def connected(:info, {:irc_conn_line, _, line}, data) do
+ data = run_handler_event({:line, line}, data)
+ :keep_state_and_data
+ end
+
+ def connected(type, content, data) do
+ handle_common(:connected, type, content, data)
+ end
+
+ # TODO: Callback stop?
+ def error(:internal, reason, data) do
+ Logger.error "#{inspect data}: #{inspect reason}"
+ data.module.stop(reason, data.modstate)
+ {:stop, :normal}
+ end
+
+ def handle_common(_, :info, {:irc_conn_error, _, reason}, data) do
+ {:next_state, :error, data, {:next_event, :internal, reason}}
+ end
+
+ def handle_common(_, :enter, _, _) do
+ :keep_state_and_data
+ end
+
+ def terminate(reason, state, data) do
+ Logger.error("#{inspect(data)} terminating in #{inspect(state)}: #{inspect reason}")
+ end
+
+ def code_change(_old_vsn, old_state, old_data, _extra) do
+ {:ok, old_state, old_data}
+ end
+
+ defp prepare_args(args) do
+ args
+ end
+
+ defp run_handler_event(event, data) do
+ case data.module.handle_event(event, data.modstate) do
+ {:ok, modstate} -> %__MODULE__{data | modstate: modstate}
+ :ok -> data
+ end
+ end
+
+ defimpl Inspect, for: __MODULE__ do
+ import Inspect.Algebra
+ def inspect(struct, _opts) do
+ concat(["#Irc.Client<", inspect(self()), ">"])
+ end
+ end
+
+
+end