The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

Mojolicious::Plugin::WebPush - plugin to aid real-time web push

SYNOPSIS

  # Mojolicious::Lite
  my $sw = plugin 'ServiceWorker' => { debug => 1 };
  my $webpush = plugin 'WebPush' => {
    save_endpoint => '/api/savesubs',
    subs_session2user_p => \&subs_session2user_p,
    subs_create_p => \&subs_create_p,
    subs_read_p => \&subs_read_p,
    subs_delete_p => \&subs_delete_p,
    ecc_private_key => 'vapid_private_key.pem',
    claim_sub => "mailto:admin@example.com",
  };

  sub subs_session2user_p {
    my ($c, $session) = @_;
    return Mojo::Promise->reject("Session not logged in") if !$session->{user_id};
    Mojo::Promise->resolve($session->{user_id});
  }

  sub subs_create_p {
    my ($c, $session, $subs_info) = @_;
    app->db->save_subs_p($session->{user_id}, $subs_info);
  }

  sub subs_read_p {
    my ($c, $user_id) = @_;
    app->db->lookup_subs_p($user_id);
  }

  sub subs_delete_p {
    my ($c, $user_id) = @_;
    app->db->delete_subs_p($user_id);
  }

DESCRIPTION

Mojolicious::Plugin::WebPush is a Mojolicious plugin. In order to function, your app needs to have first installed Mojolicious::Plugin::ServiceWorker as shown in the synopsis above.

METHODS

Mojolicious::Plugin::WebPush inherits all methods from Mojolicious::Plugin and implements the following new ones.

register

  my $p = $plugin->register(Mojolicious->new, \%conf);

Register plugin in Mojolicious application, returning the plugin object. Takes a hash-ref as configuration, see "OPTIONS" for keys.

OPTIONS

save_endpoint

Required. The route to be added to the app for the service worker to register users for push notification. The handler for that will call the "subs_create_p". If success is indicated, it will return JSON:

  { "data": { "success": true } }

If failure:

  { "errors": [ { "message": "The exception reason" } ] }

This will be handled by the provided service worker. In case it is required by the app itself, the added route is named webpush.save.

subs_session2user_p

Required. The code to be called to look up the user currently identified by this session, which returns a promise of the user ID. Must reject if no user logged in and that matters. It will be passed parameters:

subs_create_p

Required. The code to be called to store users registered for push notifications, which must return a promise of a true value if the operation succeeds, or reject with a reason. It will be passed parameters:

  • The ID to correctly identify the user. Please note that you ought to allow one person to have several devices with web-push enabled, and to design accordingly.

  • The subscription_info hash-ref, needed to push actual messages.

subs_read_p

Required. The code to be called to look up a user registered for push notifications. It will be passed parameters:

  • The opaque information your app uses to identify the user.

Returns a promise of the subscription_info hash-ref. Must reject if not found.

subs_delete_p

Required. The code to be called to delete up a user registered for push notifications. It will be passed parameters:

  • The opaque information your app uses to identify the user.

Returns a promise of the deletion result. Must reject if not found.

ecc_private_key

A value to be passed to "new" in Crypt::PK::ECC: a simple scalar is a filename, a scalar-ref is the actual key. If not provided, "webpush.authorization" will (obviously) not be able to function.

claim_sub

A value to be used as the sub claim by the "webpush.authorization", which needs it. Must be either an HTTPS or mailto: URL.

claim_exp_offset

A value to be added to current time, in seconds, in the exp claim for "webpush.authorization". Defaults to 86400 (24 hours). The maximum valid value in RFC 8292 is 86400.

push_handler

Override the default push-event handler supplied to "add_event_listener" in Mojolicious::Plugin::ServiceWorker. The default will interpret the message as a JSON object. The key title will be the notification title, deleted from that object, then the object will be the options passed to <ServiceWorkerRegistration>.showNotification.

See https://developers.google.com/web/fundamentals/push-notifications/handling-messages for possibilities.

HELPERS

webpush.create_p

  $c->webpush->create_p($user_id, $subs_info)->then(sub {
    $c->render(json => { data => { success => \1 } });
  });

webpush.read_p

  $c->webpush->read_p($user_id)->then(sub {
    $c->render(text => 'Info: ' . to_json(shift));
  });

webpush.delete_p

  $c->webpush->delete_p($user_id)->then(sub {
    $c->render(json => { data => { success => \1 } });
  });

webpush.authorization

  my $header_value = $c->webpush->authorization($subs_info);

Won't function without "claim_sub" and "ecc_private_key", or $subs_info having a valid URL to get the base of as the aud claim. Returns a suitable Authorization header value to send to a push service. Valid for a period defined by "claim_exp_offset". but could become so to avoid unnecessary computation.

webpush.public_key

  my $pkey = $c->webpush->public_key;

Gives the app's public VAPID key, calculated from the private key.

webpush.verify_token

  my $bool = $c->webpush->verify_token($authorization_header_value);

Cryptographically verifies a JSON Web Token (JWT), such as generated by "webpush.authorization".

webpush.encrypt

  use MIME::Base64 qw(decode_base64url);
  my $ciphertext = $c->webpush->encrypt($data_bytes,
    map decode_base64url($_), @{$subscription_info->{keys}}{qw(p256dh auth)}
  );

Returns the data encrypted according to RFC 8188, for the relevant subscriber.

webpush.send_p

  my $result_p = $c->webpush->send_p($jsonable_data, $user_id, $ttl, $urgency);

JSON-encodes the given value, encrypts it according to the given user's subscription data, adds a VAPID Authorization header, then sends it to the relevant web-push endpoint.

Returns a promise of the result, which will be a hash-ref with either a data key indicating success, or an errors key for an array-ref of hash-refs with a message giving reasons.

If the sending gets a status code of 404 or 410, this indicates the subscriber has unsubscribed, and "webpush.delete_p" will be used to remove the registration. This is considered success.

The urgency must be one of very-low, low, normal (the default) or high. The ttl defaults to 30 seconds.

TEMPLATES

Various templates are available for including in the app's templates:

webpush-askPermission.html.ep

JavaScript functions, also for putting inside a script element:

  • askPermission

  • subscribeUserToPush

  • sendSubscriptionToBackEnd

These each return a promise, and should be chained together:

  <button onclick="
    askPermission().then(subscribeUserToPush).then(sendSubscriptionToBackEnd)
  ">
    Ask permission
  </button>
  <script>
  %= include 'serviceworker-install'
  %= include 'webpush-askPermission'
  </script>

Each application must decide when to ask such permission, bearing in mind that once permission is refused, it is very difficult for the user to change such a refusal.

When it is granted, the JavaScript code will communicate with the application, registering the needed information needed to web-push.

SEE ALSO

Mojolicious, Mojolicious::Guides, https://mojolicious.org.

Mojolicious::Command::webpush - command-line control of web-push.

RFC 8292 - Voluntary Application Server Identification (for web push).

Crypt::RFC8188 - Encrypted Content-Encoding for HTTP (using aes128gcm).

https://developers.google.com/web/fundamentals/push-notifications

ACKNOWLEDGEMENTS

Part of this code is ported from https://github.com/web-push-libs/pywebpush.