fix(call): restore Android CallStyle banner for DM voice calls in encrypted rooms
This commit is contained in:
parent
7cafd8e8aa
commit
55cbecd4e7
5 changed files with 243 additions and 30 deletions
|
|
@ -57,7 +57,17 @@ public class VojoFirebaseMessagingService extends MessagingService {
|
|||
// for the empty string and a handful of other inputs).
|
||||
private static final int SUMMARY_NOTIFICATION_ID = Integer.MIN_VALUE;
|
||||
|
||||
// Both the unstable MSC4075 prefix that current matrix-js-sdk emits and the
|
||||
// stable name peers may upgrade to. Receive-side fallback only — send-side
|
||||
// (CallWidgetDriver cleartext bypass) keeps emitting the unstable name until
|
||||
// the SDK promotes the constant.
|
||||
private static final String RTC_NOTIFICATION_TYPE = "org.matrix.msc4075.rtc.notification";
|
||||
private static final String RTC_NOTIFICATION_TYPE_STABLE = "m.rtc.notification";
|
||||
|
||||
private static boolean isRtcNotificationType(String type) {
|
||||
return RTC_NOTIFICATION_TYPE.equals(type) || RTC_NOTIFICATION_TYPE_STABLE.equals(type);
|
||||
}
|
||||
|
||||
private static final long RTC_DEFAULT_LIFETIME_MS = 30_000L;
|
||||
private static final long RTC_LIFETIME_GRACE_MS = 2_000L;
|
||||
|
||||
|
|
@ -162,7 +172,7 @@ public class VojoFirebaseMessagingService extends MessagingService {
|
|||
+ " event=" + data.get("event_id")
|
||||
+ " fg=" + MainActivity.isInForeground);
|
||||
try {
|
||||
if (RTC_NOTIFICATION_TYPE.equals(data.get("type"))
|
||||
if (isRtcNotificationType(data.get("type"))
|
||||
&& "ring".equals(data.get("content_notification_type"))) {
|
||||
String eventId = data.get("event_id");
|
||||
String roomId = data.get("room_id");
|
||||
|
|
|
|||
|
|
@ -439,6 +439,7 @@ Tradeoff: 1-3s окно между созданием embed и `JoinCall` без
|
|||
### 5.43. 🟢 Stale ring в JS strip после killed-decline в DM — landed 2026-04-23 (commit 9e5fa6b)
|
||||
- **Landed fix:** в [useIncomingRtcNotifications.ts](../../src/app/hooks/useIncomingRtcNotifications.ts) заведён `declinedTimersRef: Map<notifEventId, timeout>` с self-expiring 30s entries. `rememberDeclined()` helper в decline-branch записывает `rel.event_id` **перед** `removeByNotifId`. Notification-branch имеет **двойной** check `declinedTimers.has(evId)`: pre-await short-circuit (не гоняем `resolveCallId` для уже отклонённых) и post-await race-guard рядом с существующим membership re-check. Unmount cleanup clear'ит pending timers. Атом / native / receiver не тронуты.
|
||||
- **Изначальная гипотеза (§5.43 pre-fix) оказалась частично ошибочна:** live-логи показали что `m.rtc.notification` летит **cleartext** даже в encrypted DM (нужно для push-сервиса), а не как `m.room.encrypted`. Настоящий race — async внутри processEvent: `resolveCallId` yields до 5s (MembershipsChanged wait) + fetchRoomEvent. За этот yield Timeline decline проходит через handleTimeline → populates declinedTimers → но notification-branch уже миновал pre-await check и после await проверял только membership, не declined set. Fix-через-set всё равно правильный, просто check нужен **с обеих сторон** от await, а не только до.
|
||||
- **Корректировка 2026-04-29 (см. §5.51):** утверждение «`m.rtc.notification` летит cleartext даже в encrypted DM» оказалось НЕВЕРНЫМ. Logcat 2026-04-29 показал FCM payload с `type=m.room.encrypted, cn_type=null` для encrypted-DM ring'а — то есть SDK всё-таки шифровал event. Скорее всего тест 2026-04-23 случайно был на unencrypted DM, либо сделан через другой path. Race-fix через `declinedTimersRef` сам по себе правильный (race в processEvent существует независимо от encryption), но вывод о cleartext был ошибочным. После §5.51 cleartext-bypass для ring уже **сделан явно** в `CallWidgetDriver`, так что текущее состояние согласовано: ring действительно cleartext, но только потому что мы сами переписали путь, а не потому что SDK так делал.
|
||||
- **Trace (confirmed by live logcat 2026-04-23):**
|
||||
1. FCM доставляет notification → native CallStyle → user жмёт Decline → `CallDeclineReceiver` шлёт cleartext `m.call.decline` PUT (200 OK).
|
||||
2. User открывает app (cold boot или resume). /sync подтягивает оба события.
|
||||
|
|
@ -590,3 +591,21 @@ App foreground, incoming DM call. JS strip + audio начинают отраба
|
|||
5. Ожидаемый результат после фикса: prompt остаётся loading до ответа, затем показывает localized error.
|
||||
- **Фикс-направление:** `await mx.leave(roomId);` в обоих prompt'ах. Не `return mx.leave(roomId)`, если сохраняем `useAsyncCallback<undefined, MatrixError, []>` contract.
|
||||
- **Scope:** не связано с i18n-диффом; pre-existing bug, найден при ревью локализации leave prompt'а.
|
||||
|
||||
### 5.51. ✅ Encrypted RTC ring FCM misclassifies as message on Android — RESOLVED 2026-04-29
|
||||
|
||||
- **Симптом (до фикса):** Android background/killed мог показать обычное message-уведомление вместо CallStyle для входящего звонка в encrypted DM.
|
||||
- **Логкат 2026-04-29 (до фикса):** ring пришёл в `VojoFirebaseMessagingService` как `recv: type=m.room.encrypted cn_type=null ... event=<rtc-notification-event-id>`. JS затем расшифровал тот же event и сделал `CallForegroundService.upsertIncomingRing`, поэтому foreground in-app звонок прошёл. Background Java FCM path не мог классифицировать payload как ring.
|
||||
- **Root cause:** matrix-js-sdk шифрует `m.rtc.notification` через `client.sendEvent` в e2ee комнате (`MatrixRTCSession.sendCallNotify` → `client.sendEvent`). Server-side push rule `EventMatch type=org.matrix.msc4075.rtc.notification` матчится только на outer cleartext type, который для encrypted envelope всегда `m.room.encrypted`. Sygnal падает в default message-rule. Java classifier тоже видит только outer type и не может распознать ring. Element Web обходит через SW fetch+decrypt; Element X — через matrix-rust-sdk native; Vojo Android FCM не имеет ни того, ни другого.
|
||||
- **Что не является доказательством:** `adb shell am force-stop chat.vojo.app` переводит приложение в Android stopped state; FCM обычно не доставляется до ручного запуска. Для killed-process теста использовать swipe-away / обычное убийство процесса, не force-stop.
|
||||
- **Применённый фикс:** cleartext bypass только для `m.rtc.notification` с `notification_type === 'ring'` через `CallWidgetDriver.sendEvent`. Server видит outer type = `org.matrix.msc4075.rtc.notification`, push rule матчит, Java classifier распознаёт ring, CallStyle появляется. Symmetрично существующему `CallDeclineReceiver.sendDecline`, который уже шлёт `m.rtc.decline` cleartext через прямой HTTP PUT.
|
||||
- **Threat model.** Утечка: server и push gateway видят факт «в этой комнате был ring» + room_id + sender + sender_ts + lifetime + relation. НЕ утечка: message bodies, LiveKit token, media keys, SDP / session secrets, аудио-поток. По MSC4075 ring content и так не несёт secret payload. Эквивалент legacy `m.call.invite` cleartext signaling в WebRTC-over-Matrix. В коде и доках называем это «cleartext ring metadata for Android FCM classification», не «unencrypted calls».
|
||||
- **Allowlist-only.** Чтобы upstream Element Call в будущем случайно не протащил secret в cleartext, форвардятся только: `notification_type, m.relates_to, sender_ts, lifetime, m.mentions, m.call.intent`. Всё неизвестное дропается на send-side.
|
||||
- **Stable-name fallback — narrow scope.** Покрыты только Android push pipeline компоненты, которые блокировали encrypted-DM ring delivery: server-side override push rule + Java FCM classifier — оба теперь матчат ОБА `org.matrix.msc4075.rtc.notification` (unstable) и `m.rtc.notification` (stable). Send-side остаётся unstable: `CallWidgetDriver` шлёт `EventType.RTCNotification` (matrix-js-sdk константа), при promotion SDK значение переключится автоматически.
|
||||
- **НЕ покрыто намеренно:** SW classification ([`src/sw.ts`](../../src/sw.ts)), JS hooks (`useIncomingRtcNotifications`, `useCallerAutoHangup` через `EventType.RTCNotification` — auto-upgrade при SDK bump), widget capability declarations ([`src/app/plugins/call/utils.ts`](../../src/app/plugins/call/utils.ts)), и timeline event-type filters в `RoomTimeline.tsx` — все они на hardcoded unstable string. По решению в [`docs/ai/desired_features.md`](../ai/desired_features.md) #6 миграция всех MSC RTC префиксов делается единым диффом, когда стабилизация landed (MSC4075 + MSC4143 + MSC4195 + MSC4310 параллельно). Текущий фикс не делает преждевременную частичную миграцию.
|
||||
- **Legacy `EventType.CallNotify` (`org.matrix.msc4075.call.notify`) defense.** matrix-js-sdk's `MatrixRTCSession.sendCallNotify` параллельно отправляет ОБА новый `m.rtc.notification` И legacy CallNotify через `Promise.all`. Защита двухслойная:
|
||||
- **Primary:** capability denial — `getCallCapabilities` не грантит Send для `EventType.CallNotify`, widget capability check rejects request ещё внутри widget API до `CallWidgetDriver.sendEvent`.
|
||||
- **Secondary:** silent no-op в `CallWidgetDriver.sendEvent` для `EventType.CallNotify` с sentinel event_id (`$vojo:suppressed-legacy-call-notify`). На случай если future widget release обходит capability check — событие на сервер не уходит. Resolve вместо throw сохраняет совместимость с upstream `Promise.all` umbrella в `sendCallNotify`: `.then` отрабатывает, `DidSendCallNotification` emit'ится (Vojo не consumer'ит, но keep upstream contract clean), нет «Unhandled promise rejection» в консоли. Push-rule layer **не** используется здесь намеренно: в encrypted DM HS видит только outer `m.room.encrypted`, `EventMatch` на inner type silently no-op'ит, suppress rule был бы misleading.
|
||||
- **Shape validation.** `sanitizeRingContent` отвергает payloads с invalid `m.relates_to` (не `m.reference` / non-string event_id), non-finite или non-positive `sender_ts`, и `lifetime` вне (0, 5min]. На invalid — fallback на encrypted send-path: in-app strip всё равно получит event через /sync; теряется только Android background CallStyle. Это укрепляет инвариант «cleartext только корректная ring metadata» против будущих upstream Element Call изменений.
|
||||
- **Что НЕ покрыто фиксом и почему:** (a) `m.rtc.member` — state event, не идёт через push pipeline; (b) `m.rtc.decline` — для killed-state уже cleartext через `CallDeclineReceiver`, для foreground шлётся через `mx.sendRtcDecline` и шифруется в e2ee, но decline path не показывает CallStyle banner — отдельная проблема, не блокирующая. Если decline cleartext bypass понадобится — добавить тем же allowlist-паттерном в `CallWidgetDriver`.
|
||||
- **Deferred follow-up — strict threat model.** Если когда-нибудь нужно скрыть от сервера сам факт звонка (не только содержимое), потребуется native-decrypt path аналогично Element X: либо порт matrix-rust-sdk в Android shell, либо headless WebView crypto bridge (хрупко из-за cold-start time budget). Большой scope, отдельным планом.
|
||||
|
|
|
|||
|
|
@ -286,7 +286,10 @@ export const useIncomingRtcNotifications = (): void => {
|
|||
// FCM path only seeds registry for 'ring' content_notification_type, so no entry to remove.
|
||||
if (content.notification_type !== 'ring') return;
|
||||
if (!mDirectRef.current.has(room.roomId)) {
|
||||
removeFromRegistry();
|
||||
// `mDirectAtom` is hydrated from account-data in a React effect, so it
|
||||
// can be empty during cold/warm startup even for a legitimate DM. Keep
|
||||
// the native registry alive here: pre-registry behavior was "no JS
|
||||
// strip", not "cancel the Android CallStyle".
|
||||
return;
|
||||
}
|
||||
if (isRtcNotificationExpired(ev)) {
|
||||
|
|
|
|||
|
|
@ -14,11 +14,13 @@ import {
|
|||
IOpenIDUpdate,
|
||||
} from 'matrix-widget-api';
|
||||
import {
|
||||
ClientPrefix,
|
||||
EventType,
|
||||
type IContent,
|
||||
MatrixError,
|
||||
type MatrixEvent,
|
||||
Direction,
|
||||
Method,
|
||||
type SendDelayedEventResponse,
|
||||
type StateEvents,
|
||||
type TimelineEvents,
|
||||
|
|
@ -27,6 +29,99 @@ import {
|
|||
import { getCallCapabilities } from './utils';
|
||||
import { downloadMedia, mxcUrlToHttp } from '../../utils/matrix';
|
||||
|
||||
// Cleartext ring metadata for Android FCM classification — NOT "unencrypted calls".
|
||||
//
|
||||
// Why this exists. matrix-js-sdk encrypts every room event in an e2ee room before
|
||||
// PUT /send. Server stores it as `m.room.encrypted`; Sygnal/HS push rules can only
|
||||
// match outer cleartext fields, so an `EventMatch type=org.matrix.msc4075.rtc.notification`
|
||||
// override never fires for encrypted DMs. Push falls back to the default message rule,
|
||||
// FCM payload arrives with `type=m.room.encrypted, cn_type=null`, the Java classifier
|
||||
// in `VojoFirebaseMessagingService.onMessageReceived` cannot recognise it as a ring,
|
||||
// and the user sees a generic message banner instead of CallStyle. Element Web mitigates
|
||||
// this by fetching+decrypting in the SW; Element X does it via native matrix-rust-sdk.
|
||||
// Vojo Android has neither path, so the only short fix is to send the ring signal cleartext.
|
||||
//
|
||||
// Threat model. The signaling layer "an incoming ring exists in this room at time T"
|
||||
// becomes server- and push-gateway-visible. Message bodies, LiveKit tokens, media
|
||||
// encryption keys, SDP/session secrets, and call audio remain end-to-end encrypted —
|
||||
// MSC4075 keeps those out of `m.rtc.notification` content by design. Equivalent to
|
||||
// legacy `m.call.invite` cleartext signaling in WebRTC-over-Matrix. Stricter threat
|
||||
// models that require hiding even the call-existence fact need the native-decrypt path
|
||||
// tracked in `docs/plans/dm_calls_techdebt.md` §5.51.
|
||||
//
|
||||
// Reconstruct the ring payload field-by-field from validated primitives.
|
||||
// We do NOT use a top-level allowlist + shallow copy: nested objects like
|
||||
// `m.relates_to`, `m.mentions`, and a future opaque `m.call.intent` would
|
||||
// then leak any sub-fields Element Call upstream might add. Each accepted
|
||||
// field below is rebuilt from primitive-typed inputs only, so unknown
|
||||
// sub-fields cannot ride along.
|
||||
|
||||
const RTC_RING_LIFETIME_MAX_MS = 5 * 60 * 1000;
|
||||
const RTC_RING_MENTIONS_MAX_USERS = 64;
|
||||
|
||||
const isObject = (v: unknown): v is Record<string, unknown> =>
|
||||
typeof v === 'object' && v !== null && !Array.isArray(v);
|
||||
|
||||
// Validate a widget-provided ring payload before we send it cleartext. Returns
|
||||
// the sanitized object on a happy path or `null` if any required field is
|
||||
// missing/malformed — caller then falls back to the encrypted send path so the
|
||||
// in-app strip still gets the event via /sync, even if Android push misses.
|
||||
// Prevents a hostile or buggy widget from leaking arbitrary cleartext under
|
||||
// the guise of "ring metadata".
|
||||
function sanitizeRingContent(content: IContent): IContent | null {
|
||||
const relates = content['m.relates_to'];
|
||||
if (!isObject(relates)) return null;
|
||||
if (relates.rel_type !== 'm.reference') return null;
|
||||
if (typeof relates.event_id !== 'string' || relates.event_id.length === 0) return null;
|
||||
|
||||
const { sender_ts: senderTs, lifetime } = content;
|
||||
if (typeof senderTs !== 'number' || !Number.isFinite(senderTs) || senderTs <= 0) return null;
|
||||
|
||||
if (
|
||||
typeof lifetime !== 'number' ||
|
||||
!Number.isFinite(lifetime) ||
|
||||
lifetime <= 0 ||
|
||||
lifetime > RTC_RING_LIFETIME_MAX_MS
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const out: IContent = {
|
||||
notification_type: 'ring',
|
||||
'm.relates_to': {
|
||||
rel_type: 'm.reference',
|
||||
event_id: relates.event_id,
|
||||
},
|
||||
sender_ts: senderTs,
|
||||
lifetime,
|
||||
};
|
||||
|
||||
// m.mentions per Matrix spec: { user_ids?: string[]; room?: boolean }.
|
||||
// Reconstruct from primitives so an upstream-added field can't leak.
|
||||
const mentions = content['m.mentions'];
|
||||
if (isObject(mentions)) {
|
||||
const sanitizedMentions: { user_ids?: string[]; room?: boolean } = {};
|
||||
if (Array.isArray(mentions.user_ids)) {
|
||||
sanitizedMentions.user_ids = mentions.user_ids
|
||||
.filter((u): u is string => typeof u === 'string')
|
||||
.slice(0, RTC_RING_MENTIONS_MAX_USERS);
|
||||
}
|
||||
if (typeof mentions.room === 'boolean') {
|
||||
sanitizedMentions.room = mentions.room;
|
||||
}
|
||||
out['m.mentions'] = sanitizedMentions;
|
||||
}
|
||||
|
||||
// m.call.intent per MSC4310 is a free-form string hint ('audio' / 'video').
|
||||
// Forward only if it's a primitive string.
|
||||
const intent = content['m.call.intent'];
|
||||
if (typeof intent === 'string' && intent.length > 0 && intent.length < 64) {
|
||||
out['m.call.intent'] = intent;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
export class CallWidgetDriver extends WidgetDriver {
|
||||
private allowedCapabilities: Set<Capability>;
|
||||
|
||||
|
|
@ -59,6 +154,35 @@ export class CallWidgetDriver extends WidgetDriver {
|
|||
if (!client || !roomId) throw new Error('Not in a room or not attached to a client');
|
||||
|
||||
let r: { event_id: string } | null;
|
||||
const sanitizedRing =
|
||||
stateKey === null &&
|
||||
eventType === EventType.RTCNotification &&
|
||||
content.notification_type === 'ring'
|
||||
? sanitizeRingContent(content)
|
||||
: null;
|
||||
|
||||
// Defense-in-depth against the legacy `EventType.CallNotify`
|
||||
// (`org.matrix.msc4075.call.notify`) sibling event that
|
||||
// `MatrixRTCSession.sendCallNotify` upstream still emits in parallel
|
||||
// with `m.rtc.notification`. `getCallCapabilities` already omits Send
|
||||
// for it, so the widget capability check should reject the request
|
||||
// before it reaches us — but if a future widget release bypasses
|
||||
// capability validation, the encrypted send would surface as a
|
||||
// default-rule message banner alongside the real ring (Vojo's ring
|
||||
// listener watches RTCNotification, not CallNotify). A push-rule
|
||||
// suppression cannot fix this: the homeserver only sees
|
||||
// `type=m.room.encrypted` for encrypted DMs and `EventMatch` against
|
||||
// the inner type silently no-ops. So we silently no-op the legacy send
|
||||
// here, returning a sentinel event_id. Throwing is tempting but would
|
||||
// poison `MatrixRTCSession.sendCallNotify`'s `Promise.all` umbrella —
|
||||
// its `.then` doesn't fire on reject, `DidSendCallNotification` never
|
||||
// emits, and an "Unhandled promise rejection" log flickers per ring.
|
||||
// Vojo doesn't consume `DidSendCallNotification`, but a clean resolve
|
||||
// keeps the sibling new-RTCNotification ack path identical to upstream.
|
||||
if (stateKey === null && eventType === EventType.CallNotify) {
|
||||
return { roomId, eventId: '$vojo:suppressed-legacy-call-notify' };
|
||||
}
|
||||
|
||||
if (typeof stateKey === 'string') {
|
||||
r = await client.sendStateEvent(
|
||||
roomId,
|
||||
|
|
@ -69,6 +193,25 @@ export class CallWidgetDriver extends WidgetDriver {
|
|||
} else if (eventType === EventType.RoomRedaction) {
|
||||
// special case: extract the `redacts` property and call redact
|
||||
r = await client.redactEvent(roomId, content.redacts);
|
||||
} else if (sanitizedRing) {
|
||||
// Bypass the encryption pipeline for ring signaling. See the rationale block
|
||||
// above the allowlist for full reasoning. Only `notification_type === 'ring'`
|
||||
// is special-cased — group-call `notification_type === 'notification'` keeps
|
||||
// the default encrypted path because it isn't routed through the Android
|
||||
// CallStyle classifier. If the payload fails shape validation we fall through
|
||||
// to the regular encrypted send below so the in-app strip still gets the
|
||||
// event via /sync; only Android background CallStyle is lost on that branch.
|
||||
const txnId = client.makeTxnId();
|
||||
const path = `/rooms/${encodeURIComponent(roomId)}/send/${encodeURIComponent(
|
||||
eventType
|
||||
)}/${encodeURIComponent(txnId)}`;
|
||||
r = await client.http.authedRequest<{ event_id: string }>(
|
||||
Method.Put,
|
||||
path,
|
||||
undefined,
|
||||
sanitizedRing,
|
||||
{ prefix: ClientPrefix.V3 }
|
||||
);
|
||||
} else {
|
||||
r = await client.sendEvent(
|
||||
roomId,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
MatrixClient,
|
||||
IPusherRequest,
|
||||
IPushRule,
|
||||
PushRuleKind,
|
||||
ConditionKind,
|
||||
PushRuleActionName,
|
||||
|
|
@ -74,45 +75,75 @@ export type PusherIds = {
|
|||
};
|
||||
|
||||
export const RTC_RING_PUSH_RULE_ID = 'chat.vojo.rtc.ring';
|
||||
export const RTC_RING_PUSH_RULE_ID_STABLE = 'chat.vojo.rtc.ring.stable';
|
||||
|
||||
// Matrix default push rules don't cover `org.matrix.msc4075.rtc.notification`,
|
||||
// so without an explicit Override rule the homeserver never dispatches RTC ring
|
||||
// events to Sygnal — background/killed devices silently miss incoming DM calls.
|
||||
// Matrix default push rules don't cover MSC4075 RTC ring events, so without
|
||||
// an explicit Override rule the homeserver never dispatches RTC ring events
|
||||
// to Sygnal — background/killed devices silently miss incoming DM calls.
|
||||
//
|
||||
// Two rules are registered in parallel: one against the unstable
|
||||
// `org.matrix.msc4075.rtc.notification` event type that current matrix-js-sdk
|
||||
// (and our send-side cleartext bypass in `CallWidgetDriver`) emits, and one
|
||||
// against the stable `m.rtc.notification` name so peers running an upgraded
|
||||
// SDK with the stable promotion don't silently miss us. EventMatch only
|
||||
// matches the outer cleartext event type, which is why our send-side relies
|
||||
// on the cleartext bypass — this rule only fires on cleartext ring events.
|
||||
//
|
||||
// Idempotent: Synapse returns 200 OK on PUT with an identical body, so calling
|
||||
// this on every pusher (re)registration and on lifecycle startup is cheap.
|
||||
// these on every pusher (re)registration and on lifecycle startup is cheap.
|
||||
// Failures are logged but not thrown — a transient /pushrules 5xx shouldn't
|
||||
// break pusher setup (foreground path via useIncomingRtcNotifications keeps
|
||||
// working regardless of whether this rule made it to the server).
|
||||
// working regardless of whether the rule made it to the server).
|
||||
const ringRuleBody = (
|
||||
eventTypePattern: string
|
||||
): Pick<IPushRule, 'actions' | 'conditions' | 'pattern'> => ({
|
||||
conditions: [
|
||||
{
|
||||
kind: ConditionKind.EventMatch,
|
||||
key: 'type',
|
||||
pattern: eventTypePattern,
|
||||
},
|
||||
{
|
||||
kind: ConditionKind.EventMatch,
|
||||
key: 'content.notification_type',
|
||||
pattern: 'ring',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
PushRuleActionName.Notify,
|
||||
{ set_tweak: TweakName.Sound, value: 'ring' },
|
||||
{ set_tweak: TweakName.Highlight, value: true },
|
||||
],
|
||||
});
|
||||
|
||||
export async function ensureRtcRingPushRule(mx: MatrixClient): Promise<void> {
|
||||
try {
|
||||
await mx.addPushRule('global', PushRuleKind.Override, RTC_RING_PUSH_RULE_ID, {
|
||||
conditions: [
|
||||
{
|
||||
kind: ConditionKind.EventMatch,
|
||||
key: 'type',
|
||||
pattern: 'org.matrix.msc4075.rtc.notification',
|
||||
},
|
||||
{
|
||||
kind: ConditionKind.EventMatch,
|
||||
key: 'content.notification_type',
|
||||
pattern: 'ring',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
PushRuleActionName.Notify,
|
||||
{ set_tweak: TweakName.Sound, value: 'ring' },
|
||||
{ set_tweak: TweakName.Highlight, value: true },
|
||||
],
|
||||
});
|
||||
await mx.addPushRule(
|
||||
'global',
|
||||
PushRuleKind.Override,
|
||||
RTC_RING_PUSH_RULE_ID,
|
||||
ringRuleBody('org.matrix.msc4075.rtc.notification')
|
||||
);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[push] ensureRtcRingPushRule failed:', err);
|
||||
console.warn('[push] ensureRtcRingPushRule (unstable) failed:', err);
|
||||
}
|
||||
try {
|
||||
await mx.addPushRule(
|
||||
'global',
|
||||
PushRuleKind.Override,
|
||||
RTC_RING_PUSH_RULE_ID_STABLE,
|
||||
ringRuleBody('m.rtc.notification')
|
||||
);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[push] ensureRtcRingPushRule (stable) failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Symmetric cleanup for `useDisablePushNotifications`. The rule lives in
|
||||
// account data so it would otherwise outlive the pusher — and other logged-in
|
||||
|
||||
// Symmetric cleanup for `useDisablePushNotifications`. The rules live in
|
||||
// account data so they would otherwise outlive the pusher — and other logged-in
|
||||
// clients (Element, a second Vojo session) would keep applying the ring
|
||||
// tweaks after the user explicitly turned push off here. 404 on delete of an
|
||||
// absent rule lands in the same warn path — that's the "already gone" case,
|
||||
|
|
@ -122,10 +153,17 @@ export async function removeRtcRingPushRule(mx: MatrixClient): Promise<void> {
|
|||
await mx.deletePushRule('global', PushRuleKind.Override, RTC_RING_PUSH_RULE_ID);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[push] removeRtcRingPushRule failed:', err);
|
||||
console.warn('[push] removeRtcRingPushRule (unstable) failed:', err);
|
||||
}
|
||||
try {
|
||||
await mx.deletePushRule('global', PushRuleKind.Override, RTC_RING_PUSH_RULE_ID_STABLE);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[push] removeRtcRingPushRule (stable) failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function registerWebPusher(
|
||||
mx: MatrixClient,
|
||||
subscription: PushSubscription,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue