Skip to content

Commit

Permalink
Add an implementation of MSC2762: send/receive events
Browse files Browse the repository at this point in the history
  • Loading branch information
turt2live committed Nov 3, 2020
1 parent 02640ed commit 06a06af
Show file tree
Hide file tree
Showing 9 changed files with 354 additions and 2 deletions.
118 changes: 117 additions & 1 deletion 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 } from "./interfaces/Capabilities";
import { WidgetDriver } from "./driver/WidgetDriver";
import { ISendEventDetails, WidgetDriver } from "./driver/WidgetDriver";
import { ICapabilitiesActionResponseData } from "./interfaces/CapabilitiesAction";
import {
ISupportedVersionsActionRequest,
Expand All @@ -40,6 +40,13 @@ import {
IModalWidgetOpenRequestDataButton,
IModalWidgetReturnData,
} from "./interfaces/ModalWidgetActions";
import {
ISendEventFromWidgetActionRequest,
ISendEventFromWidgetResponseData,
ISendEventToWidgetRequestData,
} from "./interfaces/SendEventAction";
import { EventDirection, WidgetEventCapability } from "./models/WidgetEventCapability";
import { IRoomEvent } from "./interfaces/IRoomEvent";

/**
* API handler for the client side of widgets. This raises events
Expand Down Expand Up @@ -70,6 +77,7 @@ export class ClientWidgetApi extends EventEmitter {

private capabilitiesFinished = false;
private allowedCapabilities = new Set<Capability>();
private allowedEvents: WidgetEventCapability[] = [];
private isStopped = false;

/**
Expand Down Expand Up @@ -115,6 +123,26 @@ export class ClientWidgetApi extends EventEmitter {
return this.allowedCapabilities.has(capability);
}

public canSendRoomEvent(eventType: string, msgtype: string = null): boolean {
return this.allowedEvents.some(e =>
e.matchesAsRoomEvent(eventType, msgtype) && e.direction === EventDirection.Send);
}

public canSendStateEvent(eventType: string, stateKey: string): boolean {
return this.allowedEvents.some(e =>
e.matchesAsStateEvent(eventType, stateKey) && e.direction === EventDirection.Send);
}

public canReceiveRoomEvent(eventType: string, msgtype: string = null): boolean {
return this.allowedEvents.some(e =>
e.matchesAsRoomEvent(eventType, msgtype) && e.direction === EventDirection.Receive);
}

public canReceiveStateEvent(eventType: string, stateKey: string): boolean {
return this.allowedEvents.some(e =>
e.matchesAsStateEvent(eventType, stateKey) && e.direction === EventDirection.Receive);
}

public stop() {
this.isStopped = true;
this.transport.stop();
Expand All @@ -140,7 +168,9 @@ export class ClientWidgetApi extends EventEmitter {
).then(caps => {
return this.driver.validateCapabilities(new Set(caps.capabilities));
}).then(allowedCaps => {
console.log(`Widget ${this.widget.id} is allowed capabilities:`, Array.from(allowedCaps));
this.allowedCapabilities = allowedCaps;
this.allowedEvents = Array.from(new Set(WidgetEventCapability.findEventCapabilities(allowedCaps)));
this.capabilitiesFinished = true;
this.emit("ready");
});
Expand All @@ -165,6 +195,63 @@ export class ClientWidgetApi extends EventEmitter {
});
}

private async handleSendEvent(request: ISendEventFromWidgetActionRequest) {
if (!request.data.type) {
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Invalid request - missing event type"},
});
}

const isState = request.data.state_key !== null && request.data.state_key !== undefined;
let sentEvent: ISendEventDetails;
if (isState) {
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"},
});
}

try {
sentEvent = await this.driver.sendEvent(
request.data.type,
request.data.content || {},
request.data.state_key,
);
} catch (e) {
console.error("error sending event: ", e);
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Error sending event"},
});
}
} else {
const content = request.data.content || {};
const msgtype = content['msgtype'];
if (!this.canSendRoomEvent(request.data.type, msgtype)) {
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Cannot send room events of this type"},
});
}

try {
sentEvent = await this.driver.sendEvent(
request.data.type,
content,
null, // not sending a state event
);
} catch (e) {
console.error("error sending event: ", e);
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Error sending event"},
});
}
}

return this.transport.reply<ISendEventFromWidgetResponseData>(request, {
room_id: sentEvent.roomId,
event_id: sentEvent.eventId,
});
}

private handleMessage(ev: CustomEvent<IWidgetApiRequest>) {
if (this.isStopped) return;
const actionEv = new CustomEvent(`action:${ev.detail.action}`, {
Expand All @@ -178,6 +265,8 @@ export class ClientWidgetApi extends EventEmitter {
return this.handleContentLoadedAction(<IContentLoadedActionRequest>ev.detail);
case WidgetApiFromWidgetAction.SupportedApiVersions:
return this.replyVersions(<ISupportedVersionsActionRequest>ev.detail);
case WidgetApiFromWidgetAction.SendEvent:
return this.handleSendEvent(<ISendEventFromWidgetActionRequest>ev.detail);
default:
return this.transport.reply(ev.detail, <IWidgetApiErrorResponseData>{
error: {
Expand Down Expand Up @@ -223,4 +312,31 @@ export class ClientWidgetApi extends EventEmitter {
WidgetApiToWidgetAction.CloseModalWidget, data,
).then();
}

/**
* Feeds an event to the widget. If the widget is not able to accept the event due to
* permissions, this will no-op and return calmly. If the widget failed to handle the
* event, this will raise an error.
* @param {IRoomEvent} rawEvent The event to (try to) send to the widget.
* @returns {Promise<void>} Resolves when complete, rejects if there was an error sending.
*/
public feedEvent(rawEvent: IRoomEvent): Promise<void> {
if (rawEvent.state_key !== undefined && rawEvent.state_key !== null) {
// state event
if (!this.canReceiveStateEvent(rawEvent.type, rawEvent.state_key)) {
return Promise.resolve(); // no-op
}
} else {
// message event
if (!this.canReceiveRoomEvent(rawEvent.type, (rawEvent.content || {})['msgtype'])) {
return Promise.resolve(); // no-op
}
}

// Feed the event into the widget
return this.transport.send<ISendEventToWidgetRequestData>(
WidgetApiToWidgetAction.SendEvent,
rawEvent as ISendEventToWidgetRequestData, // it's compatible, but missing the index signature
).then();
}
}
19 changes: 19 additions & 0 deletions src/WidgetApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
IModalWidgetOpenRequestDataButton,
IModalWidgetReturnData,
} from "./interfaces/ModalWidgetActions";
import { ISendEventFromWidgetRequestData, ISendEventFromWidgetResponseData } from "./interfaces/SendEventAction";

