A channel is a way to tell a person. Chime has the feed, email, and push (see Push). Your app adds its own.
The feed
The bell and its list in your app: deliver :feed. A notification is in
the feed at once, unless the person turned the feed off for its
category. Its feed/2 says what it shows: :title, and :body, :url
and :icon, and anything else, which the JSON API gives as extra.
deliver :email, with the config's :mailer (a Swoosh.Mailer) and
:email_from. The notifier's email/3 gives the subject and the body,
as a Swoosh.Email; Chime gives it:
To:, the person's address and name (a person with no address is skipped, and the log says so);From:, the config's, unless the email has one;- unless it is required, the headers of one-click unsubscribe (RFC 8058),
List-UnsubscribeandList-Unsubscribe-Post, which Gmail and Yahoo ask of a sender of many emails, and the same address in the email's assigns as:unsubscribe_url, for its footer.
The mailer's id of the email is kept as the delivery's ref.
Read everywhere
A link of an email that opens the notification reads it in the feed too:
~s(<a href="#{Chime.link(notification, "/stories/42")}">Read it</a>)Chime.link/2 is an address of chime_mail, signed: opened, it marks
the notification read and its clicked_at, tells the person's open
pages, and redirects to the address (a path goes on the config's
:url). Each email has the link of the feed's :url in its assigns, as
:link, and the digests' links are such links. A channel of yours can
use Chime.link/2 the same way.
A notifier's email in your app's templates:
def email(data, recipient, notification) do
Swoosh.Email.new(subject: "#{Chime.actors(notification)} commented on #{data["story"]}")
|> Swoosh.Email.html_body(
MyAppWeb.Emails.new_comment(
data: data,
recipient: recipient,
unsubscribe_url: Chime.unsubscribe_url(recipient, notification)
)
)
endSlack, Discord, Teams, webhooks and SMS
Chime has these too, each a module you name in the config, with its options:
{Chime, ...,
channels: [
slack: {Chime.Channels.Slack, token: System.fetch_env!("SLACK_BOT_TOKEN")},
discord: {Chime.Channels.Discord, to: System.fetch_env!("DISCORD_WEBHOOK_URL")},
teams: {Chime.Channels.Teams, to: System.fetch_env!("TEAMS_WEBHOOK_URL")},
webhook: {Chime.Channels.Webhook, to: "https://hooks.myapp.com/chime", secret: System.fetch_env!("CHIME_WEBHOOK_SECRET")},
sms:
{Chime.Channels.Twilio,
account_sid: System.fetch_env!("TWILIO_ACCOUNT_SID"),
auth_token: System.fetch_env!("TWILIO_AUTH_TOKEN"),
from: "+15551234567"}
]}| Module | Where it writes |
|---|---|
Chime.Channels.Slack | with a bot's :token, chat.postMessage to the person's Slack id (data.slack) or a channel (to: "#ops"); without, an incoming webhook's URL |
Chime.Channels.Discord | a channel's webhook URL: to: for everyone, or data.discord |
Chime.Channels.Teams | a workflow's or an incoming webhook's URL, as an Adaptive Card |
Chime.Channels.Webhook | your address, the notification as JSON, signed with :secret (x-chime-signature) |
Chime.Channels.Twilio | the person's phone (data.phone, E.164), from :from or a :messaging_service_sid |
A person's address is in their recipient's data, under the channel's name
(to: {:data, :key} for another key), or one address for everyone with
to: "...": Slack and Discord for your team's own alerts, for example.
A person with none is skipped, and the log says so.
Each says the notifier's function of the channel's name when it has one
(slack/3, sms/3: a text, or %{title:, body:, url:}), else the
feed's words, with the link that reads the notification. A refusal that
will not change (an address gone, a number that replied STOP) is
skipped; the rest is tried again. They send with Req: add
{:req, "~> 0.5"} to your deps.
Your own
Telegram, SMS, Slack, Discord, your own push: a module of
Chime.Channel, named in the config.
{Chime, ..., channels: [telegram: {MyApp.Chime.Telegram, token: System.fetch_env!("TELEGRAM_TOKEN")}]}defmodule MyApp.Chime.Telegram do
@behaviour Chime.Channel
@impl true
def deliver(%Chime.Message{} = message, opts) do
case message.recipient.data.telegram_chat do
nil ->
{:skip, "the person has no Telegram chat"}
chat ->
text = Chime.Message.render(message)
case MyApp.Telegram.send(opts[:token], chat, text) do
{:ok, %{"message_id" => id}} -> {:ok, to_string(id)}
{:error, :blocked} -> {:skip, "the person blocked the bot"}
{:error, reason} -> {:error, reason}
end
end
end
enddeliver/2 gets the Chime.Message: the notification, the person as
the resolver gives them now, the notifier's data, and whether it is
required. Chime.Message.render/1 calls the notifier's function of the
channel's name, telegram(data, recipient, notification), when it has
one, else gives what the feed shows.
It answers:
{:ok, ref}or:ok- sent;refis kept;{:skip, reason}- not sent, on purpose: not tried again, and the reason goes in the log;{:error, reason}- failed: Oban tries again with its backoff, five attempts in all, and the delivery is"failed"after the last.
Whether it goes at all (the preferences, :if, :unless) is decided
before deliver/2 is called.
A channel is in the preferences too: a person can turn it off for a category, as the feed and email.
Rules
Some rules hold a delivery for later. They are decided at its time, and the log says what held it:
# Quick messages go as one email, a minute after the last (at most 5
# after the first); no more than one email of a conversation in 15 minutes.
deliver :email, wait: {1, :minute}, debounce: true, max_wait: {5, :minute}, throttle: {15, :minute}throttle:- at most one by the channel for a group (or, with no group, for the notifier) in a time. One that comes sooner waits for the end of it, and goes as the group is then.debounce: true- withwait:, each event that joins the group moves the delivery's time towaitafter it, so a quick run goes as one;max_wait:is the most it waits from the first.Quiet hours, in the config, in each person's time zone: nothing but the feed goes in them, and what comes waits for the morning. A person's own (
:quiet_hoursof theirChime.Recipient, orfalsefor none) comes first; a channel withquiet: falsegoes all the same.{Chime, ..., quiet_hours: {22, 8}}Limits, in the config: the most each person gets by a channel in a time. One past it waits until they are under it again.
{Chime, ..., limits: [email: {5, :hour}, sms: {3, :day}]}
Required notifications are held by none of them.
When it goes
Each delivery is an Oban job at its time, of the worker Chime.Worker,
given to Oban in the transaction of the notification, so that both are
kept or neither is. Give Chime the queue (oban: [name: Oban, queue: :notifications]) and run it:
config :my_app, Oban, queues: [default: 10, notifications: 20]A delivery is decided and sent once: a second job of it, or a retry after it was sent, does nothing.