Skip to content

Commit 29dfcb6

Browse files
authored
Merge pull request #41 from matrix-org/travis/cross-room
Add timeline support from MSC2762
2 parents 8c7e09d + 8defc83 commit 29dfcb6

File tree

9 files changed

+187
-25
lines changed

9 files changed

+187
-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: 54 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,70 @@ 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(
342+
eventType: string,
343+
content: unknown,
344+
roomId?: string,
345+
): Promise<ISendEventFromWidgetResponseData> {
331346
return this.transport.send<ISendEventFromWidgetRequestData, ISendEventFromWidgetResponseData>(
332347
WidgetApiFromWidgetAction.SendEvent,
333-
{type: eventType, content},
348+
{type: eventType, content, room_id: roomId},
334349
);
335350
}
336351

337352
public sendStateEvent(
338353
eventType: string,
339354
stateKey: string,
340355
content: unknown,
356+
roomId?: string,
341357
): Promise<ISendEventFromWidgetResponseData> {
342358
return this.transport.send<ISendEventFromWidgetRequestData, ISendEventFromWidgetResponseData>(
343359
WidgetApiFromWidgetAction.SendEvent,
344-
{type: eventType, content, state_key: stateKey},
360+
{type: eventType, content, state_key: stateKey, room_id: roomId},
345361
);
346362
}
347363

348-
public readRoomEvents(eventType: string, limit = 25, msgtype?: string): Promise<unknown> {
364+
public readRoomEvents(
365+
eventType: string,
366+
limit = 25,
367+
msgtype?: string,
368+
roomIds?: (string | Symbols.AnyRoom)[],
369+
): Promise<unknown> {
370+
const data: IReadEventFromWidgetRequestData = {type: eventType, msgtype: msgtype, limit};
371+
if (roomIds) {
372+
if (roomIds.includes(Symbols.AnyRoom)) {
373+
data.room_ids = Symbols.AnyRoom;
374+
} else {
375+
data.room_ids = roomIds;
376+
}
377+
}
349378
return this.transport.send<IReadEventFromWidgetRequestData, IReadEventFromWidgetResponseData>(
350379
WidgetApiFromWidgetAction.MSC2876ReadEvents,
351-
{type: eventType, msgtype: msgtype, limit},
380+
data,
352381
).then(r => r.events);
353382
}
354383

355-
public readStateEvents(eventType: string, limit = 25, stateKey?: string): Promise<unknown> {
384+
public readStateEvents(
385+
eventType: string,
386+
limit = 25,
387+
stateKey?: string,
388+
roomIds?: (string | Symbols.AnyRoom)[],
389+
): Promise<unknown> {
390+
const data: IReadEventFromWidgetRequestData = {
391+
type: eventType,
392+
state_key: stateKey === undefined ? true : stateKey,
393+
limit,
394+
};
395+
if (roomIds) {
396+
if (roomIds.includes(Symbols.AnyRoom)) {
397+
data.room_ids = Symbols.AnyRoom;
398+
} else {
399+
data.room_ids = roomIds;
400+
}
401+
}
356402
return this.transport.send<IReadEventFromWidgetRequestData, IReadEventFromWidgetResponseData>(
357403
WidgetApiFromWidgetAction.MSC2876ReadEvents,
358-
{type: eventType, state_key: stateKey === undefined ? true : stateKey, limit},
404+
data,
359405
).then(r => r.events);
360406
}
361407

src/driver/WidgetDriver.ts

Lines changed: 35 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,73 @@ 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(
70+
eventType: string,
71+
content: unknown,
72+
stateKey: string = null,
73+
roomId: string = null,
74+
): Promise<ISendEventDetails> {
6775
return Promise.reject(new Error("Failed to override function"));
6876
}
6977

7078
/**
7179
* Reads all events of the given type, and optionally `msgtype` (if applicable/defined),
7280
* the user has access to. The widget API will have already verified that the widget is
7381
* capable of receiving the events. Less events than the limit are allowed to be returned,
74-
* but not more.
82+
* but not more. If `roomIds` is supplied, it may contain `Symbols.AnyRoom` to denote that
83+
* `limit` in each of the client's known rooms should be returned. When `null`, only the
84+
* room the user is currently looking at should be considered.
7585
* @param eventType The event type to be read.
7686
* @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
87+
* @param limit The maximum number of events to retrieve per room. Will be zero to denote "as many
7888
* as possible".
89+
* @param roomIds When null, the user's currently viewed room. Otherwise, the list of room IDs
90+
* to look within, possibly containing Symbols.AnyRoom to denote all known rooms.
7991
* @returns {Promise<*[]>} Resolves to the room events, or an empty array.
8092
*/
81-
public readRoomEvents(eventType: string, msgtype: string | undefined, limit: number): Promise<unknown[]> {
93+
public readRoomEvents(
94+
eventType: string,
95+
msgtype: string | undefined,
96+
limit: number,
97+
roomIds: string[] = null,
98+
): Promise<unknown[]> {
8299
return Promise.resolve([]);
83100
}
84101

85102
/**
86103
* Reads all events of the given type, and optionally state key (if applicable/defined),
87104
* the user has access to. The widget API will have already verified that the widget is
88105
* capable of receiving the events. Less events than the limit are allowed to be returned,
89-
* but not more.
106+
* but not more. If `roomIds` is supplied, it may contain `Symbols.AnyRoom` to denote that
107+
* `limit` in each of the client's known rooms should be returned. When `null`, only the
108+
* room the user is currently looking at should be considered.
90109
* @param eventType The event type to be read.
91110
* @param stateKey The state key of the events to be read, if applicable/defined.
92111
* @param limit The maximum number of events to retrieve. Will be zero to denote "as many
93112
* as possible".
113+
* @param roomIds When null, the user's currently viewed room. Otherwise, the list of room IDs
114+
* to look within, possibly containing Symbols.AnyRoom to denote all known rooms.
94115
* @returns {Promise<*[]>} Resolves to the state events, or an empty array.
95116
*/
96-
public readStateEvents(eventType: string, stateKey: string | undefined, limit: number): Promise<unknown[]> {
117+
public readStateEvents(
118+
eventType: string,
119+
stateKey: string | undefined,
120+
limit: number,
121+
roomIds: string[] = null,
122+
): Promise<unknown[]> {
97123
return Promise.resolve([]);
98124
}
99125

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+
}

0 commit comments

Comments
 (0)