Skip to content

Commit baa1b4e

Browse files
committed
1 parent 5a1293c commit baa1b4e

File tree

9 files changed

+155
-25
lines changed

9 files changed

+155
-25
lines changed

src/ClientWidgetApi.ts

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020 The Matrix.org Foundation C.I.C.
2+
* Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -61,6 +61,7 @@ import { SimpleObservable } from "./util/SimpleObservable";
6161
import { IOpenIDCredentialsActionRequestData } from "./interfaces/OpenIDCredentialsAction";
6262
import { INavigateActionRequest } from "./interfaces/NavigateAction";
6363
import { IReadEventFromWidgetActionRequest, IReadEventFromWidgetResponseData } from "./interfaces/ReadEventAction";
64+
import { Symbols } from "./Symbols";
6465

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

141+
public canUseRoomTimeline(roomId: string | Symbols.AnyRoom): boolean {
142+
return this.hasCapability(`org.matrix.msc2762.timeline:${Symbols.AnyRoom}`)
143+
|| this.hasCapability(`org.matrix.msc2762.timeline:${roomId}`);
144+
}
145+
140146
public canSendRoomEvent(eventType: string, msgtype: string = null): boolean {
141147
return this.allowedEvents.some(e =>
142148
e.matchesAsRoomEvent(eventType, msgtype) && e.direction === EventDirection.Send);
@@ -344,6 +350,21 @@ export class ClientWidgetApi extends EventEmitter {
344350
});
345351
}
346352

353+
let askRoomIds: string[] = null; // null denotes current room only
354+
if (request.data.room_ids) {
355+
askRoomIds = request.data.room_ids as string[];
356+
if (!Array.isArray(askRoomIds)) {
357+
askRoomIds = [askRoomIds as any as string];
358+
}
359+
for (const roomId of askRoomIds) {
360+
if (!this.canUseRoomTimeline(roomId)) {
361+
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
362+
error: {message: `Unable to access room timeline: ${roomId}`},
363+
});
364+
}
365+
}
366+
}
367+
347368
const limit = request.data.limit || 0;
348369

349370
let events: Promise<unknown[]> = Promise.resolve([]);
@@ -354,14 +375,14 @@ export class ClientWidgetApi extends EventEmitter {
354375
error: {message: "Cannot read state events of this type"},
355376
});
356377
}
357-
events = this.driver.readStateEvents(request.data.type, stateKey, limit);
378+
events = this.driver.readStateEvents(request.data.type, stateKey, limit, askRoomIds);
358379
} else {
359380
if (!this.canReceiveRoomEvent(request.data.type, request.data.msgtype)) {
360381
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
361382
error: {message: "Cannot read room events of this type"},
362383
});
363384
}
364-
events = this.driver.readRoomEvents(request.data.type, request.data.msgtype, limit);
385+
events = this.driver.readRoomEvents(request.data.type, request.data.msgtype, limit, askRoomIds);
365386
}
366387

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

398+
if (!!request.data.room_id && !this.canUseRoomTimeline(request.data.room_id)) {
399+
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
400+
error: {message: `Unable to access room timeline: ${request.data.room_id}`},
401+
});
402+
}
403+
377404
const isState = request.data.state_key !== null && request.data.state_key !== undefined;
378405
let sendEventPromise: Promise<ISendEventDetails>;
379406
if (isState) {
@@ -387,6 +414,7 @@ export class ClientWidgetApi extends EventEmitter {
387414
request.data.type,
388415
request.data.content || {},
389416
request.data.state_key,
417+
request.data.room_id,
390418
);
391419
} else {
392420
const content = request.data.content || {};
@@ -401,6 +429,7 @@ export class ClientWidgetApi extends EventEmitter {
401429
request.data.type,
402430
content,
403431
null, // not sending a state event
432+
request.data.room_id,
404433
);
405434
}
406435

