diff --git a/src/@types/requests.ts b/src/@types/requests.ts index d6b7ff75fa2..7c778b9bd9d 100644 --- a/src/@types/requests.ts +++ b/src/@types/requests.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { EitherAnd } from "matrix-events-sdk/lib/types"; + import { IContent, IEvent } from "../models/event"; import { Preset, Visibility } from "./partials"; import { IEventWithRoomId, SearchKey } from "./search"; @@ -76,6 +78,52 @@ export interface ISendEventResponse { event_id: string; } +export interface IFutureGroupId { + future_group_id: string; +} + +export interface IFutureTimeout { + future_timeout: number; +} + +export type ISendFutureRequestOpts = EitherAnd; + +export interface IFutureTokens { + send_token: string; + cancel_token: string; +} + +export interface ITimeoutFutureTokens extends IFutureTokens { + refresh_token: string; +} + +export type ISendActionFutureResponse = IFutureGroupId & IFutureTokens; +export type ISendTimeoutFutureResponse = IFutureGroupId & ITimeoutFutureTokens; + +export type ISendFutureResponse = F extends IFutureTimeout + ? ISendTimeoutFutureResponse + : ISendActionFutureResponse; + +export interface ISendFutureGroupResponse { + send_on_timeout: ITimeoutFutureTokens; + send_on_action?: { + [action_name: string]: IFutureTokens; + }; + send_now?: ISendEventResponse; +} + +interface IFuturePartialEvent { + room_id: string; + type: string; + state_key?: string; + content: IContent; +} + +export type IActionFutureInfo = IFutureGroupId & IFuturePartialEvent & IFutureTokens; +export type ITimeoutFutureInfo = IFutureGroupId & IFutureTimeout & IFuturePartialEvent & ITimeoutFutureTokens; + +export type IFutureInfo = IActionFutureInfo | ITimeoutFutureInfo; + export interface IPresenceOpts { // One of "online", "offline" or "unavailable" presence: "online" | "offline" | "unavailable"; diff --git a/src/client.ts b/src/client.ts index ae80589e97c..4b71226a4e5 100644 --- a/src/client.ts +++ b/src/client.ts @@ -120,6 +120,7 @@ import { ICreateRoomOpts, IEventSearchOpts, IFilterResponse, + IFutureInfo, IGuestAccessOpts, IJoinRoomOpts, INotificationsResponse, @@ -130,7 +131,12 @@ import { IRelationsResponse, IRoomDirectoryOptions, ISearchOpts, + ISendActionFutureResponse, ISendEventResponse, + ISendFutureGroupResponse, + ISendFutureRequestOpts, + ISendFutureResponse, + ISendTimeoutFutureResponse, IStatusResponse, ITagsResponse, KnockRoomOpts, @@ -530,6 +536,8 @@ export const UNSTABLE_MSC2666_SHARED_ROOMS = "uk.half-shot.msc2666"; export const UNSTABLE_MSC2666_MUTUAL_ROOMS = "uk.half-shot.msc2666.mutual_rooms"; export const UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS = "uk.half-shot.msc2666.query_mutual_rooms"; +const UNSTABLE_MSC4140_FUTURES = "org.matrix.msc4140"; + enum CrossSigningKeyType { MasterKey = "master_key", SelfSigningKey = "self_signing_key", @@ -4557,6 +4565,29 @@ export class MatrixClient extends TypedEventEmitter { + const { threadId, eventType, content, txnId } = this.processEventArgs( + roomId, + threadIdOrEventType, + eventTypeOrContent, + contentOrTxnId, + txnIdOrVoid, + ); + + return this.sendCompleteEvent(roomId, threadId, { type: eventType, content }, txnId); + } + + private processEventArgs( + roomId: string, + threadIdOrEventType: string | null, + eventTypeOrContent: string | IContent, + contentOrTxnId?: IContent | string, + txnIdOrVoid?: string, + ): { + threadId: string | null; + eventType: string; + content: IContent; + txnId: string | undefined; + } { let threadId: string | null; let eventType: string; let content: IContent; @@ -4597,10 +4628,16 @@ export class MatrixClient extends TypedEventEmitter, txnId?: string, - ): Promise { + ): Promise; + private sendCompleteEvent( + roomId: string, + threadId: string | null, + eventObject: Partial, + futureOpts: F, + txnId?: string, + ): Promise>; + private sendCompleteEvent( + roomId: string, + threadId: string | null, + eventObject: Partial, + futureOptsOrTxnId?: ISendFutureRequestOpts | string, + txnIdOrVoid?: string, + ): Promise { + let futureOpts: ISendFutureRequestOpts | undefined; + let txnId: string | undefined; + if (typeof futureOptsOrTxnId === "string") { + txnId = futureOptsOrTxnId; + } else { + futureOpts = futureOptsOrTxnId; + txnId = txnIdOrVoid; + } + if (!txnId) { txnId = this.makeTxnId(); } @@ -4635,6 +4695,7 @@ export class MatrixClient extends TypedEventEmitter { + protected async encryptAndSendEvent(room: Room | null, event: MatrixEvent): Promise; + protected async encryptAndSendEvent( + room: Room | null, + event: MatrixEvent, + futureOpts: F, + ): Promise>; + protected async encryptAndSendEvent( + room: Room | null, + event: MatrixEvent, + futureOpts?: ISendFutureRequestOpts, + ): Promise { + // TODO: Allow encrypted futures, and encrypt them properly + if (futureOpts) { + return this.sendEventHttpRequest(event, futureOpts); + } + try { let cancelled: boolean; this.eventsBeingEncrypted.add(event.getId()!); @@ -4824,7 +4906,15 @@ export class MatrixClient extends TypedEventEmitter { + private sendEventHttpRequest(event: MatrixEvent): Promise; + private sendEventHttpRequest( + event: MatrixEvent, + futureOpts: F, + ): Promise>; + private sendEventHttpRequest( + event: MatrixEvent, + futureOpts?: ISendFutureRequestOpts, + ): Promise { let txnId = event.getTxnId(); if (!txnId) { txnId = this.makeTxnId(); @@ -4838,30 +4928,37 @@ export class MatrixClient extends TypedEventEmitter 0) { - pathTemplate = "/rooms/$roomId/state/$eventType/$stateKey"; + pathTemplate = `/rooms/$roomId/state${futurePathPart}/$eventType/$stateKey`; } path = utils.encodeUri(pathTemplate, pathParams); } else if (event.isRedaction() && event.event.redacts) { - const pathTemplate = `/rooms/$roomId/redact/$redactsEventId/$txnId`; + // TODO: support future redactions? + const pathTemplate = `/rooms/$roomId/redact${futurePathPart}/$redactsEventId/$txnId`; path = utils.encodeUri(pathTemplate, { $redactsEventId: event.event.redacts, ...pathParams, }); } else { - path = utils.encodeUri("/rooms/$roomId/send/$eventType/$txnId", pathParams); + path = utils.encodeUri(`/rooms/$roomId/send${futurePathPart}/$eventType/$txnId`, pathParams); } - return this.http - .authedRequest(Method.Put, path, undefined, event.getWireContent()) - .then((res) => { + const content = event.getWireContent(); + if (!futureOpts) { + return this.http.authedRequest(Method.Put, path, undefined, content).then((res) => { this.logger.debug(`Event sent to ${event.getRoomId()} with event id ${res.event_id}`); return res; }); + } else { + return this.http.authedRequest(Method.Put, path, { ...futureOpts }, content, { + prefix: `${ClientPrefix.Unstable}/${UNSTABLE_MSC4140_FUTURES}`, + }); + } } /** @@ -5191,6 +5288,164 @@ export class MatrixClient extends TypedEventEmitter( + roomId: string, + futureOpts: F, + eventType: K, + content: TimelineEvents[K], + txnId?: string, + ): Promise>; + // eslint-disable-next-line + public _unstable_sendFuture( + roomId: string, + futureOpts: F, + threadId: string | null, + eventType: K, + content: TimelineEvents[K], + txnId?: string, + ): Promise>; + // eslint-disable-next-line + public async _unstable_sendFuture( + roomId: string, + futureOpts: ISendFutureRequestOpts, + threadIdOrEventType: string | null, + eventTypeOrContent: string | IContent, + contentOrTxnId?: IContent | string, + txnIdOrVoid?: string, + ): Promise { + if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_FUTURES))) { + throw Error("Server does not support the Futures API"); + } + + const { threadId, eventType, content, txnId } = this.processEventArgs( + roomId, + threadIdOrEventType, + eventTypeOrContent, + contentOrTxnId, + txnIdOrVoid, + ); + + return this.sendCompleteEvent(roomId, threadId, { type: eventType, content }, futureOpts, txnId); + } + + /** + * Send a future for a state event. + * + * Note: This endpoint is unstable, and can throw an `Error`. + * Check progress on [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140) for more details. + */ + // eslint-disable-next-line + public async _unstable_sendStateFuture( + roomId: string, + futureOpts: F, + eventType: K, + content: StateEvents[K], + stateKey = "", + opts: IRequestOpts = {}, + ): Promise> { + if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_FUTURES))) { + throw Error("Server does not support the Futures API"); + } + + const pathParams = { + $roomId: roomId, + $eventType: eventType, + $stateKey: stateKey, + }; + let path = utils.encodeUri("/rooms/$roomId/state_future/$eventType", pathParams); + if (stateKey !== undefined) { + path = utils.encodeUri(path + "/$stateKey", pathParams); + } + return this.http.authedRequest(Method.Put, path, { ...futureOpts }, content as Body, { + prefix: `${ClientPrefix.Unstable}/${UNSTABLE_MSC4140_FUTURES}`, + ...opts, + }); + } + + /** + * Send a group of future events. + * TODO: type safety & threads...or remove altogether + * + * Note: This endpoint is unstable, and can throw an `Error`. + * Check progress on [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140) for more details. + */ + // eslint-disable-next-line + public async _unstable_sendFutureGroup( + roomId: string, + timeout: number, + sendOnTimeout: MatrixEvent, + sendOnAction?: Record, + sendNow?: MatrixEvent, + ): Promise { + if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_FUTURES))) { + throw Error("Server does not support the Futures API"); + } + + return await this.http.authedRequest( + Method.Put, + // NOTE: Difference from MSC = remove /send part of path, to avoid ambiguity with regular event sending + utils.encodeUri("/rooms/$roomId/future/$txnId", { + $roomId: roomId, + $txnId: this.makeTxnId(), + }), + undefined, + { + timeout, + send_on_timeout: sendOnTimeout, + ...(sendOnAction ?? { send_on_action: sendOnAction }), + ...(sendNow ?? { send_now: sendNow }), + }, + { prefix: `${ClientPrefix.Unstable}/${UNSTABLE_MSC4140_FUTURES}` }, + ); + } + + /** + * Get all pending futures for the calling user. + * + * Note: This endpoint is unstable, and can throw an `Error`. + * Check progress on [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140) for more details. + */ + // eslint-disable-next-line + public async _unstable_getFutures(): Promise { + if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_FUTURES))) { + throw Error("Server does not support the Futures API"); + } + + return await this.http.authedRequest(Method.Get, "/future", undefined, undefined, { + prefix: `${ClientPrefix.Unstable}/${UNSTABLE_MSC4140_FUTURES}`, + }); + } + + /** + * Use a future token, taking the appropriate action given what the token is for. + * + * Note: This endpoint is unstable, and can throw an `Error`. + * Check progress on [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140) for more details. + */ + // eslint-disable-next-line + public async _unstable_useFutureToken(futureToken: string): Promise<{}> { + if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_FUTURES))) { + throw Error("Server does not support the Futures API"); + } + + return await this.http.request( + Method.Post, + utils.encodeUri("/future/$futureToken", { + $futureToken: futureToken, + }), + undefined, + undefined, + { prefix: `${ClientPrefix.Unstable}/${UNSTABLE_MSC4140_FUTURES}` }, + ); + } + /** * Send a receipt. * @param event - The event being acknowledged diff --git a/src/embedded.ts b/src/embedded.ts index 8a1492622ee..181bc14ebe4 100644 --- a/src/embedded.ts +++ b/src/embedded.ts @@ -26,8 +26,14 @@ import { } from "matrix-widget-api"; import { MatrixEvent, IEvent, IContent, EventStatus } from "./models/event"; -import { ISendEventResponse } from "./@types/requests"; -import { EventType } from "./@types/event"; +import { + ISendActionFutureResponse, + ISendEventResponse, + ISendFutureRequestOpts, + ISendFutureResponse, + ISendTimeoutFutureResponse, +} from "./@types/requests"; +import { EventType, StateEvents } from "./@types/event"; import { logger } from "./logger"; import { MatrixClient, @@ -248,7 +254,33 @@ export class RoomWidgetClient extends MatrixClient { throw new Error(`Unknown room: ${roomIdOrAlias}`); } - protected async encryptAndSendEvent(room: Room, event: MatrixEvent): Promise { + protected async encryptAndSendEvent(room: Room, event: MatrixEvent): Promise; + protected async encryptAndSendEvent( + room: Room, + event: MatrixEvent, + futureOpts: F, + ): Promise>; + protected async encryptAndSendEvent( + room: Room, + event: MatrixEvent, + futureOpts?: ISendFutureRequestOpts, + ): Promise { + if (futureOpts) { + // TODO: updatePendingEvent for futures? + const response = await this.widgetApi.sendRoomFuture( + futureOpts, + event.getType(), + event.getContent(), + room.roomId, + ); + return { + future_group_id: response.future_group_id, + send_token: response.send_token, + cancel_token: response.cancel_token, + ...(response.refresh_token && { refresh_token: response.refresh_token }), + }; + } + let response: ISendEventFromWidgetResponseData; try { response = await this.widgetApi.sendRoomEvent(event.getType(), event.getContent(), room.roomId); @@ -270,6 +302,27 @@ export class RoomWidgetClient extends MatrixClient { return (await this.widgetApi.sendStateEvent(eventType, stateKey, content, roomId)) as ISendEventResponse; } + /** + * @experimental This currently relies on an unstable MSC (MSC4140). + */ + // eslint-disable-next-line + public async _unstable_sendStateFuture( + roomId: string, + futureOpts: F, + eventType: K, + content: StateEvents[K], + stateKey = "", + ): Promise> { + // TODO: better type checking + return (await this.widgetApi.sendStateFuture( + futureOpts, + eventType, + stateKey, + content, + roomId, + )) as unknown as ISendFutureResponse; + } + public async sendToDevice(eventType: string, contentMap: SendToDeviceContentMap): Promise<{}> { await this.widgetApi.sendToDevice(eventType, false, recursiveMapToObject(contentMap)); return {}; diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 7f6d12aa203..9aa11be1338 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -137,6 +137,8 @@ export class MatrixRTCSession extends TypedEventEmitter>(); private lastEncryptionKeyUpdateRequest?: number; + private cancelFutureToken: string | undefined; + /** * The callId (sessionId) of the call. * @@ -865,18 +867,35 @@ export class MatrixRTCSession extends TypedEventEmitter this.refreshFuture(refreshToken), 3000); + } + + private refreshFuture(refreshToken: string): void { + this.client + ._unstable_useFutureToken(refreshToken) + .then(() => this.scheduleRefreshFuture(refreshToken)) + .catch((err) => logger.error("Failed to refresh future", err)); + } + private stateEventsContainOngoingLegacySession(callMemberEvents: Map): boolean { for (const callMemberEvent of callMemberEvents.values()) { const content = callMemberEvent.getContent();