howdy/notifications/web_push
Web Push: notifications from the browser itself, which reach people whose tabs are closed.
import howdy/notifications/web_push
let assert Ok(public) = env.get("VAPID_PUBLIC_KEY")
let assert Ok(private) = env.get("VAPID_PRIVATE_KEY")
let assert Ok(keys) = web_push.keys(public:, private:)
let push = web_push.new(notifier, keys, contact: "mailto:ops@example.com")
let notifier = notifications.with_push(notifier, web_push.deliver(push))
howdy.new()
|> howdy.controller(web_push.routes(push, at: "/notifications/push", identify:))
Make the keys once with gleam run -m howdy/notifications/web_push and
keep them in the environment: browsers subscribe with the public key,
and every subscription stops working if it changes.
A user turns push on in each browser they want it in. howdy/ui/bell
offers that in its panel when given the routes’ path with bell.push:
the browser registers the service worker served by routes, subscribes
with the push service its maker runs (Google’s for Chrome, Mozilla’s for
Firefox, Apple’s for Safari), and the app keeps the subscription. A
notification then goes to each of the user’s subscriptions, encrypted
so that only that browser can read it (RFC 8291), and signed with the
app’s keys (RFC 8292). Clicking it opens its link.
The app sends to whatever address a subscription gives, so only the
push services of the major browsers are accepted, by default; see
allow_endpoints. Subscriptions the push service says have gone are
deleted.
Types
The app’s VAPID key pair, which identifies it to push services.
pub opaque type Keys
Where and how to reach one browser, as its PushSubscription gives it.
pub type Subscription {
Subscription(endpoint: String, p256dh: String, auth: String)
}
Constructors
-
Subscription(endpoint: String, p256dh: String, auth: String)
Values
pub fn allow_endpoints(
push: WebPush,
allow: fn(uri.Uri) -> Bool,
) -> WebPush
Accept subscriptions whose endpoint allow accepts, as well as or
instead of the major browsers’ push services. Endpoints are always
https.
pub fn deliver(
push: WebPush,
) -> fn(notifications.Notification) -> notifications.Outcome
Deliver a notification to every browser its recipient turned push on
in, for notifications.with_push. A recipient with none is done at
once. Subscriptions the push service says have gone are deleted.
If any browser’s push service asks to be tried later, the whole notification is tried again, so the others may get it twice; it is sent with its ID as the tag, so the browser shows it once.
pub fn generate_keys() -> Keys
A new key pair. Make one once and keep it; see the module docs.
pub fn keys(
public public: String,
private private: String,
) -> Result(Keys, Nil)
Keys as generate_keys makes them and public_key and private_key
write them: base64url, the public key an uncompressed P-256 point and
the private key its 32 byte scalar. An error when they are not, or do
not belong together.
pub fn known_push_service(endpoint: uri.Uri) -> Bool
Whether an endpoint belongs to the push service of Chrome, Edge,
Firefox or Safari. Chromium builds other than Google’s own, such as
Linux distributions’, subscribe through jmt17.google.com.
pub fn main() -> Nil
Print a new key pair, as environment variables:
gleam run -m howdy/notifications/web_push.
pub fn new(
notifier: notifications.Notifier,
keys: Keys,
contact contact: String,
) -> WebPush
Push for notifier’s users. contact is a mailto: or https: URL
a push service can use to reach whoever runs the app. Panics on any
other: Apple’s push service refuses every notification signed without
one. Make it a real address, not one on localhost, which Apple also
refuses.
pub fn private_key(keys: Keys) -> String
The private key, base64url. Keep it secret.
pub fn public_key(keys: Keys) -> String
The public key, base64url, which browsers subscribe with.
pub fn routes(
push: WebPush,
at prefix: String,
identify identify: fn(context.Context(Nil)) -> Result(
String,
service.Error,
),
) -> controller.Builder(Nil)
What browsers need to turn push on and off, for bell.push or a page
of your own. identify names the signed-in user, as for
notifications.routes, and must refuse cross-site writes.
GET /worker.jsis the service worker, which shows each notification and opens its link when it is clicked.GET /keygives{"key": "..."}, the public key to subscribe with.POST /subscriptionskeeps the JSON of a browser’s subscription for the user.DELETE /subscriptionsforgets one, given{"endpoint": "..."}.
pub fn send(
push: WebPush,
subscription: Subscription,
message: String,
) -> notifications.Outcome
Encrypt and send one message to one browser.
pub fn service_worker() -> String
The service worker’s source. It shows each push as a notification, tagged with the notification’s ID, and on a click focuses a tab already at its link or opens one.
pub fn subscribe(
push: WebPush,
recipient: String,
subscription: Subscription,
) -> Result(Nil, service.Error)
Keep a browser’s subscription for recipient. A subscription is one
browser, so one already kept for someone else, after they signed out
on a shared computer, moves to recipient.
pub fn subscription_decoder() -> decode.Decoder(Subscription)
The JSON a browser’s subscription.toJSON() gives.
pub fn subscriptions(
push: WebPush,
recipient: String,
) -> Result(List(Subscription), service.Error)
Every browser recipient has turned push on in.
pub fn time_to_live(push: WebPush, seconds: Int) -> WebPush
How long a push service keeps a notification for a browser that is offline, in seconds. Defaults to a day.
pub fn unsubscribe(
push: WebPush,
recipient: String,
endpoint: String,
) -> Result(Nil, service.Error)
Forget a browser’s subscription, if it is recipient’s.