feat(pusher): opt-in event type in event_id_only push notifications #2171

Closed
mmaudet wants to merge 1 commit from mmaudet/continuwuity:feat/pusher-eio-type-upstream into main
Contributor

This pull request adds a config option, pusher_event_id_only_include_event_type (default: false), that makes push notifications for HTTP pushers registered with data.format = "event_id_only" carry the event's type (e.g. m.call.invite) in addition to the event ID, room ID and counts. Nothing changes unless the option is enabled: the default behaviour is byte-for-byte what continuwuity sends today.

Motivation

event_id_only is the format a privacy-conscious client picks so that the push gateway — typically a third party such as Sygnal, and beyond it FCM or APNs — learns as little as possible. Continuwuity honours it strictly: the notification carries only event_id, room_id and counts.

That strictness has a cost for VoIP. On iOS, every PushKit push must be reported to CallKit immediately; a client that cannot tell an incoming call from an ordinary message without fetching the event over the network cannot decide whether to report an incoming call or a generic notification. A single fetch under push time pressure is exactly what VoIP pushes cannot afford, and on a bad network it loses the call.

With the event type, the gateway can forward (and the client can act on) "this is m.call.invite" — while still seeing no content, no sender, no display name, no room name. The event type is routing metadata, not content.

Why opt-in rather than unconditional

Changing the default payload shape would surprise gateways and clients that key on today's minimal shape — some gateway implementations choose their event_id_only code path precisely by the absence of type. An opt-in config flag lets an operator adopt this deliberately, with no effect for anyone else.

Change

  • src/core/config/mod.rs: new boolean pusher_event_id_only_include_event_type, default false, documented next to the other pusher options.
  • src/service/pusher/mod.rs: in send_notice, when the pusher is event_id_only and the option is enabled, set notify.event_type = Some(event.kind().to_owned()). Nothing else is added: sender, content, display names, room name/alias and tweaks stay stripped.
  • conduwuit-example.toml: regenerated (it is a build artifact of the config doc comments).
  • changelog.d/+pusher-event-id-only-event-type.feature.md.

Verification

Built from this branch and probed with a recording HTTP gateway, a callee registered with two pushers — one data.format = "event_id_only", one full format — and two events sent by the caller: an m.room.message (control) and an m.call.invite.

Flag off (default): the event_id_only notifications carry exactly event_id, room_id, counts, devices — the same key set as the unpatched server:

{"notification": {"event_id": "$-5uq…", "room_id": "!XgEe…",
 "counts": {"unread": 2},
 "devices": [{"app_id": "sonde.test.eio", "pushkey": "sonde",
              "data": {"format": "event_id_only"}}]}}

Flag on: the same notifications additionally carry "type" — and nothing else:

{"notification": {"event_id": "$siuL…", "room_id": "!Vh1p…", "type": "m.call.invite",
 "counts": {"unread": 2},
 "devices": [{"app_id": "sonde.test.eio", "pushkey": "sonde",
              "data": {"format": "event_id_only"}}]}}

The control message carries "type": "m.room.message"; the call carries "type": "m.call.invite". No sender, content, sender_display_name, room_name, room_alias or tweaks appear in either run, and the full-format pusher's payload is identical in both runs.

Privacy note

The option is off by default and server-wide. An operator enabling it discloses, to the push gateways their users registered, only the event type of notifying events — the minimum metadata a VoIP push path needs. Users of servers that leave it off see no change at all.


Pull request checklist:

  • This pull request targets the main branch, and the branch is named something other than main.
  • I have written an appropriate pull request title and my description is clear.
  • I understand I am responsible for the contents of this pull request.
  • I have followed the contributing guidelines:
    • My contribution follows the code style, if applicable. (cargo +nightly fmt --check clean.)
    • I ran pre-commit checks before opening/drafting this pull request. (prek run on the changed files: all hooks pass.)
    • I have tested my contribution myself: cargo check --workspace is green and the behaviour was verified live with the recording-gateway probe described above (flag off vs flag on, message vs call invite).
    • My commit messages follow the commit message format and are descriptive.
