Chime.Notifier behaviour (Chime v0.1.0-dev)

Copy Markdown View Source

A kind of notification of the app: who it goes to, by which channels, and what it says in each. Chime has none of its own; the app writes one module for each kind it has.

defmodule MyApp.Notifications.NewComment do
  use Chime.Notifier, category: :comments, group_window: {10, :minute}

  deliver :feed
  deliver :email, wait: {10, :minute}, unless: :read
  deliver :telegram, if: :telegram?

  @impl true
  def recipients(%{comment: comment}),
    do: MyApp.Stories.followers(comment.story_id) -- [comment.author]

  @impl true
  def data(%{comment: comment}) do
    %{"story_id" => comment.story_id, "story" => comment.story.title, "text" => comment.text}
  end

  @impl true
  def actor(%{comment: comment}),
    do: %{"id" => "user:#{comment.author.id}", "name" => comment.author.name}

  @impl true
  def group(%{comment: comment}), do: "story:#{comment.story_id}"

  @impl true
  def feed(data, notification) do
    %{
      title: "#{Chime.actors(notification)} commented on #{data["story"]}",
      body: data["text"],
      url: "/stories/#{data["story_id"]}"
    }
  end

  @impl true
  def email(data, _recipient, notification) do
    Swoosh.Email.new(
      subject: "#{Chime.actors(notification)} commented on #{data["story"]}",
      text_body: data["text"]
    )
  end

  def telegram?(_data, recipient), do: recipient.data.telegram_chat != nil
end

Then, where the comment is made:

Chime.notify(MyApp.Notifications.NewComment, %{comment: comment},
  key: "comment:#{comment.id}")

Options of use

  • :category - the category of Chime.Config it is of, which the preferences turn on and off (required).
  • :kind - its name in the JSON API and the components ("new_comment" for MyApp.Notifications.NewComment).
  • :required - it goes whatever the person's preferences, and its emails have no link to unsubscribe (false; a category of required: true makes all its notifiers so).
  • :notify_actor - whether the person who did it (actor/1) is notified too, when they are among the recipients (false: nobody is told of what they did).
  • :audience_key - with audience/1: the field of the query's first binding its pages go by (:id).
  • :group_window - with group/1: how long a notification stays open to the events of its group, from the first ({1, :hour}).

Channels

