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.

Email

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-Unsubscribe and List-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)
    )
  )
end

Slack, 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"}
 ]}
ModuleWhere it writes
Chime.Channels.Slackwith 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.Discorda channel's webhook URL: to: for everyone, or data.discord
Chime.Channels.Teamsa workflow's or an incoming webhook's URL, as an Adaptive Card
Chime.Channels.Webhookyour address, the notification as JSON, signed with :secret (x-chime-signature)
Chime.Channels.Twiliothe 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
end

deliver/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; ref is 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 - with wait:, each event that joins the group moves the delivery's time to wait after 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_hours of their Chime.Recipient, or false for none) comes first; a channel with quiet: false goes 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.