Current section

Files

Jump to
amarula lib amarula plugin.ex
Raw

lib/amarula/plugin.ex

defmodule Amarula.Plugin do
@moduledoc """
Req-style plugin pipelines for a connection.
A connection has two pipelines, each an ordered list of **steps** on the
`Amarula.Conn`:
* `send_steps` — run over an outgoing message *before encryption*. Use to
check/authorize a send, transform the message, or drop it.
* `recv_steps` — run over an incoming message *after decryption*, before the
consumer sees it. Use to translate, filter, or drop it.
A **step** is `fn ctx -> {:cont, ctx} | {:halt, reason}`:
* `{:cont, ctx}` — continue with the (possibly transformed) ctx;
* `{:halt, reason}` — stop the pipeline. On send the message is not sent
(`{:error, {:halted, reason}}` to the caller); on receive it is dropped and
never reaches the consumer.
The ctx is a map; both pipelines put the message under `:message`. Send ctx also
carries `:to`/`:profile`/`:msg_id`; receive ctx carries `:from`/`:id`/`:profile`
(`:id` is the message id, for building a `MessageKey` to react/reply).
A **plugin** is a module that appends steps via `attach/2`, Req-style:
defmodule Blocklist do
def attach(conn, opts \\\\ []) do
jids = Keyword.get(opts, :jids, [])
Amarula.Plugin.on_send(conn, fn
%{to: to} = ctx -> if to in jids, do: {:halt, :blocked}, else: {:cont, ctx}
end)
end
end
Amarula.new(config) |> Blocklist.attach(jids: [...]) |> Amarula.connect()
Implementing the `Amarula.Plugin` behaviour (`@behaviour Amarula.Plugin`) is
optional — any module with a matching `attach/2` works in a pipe.
"""
alias Amarula.Conn
@typedoc "A pipeline step."
@type step :: Conn.step()
@doc "Attach the plugin's steps to `conn`, returning the updated `conn`."
@callback attach(Conn.t(), keyword()) :: Conn.t()
@doc "Append a step to the send pipeline (before encrypt)."
@spec on_send(Conn.t(), step()) :: Conn.t()
defdelegate on_send(conn, step), to: Conn, as: :append_send_step
@doc "Append a step to the receive pipeline (after decrypt)."
@spec on_recv(Conn.t(), step()) :: Conn.t()
defdelegate on_recv(conn, step), to: Conn, as: :append_recv_step
@doc """
Run `steps` over `ctx`, threading the ctx through each. Returns `{:cont, ctx}`
if all steps continued (with the final, possibly transformed ctx), or
`{:halt, reason}` at the first step that halted (remaining steps don't run).
"""
@spec run([step()], map()) :: {:cont, map()} | {:halt, term()}
def run(steps, ctx) do
Enum.reduce_while(steps, {:cont, ctx}, fn step, {:cont, ctx} ->
case step.(ctx) do
{:cont, ctx} -> {:cont, {:cont, ctx}}
{:halt, reason} -> {:halt, {:halt, reason}}
end
end)
end
end