Skip to content

Commit 31c4435

Browse files
authored
Merge pull request #9 from matrix-org/travis/msc-send-widget-events
Add an implementation of MSC2762: send/receive events
2 parents 02640ed + b978b53 commit 31c4435

File tree

9 files changed

+358
-2
lines changed

9 files changed

+358
-2
lines changed

src/ClientWidgetApi.ts

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { IContentLoadedActionRequest } from "./interfaces/ContentLoadedAction";
2424
import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "./interfaces/WidgetApiAction";
2525
import { IWidgetApiErrorResponseData } from "./interfaces/IWidgetApiErrorResponse";
2626
import { Capability } from "./interfaces/Capabilities";
27-
import { WidgetDriver } from "./driver/WidgetDriver";
27+
import { ISendEventDetails, WidgetDriver } from "./driver/WidgetDriver";
2828
import { ICapabilitiesActionResponseData } from "./interfaces/CapabilitiesAction";
2929
import {
3030
ISupportedVersionsActionRequest,
@@ -40,6 +40,13 @@ import {
4040
IModalWidgetOpenRequestDataButton,
4141
IModalWidgetReturnData,
4242
} from "./interfaces/ModalWidgetActions";
43+
import {
44+
ISendEventFromWidgetActionRequest,
45+
ISendEventFromWidgetResponseData,
46+
ISendEventToWidgetRequestData,
47+
} from "./interfaces/SendEventAction";
48+
import { EventDirection, WidgetEventCapability } from "./models/WidgetEventCapability";
49+
import { IRoomEvent } from "./interfaces/IRoomEvent";
4350

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

7178
private capabilitiesFinished = false;
7279
private allowedCapabilities = new Set<Capability>();
80+
private allowedEvents: WidgetEventCapability[] = [];
7381
private isStopped = false;
7482

7583
/**
@@ -115,6 +123,26 @@ export class ClientWidgetApi extends EventEmitter {
115123
return this.allowedCapabilities.has(capability);
116124
}
117125

126+
public canSendRoomEvent(eventType: string, msgtype: string = null): boolean {
127+
return this.allowedEvents.some(e =>
128+
e.matchesAsRoomEvent(eventType, msgtype) && e.direction === EventDirection.Send);
129+
}
130+
131+
public canSendStateEvent(eventType: string, stateKey: string): boolean {
132+
return this.allowedEvents.some(e =>
133+
e.matchesAsStateEvent(eventType, stateKey) && e.direction === EventDirection.Send);
134+
}
135+
136+
public canReceiveRoomEvent(eventType: string, msgtype: string = null): boolean {
137+
return this.allowedEvents.some(e =>
138+
e.matchesAsRoomEvent(eventType, msgtype) && e.direction === EventDirection.Receive);
139+
}
140+
141+
public canReceiveStateEvent(eventType: string, stateKey: string): boolean {
142+
return this.allowedEvents.some(e =>
143+
e.matchesAsStateEvent(eventType, stateKey) && e.direction === EventDirection.Receive);
144+
}
145+
118146
public stop() {
119147
this.isStopped = true;
120148
this.transport.stop();
@@ -140,7 +168,9 @@ export class ClientWidgetApi extends EventEmitter {
140168
).then(caps => {
141169
return this.driver.validateCapabilities(new Set(caps.capabilities));
142170
}).then(allowedCaps => {
171+
console.log(`Widget ${this.widget.id} is allowed capabilities:`, Array.from(allowedCaps));
143172
this.allowedCapabilities = allowedCaps;
173+
this.allowedEvents = WidgetEventCapability.findEventCapabilities(allowedCaps);
144174
this.capabilitiesFinished = true;
145175
this.emit("ready");
146176
});
@@ -165,6 +195,63 @@ export class ClientWidgetApi extends EventEmitter {
165195
});
166196
}
167197

198+
private async handleSendEvent(request: ISendEventFromWidgetActionRequest) {
199+
if (!request.data.type) {
200+
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
201+
error: {message: "Invalid request - missing event type"},
202+
});
203+
}
204+
205+
const isState = request.data.state_key !== null && request.data.state_key !== undefined;
206+
let sentEvent: ISendEventDetails;
207+
if (isState) {
208+
if (!this.canSendStateEvent(request.data.type, request.data.state_key)) {
209+
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
210+
error: {message: "Cannot send state events of this type"},
211+
});
212+
}
213+
214+
try {
215+
sentEvent = await this.driver.sendEvent(
216+
request.data.type,
217+
request.data.content || {},
218+
request.data.state_key,
219+
);
220+
} catch (e) {
221+
console.error("error sending event: ", e);
222+
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
223+
error: {message: "Error sending event"},
224+
});
225+
}
226+
} else {
227+
const content = request.data.content || {};
228+
const msgtype = content['msgtype'];
229+
if (!this.canSendRoomEvent(request.data.type, msgtype)) {
230+
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
231+
error: {message: "Cannot send room events of this type"},
232+
});
233+
}
234+
235+
try {
236+
sentEvent = await this.driver.sendEvent(
237+
request.data.type,
238+
content,
239+
null, // not sending a state event
240+
);
241+
} catch (e) {
242+
console.error("error sending event: ", e);
243+
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
244+
error: {message: "Error sending event"},
245+
});
246+
}
247+
}
248+
249+
return this.transport.reply<ISendEventFromWidgetResponseData>(request, {
250+
room_id: sentEvent.roomId,
251+
event_id: sentEvent.eventId,
252+
});
253+
}
254+
168255
private handleMessage(ev: CustomEvent<IWidgetApiRequest>) {
169256
if (this.isStopped) return;
170257
const actionEv = new CustomEvent(`action:${ev.detail.action}`, {
@@ -178,6 +265,8 @@ export class ClientWidgetApi extends EventEmitter {
178265
return this.handleContentLoadedAction(<IContentLoadedActionRequest>ev.detail);
179266
case WidgetApiFromWidgetAction.SupportedApiVersions:
180267
return this.replyVersions(<ISupportedVersionsActionRequest>ev.detail);
268+
case WidgetApiFromWidgetAction.SendEvent:
269+
return this.handleSendEvent(<ISendEventFromWidgetActionRequest>ev.detail);
181270
default:
182271
return this.transport.reply(ev.detail, <IWidgetApiErrorResponseData>{
183272
error: {
@@ -223,4 +312,31 @@ export class ClientWidgetApi extends EventEmitter {
223312
WidgetApiToWidgetAction.CloseModalWidget, data,
224313
).then();
225314
}
315+
316+
/**
317+
* Feeds an event to the widget. If the widget is not able to accept the event due to
318+
* permissions, this will no-op and return calmly. If the widget failed to handle the
319+
* event, this will raise an error.
320+
* @param {IRoomEvent} rawEvent The event to (try to) send to the widget.
321+
* @returns {Promise<void>} Resolves when complete, rejects if there was an error sending.
322+
*/
323+
public feedEvent(rawEvent: IRoomEvent): Promise<void> {
324+
if (rawEvent.state_key !== undefined && rawEvent.state_key !== null) {
325+
// state event
326+
if (!this.canReceiveStateEvent(rawEvent.type, rawEvent.state_key)) {
327+
return Promise.resolve(); // no-op
328+
}
329+
} else {
330+
// message event
331+
if (!this.canReceiveRoomEvent(rawEvent.type, (rawEvent.content || {})['msgtype'])) {
332+
return Promise.resolve(); // no-op
333+
}
334+
}
335+
336+
// Feed the event into the widget
337+
return this.transport.send<ISendEventToWidgetRequestData>(
338+
WidgetApiToWidgetAction.SendEvent,
339+
rawEvent as ISendEventToWidgetRequestData, // it's compatible, but missing the index signature
340+
).then();
341+
}
226342
}

