diff options
Diffstat (limited to 'lib/irc/base_client.ex')
-rw-r--r-- | lib/irc/base_client.ex | 170 |
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 |