Skip to content

Commit

Permalink
Implement MSC3869: Read event relations with the Widget API (#72)
Browse files Browse the repository at this point in the history
* Setup jest and create a first test

Signed-off-by: Dominik Henneke <dominik.henneke@nordeck.net>

* Setup the build pipeline

Signed-off-by: Dominik Henneke <dominik.henneke@nordeck.net>

* Add an action to read relations according to MSC3869

Signed-off-by: Dominik Henneke <dominik.henneke@nordeck.net>

* Remove empty setupTests file

Signed-off-by: Dominik Henneke <dominik.henneke@nordeck.net>

* Update copyright headers

Signed-off-by: Dominik Henneke <dominik.henneke@nordeck.net>

* Announce the api version of the action

Signed-off-by: Dominik Henneke <dominik.henneke@nordeck.net>

* Extract the return type into an interface

Signed-off-by: Dominik Henneke <dominik.henneke@nordeck.net>

* Prefer enums over strings in the tests

Signed-off-by: Dominik Henneke <dominik.henneke@nordeck.net>

Signed-off-by: Dominik Henneke <dominik.henneke@nordeck.net>
  • Loading branch information
dhenneke authored Aug 30, 2022
1 parent 8100b5f commit 839180d
Show file tree
Hide file tree
Showing 14 changed files with 2,415 additions and 53 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ module.exports = {
"no-async-promise-executor": "off",
},
overrides: [{
"files": ["src/**/*.ts"],
"files": ["src/**/*.ts", "test/**/*.ts"],
"extends": ["matrix-org/ts"],
"rules": {
"quotes": "off",
Expand Down
30 changes: 30 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Build and test

on:
push:
branches:
- master
pull_request:

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
cache: 'yarn'

- name: Install NPM packages
run: yarn install --frozen-lockfile

- name: Check Linting Rules and Types
run: yarn lint

- name: test
run: yarn test

- name: build
run: yarn build
15 changes: 13 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
"build:browser:dev": "browserify lib/index.js --debug --s mxwidgets -o dist/api.js",
"build:browser:prod": "browserify lib/index.js --s mxwidgets -p tinyify -o dist/api.min.js",
"lint": "yarn lint:types && yarn lint:ts",
"lint:ts": "eslint src",
"lint:ts": "eslint src test",
"lint:types": "tsc --noEmit",
"lint:fix": "eslint src --fix"
"lint:fix": "eslint src test --fix",
"test": "jest"
},
"files": [
"src",
Expand All @@ -37,16 +38,26 @@
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/preset-env": "^7.11.5",
"@babel/preset-typescript": "^7.10.4",
"@testing-library/dom": "^8.0.0",
"@types/jest": "^27.4.0",
"babel-eslint": "^10.1.0",
"browserify": "^17.0.0",
"eslint": "^7.8.1",
"eslint-config-matrix-org": "^0.1.2",
"eslint-plugin-babel": "^5.3.1",
"jest": "^27.4.0",
"jest-environment-jsdom": "^27.0.6",
"rimraf": "^3.0.2",
"tinyify": "^3.0.0"
},
"dependencies": {
"@types/events": "^3.0.0",
"events": "^3.2.0"
},
"jest": {
"testEnvironment": "jsdom",
"testMatch": [
"<rootDir>/test/**/*-test.[jt]s?(x)"
]
}
}
67 changes: 67 additions & 0 deletions src/ClientWidgetApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ import {
IUpdateTurnServersRequestData,
} from "./interfaces/TurnServerActions";
import { Symbols } from "./Symbols";
import {
IReadRelationsFromWidgetActionRequest,
IReadRelationsFromWidgetResponseData,
} from "./interfaces/ReadRelationsAction";

/**
* API handler for the client side of widgets. This raises events
Expand Down Expand Up @@ -564,6 +568,67 @@ export class ClientWidgetApi extends EventEmitter {
}
}

private async handleReadRelations(request: IReadRelationsFromWidgetActionRequest) {
if (!request.data.event_id) {
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: { message: "Invalid request - missing event ID" },
});
}

if (request.data.limit !== undefined && request.data.limit < 0) {
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: { message: "Invalid request - limit out of range" },
});
}

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

const result = await this.driver.readEventRelations(
request.data.event_id, request.data.room_id, request.data.rel_type,
request.data.event_type, request.data.from, request.data.to,
request.data.limit, request.data.direction);

// check if the user is permitted to receive the event in question
if (result.originalEvent) {
if (result.originalEvent.state_key !== undefined) {
if (!this.canReceiveStateEvent(result.originalEvent.type, result.originalEvent.state_key)) {
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: { message: "Cannot read state events of this type" },
});
}
} else {
if (!this.canReceiveRoomEvent(result.originalEvent.type, result.originalEvent.content['msgtype'])) {
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: { message: "Cannot read room events of this type" },
});
}
}
}

// only return events that the user has the permission to receive
const chunk = result.chunk.filter(e => {
if (e.state_key !== undefined) {
return this.canReceiveStateEvent(e.type, e.state_key);
} else {
return this.canReceiveRoomEvent(e.type, e.content['msgtype']);
}
});

return this.transport.reply<IReadRelationsFromWidgetResponseData>(
request,
{
original_event: result.originalEvent,
chunk,
prev_batch: result.prevBatch,
next_batch: result.nextBatch,
},
);
}

private handleMessage(ev: CustomEvent<IWidgetApiRequest>) {
if (this.isStopped) return;
const actionEv = new CustomEvent(`action:${ev.detail.action}`, {
Expand Down Expand Up @@ -593,6 +658,8 @@ export class ClientWidgetApi extends EventEmitter {
return this.handleWatchTurnServers(<IWatchTurnServersRequest>ev.detail);
case WidgetApiFromWidgetAction.UnwatchTurnServers:
return this.handleUnwatchTurnServers(<IUnwatchTurnServersRequest>ev.detail);
case WidgetApiFromWidgetAction.MSC3869ReadRelations:
return this.handleReadRelations(<IReadRelationsFromWidgetActionRequest>ev.detail);
default:
return this.transport.reply(ev.detail, <IWidgetApiErrorResponseData>{
error: {
Expand Down
55 changes: 55 additions & 0 deletions src/WidgetApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ import { IReadEventFromWidgetRequestData, IReadEventFromWidgetResponseData } fro
import { IRoomEvent } from "./interfaces/IRoomEvent";
import { ITurnServer, IUpdateTurnServersRequest } from "./interfaces/TurnServerActions";
import { Symbols } from "./Symbols";
import {
IReadRelationsFromWidgetRequestData,
IReadRelationsFromWidgetResponseData,
} from "./interfaces/ReadRelationsAction";

/**
* API handler for widgets. This raises events for each action
Expand Down Expand Up @@ -430,6 +434,57 @@ export class WidgetApi extends EventEmitter {
).then(r => r.events);
}

/**
* Reads all related events given a known eventId.
* @param eventId The id of the parent event to be read.
* @param roomId The room to look within. When undefined, the user's currently
* viewed room.
* @param relationType The relationship type of child events to search for.
* When undefined, all relations are returned.
* @param eventType The event type of child events to search for. When undefined,
* all related events are returned.
* @param limit The maximum number of events to retrieve per room. If not
* supplied, the server will apply a default limit.
* @param from The pagination token to start returning results from, as
* received from a previous call. If not supplied, results start at the most
* recent topological event known to the server.
* @param to The pagination token to stop returning results at. If not
* supplied, results continue up to limit or until there are no more events.
* @param direction The direction to search for according to MSC3715.
* @returns Resolves to the room relations.
*/
public async readEventRelations(
eventId: string,
roomId?: string,
relationType?: string,
eventType?: string,
limit?: number,
from?: string,
to?: string,
direction?: 'f' | 'b',
): Promise<IReadRelationsFromWidgetResponseData> {
const versions = await this.getClientVersions();
if (!versions.includes(UnstableApiVersion.MSC3869)) {
throw new Error("The read_relations action is not supported by the client.")
}

const data: IReadRelationsFromWidgetRequestData = {
event_id: eventId,
rel_type: relationType,
event_type: eventType,
room_id: roomId,
to,
from,
limit,
direction,
};

return this.transport.send<IReadRelationsFromWidgetRequestData, IReadRelationsFromWidgetResponseData>(
WidgetApiFromWidgetAction.MSC3869ReadRelations,
data,
)
}

public readStateEvents(
eventType: string,
limit?: number,
Expand Down
44 changes: 44 additions & 0 deletions src/driver/WidgetDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ export interface IOpenIDUpdate {
token?: IOpenIDCredentials;
}

export interface IReadEventRelationsResult {
originalEvent?: IRoomEvent;
chunk: IRoomEvent[];
nextBatch?: string;
prevBatch?: string;
}

/**
* Represents the functions and behaviour the widget-api is unable to
* do, such as prompting the user for information or interacting with
Expand Down Expand Up @@ -140,6 +147,43 @@ export abstract class WidgetDriver {
return Promise.resolve([]);
}

/**
* Reads all events that are related to a given event. The widget API will
* have already verified that the widget is capable of receiving the event,
* or will make sure to reject access to events which are returned from this
* function, but are not capable of receiving. If `relationType` or `eventType`
* are set, the returned events should already be filtered. Less events than
* the limit are allowed to be returned, but not more.
* @param eventId The id of the parent event to be read.
* @param roomId The room to look within. When undefined, the user's
* currently viewed room.
* @param relationType The relationship type of child events to search for.
* When undefined, all relations are returned.
* @param eventType The event type of child events to search for. When undefined,
* all related events are returned.
* @param from The pagination token to start returning results from, as
* received from a previous call. If not supplied, results start at the most
* recent topological event known to the server.
* @param to The pagination token to stop returning results at. If not
* supplied, results continue up to limit or until there are no more events.
* @param limit The maximum number of events to retrieve per room. If not
* supplied, the server will apply a default limit.
* @param direction The direction to search for according to MSC3715
* @returns Resolves to the room relations.
*/
public readEventRelations(
eventId: string,
roomId?: string,
relationType?: string,
eventType?: string,
from?: string,
to?: string,
limit?: number,
direction?: 'f' | 'b',
): Promise<IReadEventRelationsResult> {
return Promise.resolve({ chunk: [] });
}

/**
* Asks the user for permission to validate their identity through OpenID Connect. The
* interface for this function is an observable which accepts the state machine of the
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export * from "./interfaces/ReadEventAction";
export * from "./interfaces/IRoomEvent";
export * from "./interfaces/NavigateAction";
export * from "./interfaces/TurnServerActions";
export * from "./interfaces/ReadRelationsAction";

// Complex models
export * from "./models/WidgetEventCapability";
Expand Down
2 changes: 2 additions & 0 deletions src/interfaces/ApiVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export enum UnstableApiVersion {
MSC2876 = "org.matrix.msc2876",
MSC3819 = "org.matrix.msc3819",
MSC3846 = "town.robin.msc3846",
MSC3869 = "org.matrix.msc3869",
}

export type ApiVersion = MatrixApiVersion | UnstableApiVersion | string;
Expand All @@ -43,4 +44,5 @@ export const CurrentApiVersions: ApiVersion[] = [
UnstableApiVersion.MSC2876,
UnstableApiVersion.MSC3819,
UnstableApiVersion.MSC3846,
UnstableApiVersion.MSC3869,
];
49 changes: 49 additions & 0 deletions src/interfaces/ReadRelationsAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2022 Nordeck IT + Consulting GmbH.
*
* 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 { IRoomEvent } from "./IRoomEvent";
import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest";
import { IWidgetApiResponseData } from "./IWidgetApiResponse";
import { WidgetApiFromWidgetAction } from "./WidgetApiAction";

export interface IReadRelationsFromWidgetRequestData extends IWidgetApiRequestData {
event_id: string; // eslint-disable-line camelcase
rel_type?: string; // eslint-disable-line camelcase
event_type?: string; // eslint-disable-line camelcase
room_id?: string; // eslint-disable-line camelcase

limit?: number;
from?: string;
to?: string;
direction?: 'f' | 'b';
}

export interface IReadRelationsFromWidgetActionRequest extends IWidgetApiRequest {
action: WidgetApiFromWidgetAction.MSC3869ReadRelations;
data: IReadRelationsFromWidgetRequestData;
}

export interface IReadRelationsFromWidgetResponseData extends IWidgetApiResponseData {
original_event: IRoomEvent | undefined; // eslint-disable-line camelcase
chunk: IRoomEvent[];

next_batch?: string; // eslint-disable-line camelcase
prev_batch?: string; // eslint-disable-line camelcase
}

export interface IReadRelationsFromWidgetActionResponse extends IReadRelationsFromWidgetActionRequest {
response: IReadRelationsFromWidgetResponseData;
}
5 changes: 5 additions & 0 deletions src/interfaces/WidgetApiAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ export enum WidgetApiFromWidgetAction {
* @deprecated It is not recommended to rely on this existing - it can be removed without notice.
*/
MSC2974RenegotiateCapabilities = "org.matrix.msc2974.request_capabilities",

/**
* @deprecated It is not recommended to rely on this existing - it can be removed without notice.
*/
MSC3869ReadRelations = "org.matrix.msc3869.read_relations",
}

export type WidgetApiAction = WidgetApiToWidgetAction | WidgetApiFromWidgetAction | string;
Loading

0 comments on commit 839180d

Please sign in to comment.