@@ -491,9 +520,15 @@ export class ClientWidgetApi extends EventEmitter {
491520
* permissions, this will no-op and return calmly. If the widget failed to handle the
492521
* event, this will raise an error.
493522
* @param {IRoomEvent} rawEvent The event to (try to) send to the widget.
523+
* @param {string} currentViewedRoomId The room ID the user is currently interacting with.
524+
* Not the room ID of the event.
494525
* @returns {Promise<void>} Resolves when complete, rejects if there was an error sending.
495526
*/
496-
public feedEvent(rawEvent: IRoomEvent): Promise<void> {
527+
public feedEvent(rawEvent: IRoomEvent, currentViewedRoomId: string): Promise<void> {
528+
if (rawEvent.room_id !== currentViewedRoomId && !this.canUseRoomTimeline(rawEvent.room_id)) {
529+
return Promise.resolve(); // no-op
530+
}
531+
497532
if (rawEvent.state_key !== undefined && rawEvent.state_key !== null) {
498533
// state event
499534
if (!this.canReceiveStateEvent(rawEvent.type, rawEvent.state_key)) {

src/Symbols.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright 2021 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 enum Symbols {
18+
AnyRoom = "*",
19+
}

src/WidgetApi.ts

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020 The Matrix.org Foundation C.I.C.
2+
* Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -56,6 +56,7 @@ import { ISendEventFromWidgetRequestData, ISendEventFromWidgetResponseData } fro
5656
import { EventDirection, WidgetEventCapability } from "./models/WidgetEventCapability";
5757
import { INavigateActionRequestData } from "./interfaces/NavigateAction";
5858
import { IReadEventFromWidgetRequestData, IReadEventFromWidgetResponseData } from "./interfaces/ReadEventAction";
59+
import { Symbols } from "./Symbols";
5960

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

147+
/**
148+
* Requests the capability to interact with rooms other than the user's currently
149+
* viewed room. Applies to event receiving and sending.
150+
* @param {string | Symbols.AnyRoom} roomId The room ID, or `Symbols.AnyRoom` to
151+
* denote all known rooms.
152+
*/
153+
public requestCapabilityForRoomTimeline(roomId: string | Symbols.AnyRoom) {
154+
this.requestCapability(`org.matrix.msc2762.timeline:${roomId}`);
155+
}
156+
146157
/**
147158
* Requests the capability to send a given state event with optional explicit
148159
* state key. It is not guaranteed to be allowed, but will be asked for if the
@@ -327,35 +338,52 @@ export class WidgetApi extends EventEmitter {
327338
return this.transport.send<IModalWidgetReturnData>(WidgetApiFromWidgetAction.CloseModalWidget, data).then();
328339
}
329340

330-
public sendRoomEvent(eventType: string, content: unknown): Promise<ISendEventFromWidgetResponseData> {
341+
public sendRoomEvent(eventType: string, content: unknown, roomId?: string): Promise<ISendEventFromWidgetResponseData> {
331342
return this.transport.send<ISendEventFromWidgetRequestData, ISendEventFromWidgetResponseData>(
332343
WidgetApiFromWidgetAction.SendEvent,
333-
{type: eventType, content},
344+
{type: eventType, content, room_id: roomId},
334345
);
335346
}
336347

337348
public sendStateEvent(
338349
eventType: string,
339350
stateKey: string,
340351
content: unknown,
352+
roomId?: string,
341353
): Promise<ISendEventFromWidgetResponseData> {
342354
return this.transport.send<ISendEventFromWidgetRequestData, ISendEventFromWidgetResponseData>(
343355
WidgetApiFromWidgetAction.SendEvent,
344-
{type: eventType, content, state_key: stateKey},
356+
{type: eventType, content, state_key: stateKey, room_id: roomId},
345357
);
346358
}
347359

348-
public readRoomEvents(eventType: string, limit = 25, msgtype?: string): Promise<unknown> {
360+
public readRoomEvents(eventType: string, limit = 25, msgtype?: string, roomIds?: (string | Symbols.AnyRoom)[]): Promise<unknown> {
361+
const data: IReadEventFromWidgetRequestData = {type: eventType, msgtype: msgtype, limit};
362+
if (roomIds) {
363+
if (roomIds.includes(Symbols.AnyRoom)) {
364+
data.room_ids = Symbols.AnyRoom;
365+
} else {
366+
data.room_ids = roomIds;
367+
}
368+
}
349369
return this.transport.send<IReadEventFromWidgetRequestData, IReadEventFromWidgetResponseData>(
350370
WidgetApiFromWidgetAction.MSC2876ReadEvents,
351-
{type: eventType, msgtype: msgtype, limit},
371+
data,
352372
).then(r => r.events);
353373
}
354374

355-
public readStateEvents(eventType: string, limit = 25, stateKey?: string): Promise<unknown> {
375+
public readStateEvents(eventType: string, limit = 25, stateKey?: string, roomIds?: (string | Symbols.AnyRoom)[]): Promise<unknown> {
376+
const data: IReadEventFromWidgetRequestData = {type: eventType, state_key: stateKey === undefined ? true : stateKey, limit};
377+
if (roomIds) {
378+
if (roomIds.includes(Symbols.AnyRoom)) {
379+
data.room_ids = Symbols.AnyRoom;
380+
} else {
381+
data.room_ids = roomIds;
382+
}
383+
}
356384
return this.transport.send<IReadEventFromWidgetRequestData, IReadEventFromWidgetResponseData>(
357385
WidgetApiFromWidgetAction.MSC2876ReadEvents,
358-
{type: eventType, state_key: stateKey === undefined ? true : stateKey, limit},
386+
data,
359387
).then(r => r.events);
360388
}
361389

src/driver/WidgetDriver.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020 The Matrix.org Foundation C.I.C.
2+
* Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -53,47 +53,58 @@ export abstract class WidgetDriver {
5353
}
5454

5555
/**
56-
* Sends an event into the room the user is currently looking at. The widget API
57-
* will have already verified that the widget is capable of sending the event.
56+
* Sends an event into a room. If `roomId` is falsy, the client should send the event
57+
* into the room the user is currently looking at. The widget API will have already
58+
* verified that the widget is capable of sending the event to that room.
5859
* @param {string} eventType The event type to be sent.
5960
* @param {*} content The content for the event.
6061
* @param {string|null} stateKey The state key if this is a state event, otherwise null.
6162
* May be an empty string.
63+
* @param {string|null} roomId The room ID to send the event to. If falsy, the room the
64+
* user is currently looking at.
6265
* @returns {Promise<ISendEventDetails>} Resolves when the event has been sent with
6366
* details of that event.
6467
* @throws Rejected when the event could not be sent.
6568
*/
66-
public sendEvent(eventType: string, content: unknown, stateKey: string = null): Promise<ISendEventDetails> {
69+
public sendEvent(eventType: string, content: unknown, stateKey: string = null, roomId: string = null): Promise<ISendEventDetails> {
6770
return Promise.reject(new Error("Failed to override function"));
6871
}
6972

7073
/**
7174
* Reads all events of the given type, and optionally `msgtype` (if applicable/defined),
7275
* the user has access to. The widget API will have already verified that the widget is
7376
* capable of receiving the events. Less events than the limit are allowed to be returned,
74-
* but not more.
77+
* but not more. If `roomIds` is supplied, it may contain `Symbols.AnyRoom` to denote that
78+
* `limit` in each of the client's known rooms should be returned. When `null`, only the
79+
* room the user is currently looking at should be considered.
7580
* @param eventType The event type to be read.
7681
* @param msgtype The msgtype of the events to be read, if applicable/defined.
77-
* @param limit The maximum number of events to retrieve. Will be zero to denote "as many
82+
* @param limit The maximum number of events to retrieve per room. Will be zero to denote "as many
7883
* as possible".
84+
* @param roomIds When null, the user's currently viewed room. Otherwise, the list of room IDs
85+
* to look within, possibly containing Symbols.AnyRoom to denote all known rooms.
7986
* @returns {Promise<*[]>} Resolves to the room events, or an empty array.
8087
*/
81-
public readRoomEvents(eventType: string, msgtype: string | undefined, limit: number): Promise<unknown[]> {
88+
public readRoomEvents(eventType: string, msgtype: string | undefined, limit: number, roomIds: string[] = null): Promise<unknown[]> {
8289
return Promise.resolve([]);
8390
}
8491

8592
/**
8693
* Reads all events of the given type, and optionally state key (if applicable/defined),
8794
* the user has access to. The widget API will have already verified that the widget is
8895
* capable of receiving the events. Less events than the limit are allowed to be returned,
89-
* but not more.
96+
* but not more. If `roomIds` is supplied, it may contain `Symbols.AnyRoom` to denote that
97+
* `limit` in each of the client's known rooms should be returned. When `null`, only the
98+
* room the user is currently looking at should be considered.
9099
* @param eventType The event type to be read.
91100
* @param stateKey The state key of the events to be read, if applicable/defined.
92101
* @param limit The maximum number of events to retrieve. Will be zero to denote "as many
93102
* as possible".
103+
* @param roomIds When null, the user's currently viewed room. Otherwise, the list of room IDs
104+
* to look within, possibly containing Symbols.AnyRoom to denote all known rooms.
94105
* @returns {Promise<*[]>} Resolves to the state events, or an empty array.
95106
*/
96-
public readStateEvents(eventType: string, stateKey: string | undefined, limit: number): Promise<unknown[]> {
107+
public readStateEvents(eventType: string, stateKey: string | undefined, limit: number, roomIds: string[] = null): Promise<unknown[]> {
97108
return Promise.resolve([]);
98109
}
99110

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2020 The Matrix.org Foundation C.I.C.
2+
Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@ limitations under the License.
1717
// Primary structures
1818
export * from "./WidgetApi";
1919
export * from "./ClientWidgetApi";
20+
export * from "./Symbols";
2021

2122
// Transports (not sure why you'd use these directly, but might as well export all the things)
2223
export * from "./transport/ITransport";

src/interfaces/Capabilities.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020 The Matrix.org Foundation C.I.C.
2+
* Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -14,6 +14,8 @@
1414
* limitations under the License.
1515
*/
1616

17+
import { Symbols } from "../Symbols";
18+
1719
export enum MatrixCapabilities {
1820
Screenshots = "m.capability.screenshot",
1921
StickerSending = "m.sticker",
@@ -29,3 +31,32 @@ export type Capability = MatrixCapabilities | string;
2931

3032
export const StickerpickerCapabilities: Capability[] = [MatrixCapabilities.StickerSending];
3133
export const VideoConferenceCapabilities: Capability[] = [MatrixCapabilities.AlwaysOnScreen];
34+
35+
/**
36+
* Determines if a capability is a capability for a timeline.
37+
* @param {Capability} capability The capability to test.
38+
* @returns {boolean} True if a timeline capability, false otherwise.
39+
*/
40+
export function isTimelineCapability(capability: Capability): boolean {
41+
// TODO: Change when MSC2762 becomes stable.
42+
return capability?.startsWith("org.matrix.msc2762.timeline:");
43+
}
44+
45+
/**
46+
* Determines if a capability is a timeline capability for the given room.
47+
* @param {Capability} capability The capability to test.
48+
* @param {string | Symbols.AnyRoom} roomId The room ID, or `Symbols.AnyRoom` for that designation.
49+
* @returns {boolean} True if a matching capability, false otherwise.
50+
*/
51+
export function isTimelineCapabilityFor(capability: Capability, roomId: string | Symbols.AnyRoom): boolean {
52+
return capability === `org.matrix.msc2762.timeline:${roomId}`;
53+
}
54+
55+
/**
56+
* Gets the room ID described by a timeline capability.
57+
* @param {string} capability The capability to parse.
58+
* @returns {string} The room ID.
59+
*/
60+
export function getTimelineRoomIDFromCapability(capability: Capability): string {
61+
return capability.substring(capability.indexOf(":") + 1);
62+
}

src/interfaces/IRoomEvent.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020 The Matrix.org Foundation C.I.C.
2+
* Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ export interface IRoomEvent {
1818
type: string;
1919
sender: string;
2020
event_id: string; // eslint-disable-line camelcase
21+
room_id: string; // eslint-disable-line camelcase
2122
state_key?: string; // eslint-disable-line camelcase
2223
origin_server_ts: number; // eslint-disable-line camelcase
2324
content: unknown;

src/interfaces/ReadEventAction.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest";
1818
import { WidgetApiFromWidgetAction } from "./WidgetApiAction";
1919
import { IWidgetApiResponseData } from "./IWidgetApiResponse";
20+
import { Symbols } from "../Symbols";
2021

2122
export interface IReadEventFromWidgetRequestData extends IWidgetApiRequestData {
2223
state_key?: string | boolean; // eslint-disable-line camelcase
2324
msgtype?: string;
2425
type: string;
2526
limit?: number;
27+
room_ids?: Symbols.AnyRoom | string[]; // eslint-disable-line camelcase
2628
}
2729

2830
export interface IReadEventFromWidgetActionRequest extends IWidgetApiRequest {

0 commit comments

Comments
 (0)