src/WidgetApi.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
IModalWidgetOpenRequestDataButton,
4545
IModalWidgetReturnData,
4646
} from "./interfaces/ModalWidgetActions";
47+
import { ISendEventFromWidgetRequestData, ISendEventFromWidgetResponseData } from "./interfaces/SendEventAction";
4748

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

207+
public sendRoomEvent(eventType: string, content: unknown): Promise<ISendEventFromWidgetResponseData> {
208+
return this.transport.send<ISendEventFromWidgetRequestData, ISendEventFromWidgetResponseData>(
209+
WidgetApiFromWidgetAction.SendEvent,
210+
{type: eventType, content},
211+
);
212+
}
213+
214+
public sendStateEvent(
215+
eventType: string,
216+
stateKey: string,
217+
content: unknown,
218+
): Promise<ISendEventFromWidgetResponseData> {
219+
return this.transport.send<ISendEventFromWidgetRequestData, ISendEventFromWidgetResponseData>(
220+
WidgetApiFromWidgetAction.SendEvent,
221+
{type: eventType, content, state_key: stateKey},
222+
);
223+
}
224+
206225
/**
207226
* Starts the communication channel. This should be done early to ensure
208227
* that messages are not missed. Communication can only be stopped by the client.

src/driver/WidgetDriver.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616

1717
import { Capability } from "..";
1818

19+
export interface ISendEventDetails {
20+
roomId: string;
21+
eventId: string;
22+
}
23+
1924
/**
2025
* Represents the functions and behaviour the widget-api is unable to
2126
* do, such as prompting the user for information or interacting with
@@ -41,4 +46,19 @@ export abstract class WidgetDriver {
4146
public validateCapabilities(requested: Set<Capability>): Promise<Set<Capability>> {
4247
return Promise.resolve(new Set());
4348
}
49+
50+
/**
51+
* Sends an event into the room the user is currently looking at. The widget API
52+
* will have already verified that the widget is capable of sending the event.
53+
* @param {string} eventType The event type to be sent.
54+
* @param {*} content The content for the event.
55+
* @param {string|null} stateKey The state key if this is a state event, otherwise null.
56+
* May be an empty string.
57+
* @returns {Promise<ISendEventDetails>} Resolves when the event has been sent with
58+
* details of that event.
59+
* @throws Rejected when the event could not be sent.
60+
*/
61+
public sendEvent(eventType: string, content: unknown, stateKey: string = null): Promise<ISendEventDetails> {
62+
return Promise.reject(new Error("Failed to override function"));
63+
}
4464
}

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,11 @@ export * from "./interfaces/WidgetKind";
4848
export * from "./interfaces/ModalButtonKind";
4949
export * from "./interfaces/ModalWidgetActions";
5050
export * from "./interfaces/WidgetConfigAction";
51+
export * from "./interfaces/SendEventAction";
52+
export * from "./interfaces/IRoomEvent";
5153

