Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -178,6 +182,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 @@ -361,6 +385,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 = 25,
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