summaryrefslogtreecommitdiff
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
parentSome fixes (diff)
Cont. wipmaster
-rw-r--r--README.md9
-rw-r--r--config/config.exs3
-rw-r--r--lib/irc/addressable.ex12
-rw-r--r--lib/irc/application.ex1
-rw-r--r--lib/irc/batch.ex11
-rw-r--r--lib/irc/client.ex17
-rw-r--r--lib/irc/client/command.ex11
-rw-r--r--lib/irc/client/command/account.ex10
-rw-r--r--lib/irc/client/command/away.ex10
-rw-r--r--lib/irc/client/command/chghost.ex6
-rw-r--r--lib/irc/client/command/invite.ex9
-rw-r--r--lib/irc/client/command/join.ex10
-rw-r--r--lib/irc/client/command/names.ex10
-rw-r--r--lib/irc/client/command/who.ex12
-rw-r--r--lib/irc/connection.ex313
-rw-r--r--lib/irc/connection_handler.ex50
-rw-r--r--lib/irc/connection_msg_h.ex84
-rw-r--r--lib/irc/connection_socket.ex12
-rw-r--r--lib/irc/context.ex16
-rw-r--r--lib/irc/data.ex10
-rw-r--r--lib/irc/line.ex69
-rw-r--r--lib/irc/mask.ex38
-rw-r--r--lib/irc/parser/capabs.ex6
-rw-r--r--lib/irc/parser/line.ex125
-rw-r--r--lib/irc/shout.ex4
-rw-r--r--lib/irc/store.ex131
-rw-r--r--lib/irc/ts6.ex35
-rw-r--r--lib/irc/user.ex74
-rw-r--r--mix.exs11
-rw-r--r--mix.lock12
30 files changed, 855 insertions, 266 deletions
diff --git a/README.md b/README.md
index 57f19a4..688c9bb 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,12 @@
-# Irc
+# Elixir/Irc
-An modern IRC toolkit for Elixir. Compatible IRCv3!
+A modern IRC toolkit for Elixir. Compatible IRCv3. Meant to build clients, bouncers, bots, servers, and services.
* `Irc.Parser` a versatile IRC parser;
* `Irc.Connection` a basic IRC connection (handling registration and socket);
-* `Irc.Client` a fully featured and extensible IRC client.
+* `Irc.Server` client server (todo);
+* `Irc.TS6` TS6 Server (todo);
+* `Irc.Client` a fully featured and extensible IRC client (should be usable with `.Connection`, `.Server` or `.TS6`).
Examples of usage: `Irc.Shout` (a simple connect-join-message-quit on Connection), `Irc.Client` on BaseClient.
@@ -39,4 +41,3 @@ end
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at [https://hexdocs.pm/irc](https://hexdocs.pm/irc).
-
diff --git a/config/config.exs b/config/config.exs
index eaeaf25..07d76aa 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -2,3 +2,6 @@ use Mix.Config
config :irc,
sts_store_file: {:priv, "sts_policies.dets"}
+
+config :mnesia,
+ dir: 'priv/#{Mix.env}/#{node()}'
diff --git a/lib/irc/addressable.ex b/lib/irc/addressable.ex
new file mode 100644
index 0000000..02993b9
--- /dev/null
+++ b/lib/irc/addressable.ex
@@ -0,0 +1,12 @@
+defprotocol Irc.Addressable do
+ @moduledoc """
+ Protocol for accessing a `nick`, `user`, `host` and `owner` pid and module from any compatible data structure.
+ """
+
+ def nick(t)
+ def user(t)
+ def host(t)
+ def owner(t)
+ def owner_module(t)
+
+end
diff --git a/lib/irc/application.ex b/lib/irc/application.ex
index 70b3d0f..cb866e9 100644
--- a/lib/irc/application.ex
+++ b/lib/irc/application.ex
@@ -7,6 +7,7 @@ defmodule Irc.Application do
children = [
{Irc.STS, []}
]
+ ++ Irc.Store.childs()
opts = [strategy: :one_for_one, name: Irc.Supervisor]
Supervisor.start_link(children, opts)
diff --git a/lib/irc/batch.ex b/lib/irc/batch.ex
new file mode 100644
index 0000000..12c8385
--- /dev/null
+++ b/lib/irc/batch.ex
@@ -0,0 +1,11 @@
+defmodule Irc.Batch do
+ defstruct [:type, :args, :lines, :tags, :outer, :batches, :source, {:__private__, %{}}]
+ @type t :: %__MODULE__{
+ type: String.t(),
+ source: Irc.Addressable.t(),
+ args: [String.t()],
+ lines: [Irc.Line.t()],
+ tags: Map.new(),
+ }
+
+end
diff --git a/lib/irc/client.ex b/lib/irc/client.ex
index d8a7b77..b7b9fe0 100644
--- a/lib/irc/client.ex
+++ b/lib/irc/client.ex
@@ -1,5 +1,7 @@
defmodule Irc.Client do
+
@behaviour :gen_statem
+
require Logger
alias Irc.Parser.Line
alias Irc.Connection
@@ -35,6 +37,8 @@ defmodule Irc.Client do
def callback_mode, do: [:state_enter]
+ @type t :: nil
+
@type message :: {:irc_client, pid(), event}
@type event :: event_connected | event_nick | event_modes | event_line | event_down
@@ -51,7 +55,6 @@ defmodule Irc.Client do
@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()]}
@@ -117,7 +120,7 @@ defmodule Irc.Client do
end
def handle_event(:info, msg = {:irc_conn_up, _, info}, :disconnected, data) do
- data = run_handler_event({:connected, info}, data)
+ data = run_handler_event({:connected, info}, info)
{:next_state, :connected, %__MODULE__{data | nick: info.nick, info: info, modes: info.modes, error: nil}}
end
@@ -147,7 +150,7 @@ defmodule Irc.Client do
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, actions} = case module.handle_command(data.info, command, args) do
{next, next_arg} -> {{next, next_arg}, []}
{next, next_arg, actions} -> {{next, next_arg}, List.wrap(actions)}
next when is_atom(next) -> {{next, nil}, []}
@@ -188,7 +191,7 @@ defmodule Irc.Client do
#
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, actions} = case module.handle_buffer(data.info, line, data.buffer) do
{next, next_arg} -> {{next, next_arg}, []}
{next, next_arg, actions} -> {{next, next_arg}, List.wrap(actions)}
next when is_atom(next) -> {{next, nil}, []}
@@ -205,7 +208,7 @@ defmodule Irc.Client do
# TODO: Callback stop?
def handle_event(:internal, reason, :error, data) do
Logger.error "#{inspect data}: #{inspect reason}"
- data.module.stop(reason, data.modstate)
+ data.module.stop(data.info, reason)
{:stop, :normal}
end
@@ -230,7 +233,7 @@ defmodule Irc.Client do
end
defp run_handler_event(event, data) do
- case data.module.handle_event(event, data.modstate) do
+ case data.module.handle_event(data.info, event) do
{:ok, modstate} -> %__MODULE__{data | modstate: modstate}
:ok -> data
end
@@ -258,7 +261,7 @@ defmodule Irc.Client do
end)
end
- defimpl Inspect, for: __MODULE__ do
+ defimpl Inspect, for: Irc.Client do
import Inspect.Algebra
def inspect(struct, _opts) do
concat(["#Irc.Client<", inspect(self()), ">"])
diff --git a/lib/irc/client/command.ex b/lib/irc/client/command.ex
index a84324f..71b7a6b 100644
--- a/lib/irc/client/command.ex
+++ b/lib/irc/client/command.ex
@@ -31,7 +31,7 @@ defmodule Irc.Client.Command do
@doc """
Handles an incoming line.
"""
- @callback handle_line(Irc.Line.t(), Irc.Connection.t()) :: line_return
+ @callback handle_line(conn :: Irc.Connection.t(), line :: Irc.Line.t()) :: line_return
@doc """
Handles an incoming line when in buffering state.
@@ -40,11 +40,16 @@ defmodule Irc.Client.Command do
Postponed lines will be treated as usual (per their respective module) once the buffering state exits.
"""
- @callback handle_buffer(Irc.Line.t(), buffer, Irc.Connection.t()) :: buffer_return
+ @callback handle_buffer(conn :: Irc.Connection.t(), line :: Irc.Line.t(), buffer) :: buffer_return
@doc """
Handles a user requested command.
"""
- @callback handle_command(client_command :: atom, args :: list, Irc.Connection.t()) :: line_return
+ @callback handle_command(conn :: Irc.Connection.t(), client_command :: atom, args :: list) :: line_return
+
+ @doc """
+ Connection is stopped
+ """
+ @callback stop(conn :: Irc.Connection.t(), reason :: atom) :: any()
end
diff --git a/lib/irc/client/command/account.ex b/lib/irc/client/command/account.ex
index c5ec1a9..e0088cd 100644
--- a/lib/irc/client/command/account.ex
+++ b/lib/irc/client/command/account.ex
@@ -1,17 +1,17 @@
defmodule Irc.Client.Command.Account do
- alias Irc.Parser.Line
+ alias Irc.Line
- @type t :: logout :: {:account, Irc.Mask.t} | login :: {:account, Irc.Mask.t, String.t}
+ #@type t :: (logout :: {:account, Irc.Mask.t}) | (login :: {:account, Irc.Mask.t, String.t})
- def init() do
+ def init(_) do
{"ACCOUNT", nil, "account-notify"}
end
- def handle_line(%Line{command: "ACCOUNT", source: target, args: ["*"]}) do
+ def handle_line(_, %Line{command: "ACCOUNT", source: target, args: ["*"]}) do
{:event, {:account, target}}
end
- def handle_line(%Line{command: "ACCOUNT", source: target, args: [account_name]}) do
+ def handle_line(_, %Line{command: "ACCOUNT", source: target, args: [account_name]}) do
{:event, {:account, target, account_name}}
end
diff --git a/lib/irc/client/command/away.ex b/lib/irc/client/command/away.ex
index da8b5ef..3941e5b 100644
--- a/lib/irc/client/command/away.ex
+++ b/lib/irc/client/command/away.ex
@@ -1,13 +1,13 @@
defmodule Irc.Client.Command.Away do
- alias Irc.Parser.Line
+ alias Irc.Line
- @type t :: away :: {:away, Irc.Mask.t, String.t} | unaway :: {:away, Irc.Mask.t}
+ #@type t :: (away :: {:away, Irc.Mask.t, String.t}) | (unaway :: {:away, Irc.Mask.t})
- def init() do
+ def init(_) do
{"AWAY", :away, "away-notify"}
end
- def handle_command(:away, args, _) do
+ def handle_command(_, :away, args) do
command = case args do
[] -> ['AWAY']
[message] -> ['AWAY :', message]
@@ -15,7 +15,7 @@ defmodule Irc.Client.Command.Away do
{:send, command}
end
- def handle_line(%Line{command: "AWAY", source: source, args: args}, _) do
+ def handle_line(_, %Line{command: "AWAY", source: source, args: args}) do
case args do
[] -> {:event, {:away, source}}
[message] -> {:event, {:away, {source, message}}}
diff --git a/lib/irc/client/command/chghost.ex b/lib/irc/client/command/chghost.ex
index 2b4adab..9a59307 100644
--- a/lib/irc/client/command/chghost.ex
+++ b/lib/irc/client/command/chghost.ex
@@ -1,13 +1,13 @@
defmodule Irc.Client.Command.Chghost do
- alias Irc.Parser.Line
+ alias Irc.Line
@type t :: {:chghost, old_mask :: Irc.Mask.t, new_mask :: Irc.Mask.t}
- def init() do
+ def init(_) do
{"CHGHOST", nil, "chghost"}
end
- def handle_line(%Line{source: old_mask, args: [new_user, new_host]}, _) do
+ def handle_line(_, %Line{source: old_mask, args: [new_user, new_host]}) do
new_mask = %Irc.Mask{old_mask | user: new_user, host: new_host}
{:event, {:chghost, old_mask, new_mask}}
end
diff --git a/lib/irc/client/command/invite.ex b/lib/irc/client/command/invite.ex
index 833c2fc..7a14cb8 100644
--- a/lib/irc/client/command/invite.ex
+++ b/lib/irc/client/command/invite.ex
@@ -1,21 +1,20 @@
defmodule Irc.Client.Command.Invite do
- alias Irc.Parser.Line
+ alias Irc.Line
@type invite :: {:invite, channel :: String.t(), inviter :: String.t(), invited_nick :: String.t()}
@type invited :: {:invite, channel :: String.t, inviter :: Irc.Mask.t}
@type t :: invite | invited
- def init() do
+ def init(_) do
{"INVITE", :invite, "invite-notify"}
end
- def handle_line(%Line{command: "INVITE", source: inviter, args: [invited, channel]}) do
+ def handle_line(_, %Line{command: "INVITE", source: inviter, args: [invited, channel]}) do
{:event, {:invite, channel, inviter, invited}}
end
- def handle_line(%Line{command: "INVITE", source: inviter, args: [channel]}) do
+ def handle_line(_, %Line{command: "INVITE", source: inviter, args: [channel]}) do
{:event, {:invite, channel, inviter}}
end
end
-
diff --git a/lib/irc/client/command/join.ex b/lib/irc/client/command/join.ex
index f4728c7..f84dc4a 100644
--- a/lib/irc/client/command/join.ex
+++ b/lib/irc/client/command/join.ex
@@ -1,7 +1,5 @@
defmodule Irc.Client.Command.Join do
- alias Irc.Parser.Line
- require Line
- import Line
+ alias Irc.Line
@type t :: {:join, channel :: String.t()} | {:join, channel :: String.t(), Irc.User.t()}
@@ -10,7 +8,7 @@ defmodule Irc.Client.Command.Join do
end
# An user joined
- def handle_line(line = %Line{command: "JOIN", source: source, args: [channel | args]}, conn) do
+ def handle_line(conn, line = %Line{command: "JOIN", source: source, args: [channel | args]}) do
if Line.self?(line, conn) do
{:event, {:join, channel}}
else
@@ -26,12 +24,12 @@ defmodule Irc.Client.Command.Join do
end
# Join a channel
- def handle_command(:join, [channel], _conn_info) do
+ def handle_command(_, :join, [channel]) do
{:send, ['JOIN', channel]}
end
# Outdated!
- def handle_buffer(%Line{command: "JOIN", source: %Irc.Mask{nick: nick}, args: [channel]}, buffer = %{channel: channel}, %Irc.Connection{nick: nick}) do
+ def handle_buffer(%Irc.Connection{nick: nick}, %Line{command: "JOIN", source: %Irc.Mask{nick: nick}, args: [channel]}, buffer = %{channel: channel}) do
{:finish, nil, {:event, {:join, channel}}}
end
diff --git a/lib/irc/client/command/names.ex b/lib/irc/client/command/names.ex
index b5a84a6..56ff2e9 100644
--- a/lib/irc/client/command/names.ex
+++ b/lib/irc/client/command/names.ex
@@ -1,21 +1,21 @@
defmodule Irc.Client.Command.Names do
- alias Irc.Parser.Line
+ alias Irc.Line
require Irc.Parser.Numeric
import Irc.Parser.Numeric
- def init() do
+ def init(_) do
{[rpl_NAMREPLY(), rpl_ENDOFNAMES()], :names, ["userhost-in-names"]}
end
- def handle_line(%Line{command: rpl_NAMREPLY()}) do
+ def handle_line(_, %Line{command: rpl_NAMREPLY()}) do
:buffer
end
- def handle_line(%Line{command: rpl_ENDOFNAMES(), args: [_, target | _]}) do
+ def handle_line(_, %Line{command: rpl_ENDOFNAMES(), args: [_, target | _]}) do
:finish
end
- def handle_buffer(buffer = [%Line{args: [_, target | _]} | _]) do
+ def handle_buffer(_, buffer = [%Line{args: [_, target | _]} | _]) do
{:event, {:names, target, buffer}}
end
diff --git a/lib/irc/client/command/who.ex b/lib/irc/client/command/who.ex
index 1654ef6..41cc761 100644
--- a/lib/irc/client/command/who.ex
+++ b/lib/irc/client/command/who.ex
@@ -1,5 +1,5 @@
defmodule Irc.Client.Command.Who do
- alias Irc.Parser.Line
+ alias Irc.Line
alias Irc.User
use Irc.Parser.Numeric
@@ -9,7 +9,7 @@ defmodule Irc.Client.Command.Who do
{nil, "WHO", nil}
end
- def handle_command("WHO", [target], conn) do
+ def handle_command(conn, "WHO", [target]) do
args = if Irc.Connection.supports?(conn, "whox") do
" nuhs%cuhsnfdar"
end
@@ -17,12 +17,12 @@ defmodule Irc.Client.Command.Who do
end
@errors [err_NOSUCHSERVER(), rpl_TRYAGAIN()]
- def handle_buffer(%Line{command: error, args: args}, buffer, _conn) when error in @errors do
+ def handle_buffer(conn, %Line{command: error, args: args}, buffer) when error in @errors do
#?
{:buffer, %{buffer | error: {error, args}}}
end
- def handle_buffer(%Line{command: rpl_ENDOFWHO()}, buffer, _conn) do
+ def handle_buffer(conn, %Line{command: rpl_ENDOFWHO()}, buffer) do
result = if buffer.error do
{:error, buffer.error}
else
@@ -31,7 +31,7 @@ defmodule Irc.Client.Command.Who do
{:finish, nil, {:event, {:who, buffer.target, result}}}
end
- def handle_buffer(%Line{command: rpl_WHOREPLY(), args: args}, buffer, conn) do
+ def handle_buffer(conn, %Line{command: rpl_WHOREPLY(), args: args}, buffer) do
case args do
[_, c, u, h, s, n, f, d | a_r] ->
{a,r} = case a_r do
@@ -57,6 +57,6 @@ defmodule Irc.Client.Command.Who do
{:buffer, %{buffer | acc: [args | buffer.acc]}}
end
- def handle_buffer(_, _), do: :postpone
+ def handle_buffer(_, _, _), do: :postpone
end
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
diff --git a/lib/irc/connection_handler.ex b/lib/irc/connection_handler.ex
new file mode 100644
index 0000000..a5d5f3f
--- /dev/null
+++ b/lib/irc/connection_handler.ex
@@ -0,0 +1,50 @@
+defmodule Irc.Connection.Handler do
+ @moduledoc """
+ Handler behaviour
+ """
+
+ @type args :: Map.new()
+
+ @doc """
+ Initializes
+ """
+ @callback init(args()) :: args()
+
+ @doc """
+ Capability negotiation.
+
+ Allows the handler to request more capabilities from the server.
+ """
+ @callback capabs(args()) :: [String.t()]
+
+ @doc """
+ Connected callback.
+ """
+ @callback connected(conn :: Irc.Connection.t(), args()) :: {:ok, conn :: Irc.Connection.t()}
+
+ @doc """
+ An IRC line has been received.
+ """
+ @callback line(conn :: Irc.Connection.t(), line :: Irc.Line.t()) :: {:ok, conn :: Irc.Connection.t()}
+
+ @doc """
+ An IRC batch has been received.
+ """
+ @callback batch(conn :: Irc.Connection.t(), batch :: Irc.Batch.t()) :: {:ok, conn :: Irc.Connection.t()}
+
+ @doc """
+ Connection changed modes.
+ """
+ @callback modes(conn :: Irc.Connection.t(), changes :: :todo, prev :: :todo) :: {:ok, conn :: Irc.Connection.t()}
+
+ @doc """
+ Connection changed nick.
+ """
+ @callback nick(conn :: Irc.Connection.t(), prev_nick :: String.t, new_nick :: String.t) :: {:ok, conn :: Irc.Connection.t()}
+
+ @doc """
+ Disconnected callback.
+ """
+ @callback disconnected(conn :: Irc.Connection.t(), reason :: atom()) :: any()
+
+end
diff --git a/lib/irc/connection_msg_h.ex b/lib/irc/connection_msg_h.ex
new file mode 100644
index 0000000..d0cf2ba
--- /dev/null
+++ b/lib/irc/connection_msg_h.ex
@@ -0,0 +1,84 @@
+defmodule Irc.Connection.MsgHandler do
+ @behaviour Irc.Connection.Handler
+ @moduledoc """
+ Default behaviour. Sends events as messages to owner process.
+ """
+
+ @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(), Irc.Connection.t()}
+ @typedoc "Message: Received an IRC line."
+ @type msg_line :: {:irc_conn_line, pid(), Irc.Line.t()}
+ @typedoc "Message: Line batch (only if enabled, see `batch` in `t:start_opt/0`)."
+ @type msg_batch :: {:irc_conn_batch, pid(), Irc.Batch.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 "IRC Error reasons"
+ @type error_irc :: {:killed, by :: String.t, reason :: String.t}
+ | {:quit, reason :: String.t}
+ | {:irc_error, reason :: String.t}
+
+
+ @typedoc "Connection error reasons"
+ @type error :: {:error, error_irc | any}
+
+ @impl true
+ def init(%{process: process}) do
+ {:ok, process}
+ end
+
+ @impl true
+ def capabs(_process) do
+ {:ok, []}
+ end
+
+ @impl true
+ def connected(conn, process, _) do
+ msg = {:irc_conn_up, self(), conn}
+ send(conn.process, msg)
+ {:ok, conn}
+ end
+
+ @impl true
+ def line(conn, line) do
+ msg = {:irc_conn_line, self(), line}
+ send(conn.process, msg)
+ {:ok, conn}
+ end
+
+ @impl true
+ def batch(conn, batch) do
+ send(conn.process, {:irc_conn_batch, self(), batch})
+ {:ok, conn}
+ end
+
+ @impl true
+ def modes(conn, changes, prev) do
+ send(conn.process, {:irc_conn_modes, self(), changes, conn.modes})
+ {:ok, conn}
+ end
+
+ @impl true
+ def nick(conn, prev_nick, new_nick) do
+ send(conn.process, {:irc_conn_nick, self(), prev_nick, new_nick})
+ {:ok, conn}
+ end
+
+ @impl true
+ def disconnected(conn, _) do
+ nil
+ end
+
+end
diff --git a/lib/irc/connection_socket.ex b/lib/irc/connection_socket.ex
index 29c42c0..d7169fd 100644
--- a/lib/irc/connection_socket.ex
+++ b/lib/irc/connection_socket.ex
@@ -11,10 +11,10 @@ defmodule Irc.ConnectionSocket do
@type t :: {ConnectionSocket, pid(), reference()}
- @spec connect(tls :: boolean, host :: String.t, port :: Integer.t, options :: Keyword.t) :: {:ok, t} | {:error, error}
- def connect(tls, host, port, options) do
+ @spec connect(tls :: boolean, host :: String.t, port :: Integer.t, options :: Keyword.t, module()) :: {:ok, t} | {:error, error}
+ def connect(tls, host, port, options, module) do
caller = self()
- case GenServer.start(__MODULE__, [caller, tls, host, port, options]) do
+ case GenServer.start(__MODULE__, [caller, tls, host, port, options, module]) do
{:ok, pid} ->
mon = Process.monitor(pid)
{:ok, {__MODULE__, pid, mon}}
@@ -38,10 +38,10 @@ defmodule Irc.ConnectionSocket do
end
@doc false
- def init([caller, tls, host, port, options]) do
+ def init([caller, tls, host, port, options, caller_module]) do
Process.monitor(caller)
module = if tls, do: :ssl, else: :gen_tcp
- state = %{caller: caller, module: module, host: host, port: port, options: options, socket: nil}
+ state = %{caller: caller, caller_module: caller_module, module: module, host: host, port: port, options: options, socket: nil}
case module.connect(String.to_charlist(host), port, options) do
{:ok, socket} ->
{:ok, Map.put(state, :socket, socket)}
@@ -70,7 +70,7 @@ defmodule Irc.ConnectionSocket do
@doc false
# Received a line from socket
def handle_info({module, socket, line}, state = %{caller: caller, module: module, socket: socket}) do
- Kernel.send(caller, {__MODULE__, self(), Line.parse(line)})
+ Kernel.send(caller, {__MODULE__, self(), Line.parse(line, state.caller_module, state.caller)})
{:noreply, state}
end
diff --git a/lib/irc/context.ex b/lib/irc/context.ex
new file mode 100644
index 0000000..017ea5f
--- /dev/null
+++ b/lib/irc/context.ex
@@ -0,0 +1,16 @@
+defprotocol Irc.Context do
+ @doc """
+ Provides a global IRC context.
+ """
+
+ @spec capab?(t(), capability :: String.t()) :: boolean
+ def capab?(t, capability)
+
+ @spec capabs(t()) :: [String.t()]
+ def capabs(t)
+
+ def pid(t)
+
+ def module(t)
+
+end
diff --git a/lib/irc/data.ex b/lib/irc/data.ex
new file mode 100644
index 0000000..4d48906
--- /dev/null
+++ b/lib/irc/data.ex
@@ -0,0 +1,10 @@
+defmodule Irc.Data do
+ @moduledoc """
+ The `Irc` library contains a polymorphic data system.
+
+ * Universal structures: `Irc.User`, `Irc.Mask`, `Irc.Line`;
+ * Data protocols: `Irc.Addressable`, `Irc.Context`
+
+ Data structures and protocol requires the owner/creator of theses structures to attach its identity (pid and module).
+ """
+end
diff --git a/lib/irc/line.ex b/lib/irc/line.ex
new file mode 100644
index 0000000..56836a8
--- /dev/null
+++ b/lib/irc/line.ex
@@ -0,0 +1,69 @@
+defmodule Irc.Line do
+ @type t :: %__MODULE__{
+ tags: Map.t(),
+ source: Irc.Mask.t() | String.t() | nil,
+ command: String.t(),
+ args: list()
+ }
+ defstruct __private__: %{}, tags: %{}, source: nil, command: nil, args: []
+
+ def new(command) do
+ new(command, [])
+ end
+
+ def new(command, args) do
+ new(command, args, %{})
+ end
+
+ def new(command, arg, tags) when not is_list(arg) do
+ new(command, [arg], tags)
+ end
+
+ def new(command, args, tags) do
+ %__MODULE__{command: command, args: args, tags: tags, __private__: %{at: DateTime.utc_now()}}
+ end
+
+ @doc "Returns the line date (server time if sent, otherwise, parse time)"
+ @spec at(t()) :: DateTime.t()
+ def at(%__MODULE__{__private__: %{at: at}, tags: %{"time" => server_time}}) do
+ case DateTime.from_iso8601(server_time) do
+ {:ok, date} -> date
+ _ -> at
+ end
+ end
+ def at(%__MODULE__{__private__: %{at: at}}), do: at
+ def at(_), do: nil
+
+ @spec to?(t(), Irc.Addressable.t()) :: boolean
+ @doc "Returns true if the line is adressed to the connection."
+ def to?(%__MODULE__{args: [nick | _]}, addressable) do
+ Irc.Addressable.nick(addressable) == nick
+ end
+
+ @spec self?(t(), Irc.Addressable.t()) :: boolean
+ @doc "Returns true if the line source is the from the given connection/mask."
+ def self?(%__MODULE__{source: nick}, addressable) when is_binary(nick) do
+ Irc.Addressable.nick(addressable) == nick
+ end
+ def self?(%__MODULE__{source: source_addressable}, addressable) do
+ Irc.Addressable.equal?(source_addressable, addressable)
+ end
+
+ defimpl String.Chars, for: __MODULE__ do
+ def to_string(line) do
+ Irc.Parser.Line.encode(line)
+ end
+ end
+
+ defimpl Inspect, for: __MODULE__ do
+ @moduledoc false
+ import Inspect.Algebra
+
+ def inspect(struct, _opts) do
+ tags = Enum.map(struct.tags, fn({k, v}) -> concat([k, "=", v]) end) |> Enum.join(",")
+ Enum.join(["#IRC.Line<", struct.source, " ", struct.command, " ", inspect(struct.args), " (", tags, ")>"], "")
+ end
+ end
+
+
+end
diff --git a/lib/irc/mask.ex b/lib/irc/mask.ex
index 5106c26..7f68d1a 100644
--- a/lib/irc/mask.ex
+++ b/lib/irc/mask.ex
@@ -1,7 +1,8 @@
defmodule Irc.Mask do
+ alias Irc.Addressable
@type t :: %__MODULE__{nick: nil | String.t(), user: nil | String.t(), host: nil | String.t(), server: nil | String.t()}
- defstruct [:nick, :user, :host, :server]
+ defstruct [:nick, :user, :host, :server, {:__private__, %{owner: nil, owner_module: nil}}]
@spec parse(String.t) :: {:ok, t()} | {:error, :invalid_mask}
def parse(string) do
@@ -12,10 +13,39 @@ defmodule Irc.Mask do
end
end
+ def parse(string, module, pid) do
+ case parse(string) do
+ {:ok, x} -> {:ok, set_owner(x, module, pid)}
+ error -> error
+ end
+ end
+
+ @spec equal?(Addressable.t(), Addressable.t()) :: boolean
+ def equal?(a, b) do
+ import Addressable
+ (owner(a) == owner(b)) && (nick(a) == nick(b))
+ end
+
+ def from_addressable(addressable) do
+ import Addressable
+ new(nick(addressable),
+ user(addressable),
+ host(addressable)
+ )
+ |> set_owner(owner_module(addressable), owner(addressable))
+ end
+
def new(nick, user, host) do
%__MODULE__{nick: nick, user: user, host: host}
end
+ def set_owner(t = %{__private__: private = %{owner: nil, owner_module: nil}}, module, pid) when is_atom(module) and is_pid(pid) do
+ private = private
+ |> Map.put(:owner, pid)
+ |> Map.put(:owner_module, module)
+ %{t | __private__: private}
+ end
+
def to_string(%__MODULE__{server: nil, nick: nick, user: user, host: host}) do
Enum.join([nick, "!", user, "@", host])
end
@@ -40,6 +70,12 @@ defmodule Irc.Mask do
defdelegate to_string(struct), to: Irc.Mask
end
+ defimpl Irc.Addressable, for: __MODULE__ do
+ def nick(%{nick: nick}), do: nick
+ def user(%{user: user}), do: user
+ def host(%{host: host}), do: host
+ end
+
defimpl Inspect, for: __MODULE__ do
@moduledoc false
import Inspect.Algebra
diff --git a/lib/irc/parser/capabs.ex b/lib/irc/parser/capabs.ex
index 88bf017..1448d75 100644
--- a/lib/irc/parser/capabs.ex
+++ b/lib/irc/parser/capabs.ex
@@ -1,7 +1,10 @@
defmodule Irc.Parser.Capabs do
@moduledoc "Helper to parse capability lists"
+ @type t :: %{capab_name() => capab_value()}
+ @type capab_name :: String.t()
+ @type capab_value :: true | any()
- @spec parse(String.t) :: %{capab :: String.t => true | args :: Map.t}
+ @spec parse(String.t()) :: t()
def parse(string) do
string
|> String.split(" ")
@@ -29,4 +32,3 @@ defmodule Irc.Parser.Capabs do
end
end
-
diff --git a/lib/irc/parser/line.ex b/lib/irc/parser/line.ex
index 48ddd9c..b436c57 100644
--- a/lib/irc/parser/line.ex
+++ b/lib/irc/parser/line.ex
@@ -2,93 +2,58 @@ defmodule Irc.Parser.Line do
@moduledoc """
IRC line parser/encoder
"""
+ alias Irc.Line
+ require Logger
- @type t :: %__MODULE__{
- tags: Map.t(),
- source: Irc.Mask.t() | String.t() | nil,
- command: String.t(),
- args: list()
- }
- defstruct __private__: %{}, tags: %{}, source: nil, command: nil, args: []
-
- @spec parse(string) :: t()
+ @spec parse(string) :: Line.t()
@doc "Parse a server-to-client line."
- def parse(line) do
+ def parse(line, owner_module \\ nil, owner \\ nil)
+
+ def parse(line, context, _) when is_map(context) do
+ module = Irc.Context.module(context)
+ pid = Irc.Context.pid(context)
+ parse(line)
+ |> set_owner(module, pid)
+ end
+
+ def parse(line, owner_module, owner) do
{tags, rest} = parse_tags(line)
- {source, rest} = parse_source(rest)
+ {source, rest} = parse_source(rest, owner_module, owner)
{command, rest} = parse_cmd(rest)
args = parse_args(rest)
private = %{
+ owner: owner,
+ owner_module: owner_module,
at: DateTime.utc_now()
}
- %__MODULE__{__private__: private, tags: tags, source: source, command: command, args: args}
+ %Line{__private__: private, tags: tags, source: source, command: command, args: args}
end
- @spec encode(t()) :: String.t
+ @spec set_owner(Line.t(), module(), pid()) :: Line.t()
+ def set_owner(line = %Line{__private__: private = %{owner: nil, owner_module: nil}}, module, owner) when is_atom(module) and is_pid(owner) do
+ private = Map.put(private, :owner_module, module)
+ |> Map.put(:owner, owner)
+ %Line{line | __private__: private}
+ end
+
+ def set_owner(line = %Line{__private__: private = %{owner: opid, owner_module: omod}}, module, owner) when is_atom(module) and is_pid(owner) do
+ Logger.debug("Parser.Line: tried to re-own an already owned line #{inspect {{omod,opid},{module,owner}}}")
+ line
+ end
+
+ @spec encode(Line.t()) :: String.t
@doc "Encode a line"
- def encode(line = %__MODULE__{}, conn \\ %Irc.Connection{}) do
+ def encode(line = %Line{}, conn \\ %Irc.Connection{}) do
[line.source, line.tags, line.command, line.args]
src = if line.source, do: ":"<>to_string(line.source)
tags = Enum.reduce(line.tags, [], fn({k,v}, acc) -> ["#{k}=#{v}" | acc] end)
|> Enum.join(";")
- tags = if Enum.member?(conn.capabs || [], "message-tags") && tags != "", do: "@"<>tags
+ tags = if Irc.Context.capab?(conn, "message-tags") && tags != "", do: "@"<>tags
[tags, src, line.command, encode_args(line.args)]
|> Enum.filter(fn(x) -> x end)
|> Enum.join(" ")
end
- def new(command) do
- new(command, [])
- end
-
- def new(command, args) do
- new(command, args, %{})
- end
-
- def new(command, arg, tags) when not is_list(arg) do
- new(command, [arg], tags)
- end
-
- def new(command, args, tags) do
- %__MODULE__{command: command, args: args, tags: tags, __private__: %{at: DateTime.utc_now()}}
- end
-
- @doc "Returns the line date (server time if sent, otherwise, parse time)"
- @spec at(t()) :: DateTime.t()
- def at(line = %__MODULE__{__private__: %{at: at}, tags: %{"time" => server_time}}) do
- case DateTime.from_iso8601(server_time) do
- {:ok, date} -> date
- _ -> at
- end
- end
- def at(%__MODULE__{__private__: %{at: at}}), do: at
- def at(_), do: nil
-
- @spec to?(t(), Irc.Connection.t() | Irc.Mask.t() | Irc.User.t()) :: boolean
- @doc "Returns true if the line is adressed to the connection."
- def to?(line = %__MODULE__{args: [nick | _]}, target = %{__struct__: s, nick: nick}) when s in [Irc.Connection, Irc.Mask, Irc.User] do
- true
- end
- def to?(%__MODULE__{}, %{__struct__: s}) when s in [Irc.Connection, Irc.Mask, Irc.User], do: false
-
- @spec self?(t(), Irc.Connection.t() | Irc.Mask.t() | Irc.User.t()) :: boolean
- @doc "Returns true if the line source is the from the given connection/mask."
- def self?(line = %__MODULE__{source: %Irc.Mask{nick: nick}}, target = %Irc.Connection{nick: nick}) do
- true
- end
- def self?(%__MODULE__{source: mask = %Irc.Mask{}}, mask = %Irc.Mask{}) do
- true
- end
- def self?(line = %__MODULE__{source: mask = %Irc.Mask{}}, user = %Irc.User{}) do
- self?(line, Irc.User.to_mask(user))
- end
- def self?(%__MODULE__{source: nick}, %Irc.Connection{nick: nick}) do
- true
- end
- def self?(%__MODULE__{}, %Irc.Connection{}), do: false
- def self?(%__MODULE__{}, %Irc.User{}), do: false
- def self?(%__MODULE__{}, %Irc.Mask{}), do: false
-
# ARGS
defp parse_args(input), do: parse_args(input, [])
@@ -153,18 +118,18 @@ defmodule Irc.Parser.Line do
# SOURCE
- defp parse_source([?: | input]), do: parse_source(input, [])
- defp parse_source(input), do: {nil, input}
+ defp parse_source([?: | input], m, p), do: parse_source(input, [], m, p)
+ defp parse_source(input, _, _), do: {nil, input}
- defp parse_source([0x20 | rest], acc) do
+ defp parse_source([0x20 | rest], acc, m, p) do
string = acc_to_str(acc)
- result = case Irc.Mask.parse(string) do
+ result = case Irc.Mask.parse(string, m, p) do
{:error, _} -> string
{:ok, mask} -> mask
end
{result, rest}
end
- defp parse_source([char | rest], acc), do: parse_source(rest, [char | acc])
+ defp parse_source([char | rest], acc, m, p), do: parse_source(rest, [char | acc], m, p)
# TAGS
@@ -236,20 +201,4 @@ defmodule Irc.Parser.Line do
defp encode_args([], acc), do: acc
- defimpl String.Chars, for: __MODULE__ do
- def to_string(line) do
- Irc.Parser.Line.encode(line)
- end
- end
-
- defimpl Inspect, for: __MODULE__ do
- @moduledoc false
- import Inspect.Algebra
-
- def inspect(struct, _opts) do
- tags = Enum.map(struct.tags, fn({k, v}) -> concat([k, "=", v]) end) |> Enum.join(",")
- Enum.join(["#IRC.Line<", struct.source, " ", struct.command, " ", inspect(struct.args), " (", tags, ")>"], "")
- end
- end
-
end
diff --git a/lib/irc/shout.ex b/lib/irc/shout.ex
index 912517c..bcfb10a 100644
--- a/lib/irc/shout.ex
+++ b/lib/irc/shout.ex
@@ -1,5 +1,5 @@
defmodule Irc.Shout do
- alias Irc.{Connection, Parser.Line}
+ alias Irc.{Connection, Line}
def shout(nick, host, target, message, opts \\ []) do
opts = opts
@@ -44,7 +44,7 @@ defmodule Irc.Shout do
def await_join(conn, target) do
receive do
- {:irc_conn_line, conn, %Irc.Parser.Line{command: "JOIN", args: [target]}} -> :ok
+ {:irc_conn_line, conn, %Line{command: "JOIN", args: [target]}} -> :ok
{:irc_conn_error, conn, reason} -> {:error, reason}
{:DOWN, _, _, conn, reason} -> {:error, {:conn_down, reason}}
after
diff --git a/lib/irc/store.ex b/lib/irc/store.ex
new file mode 100644
index 0000000..5aaef6c
--- /dev/null
+++ b/lib/irc/store.ex
@@ -0,0 +1,131 @@
+defmodule Irc.Store do
+ @moduledoc """
+ Mnesia-based store for `Irc.Client`.
+
+ Tables:
+ * User (transient, in memory): tracks known connected users.
+ * Channel (transient, in memory): tracks known channels.
+
+ ## Setup
+
+ Needs to be ran only once at setup:
+
+ ```elixir
+ # First step: create schemas.
+ Memento.stop()
+ Memento.Schema.create([node()])
+ Memento.start()
+
+ # Second step: create tables
+ Irc.Store.setup()
+ ```
+
+
+ """
+
+ @doc false
+ # Childrens for Irc.Application supervisor tree
+ def childs do
+ [{Irc.Store.OwnerSweeper, []}]
+ end
+
+ @doc "Creates tables in Mnesia. Should only be run once."
+ def setup do
+ Memento.Table.create!(Irc.Store.User)
+ end
+
+ defmodule OwnerSweeper do
+ @moduledoc """
+ Ensures the Store only keeps alive data by monitoring owners and deleting entries when their owner dies.
+ """
+ use GenServer
+ require Logger
+
+ @doc "Register calling process to the `OwnerSweeper` process."
+ def register() do
+ GenServer.call(__MODULE__, {:monitor, self()})
+ end
+
+ @doc "Unregister calling process from the sweeper and delete all its entries."
+ def unregister() do
+ GenServer.call(__MODULE__, {:unregister, self()})
+ end
+
+ @doc false
+ def start_link(_) do
+ GenServer.start_link(__MODULE__, [], name: __MODULE__)
+ end
+
+ @impl true
+ @doc false
+ def init(_) do
+ {:ok, Map.new}
+ end
+
+ @impl true
+ @doc false
+ def handle_call({:register, pid}, _, monitors) do
+ if Map.get(monitors, pid) do
+ {:reply, :ok, monitors}
+ else
+ Logger.debug("Irc.Store.OwnerSweeper: added to sweep list #{inspect(pid)}")
+ monitor = Process.monitor(pid)
+ {:reply, :ok, Map.put(monitors, pid, monitor)}
+ end
+ end
+
+ @impl true
+ @doc false
+ def handle_call({:unregister, pid}, _, monitors) do
+ if ref = Map.get(monitors, pid) do
+ Process.demonitor(ref, [:flush])
+ clear_entries(pid)
+ {:reply, :ok, Map.drop(monitors, pid)}
+ else
+ {:reply, :not_registered, monitors}
+ end
+ end
+
+ @impl true
+ @doc false
+ def handle_info({:DOWN, monitor, :process, pid, _reason}, monitors) do
+ Logger.debug("Irc.Store.OwnerSweeper: removing entries of #{inspect(pid)}")
+ if monitor == Map.get(monitors, pid) do
+ # TODO: Delete all `User` entries where owner == pid
+ clear_entries(pid)
+ {:noreply, Map.drop(monitors, pid)}
+ else
+ {:noreply, monitors}
+ end
+ end
+
+ defp clear_entries(pid) do
+ # TODO: Find some way to use built in mnesia select_delete
+ for table <- [Irc.Store.User] do
+ Memento.transaction fn ->
+ for entry <- Memento.Query.select(table, {:==, :owner, pid}) do
+ Memento.delete(table, entry.id)
+ end
+ end
+ end
+ end
+
+ end
+
+
+ defmodule User do
+ #attributes = Irc.User.__attrs__
+ #|> Enum.map(fn
+ # ({k, _}) -> k
+ # k -> k
+ #end)
+
+ #IO.inspect(attributes)
+
+ #use Memento.Table,
+ # attributes: attributes,
+ # index: [:owner, :host, :nick, :account]
+
+ end
+
+end
diff --git a/lib/irc/ts6.ex b/lib/irc/ts6.ex
new file mode 100644
index 0000000..710f1a5
--- /dev/null
+++ b/lib/irc/ts6.ex
@@ -0,0 +1,35 @@
+defmodule Irc.TS6 do
+ @moduledoc """
+ TS6 protocol. Not implemented yet, only basic data structures are defined.
+ """
+
+ defmodule Server do
+ defstruct [
+ :name,
+ :sid,
+ :description,
+ {:depth, 1},
+ {:users, []},
+ {:servers, []},
+ :parent_sid
+ ]
+ end
+
+ defmodule User do
+ defstruct [
+ :uid,
+ :name,
+ :depth,
+ :ts,
+ :umode,
+ :username,
+ :hostname,
+ :ip,
+ :realhost,
+ :account,
+ :realname,
+ :sid
+ ]
+ end
+
+end
diff --git a/lib/irc/user.ex b/lib/irc/user.ex
index 5e2f526..06a6b97 100644
--- a/lib/irc/user.ex
+++ b/lib/irc/user.ex
@@ -1,12 +1,19 @@
defmodule Irc.User do
+ require Record
@moduledoc """
Represents an IRC user.
Not all field may be returned in events.
+
+ Its main structure is the struct `t()`, but the record `irc_user()` can be used to write queries against an ets collection.
"""
- defstruct [
+ @record :irc_user
+
+ @attrs [
+ :id,
+ :owner,
:nick,
:user,
:host,
@@ -20,27 +27,66 @@ defmodule Irc.User do
{:modes, []},
{:chanmodes, %{}},
{:channels, []},
+ {:assigns, %{}},
+ {:private, %{persisted: false}},
]
- @type t :: %__MODULE__{
- nick: String.t(),
- user: nil | String.t(),
- host: nil | String.t(),
- name: nil | String.t(),
- account: nil | String.t(),
- server: nil | String.t(),
- idle: nil | {non_neg_integer(), NaiveDateTime.t()},
- operator: boolean(),
- modes: [],
- channels: [],
- chanmodes: %{String.t() => String.t()}
- }
+ defstruct @attrs
+
+ record_attrs = Enum.map(@attrs, fn
+ ({_,_}=kv) -> kv
+ (k) -> {k, :undefined}
+ end)
+ keys = :lists.map(&elem(&1, 0), record_attrs)
+ vals = :lists.map(&{&1, [], nil}, keys)
+ pairs = :lists.zip(keys, vals)
+
+ Record.defrecord(@record, record_attrs)
+
+# @type t :: %__MODULE__{
+# id: String.t(),
+# owner: nil | pid(),
+# nick: String.t(),
+# user: nil | String.t(),
+# host: nil | String.t(),
+# name: nil | String.t(),
+# account: nil | String.t(),
+# server: nil | String.t(),
+# idle: nil | {non_neg_integer(), NaiveDateTime.t()},
+# operator: boolean(),
+# modes: [],
+# channels: [],
+# chanmodes: %{String.t() => String.t()},
+## assigns: %{any() => any()}
+# }
+# @type record :: record(:irc_user)
+
+ @doc false
+ def __attrs__, do: @attrs
+
+# @spec to_record(t()) :: t()
+ def to_record(%__MODULE__{unquote_splicing(pairs)}) do
+ {@record, unquote_splicing(vals)}
+ end
+
+# @spec from_record(t()) :: t()
+ def from_record({@record, unquote_splicing(vals)}) do
+ %__MODULE__{unquote_splicing(pairs)}
+ end
+
+# @spec from_mask(IRC.Mask.t()) :: t()
def from_mask(%Irc.Mask{user: user, nick: nick, host: host}) when is_binary(nick) do
%__MODULE__{nick: nick, user: user, host: host}
end
+# @spec to_mask(t()) :: IRC.Mask.t()
def to_mask(%__MODULE__{nick: nick, user: user, host: host}) do
Irc.Mask.new(nick, user, host)
end
+ defimpl Irc.Addressable, for: __MODULE__ do
+ def nick(%{nick: nick}), do: nick
+ def user(%{user: user}), do: user
+ def host(%{host: host}), do: host
+ end
end
diff --git a/mix.exs b/mix.exs
index 0f90e99..fde1d6a 100644
--- a/mix.exs
+++ b/mix.exs
@@ -13,7 +13,15 @@ defmodule Irc.MixProject do
homepage_url: "https://hrefhref.github.io/elixir-irc",
docs: [
main: "readme",
- extras: ["README.md"]
+ extras: ["README.md"],
+ groups_for_modules: [
+ "Data": [Irc.Mask, Irc.User, Irc.Line, Irc.Store, ~r/Irc\.Store.*/, Irc.Context, Irc.Addressable],
+ "Connection": [Irc.Connection, Irc.ConnectionSocket, Irc.STS, ~r/Irc\.Connection*/],
+ "Clients": [Irc.Shout, Irc.Client, ~r/Irc\.Client.*/],
+ "Servers": [Irc.Server, Irc.TS6, ~r/Irc\.(Server|TS6).*/],
+ "Parser": [Irc.Parser, ~r/Irc\.Parser.*/]
+ ],
+ nest_modules_by_prefix: [Irc.Parser, Irc.Client, Irc.Client.Command, Irc.Store, Irc.TS6, Irc.Server]
]
]
end
@@ -31,6 +39,7 @@ defmodule Irc.MixProject do
[
{:backoff, github: "ferd/backoff"},
{:ex_doc, "~> 0.21", only: :dev, runtime: false},
+ {:memento, "~> 0.3.1"}
]
end
end
diff --git a/mix.lock b/mix.lock
index ee01975..7223496 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,8 +1,10 @@
%{
"backoff": {:git, "https://github.com/ferd/backoff.git", "dfb19a20c37b73af067edc3c335624bdf9517396", []},
- "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm"},
- "ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
- "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
- "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
- "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm"},
+ "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"},
+ "earmark_parser": {:hex, :earmark_parser, "1.4.12", "b245e875ec0a311a342320da0551da407d9d2b65d98f7a9597ae078615af3449", [:mix], [], "hexpm", "711e2cc4d64abb7d566d43f54b78f7dc129308a63bc103fbd88550d2174b3160"},
+ "ex_doc": {:hex, :ex_doc, "0.23.0", "a069bc9b0bf8efe323ecde8c0d62afc13d308b1fa3d228b65bca5cf8703a529d", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f5e2c4702468b2fd11b10d39416ddadd2fcdd173ba2a0285ebd92c39827a5a16"},
+ "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
+ "makeup_elixir": {:hex, :makeup_elixir, "0.15.0", "98312c9f0d3730fde4049985a1105da5155bfe5c11e47bdc7406d88e01e4219b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "75ffa34ab1056b7e24844c90bfc62aaf6f3a37a15faa76b07bc5eba27e4a8b4a"},
+ "memento": {:hex, :memento, "0.3.1", "b2909390820550d8b90b68ec96f9e15ff8a45a28b6f97fa4a62ef50e87c2f9d9", [:mix], [], "hexpm", "ff8fc66255d21dcd539c5d77a0b5458715bf3efec91b389dd06017bbb4e2e916"},
+ "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
}