-
-
Notifications
You must be signed in to change notification settings - Fork 618
/
Copy pathembedded.ts
703 lines (627 loc) · 29.4 KB
/
embedded.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {
type WidgetApi,
WidgetApiToWidgetAction,
WidgetApiResponseError,
MatrixCapabilities,
type IWidgetApiRequest,
type IWidgetApiAcknowledgeResponseData,
type ISendEventToWidgetActionRequest,
type ISendToDeviceToWidgetActionRequest,
type ISendEventFromWidgetResponseData,
type IWidgetApiRequestData,
type WidgetApiAction,
type IWidgetApiResponse,
type IWidgetApiResponseData,
} from "matrix-widget-api";
import { MatrixEvent, type IEvent, type IContent, EventStatus } from "./models/event.ts";
import {
type ISendEventResponse,
type SendDelayedEventRequestOpts,
type SendDelayedEventResponse,
type UpdateDelayedEventAction,
} from "./@types/requests.ts";
import { EventType, type StateEvents } from "./@types/event.ts";
import { logger } from "./logger.ts";
import {
MatrixClient,
ClientEvent,
type IMatrixClientCreateOpts,
type IStartClientOpts,
type SendToDeviceContentMap,
type IOpenIDToken,
UNSTABLE_MSC4140_DELAYED_EVENTS,
} from "./client.ts";
import { SyncApi, SyncState } from "./sync.ts";
import { SlidingSyncSdk } from "./sliding-sync-sdk.ts";
import { ConnectionError, MatrixError } from "./http-api/errors.ts";
import { User } from "./models/user.ts";
import { type Room } from "./models/room.ts";
import { type ToDeviceBatch, type ToDevicePayload } from "./models/ToDeviceMessage.ts";
import { MapWithDefault, recursiveMapToObject } from "./utils.ts";
import { type EmptyObject, TypedEventEmitter, UnsupportedDelayedEventsEndpointError } from "./matrix.ts";
interface IStateEventRequest {
eventType: string;
stateKey?: string;
}
export interface ICapabilities {
/**
* Event types that this client expects to send.
*/
sendEvent?: string[];
/**
* Event types that this client expects to receive.
*/
receiveEvent?: string[];
/**
* Message types that this client expects to send, or true for all message
* types.
*/
sendMessage?: string[] | true;
/**
* Message types that this client expects to receive, or true for all
* message types.
*/
receiveMessage?: string[] | true;
/**
* Types of state events that this client expects to send.
*/
sendState?: IStateEventRequest[];
/**
* Types of state events that this client expects to receive.
*/
receiveState?: IStateEventRequest[];
/**
* To-device event types that this client expects to send.
*/
sendToDevice?: string[];
/**
* To-device event types that this client expects to receive.
*/
receiveToDevice?: string[];
/**
* Whether this client needs access to TURN servers.
* @defaultValue false
*/
turnServers?: boolean;
/**
* Whether this client needs to be able to send delayed events.
* @experimental Part of MSC4140 & MSC4157
* @defaultValue false
*/
sendDelayedEvents?: boolean;
/**
* Whether this client needs to be able to update delayed events.
* @experimental Part of MSC4140 & MSC4157
* @defaultValue false
*/
updateDelayedEvents?: boolean;
}
export enum RoomWidgetClientEvent {
PendingEventsChanged = "PendingEvent.pendingEventsChanged",
}
export type EventHandlerMap = { [RoomWidgetClientEvent.PendingEventsChanged]: () => void };
/**
* A MatrixClient that routes its requests through the widget API instead of the
* real CS API.
* @experimental This class is considered unstable!
*/
export class RoomWidgetClient extends MatrixClient {
private room?: Room;
private readonly widgetApiReady: Promise<void>;
private lifecycle?: AbortController;
private syncState: SyncState | null = null;
private pendingSendingEventsTxId: { type: string; id: string | undefined; txId: string }[] = [];
private eventEmitter = new TypedEventEmitter<keyof EventHandlerMap, EventHandlerMap>();
/**
*
* @param widgetApi - The widget api to use for communication.
* @param capabilities - The capabilities the widget client will request.
* @param roomId - The room id the widget is associated with.
* @param opts - The configuration options for this client.
* @param sendContentLoaded - Whether to send a content loaded widget action immediately after initial setup.
* Set to `false` if the widget uses `waitForIFrameLoad=true` (in this case the client does not expect a content loaded action at all),
* or if the the widget wants to send the `ContentLoaded` action at a later point in time after the initial setup.
*/
public constructor(
private readonly widgetApi: WidgetApi,
private readonly capabilities: ICapabilities,
private readonly roomId: string,
opts: IMatrixClientCreateOpts,
sendContentLoaded: boolean,
) {
super(opts);
const transportSend = this.widgetApi.transport.send.bind(this.widgetApi.transport);
this.widgetApi.transport.send = async <
T extends IWidgetApiRequestData,
R extends IWidgetApiResponseData = IWidgetApiAcknowledgeResponseData,
>(
action: WidgetApiAction,
data: T,
): Promise<R> => {
try {
return await transportSend(action, data);
} catch (error) {
processAndThrow(error);
}
};
const transportSendComplete = this.widgetApi.transport.sendComplete.bind(this.widgetApi.transport);
this.widgetApi.transport.sendComplete = async <T extends IWidgetApiRequestData, R extends IWidgetApiResponse>(
action: WidgetApiAction,
data: T,
): Promise<R> => {
try {
return await transportSendComplete(action, data);
} catch (error) {
processAndThrow(error);
}
};
this.widgetApiReady = new Promise<void>((resolve) => this.widgetApi.once("ready", resolve));
// Request capabilities for the functionality this client needs to support
if (
capabilities.sendEvent?.length ||
capabilities.receiveEvent?.length ||
capabilities.sendMessage === true ||
(Array.isArray(capabilities.sendMessage) && capabilities.sendMessage.length) ||
capabilities.receiveMessage === true ||
(Array.isArray(capabilities.receiveMessage) && capabilities.receiveMessage.length) ||
capabilities.sendState?.length ||
capabilities.receiveState?.length
) {
widgetApi.requestCapabilityForRoomTimeline(roomId);
}
capabilities.sendEvent?.forEach((eventType) => widgetApi.requestCapabilityToSendEvent(eventType));
capabilities.receiveEvent?.forEach((eventType) => widgetApi.requestCapabilityToReceiveEvent(eventType));
if (capabilities.sendMessage === true) {
widgetApi.requestCapabilityToSendMessage();
} else if (Array.isArray(capabilities.sendMessage)) {
capabilities.sendMessage.forEach((msgType) => widgetApi.requestCapabilityToSendMessage(msgType));
}
if (capabilities.receiveMessage === true) {
widgetApi.requestCapabilityToReceiveMessage();
} else if (Array.isArray(capabilities.receiveMessage)) {
capabilities.receiveMessage.forEach((msgType) => widgetApi.requestCapabilityToReceiveMessage(msgType));
}
capabilities.sendState?.forEach(({ eventType, stateKey }) =>
widgetApi.requestCapabilityToSendState(eventType, stateKey),
);
capabilities.receiveState?.forEach(({ eventType, stateKey }) =>
widgetApi.requestCapabilityToReceiveState(eventType, stateKey),
);
capabilities.sendToDevice?.forEach((eventType) => widgetApi.requestCapabilityToSendToDevice(eventType));
capabilities.receiveToDevice?.forEach((eventType) => widgetApi.requestCapabilityToReceiveToDevice(eventType));
if (
capabilities.sendDelayedEvents &&
(capabilities.sendEvent?.length ||
capabilities.sendMessage === true ||
(Array.isArray(capabilities.sendMessage) && capabilities.sendMessage.length) ||
capabilities.sendState?.length)
) {
widgetApi.requestCapability(MatrixCapabilities.MSC4157SendDelayedEvent);
}
if (capabilities.updateDelayedEvents) {
widgetApi.requestCapability(MatrixCapabilities.MSC4157UpdateDelayedEvent);
}
if (capabilities.turnServers) {
widgetApi.requestCapability(MatrixCapabilities.MSC3846TurnServers);
}
widgetApi.on(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
widgetApi.on(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
// Open communication with the host
widgetApi.start();
// Send a content loaded event now we've started the widget API
// Note that element-web currently does not use waitForIFrameLoad=false and so
// does *not* (yes, that is the right way around) wait for this event. Let's
// start sending this, then once this has rolled out, we can change element-web to
// use waitForIFrameLoad=false and have a widget API that's less racy.
if (sendContentLoaded) widgetApi.sendContentLoaded();
}
public async startClient(opts: IStartClientOpts = {}): Promise<void> {
this.lifecycle = new AbortController();
// Create our own user object artificially (instead of waiting for sync)
// so it's always available, even if the user is not in any rooms etc.
const userId = this.getUserId();
if (userId) {
this.store.storeUser(new User(userId));
}
// Even though we have no access token and cannot sync, the sync class
// still has some valuable helper methods that we make use of, so we
// instantiate it anyways
if (opts.slidingSync) {
this.syncApi = new SlidingSyncSdk(opts.slidingSync, this, opts, this.buildSyncApiOptions());
} else {
this.syncApi = new SyncApi(this, opts, this.buildSyncApiOptions());
}
this.room = this.syncApi.createRoom(this.roomId);
this.store.storeRoom(this.room);
await this.widgetApiReady;
// Backfill the requested events
// We only get the most recent event for every type + state key combo,
// so it doesn't really matter what order we inject them in
await Promise.all(
this.capabilities.receiveState?.map(async ({ eventType, stateKey }) => {
const rawEvents = await this.widgetApi.readStateEvents(eventType, undefined, stateKey, [this.roomId]);
const events = rawEvents.map((rawEvent) => new MatrixEvent(rawEvent as Partial<IEvent>));
if (this.syncApi instanceof SyncApi) {
// Passing undefined for `stateAfterEventList` allows will make `injectRoomEvents` run in legacy mode
// -> state events in `timelineEventList` will update the state.
await this.syncApi.injectRoomEvents(this.room!, undefined, events);
} else {
await this.syncApi!.injectRoomEvents(this.room!, events); // Sliding Sync
}
events.forEach((event) => {
this.emit(ClientEvent.Event, event);
logger.info(`Backfilled event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
});
}) ?? [],
);
if (opts.clientWellKnownPollPeriod !== undefined) {
this.clientWellKnownIntervalID = setInterval(() => {
this.fetchClientWellKnown();
}, 1000 * opts.clientWellKnownPollPeriod);
this.fetchClientWellKnown();
}
this.setSyncState(SyncState.Syncing);
logger.info("Finished backfilling events");
this.matrixRTC.start();
// Watch for TURN servers, if requested
if (this.capabilities.turnServers) this.watchTurnServers();
}
public stopClient(): void {
this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
super.stopClient();
this.lifecycle!.abort(); // Signal to other async tasks that the client has stopped
}
public async joinRoom(roomIdOrAlias: string): Promise<Room> {
if (roomIdOrAlias === this.roomId) return this.room!;
throw new Error(`Unknown room: ${roomIdOrAlias}`);
}
protected async encryptAndSendEvent(room: Room, event: MatrixEvent): Promise<ISendEventResponse>;
protected async encryptAndSendEvent(
room: Room,
event: MatrixEvent,
delayOpts: SendDelayedEventRequestOpts,
): Promise<SendDelayedEventResponse>;
protected async encryptAndSendEvent(
room: Room,
event: MatrixEvent,
delayOpts?: SendDelayedEventRequestOpts,
): Promise<ISendEventResponse | SendDelayedEventResponse> {
// We need to extend the content with the redacts parameter
// The js sdk uses event.redacts but the widget api uses event.content.redacts
// This will be converted back to event.redacts in the widget driver.
const content = event.event.redacts
? { ...event.getContent(), redacts: event.event.redacts }
: event.getContent();
// Delayed event special case.
if (delayOpts) {
// TODO: updatePendingEvent for delayed events?
const response = await this.widgetApi
.sendRoomEvent(
event.getType(),
content,
room.roomId,
"delay" in delayOpts ? delayOpts.delay : undefined,
"parent_delay_id" in delayOpts ? delayOpts.parent_delay_id : undefined,
)
.catch(timeoutToConnectionError);
return this.validateSendDelayedEventResponse(response);
}
const txId = event.getTxnId();
// Add the txnId to the pending list (still with unknown evID)
if (txId) this.pendingSendingEventsTxId.push({ type: event.getType(), id: undefined, txId });
let response: ISendEventFromWidgetResponseData;
try {
response = await this.widgetApi
.sendRoomEvent(event.getType(), content, room.roomId)
.catch(timeoutToConnectionError);
} catch (e) {
this.updatePendingEventStatus(room, event, EventStatus.NOT_SENT);
throw e;
}
// This also checks for an event id on the response
room.updatePendingEvent(event, EventStatus.SENT, response.event_id);
// Update the pending events list with the eventId
this.pendingSendingEventsTxId.forEach((p) => {
if (p.txId === txId) p.id = response.event_id;
});
this.eventEmitter.emit(RoomWidgetClientEvent.PendingEventsChanged);
return { event_id: response.event_id! };
}
public async sendStateEvent(
roomId: string,
eventType: string,
content: any,
stateKey = "",
): Promise<ISendEventResponse> {
const response = await this.widgetApi
.sendStateEvent(eventType, stateKey, content, roomId)
.catch(timeoutToConnectionError);
if (response.event_id === undefined) {
throw new Error("'event_id' absent from response to an event request");
}
return { event_id: response.event_id };
}
/**
* @experimental This currently relies on an unstable MSC (MSC4140).
*/
// eslint-disable-next-line
public async _unstable_sendDelayedStateEvent<K extends keyof StateEvents>(
roomId: string,
delayOpts: SendDelayedEventRequestOpts,
eventType: K,
content: StateEvents[K],
stateKey = "",
): Promise<SendDelayedEventResponse> {
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
throw new UnsupportedDelayedEventsEndpointError(
"Server does not support the delayed events API",
"sendDelayedStateEvent",
);
}
const response = await this.widgetApi
.sendStateEvent(
eventType,
stateKey,
content,
roomId,
"delay" in delayOpts ? delayOpts.delay : undefined,
"parent_delay_id" in delayOpts ? delayOpts.parent_delay_id : undefined,
)
.catch(timeoutToConnectionError);
return this.validateSendDelayedEventResponse(response);
}
private validateSendDelayedEventResponse(response: ISendEventFromWidgetResponseData): SendDelayedEventResponse {
if (response.delay_id === undefined) {
throw new Error("'delay_id' absent from response to a delayed event request");
}
return { delay_id: response.delay_id };
}
/**
* @experimental This currently relies on an unstable MSC (MSC4140).
*/
// eslint-disable-next-line
public async _unstable_updateDelayedEvent(delayId: string, action: UpdateDelayedEventAction): Promise<EmptyObject> {
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
throw new UnsupportedDelayedEventsEndpointError(
"Server does not support the delayed events API",
"updateDelayedEvent",
);
}
await this.widgetApi.updateDelayedEvent(delayId, action).catch(timeoutToConnectionError);
return {};
}
/**
* by {@link MatrixClient.encryptAndSendToDevice}.
*/
public async encryptAndSendToDevice(
eventType: string,
devices: { userId: string; deviceId: string }[],
payload: ToDevicePayload,
): Promise<void> {
// map: user Id → device Id → payload
const contentMap: MapWithDefault<string, Map<string, ToDevicePayload>> = new MapWithDefault(() => new Map());
for (const { userId, deviceId } of devices) {
contentMap.getOrCreate(userId).set(deviceId, payload);
}
await this.widgetApi
.sendToDevice(eventType, true, recursiveMapToObject(contentMap))
.catch(timeoutToConnectionError);
}
public async sendToDevice(eventType: string, contentMap: SendToDeviceContentMap): Promise<EmptyObject> {
await this.widgetApi
.sendToDevice(eventType, false, recursiveMapToObject(contentMap))
.catch(timeoutToConnectionError);
return {};
}
public async getOpenIdToken(): Promise<IOpenIDToken> {
const token = await this.widgetApi.requestOpenIDConnectToken().catch(timeoutToConnectionError);
// the IOpenIDCredentials from the widget-api and IOpenIDToken form the matrix-js-sdk are compatible.
// we still recreate the token to make this transparent and catch'able by the linter in case the types change in the future.
return <IOpenIDToken>{
access_token: token.access_token,
expires_in: token.expires_in,
matrix_server_name: token.matrix_server_name,
token_type: token.token_type,
};
}
public async queueToDevice({ eventType, batch }: ToDeviceBatch): Promise<void> {
// map: user Id → device Id → payload
const contentMap: MapWithDefault<string, Map<string, ToDevicePayload>> = new MapWithDefault(() => new Map());
for (const { userId, deviceId, payload } of batch) {
contentMap.getOrCreate(userId).set(deviceId, payload);
}
await this.widgetApi
.sendToDevice(eventType, false, recursiveMapToObject(contentMap))
.catch(timeoutToConnectionError);
}
/**
* Send an event to a specific list of devices via the widget API. Optionally encrypts the event.
*
* If you are using a full MatrixClient you would be calling {@link MatrixClient.getCrypto().encryptToDeviceMessages()} followed
* by {@link MatrixClient.queueToDevice}.
*
* However, this is combined into a single step when running as an embedded widget client. So, we expose this method for those
* that need it.
*
* @param eventType - Type of the event to send.
* @param encrypted - Whether the event should be encrypted.
* @param contentMap - The content to send. Map from user_id to device_id to content object.
*/
public async sendToDeviceViaWidgetApi(
eventType: string,
encrypted: boolean,
contentMap: SendToDeviceContentMap,
): Promise<void> {
await this.widgetApi
.sendToDevice(eventType, encrypted, recursiveMapToObject(contentMap))
.catch(timeoutToConnectionError);
}
// Overridden since we get TURN servers automatically over the widget API,
// and this method would otherwise complain about missing an access token
public async checkTurnServers(): Promise<boolean> {
return this.turnServers.length > 0;
}
// Overridden since we 'sync' manually without the sync API
public getSyncState(): SyncState | null {
return this.syncState;
}
private setSyncState(state: SyncState): void {
const oldState = this.syncState;
this.syncState = state;
this.emit(ClientEvent.Sync, state, oldState);
}
private async ack(ev: CustomEvent<IWidgetApiRequest>): Promise<void> {
await this.widgetApi.transport.reply<IWidgetApiAcknowledgeResponseData>(ev.detail, {});
}
private updateTxId = async (event: MatrixEvent): Promise<void> => {
// We update the txId for remote echos that originate from this client.
// This happens with the help of `pendingSendingEventsTxId` where we store all events that are currently sending
// with their widget txId and once ready the final evId.
if (
// This could theoretically be an event send by this device
// In that case we need to update the txId of the event because the embedded client/widget
// knows this event with a different transaction Id than what was used by the host client.
event.getSender() === this.getUserId() &&
// We optimize by not blocking events from types that we have not send
// with this client.
this.pendingSendingEventsTxId.some((p) => event.getType() === p.type)
) {
// Compare by event Id if we have a matching pending event where we know the txId.
let matchingTxId = this.pendingSendingEventsTxId.find((p) => p.id === event.getId())?.txId;
// Block any further processing of this event until we have received the sending response.
// -> until we know the event id.
// -> until we have not pending events anymore.
while (!matchingTxId && this.pendingSendingEventsTxId.length > 0) {
// Recheck whenever the PendingEventsChanged
await new Promise<void>((resolve) =>
this.eventEmitter.once(RoomWidgetClientEvent.PendingEventsChanged, () => resolve()),
);
matchingTxId = this.pendingSendingEventsTxId.find((p) => p.id === event.getId())?.txId;
}
// We found the correct txId: we update the event and delete the entry of the pending events.
if (matchingTxId) {
event.setTxnId(matchingTxId);
event.setUnsigned({ ...event.getUnsigned(), transaction_id: matchingTxId });
}
this.pendingSendingEventsTxId = this.pendingSendingEventsTxId.filter((p) => p.id !== event.getId());
// Emit once there are no pending events anymore to release all other events that got
// awaited in the `while (!matchingTxId && this.pendingSendingEventsTxId.length > 0)` loop
// but are not send by this client.
if (this.pendingSendingEventsTxId.length === 0) {
this.eventEmitter.emit(RoomWidgetClientEvent.PendingEventsChanged);
}
}
};
private onEvent = async (ev: CustomEvent<ISendEventToWidgetActionRequest>): Promise<void> => {
ev.preventDefault();
// Verify the room ID matches, since it's possible for the client to
// send us events from other rooms if this widget is always on screen
if (ev.detail.data.room_id === this.roomId) {
const event = new MatrixEvent(ev.detail.data as Partial<IEvent>);
// Only inject once we have update the txId
await this.updateTxId(event);
// The widget API does not tell us whether a state event came from `state_after` or not so we assume legacy behaviour for now.
if (this.syncApi instanceof SyncApi) {
// The code will want to be something like:
// ```
// if (!params.addToTimeline && !params.addToState) {
// // Passing undefined for `stateAfterEventList` makes `injectRoomEvents` run in "legacy mode"
// // -> state events part of the `timelineEventList` parameter will update the state.
// this.injectRoomEvents(this.room!, [], undefined, [event]);
// } else {
// this.injectRoomEvents(this.room!, undefined, params.addToState ? [event] : [], params.addToTimeline ? [event] : []);
// }
// ```
// Passing undefined for `stateAfterEventList` allows will make `injectRoomEvents` run in legacy mode
// -> state events in `timelineEventList` will update the state.
await this.syncApi.injectRoomEvents(this.room!, [], undefined, [event]);
} else {
// The code will want to be something like:
// ```
// if (!params.addToTimeline && !params.addToState) {
// this.injectRoomEvents(this.room!, [], [event]);
// } else {
// this.injectRoomEvents(this.room!, params.addToState ? [event] : [], params.addToTimeline ? [event] : []);
// }
// ```
await this.syncApi!.injectRoomEvents(this.room!, [], [event]); // Sliding Sync
}
this.emit(ClientEvent.Event, event);
this.setSyncState(SyncState.Syncing);
logger.info(`Received event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
} else {
const { event_id: eventId, room_id: roomId } = ev.detail.data;
logger.info(`Received event ${eventId} for a different room ${roomId}; discarding`);
}
await this.ack(ev);
};
private onToDevice = async (ev: CustomEvent<ISendToDeviceToWidgetActionRequest>): Promise<void> => {
ev.preventDefault();
const event = new MatrixEvent({
type: ev.detail.data.type,
sender: ev.detail.data.sender,
content: ev.detail.data.content as IContent,
});
// Mark the event as encrypted if it was, using fake contents and keys since those are unknown to us
if (ev.detail.data.encrypted) event.makeEncrypted(EventType.RoomMessageEncrypted, {}, "", "");
this.emit(ClientEvent.ToDeviceEvent, event);
this.setSyncState(SyncState.Syncing);
await this.ack(ev);
};
private async watchTurnServers(): Promise<void> {
const servers = this.widgetApi.getTurnServers();
const onClientStopped = (): void => {
servers.return(undefined);
};
this.lifecycle!.signal.addEventListener("abort", onClientStopped);
try {
for await (const server of servers) {
this.turnServers = [
{
urls: server.uris,
username: server.username,
credential: server.password,
},
];
this.emit(ClientEvent.TurnServers, this.turnServers);
logger.log(`Received TURN server: ${server.uris}`);
}
} catch (e) {
logger.warn("Error watching TURN servers", e);
} finally {
this.lifecycle!.signal.removeEventListener("abort", onClientStopped);
}
}
}
function processAndThrow(error: unknown): never {
if (error instanceof WidgetApiResponseError && error.data.matrix_api_error) {
throw MatrixError.fromWidgetApiErrorData(error.data.matrix_api_error);
} else {
throw error;
}
}
/**
* This converts an "Request timed out" error from the PostmessageTransport into a ConnectionError.
* It either throws the original error or a new ConnectionError.
**/
function timeoutToConnectionError(error: unknown): never {
// TODO: this should not check on error.message but instead it should be a specific type
// error instanceof WidgetTimeoutError
if (error instanceof Error && error.message === "Request timed out") {
throw new ConnectionError("widget api timeout");
}
throw error;
}