Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
43 changes: 39 additions & 4 deletions src/ClientWidgetApi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* Copyright 2020 - 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.
Expand Down Expand Up @@ -61,6 +61,7 @@ import { SimpleObservable } from "./util/SimpleObservable";
import { IOpenIDCredentialsActionRequestData } from "./interfaces/OpenIDCredentialsAction";
import { INavigateActionRequest } from "./interfaces/NavigateAction";
import { IReadEventFromWidgetActionRequest, IReadEventFromWidgetResponseData } from "./interfaces/ReadEventAction";
import { Symbols } from "./Symbols";

/**
* API handler for the client side of widgets. This raises events
Expand Down Expand Up @@ -137,6 +138,11 @@ export class ClientWidgetApi extends EventEmitter {
return this.allowedCapabilities.has(capability);
}

public canUseRoomTimeline(roomId: string | Symbols.AnyRoom): boolean {
return this.hasCapability(`org.matrix.msc2762.timeline:${Symbols.AnyRoom}`)
|| this.hasCapability(`org.matrix.msc2762.timeline:${roomId}`);
}

public canSendRoomEvent(eventType: string, msgtype: string = null): boolean {
return this.allowedEvents.some(e =>
e.matchesAsRoomEvent(eventType, msgtype) && e.direction === EventDirection.Send);
Expand Down Expand Up @@ -344,6 +350,21 @@ export class ClientWidgetApi extends EventEmitter {
});
}

let askRoomIds: string[] = null; // null denotes current room only
if (request.data.room_ids) {
askRoomIds = request.data.room_ids as string[];
if (!Array.isArray(askRoomIds)) {
askRoomIds = [askRoomIds as any as string];
}
for (const roomId of askRoomIds) {
if (!this.canUseRoomTimeline(roomId)) {
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: `Unable to access room timeline: ${roomId}`},
});
}
}
}

const limit = request.data.limit || 0;

let events: Promise<unknown[]> = Promise.resolve([]);
Expand All @@ -354,14 +375,14 @@ export class ClientWidgetApi extends EventEmitter {
error: {message: "Cannot read state events of this type"},
});
}
events = this.driver.readStateEvents(request.data.type, stateKey, limit);
events = this.driver.readStateEvents(request.data.type, stateKey, limit, askRoomIds);
} else {
if (!this.canReceiveRoomEvent(request.data.type, request.data.msgtype)) {
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Cannot read room events of this type"},
});
}
events = this.driver.readRoomEvents(request.data.type, request.data.msgtype, limit);
events = this.driver.readRoomEvents(request.data.type, request.data.msgtype, limit, askRoomIds);
}

return events.then(evs => this.transport.reply<IReadEventFromWidgetResponseData>(request, {events: evs}));
Expand All @@ -374,6 +395,12 @@ export class ClientWidgetApi extends EventEmitter {
});
}

if (!!request.data.room_id && !this.canUseRoomTimeline(request.data.room_id)) {
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: `Unable to access room timeline: ${request.data.room_id}`},
});
}

const isState = request.data.state_key !== null && request.data.state_key !== undefined;
let sendEventPromise: Promise<ISendEventDetails>;
if (isState) {
Expand All @@ -387,6 +414,7 @@ export class ClientWidgetApi extends EventEmitter {
request.data.type,
request.data.content || {},
request.data.state_key,
request.data.room_id,
);
} else {
const content = request.data.content || {};
Expand All @@ -401,6 +429,7 @@ export class ClientWidgetApi extends EventEmitter {
request.data.type,
content,
null, // not sending a state event
request.data.room_id,
);
}

Expand Down Expand Up @@ -491,9 +520,15 @@ export class ClientWidgetApi extends EventEmitter {
* 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.
* @param {string} currentViewedRoomId The room ID the user is currently interacting with.
* Not the room ID of the event.
* @returns {Promise<void>} Resolves when complete, rejects if there was an error sending.
*/
public feedEvent(rawEvent: IRoomEvent): Promise<void> {
public feedEvent(rawEvent: IRoomEvent, currentViewedRoomId: string): Promise<void> {
if (rawEvent.room_id !== currentViewedRoomId && !this.canUseRoomTimeline(rawEvent.room_id)) {
return Promise.resolve(); // no-op
}

if (rawEvent.state_key !== undefined && rawEvent.state_key !== null) {
// state event
if (!this.canReceiveStateEvent(rawEvent.type, rawEvent.state_key)) {
Expand Down
19 changes: 19 additions & 0 deletions src/Symbols.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* 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.
*/

export enum Symbols {
AnyRoom = "*",
}
62 changes: 54 additions & 8 deletions src/WidgetApi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* Copyright 2020 - 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.
Expand Down Expand Up @@ -56,6 +56,7 @@ import { ISendEventFromWidgetRequestData, ISendEventFromWidgetResponseData } fro
import { EventDirection, WidgetEventCapability } from "./models/WidgetEventCapability";
import { INavigateActionRequestData } from "./interfaces/NavigateAction";
import { IReadEventFromWidgetRequestData, IReadEventFromWidgetResponseData } from "./interfaces/ReadEventAction";
import { Symbols } from "./Symbols";

/**
* API handler for widgets. This raises events for each action
Expand Down Expand Up @@ -143,6 +144,16 @@ export class WidgetApi extends EventEmitter {
capabilities.forEach(cap => this.requestCapability(cap));
}

/**
* Requests the capability to interact with rooms other than the user's currently
* viewed room. Applies to event receiving and sending.
* @param {string | Symbols.AnyRoom} roomId The room ID, or `Symbols.AnyRoom` to
* denote all known rooms.
*/
public requestCapabilityForRoomTimeline(roomId: string | Symbols.AnyRoom) {
this.requestCapability(`org.matrix.msc2762.timeline:${roomId}`);
}

/**
* Requests the capability to send a given state event with optional explicit
* state key. It is not guaranteed to be allowed, but will be asked for if the
Expand Down Expand Up @@ -327,35 +338,70 @@ export class WidgetApi extends EventEmitter {
return this.transport.send<IModalWidgetReturnData>(WidgetApiFromWidgetAction.CloseModalWidget, data).then();
}

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

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

public readRoomEvents(eventType: string, limit = 25, msgtype?: string): Promise<unknown> {
public readRoomEvents(
eventType: string,
limit = 25,
msgtype?: string,
roomIds?: (string | Symbols.AnyRoom)[],
): Promise<unknown> {
const data: IReadEventFromWidgetRequestData = {type: eventType, msgtype: msgtype, limit};
if (roomIds) {
if (roomIds.includes(Symbols.AnyRoom)) {
data.room_ids = Symbols.AnyRoom;
} else {
data.room_ids = roomIds;
}
}
return this.transport.send<IReadEventFromWidgetRequestData, IReadEventFromWidgetResponseData>(
WidgetApiFromWidgetAction.MSC2876ReadEvents,
{type: eventType, msgtype: msgtype, limit},
data,
).then(r => r.events);
}

public readStateEvents(eventType: string, limit = 25, stateKey?: string): Promise<unknown> {
public readStateEvents(
eventType: string,
limit = 25,
stateKey?: string,
roomIds?: (string | Symbols.AnyRoom)[],
): Promise<unknown> {
const data: IReadEventFromWidgetRequestData = {
type: eventType,
state_key: stateKey === undefined ? true : stateKey,
limit,
};
if (roomIds) {
if (roomIds.includes(Symbols.AnyRoom)) {
data.room_ids = Symbols.AnyRoom;
} else {
data.room_ids = roomIds;
}
}
return this.transport.send<IReadEventFromWidgetRequestData, IReadEventFromWidgetResponseData>(
WidgetApiFromWidgetAction.MSC2876ReadEvents,
{type: eventType, state_key: stateKey === undefined ? true : stateKey, limit},
data,
).then(r => r.events);
}

Expand Down
44 changes: 35 additions & 9 deletions src/driver/WidgetDriver.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* Copyright 2020 - 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.
Expand Down Expand Up @@ -53,47 +53,73 @@ export abstract class WidgetDriver {
}

/**
* 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.
* Sends an event into a room. If `roomId` is falsy, the client should send the 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 to that room.
* @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.
* @param {string|null} roomId The room ID to send the event to. If falsy, the room the
* user is currently looking at.
* @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> {
public sendEvent(
eventType: string,
content: unknown,
stateKey: string = null,
roomId: string = null,
): Promise<ISendEventDetails> {
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.
* but not more. If `roomIds` is supplied, it may contain `Symbols.AnyRoom` to denote that
* `limit` in each of the client's known rooms should be returned. When `null`, only the
* room the user is currently looking at should be considered.
* @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
* @param limit The maximum number of events to retrieve per room. Will be zero to denote "as many
* as possible".
* @param roomIds When null, the user's currently viewed room. Otherwise, the list of room IDs
* to look within, possibly containing Symbols.AnyRoom to denote all known rooms.
* @returns {Promise<*[]>} Resolves to the room events, or an empty array.
*/
public readRoomEvents(eventType: string, msgtype: string | undefined, limit: number): Promise<unknown[]> {
public readRoomEvents(
eventType: string,
msgtype: string | undefined,
limit: number,
roomIds: string[] = null,
): Promise<unknown[]> {
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.
* but not more. If `roomIds` is supplied, it may contain `Symbols.AnyRoom` to denote that
* `limit` in each of the client's known rooms should be returned. When `null`, only the
* room the user is currently looking at should be considered.
* @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".
* @param roomIds When null, the user's currently viewed room. Otherwise, the list of room IDs
* to look within, possibly containing Symbols.AnyRoom to denote all known rooms.
* @returns {Promise<*[]>} Resolves to the state events, or an empty array.
*/
public readStateEvents(eventType: string, stateKey: string | undefined, limit: number): Promise<unknown[]> {
public readStateEvents(
eventType: string,
stateKey: string | undefined,
limit: number,
roomIds: string[] = null,
): Promise<unknown[]> {
return Promise.resolve([]);
}

Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2020 - 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.
Expand All @@ -17,6 +17,7 @@ limitations under the License.
// Primary structures
export * from "./WidgetApi";
export * from "./ClientWidgetApi";
export * from "./Symbols";

// Transports (not sure why you'd use these directly, but might as well export all the things)
export * from "./transport/ITransport";
Expand Down
33 changes: 32 additions & 1 deletion src/interfaces/Capabilities.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* Copyright 2020 - 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.
Expand All @@ -14,6 +14,8 @@
* limitations under the License.
*/

import { Symbols } from "../Symbols";

export enum MatrixCapabilities {
Screenshots = "m.capability.screenshot",
StickerSending = "m.sticker",
Expand All @@ -29,3 +31,32 @@ export type Capability = MatrixCapabilities | string;

export const StickerpickerCapabilities: Capability[] = [MatrixCapabilities.StickerSending];
export const VideoConferenceCapabilities: Capability[] = [MatrixCapabilities.AlwaysOnScreen];

/**
* Determines if a capability is a capability for a timeline.
* @param {Capability} capability The capability to test.
* @returns {boolean} True if a timeline capability, false otherwise.
*/
export function isTimelineCapability(capability: Capability): boolean {
// TODO: Change when MSC2762 becomes stable.
return capability?.startsWith("org.matrix.msc2762.timeline:");
}

/**
* Determines if a capability is a timeline capability for the given room.
* @param {Capability} capability The capability to test.
* @param {string | Symbols.AnyRoom} roomId The room ID, or `Symbols.AnyRoom` for that designation.
* @returns {boolean} True if a matching capability, false otherwise.
*/
export function isTimelineCapabilityFor(capability: Capability, roomId: string | Symbols.AnyRoom): boolean {
return capability === `org.matrix.msc2762.timeline:${roomId}`;
}

/**
* Gets the room ID described by a timeline capability.
* @param {string} capability The capability to parse.
* @returns {string} The room ID.
*/
export function getTimelineRoomIDFromCapability(capability: Capability): string {
return capability.substring(capability.indexOf(":") + 1);
}
Loading