summaryrefslogtreecommitdiff
path: root/lib/irc/connection.ex
diff options
context:
space:
mode:
authorhref <href@random.sh>2021-01-11 15:53:02 +0100
committerhref <href@random.sh>2021-01-11 15:53:02 +0100
commit0f3f0e035b43eabd3f739c41964446962cf54208 (patch)
tree9279c54e100c92375c9d980e2031e0a153245025 /lib/irc/connection.ex
parentSome fixes (diff)
Cont. wipmaster
Diffstat (limited to 'lib/irc/connection.ex')
-rw-r--r--lib/irc/connection.ex313
1 files changed, 215 insertions, 98 deletions
diff --git a/lib/irc/connection.ex b/lib/irc/connection.ex
index 23eee3f..6df8773 100644
--- a/lib/irc/connection.ex
+++ b/lib/irc/connection.ex
@@ -2,7 +2,7 @@ defmodule Irc.Connection do
@behaviour :gen_statem
require Logger
alias Irc.ConnectionSocket
- alias Irc.Parser.Line
+ alias Irc.Line
alias Irc.Parser
require Irc.Parser.Numeric
import Irc.Parser.Numeric
@@ -15,6 +15,7 @@ defmodule Irc.Connection do
@registration_timeout :timer.seconds(30)
@connection_capabs ["sasl", "cap-notify"]
@internal {:next_event, :internal, nil}
+ @default_handler Irc.Connection.MsgHandler
@moduledoc """
Lightweight implementation of an IRC Connection handling only the basics:
@@ -27,26 +28,34 @@ defmodule Irc.Connection do
* nick and user mode changes,
* batches/echo.
- Events and lines are sent to calling PID.
+ An implementation of `Irc.Connection.Handler` should be provided to interact with the connection.
+
+ By default, the `Irc.Connection.MsgHandler` is used, which sends events to the owner process.
## Usage
- The client is started using `start/4` or `start_link/4`, supplying a nickname, IRC host, and options `t:start_opt/0`:
+ The connection is started using `start/4` or `start_link/4`, supplying a nickname, IRC host, and options `t:start_opt/0`:
```elixir
- {:ok, Pid} = Irc.Connection.start("ExIrcTest", "irc.random.sh", [tls: true, port: 6697], [])
+ {:ok, pid} = Irc.Connection.start("ExIrcTest", "irc.random.sh", [tls: true, port: 6697], [])
```
- Messages will be sent to your PID, see `t:message/0` for the possible types.
+ This will start a connection using the default handler: events will be sent to your PID, see `t:Irc.Connection.MsgHandler.message/0` for the possible types.
You can send lines using `sendline/3`, and disconnect with `disconnect/3`.
Status and information about the connection (`t:t/0`) can be requested with `info/1`.
- If you are looking for a more featured client, you probably want `Irc.Client`!
+ To start a connection with a custom handler, you need to set the `handler` and `handler_opts` start options:
+
+ ```elixir
+ {:ok, pid} = Irc.Connection.start("ExIrcTest", "irc.random.sh", [tls: true, port: 6697, handler: MyHandler, handler_opts: %{:something => true}], [])
+ ```
+
+ If you are looking for a more featured client: you probably want `Irc.Client`.
"""
- defstruct [:server, :host, :port, :tls, :sts, :nick, :modes, :capabs, :server_capabs, :version, :umodes, :cmodes, :cmodes_with_params, :isupport, :motd]
+ defstruct [:pid, :server, :host, :port, :tls, :sts, :nick, :modes, :capabs, :server_capabs, :version, :umodes, :cmodes, :cmodes_with_params, :isupport, :motd, :assigns, :__private__]
@typedoc """
Connection Information state. Retrieve it with `info/1`.
@@ -82,35 +91,9 @@ defmodule Irc.Connection do
cmodes_with_params: String.t,
isupport: Irc.Parser.Isupport.t(),
motd: [String.t],
+ assigns: Map.t()
}
- @typedoc "Messages sent to the owning process."
- @type message :: msg_up() | msg_down() | msg_error()
- | msg_line() | msg_batch() | msg_umodes() | msg_nick()
-
- @typedoc "Connection is now up and registered."
- @type msg_up :: {:irc_conn_up, pid(), t()}
- @typedoc "Message: Received an IRC line."
- @type msg_line :: {:irc_conn_line, pid(), Line.t}
- @typedoc "Message: Line batch (only if enabled, see `batch` in `t:start_opt/0`)."
- @type msg_batch :: {:irc_conn_batch, pid(), type :: String.t(), type_params :: [String.t()], lines :: [Line.t]}
- @typedoc "Message: Connection user modes have changed."
- @type msg_umodes :: {:irc_conn_modes, pid(), changes :: String.t, previous_modes :: list(), modes :: list()}
- @typedoc "Message: Connection nickname have changed."
- @type msg_nick :: {:irc_conn_nick, pid(), previous_nick :: String.t, new_nick :: String.t}
- @typedoc "Message: Connection is down and will try to reconnect in `delay`."
- @type msg_down :: {:irc_conn_down, pid(), error, delay :: integer}
- @typedoc "Message: Connection encountered a fatal error and stopped."
- @type msg_error :: {:irc_conn_error, pid(), error}
-
- @typedoc "Connection error reasons"
- @type error :: {:error, error_irc | any}
-
- @typedoc "IRC Error reasons"
- @type error_irc :: {:killed, by :: String.t, reason :: String.t}
- | {:quit, reason :: String.t}
- | {:irc_error, reason :: String.t}
-
# TODO: Better batch / Echo-message / message id
# TODO: CAP lifecycle while connection is established
# TODO: SASL
@@ -131,7 +114,8 @@ defmodule Irc.Connection do
* [webirc][webirc] options, see `t:start_opt_webirc/0`,
* capabs: list to request,
* connect options (either `t:gen_tcp.connect_option/0` or `t:ssl.start_option/0`, depending on the value of the `tls`),
- * owner process.
+ * owner process,
+ * handler_module and handler_args.
"""
@type start_opt :: {:user, nil | String.t}
@@ -148,6 +132,8 @@ defmodule Irc.Connection do
| {:capabs, [String.t()]}
| {:connect_opts, [:gen_tcp.connect_option | :ssl.connect_option]}
| {:process, pid()}
+ | {:handler, Irc.Connection.MsgHandler | module()}
+ | {:handler_args, %{}}
@typedoc """
IRCv3 [Strict Transport Security]()
@@ -161,7 +147,7 @@ defmodule Irc.Connection do
[sasl]: https://ircv3.net/specs/extensions/sasl-3.1.html
"""
- @type start_opt_sasl :: %{mechanism :: String.t => [mechanism_param() :: String.t]}
+ @type start_opt_sasl :: %{mechanism :: String.t => [mechanism_param :: String.t]}
@typedoc """
IRCv3 [Batch][batch] support.
@@ -301,17 +287,20 @@ defmodule Irc.Connection do
defmodule State do
@moduledoc false
- defstruct [:args,
- :backoff, :socket,
- :process, :monitor,
- :nick,
- :server,
- :version,
- :umodes,
- :cmodes,
- :cmodes_with_params,
- :welcomed,
- :tls,
+ defstruct [
+ :args, # Start arguments
+ :backoff, # Backoff state
+ :socket, # ConnectionSocket
+ :process, # Owner PID
+ :monitor, # Owner monitor
+ :nick, # Current nickname
+ :server, # Server name
+ :version, # Server version
+ :umodes, # Server umodes
+ :cmodes, # Server cmodes
+ :cmodes_with_params, # Server parametized cmodes
+ :welcomed, # Registration
+ :tls, # Connected with TLS
:cap_302, # If server supports 302
:sts, # If an STS policy has been used
:port, # Actual connection port (may differ than args.port due to STS)
@@ -323,7 +312,9 @@ defmodule Irc.Connection do
{:client_capabs, []}, # Client enabled capabs
{:capabs, []}, # Negotiated capabs
{:motd, []},
- {:welcome, []}, # Registration log
+ {:welcome, []}, # Registration log,
+ {:assigns, %{}},
+ {:__private__, %{}}
]
end
@@ -331,32 +322,34 @@ defmodule Irc.Connection do
def callback_mode, do: [:state_functions, :state_enter]
@doc false
- def init([args]) do
- backoff_min = Map.get(args, :backoff_min, @backoff_min)
- backoff_max = Map.get(args, :backoff_max, @backoff_max)
- process = Map.get(args, :process, nil)
+ def init([start_args]) do
+ process = Map.get(start_args, :process, nil)
monitor = Process.monitor(process)
with \
- {:ok, args} <- process_args(args),
- backoff <- :backoff.init(backoff_min, backoff_max)
+ backoff <- init_backoff(start_args),
+ {:ok, args} <- process_args(start_args),
+ {tls, sts, port} <- init_sts(args),
+ {:ok, handler_args} <- handler_init(args),
+ {:ok, handler_capabs} <- handler_capabs(args, handler_args)
do
- {tls, sts, port} = case {args.sts, Irc.STS.lookup(args.host)} do
- {true, {:ok, port}} -> {true, true, port}
- _ -> {args.tls, false, args.port}
- end
- client_capabs = [Irc.Parser.capabs(),
- @connection_capabs,
- if(args.batch, do: ["batch"]),
- if(args.sasl, do: ["sasl"]),
- if(args.echo, do: ["echo-message"]),
- args.capabs,
- ]
- |> Enum.filter(fn(i) -> i end)
- |> List.flatten()
- |> Enum.uniq()
+ client_capabs = [
+ Irc.Parser.capabs(),
+ @connection_capabs,
+ if(args.batch, do: ["batch"]),
+ if(args.sasl, do: ["sasl"]),
+ if(args.echo, do: ["echo-message"]),
+ args.capabs,
+ handler_capabs
+ ]
+ |> Enum.filter(fn(i) -> i end)
+ |> List.flatten()
+ |> Enum.uniq()
+
data = %State{
args: args,
- sts: sts, port: port, tls: tls,
+ sts: sts,
+ port: port,
+ tls: tls,
client_capabs: client_capabs,
process: process,
monitor: monitor,
@@ -375,7 +368,7 @@ defmodule Irc.Connection do
def connect(:internal, _, data) do
Logger.debug("#{inspect(data)} connecting...")
options = [{:active, true}, {:packet, :line}] ++ data.args.connect_opts
- case ConnectionSocket.connect(data.tls, data.args.host, data.port, options) do
+ case ConnectionSocket.connect(data.tls, data.args.host, data.port, options, __MODULE__) do
{:ok, socket} ->
{_, backoff} = :backoff.succeed(data.backoff)
{:next_state, :reg_webirc, %State{data | socket: socket, backoff: backoff}, @internal}
@@ -516,13 +509,14 @@ defmodule Irc.Connection do
@doc false
def reg_nick(:internal, nick, data) do
- case socksend(data, Line.new("NICK", nick || data.args.nick)) do
+ nick = nick || data.nick || data.args.nick
+ case socksend(data, Line.new("NICK", nick)) do
:ok -> {:keep_state, %State{data | nick: nick}, {:state_timeout, @reply_timeout, nil}}
error -> to_disconnected(error, data)
end
end
- def reg_nick(:info, {_, _, %Line{command: cmd}} = line, data) when cmd in [err_NICKNAMEINUSE(), err_NICKCOLLISION() ]do
+ def reg_nick(:info, {_, _, %Line{command: cmd} = line}, data) when cmd in [err_NICKNAMEINUSE(), err_NICKCOLLISION() ]do
Logger.debug "#{inspect(data)} <<< #{inspect(line)}"
new_nick = data.args.nick <> to_string(:random.uniform(9999))
{:keep_state, data, {:next_event, :internal, new_nick}}
@@ -669,9 +663,9 @@ defmodule Irc.Connection do
{:next_state, :registered, %State{data | motd: []}}
end
- def motd(:info, {_, _, %Line{command: rpl_MOTDSTART(), args: [_, line]} = line}, data) do
+ def motd(:info, {_, _, %Line{command: rpl_MOTDSTART(), args: [_, motd_line]} = line}, data) do
Logger.debug "#{inspect(data)} <<< #{inspect(line)}"
- {:keep_state, %State{data | motd: [line]}, {:state_timeout, @reply_timeout, nil}}
+ {:keep_state, %State{data | motd: [motd_line]}, {:state_timeout, @reply_timeout, nil}}
end
def motd(:info, {_, _, %Line{command: rpl_MOTD(), args: [_, motd_line]} = line}, data) do
@@ -696,8 +690,9 @@ defmodule Irc.Connection do
defp to_info(data, state \\ nil) do
data
|> Map.from_struct()
- |> Map.take([:nick, :port, :tls, :sts, :modes, :server, :version, :umodes, :cmodes, :cmodes_with_params, :capabs, :isupport, :motd])
+ |> Map.take([:nick, :port, :tls, :sts, :modes, :server, :version, :umodes, :cmodes, :cmodes_with_params, :capabs, :isupport, :motd, :assigns, :__private__])
|> Map.put(:host, data.args.host)
+ |> Map.put(:pid, self())
|> Map.put(:__struct__, __MODULE__)
end
@@ -710,8 +705,9 @@ defmodule Irc.Connection do
motd: Enum.reverse(data.motd),
welcomed: true
}
- msg = {:irc_conn_up, self(), to_info(data, :registered)}
- send(data.process, msg)
+ {:ok, data} = handler_run_event(data, :connected, [:TODO_PASS_ARGS])
+ #msg = {:irc_conn_up, self(), to_info(data, :registered)}
+ #send(data.process, msg)
Logger.info("#{inspect(data)} connected and registered")
{:next_state, :registered, data, @conn_ping_timeout}
end
@@ -720,9 +716,8 @@ defmodule Irc.Connection do
Logger.debug "#{inspect(data)} <<< #{inspect(line)}"
case process_line(line, data) do
:ignore ->
- msg = {:irc_conn_line, self(), line}
- send(data.process, msg)
- :keep_state_and_data
+ {:ok, data} = handler_run_event(data, :line, [line])
+ {:keep_state, data}
return ->
return
end
@@ -766,8 +761,9 @@ defmodule Irc.Connection do
to_disconnected({:killed, source, reason}, data)
end
- def process_line(%Line{command: "QUIT", source: source, args: [reason]}, data = %{nick: nick}) do
- if String.starts_with?(source, nick<>"!") do
+ def process_line(%Line{command: "QUIT", source: source, args: [reason]} = line, data = %{nick: nick}) do
+ if Line.to?(line, to_info(:registered, data)) do
+ #if String.starts_with?(source, nick<>"!") do
to_disconnected({:quit, source, reason}, data)
else
:ignore
@@ -790,35 +786,60 @@ defmodule Irc.Connection do
end
def process_line(%Line{command: "MODE", args: [nick, changes]}, data = %{nick: nick}) do
+ prev = data.modes
modes = Irc.Parser.Mode.changes(changes, data.modes)
- send(data.process, {:irc_conn_modes, self(), changes, data.modes, modes})
- {:keep_state, %State{data | modes: modes}}
+ {:ok, data} = %State{data | modes: modes}
+ |> handler_run_event(:modes, [changes, prev])
+ {:keep_state, data}
end
def process_line(%Line{command: "NICK", args: [new_nick]}, data = %{nick: nick}) do
- send(data.process, {:irc_conn_nick, self(), nick, new_nick})
+ {:ok, data} = %State{data | nick: new_nick}
+ |> handler_run_event(:nick, [nick, new_nick])
{:keep_state, %State{data | nick: nick}}
end
# -- BATCH
- def process_line(%Line{command: "BATCH", args: ["+"<>batchid, type | args]}, data) do
- batch = %{type: type, args: args, lines: []}
- {:keep_state, %State{data | batches: Map.put(data.batches, batchid, batch)}}
+ # TODO: Improve batch - support nested in nested, ...
+ def process_line(line = %Line{command: "BATCH", args: ["+"<>batchid, type | args], tags: tags}, data) do
+ {batches, outer_id} = if outer_batchid = Map.get(tags, "batch") do
+ if outer = Map.get(data.batches, outer_batchid) do
+ outer = Map.put(outer, :batches, [batchid | outer.batches])
+ {Map.put(data.batches, outer_batchid, outer), outer_batchid}
+ end
+ else
+ {data.batches, nil}
+ end
+ batch = %{type: type, args: args, lines: [], tags: tags, outer: outer_id, batches: [], source: line.source}
+ {:keep_state, %State{data | batches: Map.put(batches, batchid, batch)}}
end
- def process_line(%Line{command: "BATCH", args: ["-"<>batchid | args]}, data) do
- %{type: type, args: args, lines: lines} = Map.get(data.batches, batchid)
- if Keyword.get(data.args, :batch, true) do
- send(data.process, {:irc_conn_batch, self(), type, args, Enum.reverse(lines)})
+ def process_line(line = %Line{command: "BATCH", args: ["-"<>batchid | args]}, data) do
+ %{type: type, args: args, lines: lines, tags: tags, outer: outer_id, batches: batches} = Map.get(data.batches, batchid)
+ if outer_id do
+ :keep_state_and_data
else
- for l <- Enum.reverse(lines), do: send(data.process, {:irc_conn_line, self(), l})
+ for batchid <- [batchid | Enum.reverse(batches)] do
+ %{type: type, args: args, lines: lines, tags: tags} = batch = Map.get(data.batches, batchid)
+ batch = batch
+ |> Map.put(:lines, Enum.reverse(lines))
+ |> Map.put(:__private__, line.__private__)
+ batch = struct(Irc.Batch, batch)
+
+ if Keyword.get(data.args, :batch, true) do
+ {:ok, data} = handler_run_event(data, :batch, batch)
+ else
+ for l <- Enum.reverse(lines), do: send(data.process, {:irc_conn_line, self(), l})
+ end
+ end
+ {:keep_state, %State{data | batches: Map.drop(data.batches, batchid)}}
end
- {:keep_state, %State{data | batches: Map.drop(data.batches, batchid)}}
end
def process_line(line = %Line{tags: %{"batch" => batch}}, data) do
if batch_data = Map.get(data.batches, batch) do
- batch_data = Map.put(batch_data, :lines, [line | batch_data.lines])
+ tags = Map.drop(line.tags, "batch")
+ batch_data = Map.put(batch_data, :lines, [%{line | tags: tags} | batch_data.lines])
{:keep_state, %State{data | batches: Map.put(data.batches, batch, batch_data)}}
else
:ignore
@@ -854,7 +875,7 @@ defmodule Irc.Connection do
else
data
end
- data = %State{data | welcomed: false, server: nil, nick: nil, welcome: [], motd: [], isupport: %{}, backoff: backoff}
+ data = %State{data | welcomed: false, server: nil, welcome: [], motd: [], isupport: %{}, backoff: backoff}
Logger.error("#{inspect data} - #{inspect error} - (reconnecting in #{inspect delay})")
{:keep_state, data, {:state_timeout, delay, error}}
end
@@ -935,7 +956,9 @@ defmodule Irc.Connection do
{:keep_state_and_data, {:reply, ref, {:error, :not_disconnected}}}
end
def handle_common(_, {:call, ref}, {:send, _}, _) do
+ # TODO: Decide if we should postpone or blanket refuse.
{:keep_state_and_data, {:reply, ref, {:error, :not_connected}}}
+ #{:keep_state_and_data, :postpone}
end
def handle_common(_, :cast, {:send, _}, _) do
@@ -995,7 +1018,7 @@ defmodule Irc.Connection do
# Send helpers.
defp socksend(data, line = %Line{}) do
- socksend(data, [Line.encode(line, to_info(data))])
+ socksend(data, [Parser.Line.encode(line, to_info(data))])
end
defp socksend(data = %State{socket: socket}, line) when is_list(line) do
Logger.debug("#{inspect data} >>> #{inspect line}")
@@ -1008,6 +1031,24 @@ defmodule Irc.Connection do
{:next_state, :disconnected, data, {:next_event, :internal, error}}
end
+ # -- Init helpers
+ defp init_backoff(start_args) do
+ min = Map.get(start_args, :backoff_min, @backoff_min)
+ max = Map.get(start_args, :backoff_max, @backoff_max)
+ :backoff.init(min, max)
+ end
+
+ @spec init_sts(Map.new) :: {use_tls :: boolean, active_sts_policy :: boolean, port :: non_neg_integer}
+ defp init_sts(args = %{sts: true}) do
+ case Irc.STS.lookup(args.host) do
+ {:ok, port} -> {true, true, port}
+ _ -> init_sts(%{args | sts: false})
+ end
+ end
+ defp init_sts(%{tls: tls, port: port}) do
+ {tls, false, port}
+ end
+
# -- Arguments processing/validation
# Prepare args (in start)
@@ -1037,6 +1078,8 @@ defmodule Irc.Connection do
sasl = Map.get(args, :sasl, false)
echo = Map.get(args, :echo, true)
connect_opts = Map.get(args, :connect_opts) || []
+ handler = Map.get(args, :handler, @default_handler)
+ handler_opts = Map.get(args, :handler_opts, [])
with \
{:nick, :ok} <- {:nick, verify_nickname(nick)},
@@ -1058,7 +1101,9 @@ defmodule Irc.Connection do
sasl: sasl,
echo: echo,
connect_opts: connect_opts,
- connect_timeout: connect_timeout
+ connect_timeout: connect_timeout,
+ handler: handler,
+ handler_opts: handler_opts
}}
else
error -> error
@@ -1081,6 +1126,78 @@ defmodule Irc.Connection do
defp verify_nickname(nick), do: {:error, {:invalid_nickname, nick}}
+ defp handler_init(args) do
+ args.handler.init(args)
+ end
+
+ def handler_capabs(args, h_args) do
+ args.handler.capabs(h_args)
+ end
+
+ defp handler_run_event(data = %{handler: handler}, callback, args) do
+ args = [to_info(:connected, data) | args]
+ case :erlang.apply(handler, callback, args) do
+ {:ok, info} ->
+ {:ok, %State{data | assigns: info.assigns, __private__: info.__private__}}
+ error ->
+ error
+ end
+ end
+
+ defimpl Irc.Context, for: __MODULE__ do
+ def capab?(conn, capab) do
+ Enum.member?(conn.capabs, capab)
+ end
+
+ def capabs(conn) do
+ conn.capabs
+ end
+
+ def pid(conn) do
+ conn.pid
+ end
+
+ def module(conn) do
+ conn.__struct__
+ end
+
+ end
+
+ defimpl Irc.Addressable, for: __MODULE__ do
+ def nick(conn), do: conn.nick
+ def user(_), do: nil
+ def host(_), do: nil
+ def owner(conn), do: conn.pid
+ def owner_module(conn), do: conn.__struct__
+ end
+
+ defimpl Irc.Context, for: State do
+ def capab?(state, capab) do
+ Enum.member?(state.capabs, capab)
+ end
+
+ def capabs(state) do
+ state.capabs
+ end
+
+ def pid(_) do
+ self()
+ end
+
+ def module(state) do
+ Irc.Connection
+ end
+
+ end
+
+ defimpl Irc.Addressable, for: State do
+ def nick(state), do: state.nick
+ def user(_), do: nil
+ def host(_), do: nil
+ def owner(_), do: self()
+ def owner_module(), do: Irc.Connection
+ end
+
defimpl Inspect, for: State do
@moduledoc false
import Inspect.Algebra