Skip to content

Commit

Permalink
Satisfy revised MSC4157 (#92)
Browse files Browse the repository at this point in the history
* Rename futures to delayed events

* Rename timeout to delay

* Replace future groups with parent delay IDs

* Require capability for sending delayed events

* Add action & capability to update delayed events

* Rename "refresh" action to "restart"

---------

Co-authored-by: Timo <[email protected]>
  • Loading branch information
AndrewFerr and toger5 authored Jul 30, 2024
1 parent 3c50d9d commit eb57b5e
Show file tree
Hide file tree
Showing 10 changed files with 440 additions and 90 deletions.
71 changes: 57 additions & 14 deletions src/ClientWidgetApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { IContentLoadedActionRequest } from "./interfaces/ContentLoadedAction";
import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "./interfaces/WidgetApiAction";
import { IWidgetApiErrorResponseData } from "./interfaces/IWidgetApiErrorResponse";
import { Capability, MatrixCapabilities } from "./interfaces/Capabilities";
import { IOpenIDUpdate, ISendEventDetails, ISendFutureDetails, WidgetDriver } from "./driver/WidgetDriver";
import { IOpenIDUpdate, ISendEventDetails, ISendDelayedEventDetails, WidgetDriver } from "./driver/WidgetDriver";
import {
ICapabilitiesActionResponseData,
INotifyCapabilitiesActionRequestData,
Expand Down Expand Up @@ -90,6 +90,10 @@ import {
IGetMediaConfigActionFromWidgetActionRequest,
IGetMediaConfigActionFromWidgetResponseData,
} from "./interfaces/GetMediaConfigAction";
import {
IUpdateDelayedEventFromWidgetActionRequest,
UpdateDelayedEventAction,
} from "./interfaces/UpdateDelayedEventAction";
import {
IUploadFileActionFromWidgetActionRequest,
IUploadFileActionFromWidgetResponseData,
Expand Down Expand Up @@ -477,25 +481,32 @@ export class ClientWidgetApi extends EventEmitter {
});
}

let sendEventPromise: Promise<ISendEventDetails|ISendFutureDetails>;
const isDelayedEvent = request.data.delay !== undefined || request.data.parent_delay_id !== undefined;
if (isDelayedEvent && !this.hasCapability(MatrixCapabilities.MSC4157SendDelayedEvent)) {
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Missing capability"},
});
}

let sendEventPromise: Promise<ISendEventDetails|ISendDelayedEventDetails>;
if (request.data.state_key !== undefined) {
if (!this.canSendStateEvent(request.data.type, request.data.state_key)) {
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Cannot send state events of this type"},
});
}

if (request.data.future_timeout === undefined && request.data.future_group_id === undefined) {
if (!isDelayedEvent) {
sendEventPromise = this.driver.sendEvent(
request.data.type,
request.data.content || {},
request.data.state_key,
request.data.room_id,
);
} else {
sendEventPromise = this.driver.sendFuture(
request.data.future_timeout ?? null,
request.data.future_group_id ?? null,
sendEventPromise = this.driver.sendDelayedEvent(
request.data.delay ?? null,
request.data.parent_delay_id ?? null,
request.data.type,
request.data.content || {},
request.data.state_key,
Expand All @@ -511,17 +522,17 @@ export class ClientWidgetApi extends EventEmitter {
});
}

if (request.data.future_timeout === undefined && request.data.future_group_id === undefined) {
if (!isDelayedEvent) {
sendEventPromise = this.driver.sendEvent(
request.data.type,
content,
null, // not sending a state event
request.data.room_id,
);
} else {
sendEventPromise = this.driver.sendFuture(
request.data.future_timeout ?? null,
request.data.future_group_id ?? null,
sendEventPromise = this.driver.sendDelayedEvent(
request.data.delay ?? null,
request.data.parent_delay_id ?? null,
request.data.type,
content,
null, // not sending a state event
Expand All @@ -536,10 +547,7 @@ export class ClientWidgetApi extends EventEmitter {
...("eventId" in sentEvent ? {
event_id: sentEvent.eventId,
} : {
future_group_id: sentEvent.futureGroupId,
send_token: sentEvent.sendToken,
cancel_token: sentEvent.cancelToken,
...("refreshToken" in sentEvent && { refresh_token: sentEvent.refreshToken }),
delay_id: sentEvent.delayId,
}),
});
}).catch(e => {
Expand All @@ -550,6 +558,39 @@ export class ClientWidgetApi extends EventEmitter {
});
}

private handleUpdateDelayedEvent(request: IUpdateDelayedEventFromWidgetActionRequest) {
if (!request.data.delay_id) {
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Invalid request - missing delay_id"},
});
}

if (!this.hasCapability(MatrixCapabilities.MSC4157UpdateDelayedEvent)) {
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Missing capability"},
});
}

