-
Notifications
You must be signed in to change notification settings - Fork 0
Notification subscriptions #87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
pgherveou
wants to merge
26
commits into
main
Choose a base branch
from
notification-subscriptions
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 5 commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
9de935b
notifications: add push_subscribe / push_unsubscribe (RFC 0020)
pgherveou 2ddcc3b
Apply suggestion from @pgherveou
pgherveou b85b3d5
update
pgherveou 8788def
fix: unit struct and doc examples for codegen compatibility
filvecchiato b43c701
Merge branch 'main' into notification-subscriptions
filvecchiato 7290dfa
update
pgherveou 6b26442
unnest
pgherveou a2497d5
simplify diagram
pgherveou a65f55b
Merge remote-tracking branch 'origin/main' into notification-subscrip…
pgherveou b51c50d
Merge branch 'main' into notification-subscriptions
filvecchiato 74daaae
Simplify notification doc examples to use result.match() pattern
filvecchiato 4602d4d
Rename BackendUnavailable to NotificationSystemUnavailable(String) an…
filvecchiato 4cfaa6e
Update RFC worked example to use explicit signer instead of implicit …
filvecchiato 792cd8e
Auto-number RFCs on merge via CI
filvecchiato e762c6e
Revert "Auto-number RFCs on merge via CI"
filvecchiato 5cb2ebe
added broadcast method to RFC0020
SBalaguer 8f3e718
edits
SBalaguer 4e2502c
makes signer mandatory on rules
SBalaguer 5b047d2
Merge pull request #139 from paritytech/sb/rfc0020-contribution
pgherveou efa4e6b
rename
pgherveou da93ed3
refactor text
pgherveou 106b096
Merge branch 'main' into notification-subscriptions
pgherveou dc1b215
simplify
pgherveou 2f73e93
use rfc skill
pgherveou 3a128dd
update specs
pgherveou be1a1e4
Merge branch 'main' into notification-subscriptions
pgherveou File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| --- | ||
| title: "Push Notification Subscriptions" | ||
| type: rfc | ||
| status: draft | ||
| owner: "@pgherveou" | ||
| pr: | ||
| --- | ||
|
|
||
| # RFC 0020 — Push Notification Subscriptions | ||
|
|
||
| ## Summary | ||
|
|
||
| Adds four TrUAPI methods — `push_add_rules`, `push_remove_rules`, `push_list_rules`, `push_set_rules` — that mirror the rule-management endpoints of the [v2 push backend spec](https://hackmd.io/@1JCaGppGSUqHtJilikYaKw/r16YTVg5Ze). A rule is a `(signer, topic)` pair: the host's push backend delivers a push to the user's device(s) whenever a signed statement matching that pair appears on the Statement Store. The product never sees push tokens. | ||
|
|
||
| The method names use `add` / `remove` rather than `subscribe` / `unsubscribe` because the `_subscribe` suffix is reserved for streaming TrUAPI methods (e.g. `statementStore.subscribe`). | ||
|
|
||
| ## References | ||
|
|
||
| - Push notifications, original (v1, peer-to-peer): https://hackmd.io/@1JCaGppGSUqHtJilikYaKw/SyPN2yV6lx | ||
| - Push notifications backend design (v2, backend-mediated): https://hackmd.io/@1JCaGppGSUqHtJilikYaKw/r16YTVg5Ze | ||
|
|
||
| This RFC exposes a TrUAPI-shaped surface over the rule-management API defined in the v2 spec. | ||
|
|
||
| ## Motivation | ||
|
|
||
| The push-notifications v2 design assigns delivery to a host-side push backend that tails the Statement Store, verifies signatures, and delivers pushes only for `(signer, topic)` pairs the user has whitelisted. TrUAPI needs a primitive that lets a product manipulate that whitelist. | ||
|
|
||
| ### Worked example: festival announcements | ||
|
|
||
| A conference product publishes festival-wide announcements as signed statements on a well-known topic. When the user taps "notify me about announcements," the subscriber app calls `push_add_rules({ rules: [{ signer: festival_signer, topic: announcements_topic }] })`. From that point on, the user is woken up for new announcements even with the product closed: | ||
|
|
||
| ``` | ||
| Publisher app Subscriber app | ||
| (organizer side) (attendee side) | ||
| | ^ | | ||
| | | | | ||
| | | | (1) pushAddRules({ | ||
| | (6) push | | rules: [{ | ||
| | back to | | signer: pkPublisher, | ||
| | caller | | topic: T_announcements | ||
| | | | }] | ||
| | | | }) | ||
| | | | | ||
| | | v | ||
| | +------------------------------------+---+------+ | ||
| | | Push backend | | ||
| | | stores rule: | | ||
| | | (pkPublisher, T_announcements) | | ||
| | | -> this subscriber app | | ||
| | +-----------------------+-----------------------+ | ||
| | ^ | ||
| | | (4) tail / match rule | ||
| | | | ||
| | +-----------------------+-----------------------+ | ||
| | | Statement Store | | ||
| | +-----------------------+-----------------------+ | ||
| | ^ | ||
| | (2) compose signed statement | | ||
| |--- (3) statementStore.submit(statement) -+ | ||
| ``` | ||
|
|
||
| ## Detailed Design | ||
|
|
||
| ### API | ||
|
|
||
| Each TrUAPI method mirrors one backend endpoint: | ||
|
|
||
| | TrUAPI method | Backend endpoint | Purpose | | ||
| | ------------------- | -------------------------------- | -------------------------------- | | ||
| | `push_add_rules` | `POST /v1/subscriptions/rules` | add one or more rules | | ||
| | `push_remove_rules` | `DELETE /v1/subscriptions/rules` | remove one or more rules | | ||
| | `push_list_rules` | `GET /v1/subscriptions` | snapshot of currently active set | | ||
| | `push_set_rules` | `PUT /v1/subscriptions/rules` | atomic replace of the full set | | ||
|
|
||
| ```rust | ||
| #[wire(request_id = 134)] | ||
| async fn push_add_rules( | ||
| &self, cx: &CallContext, request: HostPushAddRulesRequest, | ||
| ) -> Result<HostPushAddRulesResponse, CallError<HostPushAddRulesError>>; | ||
|
|
||
| #[wire(request_id = 136)] | ||
| async fn push_remove_rules( | ||
| &self, cx: &CallContext, request: HostPushRemoveRulesRequest, | ||
| ) -> Result<HostPushRemoveRulesResponse, CallError<HostPushRemoveRulesError>>; | ||
|
|
||
| #[wire(request_id = 138)] | ||
| async fn push_list_rules( | ||
| &self, cx: &CallContext, request: HostPushListRulesRequest, | ||
| ) -> Result<HostPushListRulesResponse, CallError<HostPushListRulesError>>; | ||
|
|
||
| #[wire(request_id = 140)] | ||
| async fn push_set_rules( | ||
| &self, cx: &CallContext, request: HostPushSetRulesRequest, | ||
| ) -> Result<HostPushSetRulesResponse, CallError<HostPushSetRulesError>>; | ||
| ``` | ||
|
|
||
| ### Types | ||
|
|
||
| `Topic` is reused from `v01::statement_store`. | ||
|
|
||
| ```rust | ||
| pub type StatementSigner = [u8; 32]; | ||
|
|
||
| /// A single (signer, topic) rule the user wants to be woken up for. | ||
| /// | ||
| /// At the host level the effective key is (product, signer, topic): rules | ||
| /// are scoped per calling product, so two products can register the same | ||
| /// (signer, topic) pair independently and never see each other's rules. | ||
| pub struct PushSubscriptionRule { | ||
| pub signer: StatementSigner, | ||
| pub topic: Topic, | ||
| } | ||
|
|
||
| pub struct HostPushAddRulesRequest { pub rules: Vec<PushSubscriptionRule> } | ||
| pub struct HostPushRemoveRulesRequest { pub rules: Vec<PushSubscriptionRule> } | ||
| pub struct HostPushListRulesRequest; | ||
| pub struct HostPushSetRulesRequest { pub rules: Vec<PushSubscriptionRule> } | ||
|
|
||
| pub struct HostPushListRulesResponse { | ||
| pub rules: Vec<PushSubscriptionRule>, | ||
| } | ||
|
|
||
| pub enum HostPushAddRulesError { | ||
| /// The user has not granted `DevicePermission::Notifications`. The host | ||
| /// SHOULD prompt for the permission lazily on the first such call from | ||
| /// a product; if the user dismisses or declines, this variant is | ||
| /// returned and no rules are stored. | ||
| PermissionDenied, | ||
| /// The host could not reach the push backend; no rules were stored. | ||
| BackendUnavailable, | ||
| /// Catch-all. `reason` | ||
| Unknown { reason: String }, | ||
| } | ||
|
|
||
| pub enum HostPushRemoveRulsError { | ||
| BackendUnavailable, | ||
| Unknown { reason: String }, | ||
| } | ||
|
|
||
| pub enum HostPushListRulesError { | ||
| BackendUnavailable, | ||
| Unknown { reason: String }, | ||
| } | ||
|
|
||
| pub enum HostPushSetRulesError { | ||
| PermissionDenied, | ||
| BackendUnavailable, | ||
| Unknown { reason: String }, | ||
| } | ||
| ``` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| //! Unified [`Notifications`] trait. | ||
|
|
||
| use crate::versioned::notifications::{ | ||
| HostPushAddRulesError, HostPushAddRulesRequest, HostPushAddRulesResponse, | ||
| HostPushListRulesError, HostPushListRulesRequest, HostPushListRulesResponse, | ||
| HostPushNotificationError, HostPushNotificationRequest, HostPushNotificationResponse, | ||
| HostPushRemoveRulesError, HostPushRemoveRulesRequest, HostPushRemoveRulesResponse, | ||
| HostPushSetRulesError, HostPushSetRulesRequest, HostPushSetRulesResponse, | ||
| }; | ||
| use crate::wire; | ||
| use crate::{CallContext, CallError}; | ||
|
|
||
| /// Notification methods: locally-rendered notifications and Statement Store | ||
| /// subscription rules for backend-delivered pushes. | ||
| /// | ||
| /// The rule-management methods (`push_add_rules`, `push_remove_rules`, | ||
| /// `push_list_rules`, `push_set_rules`) mirror the rule-management endpoints | ||
| /// of the push-notifications v2 backend design: | ||
| /// | ||
| /// - <https://hackmd.io/@1JCaGppGSUqHtJilikYaKw/r16YTVg5Ze> — v2, | ||
| /// backend-mediated | ||
| /// - <https://hackmd.io/@1JCaGppGSUqHtJilikYaKw/SyPN2yV6lx> — v1, | ||
| /// peer-to-peer (historical context) | ||
| pub trait Notifications: Send + Sync { | ||
| /// Send a notification to the user, rendered immediately by the host. | ||
| /// | ||
| /// ```ts | ||
| /// import { type Client } from "@parity/truapi"; | ||
| /// | ||
| /// export async function pushNotification(truapi: Client): Promise<void> { | ||
| /// const result = await truapi.notifications.pushNotification({ | ||
| /// text: "Hello!", | ||
| /// }); | ||
| /// | ||
| /// if (result.isErr()) throw result.error; | ||
| /// } | ||
| /// ``` | ||
| #[wire(request_id = 4)] | ||
| async fn push_notification( | ||
| &self, | ||
| cx: &CallContext, | ||
| request: HostPushNotificationRequest, | ||
| ) -> Result<HostPushNotificationResponse, CallError<HostPushNotificationError>>; | ||
|
|
||
| /// Register one or more `(signer, topic)` rules so the user is woken up | ||
| /// by a push when a signed statement matching any registered rule | ||
| /// appears on the Statement Store. Mirrors | ||
| /// `POST /v1/subscriptions/rules` from the v2 push backend spec. | ||
| /// | ||
| /// ```ts | ||
| /// import { type Client } from "@parity/truapi"; | ||
| /// | ||
| /// export async function addAnnouncementsRules( | ||
| /// truapi: Client, | ||
| /// ): Promise<void> { | ||
| /// const result = await truapi.notifications.pushAddRules({ | ||
| /// rules: [ | ||
| /// { | ||
| /// signer: "0x0000000000000000000000000000000000000000000000000000000000000000", | ||
| /// topic: "0x00", | ||
| /// }, | ||
| /// ], | ||
| /// }); | ||
| /// | ||
| /// if (result.isErr()) throw result.error; | ||
| /// } | ||
| /// ``` | ||
| #[wire(request_id = 134)] | ||
| async fn push_add_rules( | ||
| &self, | ||
| cx: &CallContext, | ||
| request: HostPushAddRulesRequest, | ||
| ) -> Result<HostPushAddRulesResponse, CallError<HostPushAddRulesError>>; | ||
|
|
||
| /// Remove one or more previously registered subscription rules. Mirrors | ||
| /// `DELETE /v1/subscriptions/rules` from the v2 push backend spec. | ||
| /// | ||
| /// ```ts | ||
| /// import { type Client } from "@parity/truapi"; | ||
| /// | ||
| /// export async function removeAnnouncementsRules( | ||
| /// truapi: Client, | ||
| /// ): Promise<void> { | ||
| /// const result = await truapi.notifications.pushRemoveRules({ | ||
| /// rules: [ | ||
| /// { | ||
| /// signer: "0x0000000000000000000000000000000000000000000000000000000000000000", | ||
| /// topic: "0x00", | ||
| /// }, | ||
| /// ], | ||
| /// }); | ||
| /// | ||
| /// if (result.isErr()) throw result.error; | ||
| /// } | ||
| /// ``` | ||
| #[wire(request_id = 136)] | ||
| async fn push_remove_rules( | ||
| &self, | ||
| cx: &CallContext, | ||
| request: HostPushRemoveRulesRequest, | ||
| ) -> Result<HostPushRemoveRulesResponse, CallError<HostPushRemoveRulesError>>; | ||
|
|
||
| /// List the calling product's currently registered subscription rules. | ||
| /// Useful for reconciling local UI state with what the host believes is | ||
| /// active (e.g. after logout/login). Mirrors | ||
| /// `GET /v1/subscriptions` from the v2 push backend spec. | ||
| /// | ||
| /// ```ts | ||
| /// import { type Client } from "@parity/truapi"; | ||
| /// | ||
| /// export async function listRules(truapi: Client) { | ||
| /// const result = await truapi.notifications.pushListRules({}); | ||
| /// if (result.isErr()) throw result.error; | ||
| /// return result.value.rules; | ||
| /// } | ||
| /// ``` | ||
| #[wire(request_id = 138)] | ||
| async fn push_list_rules( | ||
| &self, | ||
| cx: &CallContext, | ||
| request: HostPushListRulesRequest, | ||
| ) -> Result<HostPushListRulesResponse, CallError<HostPushListRulesError>>; | ||
|
|
||
| /// Atomically replace the calling product's entire rule set with the | ||
| /// supplied vector. After a successful call, the product's active rules | ||
| /// are exactly `rules`. Mirrors `PUT /v1/subscriptions/rules` from the | ||
| /// v2 push backend spec. | ||
| /// | ||
| /// ```ts | ||
| /// import { type Client } from "@parity/truapi"; | ||
| /// | ||
| /// export async function setRules(truapi: Client): Promise<void> { | ||
| /// const result = await truapi.notifications.pushSetRules({ | ||
| /// rules: [ | ||
| /// { | ||
| /// signer: "0x0000000000000000000000000000000000000000000000000000000000000000", | ||
| /// topic: "0x00", | ||
| /// }, | ||
| /// ], | ||
| /// }); | ||
| /// | ||
| /// if (result.isErr()) throw result.error; | ||
| /// } | ||
| /// ``` | ||
| #[wire(request_id = 140)] | ||
| async fn push_set_rules( | ||
| &self, | ||
| cx: &CallContext, | ||
| request: HostPushSetRulesRequest, | ||
| ) -> Result<HostPushSetRulesResponse, CallError<HostPushSetRulesError>>; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.