Packages

Event-driven notification system for Ash Framework with multiple transport types

Current section

Files

Jump to
ash_dispatch lib channel.ex
Raw

lib/channel.ex

defmodule AshDispatch.Channel do
@moduledoc """
Represents a delivery channel for an event.
A channel specifies:
- **Transport** - How to deliver (email, in_app, discord, etc.)
- **Audience** - Who receives (:user, :admin, custom)
- **Time** - When to deliver (immediate, delayed, scheduled)
- **Policy** - Delivery policy (:always, :skip_if_read)
- **Variant** - Template variant (e.g., :admin for admin-specific templates)
## Examples
%Channel{
transport: :email,
audience: :user,
time: {:in, 300}, # Delay 5 minutes
policy: :skip_if_read
}
%Channel{
transport: :discord,
audience: :admin,
time: :immediate,
opts: %{webhook_url: "https://..."}
}
"""
@type transport :: :email | :in_app | :discord | :sms | :slack | :webhook | atom()
@type audience :: :user | :admin | atom()
@type time ::
:immediate
| {:in, non_neg_integer()}
| {:at, DateTime.t()}
| {:window, map()}
@type policy :: :always | :skip_if_read | {:gate, function()}
@type variant :: atom() | nil
@type t :: %__MODULE__{
transport: transport(),
audience: audience(),
time: time(),
policy: policy(),
variant: variant(),
locale: String.t() | nil,
locale_from: atom() | nil,
locales: [String.t()],
webhook_url: String.t() | nil,
content: map(),
metadata: map(),
opts: map(),
load: [atom() | {atom(), any()}],
deduplicate_group: atom() | nil,
optional: boolean(),
exclude_actor: boolean()
}
@enforce_keys [:transport, :audience]
defstruct [
:transport,
:audience,
:variant,
:locale,
:locale_from,
:webhook_url,
:deduplicate_group,
time: {:in, 0},
policy: :always,
optional: false,
exclude_actor: false,
content: %{},
metadata: %{},
opts: %{},
load: [],
locales: []
]
@doc """
Creates a new channel.
## Examples
iex> Channel.new(:email, :user)
%Channel{transport: :email, audience: :user, time: {:in, 0}}
iex> Channel.new(:email, :user, time: 5.minutes(), skip_if_read: true)
%Channel{
transport: :email,
audience: :user,
time: {:in, 300},
policy: :skip_if_read
}
"""
def new(transport, audience, opts \\ []) do
time = Keyword.get(opts, :time, {:in, 0})
policy = if Keyword.get(opts, :skip_if_read), do: :skip_if_read, else: :always
%__MODULE__{
transport: transport,
audience: audience,
time: normalize_time(time),
policy: Keyword.get(opts, :policy, policy),
variant: Keyword.get(opts, :variant),
webhook_url: Keyword.get(opts, :webhook_url),
opts: Keyword.get(opts, :opts, %{})
}
end
@doc """
Helper function to create a channel struct.
Used in DSL and event modules for cleaner syntax.
## Examples
def channels(_ctx) do
[
channel(:in_app, :user),
channel(:email, :user, time: 5.minutes(), skip_if_read: true)
]
end
"""
def channel(transport, audience, opts \\ []) do
new(transport, audience, opts)
end
@doc """
Normalizes time specification to internal format.
## Examples
iex> Channel.normalize_time(:immediate)
{:in, 0}
iex> Channel.normalize_time(300)
{:in, 300}
iex> Channel.normalize_time({:in, 300})
{:in, 300}
"""
def normalize_time(:immediate), do: {:in, 0}
def normalize_time(seconds) when is_integer(seconds), do: {:in, seconds}
def normalize_time({:in, _} = time), do: time
def normalize_time({:at, %DateTime{}} = time), do: time
def normalize_time({:window, _} = time), do: time
def normalize_time(time), do: time
@doc """
Calculates delay in seconds from time specification.
## Examples
iex> Channel.calculate_delay(%Channel{time: {:in, 300}})
300
iex> Channel.calculate_delay(%Channel{time: :immediate})
0
"""
def calculate_delay(%__MODULE__{time: {:in, seconds}}), do: seconds
def calculate_delay(%__MODULE__{time: :immediate}), do: 0
def calculate_delay(%__MODULE__{time: {:at, datetime}}) do
DateTime.diff(datetime, DateTime.utc_now(), :second)
end
def calculate_delay(%__MODULE__{time: {:window, _window}}) do
# TODO: Implement business hours calculation
# For now, send immediately if in window, or delay to window start
0
end
def calculate_delay(_), do: 0
@doc """
Checks if a channel matches given transport and/or audience filters.
## Examples
iex> channel = %Channel{transport: :in_app, audience: :user}
iex> Channel.matches?(channel, transport: :in_app)
true
iex> Channel.matches?(channel, transport: :email)
false
iex> Channel.matches?(channel, transport: :in_app, audience: :user)
true
"""
def matches?(%__MODULE__{} = channel, filters) do
Enum.all?(filters, fn
{:transport, transport} -> channel.transport == transport
{:audience, audience} -> channel.audience == audience
{:variant, variant} -> channel.variant == variant
_ -> true
end)
end
end