Skip to content

Commit

Permalink
Merge pull request #41 from matrix-org/travis/cross-room
Browse files Browse the repository at this point in the history
Add timeline support from MSC2762
  • Loading branch information
turt2live authored Sep 1, 2021
2 parents 8c7e09d + 8defc83 commit 29dfcb6
Show file tree
Hide file tree
Showing 9 changed files with 187 additions and 25 deletions.
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

0 comments on commit 29dfcb6

Please sign in to comment.