switch (request.data.action) {
case UpdateDelayedEventAction.Cancel:
case UpdateDelayedEventAction.Restart:
case UpdateDelayedEventAction.Send:
this.driver.updateDelayedEvent(request.data.delay_id, request.data.action).then(() => {
return this.transport.reply<IWidgetApiAcknowledgeResponseData>(request, {});
}).catch(e => {
console.error("error updating delayed event: ", e);
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Error updating delayed event"},
});
});
break;
default:
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Invalid request - unsupported action"},
});
}
}

private async handleSendToDevice(request: ISendToDeviceFromWidgetActionRequest): Promise<void> {
if (!request.data.type) {
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
Expand Down Expand Up @@ -822,6 +863,8 @@ export class ClientWidgetApi extends EventEmitter {
return this.handleGetMediaConfig(<IGetMediaConfigActionFromWidgetActionRequest>ev.detail);
case WidgetApiFromWidgetAction.MSC4039UploadFileAction:
return this.handleUploadFile(<IUploadFileActionFromWidgetActionRequest>ev.detail);
case WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent:
return this.handleUpdateDelayedEvent(<IUpdateDelayedEventFromWidgetActionRequest>ev.detail);

default:
return this.transport.reply(ev.detail, <IWidgetApiErrorResponseData>{
Expand Down
41 changes: 31 additions & 10 deletions src/WidgetApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ import {
IUploadFileActionFromWidgetRequestData,
IUploadFileActionFromWidgetResponseData,
} from "./interfaces/UploadFileAction";
import {
IUpdateDelayedEventFromWidgetRequestData,
IUpdateDelayedEventFromWidgetResponseData,
UpdateDelayedEventAction,
} from "./interfaces/UpdateDelayedEventAction";

/**
* API handler for widgets. This raises events for each action
Expand Down Expand Up @@ -400,30 +405,30 @@ export class WidgetApi extends EventEmitter {
eventType: string,
content: unknown,
roomId?: string,
futureTimeout?: number,
futureGroupId?: string,
delay?: number,
parentDelayId?: string,
): Promise<ISendEventFromWidgetResponseData> {
return this.sendEvent(eventType, undefined, content, roomId, futureTimeout, futureGroupId);
return this.sendEvent(eventType, undefined, content, roomId, delay, parentDelayId);
}

public sendStateEvent(
eventType: string,
stateKey: string,
content: unknown,
roomId?: string,
futureTimeout?: number,
futureGroupId?: string,
delay?: number,
parentDelayId?: string,
): Promise<ISendEventFromWidgetResponseData> {
return this.sendEvent(eventType, stateKey, content, roomId, futureTimeout, futureGroupId);
return this.sendEvent(eventType, stateKey, content, roomId, delay, parentDelayId);
}

private sendEvent(
eventType: string,
stateKey: string | undefined,
content: unknown,
roomId?: string,
futureTimeout?: number,
futureGroupId?: string,
delay?: number,
parentDelayId?: string,
): Promise<ISendEventFromWidgetResponseData> {
return this.transport.send<ISendEventFromWidgetRequestData, ISendEventFromWidgetResponseData>(
WidgetApiFromWidgetAction.SendEvent,
Expand All @@ -432,8 +437,24 @@ export class WidgetApi extends EventEmitter {
content,
...(stateKey !== undefined && { state_key: stateKey }),
...(roomId !== undefined && { room_id: roomId }),
...(futureTimeout !== undefined && { future_timeout: futureTimeout }),
...(futureGroupId !== undefined && { future_group_id: futureGroupId }),
...(delay !== undefined && { delay }),
...(parentDelayId !== undefined && { parent_delay_id: parentDelayId }),
},
);
}

/**
* @deprecated This currently relies on an unstable MSC (MSC4157).
*/
public updateDelayedEvent(
delayId: string,
action: UpdateDelayedEventAction,
): Promise<IUpdateDelayedEventFromWidgetResponseData> {
return this.transport.send<IUpdateDelayedEventFromWidgetRequestData, IUpdateDelayedEventFromWidgetResponseData>(
WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent,
{
delay_id: delayId,
action,
},
);
}
Expand Down
56 changes: 33 additions & 23 deletions src/driver/WidgetDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,17 @@ import {
IRoomEvent,
IRoomAccountData,
ITurnServer,
UpdateDelayedEventAction,
} from "..";

export interface ISendEventDetails {
roomId: string;
eventId: string;
}

export interface ISendFutureDetails {
export interface ISendDelayedEventDetails {
roomId: string;
futureGroupId: string;
sendToken: string;
cancelToken: string;
refreshToken?: string;
delayId: string;
}

export interface IOpenIDUpdate {
Expand Down Expand Up @@ -113,31 +111,43 @@ export abstract class WidgetDriver {

/**
* @experimental Part of MSC4140 & MSC4157
* Sends a future into a room. If `roomId` is falsy, the client should send the future
* Sends a delayed event into a room. If `roomId` is falsy, the client should send it
* into the room the user is currently looking at. The widget API will have already
* verified that the widget is capable of sending the future's event to that room.
* @param {number|null} futureTimeout The future's timeout, or null for an action future.
* May not be null if {@link futureGroupId} is null.
* @param {string|null} futureGroupId The ID of the group the future belongs to,
* or null if it will be put in a new group. May not be null if {@link futureTimeout} is null.
* @param {string} eventType The event type of the event to be sent by the future.
* @param {*} content The content for the event to be sent by the future.
* @param {string|null} stateKey The state key if the event to be sent by the future is
* a state event, otherwise null. May be an empty string.
* @param {string|null} roomId The room ID to send the future to. If falsy, the room the
* verified that the widget is capable of sending the event to that room.
* @param {number|null} delay How much later to send the event, or null to not send the
* event automatically. May not be null if {@link parentDelayId} is null.
* @param {string|null} parentDelayId The ID of the delayed event this one is grouped with,
* or null if it will be put in a new group. May not be null if {@link delay} is null.
* @param {string} eventType The event type of the event to be sent.
* @param {*} content The content for the event to be sent.
* @param {string|null} stateKey The state key if the event to be sent a state event,
* otherwise null. May be an empty string.
* @param {string|null} roomId The room ID to send the event to. If falsy, the room the
* user is currently looking at.
* @returns {Promise<ISendFutureDetails>} Resolves when the future has been sent with
* details of that future.
* @throws Rejected when the future could not be sent.
* @returns {Promise<ISendDelayedEventDetails>} Resolves when the delayed event has been
* prepared with details of how to refer to it for updating/sending/canceling it later.
* @throws Rejected when the delayed event could not be sent.
*/
public sendFuture(
futureTimeout: number | null,
futureGroupId: string | null,
public sendDelayedEvent(
delay: number | null,
parentDelayId: string | null,
eventType: string,
content: unknown,
stateKey: string | null = null,
roomId: string | null = null,
): Promise<ISendFutureDetails> {
): Promise<ISendDelayedEventDetails> {
return Promise.reject(new Error("Failed to override function"));
}

/**
* @experimental Part of MSC4140 & MSC4157
* Run the specified {@link action} for the delayed event matching the provided {@link delayId}.
* @throws Rejected when there is no matching delayed event, or when the action failed to run.
*/
public updateDelayedEvent(
delayId: string,
action: UpdateDelayedEventAction,
): Promise<void> {
return Promise.reject(new Error("Failed to override function"));
}

Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export * from "./interfaces/NavigateAction";
export * from "./interfaces/TurnServerActions";
export * from "./interfaces/ReadRelationsAction";
export * from "./interfaces/GetMediaConfigAction";
export * from "./interfaces/UpdateDelayedEventAction";
export * from "./interfaces/UploadFileAction";

// Complex models
Expand Down
8 changes: 8 additions & 0 deletions src/interfaces/Capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ export enum MatrixCapabilities {
* @deprecated It is not recommended to rely on this existing - it can be removed without notice.
*/
MSC4039UploadFile = "org.matrix.msc4039.upload_file",
/**
* @deprecated It is not recommended to rely on this existing - it can be removed without notice.
*/
MSC4157SendDelayedEvent = "org.matrix.msc4157.send.delayed_event",
/**
* @deprecated It is not recommended to rely on this existing - it can be removed without notice.
*/
MSC4157UpdateDelayedEvent = "org.matrix.msc4157.update.delayed_event",
}

export type Capability = MatrixCapabilities | string;
Expand Down
13 changes: 5 additions & 8 deletions src/interfaces/SendEventAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ export interface ISendEventFromWidgetRequestData extends IWidgetApiRequestData {
content: unknown;
room_id?: string; // eslint-disable-line camelcase

// MSC4157: Futures
future_timeout?: number; // eslint-disable-line camelcase
future_group_id?: string; // eslint-disable-line camelcase
// MSC4157
delay?: number; // eslint-disable-line camelcase
parent_delay_id?: string; // eslint-disable-line camelcase
}

export interface ISendEventFromWidgetActionRequest extends IWidgetApiRequest {
Expand All @@ -39,11 +39,8 @@ export interface ISendEventFromWidgetResponseData extends IWidgetApiResponseData
room_id: string; // eslint-disable-line camelcase
event_id?: string; // eslint-disable-line camelcase

// MSC4157: Futures
future_group_id?: string; // eslint-disable-line camelcase
send_token?: string; // eslint-disable-line camelcase
cancel_token?: string; // eslint-disable-line camelcase
refresh_token?: string; // eslint-disable-line camelcase
// MSC4157
delay_id?: string; // eslint-disable-line camelcase
}

export interface ISendEventFromWidgetActionResponse extends ISendEventFromWidgetActionRequest {
Expand Down
43 changes: 43 additions & 0 deletions src/interfaces/UpdateDelayedEventAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2020 - 2024 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 { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest";
import { WidgetApiFromWidgetAction } from "./WidgetApiAction";
import { IWidgetApiResponseData } from "./IWidgetApiResponse";

export enum UpdateDelayedEventAction {
Cancel = "cancel",
Restart = "restart",
Send = "send",
}

export interface IUpdateDelayedEventFromWidgetRequestData extends IWidgetApiRequestData {
delay_id: string; // eslint-disable-line camelcase
action: UpdateDelayedEventAction;
}

export interface IUpdateDelayedEventFromWidgetActionRequest extends IWidgetApiRequest {
action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent;
data: IUpdateDelayedEventFromWidgetRequestData;
}

export interface IUpdateDelayedEventFromWidgetResponseData extends IWidgetApiResponseData {
// nothing
}

export interface IUpdateDelayedEventFromWidgetActionResponse extends IUpdateDelayedEventFromWidgetActionRequest {
response: IUpdateDelayedEventFromWidgetResponseData;
}
Loading

0 comments on commit eb57b5e

Please sign in to comment.