This pull request adds a config option, `pusher_event_id_only_include_event_type` (default: `false`), that makes push notifications for HTTP pushers registered with `data.format = "event_id_only"` carry the event's `type` (e.g. `m.call.invite`) in addition to the event ID, room ID and counts. Nothing changes unless the option is enabled: the default behaviour is byte-for-byte what continuwuity sends today. ## Motivation `event_id_only` is the format a privacy-conscious client picks so that the push gateway — typically a third party such as Sygnal, and beyond it FCM or APNs — learns as little as possible. Continuwuity honours it strictly: the notification carries only `event_id`, `room_id` and `counts`. That strictness has a cost for VoIP. On iOS, every PushKit push must be reported to CallKit **immediately**; a client that cannot tell an incoming call from an ordinary message without fetching the event over the network cannot decide whether to report an incoming call or a generic notification. A single fetch under push time pressure is exactly what VoIP pushes cannot afford, and on a bad network it loses the call. With the event `type`, the gateway can forward (and the client can act on) "this is `m.call.invite`" — while still seeing **no content, no sender, no display name, no room name**. The event type is routing metadata, not content. ## Why opt-in rather than unconditional Changing the default payload shape would surprise gateways and clients that key on today's minimal shape — some gateway implementations choose their `event_id_only` code path precisely by the *absence* of `type`. An opt-in config flag lets an operator adopt this deliberately, with no effect for anyone else. ## Change - `src/core/config/mod.rs`: new boolean `pusher_event_id_only_include_event_type`, default `false`, documented next to the other pusher options. - `src/service/pusher/mod.rs`: in `send_notice`, when the pusher is `event_id_only` and the option is enabled, set `notify.event_type = Some(event.kind().to_owned())`. Nothing else is added: sender, content, display names, room name/alias and tweaks stay stripped. - `conduwuit-example.toml`: regenerated (it is a build artifact of the config doc comments). - `changelog.d/+pusher-event-id-only-event-type.feature.md`. ## Verification Built from this branch and probed with a recording HTTP gateway, a callee registered with two pushers — one `data.format = "event_id_only"`, one full format — and two events sent by the caller: an `m.room.message` (control) and an `m.call.invite`. Flag **off** (default): the `event_id_only` notifications carry exactly `event_id`, `room_id`, `counts`, `devices` — the same key set as the unpatched server: ```json {"notification": {"event_id": "$-5uq…", "room_id": "!XgEe…", "counts": {"unread": 2}, "devices": [{"app_id": "sonde.test.eio", "pushkey": "sonde", "data": {"format": "event_id_only"}}]}} ``` Flag **on**: the same notifications additionally carry `"type"` — and nothing else: ```json {"notification": {"event_id": "$siuL…", "room_id": "!Vh1p…", "type": "m.call.invite", "counts": {"unread": 2}, "devices": [{"app_id": "sonde.test.eio", "pushkey": "sonde", "data": {"format": "event_id_only"}}]}} ``` The control message carries `"type": "m.room.message"`; the call carries `"type": "m.call.invite"`. No `sender`, `content`, `sender_display_name`, `room_name`, `room_alias` or `tweaks` appear in either run, and the full-format pusher's payload is identical in both runs. ## Privacy note The option is off by default and server-wide. An operator enabling it discloses, to the push gateways their users registered, only the event type of notifying events — the minimum metadata a VoIP push path needs. Users of servers that leave it off see no change at all. --- **Pull request checklist:** - [x] This pull request targets the `main` branch, and the branch is named something other than `main`. - [x] I have written an appropriate pull request title and my description is clear. - [x] I understand I am responsible for the contents of this pull request. - I have followed the [contributing guidelines][c1]: - [x] My contribution follows the [code style][c2], if applicable. (`cargo +nightly fmt --check` clean.) - [x] I ran [pre-commit checks][c1pc] before opening/drafting this pull request. (`prek run` on the changed files: all hooks pass.) - [x] I have [tested my contribution][c1t] myself: `cargo check --workspace` is green and the behaviour was verified live with the recording-gateway probe described above (flag off vs flag on, message vs call invite). - [x] My commit messages follow the [commit message format][c1cm] and are descriptive. [c1]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md [c2]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/docs/development/code_style.mdx [c1pc]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md#pre-commit-checks [c1t]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md#running-tests-locally [c1cm]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md#commit-messages
feat(pusher): opt-in event type in event_id_only push notifications
Some checks failed
Auto Labeler / Apply labels based on changed files (pull_request_target) Successful in 3s
Checks / Changelog / Check changelog is added (pull_request_target) Successful in 7s
Documentation / Build and Deploy Documentation (pull_request) Has been cancelled
Checks / Prek / Pre-commit & Formatting (pull_request) Has been cancelled
Checks / Prek / Check changed files (pull_request) Has been cancelled
Checks / Prek / Clippy and Cargo Tests (pull_request) Has been cancelled
f7e7796d99
Add pusher_event_id_only_include_event_type (default false) so that
push notifications for HTTP pushers registered with the event_id_only
format can additionally carry the event's type (e.g. m.call.invite).

