Current section
Files
Jump to
Current section
Files
lib/web_push.ex
defmodule WebPush do
@moduledoc """
Web Push sender: encrypts a JSON payload with `aes128gcm`
(`WebPush.Encryption`), signs a VAPID JWT (`WebPush.Vapid`), and POSTs the
binary body to the subscription's push service endpoint via Finch.
This is a raw library: it starts no processes. The host application must
supervise a Finch pool and tell WebPush its name:
# in your application.ex
children = [
{Finch, name: MyApp.WebPushFinch}
]
# in config
config :web_push, finch: MyApp.WebPushFinch
The pool name can also be passed per call via the `:finch` option.
## Return values
* `:ok`: push service accepted (HTTP 200/201/202/204)
* `{:error, :gone}`: subscription no longer valid (404/410); the caller
should delete the stored subscription
* `{:error, term}`: transport failure or unexpected status
"""
require Logger
alias WebPush.{Encryption, Subscription, Vapid}
@default_ttl 86_400
@type send_result :: :ok | {:error, :gone} | {:error, term()}
@doc """
Sends `payload` (JSON-encoded) to `subscription`.
## Options
* `:ttl`: seconds the push service may hold the message (default 86400)
* `:urgency`: `"very-low" | "low" | "normal" | "high"` (default `"normal"`)
* `:topic`: URL-safe string ≤ 32 chars; lets the push service collapse
pending messages with the same topic into the latest one
* `:finch`: Finch pool name, overrides `config :web_push, :finch`
"""
@spec send(Subscription.t(), map(), keyword()) :: send_result()
def send(%Subscription{} = sub, payload, opts \\ []) when is_map(payload) do
body = JSON.encode!(payload)
encrypted = Encryption.encrypt(body, sub.p256dh, sub.auth)
headers = build_headers(sub.endpoint, opts)
req = Finch.build(:post, sub.endpoint, headers, encrypted)
case Finch.request(req, finch_name(opts)) do
{:ok, %{status: status}} when status in [200, 201, 202, 204] ->
:ok
{:ok, %{status: status}} when status in [404, 410] ->
Logger.info("[web_push] subscription gone (#{status})")
{:error, :gone}
{:ok, %{status: status, body: body}} ->
Logger.warning("[web_push] push failed #{status}: #{inspect(body)}")
{:error, {:http, status}}
{:error, reason} ->
Logger.warning("[web_push] transport failure: #{inspect(reason)}")
{:error, reason}
end
end
defp build_headers(endpoint, opts) do
ttl = Keyword.get(opts, :ttl, @default_ttl)
urgency = Keyword.get(opts, :urgency, "normal")
topic = Keyword.get(opts, :topic)
base = [
{"authorization", Vapid.authorization_header(endpoint)},
{"content-type", "application/octet-stream"},
{"content-encoding", "aes128gcm"},
{"ttl", Integer.to_string(ttl)},
{"urgency", urgency}
]
if topic, do: [{"topic", topic} | base], else: base
end
defp finch_name(opts) do
Keyword.get(opts, :finch) ||
Application.get_env(:web_push, :finch) ||
raise """
No Finch pool configured for WebPush.
Supervise one in your application and set:
config :web_push, finch: MyApp.WebPushFinch
or pass the `:finch` option to WebPush.send/3.
"""
end
end