Compare commits
116 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f1db4dc668 | |||
| 98c1710ac1 | |||
| 3141a7d7c1 | |||
| 1304e811d0 | |||
| 2ce1e7e6ef | |||
| 70efed1a58 | |||
| 9e4f109e80 | |||
| 5d54bf558c | |||
| fc5f0e8047 | |||
| 6bc584ba8b | |||
| ac4e504b8d | |||
| 0abee2a0bf | |||
| 4e4afdb795 | |||
| 9b429c1902 | |||
| b782dee2ef | |||
| 54e815085f | |||
| 0739da4ef4 | |||
| c300a6bdd6 | |||
| 901d53eb38 | |||
| a54471f737 | |||
| 1fae9cb3ef | |||
| 124bfc9328 | |||
| 53aa34fba5 | |||
| 58756a1973 | |||
| 7d9800b817 | |||
| e408590c21 | |||
| 735ccca18b | |||
| 946f47e037 | |||
| 4de1699c49 | |||
| 3d9221f054 | |||
| 1d1d59c757 | |||
| af43687354 | |||
| 2ec5acb55d | |||
| 2128f67dc3 | |||
| 55dda8420c | |||
| d595717e60 | |||
| 4603d4e578 | |||
| 12e525b664 | |||
| 1ac4cc4b11 | |||
| 7b218905fb | |||
| 080426dfdd | |||
| a89c1990d6 | |||
| 4e72290d53 | |||
| 7a7318b636 | |||
| 74d24f38f7 | |||
| 78dcae9143 | |||
| 0db640a679 | |||
| a3ddfd519b | |||
| e86d8861b9 | |||
| 2910e62bb6 | |||
| 2bb14698a5 | |||
| 2fd08d72dc | |||
| 222f62164c | |||
| bf8ee39a3f | |||
| cc1d1d76d0 | |||
| 30fdd96168 | |||
| cfad8d3614 | |||
| d9c3b880fc | |||
| 25c115739c | |||
| d97c514b8d | |||
| 6b822ccd61 | |||
| 47c5c4645e | |||
| ea0eaff212 | |||
| 41bf8c2d5f | |||
| d473a2e095 | |||
| d398ffd6df | |||
| ffab55452a | |||
| 9120b1dfd9 | |||
| 5d4e3183aa | |||
| 07171a95c4 | |||
| 12afcd3850 | |||
| 2d23330b74 | |||
| 7faff66006 | |||
| b07457726b | |||
| 6bf8142ff6 | |||
| cf0ccaf93d | |||
| 34a3955b60 | |||
| 82122872bf | |||
| 9c242a9ce6 | |||
| 51f3fac87b | |||
| 66b98844a2 | |||
| d03db00e4c | |||
| cf8d2bf6ef | |||
| 913a0b51a6 | |||
| ede0f696ee | |||
| d05214a169 | |||
| 1e93d0b19f | |||
| 5e125e646c | |||
| 1643201363 | |||
| 8e99e084a8 | |||
| 67d362fdd9 | |||
| e66430ada6 | |||
| 52ca60142a | |||
| 7d2a5afa6d | |||
| 02ba233644 | |||
| 50493a3330 | |||
| 8434f29c54 | |||
| a34426a7f6 | |||
| 033693283d | |||
| fab7b7f26a | |||
| 230e3b4ace | |||
| a50a627300 | |||
| 80930f6690 | |||
| 652b3a9208 | |||
| 0a8b825702 | |||
| f4e2a38f4b | |||
| b3474531c9 | |||
| f0597e0124 | |||
| 2fdc91bd04 | |||
| 16ca09eed8 | |||
| 129fb7f10f | |||
| 016e24472a | |||
| 4acb98c496 | |||
| 96d1b30012 | |||
| 6fc586598a | |||
| 2d9c938765 |
@@ -31,6 +31,9 @@ module.exports = {
|
||||
"no-async-promise-executor": "off",
|
||||
// We use a `logger` intermediary module
|
||||
"no-console": "error",
|
||||
|
||||
// restrict EventEmitters to force callers to use TypedEventEmitter
|
||||
"no-restricted-imports": ["error", "events"],
|
||||
},
|
||||
overrides: [{
|
||||
files: [
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
comment:
|
||||
layout: "diff, files"
|
||||
behavior: default
|
||||
require_changes: false
|
||||
require_base: no
|
||||
require_head: no
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Test coverage
|
||||
on:
|
||||
pull_request: {}
|
||||
push:
|
||||
branches: [develop, main, master]
|
||||
jobs:
|
||||
test-coverage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: "yarn install && yarn build && yarn coverage"
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
verbose: true
|
||||
+144
@@ -1,3 +1,147 @@
|
||||
Changes in [16.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.0) (2022-03-15)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Improve typing around event emitter handlers ([\#2180](https://github.com/matrix-org/matrix-js-sdk/pull/2180)).
|
||||
|
||||
## ✨ Features
|
||||
* Fix defer not supporting resolving with a Promise<T> ([\#2216](https://github.com/matrix-org/matrix-js-sdk/pull/2216)).
|
||||
* add LocationAssetType enum ([\#2214](https://github.com/matrix-org/matrix-js-sdk/pull/2214)).
|
||||
* Support for mid-call devices changes ([\#2154](https://github.com/matrix-org/matrix-js-sdk/pull/2154)). Contributed by @SimonBrandner.
|
||||
* Add new room state emit RoomStateEvent.Update for lower-frequency hits ([\#2192](https://github.com/matrix-org/matrix-js-sdk/pull/2192)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix wrong event_id being sent for m.in_reply_to of threads ([\#2213](https://github.com/matrix-org/matrix-js-sdk/pull/2213)).
|
||||
* Fix wrongly asserting that PushRule::conditions is non-null ([\#2217](https://github.com/matrix-org/matrix-js-sdk/pull/2217)).
|
||||
* Make createThread more resilient when missing rootEvent ([\#2207](https://github.com/matrix-org/matrix-js-sdk/pull/2207)). Fixes vector-im/element-web#21130.
|
||||
* Fix bug with the /hierarchy API sending invalid requests ([\#2201](https://github.com/matrix-org/matrix-js-sdk/pull/2201)). Fixes vector-im/element-web#21170.
|
||||
* fix relation sender filter ([\#2196](https://github.com/matrix-org/matrix-js-sdk/pull/2196)). Fixes vector-im/element-web#20877.
|
||||
* Fix bug with one-way audio after a transfer ([\#2193](https://github.com/matrix-org/matrix-js-sdk/pull/2193)).
|
||||
|
||||
Changes in [16.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.0-rc.1) (2022-03-08)
|
||||
============================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Improve typing around event emitter handlers ([\#2180](https://github.com/matrix-org/matrix-js-sdk/pull/2180)).
|
||||
|
||||
## ✨ Features
|
||||
* Fix defer not supporting resolving with a Promise<T> ([\#2216](https://github.com/matrix-org/matrix-js-sdk/pull/2216)).
|
||||
* add LocationAssetType enum ([\#2214](https://github.com/matrix-org/matrix-js-sdk/pull/2214)).
|
||||
* Support for mid-call devices changes ([\#2154](https://github.com/matrix-org/matrix-js-sdk/pull/2154)). Contributed by @SimonBrandner.
|
||||
* Add new room state emit RoomStateEvent.Update for lower-frequency hits ([\#2192](https://github.com/matrix-org/matrix-js-sdk/pull/2192)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix wrong event_id being sent for m.in_reply_to of threads ([\#2213](https://github.com/matrix-org/matrix-js-sdk/pull/2213)).
|
||||
* Fix wrongly asserting that PushRule::conditions is non-null ([\#2217](https://github.com/matrix-org/matrix-js-sdk/pull/2217)).
|
||||
* Make createThread more resilient when missing rootEvent ([\#2207](https://github.com/matrix-org/matrix-js-sdk/pull/2207)). Fixes vector-im/element-web#21130.
|
||||
* Fix bug with the /hierarchy API sending invalid requests ([\#2201](https://github.com/matrix-org/matrix-js-sdk/pull/2201)). Fixes vector-im/element-web#21170.
|
||||
* fix relation sender filter ([\#2196](https://github.com/matrix-org/matrix-js-sdk/pull/2196)). Fixes vector-im/element-web#20877.
|
||||
* Fix bug with one-way audio after a transfer ([\#2193](https://github.com/matrix-org/matrix-js-sdk/pull/2193)).
|
||||
|
||||
Changes in [15.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.6.0) (2022-02-28)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Return send event response from MSC3089Branch.createNewVersion() ([\#2186](https://github.com/matrix-org/matrix-js-sdk/pull/2186)).
|
||||
* Add functions to support refresh tokens ([\#2178](https://github.com/matrix-org/matrix-js-sdk/pull/2178)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* [Release] Fix bug with the /hierarchy API sending invalid requests ([\#2202](https://github.com/matrix-org/matrix-js-sdk/pull/2202)).
|
||||
* Fix bug where calls could break if rejected from somewhere else ([\#2189](https://github.com/matrix-org/matrix-js-sdk/pull/2189)).
|
||||
* Fix camera stuck on after call transfer ([\#2188](https://github.com/matrix-org/matrix-js-sdk/pull/2188)).
|
||||
* Fix synthetic read receipt handling ([\#2174](https://github.com/matrix-org/matrix-js-sdk/pull/2174)). Fixes vector-im/element-web#21016.
|
||||
* Revert "Sign backup with cross-signing key when we reset it." ([\#2175](https://github.com/matrix-org/matrix-js-sdk/pull/2175)).
|
||||
* Sign backup with cross-signing key when we reset it. ([\#2170](https://github.com/matrix-org/matrix-js-sdk/pull/2170)).
|
||||
* Fix error in uploadContent() when file is empty under Node.js ([\#2155](https://github.com/matrix-org/matrix-js-sdk/pull/2155)).
|
||||
* Check the backup info against the stored private key when determining trust. ([\#2167](https://github.com/matrix-org/matrix-js-sdk/pull/2167)).
|
||||
* Back up keys before logging out ([\#2158](https://github.com/matrix-org/matrix-js-sdk/pull/2158)). Fixes vector-im/element-web#13151.
|
||||
|
||||
Changes in [15.6.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.6.0-rc.1) (2022-02-22)
|
||||
============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Return send event response from MSC3089Branch.createNewVersion() ([\#2186](https://github.com/matrix-org/matrix-js-sdk/pull/2186)).
|
||||
* Add functions to support refresh tokens ([\#2178](https://github.com/matrix-org/matrix-js-sdk/pull/2178)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix bug where calls could break if rejected from somewhere else ([\#2189](https://github.com/matrix-org/matrix-js-sdk/pull/2189)).
|
||||
* Fix camera stuck on after call transfer ([\#2188](https://github.com/matrix-org/matrix-js-sdk/pull/2188)).
|
||||
* Fix synthetic read receipt handling ([\#2174](https://github.com/matrix-org/matrix-js-sdk/pull/2174)). Fixes vector-im/element-web#21016.
|
||||
* Revert "Sign backup with cross-signing key when we reset it." ([\#2175](https://github.com/matrix-org/matrix-js-sdk/pull/2175)).
|
||||
* Sign backup with cross-signing key when we reset it. ([\#2170](https://github.com/matrix-org/matrix-js-sdk/pull/2170)).
|
||||
* Fix error in uploadContent() when file is empty under Node.js ([\#2155](https://github.com/matrix-org/matrix-js-sdk/pull/2155)).
|
||||
* Check the backup info against the stored private key when determining trust. ([\#2167](https://github.com/matrix-org/matrix-js-sdk/pull/2167)).
|
||||
* Back up keys before logging out ([\#2158](https://github.com/matrix-org/matrix-js-sdk/pull/2158)). Fixes vector-im/element-web#13151.
|
||||
|
||||
Changes in [15.5.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.2) (2022-02-17)
|
||||
==================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix synthetic read receipt handling
|
||||
|
||||
Changes in [15.5.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.1) (2022-02-14)
|
||||
==================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix issue with rooms not getting marked as unread ([\#2163](https://github.com/matrix-org/matrix-js-sdk/pull/2163)). Fixes vector-im/element-web#20971.
|
||||
* Don't store streams that are only used once ([\#2157](https://github.com/matrix-org/matrix-js-sdk/pull/2157)). Fixes vector-im/element-web#20932. Contributed by @SimonBrandner.
|
||||
* Fix edge cases around RR calculations ([\#2160](https://github.com/matrix-org/matrix-js-sdk/pull/2160)). Fixes vector-im/element-web#20922.
|
||||
* Account for encryption in `maySendMessage()` ([\#2159](https://github.com/matrix-org/matrix-js-sdk/pull/2159)). Contributed by @SimonBrandner.
|
||||
* Send references to thread root to threads, even out of order ([\#2156](https://github.com/matrix-org/matrix-js-sdk/pull/2156)).
|
||||
* Fix initial sync fail when event fetching unsuccessful ([\#2150](https://github.com/matrix-org/matrix-js-sdk/pull/2150)). Fixes vector-im/element-web#20862.
|
||||
* Don't decrypt redacted messages ([\#2143](https://github.com/matrix-org/matrix-js-sdk/pull/2143)). Contributed by @SimonBrandner.
|
||||
|
||||
Changes in [15.5.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.1-rc.1) (2022-02-08)
|
||||
============================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix issue with rooms not getting marked as unread ([\#2163](https://github.com/matrix-org/matrix-js-sdk/pull/2163)). Fixes vector-im/element-web#20971.
|
||||
* Don't store streams that are only used once ([\#2157](https://github.com/matrix-org/matrix-js-sdk/pull/2157)). Fixes vector-im/element-web#20932. Contributed by @SimonBrandner.
|
||||
* Fix edge cases around RR calculations ([\#2160](https://github.com/matrix-org/matrix-js-sdk/pull/2160)). Fixes vector-im/element-web#20922.
|
||||
* Account for encryption in `maySendMessage()` ([\#2159](https://github.com/matrix-org/matrix-js-sdk/pull/2159)). Contributed by @SimonBrandner.
|
||||
* Send references to thread root to threads, even out of order ([\#2156](https://github.com/matrix-org/matrix-js-sdk/pull/2156)).
|
||||
* Fix initial sync fail when event fetching unsuccessful ([\#2150](https://github.com/matrix-org/matrix-js-sdk/pull/2150)). Fixes vector-im/element-web#20862.
|
||||
* Don't decrypt redacted messages ([\#2143](https://github.com/matrix-org/matrix-js-sdk/pull/2143)). Contributed by @SimonBrandner.
|
||||
|
||||
Changes in [15.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.0) (2022-01-31)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Support m.asset in m.location event content ([\#2109](https://github.com/matrix-org/matrix-js-sdk/pull/2109)).
|
||||
* Send extensible events structure and support on-demand parsing ([\#2091](https://github.com/matrix-org/matrix-js-sdk/pull/2091)).
|
||||
* Support cancelling events whilst they are in status = ENCRYPTING ([\#2095](https://github.com/matrix-org/matrix-js-sdk/pull/2095)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix http-api butchering idServer requests ([\#2134](https://github.com/matrix-org/matrix-js-sdk/pull/2134)). Fixes vector-im/element-web#20680.
|
||||
* Don't remove streams that still have tracks ([\#2104](https://github.com/matrix-org/matrix-js-sdk/pull/2104)).
|
||||
|
||||
Changes in [15.5.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.0-rc.1) (2022-01-26)
|
||||
============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Support m.asset in m.location event content ([\#2109](https://github.com/matrix-org/matrix-js-sdk/pull/2109)).
|
||||
* Send extensible events structure and support on-demand parsing ([\#2091](https://github.com/matrix-org/matrix-js-sdk/pull/2091)).
|
||||
* Support cancelling events whilst they are in status = ENCRYPTING ([\#2095](https://github.com/matrix-org/matrix-js-sdk/pull/2095)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix http-api butchering idServer requests ([\#2134](https://github.com/matrix-org/matrix-js-sdk/pull/2134)). Fixes vector-im/element-web#20680.
|
||||
* Don't remove streams that still have tracks ([\#2104](https://github.com/matrix-org/matrix-js-sdk/pull/2104)).
|
||||
|
||||
Changes in [15.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.4.0) (2022-01-17)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Don't consider alt_aliases when calculating room name ([\#2094](https://github.com/matrix-org/matrix-js-sdk/pull/2094)). Fixes vector-im/element-web#13887.
|
||||
* Load room history if necessary when searching for MSC3089 getFileEvent() ([\#2066](https://github.com/matrix-org/matrix-js-sdk/pull/2066)).
|
||||
* Add support for MSC3030 `/timestamp_to_event` ([\#2072](https://github.com/matrix-org/matrix-js-sdk/pull/2072)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Stop encrypting redactions as it isn't spec compliant ([\#2098](https://github.com/matrix-org/matrix-js-sdk/pull/2098)). Fixes vector-im/element-web#20460.
|
||||
* Fix more function typings relating to key backup ([\#2086](https://github.com/matrix-org/matrix-js-sdk/pull/2086)).
|
||||
* Fix timeline search in MSC3089 getFileEvent() ([\#2085](https://github.com/matrix-org/matrix-js-sdk/pull/2085)).
|
||||
* Set a `deviceId` for VoIP example and use `const`/`let` ([\#2090](https://github.com/matrix-org/matrix-js-sdk/pull/2090)). Fixes #2083. Contributed by @SimonBrandner.
|
||||
* Fix incorrect TS return type for secret storage and key backup functions ([\#2082](https://github.com/matrix-org/matrix-js-sdk/pull/2082)).
|
||||
|
||||
Changes in [15.4.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.4.0-rc.1) (2022-01-11)
|
||||
============================================================================================================
|
||||
|
||||
|
||||
+5
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "15.4.0-rc.1",
|
||||
"version": "16.0.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
@@ -56,6 +56,7 @@
|
||||
"bs58": "^4.0.1",
|
||||
"content-type": "^1.0.4",
|
||||
"loglevel": "^1.7.1",
|
||||
"matrix-events-sdk": "^0.0.1-beta.7",
|
||||
"p-retry": "^4.5.0",
|
||||
"qs": "^6.9.6",
|
||||
"request": "^2.88.2",
|
||||
@@ -88,7 +89,7 @@
|
||||
"better-docs": "^2.4.0-beta.9",
|
||||
"browserify": "^17.0.0",
|
||||
"docdash": "^1.2.0",
|
||||
"eslint": "7.18.0",
|
||||
"eslint": "8.9.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-matrix-org": "^0.4.0",
|
||||
@@ -112,7 +113,8 @@
|
||||
"<rootDir>/src/**/*.{js,ts}"
|
||||
],
|
||||
"coverageReporters": [
|
||||
"text"
|
||||
"text",
|
||||
"json"
|
||||
]
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
|
||||
@@ -348,7 +348,7 @@ function recvMessage(httpBackend, client, sender, message) {
|
||||
return testUtils.awaitDecryption(event);
|
||||
}).then((event) => {
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent()).toEqual({
|
||||
expect(event.getContent()).toMatchObject({
|
||||
msgtype: "m.text",
|
||||
body: "Hello, World",
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as utils from "../test-utils";
|
||||
import { CRYPTO_ENABLED } from "../../src/client";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { Filter, MemoryStore, Room } from "../../src/matrix";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
@@ -392,6 +393,414 @@ describe("MatrixClient", function() {
|
||||
return prom;
|
||||
});
|
||||
});
|
||||
|
||||
describe("partitionThreadedEvents", function() {
|
||||
it("returns empty arrays when given an empty arrays", function() {
|
||||
const events = [];
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
expect(timeline).toEqual([]);
|
||||
expect(threaded).toEqual([]);
|
||||
});
|
||||
|
||||
it("copies pre-thread in-timeline vote events onto both timelines", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
|
||||
const eventMessageInThread = buildEventMessageInThread();
|
||||
const eventPollResponseReference = buildEventPollResponseReference();
|
||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||
|
||||
const events = [
|
||||
eventMessageInThread,
|
||||
eventPollResponseReference,
|
||||
eventPollStartThreadRoot,
|
||||
];
|
||||
// Vote has no threadId yet
|
||||
expect(eventPollResponseReference.threadId).toBeFalsy();
|
||||
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
|
||||
expect(timeline).toEqual([
|
||||
// The message that was sent in a thread is missing
|
||||
eventPollResponseReference,
|
||||
eventPollStartThreadRoot,
|
||||
]);
|
||||
|
||||
// The vote event has been copied into the thread
|
||||
const eventRefWithThreadId = withThreadId(
|
||||
eventPollResponseReference, eventPollStartThreadRoot.getId());
|
||||
expect(eventRefWithThreadId.threadId).toBeTruthy();
|
||||
|
||||
expect(threaded).toEqual([
|
||||
eventMessageInThread,
|
||||
eventRefWithThreadId,
|
||||
// Thread does not see thread root
|
||||
]);
|
||||
});
|
||||
|
||||
it("copies pre-thread in-timeline reactions onto both timelines", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
|
||||
const eventMessageInThread = buildEventMessageInThread();
|
||||
const eventReaction = buildEventReaction();
|
||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||
|
||||
const events = [
|
||||
eventMessageInThread,
|
||||
eventReaction,
|
||||
eventPollStartThreadRoot,
|
||||
];
|
||||
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
|
||||
expect(timeline).toEqual([
|
||||
eventReaction,
|
||||
eventPollStartThreadRoot,
|
||||
]);
|
||||
|
||||
expect(threaded).toEqual([
|
||||
eventMessageInThread,
|
||||
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
|
||||
]);
|
||||
});
|
||||
|
||||
it("copies post-thread in-timeline vote events onto both timelines", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
|
||||
const eventPollResponseReference = buildEventPollResponseReference();
|
||||
const eventMessageInThread = buildEventMessageInThread();
|
||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||
|
||||
const events = [
|
||||
eventPollResponseReference,
|
||||
eventMessageInThread,
|
||||
eventPollStartThreadRoot,
|
||||
];
|
||||
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
|
||||
expect(timeline).toEqual([
|
||||
eventPollResponseReference,
|
||||
eventPollStartThreadRoot,
|
||||
]);
|
||||
|
||||
expect(threaded).toEqual([
|
||||
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
|
||||
eventMessageInThread,
|
||||
]);
|
||||
});
|
||||
|
||||
it("copies post-thread in-timeline reactions onto both timelines", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
|
||||
const eventReaction = buildEventReaction();
|
||||
const eventMessageInThread = buildEventMessageInThread();
|
||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||
|
||||
const events = [
|
||||
eventReaction,
|
||||
eventMessageInThread,
|
||||
eventPollStartThreadRoot,
|
||||
];
|
||||
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
|
||||
expect(timeline).toEqual([
|
||||
eventReaction,
|
||||
eventPollStartThreadRoot,
|
||||
]);
|
||||
|
||||
expect(threaded).toEqual([
|
||||
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
|
||||
eventMessageInThread,
|
||||
]);
|
||||
});
|
||||
|
||||
it("sends room state events to the main timeline only", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
// This is based on recording the events in a real room:
|
||||
|
||||
const eventMessageInThread = buildEventMessageInThread();
|
||||
const eventPollResponseReference = buildEventPollResponseReference();
|
||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||
const eventRoomName = buildEventRoomName();
|
||||
const eventEncryption = buildEventEncryption();
|
||||
const eventGuestAccess = buildEventGuestAccess();
|
||||
const eventHistoryVisibility = buildEventHistoryVisibility();
|
||||
const eventJoinRules = buildEventJoinRules();
|
||||
const eventPowerLevels = buildEventPowerLevels();
|
||||
const eventMember = buildEventMember();
|
||||
const eventCreate = buildEventCreate();
|
||||
|
||||
const events = [
|
||||
eventMessageInThread,
|
||||
eventPollResponseReference,
|
||||
eventPollStartThreadRoot,
|
||||
eventRoomName,
|
||||
eventEncryption,
|
||||
eventGuestAccess,
|
||||
eventHistoryVisibility,
|
||||
eventJoinRules,
|
||||
eventPowerLevels,
|
||||
eventMember,
|
||||
eventCreate,
|
||||
];
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
|
||||
expect(timeline).toEqual([
|
||||
// The message that was sent in a thread is missing
|
||||
eventPollResponseReference,
|
||||
eventPollStartThreadRoot,
|
||||
eventRoomName,
|
||||
eventEncryption,
|
||||
eventGuestAccess,
|
||||
eventHistoryVisibility,
|
||||
eventJoinRules,
|
||||
eventPowerLevels,
|
||||
eventMember,
|
||||
eventCreate,
|
||||
]);
|
||||
|
||||
// Thread should contain only stuff that happened in the thread -
|
||||
// no thread root, and no room state events
|
||||
expect(threaded).toEqual([
|
||||
eventMessageInThread,
|
||||
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function withThreadId(event, newThreadId) {
|
||||
const ret = event.toSnapshot();
|
||||
ret.setThreadId(newThreadId);
|
||||
return ret;
|
||||
}
|
||||
|
||||
const buildEventMessageInThread = () => new MatrixEvent({
|
||||
"age": 80098509,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
"device_id": "XISFUZSKHH",
|
||||
"m.relates_to": {
|
||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
"m.in_reply_to": {
|
||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
},
|
||||
"rel_type": "io.element.thread",
|
||||
},
|
||||
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
|
||||
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
|
||||
},
|
||||
"event_id": "$W4chKIGYowtBblVLkRimeIg8TcdjETnxhDPGfi6NpDg",
|
||||
"origin_server_ts": 1643815466378,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.room.encrypted",
|
||||
"unsigned": { "age": 80098509 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventPollResponseReference = () => new MatrixEvent({
|
||||
"age": 80098509,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
"device_id": "XISFUZSKHH",
|
||||
"m.relates_to": {
|
||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
"rel_type": "m.reference",
|
||||
},
|
||||
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
|
||||
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
|
||||
},
|
||||
"event_id": "$91JvpezvsF0cKgav3g8W-uEVS4WkDHgxbJZvL3uMR1g",
|
||||
"origin_server_ts": 1643815458650,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.room.encrypted",
|
||||
"unsigned": { "age": 80106237 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventReaction = () => new MatrixEvent({
|
||||
"content": {
|
||||
"m.relates_to": {
|
||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
"key": "🤗",
|
||||
"rel_type": "m.annotation",
|
||||
},
|
||||
},
|
||||
"origin_server_ts": 1643977249238,
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.reaction",
|
||||
"unsigned": {
|
||||
"age": 22598,
|
||||
"transaction_id": "m1643977249073.16",
|
||||
},
|
||||
"event_id": "$86B2b-x3LgE4DlV4y24b7UHnt72LIA3rzjvMysTtAfA",
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventPollStartThreadRoot = () => new MatrixEvent({
|
||||
"age": 80108647,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
"device_id": "XISFUZSKHH",
|
||||
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
|
||||
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
|
||||
},
|
||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
"origin_server_ts": 1643815456240,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.room.encrypted",
|
||||
"unsigned": { "age": 80108647 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventRoomName = () => new MatrixEvent({
|
||||
"age": 80123249,
|
||||
"content": {
|
||||
"name": "1 poll, 1 vote, 1 thread",
|
||||
},
|
||||
"event_id": "$QAdyNJtKnl1j7or2yMycbOCvb6bCgvHs5lg3ZMd5xWk",
|
||||
"origin_server_ts": 1643815441638,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.name",
|
||||
"unsigned": { "age": 80123249 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventEncryption = () => new MatrixEvent({
|
||||
"age": 80123383,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
},
|
||||
"event_id": "$1hGykogKQkXbHw8bVuyE3BjHnFBEJBcUWnakd0ck2K0",
|
||||
"origin_server_ts": 1643815441504,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.encryption",
|
||||
"unsigned": { "age": 80123383 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventGuestAccess = () => new MatrixEvent({
|
||||
"age": 80123473,
|
||||
"content": {
|
||||
"guest_access": "can_join",
|
||||
},
|
||||
"event_id": "$4_2n-H6K9-0nPbnjjtIue2SU44tGJsnuTmi6UuSrh-U",
|
||||
"origin_server_ts": 1643815441414,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.guest_access",
|
||||
"unsigned": { "age": 80123473 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventHistoryVisibility = () => new MatrixEvent({
|
||||
"age": 80123556,
|
||||
"content": {
|
||||
"history_visibility": "shared",
|
||||
},
|
||||
"event_id": "$W6kp44CTnvciOiHSPyhp8dh4n2v1_9kclUPddeaQj0E",
|
||||
"origin_server_ts": 1643815441331,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.history_visibility",
|
||||
"unsigned": { "age": 80123556 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventJoinRules = () => new MatrixEvent({
|
||||
"age": 80123696,
|
||||
"content": {
|
||||
"join_rule": "invite",
|
||||
},
|
||||
"event_id": "$6JDDeDp7fEc0F6YnTWMruNcKWFltR3e9wk7wWDDJrAU",
|
||||
"origin_server_ts": 1643815441191,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.join_rules",
|
||||
"unsigned": { "age": 80123696 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventPowerLevels = () => new MatrixEvent({
|
||||
"age": 80124105,
|
||||
"content": {
|
||||
"ban": 50,
|
||||
"events": {
|
||||
"m.room.avatar": 50,
|
||||
"m.room.canonical_alias": 50,
|
||||
"m.room.encryption": 100,
|
||||
"m.room.history_visibility": 100,
|
||||
"m.room.name": 50,
|
||||
"m.room.power_levels": 100,
|
||||
"m.room.server_acl": 100,
|
||||
"m.room.tombstone": 100,
|
||||
},
|
||||
"events_default": 0,
|
||||
"historical": 100,
|
||||
"invite": 0,
|
||||
"kick": 50,
|
||||
"redact": 50,
|
||||
"state_default": 50,
|
||||
"users": {
|
||||
"@andybalaam-test1:matrix.org": 100,
|
||||
},
|
||||
"users_default": 0,
|
||||
},
|
||||
"event_id": "$XZY2YgQhXskpc7gmJJG3S0VmS9_QjjCUVeeFTfgfC2E",
|
||||
"origin_server_ts": 1643815440782,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.power_levels",
|
||||
"unsigned": { "age": 80124105 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventMember = () => new MatrixEvent({
|
||||
"age": 80125279,
|
||||
"content": {
|
||||
"avatar_url": "mxc://matrix.org/aNtbVcFfwotudypZcHsIcPOc",
|
||||
"displayname": "andybalaam-test1",
|
||||
"membership": "join",
|
||||
},
|
||||
"event_id": "$Ex5eVmMs_ti784mo8bgddynbwLvy6231lCycJr7Cl9M",
|
||||
"origin_server_ts": 1643815439608,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": { "age": 80125279 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventCreate = () => new MatrixEvent({
|
||||
"age": 80126105,
|
||||
"content": {
|
||||
"creator": "@andybalaam-test1:matrix.org",
|
||||
"room_version": "6",
|
||||
},
|
||||
"event_id": "$e7j2Gt37k5NPwB6lz2N3V9lO5pUdNK8Ai7i2FPEK-oI",
|
||||
"origin_server_ts": 1643815438782,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.create",
|
||||
"unsigned": { "age": 80126105 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
function assertObjectContains(obj, expected) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EventStatus } from "../../src/matrix";
|
||||
import { EventStatus, RoomEvent } from "../../src/matrix";
|
||||
import { MatrixScheduler } from "../../src/scheduler";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { TestClient } from "../TestClient";
|
||||
@@ -95,7 +95,7 @@ describe("MatrixClient retrying", function() {
|
||||
|
||||
// wait for the localecho of ev1 to be updated
|
||||
const p3 = new Promise<void>((resolve, reject) => {
|
||||
room.on("Room.localEchoUpdated", (ev0) => {
|
||||
room.on(RoomEvent.LocalEchoUpdated, (ev0) => {
|
||||
if (ev0 === ev1) {
|
||||
resolve();
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ export function mkEvent(opts) {
|
||||
room_id: opts.room,
|
||||
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
||||
content: opts.content,
|
||||
unsigned: opts.unsigned,
|
||||
event_id: "$" + Math.random() + "-" + Math.random(),
|
||||
};
|
||||
if (opts.skey !== undefined) {
|
||||
@@ -365,3 +366,5 @@ export function setHttpResponses(
|
||||
.respond(200, response.data);
|
||||
});
|
||||
}
|
||||
|
||||
export const emitPromise = (e, k) => new Promise(r => e.once(k, r));
|
||||
|
||||
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { ReEmitter } from "../../src/ReEmitter";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import '../olm-loader';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { Crypto } from "../../src/crypto";
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function resetCrossSigningKeys(client, {
|
||||
crypto.crossSigningInfo.keys = oldKeys;
|
||||
throw e;
|
||||
}
|
||||
crypto.baseApis.emit("crossSigning.keysChanged", {});
|
||||
crypto.emit("crossSigning.keysChanged", {});
|
||||
await crypto.afterCrossSigningLocalKeyChange();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { VerificationBase } from '../../../../src/crypto/verification/Base';
|
||||
import { CrossSigningInfo } from '../../../../src/crypto/CrossSigning';
|
||||
import { encodeBase64 } from "../../../../src/crypto/olmlib";
|
||||
import { setupWebcrypto, teardownWebcrypto } from './util';
|
||||
import { VerificationBase } from '../../../../src/crypto/verification/Base';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { FilterComponent } from "../../src/filter-component";
|
||||
import { mkEvent } from '../test-utils';
|
||||
|
||||
describe("Filter Component", function() {
|
||||
describe("types", function() {
|
||||
it("should filter out events with other types", function() {
|
||||
const filter = new FilterComponent({ types: ['m.room.message'] });
|
||||
const event = mkEvent({
|
||||
type: 'm.room.member',
|
||||
content: { },
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
});
|
||||
|
||||
const checkResult = filter.check(event);
|
||||
|
||||
expect(checkResult).toBe(false);
|
||||
});
|
||||
|
||||
it("should validate events with the same type", function() {
|
||||
const filter = new FilterComponent({ types: ['m.room.message'] });
|
||||
const event = mkEvent({
|
||||
type: 'm.room.message',
|
||||
content: { },
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
});
|
||||
|
||||
const checkResult = filter.check(event);
|
||||
|
||||
expect(checkResult).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
import {
|
||||
RelationType,
|
||||
UNSTABLE_FILTER_RELATED_BY_REL_TYPES,
|
||||
UNSTABLE_FILTER_RELATED_BY_SENDERS,
|
||||
} from "../../src";
|
||||
import { FilterComponent } from "../../src/filter-component";
|
||||
import { mkEvent } from '../test-utils';
|
||||
|
||||
describe("Filter Component", function() {
|
||||
describe("types", function() {
|
||||
it("should filter out events with other types", function() {
|
||||
const filter = new FilterComponent({ types: ['m.room.message'] });
|
||||
const event = mkEvent({
|
||||
type: 'm.room.member',
|
||||
content: { },
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
});
|
||||
|
||||
const checkResult = filter.check(event);
|
||||
|
||||
expect(checkResult).toBe(false);
|
||||
});
|
||||
|
||||
it("should validate events with the same type", function() {
|
||||
const filter = new FilterComponent({ types: ['m.room.message'] });
|
||||
const event = mkEvent({
|
||||
type: 'm.room.message',
|
||||
content: { },
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
});
|
||||
|
||||
const checkResult = filter.check(event);
|
||||
|
||||
expect(checkResult).toBe(true);
|
||||
});
|
||||
|
||||
it("should filter out events by relation participation", function() {
|
||||
const currentUserId = '@me:server.org';
|
||||
const filter = new FilterComponent({
|
||||
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]: [currentUserId],
|
||||
}, currentUserId);
|
||||
|
||||
const threadRootNotParticipated = mkEvent({
|
||||
type: 'm.room.message',
|
||||
content: {},
|
||||
room: 'roomId',
|
||||
user: '@someone-else:server.org',
|
||||
event: true,
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
[RelationType.Thread]: {
|
||||
count: 2,
|
||||
current_user_participated: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(filter.check(threadRootNotParticipated)).toBe(false);
|
||||
});
|
||||
|
||||
it("should keep events by relation participation", function() {
|
||||
const currentUserId = '@me:server.org';
|
||||
const filter = new FilterComponent({
|
||||
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]: [currentUserId],
|
||||
}, currentUserId);
|
||||
|
||||
const threadRootParticipated = mkEvent({
|
||||
type: 'm.room.message',
|
||||
content: {},
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
[RelationType.Thread]: {
|
||||
count: 2,
|
||||
current_user_participated: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
user: '@someone-else:server.org',
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
});
|
||||
|
||||
expect(filter.check(threadRootParticipated)).toBe(true);
|
||||
});
|
||||
|
||||
it("should filter out events by relation type", function() {
|
||||
const filter = new FilterComponent({
|
||||
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]: [RelationType.Thread],
|
||||
});
|
||||
|
||||
const referenceRelationEvent = mkEvent({
|
||||
type: 'm.room.message',
|
||||
content: {},
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
[RelationType.Reference]: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(filter.check(referenceRelationEvent)).toBe(false);
|
||||
});
|
||||
|
||||
it("should keep events by relation type", function() {
|
||||
const filter = new FilterComponent({
|
||||
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]: [RelationType.Thread],
|
||||
});
|
||||
|
||||
const threadRootEvent = mkEvent({
|
||||
type: 'm.room.message',
|
||||
content: {},
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
[RelationType.Thread]: {
|
||||
count: 2,
|
||||
current_user_participated: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
});
|
||||
|
||||
expect(filter.check(threadRootEvent)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
Copyright 2022 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.
|
||||
*/
|
||||
|
||||
import { makeLocationContent } from "../../src/content-helpers";
|
||||
import {
|
||||
ASSET_NODE_TYPE,
|
||||
LocationAssetType,
|
||||
LOCATION_EVENT_TYPE,
|
||||
TIMESTAMP_NODE_TYPE,
|
||||
} from "../../src/@types/location";
|
||||
import { TEXT_NODE_TYPE } from "../../src/@types/extensible_events";
|
||||
|
||||
describe("Location", function() {
|
||||
it("should create a valid location with defaults", function() {
|
||||
const loc = makeLocationContent("txt", "geo:foo", 134235435);
|
||||
expect(loc.body).toEqual("txt");
|
||||
expect(loc.msgtype).toEqual("m.location");
|
||||
expect(loc.geo_uri).toEqual("geo:foo");
|
||||
expect(LOCATION_EVENT_TYPE.findIn(loc)).toEqual({
|
||||
uri: "geo:foo",
|
||||
description: undefined,
|
||||
});
|
||||
expect(ASSET_NODE_TYPE.findIn(loc)).toEqual({ type: LocationAssetType.Self });
|
||||
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual("txt");
|
||||
expect(TIMESTAMP_NODE_TYPE.findIn(loc)).toEqual(134235435);
|
||||
});
|
||||
|
||||
it("should create a valid location with explicit properties", function() {
|
||||
const loc = makeLocationContent(
|
||||
"txxt", "geo:bar", 134235436, "desc", LocationAssetType.Pin);
|
||||
|
||||
expect(loc.body).toEqual("txxt");
|
||||
expect(loc.msgtype).toEqual("m.location");
|
||||
expect(loc.geo_uri).toEqual("geo:bar");
|
||||
expect(LOCATION_EVENT_TYPE.findIn(loc)).toEqual({
|
||||
uri: "geo:bar",
|
||||
description: "desc",
|
||||
});
|
||||
expect(ASSET_NODE_TYPE.findIn(loc)).toEqual({ type: LocationAssetType.Pin });
|
||||
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual("txxt");
|
||||
expect(TIMESTAMP_NODE_TYPE.findIn(loc)).toEqual(134235436);
|
||||
});
|
||||
});
|
||||
@@ -11,8 +11,9 @@ import {
|
||||
UNSTABLE_MSC3089_TREE_SUBTYPE,
|
||||
} from "../../src/@types/event";
|
||||
import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { EventStatus, MatrixEvent } from "../../src/models/event";
|
||||
import { Preset } from "../../src/@types/partials";
|
||||
import * as testUtils from "../test-utils";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -90,11 +91,7 @@ describe("MatrixClient", function() {
|
||||
return pendingLookup.promise;
|
||||
}
|
||||
// >1 pending thing, and they are different, whine.
|
||||
expect(false).toBe(
|
||||
true, ">1 pending request. You should probably handle them. " +
|
||||
"PENDING: " + JSON.stringify(pendingLookup) + " JUST GOT: " +
|
||||
method + " " + path,
|
||||
);
|
||||
expect(false).toBe(true);
|
||||
}
|
||||
pendingLookup = {
|
||||
promise: new Promise(() => {}),
|
||||
@@ -122,6 +119,7 @@ describe("MatrixClient", function() {
|
||||
}
|
||||
|
||||
if (next.error) {
|
||||
// eslint-disable-next-line
|
||||
return Promise.reject({
|
||||
errcode: next.error.errcode,
|
||||
httpStatus: next.error.httpStatus,
|
||||
@@ -132,7 +130,7 @@ describe("MatrixClient", function() {
|
||||
}
|
||||
return Promise.resolve(next.data);
|
||||
}
|
||||
expect(true).toBe(false, "Expected different request. " + logLine);
|
||||
expect(true).toBe(false);
|
||||
return new Promise(() => {});
|
||||
}
|
||||
|
||||
@@ -157,7 +155,7 @@ describe("MatrixClient", function() {
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: identityServerUrl,
|
||||
accessToken: "my.access.token",
|
||||
request: function() {}, // NOP
|
||||
request: function() {} as any, // NOP
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: userId,
|
||||
@@ -378,10 +376,10 @@ describe("MatrixClient", function() {
|
||||
];
|
||||
const filterId = "ehfewf";
|
||||
store.getFilterIdByName.mockReturnValue(filterId);
|
||||
const filter = new Filter(0, filterId);
|
||||
const filter = new Filter("0", filterId);
|
||||
filter.setDefinition({ "room": { "timeline": { "limit": 8 } } });
|
||||
store.getFilter.mockReturnValue(filter);
|
||||
const syncPromise = new Promise((resolve, reject) => {
|
||||
const syncPromise = new Promise<void>((resolve, reject) => {
|
||||
client.on("sync", function syncListener(state) {
|
||||
if (state === "SYNCING") {
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
@@ -402,7 +400,7 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
it("should return the same sync state as emitted sync events", async function() {
|
||||
const syncingPromise = new Promise((resolve) => {
|
||||
const syncingPromise = new Promise<void>((resolve) => {
|
||||
client.on("sync", function syncListener(state) {
|
||||
expect(state).toEqual(client.getSyncState());
|
||||
if (state === "SYNCING") {
|
||||
@@ -422,7 +420,7 @@ describe("MatrixClient", function() {
|
||||
it("should use an existing filter if id is present in localStorage", function() {
|
||||
});
|
||||
it("should handle localStorage filterId missing from the server", function(done) {
|
||||
function getFilterName(userId, suffix) {
|
||||
function getFilterName(userId, suffix?: string) {
|
||||
// scope this on the user ID because people may login on many accounts
|
||||
// and they all need to be stored!
|
||||
return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : "");
|
||||
@@ -500,7 +498,7 @@ describe("MatrixClient", function() {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(1);
|
||||
expect(client.retryImmediately()).toBe(
|
||||
true, "retryImmediately returned false",
|
||||
true,
|
||||
);
|
||||
jest.advanceTimersByTime(1);
|
||||
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
|
||||
@@ -583,33 +581,33 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
it("should transition ERROR -> CATCHUP after /sync if prev failed",
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
acceptKeepalives = false;
|
||||
httpLookups = [];
|
||||
httpLookups.push(CAPABILITIES_RESPONSE);
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH,
|
||||
error: { errcode: "KEEPALIVE_FAIL" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH, data: {},
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", data: SYNC_DATA,
|
||||
});
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
acceptKeepalives = false;
|
||||
httpLookups = [];
|
||||
httpLookups.push(CAPABILITIES_RESPONSE);
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH,
|
||||
error: { errcode: "KEEPALIVE_FAIL" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH, data: {},
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", data: SYNC_DATA,
|
||||
});
|
||||
|
||||
expectedStates.push(["RECONNECTING", null]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["CATCHUP", "ERROR"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
expectedStates.push(["RECONNECTING", null]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["CATCHUP", "ERROR"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition PREPARED -> SYNCING after /sync", function(done) {
|
||||
const expectedStates = [];
|
||||
@@ -639,32 +637,32 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
xit("should transition ERROR -> SYNCING after /sync if prev failed",
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
|
||||
});
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
|
||||
});
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["ERROR", "SYNCING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["ERROR", "SYNCING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition SYNCING -> SYNCING on subsequent /sync successes",
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["SYNCING", "SYNCING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["SYNCING", "SYNCING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition ERROR -> ERROR if keepalive keeps failing", function(done) {
|
||||
acceptKeepalives = false;
|
||||
@@ -713,6 +711,7 @@ describe("MatrixClient", function() {
|
||||
describe("guest rooms", function() {
|
||||
it("should only do /sync calls (without filter/pushrules)", function(done) {
|
||||
httpLookups = []; // no /pushrules or /filterw
|
||||
httpLookups.push(CAPABILITIES_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "GET",
|
||||
path: "/sync",
|
||||
@@ -867,4 +866,118 @@ describe("MatrixClient", function() {
|
||||
await client.redactEvent(roomId, eventId, txnId, { reason });
|
||||
});
|
||||
});
|
||||
|
||||
describe("cancelPendingEvent", () => {
|
||||
const roomId = "!room:server";
|
||||
const txnId = "m12345";
|
||||
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "join",
|
||||
updatePendingEvent: (event, status) => event.setStatus(status),
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[RoomCreateTypeField]: RoomType.Space,
|
||||
},
|
||||
});
|
||||
} else if (eventType === EventType.RoomEncryption) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({ content: {} });
|
||||
} else {
|
||||
throw new Error("Unexpected event type or state key");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let event;
|
||||
beforeEach(async () => {
|
||||
event = new MatrixEvent({
|
||||
event_id: "~" + roomId + ":" + txnId,
|
||||
user_id: client.credentials.userId,
|
||||
sender: client.credentials.userId,
|
||||
room_id: roomId,
|
||||
origin_server_ts: new Date().getTime(),
|
||||
});
|
||||
event.setTxnId(txnId);
|
||||
|
||||
client.getRoom = (getRoomId) => {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
return mockRoom;
|
||||
};
|
||||
client.crypto = { // mock crypto
|
||||
encryptEvent: (event, room) => new Promise(() => {}),
|
||||
};
|
||||
});
|
||||
|
||||
function assertCancelled() {
|
||||
expect(event.status).toBe(EventStatus.CANCELLED);
|
||||
expect(client.scheduler.removeEventFromQueue(event)).toBeFalsy();
|
||||
expect(httpLookups.filter(h => h.path.includes("/send/")).length).toBe(0);
|
||||
}
|
||||
|
||||
it("should cancel an event which is queued", () => {
|
||||
event.setStatus(EventStatus.QUEUED);
|
||||
client.scheduler.queueEvent(event);
|
||||
client.cancelPendingEvent(event);
|
||||
assertCancelled();
|
||||
});
|
||||
|
||||
it("should cancel an event which is encrypting", async () => {
|
||||
client.encryptAndSendEvent(null, event);
|
||||
await testUtils.emitPromise(event, "Event.status");
|
||||
client.cancelPendingEvent(event);
|
||||
assertCancelled();
|
||||
});
|
||||
|
||||
it("should cancel an event which is not sent", () => {
|
||||
event.setStatus(EventStatus.NOT_SENT);
|
||||
client.cancelPendingEvent(event);
|
||||
assertCancelled();
|
||||
});
|
||||
|
||||
it("should error when given any other event status", () => {
|
||||
event.setStatus(EventStatus.SENDING);
|
||||
expect(() => client.cancelPendingEvent(event)).toThrow("cannot cancel an event with status sending");
|
||||
expect(event.status).toBe(EventStatus.SENDING);
|
||||
});
|
||||
});
|
||||
|
||||
describe("threads", () => {
|
||||
it("partitions root events to room timeline and thread timeline", () => {
|
||||
const supportsExperimentalThreads = client.supportsExperimentalThreads;
|
||||
client.supportsExperimentalThreads = () => true;
|
||||
|
||||
const rootEvent = new MatrixEvent({
|
||||
"content": {},
|
||||
"origin_server_ts": 1,
|
||||
"room_id": "!room1:matrix.org",
|
||||
"sender": "@alice:matrix.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"m.relations": {
|
||||
"io.element.thread": {
|
||||
"latest_event": {},
|
||||
"count": 33,
|
||||
"current_user_participated": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
"event_id": "$ev1",
|
||||
"user_id": "@alice:matrix.org",
|
||||
});
|
||||
|
||||
expect(rootEvent.isThreadRoot).toBe(true);
|
||||
|
||||
const [room, threads] = client.partitionThreadedEvents([rootEvent]);
|
||||
expect(room).toHaveLength(1);
|
||||
expect(threads).toHaveLength(1);
|
||||
|
||||
// Restore method
|
||||
client.supportsExperimentalThreads = supportsExperimentalThreads;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -302,4 +302,20 @@ describe('NotificationService', function() {
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(false);
|
||||
});
|
||||
|
||||
it("a rule with no conditions matches every event.", function() {
|
||||
expect(pushProcessor.ruleMatchesEvent({
|
||||
rule_id: "rule1",
|
||||
actions: [],
|
||||
conditions: [],
|
||||
default: false,
|
||||
enabled: true,
|
||||
}, testEvent)).toBe(true);
|
||||
expect(pushProcessor.ruleMatchesEvent({
|
||||
rule_id: "rule1",
|
||||
actions: [],
|
||||
default: false,
|
||||
enabled: true,
|
||||
}, testEvent)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventTimelineSet } from "../../src/models/event-timeline-set";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { Relations } from "../../src/models/relations";
|
||||
|
||||
@@ -103,7 +103,7 @@ describe("Relations", function() {
|
||||
// Add the target event first, then the relation event
|
||||
{
|
||||
const relationsCreated = new Promise(resolve => {
|
||||
targetEvent.once("Event.relationsCreated", resolve);
|
||||
targetEvent.once(MatrixEventEvent.RelationsCreated, resolve);
|
||||
});
|
||||
|
||||
const timelineSet = new EventTimelineSet(room, {
|
||||
@@ -118,7 +118,7 @@ describe("Relations", function() {
|
||||
// Add the relation event first, then the target event
|
||||
{
|
||||
const relationsCreated = new Promise(resolve => {
|
||||
targetEvent.once("Event.relationsCreated", resolve);
|
||||
targetEvent.once(MatrixEventEvent.RelationsCreated, resolve);
|
||||
});
|
||||
|
||||
const timelineSet = new EventTimelineSet(room, {
|
||||
|
||||
@@ -120,7 +120,7 @@ describe("RoomState", function() {
|
||||
it("should return a single MatrixEvent if a state_key was specified",
|
||||
function() {
|
||||
const event = state.getStateEvents("m.room.member", userA);
|
||||
expect(event.getContent()).toEqual({
|
||||
expect(event.getContent()).toMatchObject({
|
||||
membership: "join",
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -82,17 +82,34 @@ class MockRTCPeerConnection {
|
||||
}
|
||||
close() {}
|
||||
getStats() { return []; }
|
||||
addTrack(track: MockMediaStreamTrack) {return new MockRTCRtpSender(track);}
|
||||
}
|
||||
|
||||
class MockRTCRtpSender {
|
||||
constructor(public track: MockMediaStreamTrack) {}
|
||||
|
||||
replaceTrack(track: MockMediaStreamTrack) {this.track = track;}
|
||||
}
|
||||
|
||||
class MockMediaStreamTrack {
|
||||
constructor(public readonly id: string, public readonly kind: "audio" | "video", public enabled = true) {}
|
||||
|
||||
stop() {}
|
||||
}
|
||||
|
||||
class MockMediaStream {
|
||||
constructor(
|
||||
public id: string,
|
||||
private tracks: MockMediaStreamTrack[] = [],
|
||||
) {}
|
||||
|
||||
getTracks() { return []; }
|
||||
getAudioTracks() { return [{ enabled: true }]; }
|
||||
getVideoTracks() { return [{ enabled: true }]; }
|
||||
getTracks() { return this.tracks; }
|
||||
getAudioTracks() { return this.tracks.filter((track) => track.kind === "audio"); }
|
||||
getVideoTracks() { return this.tracks.filter((track) => track.kind === "video"); }
|
||||
addEventListener() {}
|
||||
removeEventListener() { }
|
||||
addTrack(track: MockMediaStreamTrack) {this.tracks.push(track);}
|
||||
removeTrack(track: MockMediaStreamTrack) {this.tracks.splice(this.tracks.indexOf(track), 1);}
|
||||
}
|
||||
|
||||
class MockMediaDeviceInfo {
|
||||
@@ -102,7 +119,13 @@ class MockMediaDeviceInfo {
|
||||
}
|
||||
|
||||
class MockMediaHandler {
|
||||
getUserMediaStream() { return new MockMediaStream("mock_stream_from_media_handler"); }
|
||||
getUserMediaStream(audio: boolean, video: boolean) {
|
||||
const tracks = [];
|
||||
if (audio) tracks.push(new MockMediaStreamTrack("audio_track", "audio"));
|
||||
if (video) tracks.push(new MockMediaStreamTrack("video_track", "video"));
|
||||
|
||||
return new MockMediaStream("mock_stream_from_media_handler", tracks);
|
||||
}
|
||||
stopUserMediaStream() {}
|
||||
}
|
||||
|
||||
@@ -350,7 +373,15 @@ describe('Call', function() {
|
||||
},
|
||||
});
|
||||
|
||||
call.pushRemoteFeed(new MockMediaStream("remote_stream"));
|
||||
call.pushRemoteFeed(
|
||||
new MockMediaStream(
|
||||
"remote_stream",
|
||||
[
|
||||
new MockMediaStreamTrack("remote_audio_track", "audio"),
|
||||
new MockMediaStreamTrack("remote_video_track", "video"),
|
||||
],
|
||||
),
|
||||
);
|
||||
const feed = call.getFeeds().find((feed) => feed.stream.id === "remote_stream");
|
||||
expect(feed?.purpose).toBe(SDPStreamMetadataPurpose.Usermedia);
|
||||
expect(feed?.isAudioMuted()).toBeTruthy();
|
||||
@@ -396,4 +427,82 @@ describe('Call', function() {
|
||||
expect(client.client.mediaHandler.getUserMediaStream).toHaveBeenNthCalledWith(1, true, true);
|
||||
expect(client.client.mediaHandler.getUserMediaStream).toHaveBeenNthCalledWith(2, true, false);
|
||||
});
|
||||
|
||||
it("should handle mid-call device changes", async () => {
|
||||
client.client.mediaHandler.getUserMediaStream = jest.fn().mockReturnValue(
|
||||
new MockMediaStream(
|
||||
"stream", [
|
||||
new MockMediaStreamTrack("audio_track", "audio"),
|
||||
new MockMediaStreamTrack("video_track", "video"),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
const callPromise = call.placeVideoCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'party_id',
|
||||
answer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
await call.updateLocalUsermediaStream(
|
||||
new MockMediaStream(
|
||||
"replacement_stream",
|
||||
[
|
||||
new MockMediaStreamTrack("new_audio_track", "audio"),
|
||||
new MockMediaStreamTrack("video_track", "video"),
|
||||
],
|
||||
),
|
||||
);
|
||||
expect(call.localUsermediaStream.id).toBe("stream");
|
||||
expect(call.localUsermediaStream.getAudioTracks()[0].id).toBe("new_audio_track");
|
||||
expect(call.localUsermediaStream.getVideoTracks()[0].id).toBe("video_track");
|
||||
expect(call.usermediaSenders.find((sender) => {
|
||||
return sender?.track?.kind === "audio";
|
||||
}).track.id).toBe("new_audio_track");
|
||||
expect(call.usermediaSenders.find((sender) => {
|
||||
return sender?.track?.kind === "video";
|
||||
}).track.id).toBe("video_track");
|
||||
});
|
||||
|
||||
it("should handle upgrade to video call", async () => {
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'party_id',
|
||||
answer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
[SDPStreamMetadataKey]: {},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
await call.upgradeCall(false, true);
|
||||
|
||||
expect(call.localUsermediaStream.getAudioTracks()[0].id).toBe("audio_track");
|
||||
expect(call.localUsermediaStream.getVideoTracks()[0].id).toBe("video_track");
|
||||
expect(call.usermediaSenders.find((sender) => {
|
||||
return sender?.track?.kind === "audio";
|
||||
}).track.id).toBe("audio_track");
|
||||
expect(call.usermediaSenders.find((sender) => {
|
||||
return sender?.track?.kind === "video";
|
||||
}).track.id).toBe("video_track");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
Copyright 2022 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.
|
||||
*/
|
||||
|
||||
// disable lint because these are wire responses
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
/**
|
||||
* Represents a response to the CSAPI `/refresh` endpoint.
|
||||
*/
|
||||
export interface IRefreshTokenResponse {
|
||||
access_token: string;
|
||||
expires_in_ms: number;
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
/* eslint-enable camelcase */
|
||||
@@ -92,6 +92,7 @@ export enum EventType {
|
||||
export enum RelationType {
|
||||
Annotation = "m.annotation",
|
||||
Replace = "m.replace",
|
||||
Reference = "m.reference",
|
||||
/**
|
||||
* Note, "io.element.thread" is hardcoded
|
||||
* Should be replaced with "m.thread" once MSC3440 lands
|
||||
@@ -177,6 +178,16 @@ export const UNSTABLE_ELEMENT_FUNCTIONAL_USERS = new UnstableValue(
|
||||
"io.element.functional_members",
|
||||
"io.element.functional_members");
|
||||
|
||||
/**
|
||||
* A type of message that affects visibility of a message,
|
||||
* as per https://github.com/matrix-org/matrix-doc/pull/3531
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export const EVENT_VISIBILITY_CHANGE_TYPE = new UnstableValue(
|
||||
"m.visibility",
|
||||
"org.matrix.msc3531.visibility");
|
||||
|
||||
export interface IEncryptedFile {
|
||||
url: string;
|
||||
mimetype?: string;
|
||||
|
||||
@@ -23,8 +23,15 @@ import { TEXT_NODE_TYPE } from "./extensible_events";
|
||||
export const LOCATION_EVENT_TYPE = new UnstableValue(
|
||||
"m.location", "org.matrix.msc3488.location");
|
||||
|
||||
export const ASSET_NODE_TYPE = new UnstableValue("m.asset", "org.matrix.msc3488.asset");
|
||||
|
||||
export const TIMESTAMP_NODE_TYPE = new UnstableValue("m.ts", "org.matrix.msc3488.ts");
|
||||
|
||||
export enum LocationAssetType {
|
||||
Self = "m.self",
|
||||
Pin = "m.pin",
|
||||
}
|
||||
|
||||
/* From the spec at:
|
||||
* https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md
|
||||
{
|
||||
@@ -37,6 +44,9 @@ export const TIMESTAMP_NODE_TYPE = new UnstableValue("m.ts", "org.matrix.msc3488
|
||||
"uri": "geo:51.5008,0.1247;u=35",
|
||||
"description": "Matthew's whereabouts",
|
||||
},
|
||||
"m.asset": {
|
||||
"type": "m.self"
|
||||
},
|
||||
"m.text": "Matthew was at geo:51.5008,0.1247;u=35 as of Sat Nov 13 18:50:58 2021",
|
||||
"m.ts": 1636829458432,
|
||||
}
|
||||
@@ -52,6 +62,9 @@ export interface ILocationContent extends IContent {
|
||||
uri: string;
|
||||
description?: string;
|
||||
};
|
||||
[ASSET_NODE_TYPE.name]: {
|
||||
type: LocationAssetType;
|
||||
};
|
||||
[TEXT_NODE_TYPE.name]: string;
|
||||
[TIMESTAMP_NODE_TYPE.name]: number;
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
import { IContent } from "../models/event";
|
||||
import { TEXT_NODE_TYPE } from "./extensible_events";
|
||||
|
||||
export const POLL_START_EVENT_TYPE = new UnstableValue(
|
||||
"m.poll.start", "org.matrix.msc3381.poll.start");
|
||||
|
||||
export const POLL_RESPONSE_EVENT_TYPE = new UnstableValue(
|
||||
"m.poll.response", "org.matrix.msc3381.poll.response");
|
||||
|
||||
export const POLL_END_EVENT_TYPE = new UnstableValue(
|
||||
"m.poll.end", "org.matrix.msc3381.poll.end");
|
||||
|
||||
export const POLL_KIND_DISCLOSED = new UnstableValue(
|
||||
"m.poll.disclosed", "org.matrix.msc3381.poll.disclosed");
|
||||
|
||||
export const POLL_KIND_UNDISCLOSED = new UnstableValue(
|
||||
"m.poll.undisclosed", "org.matrix.msc3381.poll.undisclosed");
|
||||
|
||||
export interface IPollAnswer extends IContent {
|
||||
id: string;
|
||||
[TEXT_NODE_TYPE.name]: string;
|
||||
}
|
||||
|
||||
export interface IPollContent extends IContent {
|
||||
[POLL_START_EVENT_TYPE.name]: {
|
||||
kind: string; // disclosed or undisclosed (untypeable for now)
|
||||
question: {
|
||||
[TEXT_NODE_TYPE.name]: string;
|
||||
};
|
||||
answers: IPollAnswer[];
|
||||
};
|
||||
[TEXT_NODE_TYPE.name]: string;
|
||||
}
|
||||
|
||||
export interface IPollResponseContent extends IContent {
|
||||
[POLL_RESPONSE_EVENT_TYPE.name]: {
|
||||
answers: string[];
|
||||
};
|
||||
"m.relates_to": {
|
||||
"event_id": string;
|
||||
"rel_type": string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IPollEndContent extends IContent {
|
||||
[POLL_END_EVENT_TYPE.name]: {};
|
||||
"m.relates_to": {
|
||||
"event_id": string;
|
||||
"rel_type": string;
|
||||
};
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { IContent, IEvent } from "../models/event";
|
||||
import { Preset, Visibility } from "./partials";
|
||||
import { SearchKey } from "./search";
|
||||
import { IRoomEventFilter } from "../filter";
|
||||
import { Direction } from "../models/event-timeline";
|
||||
|
||||
// allow camelcase as these are things that go onto the wire
|
||||
/* eslint-disable camelcase */
|
||||
@@ -144,6 +145,7 @@ export interface IRelationsRequestOpts {
|
||||
from?: string;
|
||||
to?: string;
|
||||
limit?: number;
|
||||
direction?: Direction;
|
||||
}
|
||||
|
||||
export interface IRelationsResponse {
|
||||
|
||||
@@ -21,28 +21,6 @@ import { IStrippedState } from "../sync-accumulator";
|
||||
// Types relating to Rooms of type `m.space` and related APIs
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
/** @deprecated Use hierarchy instead where possible. */
|
||||
export interface ISpaceSummaryRoom extends IPublicRoomsChunkRoom {
|
||||
num_refs: number;
|
||||
room_type: string;
|
||||
}
|
||||
|
||||
/** @deprecated Use hierarchy instead where possible. */
|
||||
export interface ISpaceSummaryEvent {
|
||||
room_id: string;
|
||||
event_id: string;
|
||||
origin_server_ts: number;
|
||||
type: string;
|
||||
state_key: string;
|
||||
sender: string;
|
||||
content: {
|
||||
order?: string;
|
||||
suggested?: boolean;
|
||||
auto_join?: boolean;
|
||||
via?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface IHierarchyRelation extends IStrippedState {
|
||||
origin_server_ts: number;
|
||||
content: {
|
||||
|
||||
+21
-6
@@ -16,16 +16,15 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { ListenerMap, TypedEventEmitter } from "./models/typed-event-emitter";
|
||||
|
||||
export class ReEmitter {
|
||||
private target: EventEmitter;
|
||||
constructor(private readonly target: EventEmitter) {}
|
||||
|
||||
constructor(target: EventEmitter) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
reEmit(source: EventEmitter, eventNames: string[]) {
|
||||
public reEmit(source: EventEmitter, eventNames: string[]): void {
|
||||
for (const eventName of eventNames) {
|
||||
// We include the source as the last argument for event handlers which may need it,
|
||||
// such as read receipt listeners on the client class which won't have the context
|
||||
@@ -48,3 +47,19 @@ export class ReEmitter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class TypedReEmitter<
|
||||
Events extends string,
|
||||
Arguments extends ListenerMap<Events>,
|
||||
> extends ReEmitter {
|
||||
constructor(target: TypedEventEmitter<Events, Arguments>) {
|
||||
super(target);
|
||||
}
|
||||
|
||||
public reEmit<ReEmittedEvents extends string, T extends Events & ReEmittedEvents>(
|
||||
source: TypedEventEmitter<ReEmittedEvents, any>,
|
||||
eventNames: T[],
|
||||
): void {
|
||||
super.reEmit(source, eventNames);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,10 @@ import queryString from "qs";
|
||||
|
||||
import * as matrixcs from "./matrix";
|
||||
|
||||
if (matrixcs.getRequest()) {
|
||||
throw new Error("Multiple matrix-js-sdk entrypoints detected!");
|
||||
}
|
||||
|
||||
matrixcs.request(function(opts, fn) {
|
||||
// We manually fix the query string for browser-request because
|
||||
// it doesn't correctly handle cases like ?via=one&via=two. Instead
|
||||
|
||||
+430
-162
File diff suppressed because it is too large
Load Diff
+14
-34
@@ -1,6 +1,5 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2018 - 2022 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.
|
||||
@@ -19,8 +18,13 @@ limitations under the License.
|
||||
|
||||
import { MsgType } from "./@types/event";
|
||||
import { TEXT_NODE_TYPE } from "./@types/extensible_events";
|
||||
import { ILocationContent, LOCATION_EVENT_TYPE, TIMESTAMP_NODE_TYPE } from "./@types/location";
|
||||
import { IPollContent, POLL_START_EVENT_TYPE } from "./@types/polls";
|
||||
import {
|
||||
ASSET_NODE_TYPE,
|
||||
ILocationContent,
|
||||
LocationAssetType,
|
||||
LOCATION_EVENT_TYPE,
|
||||
TIMESTAMP_NODE_TYPE,
|
||||
} from "./@types/location";
|
||||
|
||||
/**
|
||||
* Generates the content for a HTML Message event
|
||||
@@ -103,35 +107,6 @@ export function makeEmoteMessage(body: string) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the content for a Poll Start event
|
||||
* @param question the poll question
|
||||
* @param answers the possible answers
|
||||
* @param kind whether the poll is disclosed or undisclosed. Allowed values are
|
||||
* "m.poll.disclosed" or "m.poll.undisclosed", or the unstable equivalents
|
||||
*/
|
||||
export function makePollContent(
|
||||
question: string,
|
||||
answers: string[],
|
||||
kind: string,
|
||||
): IPollContent {
|
||||
question = question.trim();
|
||||
answers = answers.map(a => a.trim()).filter(a => !!a);
|
||||
return {
|
||||
[TEXT_NODE_TYPE.name]:
|
||||
`${question}\n${answers.map((a, i) => `${i + 1}. ${a}`).join('\n')}`,
|
||||
[POLL_START_EVENT_TYPE.name]: {
|
||||
kind: kind,
|
||||
question: {
|
||||
[TEXT_NODE_TYPE.name]: question,
|
||||
},
|
||||
answers: answers.map(
|
||||
(a, i) => ({ id: `${i}-${a}`, [TEXT_NODE_TYPE.name]: a }),
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the content for a Location event
|
||||
* @param text a text for of our location
|
||||
@@ -139,12 +114,14 @@ export function makePollContent(
|
||||
* @param ts the timestamp when the location was correct (milliseconds since
|
||||
* the UNIX epoch)
|
||||
* @param description the (optional) label for this location on the map
|
||||
* @param asset_type the (optional) asset type of this location e.g. "m.self"
|
||||
*/
|
||||
export function makeLocationContent(
|
||||
text: string,
|
||||
uri: string,
|
||||
ts: number,
|
||||
description?: string,
|
||||
assetType?: LocationAssetType,
|
||||
): ILocationContent {
|
||||
return {
|
||||
"body": text,
|
||||
@@ -154,8 +131,11 @@ export function makeLocationContent(
|
||||
uri,
|
||||
description,
|
||||
},
|
||||
[TIMESTAMP_NODE_TYPE.name]: ts,
|
||||
[ASSET_NODE_TYPE.name]: {
|
||||
type: assetType ?? LocationAssetType.Self,
|
||||
},
|
||||
[TEXT_NODE_TYPE.name]: text,
|
||||
[TIMESTAMP_NODE_TYPE.name]: ts,
|
||||
// TODO: MSC1767 fallbacks m.image thumbnail
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ limitations under the License.
|
||||
* @module crypto/CrossSigning
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { PkSigning } from "@matrix-org/olm";
|
||||
|
||||
import { decodeBase64, encodeBase64, pkSign, pkVerify } from './olmlib';
|
||||
@@ -55,7 +54,7 @@ export interface ICrossSigningInfo {
|
||||
crossSigningVerifiedBefore: boolean;
|
||||
}
|
||||
|
||||
export class CrossSigningInfo extends EventEmitter {
|
||||
export class CrossSigningInfo {
|
||||
public keys: Record<string, ICrossSigningKey> = {};
|
||||
public firstUse = true;
|
||||
// This tracks whether we've ever verified this user with any identity.
|
||||
@@ -79,9 +78,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
public readonly userId: string,
|
||||
private callbacks: ICryptoCallbacks = {},
|
||||
private cacheCallbacks: ICacheCallbacks = {},
|
||||
) {
|
||||
super();
|
||||
}
|
||||
) {}
|
||||
|
||||
public static fromStorage(obj: ICrossSigningInfo, userId: string): CrossSigningInfo {
|
||||
const res = new CrossSigningInfo(userId);
|
||||
|
||||
@@ -20,8 +20,6 @@ limitations under the License.
|
||||
* Manages the list of other users' devices
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { logger } from '../logger';
|
||||
import { DeviceInfo, IDevice } from './deviceinfo';
|
||||
import { CrossSigningInfo, ICrossSigningInfo } from './CrossSigning';
|
||||
@@ -31,6 +29,8 @@ import { chunkPromises, defer, IDeferred, sleep } from '../utils';
|
||||
import { IDownloadKeyResult, MatrixClient } from "../client";
|
||||
import { OlmDevice } from "./OlmDevice";
|
||||
import { CryptoStore } from "./store/base";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
import { CryptoEvent, CryptoEventHandlerMap } from "./index";
|
||||
|
||||
/* State transition diagram for DeviceList.deviceTrackingStatus
|
||||
*
|
||||
@@ -62,10 +62,12 @@ export enum TrackingStatus {
|
||||
|
||||
export type DeviceInfoMap = Record<string, Record<string, DeviceInfo>>;
|
||||
|
||||
type EmittedEvents = CryptoEvent.WillUpdateDevices | CryptoEvent.DevicesUpdated | CryptoEvent.UserCrossSigningUpdated;
|
||||
|
||||
/**
|
||||
* @alias module:crypto/DeviceList
|
||||
*/
|
||||
export class DeviceList extends EventEmitter {
|
||||
export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHandlerMap> {
|
||||
private devices: { [userId: string]: { [deviceId: string]: IDevice } } = {};
|
||||
|
||||
public crossSigningInfo: { [userId: string]: ICrossSigningInfo } = {};
|
||||
@@ -634,7 +636,7 @@ export class DeviceList extends EventEmitter {
|
||||
});
|
||||
|
||||
const finished = (success: boolean): void => {
|
||||
this.emit("crypto.willUpdateDevices", users, !this.hasFetched);
|
||||
this.emit(CryptoEvent.WillUpdateDevices, users, !this.hasFetched);
|
||||
users.forEach((u) => {
|
||||
this.dirty = true;
|
||||
|
||||
@@ -659,7 +661,7 @@ export class DeviceList extends EventEmitter {
|
||||
}
|
||||
});
|
||||
this.saveIfDirty();
|
||||
this.emit("crypto.devicesUpdated", users, !this.hasFetched);
|
||||
this.emit(CryptoEvent.DevicesUpdated, users, !this.hasFetched);
|
||||
this.hasFetched = true;
|
||||
};
|
||||
|
||||
@@ -867,7 +869,7 @@ class DeviceListUpdateSerialiser {
|
||||
|
||||
// NB. Unlike most events in the js-sdk, this one is internal to the
|
||||
// js-sdk and is not re-emitted
|
||||
this.deviceList.emit('userCrossSigningUpdated', userId);
|
||||
this.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,17 +14,25 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { logger } from "../logger";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning";
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { Method, PREFIX_UNSTABLE } from "../http-api";
|
||||
import { Crypto, IBootstrapCrossSigningOpts } from "./index";
|
||||
import { CrossSigningKeys, ICrossSigningKey, ICryptoCallbacks, ISignedKey, KeySignatures } from "../matrix";
|
||||
import {
|
||||
ClientEvent,
|
||||
CrossSigningKeys,
|
||||
ClientEventHandlerMap,
|
||||
ICrossSigningKey,
|
||||
ICryptoCallbacks,
|
||||
ISignedKey,
|
||||
KeySignatures,
|
||||
} from "../matrix";
|
||||
import { ISecretStorageKeyInfo } from "./api";
|
||||
import { IKeyBackupInfo } from "./keybackup";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
import { IAccountDataClient } from "./SecretStorage";
|
||||
|
||||
interface ICrossSigningKeys {
|
||||
authUpload: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"];
|
||||
@@ -256,7 +264,10 @@ export class EncryptionSetupOperation {
|
||||
* Catches account data set by SecretStorage during bootstrapping by
|
||||
* implementing the methods related to account data in MatrixClient
|
||||
*/
|
||||
class AccountDataClientAdapter extends EventEmitter {
|
||||
class AccountDataClientAdapter
|
||||
extends TypedEventEmitter<ClientEvent.AccountData, ClientEventHandlerMap>
|
||||
implements IAccountDataClient {
|
||||
//
|
||||
public readonly values = new Map<string, MatrixEvent>();
|
||||
|
||||
/**
|
||||
@@ -303,7 +314,7 @@ class AccountDataClientAdapter extends EventEmitter {
|
||||
// and it seems to rely on this.
|
||||
return Promise.resolve().then(() => {
|
||||
const event = new MatrixEvent({ type, content });
|
||||
this.emit("accountData", event, lastEvent);
|
||||
this.emit(ClientEvent.AccountData, event, lastEvent);
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'stream';
|
||||
|
||||
import { logger } from '../logger';
|
||||
import * as olmlib from './olmlib';
|
||||
import { encodeBase64 } from './olmlib';
|
||||
import { randomString } from '../randomstring';
|
||||
import { encryptAES, decryptAES, IEncryptedPayload, calculateKeyCheck } from './aes';
|
||||
import { encodeBase64 } from "./olmlib";
|
||||
import { ICryptoCallbacks, MatrixClient, MatrixEvent } from '../matrix';
|
||||
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from './aes';
|
||||
import { ClientEvent, ICryptoCallbacks, MatrixEvent } from '../matrix';
|
||||
import { ClientEventHandlerMap, MatrixClient } from "../client";
|
||||
import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from './api';
|
||||
import { TypedEventEmitter } from '../models/typed-event-emitter';
|
||||
|
||||
export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2";
|
||||
|
||||
@@ -36,7 +36,7 @@ export interface ISecretRequest {
|
||||
cancel: (reason: string) => void;
|
||||
}
|
||||
|
||||
export interface IAccountDataClient extends EventEmitter {
|
||||
export interface IAccountDataClient extends TypedEventEmitter<ClientEvent.AccountData, ClientEventHandlerMap> {
|
||||
// Subset of MatrixClient (which also uses any for the event content)
|
||||
getAccountDataFromServer: <T extends {[k: string]: any}>(eventType: string) => Promise<T>;
|
||||
getAccountData: (eventType: string) => MatrixEvent;
|
||||
@@ -98,17 +98,17 @@ export class SecretStorage {
|
||||
ev.getType() === 'm.secret_storage.default_key' &&
|
||||
ev.getContent().key === keyId
|
||||
) {
|
||||
this.accountDataAdapter.removeListener('accountData', listener);
|
||||
this.accountDataAdapter.removeListener(ClientEvent.AccountData, listener);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
this.accountDataAdapter.on('accountData', listener);
|
||||
this.accountDataAdapter.on(ClientEvent.AccountData, listener);
|
||||
|
||||
this.accountDataAdapter.setAccountData(
|
||||
'm.secret_storage.default_key',
|
||||
{ key: keyId },
|
||||
).catch(e => {
|
||||
this.accountDataAdapter.removeListener('accountData', listener);
|
||||
this.accountDataAdapter.removeListener(ClientEvent.AccountData, listener);
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
|
||||
+28
-16
@@ -26,14 +26,13 @@ import { MEGOLM_ALGORITHM, verifySignature } from "./olmlib";
|
||||
import { DeviceInfo } from "./deviceinfo";
|
||||
import { DeviceTrustLevel } from './CrossSigning';
|
||||
import { keyFromPassphrase } from './key_passphrase';
|
||||
import { sleep } from "../utils";
|
||||
import { getCrypto, sleep } from "../utils";
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { encodeRecoveryKey } from './recoverykey';
|
||||
import { encryptAES, decryptAES, calculateKeyCheck } from './aes';
|
||||
import { getCrypto } from '../utils';
|
||||
import { ICurve25519AuthData, IAes256AuthData, IKeyBackupInfo, IKeyBackupSession } from "./keybackup";
|
||||
import { calculateKeyCheck, decryptAES, encryptAES } from './aes';
|
||||
import { IAes256AuthData, ICurve25519AuthData, IKeyBackupInfo, IKeyBackupSession } from "./keybackup";
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
import { IMegolmSessionData } from "./index";
|
||||
import { CryptoEvent, IMegolmSessionData } from "./index";
|
||||
|
||||
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
|
||||
|
||||
@@ -155,7 +154,7 @@ export class BackupManager {
|
||||
|
||||
this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey);
|
||||
|
||||
this.baseApis.emit('crypto.keyBackupStatus', true);
|
||||
this.baseApis.emit(CryptoEvent.KeyBackupStatus, true);
|
||||
|
||||
// There may be keys left over from a partially completed backup, so
|
||||
// schedule a send to check.
|
||||
@@ -173,7 +172,7 @@ export class BackupManager {
|
||||
|
||||
this.backupInfo = undefined;
|
||||
|
||||
this.baseApis.emit('crypto.keyBackupStatus', false);
|
||||
this.baseApis.emit(CryptoEvent.KeyBackupStatus, false);
|
||||
}
|
||||
|
||||
public getKeyBackupEnabled(): boolean | null {
|
||||
@@ -313,11 +312,24 @@ export class BackupManager {
|
||||
return ret;
|
||||
}
|
||||
|
||||
const trustedPubkey = this.baseApis.crypto.sessionStore.getLocalTrustedBackupPubKey();
|
||||
const privKey = await this.baseApis.crypto.getSessionBackupPrivateKey();
|
||||
if (privKey) {
|
||||
let algorithm;
|
||||
try {
|
||||
algorithm = await BackupManager.makeAlgorithm(backupInfo, async () => privKey);
|
||||
|
||||
if ("public_key" in backupInfo.auth_data && backupInfo.auth_data.public_key === trustedPubkey) {
|
||||
logger.info("Backup public key " + trustedPubkey + " is trusted locally");
|
||||
ret.trusted_locally = true;
|
||||
if (await algorithm.keyMatches(privKey)) {
|
||||
logger.info("Backup is trusted locally");
|
||||
ret.trusted_locally = true;
|
||||
}
|
||||
} catch {
|
||||
// do nothing -- if we have an error, then we don't mark it as
|
||||
// locally trusted
|
||||
} finally {
|
||||
if (algorithm) {
|
||||
algorithm.free();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mySigs = backupInfo.auth_data.signatures[this.baseApis.getUserId()] || {};
|
||||
@@ -445,7 +457,7 @@ export class BackupManager {
|
||||
await this.checkKeyBackup();
|
||||
// Backup version has changed or this backup version
|
||||
// has been deleted
|
||||
this.baseApis.crypto.emit("crypto.keyBackupFailed", err.data.errcode);
|
||||
this.baseApis.crypto.emit(CryptoEvent.KeyBackupFailed, err.data.errcode);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -467,14 +479,14 @@ export class BackupManager {
|
||||
* @param {integer} limit Maximum number of keys to back up
|
||||
* @returns {integer} Number of sessions backed up
|
||||
*/
|
||||
private async backupPendingKeys(limit: number): Promise<number> {
|
||||
public async backupPendingKeys(limit: number): Promise<number> {
|
||||
const sessions = await this.baseApis.crypto.cryptoStore.getSessionsNeedingBackup(limit);
|
||||
if (!sessions.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
|
||||
this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
this.baseApis.crypto.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
|
||||
|
||||
const rooms: IKeyBackup["rooms"] = {};
|
||||
for (const session of sessions) {
|
||||
@@ -511,7 +523,7 @@ export class BackupManager {
|
||||
|
||||
await this.baseApis.crypto.cryptoStore.unmarkSessionsNeedingBackup(sessions);
|
||||
remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
|
||||
this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
this.baseApis.crypto.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
|
||||
|
||||
return sessions.length;
|
||||
}
|
||||
@@ -567,7 +579,7 @@ export class BackupManager {
|
||||
);
|
||||
|
||||
const remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
|
||||
this.baseApis.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
this.baseApis.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
|
||||
return remaining;
|
||||
}
|
||||
|
||||
|
||||
+116
-63
@@ -22,27 +22,36 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import anotherjson from "another-json";
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { ReEmitter } from '../ReEmitter';
|
||||
import { TypedReEmitter } from '../ReEmitter';
|
||||
import { logger } from '../logger';
|
||||
import { IExportedDevice, OlmDevice } from "./OlmDevice";
|
||||
import * as olmlib from "./olmlib";
|
||||
import { DeviceInfoMap, DeviceList } from "./DeviceList";
|
||||
import { DeviceInfo, IDevice } from "./deviceinfo";
|
||||
import type { DecryptionAlgorithm, EncryptionAlgorithm } from "./algorithms";
|
||||
import * as algorithms from "./algorithms";
|
||||
import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from './CrossSigning';
|
||||
import { EncryptionSetupBuilder } from "./EncryptionSetup";
|
||||
import {
|
||||
IAccountDataClient,
|
||||
ISecretRequest,
|
||||
SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
SecretStorage,
|
||||
SecretStorageKeyTuple,
|
||||
ISecretRequest,
|
||||
SecretStorageKeyObject,
|
||||
SecretStorageKeyTuple,
|
||||
} from './SecretStorage';
|
||||
import { IAddSecretStorageKeyOpts, ICreateSecretStorageOpts, IImportRoomKeysOpts, ISecretStorageKeyInfo } from "./api";
|
||||
import {
|
||||
IAddSecretStorageKeyOpts,
|
||||
ICreateSecretStorageOpts,
|
||||
IEncryptedEventInfo,
|
||||
IImportRoomKeysOpts,
|
||||
IRecoveryKey,
|
||||
ISecretStorageKeyInfo,
|
||||
} from "./api";
|
||||
import { OutgoingRoomKeyRequestManager } from './OutgoingRoomKeyRequestManager';
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { VerificationBase } from "./verification/Base";
|
||||
import { ReciprocateQRCode, SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD } from './verification/QRCode';
|
||||
import { SAS as SASVerification } from './verification/SAS';
|
||||
import { keyFromPassphrase } from './key_passphrase';
|
||||
@@ -52,21 +61,28 @@ import { InRoomChannel, InRoomRequests } from "./verification/request/InRoomChan
|
||||
import { ToDeviceChannel, ToDeviceRequests } from "./verification/request/ToDeviceChannel";
|
||||
import { IllegalMethod } from "./verification/IllegalMethod";
|
||||
import { KeySignatureUploadError } from "../errors";
|
||||
import { decryptAES, encryptAES, calculateKeyCheck } from './aes';
|
||||
import { calculateKeyCheck, decryptAES, encryptAES } from './aes';
|
||||
import { DehydrationManager, IDeviceKeys, IOneTimeKey } from './dehydration';
|
||||
import { BackupManager } from "./backup";
|
||||
import { IStore } from "../store";
|
||||
import { Room } from "../models/room";
|
||||
import { RoomMember } from "../models/room-member";
|
||||
import { MatrixEvent, EventStatus, IClearEvent, IEvent } from "../models/event";
|
||||
import { MatrixClient, IKeysUploadResponse, SessionStore, ISignedKey, ICrossSigningKey } from "../client";
|
||||
import type { EncryptionAlgorithm, DecryptionAlgorithm } from "./algorithms/base";
|
||||
import { Room, RoomEvent } from "../models/room";
|
||||
import { RoomMember, RoomMemberEvent } from "../models/room-member";
|
||||
import { EventStatus, IClearEvent, IEvent, MatrixEvent, MatrixEventEvent } from "../models/event";
|
||||
import {
|
||||
ClientEvent,
|
||||
ICrossSigningKey,
|
||||
IKeysUploadResponse,
|
||||
ISignedKey,
|
||||
IUploadKeySignaturesResponse,
|
||||
MatrixClient,
|
||||
SessionStore,
|
||||
} from "../client";
|
||||
import type { IRoomEncryption, RoomList } from "./RoomList";
|
||||
import { IRecoveryKey, IEncryptedEventInfo } from "./api";
|
||||
import { IKeyBackupInfo } from "./keybackup";
|
||||
import { ISyncStateData } from "../sync";
|
||||
import { CryptoStore } from "./store/base";
|
||||
import { IVerificationChannel } from "./verification/request/Channel";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
|
||||
@@ -186,7 +202,45 @@ export interface IRequestsMap {
|
||||
setRequestByChannel(channel: IVerificationChannel, request: VerificationRequest): void;
|
||||
}
|
||||
|
||||
export class Crypto extends EventEmitter {
|
||||
export enum CryptoEvent {
|
||||
DeviceVerificationChanged = "deviceVerificationChanged",
|
||||
UserTrustStatusChanged = "userTrustStatusChanged",
|
||||
UserCrossSigningUpdated = "userCrossSigningUpdated",
|
||||
RoomKeyRequest = "crypto.roomKeyRequest",
|
||||
RoomKeyRequestCancellation = "crypto.roomKeyRequestCancellation",
|
||||
KeyBackupStatus = "crypto.keyBackupStatus",
|
||||
KeyBackupFailed = "crypto.keyBackupFailed",
|
||||
KeyBackupSessionsRemaining = "crypto.keyBackupSessionsRemaining",
|
||||
KeySignatureUploadFailure = "crypto.keySignatureUploadFailure",
|
||||
VerificationRequest = "crypto.verification.request",
|
||||
Warning = "crypto.warning",
|
||||
WillUpdateDevices = "crypto.willUpdateDevices",
|
||||
DevicesUpdated = "crypto.devicesUpdated",
|
||||
KeysChanged = "crossSigning.keysChanged",
|
||||
}
|
||||
|
||||
export type CryptoEventHandlerMap = {
|
||||
[CryptoEvent.DeviceVerificationChanged]: (userId: string, deviceId: string, device: DeviceInfo) => void;
|
||||
[CryptoEvent.UserTrustStatusChanged]: (userId: string, trustLevel: UserTrustLevel) => void;
|
||||
[CryptoEvent.RoomKeyRequest]: (request: IncomingRoomKeyRequest) => void;
|
||||
[CryptoEvent.RoomKeyRequestCancellation]: (request: IncomingRoomKeyRequestCancellation) => void;
|
||||
[CryptoEvent.KeyBackupStatus]: (enabled: boolean) => void;
|
||||
[CryptoEvent.KeyBackupFailed]: (errcode: string) => void;
|
||||
[CryptoEvent.KeyBackupSessionsRemaining]: (remaining: number) => void;
|
||||
[CryptoEvent.KeySignatureUploadFailure]: (
|
||||
failures: IUploadKeySignaturesResponse["failures"],
|
||||
source: "checkOwnCrossSigningTrust" | "afterCrossSigningLocalKeyChange" | "setDeviceVerification",
|
||||
upload: (opts: { shouldEmit: boolean }) => Promise<void>
|
||||
) => void;
|
||||
[CryptoEvent.VerificationRequest]: (request: VerificationRequest<any>) => void;
|
||||
[CryptoEvent.Warning]: (type: string) => void;
|
||||
[CryptoEvent.KeysChanged]: (data: {}) => void;
|
||||
[CryptoEvent.WillUpdateDevices]: (users: string[], initialFetch: boolean) => void;
|
||||
[CryptoEvent.DevicesUpdated]: (users: string[], initialFetch: boolean) => void;
|
||||
[CryptoEvent.UserCrossSigningUpdated]: (userId: string) => void;
|
||||
};
|
||||
|
||||
export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap> {
|
||||
/**
|
||||
* @return {string} The version of Olm.
|
||||
*/
|
||||
@@ -201,8 +255,8 @@ export class Crypto extends EventEmitter {
|
||||
public readonly dehydrationManager: DehydrationManager;
|
||||
public readonly secretStorage: SecretStorage;
|
||||
|
||||
private readonly reEmitter: ReEmitter;
|
||||
private readonly verificationMethods: any; // TODO types
|
||||
private readonly reEmitter: TypedReEmitter<CryptoEvent, CryptoEventHandlerMap>;
|
||||
private readonly verificationMethods: Map<VerificationMethod, typeof VerificationBase>;
|
||||
public readonly supportedAlgorithms: string[];
|
||||
private readonly outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager;
|
||||
private readonly toDeviceVerificationRequests: ToDeviceRequests;
|
||||
@@ -295,10 +349,10 @@ export class Crypto extends EventEmitter {
|
||||
private readonly clientStore: IStore,
|
||||
public readonly cryptoStore: CryptoStore,
|
||||
private readonly roomList: RoomList,
|
||||
verificationMethods: any[], // TODO types
|
||||
verificationMethods: Array<keyof typeof defaultVerificationMethods | typeof VerificationBase>,
|
||||
) {
|
||||
super();
|
||||
this.reEmitter = new ReEmitter(this);
|
||||
this.reEmitter = new TypedReEmitter(this);
|
||||
|
||||
if (verificationMethods) {
|
||||
this.verificationMethods = new Map();
|
||||
@@ -307,20 +361,21 @@ export class Crypto extends EventEmitter {
|
||||
if (defaultVerificationMethods[method]) {
|
||||
this.verificationMethods.set(
|
||||
method,
|
||||
defaultVerificationMethods[method],
|
||||
<typeof VerificationBase>defaultVerificationMethods[method],
|
||||
);
|
||||
}
|
||||
} else if (method.NAME) {
|
||||
} else if (method["NAME"]) {
|
||||
this.verificationMethods.set(
|
||||
method.NAME,
|
||||
method,
|
||||
method["NAME"],
|
||||
method as typeof VerificationBase,
|
||||
);
|
||||
} else {
|
||||
logger.warn(`Excluding unknown verification method ${method}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.verificationMethods = defaultVerificationMethods;
|
||||
this.verificationMethods =
|
||||
new Map(Object.entries(defaultVerificationMethods)) as Map<VerificationMethod, typeof VerificationBase>;
|
||||
}
|
||||
|
||||
this.backupManager = new BackupManager(baseApis, async () => {
|
||||
@@ -358,8 +413,8 @@ export class Crypto extends EventEmitter {
|
||||
|
||||
// XXX: This isn't removed at any point, but then none of the event listeners
|
||||
// this class sets seem to be removed at any point... :/
|
||||
this.deviceList.on('userCrossSigningUpdated', this.onDeviceListUserCrossSigningUpdated);
|
||||
this.reEmitter.reEmit(this.deviceList, ["crypto.devicesUpdated", "crypto.willUpdateDevices"]);
|
||||
this.deviceList.on(CryptoEvent.UserCrossSigningUpdated, this.onDeviceListUserCrossSigningUpdated);
|
||||
this.reEmitter.reEmit(this.deviceList, [CryptoEvent.DevicesUpdated, CryptoEvent.WillUpdateDevices]);
|
||||
|
||||
this.supportedAlgorithms = Object.keys(algorithms.DECRYPTION_CLASSES);
|
||||
|
||||
@@ -375,7 +430,7 @@ export class Crypto extends EventEmitter {
|
||||
|
||||
this.crossSigningInfo = new CrossSigningInfo(userId, cryptoCallbacks, cacheCallbacks);
|
||||
// Yes, we pass the client twice here: see SecretStorage
|
||||
this.secretStorage = new SecretStorage(baseApis, cryptoCallbacks, baseApis);
|
||||
this.secretStorage = new SecretStorage(baseApis as IAccountDataClient, cryptoCallbacks, baseApis);
|
||||
this.dehydrationManager = new DehydrationManager(this);
|
||||
|
||||
// Assuming no app-supplied callback, default to getting from SSSS.
|
||||
@@ -487,7 +542,7 @@ export class Crypto extends EventEmitter {
|
||||
deviceTrust.isCrossSigningVerified()
|
||||
) {
|
||||
const deviceObj = this.deviceList.getStoredDevice(userId, deviceId);
|
||||
this.emit("deviceVerificationChanged", userId, deviceId, deviceObj);
|
||||
this.emit(CryptoEvent.DeviceVerificationChanged, userId, deviceId, deviceObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1165,7 +1220,7 @@ export class Crypto extends EventEmitter {
|
||||
if (Object.keys(failures || []).length > 0) {
|
||||
if (shouldEmit) {
|
||||
this.baseApis.emit(
|
||||
"crypto.keySignatureUploadFailure",
|
||||
CryptoEvent.KeySignatureUploadFailure,
|
||||
failures,
|
||||
"afterCrossSigningLocalKeyChange",
|
||||
upload, // continuation
|
||||
@@ -1391,11 +1446,10 @@ export class Crypto extends EventEmitter {
|
||||
// that reset the keys
|
||||
this.storeTrustedSelfKeys(null);
|
||||
// emit cross-signing has been disabled
|
||||
this.emit("crossSigning.keysChanged", {});
|
||||
this.emit(CryptoEvent.KeysChanged, {});
|
||||
// as the trust for our own user has changed,
|
||||
// also emit an event for this
|
||||
this.emit("userTrustStatusChanged",
|
||||
this.userId, this.checkUserTrust(userId));
|
||||
this.emit(CryptoEvent.UserTrustStatusChanged, this.userId, this.checkUserTrust(userId));
|
||||
}
|
||||
} else {
|
||||
await this.checkDeviceVerifications(userId);
|
||||
@@ -1410,7 +1464,7 @@ export class Crypto extends EventEmitter {
|
||||
this.deviceList.setRawStoredCrossSigningForUser(userId, crossSigning.toStorage());
|
||||
}
|
||||
|
||||
this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId));
|
||||
this.emit(CryptoEvent.UserTrustStatusChanged, userId, this.checkUserTrust(userId));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1567,7 +1621,7 @@ export class Crypto extends EventEmitter {
|
||||
if (Object.keys(failures || []).length > 0) {
|
||||
if (shouldEmit) {
|
||||
this.baseApis.emit(
|
||||
"crypto.keySignatureUploadFailure",
|
||||
CryptoEvent.KeySignatureUploadFailure,
|
||||
failures,
|
||||
"checkOwnCrossSigningTrust",
|
||||
upload,
|
||||
@@ -1585,10 +1639,10 @@ export class Crypto extends EventEmitter {
|
||||
upload({ shouldEmit: true });
|
||||
}
|
||||
|
||||
this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId));
|
||||
this.emit(CryptoEvent.UserTrustStatusChanged, userId, this.checkUserTrust(userId));
|
||||
|
||||
if (masterChanged) {
|
||||
this.baseApis.emit("crossSigning.keysChanged", {});
|
||||
this.emit(CryptoEvent.KeysChanged, {});
|
||||
await this.afterCrossSigningLocalKeyChange();
|
||||
}
|
||||
|
||||
@@ -1675,18 +1729,14 @@ export class Crypto extends EventEmitter {
|
||||
* @param {external:EventEmitter} eventEmitter event source where we can register
|
||||
* for event notifications
|
||||
*/
|
||||
public registerEventHandlers(eventEmitter: EventEmitter): void {
|
||||
eventEmitter.on("RoomMember.membership", (event: MatrixEvent, member: RoomMember, oldMembership?: string) => {
|
||||
try {
|
||||
this.onRoomMembership(event, member, oldMembership);
|
||||
} catch (e) {
|
||||
logger.error("Error handling membership change:", e);
|
||||
}
|
||||
});
|
||||
|
||||
eventEmitter.on("toDeviceEvent", this.onToDeviceEvent);
|
||||
eventEmitter.on("Room.timeline", this.onTimelineEvent);
|
||||
eventEmitter.on("Event.decrypted", this.onTimelineEvent);
|
||||
public registerEventHandlers(eventEmitter: TypedEventEmitter<
|
||||
RoomMemberEvent.Membership | ClientEvent.ToDeviceEvent | RoomEvent.Timeline | MatrixEventEvent.Decrypted,
|
||||
any
|
||||
>): void {
|
||||
eventEmitter.on(RoomMemberEvent.Membership, this.onMembership);
|
||||
eventEmitter.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
|
||||
eventEmitter.on(RoomEvent.Timeline, this.onTimelineEvent);
|
||||
eventEmitter.on(MatrixEventEvent.Decrypted, this.onTimelineEvent);
|
||||
}
|
||||
|
||||
/** Start background processes related to crypto */
|
||||
@@ -2070,9 +2120,7 @@ export class Crypto extends EventEmitter {
|
||||
if (!this.crossSigningInfo.getId() && userId === this.crossSigningInfo.userId) {
|
||||
this.storeTrustedSelfKeys(xsk.keys);
|
||||
// This will cause our own user trust to change, so emit the event
|
||||
this.emit(
|
||||
"userTrustStatusChanged", this.userId, this.checkUserTrust(userId),
|
||||
);
|
||||
this.emit(CryptoEvent.UserTrustStatusChanged, this.userId, this.checkUserTrust(userId));
|
||||
}
|
||||
|
||||
// Now sign the master key with our user signing key (unless it's ourself)
|
||||
@@ -2094,7 +2142,7 @@ export class Crypto extends EventEmitter {
|
||||
if (Object.keys(failures || []).length > 0) {
|
||||
if (shouldEmit) {
|
||||
this.baseApis.emit(
|
||||
"crypto.keySignatureUploadFailure",
|
||||
CryptoEvent.KeySignatureUploadFailure,
|
||||
failures,
|
||||
"setDeviceVerification",
|
||||
upload,
|
||||
@@ -2178,7 +2226,7 @@ export class Crypto extends EventEmitter {
|
||||
if (Object.keys(failures || []).length > 0) {
|
||||
if (shouldEmit) {
|
||||
this.baseApis.emit(
|
||||
"crypto.keySignatureUploadFailure",
|
||||
CryptoEvent.KeySignatureUploadFailure,
|
||||
failures,
|
||||
"setDeviceVerification",
|
||||
upload, // continuation
|
||||
@@ -2193,7 +2241,7 @@ export class Crypto extends EventEmitter {
|
||||
}
|
||||
|
||||
const deviceObj = DeviceInfo.fromStorage(dev, deviceId);
|
||||
this.emit("deviceVerificationChanged", userId, deviceId, deviceObj);
|
||||
this.emit(CryptoEvent.DeviceVerificationChanged, userId, deviceId, deviceObj);
|
||||
return deviceObj;
|
||||
}
|
||||
|
||||
@@ -2728,7 +2776,6 @@ export class Crypto extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307
|
||||
/**
|
||||
* Encrypt an event according to the configuration of the room.
|
||||
*
|
||||
@@ -2739,8 +2786,6 @@ export class Crypto extends EventEmitter {
|
||||
* @return {Promise?} Promise which resolves when the event has been
|
||||
* encrypted, or null if nothing was needed
|
||||
*/
|
||||
/* eslint-enable valid-jsdoc */
|
||||
// TODO this return type lies
|
||||
public async encryptEvent(event: MatrixEvent, room: Room): Promise<void> {
|
||||
if (!room) {
|
||||
throw new Error("Cannot send encrypted messages in unknown rooms");
|
||||
@@ -3048,6 +3093,14 @@ export class Crypto extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
private onMembership = (event: MatrixEvent, member: RoomMember, oldMembership?: string) => {
|
||||
try {
|
||||
this.onRoomMembership(event, member, oldMembership);
|
||||
} catch (e) {
|
||||
logger.error("Error handling membership change:", e);
|
||||
}
|
||||
};
|
||||
|
||||
private onToDeviceEvent = (event: MatrixEvent): void => {
|
||||
try {
|
||||
logger.log(`received to_device ${event.getType()} from: ` +
|
||||
@@ -3073,7 +3126,7 @@ export class Crypto extends EventEmitter {
|
||||
event.attemptDecryption(this);
|
||||
}
|
||||
// once the event has been decrypted, try again
|
||||
event.once('Event.decrypted', (ev) => {
|
||||
event.once(MatrixEventEvent.Decrypted, (ev) => {
|
||||
this.onToDeviceEvent(ev);
|
||||
});
|
||||
}
|
||||
@@ -3222,15 +3275,15 @@ export class Crypto extends EventEmitter {
|
||||
reject(new Error("Event status set to CANCELLED."));
|
||||
}
|
||||
};
|
||||
event.once("Event.localEventIdReplaced", eventIdListener);
|
||||
event.on("Event.status", statusListener);
|
||||
event.once(MatrixEventEvent.LocalEventIdReplaced, eventIdListener);
|
||||
event.on(MatrixEventEvent.Status, statusListener);
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error("error while waiting for the verification event to be sent: " + err.message);
|
||||
return;
|
||||
} finally {
|
||||
event.removeListener("Event.localEventIdReplaced", eventIdListener);
|
||||
event.removeListener("Event.status", statusListener);
|
||||
event.removeListener(MatrixEventEvent.LocalEventIdReplaced, eventIdListener);
|
||||
event.removeListener(MatrixEventEvent.Status, statusListener);
|
||||
}
|
||||
}
|
||||
let request = requestsMap.getRequest(event);
|
||||
@@ -3257,7 +3310,7 @@ export class Crypto extends EventEmitter {
|
||||
!request.invalid && // check it has enough events to pass the UNSENT stage
|
||||
!request.observeOnly;
|
||||
if (shouldEmit) {
|
||||
this.baseApis.emit("crypto.verification.request", request);
|
||||
this.baseApis.emit(CryptoEvent.VerificationRequest, request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3558,7 +3611,7 @@ export class Crypto extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
this.emit("crypto.roomKeyRequest", req);
|
||||
this.emit(CryptoEvent.RoomKeyRequest, req);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3577,7 +3630,7 @@ export class Crypto extends EventEmitter {
|
||||
// we should probably only notify the app of cancellations we told it
|
||||
// about, but we don't currently have a record of that, so we just pass
|
||||
// everything through.
|
||||
this.emit("crypto.roomKeyRequestCancellation", cancellation);
|
||||
this.emit(CryptoEvent.RoomKeyRequestCancellation, cancellation);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,8 +20,6 @@ limitations under the License.
|
||||
* @module crypto/verification/Base
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { MatrixEvent } from '../../models/event';
|
||||
import { logger } from '../../logger';
|
||||
import { DeviceInfo } from '../deviceinfo';
|
||||
@@ -30,6 +28,7 @@ import { KeysDuringVerification, requestKeysDuringVerification } from "../CrossS
|
||||
import { IVerificationChannel } from "./request/Channel";
|
||||
import { MatrixClient } from "../../client";
|
||||
import { VerificationRequest } from "./request/VerificationRequest";
|
||||
import { ListenerMap, TypedEventEmitter } from "../../models/typed-event-emitter";
|
||||
|
||||
const timeoutException = new Error("Verification timed out");
|
||||
|
||||
@@ -41,7 +40,18 @@ export class SwitchStartEventError extends Error {
|
||||
|
||||
export type KeyVerifier = (keyId: string, device: DeviceInfo, keyInfo: string) => void;
|
||||
|
||||
export class VerificationBase extends EventEmitter {
|
||||
export enum VerificationEvent {
|
||||
Cancel = "cancel",
|
||||
}
|
||||
|
||||
export type VerificationEventHandlerMap = {
|
||||
[VerificationEvent.Cancel]: (e: Error | MatrixEvent) => void;
|
||||
};
|
||||
|
||||
export class VerificationBase<
|
||||
Events extends string,
|
||||
Arguments extends ListenerMap<Events | VerificationEvent>,
|
||||
> extends TypedEventEmitter<Events | VerificationEvent, Arguments, VerificationEventHandlerMap> {
|
||||
private cancelled = false;
|
||||
private _done = false;
|
||||
private promise: Promise<void> = null;
|
||||
@@ -261,7 +271,7 @@ export class VerificationBase extends EventEmitter {
|
||||
}
|
||||
// Also emit a 'cancel' event that the app can listen for to detect cancellation
|
||||
// before calling verify()
|
||||
this.emit('cancel', e);
|
||||
this.emit(VerificationEvent.Cancel, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ limitations under the License.
|
||||
* @module crypto/verification/IllegalMethod
|
||||
*/
|
||||
|
||||
import { VerificationBase as Base } from "./Base";
|
||||
import { VerificationBase as Base, VerificationEvent, VerificationEventHandlerMap } from "./Base";
|
||||
import { IVerificationChannel } from "./request/Channel";
|
||||
import { MatrixClient } from "../../client";
|
||||
import { MatrixEvent } from "../../models/event";
|
||||
@@ -30,7 +30,7 @@ import { VerificationRequest } from "./request/VerificationRequest";
|
||||
* @class crypto/verification/IllegalMethod/IllegalMethod
|
||||
* @extends {module:crypto/verification/Base}
|
||||
*/
|
||||
export class IllegalMethod extends Base {
|
||||
export class IllegalMethod extends Base<VerificationEvent, VerificationEventHandlerMap> {
|
||||
public static factory(
|
||||
channel: IVerificationChannel,
|
||||
baseApis: MatrixClient,
|
||||
|
||||
@@ -19,7 +19,7 @@ limitations under the License.
|
||||
* @module crypto/verification/QRCode
|
||||
*/
|
||||
|
||||
import { VerificationBase as Base } from "./Base";
|
||||
import { VerificationBase as Base, VerificationEventHandlerMap } from "./Base";
|
||||
import { newKeyMismatchError, newUserCancelledError } from './Error';
|
||||
import { decodeBase64, encodeUnpaddedBase64 } from "../olmlib";
|
||||
import { logger } from '../../logger';
|
||||
@@ -31,15 +31,25 @@ import { MatrixEvent } from "../../models/event";
|
||||
export const SHOW_QR_CODE_METHOD = "m.qr_code.show.v1";
|
||||
export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1";
|
||||
|
||||
interface IReciprocateQr {
|
||||
confirm(): void;
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
export enum QrCodeEvent {
|
||||
ShowReciprocateQr = "show_reciprocate_qr",
|
||||
}
|
||||
|
||||
type EventHandlerMap = {
|
||||
[QrCodeEvent.ShowReciprocateQr]: (qr: IReciprocateQr) => void;
|
||||
} & VerificationEventHandlerMap;
|
||||
|
||||
/**
|
||||
* @class crypto/verification/QRCode/ReciprocateQRCode
|
||||
* @extends {module:crypto/verification/Base}
|
||||
*/
|
||||
export class ReciprocateQRCode extends Base {
|
||||
public reciprocateQREvent: {
|
||||
confirm(): void;
|
||||
cancel(): void;
|
||||
};
|
||||
export class ReciprocateQRCode extends Base<QrCodeEvent, EventHandlerMap> {
|
||||
public reciprocateQREvent: IReciprocateQr;
|
||||
|
||||
public static factory(
|
||||
channel: IVerificationChannel,
|
||||
@@ -76,7 +86,7 @@ export class ReciprocateQRCode extends Base {
|
||||
confirm: resolve,
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
};
|
||||
this.emit("show_reciprocate_qr", this.reciprocateQREvent);
|
||||
this.emit(QrCodeEvent.ShowReciprocateQr, this.reciprocateQREvent);
|
||||
});
|
||||
|
||||
// 3. determine key to sign / mark as trusted
|
||||
|
||||
@@ -22,7 +22,7 @@ limitations under the License.
|
||||
import anotherjson from 'another-json';
|
||||
import { Utility, SAS as OlmSAS } from "@matrix-org/olm";
|
||||
|
||||
import { VerificationBase as Base, SwitchStartEventError } from "./Base";
|
||||
import { VerificationBase as Base, SwitchStartEventError, VerificationEventHandlerMap } from "./Base";
|
||||
import {
|
||||
errorFactory,
|
||||
newInvalidMessageError,
|
||||
@@ -232,11 +232,19 @@ function intersection<T>(anArray: T[], aSet: Set<T>): T[] {
|
||||
return anArray instanceof Array ? anArray.filter(x => aSet.has(x)) : [];
|
||||
}
|
||||
|
||||
export enum SasEvent {
|
||||
ShowSas = "show_sas",
|
||||
}
|
||||
|
||||
type EventHandlerMap = {
|
||||
[SasEvent.ShowSas]: (sas: ISasEvent) => void;
|
||||
} & VerificationEventHandlerMap;
|
||||
|
||||
/**
|
||||
* @alias module:crypto/verification/SAS
|
||||
* @extends {module:crypto/verification/Base}
|
||||
*/
|
||||
export class SAS extends Base {
|
||||
export class SAS extends Base<SasEvent, EventHandlerMap> {
|
||||
private waitingForAccept: boolean;
|
||||
public ourSASPubKey: string;
|
||||
public theirSASPubKey: string;
|
||||
@@ -371,7 +379,7 @@ export class SAS extends Base {
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
mismatch: () => reject(newMismatchedSASError()),
|
||||
};
|
||||
this.emit("show_sas", this.sasEvent);
|
||||
this.emit(SasEvent.ShowSas, this.sasEvent);
|
||||
});
|
||||
|
||||
[e] = await Promise.all([
|
||||
@@ -447,7 +455,7 @@ export class SAS extends Base {
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
mismatch: () => reject(newMismatchedSASError()),
|
||||
};
|
||||
this.emit("show_sas", this.sasEvent);
|
||||
this.emit(SasEvent.ShowSas, this.sasEvent);
|
||||
});
|
||||
|
||||
[e] = await Promise.all([
|
||||
|
||||
@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { logger } from '../../../logger';
|
||||
import {
|
||||
errorFactory,
|
||||
@@ -29,6 +27,7 @@ import { MatrixClient } from "../../../client";
|
||||
import { MatrixEvent } from "../../../models/event";
|
||||
import { VerificationBase } from "../Base";
|
||||
import { VerificationMethod } from "../../index";
|
||||
import { TypedEventEmitter } from "../../../models/typed-event-emitter";
|
||||
|
||||
// How long after the event's timestamp that the request times out
|
||||
const TIMEOUT_FROM_EVENT_TS = 10 * 60 * 1000; // 10 minutes
|
||||
@@ -76,13 +75,23 @@ interface ITransition {
|
||||
event?: MatrixEvent;
|
||||
}
|
||||
|
||||
export enum VerificationRequestEvent {
|
||||
Change = "change",
|
||||
}
|
||||
|
||||
type EventHandlerMap = {
|
||||
[VerificationRequestEvent.Change]: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* State machine for verification requests.
|
||||
* Things that differ based on what channel is used to
|
||||
* send and receive verification events are put in `InRoomChannel` or `ToDeviceChannel`.
|
||||
* @event "change" whenever the state of the request object has changed.
|
||||
*/
|
||||
export class VerificationRequest<C extends IVerificationChannel = IVerificationChannel> extends EventEmitter {
|
||||
export class VerificationRequest<
|
||||
C extends IVerificationChannel = IVerificationChannel,
|
||||
> extends TypedEventEmitter<VerificationRequestEvent, EventHandlerMap> {
|
||||
private eventsByUs = new Map<string, MatrixEvent>();
|
||||
private eventsByThem = new Map<string, MatrixEvent>();
|
||||
private _observeOnly = false;
|
||||
@@ -104,7 +113,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
private commonMethods: VerificationMethod[] = [];
|
||||
private _phase: Phase;
|
||||
private _cancellingUserId: string;
|
||||
private _verifier: VerificationBase;
|
||||
private _verifier: VerificationBase<any, any>;
|
||||
|
||||
constructor(
|
||||
public readonly channel: C,
|
||||
@@ -236,7 +245,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
}
|
||||
|
||||
/** The verifier to do the actual verification, once the method has been established. Only defined when the `phase` is PHASE_STARTED. */
|
||||
public get verifier(): VerificationBase {
|
||||
public get verifier(): VerificationBase<any, any> {
|
||||
return this._verifier;
|
||||
}
|
||||
|
||||
@@ -410,7 +419,10 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
* @param {string?} targetDevice.deviceId the id of the device to direct this request to
|
||||
* @returns {VerifierBase} the verifier of the given method
|
||||
*/
|
||||
public beginKeyVerification(method: VerificationMethod, targetDevice: ITargetDevice = null): VerificationBase {
|
||||
public beginKeyVerification(
|
||||
method: VerificationMethod,
|
||||
targetDevice: ITargetDevice = null,
|
||||
): VerificationBase<any, any> {
|
||||
// need to allow also when unsent in case of to_device
|
||||
if (!this.observeOnly && !this._verifier) {
|
||||
const validStartPhase =
|
||||
@@ -453,7 +465,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
public async cancel({ reason = "User declined", code = "m.user" } = {}): Promise<void> {
|
||||
if (!this.observeOnly && this._phase !== PHASE_CANCELLED) {
|
||||
this._declining = true;
|
||||
this.emit("change");
|
||||
this.emit(VerificationRequestEvent.Change);
|
||||
if (this._verifier) {
|
||||
return this._verifier.cancel(errorFactory(code, reason)());
|
||||
} else {
|
||||
@@ -471,7 +483,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
if (!this.observeOnly && this.phase === PHASE_REQUESTED && !this.initiatedByMe) {
|
||||
const methods = [...this.verificationMethods.keys()];
|
||||
this._accepting = true;
|
||||
this.emit("change");
|
||||
this.emit(VerificationRequestEvent.Change);
|
||||
await this.channel.send(READY_TYPE, { methods });
|
||||
}
|
||||
}
|
||||
@@ -495,12 +507,12 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
handled = true;
|
||||
}
|
||||
if (handled) {
|
||||
this.off("change", check);
|
||||
this.off(VerificationRequestEvent.Change, check);
|
||||
}
|
||||
return handled;
|
||||
};
|
||||
if (!check()) {
|
||||
this.on("change", check);
|
||||
this.on(VerificationRequestEvent.Change, check);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -508,7 +520,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
private setPhase(phase: Phase, notify = true): void {
|
||||
this._phase = phase;
|
||||
if (notify) {
|
||||
this.emit("change");
|
||||
this.emit(VerificationRequestEvent.Change);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -768,7 +780,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
// set phase as last thing as this emits the "change" event
|
||||
this.setPhase(phase);
|
||||
} else if (this._observeOnly !== wasObserveOnly) {
|
||||
this.emit("change");
|
||||
this.emit(VerificationRequestEvent.Change);
|
||||
}
|
||||
} finally {
|
||||
// log events we processed so we can see from rageshakes what events were added to a request
|
||||
@@ -880,7 +892,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
method: VerificationMethod,
|
||||
startEvent: MatrixEvent = null,
|
||||
targetDevice: ITargetDevice = null,
|
||||
): VerificationBase {
|
||||
): VerificationBase<any, any> {
|
||||
if (!targetDevice) {
|
||||
targetDevice = this.targetDevice;
|
||||
}
|
||||
|
||||
+12
-3
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "./client";
|
||||
import { IEvent, MatrixEvent } from "./models/event";
|
||||
import { IEvent, MatrixEvent, MatrixEventEvent } from "./models/event";
|
||||
|
||||
export type EventMapper = (obj: Partial<IEvent>) => MatrixEvent;
|
||||
|
||||
@@ -30,10 +30,16 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
|
||||
|
||||
function mapper(plainOldJsObject: Partial<IEvent>) {
|
||||
const event = new MatrixEvent(plainOldJsObject);
|
||||
|
||||
const room = client.getRoom(event.getRoomId());
|
||||
if (room?.threads.has(event.getId())) {
|
||||
event.setThread(room.threads.get(event.getId()));
|
||||
}
|
||||
|
||||
if (event.isEncrypted()) {
|
||||
if (!preventReEmit) {
|
||||
client.reEmitter.reEmit(event, [
|
||||
"Event.decrypted",
|
||||
MatrixEventEvent.Decrypted,
|
||||
]);
|
||||
}
|
||||
if (decrypt) {
|
||||
@@ -41,7 +47,10 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
|
||||
}
|
||||
}
|
||||
if (!preventReEmit) {
|
||||
client.reEmitter.reEmit(event, ["Event.replaced"]);
|
||||
client.reEmitter.reEmit(event, [
|
||||
MatrixEventEvent.Replaced,
|
||||
MatrixEventEvent.VisibilityChange,
|
||||
]);
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
+56
-7
@@ -14,7 +14,11 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { UNSTABLE_FILTER_RELATION_SENDERS, UNSTABLE_FILTER_RELATION_TYPES } from "./filter";
|
||||
import { RelationType } from "./@types/event";
|
||||
import {
|
||||
UNSTABLE_FILTER_RELATED_BY_REL_TYPES,
|
||||
UNSTABLE_FILTER_RELATED_BY_SENDERS,
|
||||
} from "./filter";
|
||||
import { MatrixEvent } from "./models/event";
|
||||
|
||||
/**
|
||||
@@ -47,6 +51,8 @@ export interface IFilterComponent {
|
||||
not_senders?: string[];
|
||||
contains_url?: boolean;
|
||||
limit?: number;
|
||||
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]?: string[];
|
||||
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]?: Array<RelationType | string>;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
@@ -62,7 +68,7 @@ export interface IFilterComponent {
|
||||
* @param {Object} filterJson the definition of this filter JSON, e.g. { 'contains_url': true }
|
||||
*/
|
||||
export class FilterComponent {
|
||||
constructor(private filterJson: IFilterComponent) {}
|
||||
constructor(private filterJson: IFilterComponent, public readonly userId?: string) {}
|
||||
|
||||
/**
|
||||
* Checks with the filter component matches the given event
|
||||
@@ -70,11 +76,25 @@ export class FilterComponent {
|
||||
* @return {boolean} true if the event matches the filter
|
||||
*/
|
||||
public check(event: MatrixEvent): boolean {
|
||||
const bundledRelationships = event.getUnsigned()?.["m.relations"] || {};
|
||||
const relations: Array<string | RelationType> = Object.keys(bundledRelationships);
|
||||
// Relation senders allows in theory a look-up of any senders
|
||||
// however clients can only know about the current user participation status
|
||||
// as sending a whole list of participants could be proven problematic in terms
|
||||
// of performance
|
||||
// This should be improved when bundled relationships solve that problem
|
||||
const relationSenders = [];
|
||||
if (this.userId && bundledRelationships?.[RelationType.Thread]?.current_user_participated) {
|
||||
relationSenders.push(this.userId);
|
||||
}
|
||||
|
||||
return this.checkFields(
|
||||
event.getRoomId(),
|
||||
event.getSender(),
|
||||
event.getType(),
|
||||
event.getContent() ? event.getContent().url !== undefined : false,
|
||||
relations,
|
||||
relationSenders,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -90,8 +110,8 @@ export class FilterComponent {
|
||||
senders: this.filterJson.senders || null,
|
||||
not_senders: this.filterJson.not_senders || [],
|
||||
contains_url: this.filterJson.contains_url || null,
|
||||
[UNSTABLE_FILTER_RELATION_SENDERS.name]: UNSTABLE_FILTER_RELATION_SENDERS.findIn(this.filterJson),
|
||||
[UNSTABLE_FILTER_RELATION_TYPES.name]: UNSTABLE_FILTER_RELATION_TYPES.findIn(this.filterJson),
|
||||
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]: UNSTABLE_FILTER_RELATED_BY_SENDERS.findIn(this.filterJson),
|
||||
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]: UNSTABLE_FILTER_RELATED_BY_REL_TYPES.findIn(this.filterJson),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -101,9 +121,18 @@ export class FilterComponent {
|
||||
* @param {String} sender the sender of the event being checked
|
||||
* @param {String} eventType the type of the event being checked
|
||||
* @param {boolean} containsUrl whether the event contains a content.url field
|
||||
* @param {boolean} relationTypes whether has aggregated relation of the given type
|
||||
* @param {boolean} relationSenders whether one of the relation is sent by the user listed
|
||||
* @return {boolean} true if the event fields match the filter
|
||||
*/
|
||||
private checkFields(roomId: string, sender: string, eventType: string, containsUrl: boolean): boolean {
|
||||
private checkFields(
|
||||
roomId: string,
|
||||
sender: string,
|
||||
eventType: string,
|
||||
containsUrl: boolean,
|
||||
relationTypes: Array<RelationType | string>,
|
||||
relationSenders: string[],
|
||||
): boolean {
|
||||
const literalKeys = {
|
||||
"rooms": function(v: string): boolean {
|
||||
return roomId === v;
|
||||
@@ -136,15 +165,35 @@ export class FilterComponent {
|
||||
return false;
|
||||
}
|
||||
|
||||
const relationTypesFilter = this.filterJson[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name];
|
||||
if (relationTypesFilter !== undefined) {
|
||||
if (!this.arrayMatchesFilter(relationTypesFilter, relationTypes)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const relationSendersFilter = this.filterJson[UNSTABLE_FILTER_RELATED_BY_SENDERS.name];
|
||||
if (relationSendersFilter !== undefined) {
|
||||
if (!this.arrayMatchesFilter(relationSendersFilter, relationSenders)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private arrayMatchesFilter(filter: any[], values: any[]): boolean {
|
||||
return values.length > 0 && values.every(value => {
|
||||
return filter.includes(value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters a list of events down to those which match this filter component
|
||||
* @param {MatrixEvent[]} events Events to be checked against the filter component
|
||||
* @return {MatrixEvent[]} events which matched the filter component
|
||||
*/
|
||||
filter(events: MatrixEvent[]): MatrixEvent[] {
|
||||
public filter(events: MatrixEvent[]): MatrixEvent[] {
|
||||
return events.filter(this.check, this);
|
||||
}
|
||||
|
||||
@@ -153,7 +202,7 @@ export class FilterComponent {
|
||||
* 10 if none is otherwise specified. Cargo-culted from Synapse.
|
||||
* @return {Number} the limit for this filter component.
|
||||
*/
|
||||
limit(): number {
|
||||
public limit(): number {
|
||||
return this.filterJson.limit !== undefined ? this.filterJson.limit : 10;
|
||||
}
|
||||
}
|
||||
|
||||
+8
-8
@@ -26,13 +26,13 @@ import { FilterComponent, IFilterComponent } from "./filter-component";
|
||||
import { MatrixEvent } from "./models/event";
|
||||
import { UnstableValue } from "./NamespacedValue";
|
||||
|
||||
export const UNSTABLE_FILTER_RELATION_SENDERS = new UnstableValue(
|
||||
"relation_senders",
|
||||
export const UNSTABLE_FILTER_RELATED_BY_SENDERS = new UnstableValue(
|
||||
"related_by_senders",
|
||||
"io.element.relation_senders",
|
||||
);
|
||||
|
||||
export const UNSTABLE_FILTER_RELATION_TYPES = new UnstableValue(
|
||||
"relation_types",
|
||||
export const UNSTABLE_FILTER_RELATED_BY_REL_TYPES = new UnstableValue(
|
||||
"related_by_rel_types",
|
||||
"io.element.relation_types",
|
||||
);
|
||||
|
||||
@@ -66,8 +66,8 @@ export interface IRoomEventFilter extends IFilterComponent {
|
||||
lazy_load_members?: boolean;
|
||||
include_redundant_members?: boolean;
|
||||
types?: Array<EventType | string>;
|
||||
[UNSTABLE_FILTER_RELATION_TYPES.name]?: Array<RelationType | string>;
|
||||
[UNSTABLE_FILTER_RELATION_SENDERS.name]?: string[];
|
||||
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]?: Array<RelationType | string>;
|
||||
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]?: string[];
|
||||
}
|
||||
|
||||
interface IStateFilter extends IRoomEventFilter {}
|
||||
@@ -185,8 +185,8 @@ export class Filter {
|
||||
}
|
||||
}
|
||||
|
||||
this.roomFilter = new FilterComponent(roomFilterFields);
|
||||
this.roomTimelineFilter = new FilterComponent(roomFilterJson?.timeline || {});
|
||||
this.roomFilter = new FilterComponent(roomFilterFields, this.userId);
|
||||
this.roomTimelineFilter = new FilterComponent(roomFilterJson?.timeline || {}, this.userId);
|
||||
|
||||
// don't bother porting this from synapse yet:
|
||||
// this._room_state_filter =
|
||||
|
||||
+36
-11
@@ -21,7 +21,6 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { parse as parseContentType, ParsedMediaType } from "content-type";
|
||||
import EventEmitter from "events";
|
||||
|
||||
import type { IncomingHttpHeaders, IncomingMessage } from "http";
|
||||
import type { Request as _Request, CoreOptions } from "request";
|
||||
@@ -35,6 +34,7 @@ import { IDeferred } from "./utils";
|
||||
import { Callback } from "./client";
|
||||
import * as utils from "./utils";
|
||||
import { logger } from './logger';
|
||||
import { TypedEventEmitter } from "./models/typed-event-emitter";
|
||||
|
||||
/*
|
||||
TODO:
|
||||
@@ -111,6 +111,12 @@ interface IRequestOpts<T> {
|
||||
json?: boolean; // defaults to true
|
||||
qsStringifyOptions?: CoreOptions["qsStringifyOptions"];
|
||||
bodyParser?(body: string): T;
|
||||
|
||||
// Set to true to prevent the request function from emitting
|
||||
// a Session.logged_out event. This is intended for use on
|
||||
// endpoints where M_UNKNOWN_TOKEN is a valid/notable error
|
||||
// response, such as with token refreshes.
|
||||
inhibitLogoutEmit?: boolean;
|
||||
}
|
||||
|
||||
export interface IUpload {
|
||||
@@ -158,6 +164,16 @@ export enum Method {
|
||||
|
||||
export type FileType = Document | XMLHttpRequestBodyInit;
|
||||
|
||||
export enum HttpApiEvent {
|
||||
SessionLoggedOut = "Session.logged_out",
|
||||
NoConsent = "no_consent",
|
||||
}
|
||||
|
||||
export type HttpApiEventHandlerMap = {
|
||||
[HttpApiEvent.SessionLoggedOut]: (err: MatrixError) => void;
|
||||
[HttpApiEvent.NoConsent]: (message: string, consentUri: string) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a MatrixHttpApi.
|
||||
* @constructor
|
||||
@@ -186,7 +202,10 @@ export type FileType = Document | XMLHttpRequestBodyInit;
|
||||
export class MatrixHttpApi {
|
||||
private uploads: IUpload[] = [];
|
||||
|
||||
constructor(private eventEmitter: EventEmitter, public readonly opts: IHttpOpts) {
|
||||
constructor(
|
||||
private eventEmitter: TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>,
|
||||
public readonly opts: IHttpOpts,
|
||||
) {
|
||||
utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]);
|
||||
opts.onlyData = !!opts.onlyData;
|
||||
opts.useAuthorizationHeader = !!opts.useAuthorizationHeader;
|
||||
@@ -437,10 +456,20 @@ export class MatrixHttpApi {
|
||||
queryParams.filename = fileName;
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = { "Content-Type": contentType };
|
||||
|
||||
// authedRequest uses `request` which is no longer maintained.
|
||||
// `request` has a bug where if the body is zero bytes then you get an error: `Argument error, options.body`.
|
||||
// See https://github.com/request/request/issues/920
|
||||
// if body looks like a byte array and empty then set the Content-Length explicitly as a workaround:
|
||||
if ((body as unknown as ArrayLike<number>).length === 0) {
|
||||
headers["Content-Length"] = "0";
|
||||
}
|
||||
|
||||
promise = this.authedRequest(
|
||||
opts.callback, Method.Post, "/upload", queryParams, body, {
|
||||
prefix: "/_matrix/media/r0",
|
||||
headers: { "Content-Type": contentType },
|
||||
headers,
|
||||
json: false,
|
||||
bodyParser,
|
||||
},
|
||||
@@ -508,7 +537,7 @@ export class MatrixHttpApi {
|
||||
if (method === Method.Get) {
|
||||
opts.qs = params;
|
||||
} else if (typeof params === "object") {
|
||||
opts.json = !!params; // XXX: this feels strange
|
||||
opts.json = params;
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
@@ -586,14 +615,10 @@ export class MatrixHttpApi {
|
||||
const requestPromise = this.request<T, O>(callback, method, path, queryParams, data, requestOpts);
|
||||
|
||||
requestPromise.catch((err: MatrixError) => {
|
||||
if (err.errcode == 'M_UNKNOWN_TOKEN') {
|
||||
this.eventEmitter.emit("Session.logged_out", err);
|
||||
if (err.errcode == 'M_UNKNOWN_TOKEN' && !requestOpts?.inhibitLogoutEmit) {
|
||||
this.eventEmitter.emit(HttpApiEvent.SessionLoggedOut, err);
|
||||
} else if (err.errcode == 'M_CONSENT_NOT_GIVEN') {
|
||||
this.eventEmitter.emit(
|
||||
"no_consent",
|
||||
err.message,
|
||||
err.data.consent_uri,
|
||||
);
|
||||
this.eventEmitter.emit(HttpApiEvent.NoConsent, err.message, err.data.consent_uri);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -20,6 +20,10 @@ import * as matrixcs from "./matrix";
|
||||
import * as utils from "./utils";
|
||||
import { logger } from './logger';
|
||||
|
||||
if (matrixcs.getRequest()) {
|
||||
throw new Error("Multiple matrix-js-sdk entrypoints detected!");
|
||||
}
|
||||
|
||||
matrixcs.request(request);
|
||||
|
||||
try {
|
||||
|
||||
@@ -48,6 +48,7 @@ export * from "./crypto/store/indexeddb-crypto-store";
|
||||
export * from "./content-repo";
|
||||
export * from './@types/event';
|
||||
export * from './@types/PushRules';
|
||||
export * from './@types/partials';
|
||||
export * from './@types/requests';
|
||||
export * from './@types/search';
|
||||
export * from './models/room-summary';
|
||||
|
||||
@@ -20,6 +20,7 @@ import { IContent, MatrixEvent } from "./event";
|
||||
import { MSC3089TreeSpace } from "./MSC3089TreeSpace";
|
||||
import { EventTimeline } from "./event-timeline";
|
||||
import { FileType } from "../http-api";
|
||||
import type { ISendEventResponse } from "..";
|
||||
|
||||
/**
|
||||
* Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) branch - a reference
|
||||
@@ -164,14 +165,14 @@ export class MSC3089Branch {
|
||||
* @param {File | String | Buffer | ReadStream | Blob} encryptedContents The encrypted contents.
|
||||
* @param {Partial<IEncryptedFile>} info The encrypted file information.
|
||||
* @param {IContent} additionalContent Optional event content fields to include in the message.
|
||||
* @returns {Promise<void>} Resolves when uploaded.
|
||||
* @returns {Promise<ISendEventResponse>} Resolves to the file event's sent response.
|
||||
*/
|
||||
public async createNewVersion(
|
||||
name: string,
|
||||
encryptedContents: FileType,
|
||||
info: Partial<IEncryptedFile>,
|
||||
additionalContent?: IContent,
|
||||
): Promise<void> {
|
||||
): Promise<ISendEventResponse> {
|
||||
const fileEventResponse = await this.directory.createFile(name, encryptedContents, info, {
|
||||
...(additionalContent ?? {}),
|
||||
"m.new_content": true,
|
||||
@@ -193,6 +194,8 @@ export class MSC3089Branch {
|
||||
...(this.indexEvent.getContent()),
|
||||
active: false,
|
||||
}, this.id);
|
||||
|
||||
return fileEventResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
Copyright 2015 - 2022 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Enum for event statuses.
|
||||
* @readonly
|
||||
* @enum {string}
|
||||
*/
|
||||
export enum EventStatus {
|
||||
/** The event was not sent and will no longer be retried. */
|
||||
NOT_SENT = "not_sent",
|
||||
|
||||
/** The message is being encrypted */
|
||||
ENCRYPTING = "encrypting",
|
||||
|
||||
/** The event is in the process of being sent. */
|
||||
SENDING = "sending",
|
||||
|
||||
/** The event is in a queue waiting to be sent. */
|
||||
QUEUED = "queued",
|
||||
|
||||
/** The event has been sent to the server, but we have not yet received the echo. */
|
||||
SENT = "sent",
|
||||
|
||||
/** The event was cancelled before it was successfully sent. */
|
||||
CANCELLED = "cancelled",
|
||||
}
|
||||
@@ -18,17 +18,15 @@ limitations under the License.
|
||||
* @module models/event-timeline-set
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { EventTimeline } from "./event-timeline";
|
||||
import { EventStatus, MatrixEvent } from "./event";
|
||||
import { EventStatus, MatrixEvent, MatrixEventEvent } from "./event";
|
||||
import { logger } from '../logger';
|
||||
import { Relations } from './relations';
|
||||
import { Room } from "./room";
|
||||
import { Room, RoomEvent } from "./room";
|
||||
import { Filter } from "../filter";
|
||||
import { EventType, RelationType } from "../@types/event";
|
||||
import { RoomState } from "./room-state";
|
||||
import { Thread } from "./thread";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
|
||||
// var DEBUG = false;
|
||||
const DEBUG = true;
|
||||
@@ -53,7 +51,20 @@ export enum DuplicateStrategy {
|
||||
Replace = "replace",
|
||||
}
|
||||
|
||||
export class EventTimelineSet extends EventEmitter {
|
||||
export interface IRoomTimelineData {
|
||||
timeline: EventTimeline;
|
||||
liveEvent?: boolean;
|
||||
}
|
||||
|
||||
type EmittedEvents = RoomEvent.Timeline | RoomEvent.TimelineReset;
|
||||
|
||||
export type EventTimelineSetHandlerMap = {
|
||||
[RoomEvent.Timeline]:
|
||||
(event: MatrixEvent, room: Room, toStartOfTimeline: boolean, removed: boolean, data: IRoomTimelineData) => void;
|
||||
[RoomEvent.TimelineReset]: (room: Room, eventTimelineSet: EventTimelineSet, resetAllTimelines: boolean) => void;
|
||||
};
|
||||
|
||||
export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTimelineSetHandlerMap> {
|
||||
private readonly timelineSupport: boolean;
|
||||
private unstableClientRelationAggregation: boolean;
|
||||
private displayPendingEvents: boolean;
|
||||
@@ -154,17 +165,12 @@ export class EventTimelineSet extends EventEmitter {
|
||||
*
|
||||
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
|
||||
*/
|
||||
public getPendingEvents(thread?: Thread): MatrixEvent[] {
|
||||
public getPendingEvents(): MatrixEvent[] {
|
||||
if (!this.room || !this.displayPendingEvents) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const pendingEvents = this.room.getPendingEvents(thread);
|
||||
if (this.filter) {
|
||||
return this.filter.filterRoomTimeline(pendingEvents);
|
||||
} else {
|
||||
return pendingEvents;
|
||||
}
|
||||
return this.room.getPendingEvents();
|
||||
}
|
||||
/**
|
||||
* Get the live timeline for this room.
|
||||
@@ -248,7 +254,7 @@ export class EventTimelineSet extends EventEmitter {
|
||||
|
||||
// Now we can swap the live timeline to the new one.
|
||||
this.liveTimeline = newTimeline;
|
||||
this.emit("Room.timelineReset", this.room, this, resetAllTimelines);
|
||||
this.emit(RoomEvent.TimelineReset, this.room, this, resetAllTimelines);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -594,12 +600,11 @@ export class EventTimelineSet extends EventEmitter {
|
||||
this.setRelationsTarget(event);
|
||||
this.aggregateRelations(event);
|
||||
|
||||
const data = {
|
||||
const data: IRoomTimelineData = {
|
||||
timeline: timeline,
|
||||
liveEvent: !toStartOfTimeline && timeline == this.liveTimeline && !fromCache,
|
||||
};
|
||||
this.emit("Room.timeline", event, this.room,
|
||||
Boolean(toStartOfTimeline), false, data);
|
||||
this.emit(RoomEvent.Timeline, event, this.room, Boolean(toStartOfTimeline), false, data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -653,7 +658,7 @@ export class EventTimelineSet extends EventEmitter {
|
||||
const data = {
|
||||
timeline: timeline,
|
||||
};
|
||||
this.emit("Room.timeline", removed, this.room, undefined, true, data);
|
||||
this.emit(RoomEvent.Timeline, removed, this.room, undefined, true, data);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
@@ -751,7 +756,7 @@ export class EventTimelineSet extends EventEmitter {
|
||||
*/
|
||||
public getRelationsForEvent(
|
||||
eventId: string,
|
||||
relationType: RelationType,
|
||||
relationType: RelationType | string,
|
||||
eventType: EventType | string,
|
||||
): Relations | undefined {
|
||||
if (!this.unstableClientRelationAggregation) {
|
||||
@@ -769,6 +774,17 @@ export class EventTimelineSet extends EventEmitter {
|
||||
return relationsWithRelType[eventType];
|
||||
}
|
||||
|
||||
public getAllRelationsEventForEvent(eventId: string): MatrixEvent[] {
|
||||
const relationsForEvent = this.relations[eventId] || {};
|
||||
const events = [];
|
||||
for (const relationsRecord of Object.values(relationsForEvent)) {
|
||||
for (const relations of Object.values(relationsRecord)) {
|
||||
events.push(...relations.getRelations());
|
||||
}
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an event as the target event if any Relations exist for it already
|
||||
*
|
||||
@@ -809,7 +825,7 @@ export class EventTimelineSet extends EventEmitter {
|
||||
|
||||
// If the event is currently encrypted, wait until it has been decrypted.
|
||||
if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) {
|
||||
event.once("Event.decrypted", () => {
|
||||
event.once(MatrixEventEvent.Decrypted, () => {
|
||||
this.aggregateRelations(event);
|
||||
});
|
||||
return;
|
||||
|
||||
+286
-72
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015 - 2022 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.
|
||||
@@ -20,47 +20,22 @@ limitations under the License.
|
||||
* @module models/event
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { ExtensibleEvent, ExtensibleEvents, Optional } from "matrix-events-sdk";
|
||||
|
||||
import { logger } from '../logger';
|
||||
import { VerificationRequest } from "../crypto/verification/request/VerificationRequest";
|
||||
import {
|
||||
EventType,
|
||||
MsgType,
|
||||
RelationType,
|
||||
} from "../@types/event";
|
||||
import { EVENT_VISIBILITY_CHANGE_TYPE, EventType, MsgType, RelationType } from "../@types/event";
|
||||
import { Crypto, IEventDecryptionResult } from "../crypto";
|
||||
import { deepSortedObjectEntries } from "../utils";
|
||||
import { RoomMember } from "./room-member";
|
||||
import { Thread, ThreadEvent } from "./thread";
|
||||
import { Thread, ThreadEvent, EventHandlerMap as ThreadEventHandlerMap } from "./thread";
|
||||
import { IActionsObject } from '../pushprocessor';
|
||||
import { ReEmitter } from '../ReEmitter';
|
||||
import { TypedReEmitter } from '../ReEmitter';
|
||||
import { MatrixError } from "../http-api";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
import { EventStatus } from "./event-status";
|
||||
|
||||
/**
|
||||
* Enum for event statuses.
|
||||
* @readonly
|
||||
* @enum {string}
|
||||
*/
|
||||
export enum EventStatus {
|
||||
/** The event was not sent and will no longer be retried. */
|
||||
NOT_SENT = "not_sent",
|
||||
|
||||
/** The message is being encrypted */
|
||||
ENCRYPTING = "encrypting",
|
||||
|
||||
/** The event is in the process of being sent. */
|
||||
SENDING = "sending",
|
||||
|
||||
/** The event is in a queue waiting to be sent. */
|
||||
QUEUED = "queued",
|
||||
|
||||
/** The event has been sent to the server, but we have not yet received the echo. */
|
||||
SENT = "sent",
|
||||
|
||||
/** The event was cancelled before it was successfully sent. */
|
||||
CANCELLED = "cancelled",
|
||||
}
|
||||
export { EventStatus } from "./event-status";
|
||||
|
||||
const interns: Record<string, string> = {};
|
||||
function intern(str: string): string {
|
||||
@@ -89,6 +64,13 @@ export interface IUnsigned {
|
||||
redacted_because?: IEvent;
|
||||
transaction_id?: string;
|
||||
invite_room_state?: StrippedState[];
|
||||
"m.relations"?: Record<RelationType | string, any>; // No common pattern for aggregated relations
|
||||
}
|
||||
|
||||
export interface IThreadBundledRelationship {
|
||||
latest_event: IEvent;
|
||||
count: number;
|
||||
current_user_participated?: boolean;
|
||||
}
|
||||
|
||||
export interface IEvent {
|
||||
@@ -110,7 +92,7 @@ export interface IEvent {
|
||||
age?: number;
|
||||
}
|
||||
|
||||
interface IAggregatedRelation {
|
||||
export interface IAggregatedRelation {
|
||||
origin_server_ts: number;
|
||||
event_id?: string;
|
||||
sender?: string;
|
||||
@@ -122,9 +104,42 @@ interface IAggregatedRelation {
|
||||
export interface IEventRelation {
|
||||
rel_type: RelationType | string;
|
||||
event_id: string;
|
||||
"m.in_reply_to"?: {
|
||||
event_id: string;
|
||||
"m.render_in"?: string[];
|
||||
};
|
||||
key?: string;
|
||||
}
|
||||
|
||||
export interface IVisibilityEventRelation extends IEventRelation {
|
||||
visibility: "visible" | "hidden";
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* When an event is a visibility change event, as per MSC3531,
|
||||
* the visibility change implied by the event.
|
||||
*/
|
||||
export interface IVisibilityChange {
|
||||
/**
|
||||
* If `true`, the target event should be made visible.
|
||||
* Otherwise, it should be hidden.
|
||||
*/
|
||||
visible: boolean;
|
||||
|
||||
/**
|
||||
* The event id affected.
|
||||
*/
|
||||
eventId: string;
|
||||
|
||||
/**
|
||||
* Optionally, a human-readable reason explaining why
|
||||
* the event was hidden. Ignored if the event was made
|
||||
* visible.
|
||||
*/
|
||||
reason: string | null;
|
||||
}
|
||||
|
||||
export interface IClearEvent {
|
||||
room_id?: string;
|
||||
type: string;
|
||||
@@ -143,13 +158,71 @@ export interface IDecryptOptions {
|
||||
isRetry?: boolean;
|
||||
}
|
||||
|
||||
export class MatrixEvent extends EventEmitter {
|
||||
/**
|
||||
* Message hiding, as specified by https://github.com/matrix-org/matrix-doc/pull/3531.
|
||||
*/
|
||||
export type MessageVisibility = IMessageVisibilityHidden | IMessageVisibilityVisible;
|
||||
/**
|
||||
* Variant of `MessageVisibility` for the case in which the message should be displayed.
|
||||
*/
|
||||
export interface IMessageVisibilityVisible {
|
||||
readonly visible: true;
|
||||
}
|
||||
/**
|
||||
* Variant of `MessageVisibility` for the case in which the message should be hidden.
|
||||
*/
|
||||
export interface IMessageVisibilityHidden {
|
||||
readonly visible: false;
|
||||
/**
|
||||
* Optionally, a human-readable reason to show to the user indicating why the
|
||||
* message has been hidden (e.g. "Message Pending Moderation").
|
||||
*/
|
||||
readonly reason: string | null;
|
||||
}
|
||||
// A singleton implementing `IMessageVisibilityVisible`.
|
||||
const MESSAGE_VISIBLE: IMessageVisibilityVisible = Object.freeze({ visible: true });
|
||||
|
||||
export enum MatrixEventEvent {
|
||||
Decrypted = "Event.decrypted",
|
||||
BeforeRedaction = "Event.beforeRedaction",
|
||||
VisibilityChange = "Event.visibilityChange",
|
||||
LocalEventIdReplaced = "Event.localEventIdReplaced",
|
||||
Status = "Event.status",
|
||||
Replaced = "Event.replaced",
|
||||
RelationsCreated = "Event.relationsCreated",
|
||||
}
|
||||
|
||||
type EmittedEvents = MatrixEventEvent | ThreadEvent.Update;
|
||||
|
||||
export type MatrixEventHandlerMap = {
|
||||
[MatrixEventEvent.Decrypted]: (event: MatrixEvent, err?: Error) => void;
|
||||
[MatrixEventEvent.BeforeRedaction]: (event: MatrixEvent, redactionEvent: MatrixEvent) => void;
|
||||
[MatrixEventEvent.VisibilityChange]: (event: MatrixEvent, visible: boolean) => void;
|
||||
[MatrixEventEvent.LocalEventIdReplaced]: (event: MatrixEvent) => void;
|
||||
[MatrixEventEvent.Status]: (event: MatrixEvent, status: EventStatus) => void;
|
||||
[MatrixEventEvent.Replaced]: (event: MatrixEvent) => void;
|
||||
[MatrixEventEvent.RelationsCreated]: (relationType: string, eventType: string) => void;
|
||||
} & ThreadEventHandlerMap;
|
||||
|
||||
export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHandlerMap> {
|
||||
private pushActions: IActionsObject = null;
|
||||
private _replacingEvent: MatrixEvent = null;
|
||||
private _localRedactionEvent: MatrixEvent = null;
|
||||
private _isCancelled = false;
|
||||
private clearEvent?: IClearEvent;
|
||||
|
||||
/* Message hiding, as specified by https://github.com/matrix-org/matrix-doc/pull/3531.
|
||||
|
||||
Note: We're returning this object, so any value stored here MUST be frozen.
|
||||
*/
|
||||
private visibility: MessageVisibility = MESSAGE_VISIBLE;
|
||||
|
||||
// Not all events will be extensible-event compatible, so cache a flag in
|
||||
// addition to a falsy cached event value. We check the flag later on in
|
||||
// a public getter to decide if the cache is valid.
|
||||
private _hasCachedExtEv = false;
|
||||
private _cachedExtEv: Optional<ExtensibleEvent> = undefined;
|
||||
|
||||
/* curve25519 key which we believe belongs to the sender of the event. See
|
||||
* getSenderKey()
|
||||
*/
|
||||
@@ -191,6 +264,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
* A reference to the thread this event belongs to
|
||||
*/
|
||||
private thread: Thread = null;
|
||||
private threadId: string;
|
||||
|
||||
/* Set an approximate timestamp for the event relative the local clock.
|
||||
* This will inherently be approximate because it doesn't take into account
|
||||
@@ -198,7 +272,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
* it to us and the time we're now constructing this event, but that's better
|
||||
* than assuming the local clock is in sync with the origin HS's clock.
|
||||
*/
|
||||
private readonly localTimestamp: number;
|
||||
public localTimestamp: number;
|
||||
|
||||
// XXX: these should be read-only
|
||||
public sender: RoomMember = null;
|
||||
@@ -213,7 +287,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
*/
|
||||
public verificationRequest: VerificationRequest = null;
|
||||
|
||||
private readonly reEmitter: ReEmitter;
|
||||
private readonly reEmitter: TypedReEmitter<EmittedEvents, MatrixEventHandlerMap>;
|
||||
|
||||
/**
|
||||
* Construct a Matrix Event object
|
||||
@@ -263,8 +337,27 @@ export class MatrixEvent extends EventEmitter {
|
||||
});
|
||||
|
||||
this.txnId = event.txn_id || null;
|
||||
this.localTimestamp = Date.now() - this.getAge();
|
||||
this.reEmitter = new ReEmitter(this);
|
||||
this.localTimestamp = Date.now() - (this.getAge() ?? 0);
|
||||
this.reEmitter = new TypedReEmitter(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unstable getter to try and get an extensible event. Note that this might
|
||||
* return a falsy value if the event could not be parsed as an extensible
|
||||
* event.
|
||||
*
|
||||
* @deprecated Use stable functions where possible.
|
||||
*/
|
||||
public get unstableExtensibleEvent(): Optional<ExtensibleEvent> {
|
||||
if (!this._hasCachedExtEv) {
|
||||
this._cachedExtEv = ExtensibleEvents.parse(this.getEffectiveEvent());
|
||||
}
|
||||
return this._cachedExtEv;
|
||||
}
|
||||
|
||||
private invalidateExtensibleEvent() {
|
||||
// just reset the flag - that'll trick the getter into parsing a new event
|
||||
this._hasCachedExtEv = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -339,10 +432,10 @@ export class MatrixEvent extends EventEmitter {
|
||||
/**
|
||||
* Get the room_id for this event. This will return <code>undefined</code>
|
||||
* for <code>m.presence</code> events.
|
||||
* @return {string} The room ID, e.g. <code>!cURbafjkfsMDVwdRDQ:matrix.org
|
||||
* @return {string?} The room ID, e.g. <code>!cURbafjkfsMDVwdRDQ:matrix.org
|
||||
* </code>
|
||||
*/
|
||||
public getRoomId(): string {
|
||||
public getRoomId(): string | undefined {
|
||||
return this.event.room_id;
|
||||
}
|
||||
|
||||
@@ -409,10 +502,12 @@ export class MatrixEvent extends EventEmitter {
|
||||
* @experimental
|
||||
* Get the event ID of the thread head
|
||||
*/
|
||||
public get threadRootId(): string {
|
||||
public get threadRootId(): string | undefined {
|
||||
const relatesTo = this.getWireContent()?.["m.relates_to"];
|
||||
if (relatesTo?.rel_type === RelationType.Thread) {
|
||||
return relatesTo.event_id;
|
||||
} else {
|
||||
return this.getThread()?.id || this.threadId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,26 +515,30 @@ export class MatrixEvent extends EventEmitter {
|
||||
* @experimental
|
||||
*/
|
||||
public get isThreadRelation(): boolean {
|
||||
return !!this.threadRootId;
|
||||
return !!this.threadRootId && this.threadId !== this.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public get isThreadRoot(): boolean {
|
||||
// TODO, change the inner working of this getter for it to use the
|
||||
// bundled relationship return on the event, view MSC3440
|
||||
const thread = this.getThread();
|
||||
return thread?.id === this.getId();
|
||||
}
|
||||
const threadDetails = this
|
||||
.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
|
||||
|
||||
public get parentEventId(): string {
|
||||
return this.replyEventId || this.relationEventId;
|
||||
// Bundled relationships only returned when the sync response is limited
|
||||
// hence us having to check both bundled relation and inspect the thread
|
||||
// model
|
||||
return !!threadDetails || (this.getThread()?.id === this.getId());
|
||||
}
|
||||
|
||||
public get replyEventId(): string {
|
||||
const relations = this.getWireContent()["m.relates_to"];
|
||||
return relations?.["m.in_reply_to"]?.["event_id"];
|
||||
// We're prefer ev.getContent() over ev.getWireContent() to make sure
|
||||
// we grab the latest edit with potentially new relations. But we also
|
||||
// can't just rely on ev.getContent() by itself because historically we
|
||||
// still show the reply from the original message even though the edit
|
||||
// event does not include the relation reply.
|
||||
const mRelatesTo = this.getContent()['m.relates_to'] || this.getWireContent()['m.relates_to'];
|
||||
return mRelatesTo?.['m.in_reply_to']?.event_id;
|
||||
}
|
||||
|
||||
public get relationEventId(): string {
|
||||
@@ -475,9 +574,10 @@ export class MatrixEvent extends EventEmitter {
|
||||
* Get the age of this event. This represents the age of the event when the
|
||||
* event arrived at the device, and not the age of the event when this
|
||||
* function was called.
|
||||
* @return {Number} The age of this event in milliseconds.
|
||||
* Can only be returned once the server has echo'ed back
|
||||
* @return {Number|undefined} The age of this event in milliseconds.
|
||||
*/
|
||||
public getAge(): number {
|
||||
public getAge(): number | undefined {
|
||||
return this.getUnsigned().age || this.event.age; // v2 / v1
|
||||
}
|
||||
|
||||
@@ -570,7 +670,12 @@ export class MatrixEvent extends EventEmitter {
|
||||
}
|
||||
|
||||
public shouldAttemptDecryption() {
|
||||
return this.isEncrypted() && !this.isBeingDecrypted() && !this.clearEvent;
|
||||
if (this.isRedacted()) return false;
|
||||
if (this.isBeingDecrypted()) return false;
|
||||
if (this.clearEvent) return false;
|
||||
if (!this.isEncrypted()) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -761,7 +866,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
this.setPushActions(null);
|
||||
|
||||
if (options.emit !== false) {
|
||||
this.emit("Event.decrypted", this, err);
|
||||
this.emit(MatrixEventEvent.Decrypted, this, err);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -801,6 +906,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
this.forwardingCurve25519KeyChain =
|
||||
decryptionResult.forwardingCurve25519KeyChain || [];
|
||||
this.untrusted = decryptionResult.untrusted || false;
|
||||
this.invalidateExtensibleEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -904,6 +1010,10 @@ export class MatrixEvent extends EventEmitter {
|
||||
return this.event.unsigned || {};
|
||||
}
|
||||
|
||||
public setUnsigned(unsigned: IUnsigned): void {
|
||||
this.event.unsigned = unsigned;
|
||||
}
|
||||
|
||||
public unmarkLocallyRedacted(): boolean {
|
||||
const value = this._localRedactionEvent;
|
||||
this._localRedactionEvent = null;
|
||||
@@ -915,7 +1025,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
|
||||
public markLocallyRedacted(redactionEvent: MatrixEvent): void {
|
||||
if (this._localRedactionEvent) return;
|
||||
this.emit("Event.beforeRedaction", this, redactionEvent);
|
||||
this.emit(MatrixEventEvent.BeforeRedaction, this, redactionEvent);
|
||||
this._localRedactionEvent = redactionEvent;
|
||||
if (!this.event.unsigned) {
|
||||
this.event.unsigned = {};
|
||||
@@ -923,6 +1033,53 @@ export class MatrixEvent extends EventEmitter {
|
||||
this.event.unsigned.redacted_because = redactionEvent.event as IEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the visibility of an event, as per https://github.com/matrix-org/matrix-doc/pull/3531 .
|
||||
*
|
||||
* @fires module:models/event.MatrixEvent#"Event.visibilityChange" if `visibilityEvent`
|
||||
* caused a change in the actual visibility of this event, either by making it
|
||||
* visible (if it was hidden), by making it hidden (if it was visible) or by
|
||||
* changing the reason (if it was hidden).
|
||||
* @param visibilityEvent event holding a hide/unhide payload, or nothing
|
||||
* if the event is being reset to its original visibility (presumably
|
||||
* by a visibility event being redacted).
|
||||
*/
|
||||
public applyVisibilityEvent(visibilityChange?: IVisibilityChange): void {
|
||||
const visible = visibilityChange ? visibilityChange.visible : true;
|
||||
const reason = visibilityChange ? visibilityChange.reason : null;
|
||||
let change = false;
|
||||
if (this.visibility.visible !== visibilityChange.visible) {
|
||||
change = true;
|
||||
} else if (!this.visibility.visible && this.visibility["reason"] !== reason) {
|
||||
change = true;
|
||||
}
|
||||
if (change) {
|
||||
if (visible) {
|
||||
this.visibility = MESSAGE_VISIBLE;
|
||||
} else {
|
||||
this.visibility = Object.freeze({
|
||||
visible: false,
|
||||
reason: reason,
|
||||
});
|
||||
}
|
||||
if (change) {
|
||||
this.emit(MatrixEventEvent.VisibilityChange, this, visible);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return instructions to display or hide the message.
|
||||
*
|
||||
* @returns Instructions determining whether the message
|
||||
* should be displayed.
|
||||
*/
|
||||
public messageVisibility(): MessageVisibility {
|
||||
// Note: We may return `this.visibility` without fear, as
|
||||
// this is a shallow frozen object.
|
||||
return this.visibility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the content of an event in the same way it would be by the server
|
||||
* if it were redacted before it was sent to us
|
||||
@@ -938,7 +1095,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
|
||||
this._localRedactionEvent = null;
|
||||
|
||||
this.emit("Event.beforeRedaction", this, redactionEvent);
|
||||
this.emit(MatrixEventEvent.BeforeRedaction, this, redactionEvent);
|
||||
|
||||
this._replacingEvent = null;
|
||||
// we attempt to replicate what we would see from the server if
|
||||
@@ -972,6 +1129,8 @@ export class MatrixEvent extends EventEmitter {
|
||||
delete content[key];
|
||||
}
|
||||
}
|
||||
|
||||
this.invalidateExtensibleEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -992,6 +1151,54 @@ export class MatrixEvent extends EventEmitter {
|
||||
return this.getType() === EventType.RoomRedaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the visibility change caused by this event,
|
||||
* as per https://github.com/matrix-org/matrix-doc/pull/3531.
|
||||
*
|
||||
* @returns If the event is a well-formed visibility change event,
|
||||
* an instance of `IVisibilityChange`, otherwise `null`.
|
||||
*/
|
||||
public asVisibilityChange(): IVisibilityChange | null {
|
||||
if (!EVENT_VISIBILITY_CHANGE_TYPE.matches(this.getType())) {
|
||||
// Not a visibility change event.
|
||||
return null;
|
||||
}
|
||||
const relation = this.getRelation();
|
||||
if (!relation || relation.rel_type != "m.reference") {
|
||||
// Ill-formed, ignore this event.
|
||||
return null;
|
||||
}
|
||||
const eventId = relation.event_id;
|
||||
if (!eventId) {
|
||||
// Ill-formed, ignore this event.
|
||||
return null;
|
||||
}
|
||||
const content = this.getWireContent();
|
||||
const visible = !!content.visible;
|
||||
const reason = content.reason;
|
||||
if (reason && typeof reason != "string") {
|
||||
// Ill-formed, ignore this event.
|
||||
return null;
|
||||
}
|
||||
// Well-formed visibility change event.
|
||||
return {
|
||||
visible,
|
||||
reason,
|
||||
eventId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this event alters the visibility of another event,
|
||||
* as per https://github.com/matrix-org/matrix-doc/pull/3531.
|
||||
*
|
||||
* @returns {boolean} True if this event alters the visibility
|
||||
* of another event.
|
||||
*/
|
||||
public isVisibilityEvent(): boolean {
|
||||
return EVENT_VISIBILITY_CHANGE_TYPE.matches(this.getType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the (decrypted, if necessary) redaction event JSON
|
||||
* if event was redacted
|
||||
@@ -1051,8 +1258,10 @@ export class MatrixEvent extends EventEmitter {
|
||||
this.setStatus(null);
|
||||
if (this.getId() !== oldId) {
|
||||
// emit the event if it changed
|
||||
this.emit("Event.localEventIdReplaced", this);
|
||||
this.emit(MatrixEventEvent.LocalEventIdReplaced, this);
|
||||
}
|
||||
|
||||
this.localTimestamp = Date.now() - this.getAge();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1072,12 +1281,12 @@ export class MatrixEvent extends EventEmitter {
|
||||
*/
|
||||
public setStatus(status: EventStatus): void {
|
||||
this.status = status;
|
||||
this.emit("Event.status", this, status);
|
||||
this.emit(MatrixEventEvent.Status, this, status);
|
||||
}
|
||||
|
||||
public replaceLocalEventId(eventId: string): void {
|
||||
this.event.event_id = eventId;
|
||||
this.emit("Event.localEventIdReplaced", this);
|
||||
this.emit(MatrixEventEvent.LocalEventIdReplaced, this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1126,7 +1335,8 @@ export class MatrixEvent extends EventEmitter {
|
||||
}
|
||||
if (this._replacingEvent !== newEvent) {
|
||||
this._replacingEvent = newEvent;
|
||||
this.emit("Event.replaced", this);
|
||||
this.emit(MatrixEventEvent.Replaced, this);
|
||||
this.invalidateExtensibleEvent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1146,11 +1356,8 @@ export class MatrixEvent extends EventEmitter {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public getServerAggregatedRelation(relType: RelationType): IAggregatedRelation {
|
||||
const relations = this.getUnsigned()["m.relations"];
|
||||
if (relations) {
|
||||
return relations[relType];
|
||||
}
|
||||
public getServerAggregatedRelation<T>(relType: RelationType): T | undefined {
|
||||
return this.getUnsigned()["m.relations"]?.[relType];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1159,7 +1366,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
* @return {string?}
|
||||
*/
|
||||
public replacingEventId(): string | undefined {
|
||||
const replaceRelation = this.getServerAggregatedRelation(RelationType.Replace);
|
||||
const replaceRelation = this.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
|
||||
if (replaceRelation) {
|
||||
return replaceRelation.event_id;
|
||||
} else if (this._replacingEvent) {
|
||||
@@ -1184,7 +1391,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
* @return {Date?}
|
||||
*/
|
||||
public replacingEventDate(): Date | undefined {
|
||||
const replaceRelation = this.getServerAggregatedRelation(RelationType.Replace);
|
||||
const replaceRelation = this.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
|
||||
if (replaceRelation) {
|
||||
const ts = replaceRelation.origin_server_ts;
|
||||
if (Number.isFinite(ts)) {
|
||||
@@ -1210,7 +1417,9 @@ export class MatrixEvent extends EventEmitter {
|
||||
*/
|
||||
public getAssociatedId(): string | undefined {
|
||||
const relation = this.getRelation();
|
||||
if (relation) {
|
||||
if (this.replyEventId) {
|
||||
return this.replyEventId;
|
||||
} else if (relation) {
|
||||
return relation.event_id;
|
||||
} else if (this.isRedaction()) {
|
||||
return this.event.redacts;
|
||||
@@ -1344,15 +1553,20 @@ export class MatrixEvent extends EventEmitter {
|
||||
*/
|
||||
public setThread(thread: Thread): void {
|
||||
this.thread = thread;
|
||||
this.reEmitter.reEmit(thread, [ThreadEvent.Ready, ThreadEvent.Update]);
|
||||
this.setThreadId(thread.id);
|
||||
this.reEmitter.reEmit(thread, [ThreadEvent.Update]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public getThread(): Thread {
|
||||
public getThread(): Thread | undefined {
|
||||
return this.thread;
|
||||
}
|
||||
|
||||
public setThreadId(threadId: string): void {
|
||||
this.threadId = threadId;
|
||||
}
|
||||
}
|
||||
|
||||
/* REDACT_KEEP_KEYS gives the keys we keep when an event is redacted
|
||||
|
||||
@@ -20,6 +20,7 @@ limitations under the License.
|
||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import * as utils from "../utils";
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
Copyright 2022 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.
|
||||
*/
|
||||
|
||||
import { Relations, RelationsEvent, EventHandlerMap } from "./relations";
|
||||
import { MatrixEvent } from "./event";
|
||||
import { Listener } from "./typed-event-emitter";
|
||||
|
||||
export class RelatedRelations {
|
||||
private relations: Relations[];
|
||||
|
||||
public constructor(relations: Relations[]) {
|
||||
this.relations = relations.filter(r => !!r);
|
||||
}
|
||||
|
||||
public getRelations(): MatrixEvent[] {
|
||||
return this.relations.reduce((c, p) => [...c, ...p.getRelations()], []);
|
||||
}
|
||||
|
||||
public on<T extends RelationsEvent>(ev: T, fn: Listener<RelationsEvent, EventHandlerMap, T>) {
|
||||
this.relations.forEach(r => r.on(ev, fn));
|
||||
}
|
||||
|
||||
public off<T extends RelationsEvent>(ev: T, fn: Listener<RelationsEvent, EventHandlerMap, T>) {
|
||||
this.relations.forEach(r => r.off(ev, fn));
|
||||
}
|
||||
}
|
||||
+29
-18
@@ -14,12 +14,23 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { EventStatus, MatrixEvent } from './event';
|
||||
import { EventStatus, IAggregatedRelation, MatrixEvent, MatrixEventEvent } from './event';
|
||||
import { Room } from './room';
|
||||
import { logger } from '../logger';
|
||||
import { RelationType } from "../@types/event";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
|
||||
export enum RelationsEvent {
|
||||
Add = "Relations.add",
|
||||
Remove = "Relations.remove",
|
||||
Redaction = "Relations.redaction",
|
||||
}
|
||||
|
||||
export type EventHandlerMap = {
|
||||
[RelationsEvent.Add]: (event: MatrixEvent) => void;
|
||||
[RelationsEvent.Remove]: (event: MatrixEvent) => void;
|
||||
[RelationsEvent.Redaction]: (event: MatrixEvent) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* A container for relation events that supports easy access to common ways of
|
||||
@@ -29,7 +40,7 @@ import { RelationType } from "../@types/event";
|
||||
* The typical way to get one of these containers is via
|
||||
* EventTimelineSet#getRelationsForEvent.
|
||||
*/
|
||||
export class Relations extends EventEmitter {
|
||||
export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap> {
|
||||
private relationEventIds = new Set<string>();
|
||||
private relations = new Set<MatrixEvent>();
|
||||
private annotationsByKey: Record<string, Set<MatrixEvent>> = {};
|
||||
@@ -84,7 +95,7 @@ export class Relations extends EventEmitter {
|
||||
// If the event is in the process of being sent, listen for cancellation
|
||||
// so we can remove the event from the collection.
|
||||
if (event.isSending()) {
|
||||
event.on("Event.status", this.onEventStatus);
|
||||
event.on(MatrixEventEvent.Status, this.onEventStatus);
|
||||
}
|
||||
|
||||
this.relations.add(event);
|
||||
@@ -97,9 +108,9 @@ export class Relations extends EventEmitter {
|
||||
this.targetEvent.makeReplaced(lastReplacement);
|
||||
}
|
||||
|
||||
event.on("Event.beforeRedaction", this.onBeforeRedaction);
|
||||
event.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
|
||||
|
||||
this.emit("Relations.add", event);
|
||||
this.emit(RelationsEvent.Add, event);
|
||||
|
||||
this.maybeEmitCreated();
|
||||
}
|
||||
@@ -138,7 +149,7 @@ export class Relations extends EventEmitter {
|
||||
this.targetEvent.makeReplaced(lastReplacement);
|
||||
}
|
||||
|
||||
this.emit("Relations.remove", event);
|
||||
this.emit(RelationsEvent.Remove, event);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,14 +161,14 @@ export class Relations extends EventEmitter {
|
||||
private onEventStatus = (event: MatrixEvent, status: EventStatus) => {
|
||||
if (!event.isSending()) {
|
||||
// Sending is done, so we don't need to listen anymore
|
||||
event.removeListener("Event.status", this.onEventStatus);
|
||||
event.removeListener(MatrixEventEvent.Status, this.onEventStatus);
|
||||
return;
|
||||
}
|
||||
if (status !== EventStatus.CANCELLED) {
|
||||
return;
|
||||
}
|
||||
// Event was cancelled, remove from the collection
|
||||
event.removeListener("Event.status", this.onEventStatus);
|
||||
event.removeListener(MatrixEventEvent.Status, this.onEventStatus);
|
||||
this.removeEvent(event);
|
||||
};
|
||||
|
||||
@@ -171,11 +182,11 @@ export class Relations extends EventEmitter {
|
||||
* @return {Array}
|
||||
* Relation events in insertion order.
|
||||
*/
|
||||
public getRelations() {
|
||||
public getRelations(): MatrixEvent[] {
|
||||
return [...this.relations];
|
||||
}
|
||||
|
||||
private addAnnotationToAggregation(event: MatrixEvent) {
|
||||
private addAnnotationToAggregation(event: MatrixEvent): void {
|
||||
const { key } = event.getRelation();
|
||||
if (!key) {
|
||||
return;
|
||||
@@ -204,7 +215,7 @@ export class Relations extends EventEmitter {
|
||||
eventsFromSender.add(event);
|
||||
}
|
||||
|
||||
private removeAnnotationFromAggregation(event: MatrixEvent) {
|
||||
private removeAnnotationFromAggregation(event: MatrixEvent): void {
|
||||
const { key } = event.getRelation();
|
||||
if (!key) {
|
||||
return;
|
||||
@@ -240,7 +251,7 @@ export class Relations extends EventEmitter {
|
||||
* @param {MatrixEvent} redactedEvent
|
||||
* The original relation event that is about to be redacted.
|
||||
*/
|
||||
private onBeforeRedaction = async (redactedEvent: MatrixEvent) => {
|
||||
private onBeforeRedaction = async (redactedEvent: MatrixEvent): Promise<void> => {
|
||||
if (!this.relations.has(redactedEvent)) {
|
||||
return;
|
||||
}
|
||||
@@ -255,9 +266,9 @@ export class Relations extends EventEmitter {
|
||||
this.targetEvent.makeReplaced(lastReplacement);
|
||||
}
|
||||
|
||||
redactedEvent.removeListener("Event.beforeRedaction", this.onBeforeRedaction);
|
||||
redactedEvent.removeListener(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
|
||||
|
||||
this.emit("Relations.redaction", redactedEvent);
|
||||
this.emit(RelationsEvent.Redaction, redactedEvent);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -319,7 +330,7 @@ export class Relations extends EventEmitter {
|
||||
|
||||
// the all-knowning server tells us that the event at some point had
|
||||
// this timestamp for its replacement, so any following replacement should definitely not be less
|
||||
const replaceRelation = this.targetEvent.getServerAggregatedRelation(RelationType.Replace);
|
||||
const replaceRelation = this.targetEvent.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
|
||||
const minTs = replaceRelation && replaceRelation.origin_server_ts;
|
||||
|
||||
const lastReplacement = this.getRelations().reduce((last, event) => {
|
||||
@@ -375,6 +386,6 @@ export class Relations extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
this.creationEmitted = true;
|
||||
this.targetEvent.emit("Event.relationsCreated", this.relationType, this.eventType);
|
||||
this.targetEvent.emit(MatrixEventEvent.RelationsCreated, this.relationType, this.eventType);
|
||||
}
|
||||
}
|
||||
|
||||
+34
-10
@@ -18,15 +18,30 @@ limitations under the License.
|
||||
* @module models/room-member
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { getHttpUriForMxc } from "../content-repo";
|
||||
import * as utils from "../utils";
|
||||
import { User } from "./user";
|
||||
import { MatrixEvent } from "./event";
|
||||
import { RoomState } from "./room-state";
|
||||
import { logger } from "../logger";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
import { EventType } from "../@types/event";
|
||||
|
||||
export class RoomMember extends EventEmitter {
|
||||
export enum RoomMemberEvent {
|
||||
Membership = "RoomMember.membership",
|
||||
Name = "RoomMember.name",
|
||||
PowerLevel = "RoomMember.powerLevel",
|
||||
Typing = "RoomMember.typing",
|
||||
}
|
||||
|
||||
export type RoomMemberEventHandlerMap = {
|
||||
[RoomMemberEvent.Membership]: (event: MatrixEvent, member: RoomMember, oldMembership: string | null) => void;
|
||||
[RoomMemberEvent.Name]: (event: MatrixEvent, member: RoomMember, oldName: string | null) => void;
|
||||
[RoomMemberEvent.PowerLevel]: (event: MatrixEvent, member: RoomMember) => void;
|
||||
[RoomMemberEvent.Typing]: (event: MatrixEvent, member: RoomMember) => void;
|
||||
};
|
||||
|
||||
export class RoomMember extends TypedEventEmitter<RoomMemberEvent, RoomMemberEventHandlerMap> {
|
||||
private _isOutOfBand = false;
|
||||
private _modified: number;
|
||||
public _requestedProfileInfo: boolean; // used by sync.ts
|
||||
@@ -43,8 +58,8 @@ export class RoomMember extends EventEmitter {
|
||||
public events: {
|
||||
member?: MatrixEvent;
|
||||
} = {
|
||||
member: null,
|
||||
};
|
||||
member: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a new room member.
|
||||
@@ -106,7 +121,7 @@ export class RoomMember extends EventEmitter {
|
||||
public setMembershipEvent(event: MatrixEvent, roomState?: RoomState): void {
|
||||
const displayName = event.getDirectionalContent().displayname;
|
||||
|
||||
if (event.getType() !== "m.room.member") {
|
||||
if (event.getType() !== EventType.RoomMember) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -116,6 +131,15 @@ export class RoomMember extends EventEmitter {
|
||||
|
||||
const oldMembership = this.membership;
|
||||
this.membership = event.getDirectionalContent().membership;
|
||||
if (this.membership === undefined) {
|
||||
// logging to diagnose https://github.com/vector-im/element-web/issues/20962
|
||||
// (logs event content, although only of membership events)
|
||||
logger.trace(
|
||||
`membership event with membership undefined (forwardLooking: ${event.forwardLooking})!`,
|
||||
event.getContent(),
|
||||
`prevcontent is `, event.getPrevContent(),
|
||||
);
|
||||
}
|
||||
|
||||
this.disambiguate = shouldDisambiguate(
|
||||
this.userId,
|
||||
@@ -140,11 +164,11 @@ export class RoomMember extends EventEmitter {
|
||||
|
||||
if (oldMembership !== this.membership) {
|
||||
this.updateModifiedTime();
|
||||
this.emit("RoomMember.membership", event, this, oldMembership);
|
||||
this.emit(RoomMemberEvent.Membership, event, this, oldMembership);
|
||||
}
|
||||
if (oldName !== this.name) {
|
||||
this.updateModifiedTime();
|
||||
this.emit("RoomMember.name", event, this, oldName);
|
||||
this.emit(RoomMemberEvent.Name, event, this, oldName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +210,7 @@ export class RoomMember extends EventEmitter {
|
||||
// redraw everyone's level if the max has changed)
|
||||
if (oldPowerLevel !== this.powerLevel || oldPowerLevelNorm !== this.powerLevelNorm) {
|
||||
this.updateModifiedTime();
|
||||
this.emit("RoomMember.powerLevel", powerLevelEvent, this);
|
||||
this.emit(RoomMemberEvent.PowerLevel, powerLevelEvent, this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +236,7 @@ export class RoomMember extends EventEmitter {
|
||||
}
|
||||
if (oldTyping !== this.typing) {
|
||||
this.updateModifiedTime();
|
||||
this.emit("RoomMember.typing", event, this);
|
||||
this.emit(RoomMemberEvent.Typing, event, this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+33
-18
@@ -18,8 +18,6 @@ limitations under the License.
|
||||
* @module models/room-state
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { RoomMember } from "./room-member";
|
||||
import { logger } from '../logger';
|
||||
import * as utils from "../utils";
|
||||
@@ -27,6 +25,7 @@ import { EventType } from "../@types/event";
|
||||
import { MatrixEvent } from "./event";
|
||||
import { MatrixClient } from "../client";
|
||||
import { GuestAccess, HistoryVisibility, IJoinRuleEventContent, JoinRule } from "../@types/partials";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
|
||||
// possible statuses for out-of-band member loading
|
||||
enum OobStatus {
|
||||
@@ -35,7 +34,21 @@ enum OobStatus {
|
||||
Finished,
|
||||
}
|
||||
|
||||
export class RoomState extends EventEmitter {
|
||||
export enum RoomStateEvent {
|
||||
Events = "RoomState.events",
|
||||
Members = "RoomState.members",
|
||||
NewMember = "RoomState.newMember",
|
||||
Update = "RoomState.update", // signals batches of updates without specificity
|
||||
}
|
||||
|
||||
export type RoomStateEventHandlerMap = {
|
||||
[RoomStateEvent.Events]: (event: MatrixEvent, state: RoomState, lastStateEvent: MatrixEvent | null) => void;
|
||||
[RoomStateEvent.Members]: (event: MatrixEvent, state: RoomState, member: RoomMember) => void;
|
||||
[RoomStateEvent.NewMember]: (event: MatrixEvent, state: RoomState, member: RoomMember) => void;
|
||||
[RoomStateEvent.Update]: (state: RoomState) => void;
|
||||
};
|
||||
|
||||
export class RoomState extends TypedEventEmitter<RoomStateEvent, RoomStateEventHandlerMap> {
|
||||
private sentinels: Record<string, RoomMember> = {}; // userId: RoomMember
|
||||
// stores fuzzy matches to a list of userIDs (applies utils.removeHiddenChars to keys)
|
||||
private displayNameToUserIds: Record<string, string[]> = {};
|
||||
@@ -307,7 +320,7 @@ export class RoomState extends EventEmitter {
|
||||
this.updateDisplayNameCache(event.getStateKey(), event.getContent().displayname);
|
||||
this.updateThirdPartyTokenCache(event);
|
||||
}
|
||||
this.emit("RoomState.events", event, this, lastStateEvent);
|
||||
this.emit(RoomStateEvent.Events, event, this, lastStateEvent);
|
||||
});
|
||||
|
||||
// update higher level data structures. This needs to be done AFTER the
|
||||
@@ -342,7 +355,7 @@ export class RoomState extends EventEmitter {
|
||||
member.setMembershipEvent(event, this);
|
||||
|
||||
this.updateMember(member);
|
||||
this.emit("RoomState.members", event, this, member);
|
||||
this.emit(RoomStateEvent.Members, event, this, member);
|
||||
} else if (event.getType() === EventType.RoomPowerLevels) {
|
||||
// events with unknown state keys should be ignored
|
||||
// and should not aggregate onto members power levels
|
||||
@@ -357,7 +370,7 @@ export class RoomState extends EventEmitter {
|
||||
const oldLastModified = member.getLastModifiedTime();
|
||||
member.setPowerLevelEvent(event);
|
||||
if (oldLastModified !== member.getLastModifiedTime()) {
|
||||
this.emit("RoomState.members", event, this, member);
|
||||
this.emit(RoomStateEvent.Members, event, this, member);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -365,6 +378,8 @@ export class RoomState extends EventEmitter {
|
||||
this.sentinels = {};
|
||||
}
|
||||
});
|
||||
|
||||
this.emit(RoomStateEvent.Update, this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -384,7 +399,7 @@ export class RoomState extends EventEmitter {
|
||||
// add member to members before emitting any events,
|
||||
// as event handlers often lookup the member
|
||||
this.members[userId] = member;
|
||||
this.emit("RoomState.newMember", event, this, member);
|
||||
this.emit(RoomStateEvent.NewMember, event, this, member);
|
||||
}
|
||||
return member;
|
||||
}
|
||||
@@ -397,8 +412,7 @@ export class RoomState extends EventEmitter {
|
||||
}
|
||||
|
||||
private getStateEventMatching(event: MatrixEvent): MatrixEvent | null {
|
||||
if (!this.events.has(event.getType())) return null;
|
||||
return this.events.get(event.getType()).get(event.getStateKey());
|
||||
return this.events.get(event.getType())?.get(event.getStateKey()) ?? null;
|
||||
}
|
||||
|
||||
private updateMember(member: RoomMember): void {
|
||||
@@ -475,6 +489,7 @@ export class RoomState extends EventEmitter {
|
||||
logger.log(`LL: RoomState put in finished state ...`);
|
||||
this.oobMemberFlags.status = OobStatus.Finished;
|
||||
stateEvents.forEach((e) => this.setOutOfBandMember(e));
|
||||
this.emit(RoomStateEvent.Update, this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -503,7 +518,7 @@ export class RoomState extends EventEmitter {
|
||||
|
||||
this.setStateEvent(stateEvent);
|
||||
this.updateMember(member);
|
||||
this.emit("RoomState.members", stateEvent, this, member);
|
||||
this.emit(RoomStateEvent.Members, stateEvent, this, member);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -618,14 +633,14 @@ export class RoomState extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given MatrixClient has permission to send a state
|
||||
* event of type `stateEventType` into this room.
|
||||
* @param {string} stateEventType The type of state events to test
|
||||
* @param {MatrixClient} cli The client to test permission for
|
||||
* @return {boolean} true if the given client should be permitted to send
|
||||
* the given type of state event into this room,
|
||||
* according to the room's state.
|
||||
*/
|
||||
* Returns true if the given MatrixClient has permission to send a state
|
||||
* event of type `stateEventType` into this room.
|
||||
* @param {string} stateEventType The type of state events to test
|
||||
* @param {MatrixClient} cli The client to test permission for
|
||||
* @return {boolean} true if the given client should be permitted to send
|
||||
* the given type of state event into this room,
|
||||
* according to the room's state.
|
||||
*/
|
||||
public mayClientSendStateEvent(stateEventType: EventType | string, cli: MatrixClient): boolean {
|
||||
if (cli.isGuest()) {
|
||||
return false;
|
||||
|
||||
+451
-145
@@ -18,25 +18,28 @@ limitations under the License.
|
||||
* @module models/room
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { EventTimelineSet, DuplicateStrategy } from "./event-timeline-set";
|
||||
import { Direction, EventTimeline } from "./event-timeline";
|
||||
import { getHttpUriForMxc } from "../content-repo";
|
||||
import * as utils from "../utils";
|
||||
import { normalize } from "../utils";
|
||||
import { EventStatus, IEvent, MatrixEvent } from "./event";
|
||||
import { IEvent, MatrixEvent } from "./event";
|
||||
import { EventStatus } from "./event-status";
|
||||
import { RoomMember } from "./room-member";
|
||||
import { IRoomSummary, RoomSummary } from "./room-summary";
|
||||
import { logger } from '../logger';
|
||||
import { ReEmitter } from '../ReEmitter';
|
||||
import { EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../@types/event";
|
||||
import { TypedReEmitter } from '../ReEmitter';
|
||||
import {
|
||||
EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS,
|
||||
EVENT_VISIBILITY_CHANGE_TYPE,
|
||||
} from "../@types/event";
|
||||
import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
|
||||
import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials";
|
||||
import { Filter } from "../filter";
|
||||
import { RoomState } from "./room-state";
|
||||
import { Thread, ThreadEvent } from "./thread";
|
||||
import { Thread, ThreadEvent, EventHandlerMap as ThreadHandlerMap } from "./thread";
|
||||
import { Method } from "../http-api";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
|
||||
// These constants are used as sane defaults when the homeserver doesn't support
|
||||
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
|
||||
@@ -93,7 +96,7 @@ interface ICachedReceipt {
|
||||
data: IReceipt;
|
||||
}
|
||||
|
||||
type ReceiptCache = Record<string, ICachedReceipt[]>;
|
||||
type ReceiptCache = {[eventId: string]: ICachedReceipt[]};
|
||||
|
||||
interface IReceiptContent {
|
||||
[eventId: string]: {
|
||||
@@ -103,7 +106,30 @@ interface IReceiptContent {
|
||||
};
|
||||
}
|
||||
|
||||
type Receipts = Record<string, Record<string, IWrappedReceipt>>;
|
||||
const ReceiptPairRealIndex = 0;
|
||||
const ReceiptPairSyntheticIndex = 1;
|
||||
// We will only hold a synthetic receipt if we do not have a real receipt or the synthetic is newer.
|
||||
type Receipts = {
|
||||
[receiptType: string]: {
|
||||
[userId: string]: [IWrappedReceipt, IWrappedReceipt]; // Pair<real receipt, synthetic receipt> (both nullable)
|
||||
};
|
||||
};
|
||||
|
||||
// When inserting a visibility event affecting event `eventId`, we
|
||||
// need to scan through existing visibility events for `eventId`.
|
||||
// In theory, this could take an unlimited amount of time if:
|
||||
//
|
||||
// - the visibility event was sent by a moderator; and
|
||||
// - `eventId` already has many visibility changes (usually, it should
|
||||
// be 2 or less); and
|
||||
// - for some reason, the visibility changes are received out of order
|
||||
// (usually, this shouldn't happen at all).
|
||||
//
|
||||
// For this reason, we limit the number of events to scan through,
|
||||
// expecting that a broken visibility change for a single event in
|
||||
// an extremely uncommon case (possibly a DoS) is a small
|
||||
// price to pay to keep matrix-js-sdk responsive.
|
||||
const MAX_NUMBER_OF_VISIBILITY_EVENTS_TO_SCAN_THROUGH = 30;
|
||||
|
||||
export enum NotificationCountType {
|
||||
Highlight = "highlight",
|
||||
@@ -117,17 +143,51 @@ export interface ICreateFilterOpts {
|
||||
prepopulateTimeline?: boolean;
|
||||
}
|
||||
|
||||
export class Room extends EventEmitter {
|
||||
private readonly reEmitter: ReEmitter;
|
||||
export enum RoomEvent {
|
||||
MyMembership = "Room.myMembership",
|
||||
Tags = "Room.tags",
|
||||
AccountData = "Room.accountData",
|
||||
Receipt = "Room.receipt",
|
||||
Name = "Room.name",
|
||||
Redaction = "Room.redaction",
|
||||
RedactionCancelled = "Room.redactionCancelled",
|
||||
LocalEchoUpdated = "Room.localEchoUpdated",
|
||||
Timeline = "Room.timeline",
|
||||
TimelineReset = "Room.timelineReset",
|
||||
}
|
||||
|
||||
type EmittedEvents = RoomEvent
|
||||
| ThreadEvent.New
|
||||
| ThreadEvent.Update
|
||||
| RoomEvent.Timeline
|
||||
| RoomEvent.TimelineReset;
|
||||
|
||||
export type RoomEventHandlerMap = {
|
||||
[RoomEvent.MyMembership]: (room: Room, membership: string, prevMembership?: string) => void;
|
||||
[RoomEvent.Tags]: (event: MatrixEvent, room: Room) => void;
|
||||
[RoomEvent.AccountData]: (event: MatrixEvent, room: Room, lastEvent?: MatrixEvent) => void;
|
||||
[RoomEvent.Receipt]: (event: MatrixEvent, room: Room) => void;
|
||||
[RoomEvent.Name]: (room: Room) => void;
|
||||
[RoomEvent.Redaction]: (event: MatrixEvent, room: Room) => void;
|
||||
[RoomEvent.RedactionCancelled]: (event: MatrixEvent, room: Room) => void;
|
||||
[RoomEvent.LocalEchoUpdated]: (
|
||||
event: MatrixEvent,
|
||||
room: Room,
|
||||
oldEventId?: string,
|
||||
oldStatus?: EventStatus,
|
||||
) => void;
|
||||
[ThreadEvent.New]: (thread: Thread) => void;
|
||||
} & ThreadHandlerMap;
|
||||
|
||||
export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap> {
|
||||
private readonly reEmitter: TypedReEmitter<EmittedEvents, RoomEventHandlerMap>;
|
||||
private txnToEvent: Record<string, MatrixEvent> = {}; // Pending in-flight requests { string: MatrixEvent }
|
||||
// receipts should clobber based on receipt_type and user_id pairs hence
|
||||
// the form of this structure. This is sub-optimal for the exposed APIs
|
||||
// which pass in an event ID and get back some receipts, so we also store
|
||||
// a pre-cached list for this purpose.
|
||||
private receipts: Receipts = {}; // { receipt_type: { user_id: IReceipt } }
|
||||
private receiptCacheByEventId: ReceiptCache = {}; // { event_id: IReceipt2[] }
|
||||
// only receipts that came from the server, not synthesized ones
|
||||
private realReceipts: Receipts = {};
|
||||
private receiptCacheByEventId: ReceiptCache = {}; // { event_id: ICachedReceipt[] }
|
||||
private notificationCounts: Partial<Record<NotificationCountType, number>> = {};
|
||||
private readonly timelineSets: EventTimelineSet[];
|
||||
// any filtered timeline sets we're maintaining for this room
|
||||
@@ -192,6 +252,25 @@ export class Room extends EventEmitter {
|
||||
* @experimental
|
||||
*/
|
||||
public threads = new Map<string, Thread>();
|
||||
public lastThread: Thread;
|
||||
|
||||
/**
|
||||
* A mapping of eventId to all visibility changes to apply
|
||||
* to the event, by chronological order, as per
|
||||
* https://github.com/matrix-org/matrix-doc/pull/3531
|
||||
*
|
||||
* # Invariants
|
||||
*
|
||||
* - within each list, all events are classed by
|
||||
* chronological order;
|
||||
* - all events are events such that
|
||||
* `asVisibilityEvent()` returns a non-null `IVisibilityChange`;
|
||||
* - within each list with key `eventId`, all events
|
||||
* are in relation to `eventId`.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
private visibilityEvents = new Map<string, MatrixEvent[]>();
|
||||
|
||||
/**
|
||||
* Construct a new Room.
|
||||
@@ -244,7 +323,7 @@ export class Room extends EventEmitter {
|
||||
// In some cases, we add listeners for every displayed Matrix event, so it's
|
||||
// common to have quite a few more than the default limit.
|
||||
this.setMaxListeners(100);
|
||||
this.reEmitter = new ReEmitter(this);
|
||||
this.reEmitter = new TypedReEmitter(this);
|
||||
|
||||
opts.pendingEventOrdering = opts.pendingEventOrdering || PendingEventOrdering.Chronological;
|
||||
|
||||
@@ -253,7 +332,10 @@ export class Room extends EventEmitter {
|
||||
// all our per-room timeline sets. the first one is the unfiltered ones;
|
||||
// the subsequent ones are the filtered ones in no particular order.
|
||||
this.timelineSets = [new EventTimelineSet(this, opts)];
|
||||
this.reEmitter.reEmit(this.getUnfilteredTimelineSet(), ["Room.timeline", "Room.timelineReset"]);
|
||||
this.reEmitter.reEmit(this.getUnfilteredTimelineSet(), [
|
||||
RoomEvent.Timeline,
|
||||
RoomEvent.TimelineReset,
|
||||
]);
|
||||
|
||||
this.fixUpLegacyTimelineFields();
|
||||
|
||||
@@ -468,16 +550,14 @@ export class Room extends EventEmitter {
|
||||
*
|
||||
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
|
||||
*/
|
||||
public getPendingEvents(thread?: Thread): MatrixEvent[] {
|
||||
public getPendingEvents(): MatrixEvent[] {
|
||||
if (this.opts.pendingEventOrdering !== PendingEventOrdering.Detached) {
|
||||
throw new Error(
|
||||
"Cannot call getPendingEvents with pendingEventOrdering == " +
|
||||
this.opts.pendingEventOrdering);
|
||||
}
|
||||
|
||||
return this.pendingEventList.filter(event => {
|
||||
return !thread || thread.id === event.threadRootId;
|
||||
});
|
||||
return this.pendingEventList;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -669,7 +749,7 @@ export class Room extends EventEmitter {
|
||||
if (membership === "leave") {
|
||||
this.cleanupAfterLeaving();
|
||||
}
|
||||
this.emit("Room.myMembership", this, membership, prevMembership);
|
||||
this.emit(RoomEvent.MyMembership, this, membership, prevMembership);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1242,7 +1322,10 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
const opts = Object.assign({ filter: filter }, this.opts);
|
||||
const timelineSet = new EventTimelineSet(this, opts);
|
||||
this.reEmitter.reEmit(timelineSet, ["Room.timeline", "Room.timelineReset"]);
|
||||
this.reEmitter.reEmit(timelineSet, [
|
||||
RoomEvent.Timeline,
|
||||
RoomEvent.TimelineReset,
|
||||
]);
|
||||
this.filteredTimelineSets[filter.filterId] = timelineSet;
|
||||
this.timelineSets.push(timelineSet);
|
||||
|
||||
@@ -1304,7 +1387,7 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
public findThreadForEvent(event: MatrixEvent): Thread {
|
||||
public findThreadForEvent(event: MatrixEvent): Thread | null {
|
||||
if (!event) {
|
||||
return null;
|
||||
}
|
||||
@@ -1314,7 +1397,7 @@ export class Room extends EventEmitter {
|
||||
} else if (event.isThreadRoot) {
|
||||
return this.threads.get(event.getId());
|
||||
} else {
|
||||
const parentEvent = this.findEventById(event.parentEventId);
|
||||
const parentEvent = this.findEventById(event.getAssociatedId());
|
||||
return this.findThreadForEvent(parentEvent);
|
||||
}
|
||||
}
|
||||
@@ -1323,46 +1406,69 @@ export class Room extends EventEmitter {
|
||||
* Add an event to a thread's timeline. Will fire "Thread.update"
|
||||
* @experimental
|
||||
*/
|
||||
public async addThreadedEvent(event: MatrixEvent): Promise<void> {
|
||||
public async addThreadedEvent(event: MatrixEvent, toStartOfTimeline: boolean): Promise<void> {
|
||||
let thread = this.findThreadForEvent(event);
|
||||
if (thread) {
|
||||
thread.addEvent(event);
|
||||
thread.addEvent(event, toStartOfTimeline);
|
||||
} else {
|
||||
const events = [event];
|
||||
let rootEvent = this.findEventById(event.threadRootId);
|
||||
// If the rootEvent does not exist in the current sync, then look for
|
||||
// it over the network
|
||||
if (!rootEvent) {
|
||||
const eventData = await this.client.fetchRoomEvent(this.roomId, event.threadRootId);
|
||||
rootEvent = new MatrixEvent(eventData);
|
||||
}
|
||||
events.unshift(rootEvent);
|
||||
thread = this.createThread(events);
|
||||
}
|
||||
try {
|
||||
let eventData;
|
||||
if (event.threadRootId) {
|
||||
eventData = await this.client.fetchRoomEvent(this.roomId, event.threadRootId);
|
||||
}
|
||||
|
||||
if (event.getUnsigned().transaction_id) {
|
||||
const existingEvent = this.txnToEvent[event.getUnsigned().transaction_id];
|
||||
if (existingEvent) {
|
||||
// remote echo of an event we sent earlier
|
||||
this.handleRemoteEcho(event, existingEvent);
|
||||
return;
|
||||
if (!rootEvent) {
|
||||
rootEvent = new MatrixEvent(eventData);
|
||||
} else {
|
||||
rootEvent.setUnsigned(eventData.unsigned);
|
||||
}
|
||||
} finally {
|
||||
// The root event might be not be visible to the person requesting
|
||||
// it. If it wasn't fetched successfully the thread will work
|
||||
// in "limited" mode and won't benefit from all the APIs a homeserver
|
||||
// can provide to enhance the thread experience
|
||||
thread = this.createThread(rootEvent, events);
|
||||
}
|
||||
}
|
||||
|
||||
this.emit(ThreadEvent.Update, thread);
|
||||
}
|
||||
|
||||
public createThread(events: MatrixEvent[]): Thread {
|
||||
const thread = new Thread(events, this, this.client);
|
||||
this.threads.set(thread.id, thread);
|
||||
this.reEmitter.reEmit(thread, [
|
||||
ThreadEvent.Update,
|
||||
ThreadEvent.Ready,
|
||||
"Room.timeline",
|
||||
"Room.timelineReset",
|
||||
]);
|
||||
this.emit(ThreadEvent.New, thread);
|
||||
return thread;
|
||||
public createThread(rootEvent: MatrixEvent | undefined, events: MatrixEvent[] = []): Thread | undefined {
|
||||
if (rootEvent) {
|
||||
const tl = this.getTimelineForEvent(rootEvent.getId());
|
||||
const relatedEvents = tl?.getTimelineSet().getAllRelationsEventForEvent(rootEvent.getId());
|
||||
if (relatedEvents) {
|
||||
events = events.concat(relatedEvents);
|
||||
}
|
||||
}
|
||||
|
||||
const thread = new Thread(rootEvent, {
|
||||
initialEvents: events,
|
||||
room: this,
|
||||
client: this.client,
|
||||
});
|
||||
// If we managed to create a thread and figure out its `id`
|
||||
// then we can use it
|
||||
if (thread.id) {
|
||||
this.threads.set(thread.id, thread);
|
||||
this.reEmitter.reEmit(thread, [
|
||||
ThreadEvent.Update,
|
||||
RoomEvent.Timeline,
|
||||
RoomEvent.TimelineReset,
|
||||
]);
|
||||
|
||||
if (!this.lastThread || this.lastThread.rootEvent?.localTimestamp < rootEvent?.localTimestamp) {
|
||||
this.lastThread = thread;
|
||||
}
|
||||
|
||||
this.emit(ThreadEvent.New, thread);
|
||||
return thread;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1395,13 +1501,22 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
this.emit("Room.redaction", event, this);
|
||||
this.emit(RoomEvent.Redaction, event, this);
|
||||
|
||||
// TODO: we stash user displaynames (among other things) in
|
||||
// RoomMember objects which are then attached to other events
|
||||
// (in the sender and target fields). We should get those
|
||||
// RoomMember objects to update themselves when the events that
|
||||
// they are based on are changed.
|
||||
|
||||
// Remove any visibility change on this event.
|
||||
this.visibilityEvents.delete(redactId);
|
||||
|
||||
// If this event is a visibility change event, remove it from the
|
||||
// list of visibility changes and update any event affected by it.
|
||||
if (redactedEvent.isVisibilityEvent()) {
|
||||
this.redactVisibilityChangeEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: apply redactions to notification list
|
||||
@@ -1411,6 +1526,15 @@ export class Room extends EventEmitter {
|
||||
// this may be needed to trigger an update.
|
||||
}
|
||||
|
||||
// Implement MSC3531: hiding messages.
|
||||
if (event.isVisibilityEvent()) {
|
||||
// This event changes the visibility of another event, record
|
||||
// the visibility change, inform clients if necessary.
|
||||
this.applyNewVisibilityEvent(event);
|
||||
}
|
||||
// If any pending visibility change is waiting for this (older) event,
|
||||
this.applyPendingVisibilityEvents(event);
|
||||
|
||||
if (event.getUnsigned().transaction_id) {
|
||||
const existingEvent = this.txnToEvent[event.getUnsigned().transaction_id];
|
||||
if (existingEvent) {
|
||||
@@ -1477,8 +1601,7 @@ export class Room extends EventEmitter {
|
||||
EventTimeline.setEventMetadata(event, this.getLiveTimeline().getState(EventTimeline.FORWARDS), false);
|
||||
|
||||
this.txnToEvent[txnId] = event;
|
||||
const thread = this.threads.get(event.threadRootId);
|
||||
if (this.opts.pendingEventOrdering === PendingEventOrdering.Detached && !thread) {
|
||||
if (this.opts.pendingEventOrdering === PendingEventOrdering.Detached) {
|
||||
if (this.pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) {
|
||||
logger.warn("Setting event as NOT_SENT due to messages in the same state");
|
||||
event.setStatus(EventStatus.NOT_SENT);
|
||||
@@ -1494,37 +1617,31 @@ export class Room extends EventEmitter {
|
||||
|
||||
if (event.isRedaction()) {
|
||||
const redactId = event.event.redacts;
|
||||
let redactedEvent = this.pendingEventList &&
|
||||
this.pendingEventList.find(e => e.getId() === redactId);
|
||||
let redactedEvent = this.pendingEventList?.find(e => e.getId() === redactId);
|
||||
if (!redactedEvent) {
|
||||
redactedEvent = this.findEventById(redactId);
|
||||
}
|
||||
if (redactedEvent) {
|
||||
redactedEvent.markLocallyRedacted(event);
|
||||
this.emit("Room.redaction", event, this);
|
||||
this.emit(RoomEvent.Redaction, event, this);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (thread) {
|
||||
thread.timelineSet.addEventToTimeline(event,
|
||||
thread.timelineSet.getLiveTimeline(), false);
|
||||
} else {
|
||||
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||
const timelineSet = this.timelineSets[i];
|
||||
if (timelineSet.getFilter()) {
|
||||
if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
|
||||
timelineSet.addEventToTimeline(event,
|
||||
timelineSet.getLiveTimeline(), false);
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||
const timelineSet = this.timelineSets[i];
|
||||
if (timelineSet.getFilter()) {
|
||||
if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
|
||||
timelineSet.addEventToTimeline(event,
|
||||
timelineSet.getLiveTimeline(), false);
|
||||
}
|
||||
} else {
|
||||
timelineSet.addEventToTimeline(event,
|
||||
timelineSet.getLiveTimeline(), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.emit("Room.localEchoUpdated", event, this, null, null);
|
||||
this.emit(RoomEvent.LocalEchoUpdated, event, this, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1580,7 +1697,9 @@ export class Room extends EventEmitter {
|
||||
const thread = this.findThreadForEvent(event);
|
||||
if (thread) {
|
||||
thread.timelineSet.aggregateRelations(event);
|
||||
} else {
|
||||
}
|
||||
|
||||
if (thread?.id === event.getAssociatedId() || !thread) {
|
||||
// TODO: We should consider whether this means it would be a better
|
||||
// design to lift the relations handling up to the room instead.
|
||||
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||
@@ -1596,6 +1715,10 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
public getEventForTxnId(txnId: string): MatrixEvent {
|
||||
return this.txnToEvent[txnId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deal with the echo of a message we sent.
|
||||
*
|
||||
@@ -1610,7 +1733,7 @@ export class Room extends EventEmitter {
|
||||
* @fires module:client~MatrixClient#event:"Room.localEchoUpdated"
|
||||
* @private
|
||||
*/
|
||||
private handleRemoteEcho(remoteEvent: MatrixEvent, localEvent: MatrixEvent): void {
|
||||
public handleRemoteEcho(remoteEvent: MatrixEvent, localEvent: MatrixEvent): void {
|
||||
const oldEventId = localEvent.getId();
|
||||
const newEventId = remoteEvent.getId();
|
||||
const oldStatus = localEvent.status;
|
||||
@@ -1632,10 +1755,12 @@ export class Room extends EventEmitter {
|
||||
// any, which is good, because we don't want to try decoding it again).
|
||||
localEvent.handleRemoteEcho(remoteEvent.event);
|
||||
|
||||
const thread = this.threads.get(remoteEvent.threadRootId);
|
||||
const thread = this.findThreadForEvent(remoteEvent);
|
||||
if (thread) {
|
||||
thread.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
|
||||
} else {
|
||||
}
|
||||
|
||||
if (thread?.id === remoteEvent.getAssociatedId() || !thread) {
|
||||
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||
const timelineSet = this.timelineSets[i];
|
||||
|
||||
@@ -1644,8 +1769,7 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
this.emit("Room.localEchoUpdated", localEvent, this,
|
||||
oldEventId, oldStatus);
|
||||
this.emit(RoomEvent.LocalEchoUpdated, localEvent, this, oldEventId, oldStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1705,7 +1829,8 @@ export class Room extends EventEmitter {
|
||||
const thread = this.findThreadForEvent(event);
|
||||
if (thread) {
|
||||
thread.timelineSet.replaceEventId(oldEventId, newEventId);
|
||||
} else {
|
||||
}
|
||||
if (thread?.id === event.getAssociatedId() || !thread) {
|
||||
// if the event was already in the timeline (which will be the case if
|
||||
// opts.pendingEventOrdering==chronological), we need to update the
|
||||
// timeline map.
|
||||
@@ -1728,7 +1853,7 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
this.savePendingEvents();
|
||||
|
||||
this.emit("Room.localEchoUpdated", event, this, oldEventId, oldStatus);
|
||||
this.emit(RoomEvent.LocalEchoUpdated, event, this, oldEventId, oldStatus);
|
||||
}
|
||||
|
||||
private revertRedactionLocalEcho(redactionEvent: MatrixEvent): void {
|
||||
@@ -1741,7 +1866,7 @@ export class Room extends EventEmitter {
|
||||
if (redactedEvent) {
|
||||
redactedEvent.unmarkLocallyRedacted();
|
||||
// re-render after undoing redaction
|
||||
this.emit("Room.redactionCancelled", redactionEvent, this);
|
||||
this.emit(RoomEvent.RedactionCancelled, redactionEvent, this);
|
||||
// reapply relation now redaction failed
|
||||
if (redactedEvent.isRelation()) {
|
||||
this.aggregateNonLiveRelation(redactedEvent);
|
||||
@@ -1793,7 +1918,7 @@ export class Room extends EventEmitter {
|
||||
// TODO: We should have a filter to say "only add state event
|
||||
// types X Y Z to the timeline".
|
||||
this.addLiveEvent(events[i], duplicateStrategy, fromCache);
|
||||
const thread = this.threads.get(events[i].getId());
|
||||
const thread = this.findThreadForEvent(events[i]);
|
||||
if (thread) {
|
||||
thread.addEvent(events[i], true);
|
||||
}
|
||||
@@ -1881,7 +2006,7 @@ export class Room extends EventEmitter {
|
||||
});
|
||||
|
||||
if (oldName !== this.name) {
|
||||
this.emit("Room.name", this);
|
||||
this.emit(RoomEvent.Name, this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1899,19 +2024,12 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
|
||||
public getReadReceiptForUserId(userId: string, ignoreSynthesized = false): IWrappedReceipt | null {
|
||||
let receipts = this.receipts;
|
||||
const [realReceipt, syntheticReceipt] = this.receipts["m.read"]?.[userId] ?? [];
|
||||
if (ignoreSynthesized) {
|
||||
receipts = this.realReceipts;
|
||||
return realReceipt;
|
||||
}
|
||||
|
||||
if (
|
||||
receipts["m.read"] === undefined ||
|
||||
receipts["m.read"][userId] === undefined
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return receipts["m.read"][userId];
|
||||
return syntheticReceipt ?? realReceipt;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1975,84 +2093,115 @@ export class Room extends EventEmitter {
|
||||
/**
|
||||
* Add a receipt event to the room.
|
||||
* @param {MatrixEvent} event The m.receipt event.
|
||||
* @param {Boolean} fake True if this event is implicit
|
||||
* @param {Boolean} synthetic True if this event is implicit.
|
||||
*/
|
||||
public addReceipt(event: MatrixEvent, fake = false): void {
|
||||
if (!fake) {
|
||||
this.addReceiptsToStructure(event, this.realReceipts);
|
||||
// we don't bother caching real receipts by event ID
|
||||
// as there's nothing that would read it.
|
||||
}
|
||||
this.addReceiptsToStructure(event, this.receipts);
|
||||
this.receiptCacheByEventId = this.buildReceiptCache(this.receipts);
|
||||
|
||||
// send events after we've regenerated the cache, otherwise things that
|
||||
// listened for the event would read from a stale cache
|
||||
this.emit("Room.receipt", event, this);
|
||||
public addReceipt(event: MatrixEvent, synthetic = false): void {
|
||||
this.addReceiptsToStructure(event, synthetic);
|
||||
// send events after we've regenerated the structure & cache, otherwise things that
|
||||
// listened for the event would read stale data.
|
||||
this.emit(RoomEvent.Receipt, event, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a receipt event to the room.
|
||||
* @param {MatrixEvent} event The m.receipt event.
|
||||
* @param {Object} receipts The object to add receipts to
|
||||
* @param {Boolean} synthetic True if this event is implicit.
|
||||
*/
|
||||
private addReceiptsToStructure(event: MatrixEvent, receipts: Receipts): void {
|
||||
private addReceiptsToStructure(event: MatrixEvent, synthetic: boolean): void {
|
||||
const content = event.getContent<IReceiptContent>();
|
||||
Object.keys(content).forEach((eventId) => {
|
||||
Object.keys(content[eventId]).forEach((receiptType) => {
|
||||
Object.keys(content[eventId][receiptType]).forEach((userId) => {
|
||||
const receipt = content[eventId][receiptType][userId];
|
||||
|
||||
if (!receipts[receiptType]) {
|
||||
receipts[receiptType] = {};
|
||||
if (!this.receipts[receiptType]) {
|
||||
this.receipts[receiptType] = {};
|
||||
}
|
||||
if (!this.receipts[receiptType][userId]) {
|
||||
this.receipts[receiptType][userId] = [null, null];
|
||||
}
|
||||
|
||||
const existingReceipt = receipts[receiptType][userId];
|
||||
const pair = this.receipts[receiptType][userId];
|
||||
|
||||
if (!existingReceipt) {
|
||||
receipts[receiptType][userId] = {} as IWrappedReceipt;
|
||||
} else {
|
||||
// we only want to add this receipt if we think it is later
|
||||
// than the one we already have. (This is managed
|
||||
// server-side, but because we synthesize RRs locally we
|
||||
// have to do it here too.)
|
||||
let existingReceipt = pair[ReceiptPairRealIndex];
|
||||
if (synthetic) {
|
||||
existingReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
|
||||
}
|
||||
|
||||
if (existingReceipt) {
|
||||
// we only want to add this receipt if we think it is later than the one we already have.
|
||||
// This is managed server-side, but because we synthesize RRs locally we have to do it here too.
|
||||
const ordering = this.getUnfilteredTimelineSet().compareEventOrdering(
|
||||
existingReceipt.eventId, eventId);
|
||||
existingReceipt.eventId,
|
||||
eventId,
|
||||
);
|
||||
if (ordering !== null && ordering >= 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
receipts[receiptType][userId] = {
|
||||
eventId: eventId,
|
||||
const wrappedReceipt: IWrappedReceipt = {
|
||||
eventId,
|
||||
data: receipt,
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and return a map of receipts by event ID
|
||||
* @param {Object} receipts A map of receipts
|
||||
* @return {Object} Map of receipts by event ID
|
||||
*/
|
||||
private buildReceiptCache(receipts: Receipts): ReceiptCache {
|
||||
const receiptCacheByEventId: ReceiptCache = {};
|
||||
Object.keys(receipts).forEach(function(receiptType) {
|
||||
Object.keys(receipts[receiptType]).forEach(function(userId) {
|
||||
const receipt = receipts[receiptType][userId];
|
||||
if (!receiptCacheByEventId[receipt.eventId]) {
|
||||
receiptCacheByEventId[receipt.eventId] = [];
|
||||
}
|
||||
receiptCacheByEventId[receipt.eventId].push({
|
||||
userId: userId,
|
||||
type: receiptType,
|
||||
data: receipt.data,
|
||||
const realReceipt = synthetic ? pair[ReceiptPairRealIndex] : wrappedReceipt;
|
||||
const syntheticReceipt = synthetic ? wrappedReceipt : pair[ReceiptPairSyntheticIndex];
|
||||
|
||||
let ordering: number | null = null;
|
||||
if (realReceipt && syntheticReceipt) {
|
||||
ordering = this.getUnfilteredTimelineSet().compareEventOrdering(
|
||||
realReceipt.eventId,
|
||||
syntheticReceipt.eventId,
|
||||
);
|
||||
}
|
||||
|
||||
const preferSynthetic = ordering === null || ordering < 0;
|
||||
|
||||
// we don't bother caching just real receipts by event ID as there's nothing that would read it.
|
||||
// Take the current cached receipt before we overwrite the pair elements.
|
||||
const cachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
|
||||
|
||||
if (synthetic && preferSynthetic) {
|
||||
pair[ReceiptPairSyntheticIndex] = wrappedReceipt;
|
||||
} else if (!synthetic) {
|
||||
pair[ReceiptPairRealIndex] = wrappedReceipt;
|
||||
|
||||
if (!preferSynthetic) {
|
||||
pair[ReceiptPairSyntheticIndex] = null;
|
||||
}
|
||||
}
|
||||
|
||||
const newCachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
|
||||
if (cachedReceipt === newCachedReceipt) return;
|
||||
|
||||
// clean up any previous cache entry
|
||||
if (cachedReceipt && this.receiptCacheByEventId[cachedReceipt.eventId]) {
|
||||
const previousEventId = cachedReceipt.eventId;
|
||||
// Remove the receipt we're about to clobber out of existence from the cache
|
||||
this.receiptCacheByEventId[previousEventId] = (
|
||||
this.receiptCacheByEventId[previousEventId].filter(r => {
|
||||
return r.type !== receiptType || r.userId !== userId;
|
||||
})
|
||||
);
|
||||
|
||||
if (this.receiptCacheByEventId[previousEventId].length < 1) {
|
||||
delete this.receiptCacheByEventId[previousEventId]; // clean up the cache keys
|
||||
}
|
||||
}
|
||||
|
||||
// cache the new one
|
||||
if (!this.receiptCacheByEventId[eventId]) {
|
||||
this.receiptCacheByEventId[eventId] = [];
|
||||
}
|
||||
this.receiptCacheByEventId[eventId].push({
|
||||
userId: userId,
|
||||
type: receiptType,
|
||||
data: receipt,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
return receiptCacheByEventId;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2084,7 +2233,7 @@ export class Room extends EventEmitter {
|
||||
|
||||
// XXX: we could do a deep-comparison to see if the tags have really
|
||||
// changed - but do we want to bother?
|
||||
this.emit("Room.tags", event, this);
|
||||
this.emit(RoomEvent.Tags, event, this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2099,7 +2248,7 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
const lastEvent = this.accountData[event.getType()];
|
||||
this.accountData[event.getType()] = event;
|
||||
this.emit("Room.accountData", event, this, lastEvent);
|
||||
this.emit(RoomEvent.AccountData, event, this, lastEvent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2118,8 +2267,9 @@ export class Room extends EventEmitter {
|
||||
* message events into the room.
|
||||
*/
|
||||
public maySendMessage(): boolean {
|
||||
return this.getMyMembership() === 'join' &&
|
||||
this.currentState.maySendEvent(EventType.RoomMessage, this.myUserId);
|
||||
return this.getMyMembership() === 'join' && (this.client.isRoomEncrypted(this.roomId)
|
||||
? this.currentState.maySendEvent(EventType.RoomMessageEncrypted, this.myUserId)
|
||||
: this.currentState.maySendEvent(EventType.RoomMessage, this.myUserId));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2292,6 +2442,161 @@ export class Room extends EventEmitter {
|
||||
return "Empty room";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When we receive a new visibility change event:
|
||||
*
|
||||
* - store this visibility change alongside the timeline, in case we
|
||||
* later need to apply it to an event that we haven't received yet;
|
||||
* - if we have already received the event whose visibility has changed,
|
||||
* patch it to reflect the visibility change and inform listeners.
|
||||
*/
|
||||
private applyNewVisibilityEvent(event: MatrixEvent): void {
|
||||
const visibilityChange = event.asVisibilityChange();
|
||||
if (!visibilityChange) {
|
||||
// The event is ill-formed.
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore visibility change events that are not emitted by moderators.
|
||||
const userId = event.getSender();
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
const isPowerSufficient =
|
||||
(
|
||||
EVENT_VISIBILITY_CHANGE_TYPE.name
|
||||
&& this.currentState.maySendStateEvent(EVENT_VISIBILITY_CHANGE_TYPE.name, userId)
|
||||
)
|
||||
|| (
|
||||
EVENT_VISIBILITY_CHANGE_TYPE.altName
|
||||
&& this.currentState.maySendStateEvent(EVENT_VISIBILITY_CHANGE_TYPE.altName, userId)
|
||||
);
|
||||
if (!isPowerSufficient) {
|
||||
// Powerlevel is insufficient.
|
||||
return;
|
||||
}
|
||||
|
||||
// Record this change in visibility.
|
||||
// If the event is not in our timeline and we only receive it later,
|
||||
// we may need to apply the visibility change at a later date.
|
||||
|
||||
const visibilityEventsOnOriginalEvent = this.visibilityEvents.get(visibilityChange.eventId);
|
||||
if (visibilityEventsOnOriginalEvent) {
|
||||
// It would be tempting to simply erase the latest visibility change
|
||||
// but we need to record all of the changes in case the latest change
|
||||
// is ever redacted.
|
||||
//
|
||||
// In practice, linear scans through `visibilityEvents` should be fast.
|
||||
// However, to protect against a potential DoS attack, we limit the
|
||||
// number of iterations in this loop.
|
||||
let index = visibilityEventsOnOriginalEvent.length - 1;
|
||||
const min = Math.max(0,
|
||||
visibilityEventsOnOriginalEvent.length - MAX_NUMBER_OF_VISIBILITY_EVENTS_TO_SCAN_THROUGH);
|
||||
for (; index >= min; --index) {
|
||||
const target = visibilityEventsOnOriginalEvent[index];
|
||||
if (target.getTs() < event.getTs()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index === -1) {
|
||||
visibilityEventsOnOriginalEvent.unshift(event);
|
||||
} else {
|
||||
visibilityEventsOnOriginalEvent.splice(index + 1, 0, event);
|
||||
}
|
||||
} else {
|
||||
this.visibilityEvents.set(visibilityChange.eventId, [event]);
|
||||
}
|
||||
|
||||
// Finally, let's check if the event is already in our timeline.
|
||||
// If so, we need to patch it and inform listeners.
|
||||
|
||||
const originalEvent = this.findEventById(visibilityChange.eventId);
|
||||
if (!originalEvent) {
|
||||
return;
|
||||
}
|
||||
originalEvent.applyVisibilityEvent(visibilityChange);
|
||||
}
|
||||
|
||||
private redactVisibilityChangeEvent(event: MatrixEvent) {
|
||||
// Sanity checks.
|
||||
if (!event.isVisibilityEvent) {
|
||||
throw new Error("expected a visibility change event");
|
||||
}
|
||||
const relation = event.getRelation();
|
||||
const originalEventId = relation.event_id;
|
||||
const visibilityEventsOnOriginalEvent = this.visibilityEvents.get(originalEventId);
|
||||
if (!visibilityEventsOnOriginalEvent) {
|
||||
// No visibility changes on the original event.
|
||||
// In particular, this change event was not recorded,
|
||||
// most likely because it was ill-formed.
|
||||
return;
|
||||
}
|
||||
const index = visibilityEventsOnOriginalEvent.findIndex(change => change.getId() === event.getId());
|
||||
if (index === -1) {
|
||||
// This change event was not recorded, most likely because
|
||||
// it was ill-formed.
|
||||
return;
|
||||
}
|
||||
// Remove visibility change.
|
||||
visibilityEventsOnOriginalEvent.splice(index, 1);
|
||||
|
||||
// If we removed the latest visibility change event, propagate changes.
|
||||
if (index === visibilityEventsOnOriginalEvent.length) {
|
||||
const originalEvent = this.findEventById(originalEventId);
|
||||
if (!originalEvent) {
|
||||
return;
|
||||
}
|
||||
if (index === 0) {
|
||||
// We have just removed the only visibility change event.
|
||||
this.visibilityEvents.delete(originalEventId);
|
||||
originalEvent.applyVisibilityEvent();
|
||||
} else {
|
||||
const newEvent = visibilityEventsOnOriginalEvent[visibilityEventsOnOriginalEvent.length - 1];
|
||||
const newVisibility = newEvent.asVisibilityChange();
|
||||
if (!newVisibility) {
|
||||
// Event is ill-formed.
|
||||
// This breaks our invariant.
|
||||
throw new Error("at this stage, visibility changes should be well-formed");
|
||||
}
|
||||
originalEvent.applyVisibilityEvent(newVisibility);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When we receive an event whose visibility has been altered by
|
||||
* a (more recent) visibility change event, patch the event in
|
||||
* place so that clients now not to display it.
|
||||
*
|
||||
* @param event Any matrix event. If this event has at least one a
|
||||
* pending visibility change event, apply the latest visibility
|
||||
* change event.
|
||||
*/
|
||||
private applyPendingVisibilityEvents(event: MatrixEvent): void {
|
||||
const visibilityEvents = this.visibilityEvents.get(event.getId());
|
||||
if (!visibilityEvents || visibilityEvents.length == 0) {
|
||||
// No pending visibility change in store.
|
||||
return;
|
||||
}
|
||||
const visibilityEvent = visibilityEvents[visibilityEvents.length - 1];
|
||||
const visibilityChange = visibilityEvent.asVisibilityChange();
|
||||
if (!visibilityChange) {
|
||||
return;
|
||||
}
|
||||
if (visibilityChange.visible) {
|
||||
// Events are visible by default, no need to apply a visibility change.
|
||||
// Note that we need to keep the visibility changes in `visibilityEvents`,
|
||||
// in case we later fetch an older visibility change event that is superseded
|
||||
// by `visibilityChange`.
|
||||
}
|
||||
if (visibilityEvent.getTs() < event.getTs()) {
|
||||
// Something is wrong, the visibility change cannot happen before the
|
||||
// event. Presumably an ill-formed event.
|
||||
return;
|
||||
}
|
||||
event.applyVisibilityEvent(visibilityChange);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2302,12 +2607,12 @@ function pendingEventsKey(roomId: string): string {
|
||||
return `mx_pending_events_${roomId}`;
|
||||
}
|
||||
|
||||
/* a map from current event status to a list of allowed next statuses
|
||||
*/
|
||||
// a map from current event status to a list of allowed next statuses
|
||||
const ALLOWED_TRANSITIONS: Record<EventStatus, EventStatus[]> = {
|
||||
[EventStatus.ENCRYPTING]: [
|
||||
EventStatus.SENDING,
|
||||
EventStatus.NOT_SENT,
|
||||
EventStatus.CANCELLED,
|
||||
],
|
||||
[EventStatus.SENDING]: [
|
||||
EventStatus.ENCRYPTING,
|
||||
@@ -2460,3 +2765,4 @@ function memberNamesToRoomName(names: string[], count = (names.length + 1)) {
|
||||
* @param {string} membership The new membership value
|
||||
* @param {string} prevMembership The previous membership value
|
||||
*/
|
||||
|
||||
|
||||
+256
-88
@@ -14,118 +14,252 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "../matrix";
|
||||
import { ReEmitter } from "../ReEmitter";
|
||||
import { MatrixEvent } from "./event";
|
||||
import { EventTimeline } from "./event-timeline";
|
||||
import { EventTimelineSet } from './event-timeline-set';
|
||||
import { MatrixClient, RoomEvent } from "../matrix";
|
||||
import { TypedReEmitter } from "../ReEmitter";
|
||||
import { RelationType } from "../@types/event";
|
||||
import { IRelationsRequestOpts } from "../@types/requests";
|
||||
import { IThreadBundledRelationship, MatrixEvent } from "./event";
|
||||
import { Direction, EventTimeline } from "./event-timeline";
|
||||
import { EventTimelineSet, EventTimelineSetHandlerMap } from './event-timeline-set';
|
||||
import { Room } from './room';
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
import { RoomState } from "./room-state";
|
||||
|
||||
export enum ThreadEvent {
|
||||
New = "Thread.new",
|
||||
Ready = "Thread.ready",
|
||||
Update = "Thread.update",
|
||||
NewReply = "Thread.newReply",
|
||||
ViewThread = "Thred.viewThread",
|
||||
ViewThread = "Thread.viewThread",
|
||||
}
|
||||
|
||||
type EmittedEvents = Exclude<ThreadEvent, ThreadEvent.New>
|
||||
| RoomEvent.Timeline
|
||||
| RoomEvent.TimelineReset;
|
||||
|
||||
export type EventHandlerMap = {
|
||||
[ThreadEvent.Update]: (thread: Thread) => void;
|
||||
[ThreadEvent.NewReply]: (thread: Thread, event: MatrixEvent) => void;
|
||||
[ThreadEvent.ViewThread]: () => void;
|
||||
} & EventTimelineSetHandlerMap;
|
||||
|
||||
interface IThreadOpts {
|
||||
initialEvents?: MatrixEvent[];
|
||||
room: Room;
|
||||
client: MatrixClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
export class Thread extends TypedEventEmitter<ThreadEvent> {
|
||||
/**
|
||||
* A reference to the event ID at the top of the thread
|
||||
*/
|
||||
private root: string;
|
||||
export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
||||
public static hasServerSideSupport: boolean;
|
||||
private static serverSupportPromise: Promise<boolean> | null;
|
||||
|
||||
/**
|
||||
* A reference to all the events ID at the bottom of the threads
|
||||
*/
|
||||
public readonly timelineSet;
|
||||
public readonly timelineSet: EventTimelineSet;
|
||||
|
||||
private _currentUserParticipated = false;
|
||||
|
||||
private reEmitter: ReEmitter;
|
||||
private reEmitter: TypedReEmitter<EmittedEvents, EventHandlerMap>;
|
||||
|
||||
private lastEvent: MatrixEvent;
|
||||
private replyCount = 0;
|
||||
|
||||
public readonly room: Room;
|
||||
public readonly client: MatrixClient;
|
||||
|
||||
public initialEventsFetched = false;
|
||||
|
||||
public readonly id: string;
|
||||
|
||||
constructor(
|
||||
events: MatrixEvent[] = [],
|
||||
public readonly room: Room,
|
||||
public readonly client: MatrixClient,
|
||||
public readonly rootEvent: MatrixEvent | undefined,
|
||||
opts: IThreadOpts,
|
||||
) {
|
||||
super();
|
||||
if (events.length === 0) {
|
||||
throw new Error("Can't create an empty thread");
|
||||
}
|
||||
|
||||
this.reEmitter = new ReEmitter(this);
|
||||
|
||||
this.room = opts.room;
|
||||
this.client = opts.client;
|
||||
this.timelineSet = new EventTimelineSet(this.room, {
|
||||
unstableClientRelationAggregation: true,
|
||||
timelineSupport: true,
|
||||
pendingEvents: true,
|
||||
});
|
||||
this.reEmitter = new TypedReEmitter(this);
|
||||
|
||||
this.reEmitter.reEmit(this.timelineSet, [
|
||||
"Room.timeline",
|
||||
"Room.timelineReset",
|
||||
RoomEvent.Timeline,
|
||||
RoomEvent.TimelineReset,
|
||||
]);
|
||||
|
||||
events.forEach(event => this.addEvent(event));
|
||||
if (Thread.hasServerSideSupport === undefined) {
|
||||
Thread.serverSupportPromise = this.client.doesServerSupportUnstableFeature("org.matrix.msc3440");
|
||||
Thread.serverSupportPromise.then((serverSupportsThread) => {
|
||||
Thread.hasServerSideSupport = serverSupportsThread;
|
||||
}).catch(() => {
|
||||
Thread.serverSupportPromise = null;
|
||||
});
|
||||
}
|
||||
|
||||
room.on("Room.localEchoUpdated", this.onEcho);
|
||||
room.on("Room.timeline", this.onEcho);
|
||||
// If we weren't able to find the root event, it's probably missing
|
||||
// and we define the thread ID from one of the thread relation
|
||||
if (!rootEvent) {
|
||||
this.id = opts?.initialEvents
|
||||
?.find(event => event.isThreadRelation)?.relationEventId;
|
||||
} else {
|
||||
this.id = rootEvent.getId();
|
||||
}
|
||||
this.initialiseThread(this.rootEvent);
|
||||
|
||||
opts?.initialEvents?.forEach(event => this.addEvent(event, false));
|
||||
|
||||
this.room.on(RoomEvent.LocalEchoUpdated, this.onEcho);
|
||||
this.room.on(RoomEvent.Timeline, this.onEcho);
|
||||
}
|
||||
|
||||
onEcho = (event: MatrixEvent) => {
|
||||
private onEcho = (event: MatrixEvent) => {
|
||||
if (this.timelineSet.eventIdToTimeline(event.getId())) {
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
}
|
||||
};
|
||||
|
||||
public get roomState(): RoomState {
|
||||
return this.room.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||
}
|
||||
|
||||
private addEventToTimeline(event: MatrixEvent, toStartOfTimeline: boolean): void {
|
||||
if (event.getUnsigned().transaction_id) {
|
||||
const existingEvent = this.room.getEventForTxnId(event.getUnsigned().transaction_id);
|
||||
if (existingEvent) {
|
||||
// remote echo of an event we sent earlier
|
||||
this.room.handleRemoteEcho(event, existingEvent);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.findEventById(event.getId())) {
|
||||
this.timelineSet.addEventToTimeline(
|
||||
event,
|
||||
this.liveTimeline,
|
||||
toStartOfTimeline,
|
||||
false,
|
||||
this.roomState,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an event to the thread and updates
|
||||
* the tail/root references if needed
|
||||
* Will fire "Thread.update"
|
||||
* @param event The event to add
|
||||
* @param {boolean} toStartOfTimeline whether the event is being added
|
||||
* to the start (and not the end) of the timeline.
|
||||
*/
|
||||
public async addEvent(event: MatrixEvent, toStartOfTimeline = false): Promise<void> {
|
||||
if (this.timelineSet.findEventById(event.getId()) || event.status !== null) {
|
||||
return;
|
||||
public async addEvent(event: MatrixEvent, toStartOfTimeline: boolean): Promise<void> {
|
||||
if (Thread.hasServerSideSupport === undefined) {
|
||||
await Thread.serverSupportPromise;
|
||||
}
|
||||
|
||||
if (!this.root) {
|
||||
if (event.isThreadRelation) {
|
||||
this.root = event.threadRootId;
|
||||
} else {
|
||||
this.root = event.getId();
|
||||
// Add all incoming events to the thread's timeline set when there's no server support
|
||||
if (!Thread.hasServerSideSupport) {
|
||||
// all the relevant membership info to hydrate events with a sender
|
||||
// is held in the main room timeline
|
||||
// We want to fetch the room state from there and pass it down to this thread
|
||||
// timeline set to let it reconcile an event with its relevant RoomMember
|
||||
|
||||
event.setThread(this);
|
||||
this.addEventToTimeline(event, toStartOfTimeline);
|
||||
|
||||
await this.client.decryptEventIfNeeded(event, {});
|
||||
}
|
||||
|
||||
if (Thread.hasServerSideSupport && this.initialEventsFetched) {
|
||||
if (event.localTimestamp > this.lastReply().localTimestamp) {
|
||||
this.addEventToTimeline(event, false);
|
||||
}
|
||||
}
|
||||
|
||||
// all the relevant membership info to hydrate events with a sender
|
||||
// is held in the main room timeline
|
||||
// We want to fetch the room state from there and pass it down to this thread
|
||||
// timeline set to let it reconcile an event with its relevant RoomMember
|
||||
const roomState = this.room.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||
|
||||
event.setThread(this);
|
||||
this.timelineSet.addEventToTimeline(
|
||||
event,
|
||||
this.timelineSet.getLiveTimeline(),
|
||||
toStartOfTimeline,
|
||||
false,
|
||||
roomState,
|
||||
);
|
||||
|
||||
if (!this._currentUserParticipated && event.getSender() === this.client.getUserId()) {
|
||||
this._currentUserParticipated = true;
|
||||
}
|
||||
|
||||
await this.client.decryptEventIfNeeded(event, {});
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
|
||||
if (event.isThreadRelation) {
|
||||
this.emit(ThreadEvent.NewReply, this, event);
|
||||
const isThreadReply = event.getRelation()?.rel_type === RelationType.Thread;
|
||||
// If no thread support exists we want to count all thread relation
|
||||
// added as a reply. We can't rely on the bundled relationships count
|
||||
if (!Thread.hasServerSideSupport && isThreadReply) {
|
||||
this.replyCount++;
|
||||
}
|
||||
|
||||
// There is a risk that the `localTimestamp` approximation will not be accurate
|
||||
// when threads are used over federation. That could results in the reply
|
||||
// count value drifting away from the value returned by the server
|
||||
if (!this.lastEvent || (isThreadReply && event.localTimestamp > this.replyToEvent.localTimestamp)) {
|
||||
this.lastEvent = event;
|
||||
if (this.lastEvent.getId() !== this.id) {
|
||||
// This counting only works when server side support is enabled
|
||||
// as we started the counting from the value returned in the
|
||||
// bundled relationship
|
||||
if (Thread.hasServerSideSupport) {
|
||||
this.replyCount++;
|
||||
}
|
||||
|
||||
this.emit(ThreadEvent.NewReply, this, event);
|
||||
}
|
||||
}
|
||||
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
}
|
||||
|
||||
private initialiseThread(rootEvent: MatrixEvent | undefined): void {
|
||||
if (Thread.hasServerSideSupport === undefined) {
|
||||
Thread.serverSupportPromise.then(() => {
|
||||
this.initialiseThread(rootEvent);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const bundledRelationship = rootEvent
|
||||
?.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
|
||||
|
||||
if (Thread.hasServerSideSupport && bundledRelationship) {
|
||||
this.replyCount = bundledRelationship.count;
|
||||
this._currentUserParticipated = bundledRelationship.current_user_participated;
|
||||
|
||||
const event = new MatrixEvent(bundledRelationship.latest_event);
|
||||
this.setEventMetadata(event);
|
||||
this.lastEvent = event;
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchInitialEvents(): Promise<{
|
||||
originalEvent: MatrixEvent;
|
||||
events: MatrixEvent[];
|
||||
nextBatch?: string;
|
||||
prevBatch?: string;
|
||||
} | null> {
|
||||
if (Thread.hasServerSideSupport === undefined) {
|
||||
await Thread.serverSupportPromise;
|
||||
}
|
||||
|
||||
if (!Thread.hasServerSideSupport) {
|
||||
this.initialEventsFetched = true;
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const response = await this.fetchEvents();
|
||||
this.initialEventsFetched = true;
|
||||
return response;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private setEventMetadata(event: MatrixEvent): void {
|
||||
EventTimeline.setEventMetadata(event, this.roomState, false);
|
||||
event.setThread(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,28 +272,17 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
|
||||
/**
|
||||
* Return last reply to the thread
|
||||
*/
|
||||
public get lastReply(): MatrixEvent {
|
||||
const threadReplies = this.events
|
||||
.filter(event => event.isThreadRelation);
|
||||
return threadReplies[threadReplies.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* The thread ID, which is the same as the root event ID
|
||||
*/
|
||||
public get id(): string {
|
||||
return this.root;
|
||||
}
|
||||
|
||||
/**
|
||||
* The thread root event
|
||||
*/
|
||||
public get rootEvent(): MatrixEvent {
|
||||
return this.findEventById(this.root);
|
||||
public lastReply(matches: (ev: MatrixEvent) => boolean = () => true): MatrixEvent {
|
||||
for (let i = this.events.length - 1; i >= 0; i--) {
|
||||
const event = this.events[i];
|
||||
if (matches(event)) {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public get roomId(): string {
|
||||
return this.rootEvent.getRoomId();
|
||||
return this.room.roomId;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,28 +291,18 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
|
||||
* exclude annotations from that number
|
||||
*/
|
||||
public get length(): number {
|
||||
return this.events
|
||||
.filter(event => event.isThreadRelation)
|
||||
.length;
|
||||
return this.replyCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* A getter for the last event added to the thread
|
||||
*/
|
||||
public get replyToEvent(): MatrixEvent {
|
||||
const events = this.events;
|
||||
return events[events.length -1];
|
||||
return this.lastEvent;
|
||||
}
|
||||
|
||||
public get events(): MatrixEvent[] {
|
||||
return this.timelineSet.getLiveTimeline().getEvents();
|
||||
}
|
||||
|
||||
public merge(thread: Thread): void {
|
||||
thread.events.forEach(event => {
|
||||
this.addEvent(event);
|
||||
});
|
||||
this.events.forEach(event => event.setThread(this));
|
||||
return this.liveTimeline.getEvents();
|
||||
}
|
||||
|
||||
public has(eventId: string): boolean {
|
||||
@@ -199,4 +312,59 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
|
||||
public get hasCurrentUserParticipated(): boolean {
|
||||
return this._currentUserParticipated;
|
||||
}
|
||||
|
||||
public get liveTimeline(): EventTimeline {
|
||||
return this.timelineSet.getLiveTimeline();
|
||||
}
|
||||
|
||||
public async fetchEvents(opts: IRelationsRequestOpts = { limit: 20 }): Promise<{
|
||||
originalEvent: MatrixEvent;
|
||||
events: MatrixEvent[];
|
||||
nextBatch?: string;
|
||||
prevBatch?: string;
|
||||
}> {
|
||||
if (Thread.hasServerSideSupport === undefined) {
|
||||
await Thread.serverSupportPromise;
|
||||
}
|
||||
|
||||
let {
|
||||
originalEvent,
|
||||
events,
|
||||
prevBatch,
|
||||
nextBatch,
|
||||
} = await this.client.relations(
|
||||
this.room.roomId,
|
||||
this.id,
|
||||
RelationType.Thread,
|
||||
null,
|
||||
opts,
|
||||
);
|
||||
|
||||
// When there's no nextBatch returned with a `from` request we have reached
|
||||
// the end of the thread, and therefore want to return an empty one
|
||||
if (!opts.to && !nextBatch) {
|
||||
events = [...events, originalEvent];
|
||||
}
|
||||
|
||||
await Promise.all(events.map(event => {
|
||||
this.setEventMetadata(event);
|
||||
return this.client.decryptEventIfNeeded(event);
|
||||
}));
|
||||
|
||||
const prependEvents = !opts.direction || opts.direction === Direction.Backward;
|
||||
|
||||
this.timelineSet.addEventsToTimeline(
|
||||
events,
|
||||
prependEvents,
|
||||
this.liveTimeline,
|
||||
prependEvents ? nextBatch : prevBatch,
|
||||
);
|
||||
|
||||
return {
|
||||
originalEvent,
|
||||
events,
|
||||
prevBatch,
|
||||
nextBatch,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,13 +14,28 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
enum EventEmitterEvents {
|
||||
export enum EventEmitterEvents {
|
||||
NewListener = "newListener",
|
||||
RemoveListener = "removeListener",
|
||||
Error = "error",
|
||||
}
|
||||
|
||||
type AnyListener = (...args: any) => any;
|
||||
export type ListenerMap<E extends string> = { [eventName in E]: AnyListener };
|
||||
type EventEmitterEventListener = (eventName: string, listener: AnyListener) => void;
|
||||
type EventEmitterErrorListener = (error: Error) => void;
|
||||
|
||||
export type Listener<
|
||||
E extends string,
|
||||
A extends ListenerMap<E>,
|
||||
T extends E | EventEmitterEvents,
|
||||
> = T extends E ? A[T]
|
||||
: T extends EventEmitterEvents ? EventEmitterErrorListener
|
||||
: EventEmitterEventListener;
|
||||
|
||||
/**
|
||||
* Typed Event Emitter class which can act as a Base Model for all our model
|
||||
* and communication events.
|
||||
@@ -28,17 +43,26 @@ enum EventEmitterEvents {
|
||||
* to properly type this, so that our events are not stringly-based and prone
|
||||
* to silly typos.
|
||||
*/
|
||||
export abstract class TypedEventEmitter<Events extends string> extends EventEmitter {
|
||||
public addListener(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this {
|
||||
export class TypedEventEmitter<
|
||||
Events extends string,
|
||||
Arguments extends ListenerMap<Events>,
|
||||
SuperclassArguments extends ListenerMap<any> = Arguments,
|
||||
> extends EventEmitter {
|
||||
public addListener<T extends Events | EventEmitterEvents>(
|
||||
event: T,
|
||||
listener: Listener<Events, Arguments, T>,
|
||||
): this {
|
||||
return super.addListener(event, listener);
|
||||
}
|
||||
|
||||
public emit(event: Events | EventEmitterEvents, ...args: any[]): boolean {
|
||||
public emit<T extends Events>(event: T, ...args: Parameters<SuperclassArguments[T]>): boolean;
|
||||
public emit<T extends Events>(event: T, ...args: Parameters<Arguments[T]>): boolean;
|
||||
public emit<T extends Events>(event: T, ...args: any[]): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
|
||||
public eventNames(): (Events | EventEmitterEvents)[] {
|
||||
return super.eventNames() as Events[];
|
||||
return super.eventNames() as Array<Events | EventEmitterEvents>;
|
||||
}
|
||||
|
||||
public listenerCount(event: Events | EventEmitterEvents): number {
|
||||
@@ -49,23 +73,38 @@ export abstract class TypedEventEmitter<Events extends string> extends EventEmit
|
||||
return super.listeners(event);
|
||||
}
|
||||
|
||||
public off(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this {
|
||||
public off<T extends Events | EventEmitterEvents>(
|
||||
event: T,
|
||||
listener: Listener<Events, Arguments, T>,
|
||||
): this {
|
||||
return super.off(event, listener);
|
||||
}
|
||||
|
||||
public on(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this {
|
||||
public on<T extends Events | EventEmitterEvents>(
|
||||
event: T,
|
||||
listener: Listener<Events, Arguments, T>,
|
||||
): this {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
|
||||
public once(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this {
|
||||
public once<T extends Events | EventEmitterEvents>(
|
||||
event: T,
|
||||
listener: Listener<Events, Arguments, T>,
|
||||
): this {
|
||||
return super.once(event, listener);
|
||||
}
|
||||
|
||||
public prependListener(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this {
|
||||
public prependListener<T extends Events | EventEmitterEvents>(
|
||||
event: T,
|
||||
listener: Listener<Events, Arguments, T>,
|
||||
): this {
|
||||
return super.prependListener(event, listener);
|
||||
}
|
||||
|
||||
public prependOnceListener(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this {
|
||||
public prependOnceListener<T extends Events | EventEmitterEvents>(
|
||||
event: T,
|
||||
listener: Listener<Events, Arguments, T>,
|
||||
): this {
|
||||
return super.prependOnceListener(event, listener);
|
||||
}
|
||||
|
||||
@@ -73,7 +112,10 @@ export abstract class TypedEventEmitter<Events extends string> extends EventEmit
|
||||
return super.removeAllListeners(event);
|
||||
}
|
||||
|
||||
public removeListener(event: Events | EventEmitterEvents, listener: (...args: any[]) => void): this {
|
||||
public removeListener<T extends Events | EventEmitterEvents>(
|
||||
event: T,
|
||||
listener: Listener<Events, Arguments, T>,
|
||||
): this {
|
||||
return super.removeListener(event, listener);
|
||||
}
|
||||
|
||||
|
||||
+31
-14
@@ -18,12 +18,29 @@ limitations under the License.
|
||||
* @module models/user
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { MatrixEvent } from "./event";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
|
||||
export class User extends EventEmitter {
|
||||
// eslint-disable-next-line camelcase
|
||||
export enum UserEvent {
|
||||
DisplayName = "User.displayName",
|
||||
AvatarUrl = "User.avatarUrl",
|
||||
Presence = "User.presence",
|
||||
CurrentlyActive = "User.currentlyActive",
|
||||
LastPresenceTs = "User.lastPresenceTs",
|
||||
/* @deprecated */
|
||||
_UnstableStatusMessage = "User.unstable_statusMessage",
|
||||
}
|
||||
|
||||
export type UserEventHandlerMap = {
|
||||
[UserEvent.DisplayName]: (event: MatrixEvent | undefined, user: User) => void;
|
||||
[UserEvent.AvatarUrl]: (event: MatrixEvent | undefined, user: User) => void;
|
||||
[UserEvent.Presence]: (event: MatrixEvent | undefined, user: User) => void;
|
||||
[UserEvent.CurrentlyActive]: (event: MatrixEvent | undefined, user: User) => void;
|
||||
[UserEvent.LastPresenceTs]: (event: MatrixEvent | undefined, user: User) => void;
|
||||
[UserEvent._UnstableStatusMessage]: (user: User) => void;
|
||||
};
|
||||
|
||||
export class User extends TypedEventEmitter<UserEvent, UserEventHandlerMap> {
|
||||
private modified: number;
|
||||
|
||||
// XXX these should be read-only
|
||||
@@ -39,9 +56,9 @@ export class User extends EventEmitter {
|
||||
presence?: MatrixEvent;
|
||||
profile?: MatrixEvent;
|
||||
} = {
|
||||
presence: null,
|
||||
profile: null,
|
||||
};
|
||||
presence: null,
|
||||
profile: null,
|
||||
};
|
||||
// eslint-disable-next-line camelcase
|
||||
public unstable_statusMessage = "";
|
||||
|
||||
@@ -94,25 +111,25 @@ export class User extends EventEmitter {
|
||||
const firstFire = this.events.presence === null;
|
||||
this.events.presence = event;
|
||||
|
||||
const eventsToFire = [];
|
||||
const eventsToFire: UserEvent[] = [];
|
||||
if (event.getContent().presence !== this.presence || firstFire) {
|
||||
eventsToFire.push("User.presence");
|
||||
eventsToFire.push(UserEvent.Presence);
|
||||
}
|
||||
if (event.getContent().avatar_url &&
|
||||
event.getContent().avatar_url !== this.avatarUrl) {
|
||||
eventsToFire.push("User.avatarUrl");
|
||||
eventsToFire.push(UserEvent.AvatarUrl);
|
||||
}
|
||||
if (event.getContent().displayname &&
|
||||
event.getContent().displayname !== this.displayName) {
|
||||
eventsToFire.push("User.displayName");
|
||||
eventsToFire.push(UserEvent.DisplayName);
|
||||
}
|
||||
if (event.getContent().currently_active !== undefined &&
|
||||
event.getContent().currently_active !== this.currentlyActive) {
|
||||
eventsToFire.push("User.currentlyActive");
|
||||
eventsToFire.push(UserEvent.CurrentlyActive);
|
||||
}
|
||||
|
||||
this.presence = event.getContent().presence;
|
||||
eventsToFire.push("User.lastPresenceTs");
|
||||
eventsToFire.push(UserEvent.LastPresenceTs);
|
||||
|
||||
if (event.getContent().status_msg) {
|
||||
this.presenceStatusMsg = event.getContent().status_msg;
|
||||
@@ -213,7 +230,7 @@ export class User extends EventEmitter {
|
||||
if (!event.getContent()) this.unstable_statusMessage = "";
|
||||
else this.unstable_statusMessage = event.getContent()["status"];
|
||||
this.updateModifiedTime();
|
||||
this.emit("User.unstable_statusMessage", this);
|
||||
this.emit(UserEvent._UnstableStatusMessage, this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -158,8 +158,7 @@ export class PushProcessor {
|
||||
.find((r) => r.rule_id === override.rule_id);
|
||||
|
||||
if (existingRule) {
|
||||
// Copy over the actions, default, and conditions. Don't touch the user's
|
||||
// preference.
|
||||
// Copy over the actions, default, and conditions. Don't touch the user's preference.
|
||||
existingRule.default = override.default;
|
||||
existingRule.conditions = override.conditions;
|
||||
existingRule.actions = override.actions;
|
||||
@@ -447,6 +446,8 @@ export class PushProcessor {
|
||||
}
|
||||
|
||||
public ruleMatchesEvent(rule: IPushRule, ev: MatrixEvent): boolean {
|
||||
if (!rule.conditions?.length) return true;
|
||||
|
||||
let ret = true;
|
||||
for (let i = 0; i < rule.conditions.length; ++i) {
|
||||
const cond = rule.conditions[i];
|
||||
|
||||
@@ -16,8 +16,6 @@ limitations under the License.
|
||||
|
||||
/* eslint-disable @babel/no-invalid-this */
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { MemoryStore, IOpts as IBaseOpts } from "./memory";
|
||||
import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend";
|
||||
import { RemoteIndexedDBStoreBackend } from "./indexeddb-remote-backend";
|
||||
@@ -27,6 +25,7 @@ import { logger } from '../logger';
|
||||
import { ISavedSync } from "./index";
|
||||
import { IIndexedDBBackend } from "./indexeddb-backend";
|
||||
import { ISyncResponse } from "../sync-accumulator";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
|
||||
/**
|
||||
* This is an internal module. See {@link IndexedDBStore} for the public class.
|
||||
@@ -46,6 +45,10 @@ interface IOpts extends IBaseOpts {
|
||||
workerFactory?: () => Worker;
|
||||
}
|
||||
|
||||
type EventHandlerMap = {
|
||||
"degraded": (e: Error) => void;
|
||||
};
|
||||
|
||||
export class IndexedDBStore extends MemoryStore {
|
||||
static exists(indexedDB: IDBFactory, dbName: string): Promise<boolean> {
|
||||
return LocalIndexedDBStoreBackend.exists(indexedDB, dbName);
|
||||
@@ -59,7 +62,7 @@ export class IndexedDBStore extends MemoryStore {
|
||||
// the database, such that we can derive the set if users that have been
|
||||
// modified since we last saved.
|
||||
private userModifiedMap: Record<string, number> = {}; // user_id : timestamp
|
||||
private emitter = new EventEmitter();
|
||||
private emitter = new TypedEventEmitter<keyof EventHandlerMap, EventHandlerMap>();
|
||||
|
||||
/**
|
||||
* Construct a new Indexed Database store, which extends MemoryStore.
|
||||
|
||||
@@ -25,6 +25,15 @@ export enum LocalStorageErrors {
|
||||
QuotaExceededError = 'QuotaExceededError'
|
||||
}
|
||||
|
||||
type EventHandlerMap = {
|
||||
[LocalStorageErrors.Global]: (error: Error) => void;
|
||||
[LocalStorageErrors.SetItemError]: (error: Error) => void;
|
||||
[LocalStorageErrors.GetItemError]: (error: Error) => void;
|
||||
[LocalStorageErrors.RemoveItemError]: (error: Error) => void;
|
||||
[LocalStorageErrors.ClearError]: (error: Error) => void;
|
||||
[LocalStorageErrors.QuotaExceededError]: (error: Error) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Used in element-web as a temporary hack to handle all the localStorage errors on the highest level possible
|
||||
* As of 15.11.2021 (DD/MM/YYYY) we're not properly handling local storage exceptions anywhere.
|
||||
@@ -33,5 +42,5 @@ export enum LocalStorageErrors {
|
||||
* maybe you should check out your disk, as it's probably dying and your session may die with it.
|
||||
* See: https://github.com/vector-im/element-web/issues/18423
|
||||
*/
|
||||
class LocalStorageErrorsEventsEmitter extends TypedEventEmitter<LocalStorageErrors> {}
|
||||
class LocalStorageErrorsEventsEmitter extends TypedEventEmitter<LocalStorageErrors, EventHandlerMap> {}
|
||||
export const localStorageErrorsEventsEmitter = new LocalStorageErrorsEventsEmitter();
|
||||
|
||||
+3
-3
@@ -24,7 +24,7 @@ import { Group } from "../models/group";
|
||||
import { Room } from "../models/room";
|
||||
import { User } from "../models/user";
|
||||
import { IEvent, MatrixEvent } from "../models/event";
|
||||
import { RoomState } from "../models/room-state";
|
||||
import { RoomState, RoomStateEvent } from "../models/room-state";
|
||||
import { RoomMember } from "../models/room-member";
|
||||
import { Filter } from "../filter";
|
||||
import { ISavedSync, IStore } from "./index";
|
||||
@@ -126,7 +126,7 @@ export class MemoryStore implements IStore {
|
||||
this.rooms[room.roomId] = room;
|
||||
// add listeners for room member changes so we can keep the room member
|
||||
// map up-to-date.
|
||||
room.currentState.on("RoomState.members", this.onRoomMember);
|
||||
room.currentState.on(RoomStateEvent.Members, this.onRoomMember);
|
||||
// add existing members
|
||||
room.currentState.getMembers().forEach((m) => {
|
||||
this.onRoomMember(null, room.currentState, m);
|
||||
@@ -185,7 +185,7 @@ export class MemoryStore implements IStore {
|
||||
*/
|
||||
public removeRoom(roomId: string): void {
|
||||
if (this.rooms[roomId]) {
|
||||
this.rooms[roomId].removeListener("RoomState.members", this.onRoomMember);
|
||||
this.rooms[roomId].currentState.removeListener(RoomStateEvent.Members, this.onRoomMember);
|
||||
}
|
||||
delete this.rooms[roomId];
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ interface IOpts {
|
||||
export interface IMinimalEvent {
|
||||
content: IContent;
|
||||
type: EventType | string;
|
||||
unsigned?: IUnsigned;
|
||||
}
|
||||
|
||||
export interface IEphemeral {
|
||||
|
||||
+76
-58
@@ -23,8 +23,8 @@ limitations under the License.
|
||||
* for HTTP and WS at some point.
|
||||
*/
|
||||
|
||||
import { User } from "./models/user";
|
||||
import { NotificationCountType, Room } from "./models/room";
|
||||
import { User, UserEvent } from "./models/user";
|
||||
import { NotificationCountType, Room, RoomEvent } from "./models/room";
|
||||
import { Group } from "./models/group";
|
||||
import * as utils from "./utils";
|
||||
import { IDeferred } from "./utils";
|
||||
@@ -33,7 +33,7 @@ import { EventTimeline } from "./models/event-timeline";
|
||||
import { PushProcessor } from "./pushprocessor";
|
||||
import { logger } from './logger';
|
||||
import { InvalidStoreError } from './errors';
|
||||
import { IStoredClientOpts, MatrixClient, PendingEventOrdering } from "./client";
|
||||
import { ClientEvent, IStoredClientOpts, MatrixClient, PendingEventOrdering } from "./client";
|
||||
import {
|
||||
Category,
|
||||
IEphemeral,
|
||||
@@ -53,6 +53,8 @@ import { MatrixError, Method } from "./http-api";
|
||||
import { ISavedSync } from "./store";
|
||||
import { EventType } from "./@types/event";
|
||||
import { IPushRules } from "./@types/PushRules";
|
||||
import { RoomStateEvent } from "./models/room-state";
|
||||
import { RoomMemberEvent } from "./models/room-member";
|
||||
|
||||
const DEBUG = true;
|
||||
|
||||
@@ -171,8 +173,10 @@ export class SyncApi {
|
||||
}
|
||||
|
||||
if (client.getNotifTimelineSet()) {
|
||||
client.reEmitter.reEmit(client.getNotifTimelineSet(),
|
||||
["Room.timeline", "Room.timelineReset"]);
|
||||
client.reEmitter.reEmit(client.getNotifTimelineSet(), [
|
||||
RoomEvent.Timeline,
|
||||
RoomEvent.TimelineReset,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,15 +196,17 @@ export class SyncApi {
|
||||
timelineSupport,
|
||||
unstableClientRelationAggregation,
|
||||
});
|
||||
client.reEmitter.reEmit(room, ["Room.name", "Room.timeline",
|
||||
"Room.redaction",
|
||||
"Room.redactionCancelled",
|
||||
"Room.receipt", "Room.tags",
|
||||
"Room.timelineReset",
|
||||
"Room.localEchoUpdated",
|
||||
"Room.accountData",
|
||||
"Room.myMembership",
|
||||
"Room.replaceEvent",
|
||||
client.reEmitter.reEmit(room, [
|
||||
RoomEvent.Name,
|
||||
RoomEvent.Redaction,
|
||||
RoomEvent.RedactionCancelled,
|
||||
RoomEvent.Receipt,
|
||||
RoomEvent.Tags,
|
||||
RoomEvent.LocalEchoUpdated,
|
||||
RoomEvent.AccountData,
|
||||
RoomEvent.MyMembership,
|
||||
RoomEvent.Timeline,
|
||||
RoomEvent.TimelineReset,
|
||||
]);
|
||||
this.registerStateListeners(room);
|
||||
return room;
|
||||
@@ -213,7 +219,10 @@ export class SyncApi {
|
||||
public createGroup(groupId: string): Group {
|
||||
const client = this.client;
|
||||
const group = new Group(groupId);
|
||||
client.reEmitter.reEmit(group, ["Group.profile", "Group.myMembership"]);
|
||||
client.reEmitter.reEmit(group, [
|
||||
ClientEvent.GroupProfile,
|
||||
ClientEvent.GroupMyMembership,
|
||||
]);
|
||||
client.store.storeGroup(group);
|
||||
return group;
|
||||
}
|
||||
@@ -228,17 +237,19 @@ export class SyncApi {
|
||||
// to the client now. We need to add a listener for RoomState.members in
|
||||
// order to hook them correctly. (TODO: find a better way?)
|
||||
client.reEmitter.reEmit(room.currentState, [
|
||||
"RoomState.events", "RoomState.members", "RoomState.newMember",
|
||||
RoomStateEvent.Events,
|
||||
RoomStateEvent.Members,
|
||||
RoomStateEvent.NewMember,
|
||||
RoomStateEvent.Update,
|
||||
]);
|
||||
room.currentState.on("RoomState.newMember", function(event, state, member) {
|
||||
room.currentState.on(RoomStateEvent.NewMember, function(event, state, member) {
|
||||
member.user = client.getUser(member.userId);
|
||||
client.reEmitter.reEmit(
|
||||
member,
|
||||
[
|
||||
"RoomMember.name", "RoomMember.typing", "RoomMember.powerLevel",
|
||||
"RoomMember.membership",
|
||||
],
|
||||
);
|
||||
client.reEmitter.reEmit(member, [
|
||||
RoomMemberEvent.Name,
|
||||
RoomMemberEvent.Typing,
|
||||
RoomMemberEvent.PowerLevel,
|
||||
RoomMemberEvent.Membership,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -248,9 +259,9 @@ export class SyncApi {
|
||||
*/
|
||||
private deregisterStateListeners(room: Room): void {
|
||||
// could do with a better way of achieving this.
|
||||
room.currentState.removeAllListeners("RoomState.events");
|
||||
room.currentState.removeAllListeners("RoomState.members");
|
||||
room.currentState.removeAllListeners("RoomState.newMember");
|
||||
room.currentState.removeAllListeners(RoomStateEvent.Events);
|
||||
room.currentState.removeAllListeners(RoomStateEvent.Members);
|
||||
room.currentState.removeAllListeners(RoomStateEvent.NewMember);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,7 +294,7 @@ export class SyncApi {
|
||||
leaveRooms = this.mapSyncResponseToRoomArray(data.rooms.leave);
|
||||
}
|
||||
const rooms = [];
|
||||
leaveRooms.forEach((leaveObj) => {
|
||||
leaveRooms.forEach(async (leaveObj) => {
|
||||
const room = leaveObj.room;
|
||||
rooms.push(room);
|
||||
if (!leaveObj.isBrandNewRoom) {
|
||||
@@ -309,11 +320,11 @@ export class SyncApi {
|
||||
EventTimeline.BACKWARDS);
|
||||
|
||||
this.processRoomEvents(room, stateEvents, timelineEvents);
|
||||
this.processThreadEvents(room, threadedEvents);
|
||||
await this.processThreadEvents(room, threadedEvents, false);
|
||||
|
||||
room.recalculate();
|
||||
client.store.storeRoom(room);
|
||||
client.emit("Room", room);
|
||||
client.emit(ClientEvent.Room, room);
|
||||
|
||||
this.processEventsForNotifs(room, events);
|
||||
});
|
||||
@@ -361,7 +372,7 @@ export class SyncApi {
|
||||
user.setPresenceEvent(presenceEvent);
|
||||
client.store.storeUser(user);
|
||||
}
|
||||
client.emit("event", presenceEvent);
|
||||
client.emit(ClientEvent.Event, presenceEvent);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -387,7 +398,7 @@ export class SyncApi {
|
||||
response.messages.start);
|
||||
|
||||
client.store.storeRoom(this._peekRoom);
|
||||
client.emit("Room", this._peekRoom);
|
||||
client.emit(ClientEvent.Room, this._peekRoom);
|
||||
|
||||
this.peekPoll(this._peekRoom);
|
||||
return this._peekRoom;
|
||||
@@ -444,7 +455,7 @@ export class SyncApi {
|
||||
user.setPresenceEvent(presenceEvent);
|
||||
this.client.store.storeUser(user);
|
||||
}
|
||||
this.client.emit("event", presenceEvent);
|
||||
this.client.emit(ClientEvent.Event, presenceEvent);
|
||||
});
|
||||
|
||||
// strip out events which aren't for the given room_id (e.g presence)
|
||||
@@ -839,7 +850,7 @@ export class SyncApi {
|
||||
logger.error("Caught /sync error", e.stack || e);
|
||||
|
||||
// Emit the exception for client handling
|
||||
this.client.emit("sync.unexpectedError", e);
|
||||
this.client.emit(ClientEvent.SyncUnexpectedError, e);
|
||||
}
|
||||
|
||||
// update this as it may have changed
|
||||
@@ -1072,7 +1083,7 @@ export class SyncApi {
|
||||
user.setPresenceEvent(presenceEvent);
|
||||
client.store.storeUser(user);
|
||||
}
|
||||
client.emit("event", presenceEvent);
|
||||
client.emit(ClientEvent.Event, presenceEvent);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1095,7 +1106,7 @@ export class SyncApi {
|
||||
client.pushRules = PushProcessor.rewriteDefaultRules(rules);
|
||||
}
|
||||
const prevEvent = prevEventsMap[accountDataEvent.getId()];
|
||||
client.emit("accountData", accountDataEvent, prevEvent);
|
||||
client.emit(ClientEvent.AccountData, accountDataEvent, prevEvent);
|
||||
return accountDataEvent;
|
||||
},
|
||||
);
|
||||
@@ -1148,7 +1159,7 @@ export class SyncApi {
|
||||
}
|
||||
}
|
||||
|
||||
client.emit("toDeviceEvent", toDeviceEvent);
|
||||
client.emit(ClientEvent.ToDeviceEvent, toDeviceEvent);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
@@ -1200,10 +1211,10 @@ export class SyncApi {
|
||||
if (inviteObj.isBrandNewRoom) {
|
||||
room.recalculate();
|
||||
client.store.storeRoom(room);
|
||||
client.emit("Room", room);
|
||||
client.emit(ClientEvent.Room, room);
|
||||
}
|
||||
stateEvents.forEach(function(e) {
|
||||
client.emit("event", e);
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
room.updateMyMembership("invite");
|
||||
});
|
||||
@@ -1306,7 +1317,7 @@ export class SyncApi {
|
||||
const [timelineEvents, threadedEvents] = this.client.partitionThreadedEvents(events);
|
||||
|
||||
this.processRoomEvents(room, stateEvents, timelineEvents, syncEventData.fromCache);
|
||||
this.processThreadEvents(room, threadedEvents);
|
||||
await this.processThreadEvents(room, threadedEvents, false);
|
||||
|
||||
// set summary after processing events,
|
||||
// because it will trigger a name calculation
|
||||
@@ -1324,13 +1335,13 @@ export class SyncApi {
|
||||
room.recalculate();
|
||||
if (joinObj.isBrandNewRoom) {
|
||||
client.store.storeRoom(room);
|
||||
client.emit("Room", room);
|
||||
client.emit(ClientEvent.Room, room);
|
||||
}
|
||||
|
||||
this.processEventsForNotifs(room, events);
|
||||
|
||||
const processRoomEvent = async (e) => {
|
||||
client.emit("event", e);
|
||||
client.emit(ClientEvent.Event, e);
|
||||
if (e.isState() && e.getType() == "m.room.encryption" && this.opts.crypto) {
|
||||
await this.opts.crypto.onCryptoEvent(e);
|
||||
}
|
||||
@@ -1350,10 +1361,10 @@ export class SyncApi {
|
||||
await utils.promiseMapSeries(timelineEvents, processRoomEvent);
|
||||
await utils.promiseMapSeries(threadedEvents, processRoomEvent);
|
||||
ephemeralEvents.forEach(function(e) {
|
||||
client.emit("event", e);
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
accountDataEvents.forEach(function(e) {
|
||||
client.emit("event", e);
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
|
||||
room.updateMyMembership("join");
|
||||
@@ -1365,7 +1376,7 @@ export class SyncApi {
|
||||
});
|
||||
|
||||
// Handle leaves (e.g. kicked rooms)
|
||||
leaveRooms.forEach((leaveObj) => {
|
||||
leaveRooms.forEach(async (leaveObj) => {
|
||||
const room = leaveObj.room;
|
||||
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
|
||||
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
|
||||
@@ -1374,28 +1385,28 @@ export class SyncApi {
|
||||
const [timelineEvents, threadedEvents] = this.client.partitionThreadedEvents(events);
|
||||
|
||||
this.processRoomEvents(room, stateEvents, timelineEvents);
|
||||
this.processThreadEvents(room, threadedEvents);
|
||||
await this.processThreadEvents(room, threadedEvents, false);
|
||||
room.addAccountData(accountDataEvents);
|
||||
|
||||
room.recalculate();
|
||||
if (leaveObj.isBrandNewRoom) {
|
||||
client.store.storeRoom(room);
|
||||
client.emit("Room", room);
|
||||
client.emit(ClientEvent.Room, room);
|
||||
}
|
||||
|
||||
this.processEventsForNotifs(room, events);
|
||||
|
||||
stateEvents.forEach(function(e) {
|
||||
client.emit("event", e);
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
timelineEvents.forEach(function(e) {
|
||||
client.emit("event", e);
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
threadedEvents.forEach(function(e) {
|
||||
client.emit("event", e);
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
accountDataEvents.forEach(function(e) {
|
||||
client.emit("event", e);
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
|
||||
room.updateMyMembership("leave");
|
||||
@@ -1550,7 +1561,7 @@ export class SyncApi {
|
||||
group.setMyMembership(sectionName);
|
||||
if (isBrandNew) {
|
||||
// Now we've filled in all the fields, emit the Group event
|
||||
this.client.emit("Group", group);
|
||||
this.client.emit(ClientEvent.Group, group);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1719,14 +1730,18 @@ export class SyncApi {
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
private processThreadEvents(room: Room, threadedEvents: MatrixEvent[]): void {
|
||||
return this.client.processThreadEvents(room, threadedEvents);
|
||||
private processThreadEvents(
|
||||
room: Room,
|
||||
threadedEvents: MatrixEvent[],
|
||||
toStartOfTimeline: boolean,
|
||||
): Promise<void> {
|
||||
return this.client.processThreadEvents(room, threadedEvents, toStartOfTimeline);
|
||||
}
|
||||
|
||||
// extractRelatedEvents(event: MatrixEvent, events: MatrixEvent[], relatedEvents: MatrixEvent[] = []): MatrixEvent[] {
|
||||
// relatedEvents.push(event);
|
||||
|
||||
// const parentEventId = event.parentEventId;
|
||||
// const parentEventId = event.getAssociatedId();
|
||||
// const parentEventIndex = events.findIndex(event => event.getId() === parentEventId);
|
||||
|
||||
// if (parentEventIndex > -1) {
|
||||
@@ -1777,7 +1792,7 @@ export class SyncApi {
|
||||
const old = this.syncState;
|
||||
this.syncState = newState;
|
||||
this.syncStateData = data;
|
||||
this.client.emit("sync", this.syncState, old, data);
|
||||
this.client.emit(ClientEvent.Sync, this.syncState, old, data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1795,8 +1810,11 @@ export class SyncApi {
|
||||
function createNewUser(client: MatrixClient, userId: string): User {
|
||||
const user = new User(userId);
|
||||
client.reEmitter.reEmit(user, [
|
||||
"User.avatarUrl", "User.displayName", "User.presence",
|
||||
"User.currentlyActive", "User.lastPresenceTs",
|
||||
UserEvent.AvatarUrl,
|
||||
UserEvent.DisplayName,
|
||||
UserEvent.Presence,
|
||||
UserEvent.CurrentlyActive,
|
||||
UserEvent.LastPresenceTs,
|
||||
]);
|
||||
return user;
|
||||
}
|
||||
|
||||
+1
-1
@@ -443,7 +443,7 @@ export function isNullOrUndefined(val: any): boolean {
|
||||
}
|
||||
|
||||
export interface IDeferred<T> {
|
||||
resolve: (value: T) => void;
|
||||
resolve: (value: T | Promise<T>) => void;
|
||||
reject: (reason?: any) => void;
|
||||
promise: Promise<T>;
|
||||
}
|
||||
|
||||
+106
-35
@@ -2,6 +2,7 @@
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2021 - 2022 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -21,8 +22,6 @@ limitations under the License.
|
||||
* @module webrtc/call
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { logger } from '../logger';
|
||||
import * as utils from '../utils';
|
||||
import { MatrixEvent } from '../models/event';
|
||||
@@ -47,6 +46,7 @@ import {
|
||||
import { CallFeed } from './callFeed';
|
||||
import { MatrixClient } from "../client";
|
||||
import { ISendEventResponse } from "../@types/requests";
|
||||
import { EventEmitterEvents, TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
|
||||
// events: hangup, error(err), replaced(call), state(state, oldState)
|
||||
|
||||
@@ -241,6 +241,21 @@ function genCallID(): string {
|
||||
return Date.now().toString() + randomString(16);
|
||||
}
|
||||
|
||||
export type CallEventHandlerMap = {
|
||||
[CallEvent.DataChannel]: (channel: RTCDataChannel) => void;
|
||||
[CallEvent.FeedsChanged]: (feeds: CallFeed[]) => void;
|
||||
[CallEvent.Replaced]: (newCall: MatrixCall) => void;
|
||||
[CallEvent.Error]: (error: CallError) => void;
|
||||
[CallEvent.RemoteHoldUnhold]: (onHold: boolean) => void;
|
||||
[CallEvent.LocalHoldUnhold]: (onHold: boolean) => void;
|
||||
[CallEvent.LengthChanged]: (length: number) => void;
|
||||
[CallEvent.State]: (state: CallState, oldState?: CallState) => void;
|
||||
[CallEvent.Hangup]: () => void;
|
||||
[CallEvent.AssertedIdentityChanged]: () => void;
|
||||
/* @deprecated */
|
||||
[CallEvent.HoldUnhold]: (onHold: boolean) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a new Matrix Call.
|
||||
* @constructor
|
||||
@@ -252,7 +267,7 @@ function genCallID(): string {
|
||||
* @param {Array<Object>} opts.turnServers Optional. A list of TURN servers.
|
||||
* @param {MatrixClient} opts.client The Matrix Client instance to send events to.
|
||||
*/
|
||||
export class MatrixCall extends EventEmitter {
|
||||
export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap> {
|
||||
public roomId: string;
|
||||
public callId: string;
|
||||
public state = CallState.Fledgling;
|
||||
@@ -571,9 +586,11 @@ export class MatrixCall extends EventEmitter {
|
||||
private pushNewLocalFeed(stream: MediaStream, purpose: SDPStreamMetadataPurpose, addToPeerConnection = true): void {
|
||||
const userId = this.client.getUserId();
|
||||
|
||||
// TODO: Find out what is going on here
|
||||
// why do we enable audio (and only audio) tracks here? -- matthew
|
||||
// Tracks don't always start off enabled, eg. chrome will give a disabled
|
||||
// audio track if you ask for user media audio and already had one that
|
||||
// you'd set to disabled (presumably because it clones them internally).
|
||||
setTracksEnabled(stream.getAudioTracks(), true);
|
||||
setTracksEnabled(stream.getVideoTracks(), true);
|
||||
|
||||
// We try to replace an existing feed if there already is one with the same purpose
|
||||
const existingFeed = this.getLocalFeeds().find((feed) => feed.purpose === purpose);
|
||||
@@ -616,7 +633,8 @@ export class MatrixCall extends EventEmitter {
|
||||
`id="${track.id}", ` +
|
||||
`kind="${track.kind}", ` +
|
||||
`streamId="${callFeed.stream.id}", ` +
|
||||
`streamPurpose="${callFeed.purpose}"` +
|
||||
`streamPurpose="${callFeed.purpose}", ` +
|
||||
`enabled=${track.enabled}` +
|
||||
`) to peer connection`,
|
||||
);
|
||||
senderArray.push(this.peerConn.addTrack(track, callFeed.stream));
|
||||
@@ -933,29 +951,13 @@ export class MatrixCall extends EventEmitter {
|
||||
if (!this.opponentSupportsSDPStreamMetadata()) return;
|
||||
|
||||
try {
|
||||
const upgradeAudio = audio && !this.hasLocalUserMediaAudioTrack;
|
||||
const upgradeVideo = video && !this.hasLocalUserMediaVideoTrack;
|
||||
logger.debug(`Upgrading call: audio?=${upgradeAudio} video?=${upgradeVideo}`);
|
||||
const getAudio = audio || this.hasLocalUserMediaAudioTrack;
|
||||
const getVideo = video || this.hasLocalUserMediaVideoTrack;
|
||||
|
||||
const stream = await this.client.getMediaHandler().getUserMediaStream(upgradeAudio, upgradeVideo);
|
||||
if (upgradeAudio && upgradeVideo) {
|
||||
if (this.hasLocalUserMediaAudioTrack) return;
|
||||
if (this.hasLocalUserMediaVideoTrack) return;
|
||||
|
||||
this.pushNewLocalFeed(stream, SDPStreamMetadataPurpose.Usermedia);
|
||||
} else if (upgradeAudio) {
|
||||
if (this.hasLocalUserMediaAudioTrack) return;
|
||||
|
||||
const audioTrack = stream.getAudioTracks()[0];
|
||||
this.localUsermediaStream.addTrack(audioTrack);
|
||||
this.peerConn.addTrack(audioTrack, this.localUsermediaStream);
|
||||
} else if (upgradeVideo) {
|
||||
if (this.hasLocalUserMediaVideoTrack) return;
|
||||
|
||||
const videoTrack = stream.getVideoTracks()[0];
|
||||
this.localUsermediaStream.addTrack(videoTrack);
|
||||
this.peerConn.addTrack(videoTrack, this.localUsermediaStream);
|
||||
}
|
||||
// updateLocalUsermediaStream() will take the tracks, use them as
|
||||
// replacement and throw the stream away, so it isn't reusable
|
||||
const stream = await this.client.getMediaHandler().getUserMediaStream(getAudio, getVideo, false);
|
||||
await this.updateLocalUsermediaStream(stream, audio, video);
|
||||
} catch (error) {
|
||||
logger.error("Failed to upgrade the call", error);
|
||||
this.emit(CallEvent.Error,
|
||||
@@ -1071,6 +1073,63 @@ export class MatrixCall extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces/adds the tracks from the passed stream to the localUsermediaStream
|
||||
* @param {MediaStream} stream to use a replacement for the local usermedia stream
|
||||
*/
|
||||
public async updateLocalUsermediaStream(
|
||||
stream: MediaStream, forceAudio = false, forceVideo = false,
|
||||
): Promise<void> {
|
||||
const callFeed = this.localUsermediaFeed;
|
||||
const audioEnabled = forceAudio || (!callFeed.isAudioMuted() && !this.remoteOnHold);
|
||||
const videoEnabled = forceVideo || (!callFeed.isVideoMuted() && !this.remoteOnHold);
|
||||
setTracksEnabled(stream.getAudioTracks(), audioEnabled);
|
||||
setTracksEnabled(stream.getVideoTracks(), videoEnabled);
|
||||
|
||||
// We want to keep the same stream id, so we replace the tracks rather than the whole stream
|
||||
for (const track of this.localUsermediaStream.getTracks()) {
|
||||
this.localUsermediaStream.removeTrack(track);
|
||||
track.stop();
|
||||
}
|
||||
for (const track of stream.getTracks()) {
|
||||
this.localUsermediaStream.addTrack(track);
|
||||
}
|
||||
|
||||
const newSenders = [];
|
||||
|
||||
for (const track of stream.getTracks()) {
|
||||
const oldSender = this.usermediaSenders.find((sender) => sender.track?.kind === track.kind);
|
||||
let newSender: RTCRtpSender;
|
||||
|
||||
if (oldSender) {
|
||||
logger.info(
|
||||
`Replacing track (` +
|
||||
`id="${track.id}", ` +
|
||||
`kind="${track.kind}", ` +
|
||||
`streamId="${stream.id}", ` +
|
||||
`streamPurpose="${callFeed.purpose}"` +
|
||||
`) to peer connection`,
|
||||
);
|
||||
await oldSender.replaceTrack(track);
|
||||
newSender = oldSender;
|
||||
} else {
|
||||
logger.info(
|
||||
`Adding track (` +
|
||||
`id="${track.id}", ` +
|
||||
`kind="${track.kind}", ` +
|
||||
`streamId="${stream.id}", ` +
|
||||
`streamPurpose="${callFeed.purpose}"` +
|
||||
`) to peer connection`,
|
||||
);
|
||||
newSender = this.peerConn.addTrack(track, this.localUsermediaStream);
|
||||
}
|
||||
|
||||
newSenders.push(newSender);
|
||||
}
|
||||
|
||||
this.usermediaSenders = newSenders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether our outbound video should be muted or not.
|
||||
* @param {boolean} muted True to mute the outbound video.
|
||||
@@ -1199,8 +1258,8 @@ export class MatrixCall extends EventEmitter {
|
||||
[SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(),
|
||||
});
|
||||
|
||||
const micShouldBeMuted = this.localUsermediaFeed?.isAudioMuted() || this.remoteOnHold;
|
||||
const vidShouldBeMuted = this.localUsermediaFeed?.isVideoMuted() || this.remoteOnHold;
|
||||
const micShouldBeMuted = this.isMicrophoneMuted() || this.remoteOnHold;
|
||||
const vidShouldBeMuted = this.isLocalVideoMuted() || this.remoteOnHold;
|
||||
|
||||
setTracksEnabled(this.localUsermediaStream.getAudioTracks(), !micShouldBeMuted);
|
||||
setTracksEnabled(this.localUsermediaStream.getVideoTracks(), !vidShouldBeMuted);
|
||||
@@ -1717,7 +1776,12 @@ export class MatrixCall extends EventEmitter {
|
||||
|
||||
const stream = ev.streams[0];
|
||||
this.pushRemoteFeed(stream);
|
||||
stream.addEventListener("removetrack", () => this.deleteFeedByStream(stream));
|
||||
stream.addEventListener("removetrack", () => {
|
||||
if (stream.getTracks().length === 0) {
|
||||
logger.info(`Stream ID ${stream.id} has no tracks remaining - removing`);
|
||||
this.deleteFeedByStream(stream);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
private onDataChannel = (ev: RTCDataChannelEvent): void => {
|
||||
@@ -1938,7 +2002,7 @@ export class MatrixCall extends EventEmitter {
|
||||
|
||||
await this.sendVoipEvent(EventType.CallReplaces, bodyToTransferee);
|
||||
|
||||
await this.terminate(CallParty.Local, CallErrorCode.Replaced, true);
|
||||
await this.terminate(CallParty.Local, CallErrorCode.Transfered, true);
|
||||
await transferTargetCall.terminate(CallParty.Local, CallErrorCode.Transfered, true);
|
||||
}
|
||||
|
||||
@@ -1968,12 +2032,12 @@ export class MatrixCall extends EventEmitter {
|
||||
this.peerConn.close();
|
||||
}
|
||||
if (shouldEmit) {
|
||||
this.emit(CallEvent.Hangup, this);
|
||||
this.emit(CallEvent.Hangup);
|
||||
}
|
||||
}
|
||||
|
||||
private stopAllMedia(): void {
|
||||
logger.debug(`stopAllMedia (stream=${this.localUsermediaStream})`);
|
||||
logger.debug("Stopping all media for call", this.callId);
|
||||
|
||||
for (const feed of this.feeds) {
|
||||
if (feed.isLocal() && feed.purpose === SDPStreamMetadataPurpose.Usermedia) {
|
||||
@@ -1981,6 +2045,7 @@ export class MatrixCall extends EventEmitter {
|
||||
} else if (feed.isLocal() && feed.purpose === SDPStreamMetadataPurpose.Screenshare) {
|
||||
this.client.getMediaHandler().stopScreensharingStream(feed.stream);
|
||||
} else {
|
||||
logger.debug("Stopping remote stream", feed.stream.id);
|
||||
for (const track of feed.stream.getTracks()) {
|
||||
track.stop();
|
||||
}
|
||||
@@ -1989,7 +2054,7 @@ export class MatrixCall extends EventEmitter {
|
||||
}
|
||||
|
||||
private checkForErrorListener(): void {
|
||||
if (this.listeners("error").length === 0) {
|
||||
if (this.listeners(EventEmitterEvents.Error).length === 0) {
|
||||
throw new Error(
|
||||
"You MUST attach an error listener using call.on('error', function() {})",
|
||||
);
|
||||
@@ -2058,6 +2123,12 @@ export class MatrixCall extends EventEmitter {
|
||||
|
||||
try {
|
||||
const stream = await this.client.getMediaHandler().getUserMediaStream(audio, video);
|
||||
|
||||
// make sure all the tracks are enabled (same as pushNewLocalFeed -
|
||||
// we probably ought to just have one code path for adding streams)
|
||||
setTracksEnabled(stream.getAudioTracks(), true);
|
||||
setTracksEnabled(stream.getVideoTracks(), true);
|
||||
|
||||
const callFeed = new CallFeed({
|
||||
client: this.client,
|
||||
roomId: this.roomId,
|
||||
|
||||
@@ -14,17 +14,27 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixEvent } from '../models/event';
|
||||
import { MatrixEvent, MatrixEventEvent } from '../models/event';
|
||||
import { logger } from '../logger';
|
||||
import { createNewMatrixCall, MatrixCall, CallErrorCode, CallState, CallDirection } from './call';
|
||||
import { CallDirection, CallErrorCode, CallState, createNewMatrixCall, MatrixCall } from './call';
|
||||
import { EventType } from '../@types/event';
|
||||
import { MatrixClient } from '../client';
|
||||
import { ClientEvent, MatrixClient } from '../client';
|
||||
import { MCallAnswer, MCallHangupReject } from "./callEventTypes";
|
||||
import { SyncState } from "../sync";
|
||||
import { RoomEvent } from "../models/room";
|
||||
|
||||
// Don't ring unless we'd be ringing for at least 3 seconds: the user needs some
|
||||
// time to press the 'accept' button
|
||||
const RING_GRACE_PERIOD = 3000;
|
||||
|
||||
export enum CallEventHandlerEvent {
|
||||
Incoming = "Call.incoming",
|
||||
}
|
||||
|
||||
export type CallEventHandlerEventHandlerMap = {
|
||||
[CallEventHandlerEvent.Incoming]: (call: MatrixCall) => void;
|
||||
};
|
||||
|
||||
export class CallEventHandler {
|
||||
client: MatrixClient;
|
||||
calls: Map<string, MatrixCall>;
|
||||
@@ -47,17 +57,17 @@ export class CallEventHandler {
|
||||
}
|
||||
|
||||
public start() {
|
||||
this.client.on("sync", this.evaluateEventBuffer);
|
||||
this.client.on("Room.timeline", this.onRoomTimeline);
|
||||
this.client.on(ClientEvent.Sync, this.evaluateEventBuffer);
|
||||
this.client.on(RoomEvent.Timeline, this.onRoomTimeline);
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.client.removeListener("sync", this.evaluateEventBuffer);
|
||||
this.client.removeListener("Room.timeline", this.onRoomTimeline);
|
||||
this.client.removeListener(ClientEvent.Sync, this.evaluateEventBuffer);
|
||||
this.client.removeListener(RoomEvent.Timeline, this.onRoomTimeline);
|
||||
}
|
||||
|
||||
private evaluateEventBuffer = async () => {
|
||||
if (this.client.getSyncState() === "SYNCING") {
|
||||
if (this.client.getSyncState() === SyncState.Syncing) {
|
||||
await Promise.all(this.callEventBuffer.map(event => {
|
||||
this.client.decryptEventIfNeeded(event);
|
||||
}));
|
||||
@@ -101,7 +111,7 @@ export class CallEventHandler {
|
||||
|
||||
if (event.isBeingDecrypted() || event.isDecryptionFailure()) {
|
||||
// add an event listener for once the event is decrypted.
|
||||
event.once("Event.decrypted", async () => {
|
||||
event.once(MatrixEventEvent.Decrypted, async () => {
|
||||
if (!this.eventIsACall(event)) return;
|
||||
|
||||
if (this.callEventBuffer.includes(event)) {
|
||||
@@ -221,7 +231,7 @@ export class CallEventHandler {
|
||||
call.hangup(CallErrorCode.Replaced, true);
|
||||
}
|
||||
} else {
|
||||
this.client.emit("Call.incoming", call);
|
||||
this.client.emit(CallEventHandlerEvent.Incoming, call);
|
||||
}
|
||||
return;
|
||||
} else if (type === EventType.CallCandidates) {
|
||||
@@ -257,7 +267,11 @@ export class CallEventHandler {
|
||||
} else {
|
||||
call.onRejectReceived(content as MCallHangupReject);
|
||||
}
|
||||
this.calls.delete(content.call_id);
|
||||
|
||||
// @ts-expect-error typescript thinks the state can't be 'ended' because we're
|
||||
// inside the if block where it wasn't, but it could have changed because
|
||||
// on[Hangup|Reject]Received are side-effecty.
|
||||
if (call.state === CallState.Ended) this.calls.delete(content.call_id);
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -14,11 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import EventEmitter from "events";
|
||||
|
||||
import { SDPStreamMetadataPurpose } from "./callEventTypes";
|
||||
import { MatrixClient } from "../client";
|
||||
import { RoomMember } from "../models/room-member";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
|
||||
const POLLING_INTERVAL = 200; // ms
|
||||
export const SPEAKING_THRESHOLD = -60; // dB
|
||||
@@ -47,7 +46,14 @@ export enum CallFeedEvent {
|
||||
Speaking = "speaking",
|
||||
}
|
||||
|
||||
export class CallFeed extends EventEmitter {
|
||||
type EventHandlerMap = {
|
||||
[CallFeedEvent.NewStream]: (stream: MediaStream) => void;
|
||||
[CallFeedEvent.MuteStateChanged]: (audioMuted: boolean, videoMuted: boolean) => void;
|
||||
[CallFeedEvent.VolumeChanged]: (volume: number) => void;
|
||||
[CallFeedEvent.Speaking]: (speaking: boolean) => void;
|
||||
};
|
||||
|
||||
export class CallFeed extends TypedEventEmitter<CallFeedEvent, EventHandlerMap> {
|
||||
public stream: MediaStream;
|
||||
public userId: string;
|
||||
public purpose: SDPStreamMetadataPurpose;
|
||||
|
||||
+99
-20
@@ -2,7 +2,7 @@
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
Copyright 2021 - 2022 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -18,20 +18,30 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from "../logger";
|
||||
import { MatrixClient } from "../client";
|
||||
import { CallState } from "./call";
|
||||
|
||||
export class MediaHandler {
|
||||
private audioInput: string;
|
||||
private videoInput: string;
|
||||
private userMediaStreams: MediaStream[] = [];
|
||||
private screensharingStreams: MediaStream[] = [];
|
||||
private localUserMediaStream?: MediaStream;
|
||||
public userMediaStreams: MediaStream[] = [];
|
||||
public screensharingStreams: MediaStream[] = [];
|
||||
|
||||
constructor(private client: MatrixClient) { }
|
||||
|
||||
/**
|
||||
* Set an audio input device to use for MatrixCalls
|
||||
* @param {string} deviceId the identifier for the device
|
||||
* undefined treated as unset
|
||||
*/
|
||||
public setAudioInput(deviceId: string): void {
|
||||
public async setAudioInput(deviceId: string): Promise<void> {
|
||||
logger.info("LOG setting audio input to", deviceId);
|
||||
|
||||
if (this.audioInput === deviceId) return;
|
||||
|
||||
this.audioInput = deviceId;
|
||||
await this.updateLocalUsermediaStreams();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,8 +49,39 @@ export class MediaHandler {
|
||||
* @param {string} deviceId the identifier for the device
|
||||
* undefined treated as unset
|
||||
*/
|
||||
public setVideoInput(deviceId: string): void {
|
||||
public async setVideoInput(deviceId: string): Promise<void> {
|
||||
logger.info("LOG setting video input to", deviceId);
|
||||
|
||||
if (this.videoInput === deviceId) return;
|
||||
|
||||
this.videoInput = deviceId;
|
||||
await this.updateLocalUsermediaStreams();
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests new usermedia streams and replace the old ones
|
||||
*/
|
||||
public async updateLocalUsermediaStreams(): Promise<void> {
|
||||
if (this.userMediaStreams.length === 0) return;
|
||||
|
||||
const callMediaStreamParams: Map<string, { audio: boolean, video: boolean }> = new Map();
|
||||
for (const call of this.client.callEventHandler.calls.values()) {
|
||||
callMediaStreamParams.set(call.callId, {
|
||||
audio: call.hasLocalUserMediaAudioTrack,
|
||||
video: call.hasLocalUserMediaVideoTrack,
|
||||
});
|
||||
}
|
||||
|
||||
for (const call of this.client.callEventHandler.calls.values()) {
|
||||
if (call.state === CallState.Ended || !callMediaStreamParams.has(call.callId)) continue;
|
||||
|
||||
const { audio, video } = callMediaStreamParams.get(call.callId);
|
||||
|
||||
// This stream won't be reusable as we will replace the tracks of the old stream
|
||||
const stream = await this.getUserMediaStream(audio, video, false);
|
||||
|
||||
await call.updateLocalUsermediaStream(stream);
|
||||
}
|
||||
}
|
||||
|
||||
public async hasAudioDevice(): Promise<boolean> {
|
||||
@@ -54,31 +95,60 @@ export class MediaHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param audio should have an audio track
|
||||
* @param video should have a video track
|
||||
* @param reusable is allowed to be reused by the MediaHandler
|
||||
* @returns {MediaStream} based on passed parameters
|
||||
*/
|
||||
public async getUserMediaStream(audio: boolean, video: boolean): Promise<MediaStream> {
|
||||
public async getUserMediaStream(audio: boolean, video: boolean, reusable = true): Promise<MediaStream> {
|
||||
const shouldRequestAudio = audio && await this.hasAudioDevice();
|
||||
const shouldRequestVideo = video && await this.hasVideoDevice();
|
||||
|
||||
let stream: MediaStream;
|
||||
|
||||
// Find a stream with matching tracks
|
||||
const matchingStream = this.userMediaStreams.find((stream) => {
|
||||
if (shouldRequestAudio !== (stream.getAudioTracks().length > 0)) return false;
|
||||
if (shouldRequestVideo !== (stream.getVideoTracks().length > 0)) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (matchingStream) {
|
||||
logger.log("Cloning user media stream", matchingStream.id);
|
||||
stream = matchingStream.clone();
|
||||
} else {
|
||||
if (
|
||||
!this.localUserMediaStream ||
|
||||
(this.localUserMediaStream.getAudioTracks().length === 0 && shouldRequestAudio) ||
|
||||
(this.localUserMediaStream.getVideoTracks().length === 0 && shouldRequestVideo) ||
|
||||
(this.localUserMediaStream.getAudioTracks()[0]?.getSettings()?.deviceId !== this.audioInput) ||
|
||||
(this.localUserMediaStream.getVideoTracks()[0]?.getSettings()?.deviceId !== this.videoInput)
|
||||
) {
|
||||
const constraints = this.getUserMediaContraints(shouldRequestAudio, shouldRequestVideo);
|
||||
logger.log("Getting user media with constraints", constraints);
|
||||
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
|
||||
for (const track of stream.getTracks()) {
|
||||
const settings = track.getSettings();
|
||||
|
||||
if (track.kind === "audio") {
|
||||
this.audioInput = settings.deviceId;
|
||||
} else if (track.kind === "video") {
|
||||
this.videoInput = settings.deviceId;
|
||||
}
|
||||
}
|
||||
|
||||
if (reusable) {
|
||||
this.localUserMediaStream = stream;
|
||||
}
|
||||
} else {
|
||||
stream = this.localUserMediaStream.clone();
|
||||
|
||||
if (!shouldRequestAudio) {
|
||||
for (const track of stream.getAudioTracks()) {
|
||||
stream.removeTrack(track);
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldRequestVideo) {
|
||||
for (const track of stream.getVideoTracks()) {
|
||||
stream.removeTrack(track);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.userMediaStreams.push(stream);
|
||||
if (reusable) {
|
||||
this.userMediaStreams.push(stream);
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
@@ -98,12 +168,18 @@ export class MediaHandler {
|
||||
logger.debug("Splicing usermedia stream out stream array", mediaStream.id);
|
||||
this.userMediaStreams.splice(index, 1);
|
||||
}
|
||||
|
||||
if (this.localUserMediaStream === mediaStream) {
|
||||
this.localUserMediaStream = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param desktopCapturerSourceId sourceId for Electron DesktopCapturer
|
||||
* @param reusable is allowed to be reused by the MediaHandler
|
||||
* @returns {MediaStream} based on passed parameters
|
||||
*/
|
||||
public async getScreensharingStream(desktopCapturerSourceId: string): Promise<MediaStream | null> {
|
||||
public async getScreensharingStream(desktopCapturerSourceId: string, reusable = true): Promise<MediaStream | null> {
|
||||
let stream: MediaStream;
|
||||
|
||||
if (this.screensharingStreams.length === 0) {
|
||||
@@ -125,7 +201,9 @@ export class MediaHandler {
|
||||
stream = matchingStream.clone();
|
||||
}
|
||||
|
||||
this.screensharingStreams.push(stream);
|
||||
if (reusable) {
|
||||
this.screensharingStreams.push(stream);
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
@@ -165,6 +243,7 @@ export class MediaHandler {
|
||||
|
||||
this.userMediaStreams = [];
|
||||
this.screensharingStreams = [];
|
||||
this.localUserMediaStream = undefined;
|
||||
}
|
||||
|
||||
private getUserMediaContraints(audio: boolean, video: boolean): MediaStreamConstraints {
|
||||
|
||||
Reference in New Issue
Block a user