From 81a6d9c9edcf7d45ffbdcfc29771a03d4fa972a7 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Mon, 14 Sep 2026 19:11:41 -0400 Subject: [PATCH] =?UTF-8?q?fix(call):=20serve=20RTC=20transports=20to=20El?= =?UTF-8?q?ement=20Call=20over=20MSC4515=20=E2=80=94=20calls=20work=20agai?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since the upstream v0.25.0 merge, Element Call in widget mode no longer reads .well-known for its LiveKit transport; it asks the HOST via the widget API (org.matrix.msc4515.get_rtc_transports, capability org.matrix.msc4515.rtc_transports). cinny never granted the capability nor implemented WidgetDriver.getRtcTransports(), so discovery returned nothing and every join failed with "Call is not supported" (MISSING_MATRIX_RTC_TRANSPORT) — the [LocalMembership] Multiple Transport Errors line in the browser console. - matrix-widget-api 1.17.0 -> 1.18.0 (adds MSC4515; also changes the sendDelayedEvent driver signature, adapted — parent delay ids were removed from the draft). - Grant MSC4515RtcTransports in getCallCapabilities. - CallWidgetDriver.getRtcTransports(): homeserver /rtc/transports (MSC4143) first, then the .well-known org.matrix.msc4143.rtc_foci list (what matrix.lotusguild.org advertises today). Unit-tested. Server side needs no change: the well-known already carries the livekit focus and the JWT service answers. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- package-lock.json | 8 +-- package.json | 4 +- .../CallWidgetDriver.rtcTransports.test.ts | 58 +++++++++++++++++++ src/app/plugins/call/CallWidgetDriver.ts | 55 ++++++++++++------ src/app/plugins/call/utils.ts | 4 ++ 5 files changed, 106 insertions(+), 23 deletions(-) create mode 100644 src/app/plugins/call/CallWidgetDriver.rtcTransports.test.ts diff --git a/package-lock.json b/package-lock.json index 7e4336516..9008ea1b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,7 @@ "linkify-react": "4.3.3", "linkifyjs": "4.3.3", "matrix-js-sdk": "41.7.0", - "matrix-widget-api": "1.17.0", + "matrix-widget-api": "1.18.0", "millify": "6.1.0", "pdfjs-dist": "6.3.289", "prismjs": "1.30.0", @@ -10045,9 +10045,9 @@ } }, "node_modules/matrix-widget-api": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.17.0.tgz", - "integrity": "sha512-5FHoo3iEP3Bdlv5jsYPWOqj+pGdFQNLWnJLiB0V7Ygne7bb+Gsj3ibyFyHWC6BVw+Z+tSW4ljHpO17I9TwStwQ==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.18.0.tgz", + "integrity": "sha512-4T2f2koWmx05p1BLcT/9YGGGPSXpPT+PA4Oap/5fjhXsWPxMGJiGL97YpANMYLKsnE1sScYFeuyxIgdc1Qo+Ew==", "license": "Apache-2.0", "dependencies": { "@types/events": "^3.0.0", diff --git a/package.json b/package.json index 77be7364b..3cc943168 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "lotus-chat", "version": "4.12.3-lotus", - "description": "Lotus Chat \u2014 Matrix client for Lotus Guild", + "description": "Lotus Chat — Matrix client for Lotus Guild", "main": "index.js", "type": "module", "engines": { @@ -81,7 +81,7 @@ "linkify-react": "4.3.3", "linkifyjs": "4.3.3", "matrix-js-sdk": "41.7.0", - "matrix-widget-api": "1.17.0", + "matrix-widget-api": "1.18.0", "millify": "6.1.0", "pdfjs-dist": "6.3.289", "prismjs": "1.30.0", diff --git a/src/app/plugins/call/CallWidgetDriver.rtcTransports.test.ts b/src/app/plugins/call/CallWidgetDriver.rtcTransports.test.ts new file mode 100644 index 000000000..16193657d --- /dev/null +++ b/src/app/plugins/call/CallWidgetDriver.rtcTransports.test.ts @@ -0,0 +1,58 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import type { MatrixClient } from 'matrix-js-sdk'; +import { CallWidgetDriver } from './CallWidgetDriver'; + +// MSC4515: Element Call (widget mode) asks the host for RTC transports. Without a +// working answer every call fails with MISSING_MATRIX_RTC_TRANSPORT. +function fakeClient(opts: { + serverTransports?: unknown[] | Error; + wellKnown?: Record; +}): MatrixClient { + return { + getDeviceId: () => 'DEV', + getSafeUserId: () => '@me:example.org', + _unstable_getRTCTransports: async () => { + if (opts.serverTransports instanceof Error) throw opts.serverTransports; + return opts.serverTransports ?? []; + }, + getClientWellKnown: () => opts.wellKnown, + } as unknown as MatrixClient; +} + +const focus = { type: 'livekit', livekit_service_url: 'https://matrix.example.org' }; + +test('getRtcTransports prefers the homeserver MSC4143 endpoint', async () => { + const driver = new CallWidgetDriver(fakeClient({ serverTransports: [focus] }), '!r:x'); + const r = await driver.getRtcTransports(); + assert.deepEqual(r.rtc_transports, [focus]); +}); + +test('getRtcTransports falls back to .well-known rtc_foci when the endpoint 404s', async () => { + const driver = new CallWidgetDriver( + fakeClient({ + serverTransports: new Error('404'), + wellKnown: { 'org.matrix.msc4143.rtc_foci': [focus, { junk: true }, null] }, + }), + '!r:x', + ); + const r = await driver.getRtcTransports(); + assert.deepEqual(r.rtc_transports, [focus]); +}); + +test('getRtcTransports returns an empty list when nothing is advertised', async () => { + const driver = new CallWidgetDriver( + fakeClient({ serverTransports: [], wellKnown: { 'm.homeserver': {} } }), + '!r:x', + ); + const r = await driver.getRtcTransports(); + assert.deepEqual(r.rtc_transports, []); +}); + +test('the MSC4515 capability is granted to the call widget', async () => { + const driver = new CallWidgetDriver(fakeClient({}), '!r:x'); + const allowed = await driver.validateCapabilities( + new Set(['org.matrix.msc4515.rtc_transports', 'org.example.not_allowed']), + ); + assert.deepEqual(Array.from(allowed), ['org.matrix.msc4515.rtc_transports']); +}); diff --git a/src/app/plugins/call/CallWidgetDriver.ts b/src/app/plugins/call/CallWidgetDriver.ts index 0dff3f70c..3fd74f5c6 100644 --- a/src/app/plugins/call/CallWidgetDriver.ts +++ b/src/app/plugins/call/CallWidgetDriver.ts @@ -8,6 +8,8 @@ import { type IWidgetApiErrorResponseDataDetails, type ISearchUserDirectoryResult, type IGetMediaConfigResult, + type IRtcTransportsResult, + type IRtcTransport, OpenIDRequestState, SimpleObservable, IOpenIDUpdate, @@ -79,29 +81,18 @@ export class CallWidgetDriver extends WidgetDriver { return { roomId, eventId: r.event_id }; } + // matrix-widget-api >= 1.18 dropped the `parentDelayId` argument (MSC4140 + // parent delays were removed from the spec draft); the signature is now + // (delay, eventType, content, stateKey?, roomId?). public async sendDelayedEvent( - delay: number | null, - parentDelayId: string | null, + delay: number, eventType: string, - content: IContent, + content: unknown, stateKey: string | null = null, targetRoomId: string | null = null, ): Promise { const roomId = targetRoomId || this.inRoomId; - - let delayOpts; - if (delay !== null) { - delayOpts = { - delay, - ...(parentDelayId !== null && { parent_delay_id: parentDelayId }), - }; - } else if (parentDelayId !== null) { - delayOpts = { - parent_delay_id: parentDelayId, - }; - } else { - throw new Error('Must provide at least one of delay or parentDelayId'); - } + const delayOpts = { delay }; let r: SendDelayedEventResponse | null; if (stateKey !== null) { @@ -295,6 +286,36 @@ export class CallWidgetDriver extends WidgetDriver { }; } + /** + * MSC4515: the embedded Element Call asks us for the RTC transports because + * a widget can't make authenticated homeserver calls itself. Prefer the + * homeserver's MSC4143 `/rtc/transports` endpoint; fall back to the + * `org.matrix.msc4143.rtc_foci` list in the client .well-known (what + * Synapse deployments without MSC4143 advertise today, incl. ours). + * Returning an empty list makes EC report "Call is not supported". + */ + public async getRtcTransports(): Promise { + try { + const fromServer = await this.mx._unstable_getRTCTransports(); + if (Array.isArray(fromServer) && fromServer.length > 0) { + return { rtc_transports: fromServer as unknown as IRtcTransport[] }; + } + } catch { + // 404 / M_UNRECOGNIZED on homeservers without MSC4143 — fall through. + } + const wellKnown = this.mx.getClientWellKnown() as + | { 'org.matrix.msc4143.rtc_foci'?: unknown } + | undefined; + const foci = wellKnown?.['org.matrix.msc4143.rtc_foci']; + const transports = Array.isArray(foci) + ? (foci.filter( + (f): f is IRtcTransport => + !!f && typeof f === 'object' && typeof (f as { type?: unknown }).type === 'string', + ) as IRtcTransport[]) + : []; + return { rtc_transports: transports }; + } + public async getMediaConfig(): Promise { return this.mx.getMediaConfig(); } diff --git a/src/app/plugins/call/utils.ts b/src/app/plugins/call/utils.ts index 30a59b9d0..7841a5fa2 100644 --- a/src/app/plugins/call/utils.ts +++ b/src/app/plugins/call/utils.ts @@ -20,6 +20,10 @@ export function getCallCapabilities( capabilities.add(MatrixCapabilities.MSC3846TurnServers); capabilities.add(MatrixCapabilities.MSC4157SendDelayedEvent); capabilities.add(MatrixCapabilities.MSC4157UpdateDelayedEvent); + // MSC4515: Element Call >= 0.22 in widget mode discovers its LiveKit + // transport by asking the host (it no longer reads .well-known itself), so + // without this capability every call fails with MISSING_MATRIX_RTC_TRANSPORT. + capabilities.add(MatrixCapabilities.MSC4515RtcTransports); capabilities.add(`org.matrix.msc2762.timeline:${roomId}`); capabilities.add(`org.matrix.msc2762.state:${roomId}`);