The default behaviour is unchanged: event_id_only notifications carry
only the event ID, the room ID and the counts. With the option enabled
the gateway additionally learns the event type — and nothing else: no
content, sender, display name or room name. A VoIP push path (e.g. iOS
PushKit, where every push must be reported to CallKit immediately) can
then distinguish an incoming call from a message without a network
fetch.
Owner

Hey there. This seems like it should have an associated matrix spec poposal.

Hey there. This seems like it should have an associated matrix spec poposal.
mmaudet force-pushed feat/pusher-eio-type-upstream from f7e7796d99
Some checks failed
Auto Labeler / Apply labels based on changed files (pull_request_target) Successful in 3s
Checks / Changelog / Check changelog is added (pull_request_target) Successful in 7s
Documentation / Build and Deploy Documentation (pull_request) Has been cancelled
Checks / Prek / Pre-commit & Formatting (pull_request) Has been cancelled
Checks / Prek / Check changed files (pull_request) Has been cancelled
Checks / Prek / Clippy and Cargo Tests (pull_request) Has been cancelled
to d0a5bdc445
Some checks failed
Checks / Changelog / Check changelog is added (pull_request_target) Successful in 6s
Documentation / Build and Deploy Documentation (pull_request) Has been cancelled
Checks / Prek / Pre-commit & Formatting (pull_request) Has been cancelled
Checks / Prek / Check changed files (pull_request) Has been cancelled
Checks / Prek / Clippy and Cargo Tests (pull_request) Has been cancelled
2026-08-23 13:59:25 +00:00
Compare
Author
Contributor

@Jade wrote in #2171 (comment):

Hey there. This seems like it should have an associated matrix spec poposal.

Agreed : this changes a payload the spec defines, so it belongs in an MSC rather than in a server option alone. I'll write one.

The constraint I'm trying to name, in case it's useful for the proposal: on iOS, a PushKit VoIP push must report an incoming call to CallKit before the handler returns, or the system terminates the app. There is no room for a network round-trip to /event first. With event_id_only the gateway cannot tell a call invite from a message, so a privacy-preserving push format and a working VoIP path are currently exclusive on that platform. The event type alone is the smallest thing that resolves it : no content, no sender, no room name.

Whichever you prefer: I can keep this open as a reference implementation while the MSC is discussed, or close it and reopen alongside the proposal. Say the word and I'll follow.

(I've also amended the commit : it carried a test-harness author identity by mistake.

@Jade wrote in https://forgejo.ellis.link/continuwuation/continuwuity/pulls/2171#issuecomment-34734: > Hey there. This seems like it should have an associated matrix spec poposal. Agreed : this changes a payload the spec defines, so it belongs in an MSC rather than in a server option alone. I'll write one. The constraint I'm trying to name, in case it's useful for the proposal: on iOS, a PushKit VoIP push must report an incoming call to CallKit before the handler returns, or the system terminates the app. There is no room for a network round-trip to /event first. With event_id_only the gateway cannot tell a call invite from a message, so a privacy-preserving push format and a working VoIP path are currently exclusive on that platform. The event type alone is the smallest thing that resolves it : no content, no sender, no room name. Whichever you prefer: I can keep this open as a reference implementation while the MSC is discussed, or close it and reopen alongside the proposal. Say the word and I'll follow. (I've also amended the commit : it carried a test-harness author identity by mistake.
Owner

You can use this as an example implementation even with it closed.

You can use this as an example implementation even with it closed.
Jade closed this pull request 2026-08-23 21:05:21 +00:00
Some checks failed
Checks / Changelog / Check changelog is added (pull_request_target) Successful in 6s
Required
Details
Documentation / Build and Deploy Documentation (pull_request) Has been cancelled
Checks / Prek / Pre-commit & Formatting (pull_request) Has been cancelled
Required
Details
Checks / Prek / Check changed files (pull_request) Has been cancelled
Required
Details
Checks / Prek / Clippy and Cargo Tests (pull_request) Has been cancelled
Required
Details

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
continuwuation/continuwuity!2171
No description provided.