5254
// Complex models
55+
export * from "./models/WidgetEventCapability";
5356
export * from "./models/validation/url";
5457
export * from "./models/validation/utils";
5558
export * from "./models/Widget";

src/interfaces/ApiVersion.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,15 @@ export enum MatrixApiVersion {
2020
V010 = "0.1.0", // first release
2121
}
2222

23-
export type ApiVersion = MatrixApiVersion | string;
23+
export enum UnstableApiVersion {
24+
MSC2762 = "org.matrix.msc2762",
25+
}
26+
27+
export type ApiVersion = MatrixApiVersion | UnstableApiVersion | string;
2428

2529
export const CurrentApiVersions: ApiVersion[] = [
2630
MatrixApiVersion.Prerelease1,
2731
MatrixApiVersion.Prerelease2,
2832
MatrixApiVersion.V010,
33+
UnstableApiVersion.MSC2762,
2934
];

src/interfaces/IRoomEvent.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2020 The Matrix.org Foundation C.I.C.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
export interface IRoomEvent {
18+
type: string;
19+
sender: string;
20+
event_id: string; // eslint-disable-line camelcase
21+
state_key?: string; // eslint-disable-line camelcase
22+
origin_server_ts: number; // eslint-disable-line camelcase
23+
content: unknown;
24+
unsigned: unknown;
25+
}

src/interfaces/SendEventAction.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2020 The Matrix.org Foundation C.I.C.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest";
18+
import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "./WidgetApiAction";
19+
import { IWidgetApiResponseData } from "./IWidgetApiResponse";
20+
import { IRoomEvent } from "./IRoomEvent";
21+
22+
export interface ISendEventFromWidgetRequestData extends IWidgetApiRequestData {
23+
state_key?: string; // eslint-disable-line camelcase
24+
type: string;
25+
content: unknown;
26+
}
27+
28+
export interface ISendEventFromWidgetActionRequest extends IWidgetApiRequest {
29+
action: WidgetApiFromWidgetAction.SendEvent;
30+
data: ISendEventFromWidgetRequestData;
31+
}
32+
33+
export interface ISendEventFromWidgetResponseData extends IWidgetApiResponseData {
34+
room_id: string; // eslint-disable-line camelcase
35+
event_id: string; // eslint-disable-line camelcase
36+
}
37+
38+
export interface ISendEventFromWidgetActionResponse extends ISendEventFromWidgetActionRequest {
39+
response: ISendEventFromWidgetResponseData;
40+
}
41+
42+
export interface ISendEventToWidgetRequestData extends IWidgetApiRequestData, IRoomEvent {
43+
}
44+
45+
export interface ISendEventToWidgetActionRequest extends IWidgetApiRequest {
46+
action: WidgetApiToWidgetAction.SendEvent;
47+
data: ISendEventToWidgetRequestData;
48+
}
49+
50+
export interface ISendEventToWidgetResponseData extends IWidgetApiResponseData {
51+
// nothing
52+
}
53+
54+
export interface ISendEventToWidgetActionResponse extends ISendEventToWidgetActionRequest {
55+
response: ISendEventToWidgetResponseData;
56+
}

src/interfaces/WidgetApiAction.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export enum WidgetApiToWidgetAction {
2323
WidgetConfig = "widget_config",
2424
CloseModalWidget = "close_modal",
2525
ButtonClicked = "button_clicked",
26+
SendEvent = "send_event",
2627
}
2728

2829
export enum WidgetApiFromWidgetAction {
@@ -33,6 +34,7 @@ export enum WidgetApiFromWidgetAction {
3334
GetOpenIDCredentials = "get_openid",
3435
CloseModalWidget = "close_modal",
3536
OpenModalWidget = "open_modal",
37+
SendEvent = "send_event",
3638
}
3739

3840
export type WidgetApiAction = WidgetApiToWidgetAction | WidgetApiFromWidgetAction | string;

0 commit comments

Comments
 (0)