feat: complete Ancestor quote bot with production-ready Docker support
This commit is contained in:
683
node_modules/matrix-js-sdk/src/embedded.ts
generated
vendored
683
node_modules/matrix-js-sdk/src/embedded.ts
generated
vendored
@@ -15,51 +15,29 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type WidgetApi,
|
||||
WidgetApi,
|
||||
WidgetApiToWidgetAction,
|
||||
WidgetApiResponseError,
|
||||
MatrixCapabilities,
|
||||
type IWidgetApiRequest,
|
||||
type IWidgetApiAcknowledgeResponseData,
|
||||
type ISendEventToWidgetActionRequest,
|
||||
type ISendToDeviceToWidgetActionRequest,
|
||||
type ISendEventFromWidgetResponseData,
|
||||
type IWidgetApiRequestData,
|
||||
type WidgetApiAction,
|
||||
type IWidgetApiResponse,
|
||||
type IWidgetApiResponseData,
|
||||
type IUpdateStateToWidgetActionRequest,
|
||||
UnstableApiVersion,
|
||||
IWidgetApiRequest,
|
||||
IWidgetApiAcknowledgeResponseData,
|
||||
ISendEventToWidgetActionRequest,
|
||||
ISendToDeviceToWidgetActionRequest,
|
||||
ISendEventFromWidgetResponseData,
|
||||
} from "matrix-widget-api";
|
||||
|
||||
import { type Transport } from "./matrixrtc/index.ts";
|
||||
import { MatrixEvent, type IEvent, EventStatus } from "./models/event.ts";
|
||||
import {
|
||||
type ISendEventResponse,
|
||||
type SendDelayedEventRequestOpts,
|
||||
type SendDelayedEventResponse,
|
||||
UpdateDelayedEventAction,
|
||||
isSendDelayedEventRequestOpts,
|
||||
} 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, type QueryDict, recursiveMapToObject } from "./utils.ts";
|
||||
import { type EmptyObject, TypedEventEmitter, UnsupportedDelayedEventsEndpointError } from "./matrix.ts";
|
||||
import { MatrixEvent, IEvent, IContent, EventStatus } from "./models/event";
|
||||
import { ISendEventResponse } from "./@types/requests";
|
||||
import { EventType } from "./@types/event";
|
||||
import { logger } from "./logger";
|
||||
import { MatrixClient, ClientEvent, IMatrixClientCreateOpts, IStartClientOpts, SendToDeviceContentMap } from "./client";
|
||||
import { SyncApi, SyncState } from "./sync";
|
||||
import { SlidingSyncSdk } from "./sliding-sync-sdk";
|
||||
import { User } from "./models/user";
|
||||
import { Room } from "./models/room";
|
||||
import { ToDeviceBatch, ToDevicePayload } from "./models/ToDeviceMessage";
|
||||
import { DeviceInfo } from "./crypto/deviceinfo";
|
||||
import { IOlmDevice } from "./crypto/algorithms/megolm";
|
||||
import { MapWithDefault, recursiveMapToObject } from "./utils";
|
||||
|
||||
interface IStateEventRequest {
|
||||
eventType: string;
|
||||
@@ -110,48 +88,8 @@ export interface ICapabilities {
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* Whether this client needs to be able to send sticky events.
|
||||
* @experimental Part of MSC4354 & MSC4407
|
||||
* @defaultValue false
|
||||
*/
|
||||
sendSticky?: boolean;
|
||||
|
||||
/**
|
||||
* Whether this client needs to be able to receive sticky events.
|
||||
* @experimental Part of MSC4354 & MSC4407
|
||||
* @defaultValue false
|
||||
*/
|
||||
receiveSticky?: boolean;
|
||||
|
||||
/**
|
||||
* Whether this client needs to be able to discover the RTC transports the host can reach.
|
||||
* @experimental Part of MSC4515
|
||||
* @defaultValue false
|
||||
*/
|
||||
rtcTransports?: 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.
|
||||
@@ -160,83 +98,19 @@ export type EventHandlerMap = { [RoomWidgetClientEvent.PendingEventsChanged]: ()
|
||||
export class RoomWidgetClient extends MatrixClient {
|
||||
private room?: Room;
|
||||
private readonly widgetApiReady: Promise<void>;
|
||||
private readonly roomStateSynced: 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));
|
||||
this.roomStateSynced = capabilities.receiveState?.length
|
||||
? new Promise<void>((resolve) =>
|
||||
this.widgetApi.once(`action:${WidgetApiToWidgetAction.UpdateState}`, resolve),
|
||||
)
|
||||
: Promise.resolve();
|
||||
|
||||
this.requestInitialCapabilities(capabilities, roomId);
|
||||
|
||||
widgetApi.on(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
|
||||
widgetApi.on(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
|
||||
widgetApi.on(`action:${WidgetApiToWidgetAction.UpdateState}`, this.onStateUpdate);
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
private requestInitialCapabilities(capabilities: ICapabilities, roomId: string): void {
|
||||
// Request capabilities for the functionality this client needs to support
|
||||
if (
|
||||
capabilities.sendEvent?.length ||
|
||||
@@ -248,61 +122,39 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
capabilities.sendState?.length ||
|
||||
capabilities.receiveState?.length
|
||||
) {
|
||||
this.widgetApi.requestCapabilityForRoomTimeline(roomId);
|
||||
widgetApi.requestCapabilityForRoomTimeline(roomId);
|
||||
}
|
||||
capabilities.sendEvent?.forEach((eventType) => this.widgetApi.requestCapabilityToSendEvent(eventType));
|
||||
capabilities.receiveEvent?.forEach((eventType) => this.widgetApi.requestCapabilityToReceiveEvent(eventType));
|
||||
capabilities.sendEvent?.forEach((eventType) => widgetApi.requestCapabilityToSendEvent(eventType));
|
||||
capabilities.receiveEvent?.forEach((eventType) => widgetApi.requestCapabilityToReceiveEvent(eventType));
|
||||
if (capabilities.sendMessage === true) {
|
||||
this.widgetApi.requestCapabilityToSendMessage();
|
||||
widgetApi.requestCapabilityToSendMessage();
|
||||
} else if (Array.isArray(capabilities.sendMessage)) {
|
||||
capabilities.sendMessage.forEach((msgType) => this.widgetApi.requestCapabilityToSendMessage(msgType));
|
||||
capabilities.sendMessage.forEach((msgType) => widgetApi.requestCapabilityToSendMessage(msgType));
|
||||
}
|
||||
if (capabilities.receiveMessage === true) {
|
||||
this.widgetApi.requestCapabilityToReceiveMessage();
|
||||
widgetApi.requestCapabilityToReceiveMessage();
|
||||
} else if (Array.isArray(capabilities.receiveMessage)) {
|
||||
capabilities.receiveMessage.forEach((msgType) => this.widgetApi.requestCapabilityToReceiveMessage(msgType));
|
||||
capabilities.receiveMessage.forEach((msgType) => widgetApi.requestCapabilityToReceiveMessage(msgType));
|
||||
}
|
||||
capabilities.sendState?.forEach(({ eventType, stateKey }) =>
|
||||
this.widgetApi.requestCapabilityToSendState(eventType, stateKey),
|
||||
widgetApi.requestCapabilityToSendState(eventType, stateKey),
|
||||
);
|
||||
capabilities.receiveState?.forEach(({ eventType, stateKey }) =>
|
||||
this.widgetApi.requestCapabilityToReceiveState(eventType, stateKey),
|
||||
widgetApi.requestCapabilityToReceiveState(eventType, stateKey),
|
||||
);
|
||||
capabilities.sendToDevice?.forEach((eventType) => this.widgetApi.requestCapabilityToSendToDevice(eventType));
|
||||
capabilities.receiveToDevice?.forEach((eventType) =>
|
||||
this.widgetApi.requestCapabilityToReceiveToDevice(eventType),
|
||||
);
|
||||
if (
|
||||
capabilities.sendDelayedEvents &&
|
||||
(capabilities.sendEvent?.length ||
|
||||
capabilities.sendMessage === true ||
|
||||
(Array.isArray(capabilities.sendMessage) && capabilities.sendMessage.length) ||
|
||||
capabilities.sendState?.length)
|
||||
) {
|
||||
this.widgetApi.requestCapability(MatrixCapabilities.MSC4157SendDelayedEvent);
|
||||
}
|
||||
if (capabilities.updateDelayedEvents) {
|
||||
this.widgetApi.requestCapability(MatrixCapabilities.MSC4157UpdateDelayedEvent);
|
||||
}
|
||||
if (capabilities.sendSticky) {
|
||||
this.widgetApi.requestCapability(MatrixCapabilities.MSC4407SendStickyEvent);
|
||||
}
|
||||
if (capabilities.receiveSticky) {
|
||||
this.widgetApi.requestCapability(MatrixCapabilities.MSC4407ReceiveStickyEvent);
|
||||
}
|
||||
capabilities.sendToDevice?.forEach((eventType) => widgetApi.requestCapabilityToSendToDevice(eventType));
|
||||
capabilities.receiveToDevice?.forEach((eventType) => widgetApi.requestCapabilityToReceiveToDevice(eventType));
|
||||
if (capabilities.turnServers) {
|
||||
this.widgetApi.requestCapability(MatrixCapabilities.MSC3846TurnServers);
|
||||
}
|
||||
if (capabilities.rtcTransports) {
|
||||
this.widgetApi.requestCapability(MatrixCapabilities.MSC4515RtcTransports);
|
||||
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();
|
||||
}
|
||||
|
||||
public async supportUpdateState(): Promise<boolean> {
|
||||
return (await this.widgetApi.getClientVersions()).includes(UnstableApiVersion.MSC2762_UPDATE_STATE);
|
||||
}
|
||||
|
||||
private readonly syncApiResolver = Promise.withResolvers<void>();
|
||||
public async startClient(opts: IStartClientOpts = {}): Promise<void> {
|
||||
this.lifecycle = new AbortController();
|
||||
|
||||
@@ -321,49 +173,29 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
} else {
|
||||
this.syncApi = new SyncApi(this, opts, this.buildSyncApiOptions());
|
||||
}
|
||||
this.syncApiResolver.resolve();
|
||||
|
||||
this.room = this.syncApi.createRoom(this.roomId);
|
||||
this.store.storeRoom(this.room);
|
||||
|
||||
await this.widgetApiReady;
|
||||
|
||||
// sync room state:
|
||||
if (await this.supportUpdateState()) {
|
||||
// This will resolve once the client driver has sent us all the allowed room state.
|
||||
await this.roomStateSynced;
|
||||
} else {
|
||||
// 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>));
|
||||
// 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 events as `stateAfterEventList` 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()}`);
|
||||
});
|
||||
}) ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
this.cachedWellKnown.start(
|
||||
opts.clientWellKnownPollPeriod !== undefined ? 1000 * opts.clientWellKnownPollPeriod : undefined,
|
||||
await this.syncApi!.injectRoomEvents(this.room!, [], events);
|
||||
events.forEach((event) => {
|
||||
this.emit(ClientEvent.Event, event);
|
||||
logger.info(`Backfilled event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
|
||||
});
|
||||
}) ?? [],
|
||||
);
|
||||
this.setSyncState(SyncState.Syncing);
|
||||
logger.info("Finished initial sync");
|
||||
|
||||
this.matrixRTC.start();
|
||||
logger.info("Finished backfilling events");
|
||||
|
||||
// Watch for TURN servers, if requested
|
||||
if (this.capabilities.turnServers) this.watchTurnServers();
|
||||
@@ -372,7 +204,6 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
public stopClient(): void {
|
||||
this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
|
||||
this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
|
||||
this.widgetApi.off(`action:${WidgetApiToWidgetAction.UpdateState}`, this.onStateUpdate);
|
||||
|
||||
super.stopClient();
|
||||
this.lifecycle!.abort(); // Signal to other async tasks that the client has stopped
|
||||
@@ -383,87 +214,17 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
throw new Error(`Unknown room: ${roomIdOrAlias}`);
|
||||
}
|
||||
|
||||
protected async encryptAndSendEvent(
|
||||
room: Room,
|
||||
event: MatrixEvent,
|
||||
queryDict?: QueryDict,
|
||||
): Promise<ISendEventResponse>;
|
||||
protected async encryptAndSendEvent(
|
||||
room: Room,
|
||||
event: MatrixEvent,
|
||||
delayOpts: SendDelayedEventRequestOpts,
|
||||
queryDict?: QueryDict,
|
||||
): Promise<ISendEventResponse>;
|
||||
protected async encryptAndSendEvent(
|
||||
room: Room,
|
||||
event: MatrixEvent,
|
||||
delayOptsOrQuery?: SendDelayedEventRequestOpts | QueryDict,
|
||||
queryDict?: QueryDict,
|
||||
): Promise<ISendEventResponse | SendDelayedEventResponse> {
|
||||
let queryOpts = queryDict;
|
||||
let delayOpts: SendDelayedEventRequestOpts | undefined;
|
||||
if (delayOptsOrQuery && isSendDelayedEventRequestOpts(delayOptsOrQuery)) {
|
||||
delayOpts = delayOptsOrQuery;
|
||||
} else if (!queryOpts) {
|
||||
queryOpts = delayOptsOrQuery;
|
||||
}
|
||||
|
||||
const stickyDurationMs = queryOpts?.["org.matrix.msc4354.sticky_duration_ms"];
|
||||
if (stickyDurationMs !== undefined && typeof stickyDurationMs !== "number") {
|
||||
throw new Error("Sticky duration must be a number when defined");
|
||||
}
|
||||
// This is save since we just checked that above
|
||||
// We need the additional as assertion for the EW linter to be happy.
|
||||
// It is not capable of implying the type based on the throw if `stickyDurationMs !== undefined && typeof stickyDurationMs !== "number"`
|
||||
// above
|
||||
const stickyDurationMsAsNumber: number | undefined = stickyDurationMs;
|
||||
|
||||
// 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,
|
||||
stickyDurationMsAsNumber,
|
||||
)
|
||||
.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 });
|
||||
|
||||
protected async encryptAndSendEvent(room: Room, event: MatrixEvent): Promise<ISendEventResponse> {
|
||||
let response: ISendEventFromWidgetResponseData;
|
||||
try {
|
||||
response = await this.widgetApi
|
||||
.sendRoomEvent(event.getType(), content, room.roomId, undefined, undefined, stickyDurationMsAsNumber)
|
||||
.catch(timeoutToConnectionError);
|
||||
response = await this.widgetApi.sendRoomEvent(event.getType(), event.getContent(), room.roomId);
|
||||
} 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! };
|
||||
return { event_id: response.event_id };
|
||||
}
|
||||
|
||||
public async sendStateEvent(
|
||||
@@ -472,179 +233,14 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
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 };
|
||||
return await this.widgetApi.sendStateEvent(eventType, stateKey, content, roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental This currently relies on an unstable MSC (MSC4140).
|
||||
*/
|
||||
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).
|
||||
* @deprecated Instead use one of:
|
||||
* - {@link _unstable_cancelScheduledDelayedEvent}
|
||||
* - {@link _unstable_restartScheduledDelayedEvent}
|
||||
* - {@link _unstable_sendScheduledDelayedEvent}
|
||||
*/
|
||||
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",
|
||||
);
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case UpdateDelayedEventAction.Cancel:
|
||||
await this.widgetApi.cancelScheduledDelayedEvent(delayId).catch(timeoutToConnectionError);
|
||||
break;
|
||||
case UpdateDelayedEventAction.Restart:
|
||||
await this.widgetApi.restartScheduledDelayedEvent(delayId).catch(timeoutToConnectionError);
|
||||
break;
|
||||
case UpdateDelayedEventAction.Send:
|
||||
await this.widgetApi.sendScheduledDelayedEvent(delayId).catch(timeoutToConnectionError);
|
||||
break;
|
||||
}
|
||||
public async sendToDevice(eventType: string, contentMap: SendToDeviceContentMap): Promise<{}> {
|
||||
await this.widgetApi.sendToDevice(eventType, false, recursiveMapToObject(contentMap));
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental This currently relies on an unstable MSC (MSC4140).
|
||||
*/
|
||||
public async _unstable_cancelScheduledDelayedEvent(delayId: string): Promise<EmptyObject> {
|
||||
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
|
||||
throw new UnsupportedDelayedEventsEndpointError(
|
||||
"Server does not support the delayed events API",
|
||||
"cancelScheduledDelayedEvent",
|
||||
);
|
||||
}
|
||||
|
||||
await this.widgetApi.cancelScheduledDelayedEvent(delayId).catch(timeoutToConnectionError);
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental This currently relies on an unstable MSC (MSC4140).
|
||||
*/
|
||||
public async _unstable_restartScheduledDelayedEvent(delayId: string): Promise<EmptyObject> {
|
||||
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
|
||||
throw new UnsupportedDelayedEventsEndpointError(
|
||||
"Server does not support the delayed events API",
|
||||
"restartScheduledDelayedEvent",
|
||||
);
|
||||
}
|
||||
|
||||
await this.widgetApi.restartScheduledDelayedEvent(delayId).catch(timeoutToConnectionError);
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental This currently relies on an unstable MSC (MSC4140).
|
||||
*/
|
||||
public async _unstable_sendScheduledDelayedEvent(delayId: string): Promise<EmptyObject> {
|
||||
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
|
||||
throw new UnsupportedDelayedEventsEndpointError(
|
||||
"Server does not support the delayed events API",
|
||||
"sendScheduledDelayedEvent",
|
||||
);
|
||||
}
|
||||
|
||||
await this.widgetApi.sendScheduledDelayedEvent(delayId).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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of configured RTC transports supported by the homeserver.
|
||||
*
|
||||
* Overrides the homeserver-side {@link MatrixClient._unstable_getRTCTransports} (MSC4143):
|
||||
* a widget cannot make authenticated homeserver calls itself, so we ask the host over the
|
||||
* widget API instead (MSC4515). Requires the `rtcTransports` capability and a host that
|
||||
* advertises the `org.matrix.msc4515` API version (otherwise the request throws).
|
||||
*/
|
||||
public override async _unstable_getRTCTransports(): Promise<Transport[]> {
|
||||
const { rtc_transports: rtcTransports } = await this.widgetApi
|
||||
.getRtcTransports()
|
||||
.catch(timeoutToConnectionError);
|
||||
return rtcTransports;
|
||||
}
|
||||
|
||||
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());
|
||||
@@ -652,32 +248,20 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
contentMap.getOrCreate(userId).set(deviceId, payload);
|
||||
}
|
||||
|
||||
await this.widgetApi
|
||||
.sendToDevice(eventType, false, recursiveMapToObject(contentMap))
|
||||
.catch(timeoutToConnectionError);
|
||||
await this.widgetApi.sendToDevice(eventType, false, recursiveMapToObject(contentMap));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
public async encryptAndSendToDevices(userDeviceInfoArr: IOlmDevice<DeviceInfo>[], payload: object): Promise<void> {
|
||||
// map: user Id → device Id → payload
|
||||
const contentMap: MapWithDefault<string, Map<string, object>> = new MapWithDefault(() => new Map());
|
||||
for (const {
|
||||
userId,
|
||||
deviceInfo: { deviceId },
|
||||
} of userDeviceInfoArr) {
|
||||
contentMap.getOrCreate(userId).set(deviceId, payload);
|
||||
}
|
||||
|
||||
await this.widgetApi.sendToDevice((payload as { type: string }).type, true, recursiveMapToObject(contentMap));
|
||||
}
|
||||
|
||||
// Overridden since we get TURN servers automatically over the widget API,
|
||||
@@ -698,85 +282,20 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
}
|
||||
|
||||
private async ack(ev: CustomEvent<IWidgetApiRequest>): Promise<void> {
|
||||
this.widgetApi.transport.reply<IWidgetApiAcknowledgeResponseData>(ev.detail, {});
|
||||
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);
|
||||
|
||||
// Only inject once we have update the txId
|
||||
await this.updateTxId(event);
|
||||
await this.syncApiResolver.promise;
|
||||
if (this.syncApi instanceof SyncApi) {
|
||||
if (await this.supportUpdateState()) {
|
||||
await this.syncApi.injectRoomEvents(this.room!, undefined, [], [event]);
|
||||
} else {
|
||||
// Passing undefined for `stateAfterEventList` will make `injectRoomEvents` run in legacy mode
|
||||
// -> state events in `timelineEventList` will update the state.
|
||||
await this.syncApi.injectRoomEvents(this.room!, [], undefined, [event]);
|
||||
}
|
||||
} else {
|
||||
// Sliding Sync
|
||||
if (await this.supportUpdateState()) {
|
||||
await this.syncApi!.injectRoomEvents(this.room!, [], [event]);
|
||||
} else {
|
||||
logger.error(
|
||||
"slididng sync cannot be used in widget mode if the client widget driver does not support the version: 'org.matrix.msc2762_update_state'",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const event = new MatrixEvent(ev.detail.data as Partial<IEvent>);
|
||||
await this.syncApi!.injectRoomEvents(this.room!, [], [event]);
|
||||
this.emit(ClientEvent.Event, event);
|
||||
if (event.unstableStickyInfo !== undefined) this.room!._unstable_addStickyEvents([event]);
|
||||
this.setSyncState(SyncState.Syncing);
|
||||
logger.info(`Received event ${event.getId()} ${event.getType()}`);
|
||||
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`);
|
||||
@@ -791,7 +310,7 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
const event = new MatrixEvent({
|
||||
type: ev.detail.data.type,
|
||||
sender: ev.detail.data.sender,
|
||||
content: ev.detail.data.content,
|
||||
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, {}, "", "");
|
||||
@@ -801,37 +320,6 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
await this.ack(ev);
|
||||
};
|
||||
|
||||
private onStateUpdate = async (ev: CustomEvent<IUpdateStateToWidgetActionRequest>): Promise<void> => {
|
||||
ev.preventDefault();
|
||||
if (!(await this.supportUpdateState())) {
|
||||
logger.warn(
|
||||
"received update_state widget action but the widget driver did not claim to support 'org.matrix.msc2762_update_state'",
|
||||
);
|
||||
}
|
||||
await this.syncApiResolver.promise;
|
||||
for (const rawEvent of ev.detail.data.state) {
|
||||
// Verify the room ID matches, since it's possible for the client to
|
||||
// send us state updates from other rooms if this widget is always
|
||||
// on screen
|
||||
if (rawEvent.room_id === this.roomId) {
|
||||
const event = new MatrixEvent(rawEvent);
|
||||
|
||||
if (this.syncApi instanceof SyncApi) {
|
||||
await this.syncApi.injectRoomEvents(this.room!, undefined, [event]);
|
||||
} else {
|
||||
// Sliding Sync
|
||||
await this.syncApi!.injectRoomEvents(this.room!, [event]);
|
||||
}
|
||||
logger.debug(`Updated state entry ${event.getType()} ${event.getStateKey()} to ${event.getId()}`);
|
||||
} else {
|
||||
const { event_id: eventId, room_id: roomId } = ev.detail.data;
|
||||
logger.info(`Received state entry ${eventId} for a different room ${roomId}; discarding`);
|
||||
}
|
||||
}
|
||||
|
||||
await this.ack(ev);
|
||||
};
|
||||
|
||||
private async watchTurnServers(): Promise<void> {
|
||||
const servers = this.widgetApi.getTurnServers();
|
||||
const onClientStopped = (): void => {
|
||||
@@ -858,24 +346,3 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user