Skip to content

Commit

Permalink
Implement MSC3819: Allowing widgets to send/receive to-device messages (
Browse files Browse the repository at this point in the history
#57)

* Implement MSC3819: Allowing widgets to send/receive to-device messages

* Fix typo
  • Loading branch information
robintown authored Jul 13, 2022
1 parent b57902f commit f35e37a
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 44 deletions.
83 changes: 68 additions & 15 deletions src/ClientWidgetApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ import {
ISendEventFromWidgetResponseData,
ISendEventToWidgetRequestData,
} from "./interfaces/SendEventAction";
import {
ISendToDeviceFromWidgetActionRequest,
ISendToDeviceFromWidgetResponseData,
ISendToDeviceToWidgetRequestData,
} from "./interfaces/SendToDeviceAction";
import { EventDirection, WidgetEventCapability } from "./models/WidgetEventCapability";
import { IRoomEvent } from "./interfaces/IRoomEvent";
import {
Expand Down Expand Up @@ -143,23 +148,27 @@ export class ClientWidgetApi extends EventEmitter {
}

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

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

public canSendToDeviceEvent(eventType: string): boolean {
return this.allowedEvents.some(e => e.matchesAsToDeviceEvent(EventDirection.Send, eventType));
}

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

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

public canReceiveToDeviceEvent(eventType: string): boolean {
return this.allowedEvents.some(e => e.matchesAsToDeviceEvent(EventDirection.Receive, eventType));
}

public stop() {
Expand Down Expand Up @@ -453,6 +462,32 @@ export class ClientWidgetApi extends EventEmitter {
});
}

private async handleSendToDevice(request: ISendToDeviceFromWidgetActionRequest): Promise<void> {
if (!request.data.type) {
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Invalid request - missing event type"},
});
} else if (!request.data.messages) {
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Invalid request - missing event contents"},
});
} else if (!this.canSendToDeviceEvent(request.data.type)) {
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Cannot send to-device events of this type"},
});
} else {
try {
await this.driver.sendToDevice(request.data.type, request.data.messages);
await this.transport.reply<ISendToDeviceFromWidgetResponseData>(request, {});
} catch (e) {
console.error("error sending to-device event", e);
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Error sending event"},
});
}
}
}

