diff --git a/src/ClientWidgetApi.ts b/src/ClientWidgetApi.ts index 499c72b..b657c69 100644 --- a/src/ClientWidgetApi.ts +++ b/src/ClientWidgetApi.ts @@ -60,6 +60,7 @@ import { import { SimpleObservable } from "./util/SimpleObservable"; import { IOpenIDCredentialsActionRequestData } from "./interfaces/OpenIDCredentialsAction"; import { INavigateActionRequest } from "./interfaces/NavigateAction"; +import { IReadEventFromWidgetActionRequest, IReadEventFromWidgetResponseData } from "./interfaces/ReadEventAction"; /** * API handler for the client side of widgets. This raises events @@ -156,6 +157,16 @@ export class ClientWidgetApi extends EventEmitter { e.matchesAsStateEvent(eventType, stateKey) && e.direction === EventDirection.Receive); } + public canReadRoomEvent(eventType: string, msgtype: string = null): boolean { + return this.allowedEvents.some(e => + e.matchesAsRoomEvent(eventType, msgtype) && e.direction === EventDirection.Read); + } + + public canReadStateEvent(eventType: string, stateKey: string): boolean { + return this.allowedEvents.some(e => + e.matchesAsStateEvent(eventType, stateKey) && e.direction === EventDirection.Read); + } + public stop() { this.isStopped = true; this.transport.stop(); @@ -331,6 +342,41 @@ export class ClientWidgetApi extends EventEmitter { this.driver.askOpenID(observer); } + private handleReadEvents(request: IReadEventFromWidgetActionRequest) { + if (!request.data.type) { + return this.transport.reply(request, { + error: {message: "Invalid request - missing event type"}, + }); + } + if (request.data.limit !== undefined && (!request.data.limit || request.data.limit < 0)) { + return this.transport.reply(request, { + error: {message: "Invalid request - limit out of range"}, + }); + } + + const limit = request.data.limit || 0; + + let events: Promise = Promise.resolve([]); + if (request.data.state_key !== undefined) { + const stateKey = request.data.state_key === true ? undefined : request.data.state_key.toString(); + if (!this.canReadStateEvent(request.data.type, stateKey)) { + return this.transport.reply(request, { + error: {message: "Cannot read state events of this type"}, + }); + } + events = this.driver.readStateEvents(request.data.type, stateKey, limit); + } else { + if (!this.canReadRoomEvent(request.data.type, request.data.msgtype)) { + return this.transport.reply(request, { + error: {message: "Cannot read room events of this type"}, + }); + } + events = this.driver.readRoomEvents(request.data.type, request.data.msgtype, limit); + } + + return events.then(evs => this.transport.reply(request, {events: evs})); + } + private handleSendEvent(request: ISendEventFromWidgetActionRequest) { if (!request.data.type) { return this.transport.reply(request, { @@ -402,6 +448,8 @@ export class ClientWidgetApi extends EventEmitter { return this.handleNavigate(ev.detail); case WidgetApiFromWidgetAction.MSC2974RenegotiateCapabilities: return this.handleCapabilitiesRenegotiate(ev.detail); + case WidgetApiFromWidgetAction.MSC2876ReadEvents: + return this.handleReadEvents(ev.detail); default: return this.transport.reply(ev.detail, { error: { diff --git a/src/WidgetApi.ts b/src/WidgetApi.ts index 160318d..dfe34b3 100644 --- a/src/WidgetApi.ts +++ b/src/WidgetApi.ts @@ -55,6 +55,7 @@ import { ISetModalButtonEnabledActionRequestData } from "./interfaces/SetModalBu import { ISendEventFromWidgetRequestData, ISendEventFromWidgetResponseData } from "./interfaces/SendEventAction"; import { EventDirection, WidgetEventCapability } from "./models/WidgetEventCapability"; import { INavigateActionRequestData } from "./interfaces/NavigateAction"; +import { IReadEventFromWidgetRequestData, IReadEventFromWidgetResponseData } from "./interfaces/ReadEventAction"; /** * API handler for widgets. This raises events for each action @@ -166,6 +167,18 @@ export class WidgetApi extends EventEmitter { this.requestCapability(WidgetEventCapability.forStateEvent(EventDirection.Receive, eventType, stateKey).raw); } + /** + * Requests the capability to read a given state event with optional explicit + * state key. It is not guaranteed to be allowed, but will be asked for if the + * negotiation has not already happened. + * @param {string} eventType The state event type to ask for. + * @param {string} stateKey If specified, the specific state key to request. + * Otherwise all state keys will be requested. + */ + public requestCapabilityToReadState(eventType: string, stateKey?: string) { + this.requestCapability(WidgetEventCapability.forStateEvent(EventDirection.Read, eventType, stateKey).raw); + } + /** * Requests the capability to send a given room event. It is not guaranteed to be * allowed, but will be asked for if the negotiation has not already happened. @@ -184,6 +197,15 @@ export class WidgetApi extends EventEmitter { this.requestCapability(WidgetEventCapability.forRoomEvent(EventDirection.Receive, eventType).raw); } + /** + * Requests the capability to read a given room event. It is not guaranteed to be allowed, + * but will be asked for if the negotiation has not already happened. + * @param {string} eventType The room event type to ask for. + */ + public requestCapabilityToReadEvent(eventType: string) { + this.requestCapability(WidgetEventCapability.forRoomEvent(EventDirection.Read, eventType).raw); + } + /** * Requests the capability to send a given message event with optional explicit * `msgtype`. It is not guaranteed to be allowed, but will be asked for if the @@ -206,6 +228,17 @@ export class WidgetApi extends EventEmitter { this.requestCapability(WidgetEventCapability.forRoomMessageEvent(EventDirection.Receive, msgtype).raw); } + /** + * Requests the capability to read a given message event with optional explicit + * `msgtype`. It is not guaranteed to be allowed, but will be asked for if the + * negotiation has not already happened. + * @param {string} msgtype If specified, the specific msgtype to request. + * Otherwise all message types will be requested. + */ + public requestCapabilityToReadMessage(msgtype?: string) { + this.requestCapability(WidgetEventCapability.forRoomMessageEvent(EventDirection.Read, msgtype).raw); + } + /** * Requests an OpenID Connect token from the client for the currently logged in * user. This token can be validated server-side with the federation API. Note @@ -344,6 +377,20 @@ export class WidgetApi extends EventEmitter { ); } + public readRoomEvents(eventType: string, limit = 25, msgtype?: string): Promise { + return this.transport.send( + WidgetApiFromWidgetAction.MSC2876ReadEvents, + {type: eventType, msgtype: msgtype, limit}, + ).then(r => r.events); + } + + public readStateEvents(eventType: string, limit = 25, stateKey?: string): Promise { + return this.transport.send( + WidgetApiFromWidgetAction.MSC2876ReadEvents, + {type: eventType, state_key: stateKey === undefined ? true : stateKey, limit}, + ).then(r => r.events); + } + /** * Sets a button as disabled or enabled on the modal widget. Buttons are enabled by default. * @param {ModalButtonID} buttonId The button ID to enable/disable. diff --git a/src/driver/WidgetDriver.ts b/src/driver/WidgetDriver.ts index fdbc93d..7cf2ce0 100644 --- a/src/driver/WidgetDriver.ts +++ b/src/driver/WidgetDriver.ts @@ -67,6 +67,36 @@ export abstract class WidgetDriver { return Promise.reject(new Error("Failed to override function")); } + /** + * Reads all events of the given type, and optionally `msgtype` (if applicable/defined), + * the user has access to. The widget API will have already verified that the widget is + * capable of receiving the events. Less events than the limit are allowed to be returned, + * but not more. + * @param eventType The event type to be read. + * @param msgtype The msgtype of the events to be read, if applicable/defined. + * @param limit The maximum number of events to retrieve. Will be zero to denote "as many + * as possible". + * @returns {Promise<*[]>} Resolves to the room events, or an empty array. + */ + public readRoomEvents(eventType: string, msgtype: string | undefined, limit: number): Promise { + return Promise.resolve([]); + } + + /** + * Reads all events of the given type, and optionally state key (if applicable/defined), + * the user has access to. The widget API will have already verified that the widget is + * capable of receiving the events. Less events than the limit are allowed to be returned, + * but not more. + * @param eventType The event type to be read. + * @param stateKey The state key of the events to be read, if applicable/defined. + * @param limit The maximum number of events to retrieve. Will be zero to denote "as many + * as possible". + * @returns {Promise<*[]>} Resolves to the state events, or an empty array. + */ + public readStateEvents(eventType: string, stateKey: string | undefined, limit: number): Promise { + return Promise.resolve([]); + } + /** * Asks the user for permission to validate their identity through OpenID Connect. The * interface for this function is an observable which accepts the state machine of the diff --git a/src/index.ts b/src/index.ts index 82f0d64..94f1d79 100644 --- a/src/index.ts +++ b/src/index.ts @@ -50,6 +50,7 @@ export * from "./interfaces/ModalWidgetActions"; export * from "./interfaces/SetModalButtonEnabledAction"; export * from "./interfaces/WidgetConfigAction"; export * from "./interfaces/SendEventAction"; +export * from "./interfaces/ReadEventAction"; export * from "./interfaces/IRoomEvent"; export * from "./interfaces/NavigateAction"; diff --git a/src/interfaces/ApiVersion.ts b/src/interfaces/ApiVersion.ts index e0e4c6a..697b1eb 100644 --- a/src/interfaces/ApiVersion.ts +++ b/src/interfaces/ApiVersion.ts @@ -25,6 +25,7 @@ export enum UnstableApiVersion { MSC2871 = "org.matrix.msc2871", MSC2931 = "org.matrix.msc2931", MSC2974 = "org.matrix.msc2974", + MSC2876 = "org.matrix.msc2876", } export type ApiVersion = MatrixApiVersion | UnstableApiVersion | string; @@ -37,4 +38,5 @@ export const CurrentApiVersions: ApiVersion[] = [ UnstableApiVersion.MSC2871, UnstableApiVersion.MSC2931, UnstableApiVersion.MSC2974, + UnstableApiVersion.MSC2876, ]; diff --git a/src/interfaces/ReadEventAction.ts b/src/interfaces/ReadEventAction.ts new file mode 100644 index 0000000..72b040a --- /dev/null +++ b/src/interfaces/ReadEventAction.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2021 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 interface IReadEventFromWidgetRequestData extends IWidgetApiRequestData { + state_key?: string | boolean; // eslint-disable-line camelcase + msgtype?: string; + type: string; + limit?: number; +} + +export interface IReadEventFromWidgetActionRequest extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC2876ReadEvents; + data: IReadEventFromWidgetRequestData; +} + +export interface IReadEventFromWidgetResponseData extends IWidgetApiResponseData { + events: unknown[]; +} + +export interface IReadEventFromWidgetActionResponse extends IReadEventFromWidgetActionRequest { + response: IReadEventFromWidgetResponseData; +} diff --git a/src/interfaces/WidgetApiAction.ts b/src/interfaces/WidgetApiAction.ts index 4977315..785e13b 100644 --- a/src/interfaces/WidgetApiAction.ts +++ b/src/interfaces/WidgetApiAction.ts @@ -38,6 +38,11 @@ export enum WidgetApiFromWidgetAction { SetModalButtonEnabled = "set_button_enabled", SendEvent = "send_event", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC2876ReadEvents = "org.matrix.msc2876.read_events", + /** * @deprecated It is not recommended to rely on this existing - it can be removed without notice. */ diff --git a/src/models/WidgetEventCapability.ts b/src/models/WidgetEventCapability.ts index 51e7e1d..64236be 100644 --- a/src/models/WidgetEventCapability.ts +++ b/src/models/WidgetEventCapability.ts @@ -19,6 +19,7 @@ import { Capability } from ".."; export enum EventDirection { Send = "send", Receive = "receive", + Read = "read", } export class WidgetEventCapability { @@ -123,6 +124,15 @@ export class WidgetEventCapability { isState = true; eventSegment = cap.substring("org.matrix.msc2762.receive.state_event:".length); } + } else if (cap.startsWith("org.matrix.msc2762.read.")) { + if (cap.startsWith("org.matrix.msc2762.read.event:")) { + direction = EventDirection.Read; + eventSegment = cap.substring("org.matrix.msc2762.read.event:".length); + } else if (cap.startsWith("org.matrix.msc2762.read.state_event:")) { + direction = EventDirection.Read; + isState = true; + eventSegment = cap.substring("org.matrix.msc2762.read.state_event:".length); + } } if (direction === null) continue;