/**
* API handler for widgets. This raises events for each action
Expand Down Expand Up @@ -203,6 +204,24 @@ export class WidgetApi extends EventEmitter {
return this.transport.send<IModalWidgetReturnData>(WidgetApiFromWidgetAction.CloseModalWidget, data).then();
}

public sendRoomEvent(eventType: string, content: unknown): Promise<ISendEventFromWidgetResponseData> {
return this.transport.send<ISendEventFromWidgetRequestData, ISendEventFromWidgetResponseData>(
WidgetApiFromWidgetAction.SendEvent,
{type: eventType, content},
);
}

public sendStateEvent(
eventType: string,
stateKey: string,
content: unknown,
): Promise<ISendEventFromWidgetResponseData> {
return this.transport.send<ISendEventFromWidgetRequestData, ISendEventFromWidgetResponseData>(
WidgetApiFromWidgetAction.SendEvent,
{type: eventType, content, state_key: stateKey},
);
}

/**
* Starts the communication channel. This should be done early to ensure
* that messages are not missed. Communication can only be stopped by the client.
Expand Down
20 changes: 20 additions & 0 deletions src/driver/WidgetDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@

import { Capability } from "..";

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

/**
* Represents the functions and behaviour the widget-api is unable to
* do, such as prompting the user for information or interacting with
Expand All @@ -41,4 +46,19 @@ export abstract class WidgetDriver {
public validateCapabilities(requested: Set<Capability>): Promise<Set<Capability>> {
return Promise.resolve(new Set());
}

/**
* Sends an event into the room the user is currently looking at. The widget API
* will have already verified that the widget is capable of sending the event.
* @param {string} eventType The event type to be sent.
* @param {*} content The content for the event.
* @param {string|null} stateKey The state key if this is a state event, otherwise null.
* May be an empty string.
* @returns {Promise<ISendEventDetails>} Resolves when the event has been sent with
* details of that event.
* @throws Rejected when the event could not be sent.
*/
public sendEvent(eventType: string, content: unknown, stateKey: string = null): Promise<ISendEventDetails> {
return Promise.reject(new Error("Failed to override function"));
}
}
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,11 @@ export * from "./interfaces/WidgetKind";
export * from "./interfaces/ModalButtonKind";
export * from "./interfaces/ModalWidgetActions";
export * from "./interfaces/WidgetConfigAction";
export * from "./interfaces/SendEventAction";
export * from "./interfaces/IRoomEvent";

