Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Satisfy revised MSC4157 #92

Merged
merged 12 commits into from
Jul 30, 2024
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 { 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 @@
IGetMediaConfigActionFromWidgetActionRequest,
IGetMediaConfigActionFromWidgetResponseData,
} from "./interfaces/GetMediaConfigAction";
import {
IUpdateDelayedEventFromWidgetActionRequest,
UpdateDelayedEventAction,
} from "./interfaces/UpdateDelayedEventAction";
import {
IUploadFileActionFromWidgetActionRequest,
IUploadFileActionFromWidgetResponseData,
Expand Down Expand Up @@ -322,7 +326,7 @@
});
}

const onErr = (e: any) => {

Check warning on line 329 in src/ClientWidgetApi.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected any. Specify a different type
console.error("[ClientWidgetApi] Failed to handle navigation: ", e);
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Error handling navigation"},
Expand Down Expand Up @@ -429,7 +433,7 @@
if (request.data.room_ids) {
askRoomIds = request.data.room_ids as string[];
if (!Array.isArray(askRoomIds)) {
askRoomIds = [askRoomIds as any as string];

Check warning on line 436 in src/ClientWidgetApi.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected any. Specify a different type
}
for (const roomId of askRoomIds) {
if (!this.canUseRoomTimeline(roomId)) {
Expand Down Expand Up @@ -477,25 +481,32 @@
});
}

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 @@
});
}

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,
AndrewFerr marked this conversation as resolved.
Show resolved Hide resolved
request.data.type,
content,
null, // not sending a state event
Expand All @@ -536,10 +547,7 @@
...("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 @@
});
}

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 @@
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).
AndrewFerr marked this conversation as resolved.
Show resolved Hide resolved
*/
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.
AndrewFerr marked this conversation as resolved.
Show resolved Hide resolved
*/
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.
AndrewFerr marked this conversation as resolved.
Show resolved Hide resolved
*/
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
Loading