summaryrefslogtreecommitdiff
path: root/lib/irc/client.ex
diff options
context:
space:
mode:
Diffstat (limited to 'lib/irc/client.ex')
-rw-r--r--lib/irc/client.ex265
1 files changed, 243 insertions, 22 deletions
diff --git a/lib/irc/client.ex b/lib/irc/client.ex
index 1c4b0c5..d8a7b77 100644
--- a/lib/irc/client.ex
+++ b/lib/irc/client.ex
@@ -1,48 +1,269 @@
defmodule Irc.Client do
- alias Irc.BaseClient
- @behaviour Irc.BaseClient
+ @behaviour :gen_statem
require Logger
+ alias Irc.Parser.Line
+ alias Irc.Connection
+ require Irc.Parser.Numeric
+ import Irc.Parser.Numeric
@moduledoc """
- Fully-featured IRC client.
+ Fully-featured IRC Client.
- The client uses message passing.
+ Events emanating from the client are sent to the owner PID, `t:message/0`, and commands can be sent using `command/3`. The response is asynchronous.
+ If a command is sent when the client is disconnected, it will be postponed until the client is connected again.
+
+ Known IRC users are cached into `Irc.Client.UserCache`. Depending on the connection capabilities, data returned by the cache may not be up-to-date, and can be retreived with `whois/2`.
+
+ ## Extending
+
+ Most of the commands and capabs are implemented in separate modules. Additional modules can be set in the start options. See `Irc.Client.Capabs`.
"""
- @type message :: Irc.BaseClient.event
- @type start_ret :: BaseClient.start_ret
+ @commands [
+ Irc.Client.Command.Join,
+ Irc.Client.Command.Who,
+ Irc.Client.Command.Names,
+ Irc.Client.Command.Away,
+ Irc.Client.Command.Chghost,
+ Irc.Client.Command.Account,
+ ]
+
+ @internal {:next_event, :internal, nil}
+
+ defstruct [:conn, :conn_mon, :module, :modstate, :args, :nick, :modes, :info, :error, :commands, :buffer]
+
+ def callback_mode, do: [:state_enter]
+
+ @type message :: {:irc_client, pid(), event}
+
+ @type event :: event_connected | event_nick | event_modes | event_line | event_down
+ | Irc.Client.Command.Join.t
+ | Irc.Client.Command.Names.t
+ | Irc.Client.Command.Who.t
+ | Irc.Client.Command.Away.t
+ | Irc.Client.Command.Chghost.t
+ | Irc.Client.Command.AccountNotify.t
- @spec start(Irc.Connection.nick, Irc.Connection.host, [BaseClient.start_opt], [:gen_statem.start_opt]) :: start_ret
+ @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 start_opt :: Connection.start_opt
+ | {:module, module() | Irc.Client}
+ | {:commands, [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(Irc.Connection.nick, Irc.Connection.host, [BaseClient.start_opt], [:gen_statem.start_opt]) :: start_ret
+ @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(_) do
- Logger.debug("CLIENT initiated")
- {:ok, %{process: nil}}
+ def command(pid, command, args) do
+ ref = make_ref()
+ Process.send(pid, {:irc_command, command, ref, args})
+ ref
end
- @doc false
- def handle_event(event, state) do
- Logger.debug("CLIENT EVENT: #{inspect event}")
- :ok
+ @doc "Whois an user. Depending on the connection capabilities and cache contents, a `WHOIS` irc command may not be executed; unless `cached: false`."
+ @type whois_opt :: {:cached, true | false}
+ @spec whois(pid(), String.t, Keyword.t) :: Irc.User.t()
+ def whois(pid, nick, opts \\ []) do
+ end
+
+ def call_command(pid, command, args, opts \\ []) do
+ timeout = Keyword.get(opts, :timeout, 10_000)
+ ref = command(pid, command, args)
+ receive do
+ {:irc_command, command, result} -> result
+ after
+ timeout -> {:error, :timeout}
+ end
end
@doc false
- def stop(reason, state) do
- Logger.debug("CLIENT: Stopping, #{inspect reason}")
+ def init([args]) do
+ with \
+ commands <- init_commands(args),
+ capabs <- Enum.uniq(commands.capabs ++ Keyword.get(args, :capabs, [])),
+ args <- Keyword.put(args, :capabs, capabs),
+ {:ok, conn} <- Connection.start(Keyword.get(args, :nick), Keyword.get(args, :host), args, Keyword.get(args, :conn_statem_opts, [])),
+ conn_mon <- Process.monitor(conn)
+ do
+ Logger.info("Commands registered: #{inspect commands}")
+ data = %__MODULE__{
+ conn: conn, conn_mon: conn_mon,
+ args: args,
+ commands: commands
+ }
+ {:ok, :disconnected, data}
+ else
+ error -> {:stop, error}
+ end
end
- defp prepare_args(opts) do
- opts
- |> Keyword.put_new(:module, __MODULE__)
+ def handle_event(:enter, :connected, :disconnected, data) do
+ Logger.debug "#{inspect data} disconnected: #{inspect data.error}"
+ :keep_state_and_data
end
-end
+ def handle_event(:info, msg = {:irc_conn_up, _, info}, :disconnected, 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
+
+ #
+ # --- Connected
+ #
+
+ def handle_event(:enter, _, :connected, data) do
+ Logger.debug "#{inspect data} UP"
+ :keep_state_and_data
+ end
+
+ def handle_event(:info, msg = {:irc_conn_nick, _, prev_nick, new_nick}, :connected, data) do
+ data = run_handler_event({:nick, prev_nick, new_nick}, data)
+ {:keep_state, %__MODULE__{data | nick: new_nick}}
+ end
+
+ def handle_event(:info, msg = {:irc_conn_modes, _, changes, prev, modes}, :connected, data) do
+ data = run_handler_event({:modes, changes, prev, modes}, data)
+ {:keep_state, %__MODULE__{data | modes: modes}}
+ end
+
+ def handle_event(:info, {:irc_conn_down, _, reason, delay}, :connected, data) do
+ data = run_handler_event({:down, reason, delay}, data)
+ {:next_state, :disconnected, %__MODULE__{data | error: reason}}
+ end
+
+ def handle_event(:info, {:irc_command, command, args}, :connected, data) do
+ if module = get_in(data, [:commands, :user, command]) do
+ {next, actions} = case module.handle_command(command, args, data.info) do
+ {next, next_arg} -> {{next, next_arg}, []}
+ {next, next_arg, actions} -> {{next, next_arg}, List.wrap(actions)}
+ next when is_atom(next) -> {{next, nil}, []}
+ end
+
+ data = handle_actions(actions, data)
+
+ case next do
+ {:buffer, acc} -> {:next_state, {:buffer, module}, %__MODULE__{data | buffer: acc}}
+ end
+ else
+ data = run_handler_event({:irc_command, command, {:error, :unsupported_command, command}}, data) # MEH?
+ {:keep_state, data}
+ end
+ end
+
+ # DOES NOT HANDLE ERROR (actions can just return data now)
+ defp handle_actions([{:send, line} | rest], data) do
+ case Connection.sendline(data.conn, line) do
+ :ok -> handle_actions(rest, data)
+ error -> {:next_state, :disconnected, %__MODULE__{data | error: error}}
+ end
+ data
+ end
+ defp handle_actions([{:event, event} | rest], data) do
+ run_handler_event(event, data)
+ end
+ defp handle_actions([], data), do: data
+
+ # TODO: REMOVE?
+ def handle_event(:info, {:irc_conn_line, _, line}, :connected, data) do
+ data = run_handler_event({:line, line}, data)
+ :keep_state_and_data
+ end
+
+ #
+ # --- Buffer
+ #
+
+ def handle_event(:info, {:irc_conn_line, _, line}, {:buffer, module}, data) do
+ {next, actions} = case module.handle_buffer(line, data.buffer, data.info) do
+ {next, next_arg} -> {{next, next_arg}, []}
+ {next, next_arg, actions} -> {{next, next_arg}, List.wrap(actions)}
+ next when is_atom(next) -> {{next, nil}, []}
+ end
+
+ data = handle_actions(actions, data)
+
+ case next do
+ {:finish, _} -> {:next_state, :connected, %__MODULE__{data | buffer: nil}}
+ {:buffer, acc} -> {:keep_state, %__MODULE__{data | buffer: acc}}
+ end
+ end
+
+ # TODO: Callback stop?
+ def handle_event(:internal, reason, :error, data) do
+ Logger.error "#{inspect data}: #{inspect reason}"
+ data.module.stop(reason, data.modstate)
+ {:stop, :normal}
+ end
+
+ def handle_event(:info, {:irc_conn_error, _, reason}, _, data) do
+ {:next_state, :error, data, {:next_event, :internal, reason}}
+ end
+
+ def handle_event(: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
+
+ defp init_commands(args) do
+ commands = @commands ++ Keyword.get(args, :commands, [])
+ Enum.reduce(commands, %{server: %{}, user: %{}, capabs: []}, fn(module, acc) ->
+ {server_commands, user_commands, capabs} = module.init()
+
+ get_value = fn(option) ->
+ case option do
+ str when is_binary(str) -> [str]
+ nil -> []
+ list when is_list(list) -> list
+ end
+ end
+
+ reduce_fun = fn(command, acc) -> Map.put(acc, command, module) end
+
+ server = Enum.reduce(List.wrap(server_commands), acc.server, reduce_fun)
+ user = Enum.reduce(List.wrap(user_commands), acc.user, reduce_fun)
+ capabs = List.concat(acc.capabs, List.wrap(capabs))
+ %{server: server, user: user, capabs: capabs}
+ end)
+ end
+
+ defimpl Inspect, for: __MODULE__ do
+ import Inspect.Algebra
+ def inspect(struct, _opts) do
+ concat(["#Irc.Client<", inspect(self()), ">"])
+ end
+ end
+
+
+end