private handleMessage(ev: CustomEvent<IWidgetApiRequest>) {
if (this.isStopped) return;
const actionEv = new CustomEvent(`action:${ev.detail.action}`, {
Expand All @@ -468,6 +503,8 @@ export class ClientWidgetApi extends EventEmitter {
return this.replyVersions(<ISupportedVersionsActionRequest>ev.detail);
case WidgetApiFromWidgetAction.SendEvent:
return this.handleSendEvent(<ISendEventFromWidgetActionRequest>ev.detail);
case WidgetApiFromWidgetAction.SendToDevice:
return this.handleSendToDevice(<ISendToDeviceFromWidgetActionRequest>ev.detail);
case WidgetApiFromWidgetAction.GetOpenIDCredentials:
return this.handleOIDC(<IGetOpenIDActionRequest>ev.detail);
case WidgetApiFromWidgetAction.MSC2931Navigate:
Expand Down Expand Up @@ -531,27 +568,43 @@ export class ClientWidgetApi extends EventEmitter {
* Not the room ID of the event.
* @returns {Promise<void>} Resolves when complete, rejects if there was an error sending.
*/
public feedEvent(rawEvent: IRoomEvent, currentViewedRoomId: string): Promise<void> {
public async feedEvent(rawEvent: IRoomEvent, currentViewedRoomId: string): Promise<void> {
if (rawEvent.room_id !== currentViewedRoomId && !this.canUseRoomTimeline(rawEvent.room_id)) {
return Promise.resolve(); // no-op
return; // no-op
}

if (rawEvent.state_key !== undefined && rawEvent.state_key !== null) {
// state event
if (!this.canReceiveStateEvent(rawEvent.type, rawEvent.state_key)) {
return Promise.resolve(); // no-op
return; // no-op
}
} else {
// message event
if (!this.canReceiveRoomEvent(rawEvent.type, (rawEvent.content || {})['msgtype'])) {
return Promise.resolve(); // no-op
if (!this.canReceiveRoomEvent(rawEvent.type, rawEvent.content?.["msgtype"])) {
return; // no-op
}
}

// Feed the event into the widget
return this.transport.send<ISendEventToWidgetRequestData>(
await this.transport.send<ISendEventToWidgetRequestData>(
WidgetApiToWidgetAction.SendEvent,
rawEvent as ISendEventToWidgetRequestData, // it's compatible, but missing the index signature
).then();
);
}

/**
* Feeds a to-device 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 async feedToDevice(rawEvent: IRoomEvent): Promise<void> {
if (this.canReceiveToDeviceEvent(rawEvent.type)) {
await this.transport.send<ISendToDeviceToWidgetRequestData>(
WidgetApiToWidgetAction.SendToDevice,
rawEvent as ISendToDeviceToWidgetRequestData, // it's compatible, but missing the index signature
);
}
}
}
34 changes: 34 additions & 0 deletions src/WidgetApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ import {
} from "./interfaces/ModalWidgetActions";
import { ISetModalButtonEnabledActionRequestData } from "./interfaces/SetModalButtonEnabledAction";
import { ISendEventFromWidgetRequestData, ISendEventFromWidgetResponseData } from "./interfaces/SendEventAction";
import {
ISendToDeviceFromWidgetRequestData,
ISendToDeviceFromWidgetResponseData,
} from "./interfaces/SendToDeviceAction";
import { EventDirection, WidgetEventCapability } from "./models/WidgetEventCapability";
import { INavigateActionRequestData } from "./interfaces/NavigateAction";
import { IReadEventFromWidgetRequestData, IReadEventFromWidgetResponseData } from "./interfaces/ReadEventAction";
Expand Down Expand Up @@ -179,6 +183,26 @@ export class WidgetApi extends EventEmitter {
this.requestCapability(WidgetEventCapability.forStateEvent(EventDirection.Receive, eventType, stateKey).raw);
}

/**
* Requests the capability to send a given to-device 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 requestCapabilityToSendToDevice(eventType: string) {
this.requestCapability(WidgetEventCapability.forToDeviceEvent(EventDirection.Send, eventType).raw);
}

/**
* Requests the capability to receive a given to-device 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 requestCapabilityToReceiveToDevice(eventType: string) {
this.requestCapability(WidgetEventCapability.forToDeviceEvent(EventDirection.Receive, eventType).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.
Expand Down Expand Up @@ -362,6 +386,16 @@ export class WidgetApi extends EventEmitter {
);
}

public sendToDevice(
eventType: string,
contentMap: { [userId: string]: { [deviceId: string]: unknown } },
): Promise<ISendToDeviceFromWidgetResponseData> {
return this.transport.send<ISendToDeviceFromWidgetRequestData, ISendToDeviceFromWidgetResponseData>(
WidgetApiFromWidgetAction.SendToDevice,
{type: eventType, messages: contentMap},
);
}

public readRoomEvents(
eventType: string,
limit?: number,
Expand Down
15 changes: 15 additions & 0 deletions src/driver/WidgetDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,21 @@ export abstract class WidgetDriver {
return Promise.reject(new Error("Failed to override function"));
}

/**
* Sends a to-device event. 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 {Object} contentMap A map from user ID and device ID to event content.
* @returns {Promise<void>} Resolves when the event has been sent.
* @throws Rejected when the event could not be sent.
*/
public sendToDevice(
eventType: string,
contentMap: { [userId: string]: { [deviceId: string]: unknown } },
): Promise<void> {
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
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export * from "./interfaces/ModalWidgetActions";
export * from "./interfaces/SetModalButtonEnabledAction";
export * from "./interfaces/WidgetConfigAction";
export * from "./interfaces/SendEventAction";
export * from "./interfaces/SendToDeviceAction";
export * from "./interfaces/ReadEventAction";
export * from "./interfaces/IRoomEvent";
export * from "./interfaces/NavigateAction";
Expand Down
54 changes: 54 additions & 0 deletions src/interfaces/SendToDeviceAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2022 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 ISendToDeviceFromWidgetRequestData extends IWidgetApiRequestData {
type: string,
messages: { [userId: string]: { [deviceId: string]: unknown } },
}

export interface ISendToDeviceFromWidgetActionRequest extends IWidgetApiRequest {
action: WidgetApiFromWidgetAction.SendToDevice;
data: ISendToDeviceFromWidgetRequestData;
}

export interface ISendToDeviceFromWidgetResponseData extends IWidgetApiResponseData {
// nothing
}

export interface ISendToDeviceFromWidgetActionResponse extends ISendToDeviceFromWidgetActionRequest {
response: ISendToDeviceFromWidgetResponseData;
}

export interface ISendToDeviceToWidgetRequestData extends IWidgetApiRequestData, IRoomEvent {
}

export interface ISendToDeviceToWidgetActionRequest extends IWidgetApiRequest {
action: WidgetApiToWidgetAction.SendToDevice;
data: ISendToDeviceToWidgetRequestData;
}

export interface ISendToDeviceToWidgetResponseData extends IWidgetApiResponseData {
// nothing
}

export interface ISendToDeviceToWidgetActionResponse extends ISendToDeviceToWidgetActionRequest {
response: ISendToDeviceToWidgetResponseData;
}
2 changes: 2 additions & 0 deletions src/interfaces/WidgetApiAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export enum WidgetApiToWidgetAction {
CloseModalWidget = "close_modal",
ButtonClicked = "button_clicked",
SendEvent = "send_event",
SendToDevice = "send_to_device",
}

export enum WidgetApiFromWidgetAction {
Expand All @@ -37,6 +38,7 @@ export enum WidgetApiFromWidgetAction {
OpenModalWidget = "open_modal",
SetModalButtonEnabled = "set_button_enabled",
SendEvent = "send_event",
SendToDevice = "send_to_device",

/**
* @deprecated It is not recommended to rely on this existing - it can be removed without notice.
Expand Down
Loading

0 comments on commit f35e37a

Please sign in to comment.