deliver/2 declares a channel, in the order they go: :feed, the bell and its list in the app (decided when the notification comes, with :if and :unless of the notifier's functions only); :email, through the app's mailer; or one of the app's :channels (Chime.Channel). Options:

  • :wait - how long after the notification it goes: {10, :minute} (:second, :minute, :hour, :day) or a number of seconds. What is decided then (the conditions, the preferences) is decided at that time.
  • :unless - it does not go if: :read (the person read it in the feed meanwhile), :seen (they saw it in the feed), or the name of a function of the notifier, (data, recipient) -> boolean. A list is any of them.
  • :if - it goes only if this function of the notifier, (data, recipient) -> boolean, says so; or {module, function}.
  • :throttle - at most one by the channel for a group (or, with no group, for the notifier) in this time: {15, :minute}. One that comes sooner waits for the end of it.
  • :debounce - with :wait: each event that joins the group while it waits moves its time to :wait after that event, so a quick run of events (messages a minute apart) goes as one. :max_wait is the most it waits from the first (none).
  • :quiet - false sends it in the person's quiet hours too (:quiet_hours of Chime.Config). A required notification always goes.

What it says

The feed and the emails are made from data/1, kept on the event as JSON when the notification comes, and rendered when they are shown: in the person's language, as the app's words are then, and with the count of a group ("Ana and 4 others"). Each channel calls its function of the notifier: feed/2, email/3, and, for an app's channel, the function of its name (telegram/3) when there is one.

See the Notifiers guide.

Summary

Types

What the feed shows of a notification.

The params given to Chime.notify/3.

Callbacks

Takes an action of the feed's :actions (Chime.act/3): with its key, the data and the notification (notification.recipient is who takes it). :ok, or {:ok, url} for where the person goes next; an error leaves the notification as it was.

Who did it: %{"id" => ..., "name" => ...}, or nil (nil).

Everyone it goes to, for many people: a query of the app's (Ecto.Query), from the event's data, instead of recipients/1. Chime goes through it in Oban jobs, 1000 people a page, by the key of the query's first binding (:audience_key of use, :id), and makes each key a person's id with recipient_id/1

What is kept of the params, as JSON (a map of strings, numbers, lists and maps), for the feed and the channels to render from. By default, the params themselves, as JSON.

The email: a Swoosh.Email with its subject and body. Chime gives it its To: and, unless the notification is required, the link and the headers to unsubscribe; its From: is the config's unless it has one.

What the feed shows: :title, and :body, :url, :icon, and :actions, the buttons of the notification: [%{key: "accept", label: "Accept"}, %{key: "decline", label: "Decline"}], which act/3 takes. Once the person took one, notification.action is its key.

The group its notifications join: a string, such as "story:42", or nil. A person's notification of the same group, unread and within the :group_window, takes the new event instead of a new notification: its count goes up, its actors take the new one, and it comes to the top of the feed. By default, nil: no groups.

Whether a person is notified at all: the guard of the app's rules, such as "may this person read the channel?", checked for each person when the notification comes, with the params of Chime.notify/3 and the Chime.Recipient (as the resolver gives them). A person it says no to gets nothing, by any channel. By default, everyone.

Params to see the notifier with, in the admin's previews (Chime.Router.Admin): a map, as Chime.notify/3 gets them, or a list of {name, params} for more than one. They are given to data/1 and actor/1, so they can be plain maps of what those read

The id of a person of the audience, from its key (the key as a string).

Who it goes to: the app's users (turned into recipients by Chime.Resolver.recipient/1), Chime.Recipients, or their ids. Chime.notify/3 with :to does not call it.

The workspace it happens in: the id of a team, an organisation, a collective of the app's, as a string, or nil (nil). Its notifications are of that workspace: a feed, counts and preferences can be of one workspace (see the Workspaces guide). Chime.notify/3 with :workspace gives it instead.

Functions

Declares a channel, with its options (see the moduledoc).

Types

feed()

@type feed() :: %{
  :title => String.t(),
  optional(:body) => String.t() | nil,
  optional(:url) => String.t() | nil,
  optional(:icon) => String.t() | nil,
  optional(atom()) => term()
}

What the feed shows of a notification.

params()

@type params() :: term()

The params given to Chime.notify/3.

Callbacks

act(action, data, t)

(optional)
@callback act(action :: String.t(), data :: map(), Chime.Notification.t()) ::
  :ok | {:ok, String.t()} | {:error, term()}

Takes an action of the feed's :actions (Chime.act/3): with its key, the data and the notification (notification.recipient is who takes it). :ok, or {:ok, url} for where the person goes next; an error leaves the notification as it was.

actor(params)

(optional)
@callback actor(params()) :: map() | nil

Who did it: %{"id" => ..., "name" => ...}, or nil (nil).

audience(data)

(optional)
@callback audience(data :: map()) :: Ecto.Queryable.t()

Everyone it goes to, for many people: a query of the app's (Ecto.Query), from the event's data, instead of recipients/1. Chime goes through it in Oban jobs, 1000 people a page, by the key of the query's first binding (:audience_key of use, :id), and makes each key a person's id with recipient_id/1:

@impl true
def audience(_data), do: from(u in MyApp.User, where: not is_nil(u.confirmed_at))

@impl true
def recipient_id(id), do: "user:#{id}"

The guard (notify?/2) is not asked of an audience: its people are the query's.

data(params)

(optional)
@callback data(params()) :: map()

What is kept of the params, as JSON (a map of strings, numbers, lists and maps), for the feed and the channels to render from. By default, the params themselves, as JSON.

email(data, t, t)

(optional)
@callback email(data :: map(), Chime.Recipient.t(), Chime.Notification.t()) :: struct()

The email: a Swoosh.Email with its subject and body. Chime gives it its To: and, unless the notification is required, the link and the headers to unsubscribe; its From: is the config's unless it has one.

feed(data, t)

(optional)
@callback feed(data :: map(), Chime.Notification.t()) :: feed()

What the feed shows: :title, and :body, :url, :icon, and :actions, the buttons of the notification: [%{key: "accept", label: "Accept"}, %{key: "decline", label: "Decline"}], which act/3 takes. Once the person took one, notification.action is its key.

group(params)

(optional)
@callback group(params()) :: String.t() | nil

The group its notifications join: a string, such as "story:42", or nil. A person's notification of the same group, unread and within the :group_window, takes the new event instead of a new notification: its count goes up, its actors take the new one, and it comes to the top of the feed. By default, nil: no groups.

notify?(params, t)

(optional)
@callback notify?(params(), Chime.Recipient.t()) :: boolean()

Whether a person is notified at all: the guard of the app's rules, such as "may this person read the channel?", checked for each person when the notification comes, with the params of Chime.notify/3 and the Chime.Recipient (as the resolver gives them). A person it says no to gets nothing, by any channel. By default, everyone.

preview()

(optional)
@callback preview() :: term() | [{String.t(), term()}]

Params to see the notifier with, in the admin's previews (Chime.Router.Admin): a map, as Chime.notify/3 gets them, or a list of {name, params} for more than one. They are given to data/1 and actor/1, so they can be plain maps of what those read:

@impl true
def preview do
  [
    {"A comment", %{comment: %{story_id: 1, story: %{title: "Tokyo"}, text: "Lovely.", author: %{id: 7, name: "Ana"}}}},
    {"A long one", %{comment: %{story_id: 1, story: %{title: "Tokyo"}, text: String.duplicate("Lovely. ", 40), author: %{id: 7, name: "Ana"}}}}
  ]
end

recipient_id(term)

(optional)
@callback recipient_id(term()) :: String.t()

The id of a person of the audience, from its key (the key as a string).

recipients(params)

(optional)
@callback recipients(params()) :: [term()]

Who it goes to: the app's users (turned into recipients by Chime.Resolver.recipient/1), Chime.Recipients, or their ids. Chime.notify/3 with :to does not call it.

workspace(params)

(optional)
@callback workspace(params()) :: String.t() | nil

The workspace it happens in: the id of a team, an organisation, a collective of the app's, as a string, or nil (nil). Its notifications are of that workspace: a feed, counts and preferences can be of one workspace (see the Workspaces guide). Chime.notify/3 with :workspace gives it instead.

Functions

deliver(channel, opts \\ [])

(macro)

Declares a channel, with its options (see the moduledoc).