// Complex models
export * from "./models/WidgetEventCapability";
export * from "./models/validation/url";
export * from "./models/validation/utils";
export * from "./models/Widget";
Expand Down
7 changes: 6 additions & 1 deletion src/interfaces/ApiVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,15 @@ export enum MatrixApiVersion {
V010 = "0.1.0", // first release
}

export type ApiVersion = MatrixApiVersion | string;
export enum UnstableApiVersion {
MSC2762 = "org.matrix.msc2762",
}

export type ApiVersion = MatrixApiVersion | UnstableApiVersion | string;

export const CurrentApiVersions: ApiVersion[] = [
MatrixApiVersion.Prerelease1,
MatrixApiVersion.Prerelease2,
MatrixApiVersion.V010,
UnstableApiVersion.MSC2762,
];
25 changes: 25 additions & 0 deletions src/interfaces/IRoomEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2020 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.
*/

export interface IRoomEvent {
type: string;
sender: string;
event_id: string; // eslint-disable-line camelcase
state_key?: string; // eslint-disable-line camelcase
origin_server_ts: number; // eslint-disable-line camelcase
content: unknown;
unsigned: unknown;
}
56 changes: 56 additions & 0 deletions src/interfaces/SendEventAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2020 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, WidgetApiToWidgetAction } from "./WidgetApiAction";
import { IWidgetApiResponseData } from "./IWidgetApiResponse";
import { IRoomEvent } from "./IRoomEvent";

export interface ISendEventFromWidgetRequestData extends IWidgetApiRequestData {
state_key?: string; // eslint-disable-line camelcase
type: string;
content: unknown;
}

export interface ISendEventFromWidgetActionRequest extends IWidgetApiRequest {
action: WidgetApiFromWidgetAction.SendEvent;
data: ISendEventFromWidgetRequestData;
}

export interface ISendEventFromWidgetResponseData extends IWidgetApiResponseData {
room_id: string; // eslint-disable-line camelcase
event_id: string; // eslint-disable-line camelcase
}

export interface ISendEventFromWidgetActionResponse extends ISendEventFromWidgetActionRequest {
response: ISendEventFromWidgetResponseData;
}

export interface ISendEventToWidgetRequestData extends IWidgetApiRequestData, IRoomEvent {
}

export interface ISendEventToWidgetActionRequest extends IWidgetApiRequest {
action: WidgetApiToWidgetAction.SendEvent;
data: ISendEventToWidgetRequestData;
}

export interface ISendEventToWidgetResponseData extends IWidgetApiResponseData {
// nothing
}

export interface ISendEventToWidgetActionResponse extends ISendEventToWidgetActionRequest {
response: ISendEventToWidgetResponseData;
}
2 changes: 2 additions & 0 deletions src/interfaces/WidgetApiAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export enum WidgetApiToWidgetAction {
WidgetConfig = "widget_config",
CloseModalWidget = "close_modal",
ButtonClicked = "button_clicked",
SendEvent = "send_event",
}

export enum WidgetApiFromWidgetAction {
Expand All @@ -33,6 +34,7 @@ export enum WidgetApiFromWidgetAction {
GetOpenIDCredentials = "get_openid",
CloseModalWidget = "close_modal",
OpenModalWidget = "open_modal",
SendEvent = "send_event",
}

export type WidgetApiAction = WidgetApiToWidgetAction | WidgetApiFromWidgetAction | string;
Loading

0 comments on commit 06a06af

Please sign in to comment.