Compare commits

...

56 Commits

Author SHA1 Message Date
RiotRobot 3141a7d7c1 v16.0.0-rc.1 2022-03-08 14:43:46 +00:00
RiotRobot 1304e811d0 Prepare changelog for v16.0.0-rc.1 2022-03-08 13:31:50 +00:00
Andy Balaam 2ce1e7e6ef Move codecov into the .github dir (#2220) 2022-03-04 15:56:14 +00:00
Michael Telatynski 70efed1a58 Add test coverage around push rules with no conditions (#2219) 2022-03-04 14:03:35 +00:00
Michael Telatynski 9e4f109e80 Fix wrongly asserting that PushRule::conditions is non-null (#2217) 2022-03-04 08:36:00 +00:00
Michael Telatynski 5d54bf558c Fix defer not supporting resolving with a Promise<T> (#2216) 2022-03-03 21:41:23 +00:00
Germain fc5f0e8047 Fix message ordering in threads (#2215) 2022-03-03 15:21:17 +00:00
Kerry 6bc584ba8b add LocationAssetType enum (#2214)
Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-03-03 10:49:16 +01:00
Germain ac4e504b8d Make eventType optional for relations (#2212) 2022-03-02 14:37:15 +00:00
Michael Telatynski 0abee2a0bf Fix wrong event_id being sent for m.in_reply_to of threads (#2213) 2022-03-02 14:27:08 +00:00
Germain 4e4afdb795 Update thread info after MSC3440 updates (#2209) 2022-03-02 10:52:08 +00:00
Michael Telatynski 9b429c1902 Throw error if both browser-index and (node) index are loaded (#2211) 2022-03-01 20:42:13 +00:00
Germain b782dee2ef Partition root event in thread and room timeline (#2210) 2022-03-01 13:04:24 +00:00
RiotRobot 54e815085f Fix main field 2022-02-28 17:38:59 +00:00
RiotRobot 0739da4ef4 Resetting package fields for development 2022-02-28 16:25:58 +00:00
RiotRobot c300a6bdd6 Merge branch 'master' into develop 2022-02-28 16:24:54 +00:00
RiotRobot 901d53eb38 v15.6.0 2022-02-28 16:16:51 +00:00
RiotRobot a54471f737 Prepare changelog for v15.6.0 2022-02-28 11:53:35 +00:00
Germain 1fae9cb3ef Update versions response type (#2208) 2022-02-28 10:44:38 +00:00
Germain 124bfc9328 Make createThread more resilient when missing rootEvent (#2207) 2022-02-28 10:02:09 +00:00
Šimon Brandner 53aa34fba5 Support for mid-call devices changes (#2154)
* Push to `usermediaSenders` in `upgradeCall()`

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Make sure to enable tracks after a call upgrade

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Simplify `updateMuteStatus()`

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Add copyright for 2022

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Add `updateLocalUsermediaStream()`

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Support mid-call device changes

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Use `updateLocalUsermediaStream()` for call upgrades

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Improve mock classes

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Add new tests

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-02-25 14:52:05 +00:00
Andy Balaam 58756a1973 Provide settings to control the comments codecov adds to PRs (#2206) 2022-02-25 14:22:09 +00:00
Michael Telatynski 7d9800b817 Stop using development /spaces API in favour of v1 /hierarchy API (#2204) 2022-02-25 10:43:24 +00:00
Michael Telatynski e408590c21 [Release] Fix bug with the /hierarchy API sending invalid requests (#2202) 2022-02-24 14:49:51 +00:00
Michael Telatynski 735ccca18b Fix bug with the /hierarchy API sending invalid requests (#2201) 2022-02-24 14:49:48 +00:00
Andy Balaam 946f47e037 Workflow for uploading coverage to codecov (#2200) 2022-02-24 14:40:20 +00:00
Michael Telatynski 4de1699c49 Add new room state emit RoomStateEvent.Update for lower-frequency hits (#2192)
Co-authored-by: Travis Ralston <travisr@matrix.org>
2022-02-24 13:16:59 +00:00
Andy Balaam 3d9221f054 Generate a JSON coverage file when requested to generate coverage (#2199) 2022-02-24 13:05:02 +00:00
Kerry 1d1d59c757 eslint to 8.9.0 (#2198)
Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-02-24 11:42:31 +01:00
Stanislav Demydiuk af43687354 Export additional types (#2195) 2022-02-24 09:35:05 +00:00
Kerry 2ec5acb55d fix relation sender filter (#2196)
* fix relation sender filter

Signed-off-by: Kerry Archibald <kerrya@element.io>

* lint

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-02-24 09:44:18 +01:00
David Baker 2128f67dc3 Fix bug with ine-way audio after a transfer (#2193)
Seems chrome at least will give you a disabled audio track if you
already had another user media audio track and disabled it, so make
sure our tracks are enabled when we add them. We already did this
on one code path but it didn't get moved over when a new code path
was added.

On the plus side, we now know the reason for the ancient code that
had the comment asking what it was for, so update that.
2022-02-23 09:54:14 +00:00
Germain 55dda8420c Update return type for client.getRoom (#2190) 2022-02-22 15:50:23 +00:00
RiotRobot d595717e60 v15.6.0-rc.1 2022-02-22 13:30:52 +00:00
RiotRobot 4603d4e578 Prepare changelog for v15.6.0-rc.1 2022-02-22 13:30:51 +00:00
Michael Telatynski 12e525b664 Improve typing around event emitter handlers (#2180) 2022-02-22 12:18:07 +00:00
Andy Balaam 1ac4cc4b11 Bump matrix-events-sdk to 0.0.1-beta.7 (#2184)
* Bump matrix-events-sdk to 0.0.1-beta.7

* Update lockfile for matrix-events-sdk-0.0.1-beta.7
2022-02-22 11:30:50 +00:00
David Baker 7b218905fb Fix bug where calls could break if rejected from somewhere else (#2189)
* Fix bug where calls could ignore new events of rejected from somewhere else

When callEventHandler passed a reject event to the call object, it assumed
that always caused the call to end and deleted it from the list, so it
never got any more events. The point of a reject is that it doesn't
end the call if it's already been picked up though. This only removes
the call if it's actually ended.

* Use ts-expect-error
2022-02-21 18:52:24 +00:00
David Baker 080426dfdd Fix camera stuck on after call transfer (#2188)
The 'Replaced' error code is handled specially (for better or worse)
so was leaving the capture feeds open. Use the 'transfer' hangup
reason which is what we should be using anyway (and were, on the line
below for the other call...)
2022-02-21 17:11:14 +00:00
Hugh Nimmo-Smith a89c1990d6 Return send event response from MSC3089Branch.createNewVersion() (#2186)
* Return send event response from MSC3089Branch.createNewVersion()

* docs: update JSDoc to match
2022-02-21 15:45:43 +00:00
Germain 4e72290d53 Null-guard for undefined rootEvent when creating a thread (#2187) 2022-02-21 15:06:01 +00:00
Patrick Cloke 7a7318b636 Fix incorrect type referenced in receiptCacheByEventId. (#2185) 2022-02-17 17:39:17 +00:00
RiotRobot 74d24f38f7 Merge branch 'master' into develop 2022-02-17 11:48:50 +00:00
Travis Ralston e86d8861b9 Add functions to support refresh tokens (#2178)
* Add functions for refreshing access tokens

* Add function to change the client's access token in flight

* Appease the linter

* Use sensible code style
2022-02-15 20:07:36 +00:00
Michael Telatynski 2910e62bb6 Fix synthetic read receipt handling (#2174) 2022-02-14 18:45:02 -07:00
Michael Telatynski 2bb14698a5 Revert "Sign backup with cross-signing key when we reset it." (#2175) 2022-02-14 19:11:06 +00:00
RiotRobot 2fd08d72dc Resetting package fields for development 2022-02-14 15:17:32 +00:00
RiotRobot 222f62164c Merge branch 'master' into develop 2022-02-14 15:17:32 +00:00
Hubert Chathi cfad8d3614 Enable key backup after we reset it (#2170)
This ensures that we remember it if bootstrapCrossSigning gets called, so that
the authData gets signed by the master key, if a new key is created.
2022-02-11 08:42:49 -05:00
Germain d9c3b880fc Revert event-mapper optimisations (#2171) 2022-02-11 12:55:44 +00:00
Hugh Nimmo-Smith 25c115739c Fix error in uploadContent() when file is empty under Node.js (#2155)
* Fix error in uploadContent() when file is empty under Node.js

* Make type safe check work

* Make comment actually make sense
2022-02-11 11:16:10 +00:00
David Baker d97c514b8d Log when member event membership is undefined (#2169)
* Log when member event membership is undefined

To diagnose https://github.com/vector-im/element-web/issues/20962

* May as well have a stack trace too
2022-02-10 22:33:49 +00:00
Germain 6b822ccd61 Improve thread partitioning for 2nd degree relations (#2165) 2022-02-10 15:09:46 +00:00
Hubert Chathi 47c5c4645e Check the backup info against the stored private key when determining trust. (#2167) 2022-02-10 08:34:21 -05:00
Hubert Chathi ea0eaff212 Back up keys before logging out (#2158) 2022-02-09 08:36:43 -05:00
David Baker 41bf8c2d5f Yarn upgrade (#2164) 2022-02-08 19:17:28 +00:00
69 changed files with 2934 additions and 1779 deletions
+3
View File
@@ -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: [
+6
View File
@@ -0,0 +1,6 @@
comment:
layout: "diff, files"
behavior: default
require_changes: false
require_base: no
require_head: no
+19
View File
@@ -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
+55
View File
@@ -1,3 +1,58 @@
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)
==================================================================================================
+5 -4
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "15.5.2",
"version": "16.0.0-rc.1",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepublishOnly": "yarn build",
@@ -56,7 +56,7 @@
"bs58": "^4.0.1",
"content-type": "^1.0.4",
"loglevel": "^1.7.1",
"matrix-events-sdk": "^0.0.1-beta.6",
"matrix-events-sdk": "^0.0.1-beta.7",
"p-retry": "^4.5.0",
"qs": "^6.9.6",
"request": "^2.88.2",
@@ -89,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",
@@ -113,7 +113,8 @@
"<rootDir>/src/**/*.{js,ts}"
],
"coverageReporters": [
"text"
"text",
"json"
]
},
"typings": "./lib/index.d.ts"
+45 -124
View File
@@ -3,7 +3,6 @@ import { CRYPTO_ENABLED } from "../../src/client";
import { MatrixEvent } from "../../src/models/event";
import { Filter, MemoryStore, Room } from "../../src/matrix";
import { TestClient } from "../TestClient";
import { Thread } from "../../src/models/thread";
describe("MatrixClient", function() {
let client = null;
@@ -405,6 +404,11 @@ describe("MatrixClient", function() {
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,
@@ -435,6 +439,11 @@ describe("MatrixClient", function() {
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,
@@ -456,6 +465,11 @@ describe("MatrixClient", function() {
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,
@@ -475,119 +489,13 @@ describe("MatrixClient", function() {
]);
});
it("copies post-thread in-thread vote events onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true };
// Events for this test only, because we hack around with them
const eventMessageInThread2 = new MatrixEvent({
"age": 80098509,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
"device_id": "XISFUZSKHH",
"m.relates_to": {
"event_id": "$AAA2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
"m.in_reply_to": {
"event_id": "$AAA2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
},
"rel_type": "io.element.thread",
},
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
},
"event_id": "$AAAhKIGYowtBblVLkRimeIg8TcdjETnxhDPGfi6NpDg",
"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 eventPollStartThreadRoot2 = 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": "$AAA2ojbPmxb6x8ECetn45hmND6cRDcjgv-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 eventPollResponseReference2 = new MatrixEvent({
"age": 80098509,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
"device_id": "XISFUZSKHH",
"m.relates_to": {
"event_id": "$AAA2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
"rel_type": "m.reference",
},
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
},
"event_id": "$AAAvpezvsF0cKgav3g8W-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",
});
// When we react within a thread, sometimes the thread root
// has isThreadRelation === true, because thread is set on it,
// but threadId is not.
eventPollStartThreadRoot2.setThread(
new Thread(
eventPollStartThreadRoot2,
{
client,
room: new Room(),
},
),
);
const events = [
eventPollResponseReference2,
eventMessageInThread2,
eventPollStartThreadRoot2,
];
const [timeline, threaded] = client.partitionThreadedEvents(events);
expect(timeline).toEqual([
eventPollResponseReference2,
// eventPollStartThreadRoot2,
// This is weird: by hacking the thread root to have an inconsistency
// between thread and threadId (which is what I have observed in the
// wild), we have persuaded the code that the thread root is actually
// within the thread, so it is not provided to the main timeline.
//
// This should go away when we fix this inconsistency. When that
// happens, we should probably delete this test.
]);
expect(threaded).toEqual([
withThreadId(
eventPollResponseReference2, eventPollStartThreadRoot2.getId(),
),
eventMessageInThread2,
eventPollStartThreadRoot2, // See note above for why this appears here.
]);
});
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,
@@ -610,6 +518,19 @@ describe("MatrixClient", function() {
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,
@@ -655,7 +576,7 @@ function withThreadId(event, newThreadId) {
return ret;
}
const eventMessageInThread = new MatrixEvent({
const buildEventMessageInThread = () => new MatrixEvent({
"age": 80098509,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
@@ -680,7 +601,7 @@ const eventMessageInThread = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
const eventPollResponseReference = new MatrixEvent({
const buildEventPollResponseReference = () => new MatrixEvent({
"age": 80098509,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
@@ -702,7 +623,7 @@ const eventPollResponseReference = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
const eventReaction = new MatrixEvent({
const buildEventReaction = () => new MatrixEvent({
"content": {
"m.relates_to": {
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
@@ -721,7 +642,7 @@ const eventReaction = new MatrixEvent({
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
});
const eventPollStartThreadRoot = new MatrixEvent({
const buildEventPollStartThreadRoot = () => new MatrixEvent({
"age": 80108647,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
@@ -739,7 +660,7 @@ const eventPollStartThreadRoot = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
const eventRoomName = new MatrixEvent({
const buildEventRoomName = () => new MatrixEvent({
"age": 80123249,
"content": {
"name": "1 poll, 1 vote, 1 thread",
@@ -754,7 +675,7 @@ const eventRoomName = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
const eventEncryption = new MatrixEvent({
const buildEventEncryption = () => new MatrixEvent({
"age": 80123383,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
@@ -769,7 +690,7 @@ const eventEncryption = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
const eventGuestAccess = new MatrixEvent({
const buildEventGuestAccess = () => new MatrixEvent({
"age": 80123473,
"content": {
"guest_access": "can_join",
@@ -784,7 +705,7 @@ const eventGuestAccess = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
const eventHistoryVisibility = new MatrixEvent({
const buildEventHistoryVisibility = () => new MatrixEvent({
"age": 80123556,
"content": {
"history_visibility": "shared",
@@ -799,7 +720,7 @@ const eventHistoryVisibility = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
const eventJoinRules = new MatrixEvent({
const buildEventJoinRules = () => new MatrixEvent({
"age": 80123696,
"content": {
"join_rule": "invite",
@@ -814,7 +735,7 @@ const eventJoinRules = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
const eventPowerLevels = new MatrixEvent({
const buildEventPowerLevels = () => new MatrixEvent({
"age": 80124105,
"content": {
"ban": 50,
@@ -849,7 +770,7 @@ const eventPowerLevels = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
const eventMember = new MatrixEvent({
const buildEventMember = () => new MatrixEvent({
"age": 80125279,
"content": {
"avatar_url": "mxc://matrix.org/aNtbVcFfwotudypZcHsIcPOc",
@@ -866,7 +787,7 @@ const eventMember = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
const eventCreate = new MatrixEvent({
const buildEventCreate = () => new MatrixEvent({
"age": 80126105,
"content": {
"creator": "@andybalaam-test1:matrix.org",
+2 -2
View File
@@ -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();
}
+1
View File
@@ -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) {
+1
View File
@@ -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
View File
@@ -1,4 +1,5 @@
import '../olm-loader';
// eslint-disable-next-line no-restricted-imports
import { EventEmitter } from "events";
import { Crypto } from "../../src/crypto";
+1 -1
View File
@@ -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();
-34
View File
@@ -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);
});
});
});
+132
View File
@@ -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);
});
});
});
+4 -4
View File
@@ -17,7 +17,7 @@ limitations under the License.
import { makeLocationContent } from "../../src/content-helpers";
import {
ASSET_NODE_TYPE,
ASSET_TYPE_SELF,
LocationAssetType,
LOCATION_EVENT_TYPE,
TIMESTAMP_NODE_TYPE,
} from "../../src/@types/location";
@@ -33,14 +33,14 @@ describe("Location", function() {
uri: "geo:foo",
description: undefined,
});
expect(ASSET_NODE_TYPE.findIn(loc)).toEqual({ type: ASSET_TYPE_SELF });
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", "m.something");
"txxt", "geo:bar", 134235436, "desc", LocationAssetType.Pin);
expect(loc.body).toEqual("txxt");
expect(loc.msgtype).toEqual("m.location");
@@ -49,7 +49,7 @@ describe("Location", function() {
uri: "geo:bar",
description: "desc",
});
expect(ASSET_NODE_TYPE.findIn(loc)).toEqual({ type: "m.something" });
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);
});
@@ -91,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(() => {}),
@@ -123,6 +119,7 @@ describe("MatrixClient", function() {
}
if (next.error) {
// eslint-disable-next-line
return Promise.reject({
errcode: next.error.errcode,
httpStatus: next.error.httpStatus,
@@ -133,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(() => {});
}
@@ -158,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,
@@ -379,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);
@@ -403,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") {
@@ -423,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 : "");
@@ -501,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) {
@@ -584,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 = [];
@@ -640,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;
@@ -948,4 +945,39 @@ describe("MatrixClient", function() {
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;
});
});
});
+16
View File
@@ -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);
});
});
+3 -3
View File
@@ -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, {
File diff suppressed because it is too large Load Diff
+114 -5
View File
@@ -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");
});
});
+29
View File
@@ -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 */
+1
View File
@@ -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
+5 -2
View File
@@ -27,7 +27,10 @@ export const ASSET_NODE_TYPE = new UnstableValue("m.asset", "org.matrix.msc3488.
export const TIMESTAMP_NODE_TYPE = new UnstableValue("m.ts", "org.matrix.msc3488.ts");
export const ASSET_TYPE_SELF = "m.self";
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
@@ -60,7 +63,7 @@ export interface ILocationContent extends IContent {
description?: string;
};
[ASSET_NODE_TYPE.name]: {
type: string;
type: LocationAssetType;
};
[TEXT_NODE_TYPE.name]: string;
[TIMESTAMP_NODE_TYPE.name]: number;
-22
View File
@@ -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
View File
@@ -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);
}
}
+4
View File
@@ -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
+307 -141
View File
@@ -19,15 +19,22 @@ limitations under the License.
* @module client
*/
import { EventEmitter } from "events";
import { EmoteEvent, MessageEvent, NoticeEvent, IPartialEvent } from "matrix-events-sdk";
import { EmoteEvent, IPartialEvent, MessageEvent, NoticeEvent } from "matrix-events-sdk";
import { ISyncStateData, SyncApi, SyncState } from "./sync";
import { EventStatus, IContent, IDecryptOptions, IEvent, MatrixEvent } from "./models/event";
import {
EventStatus,
IContent,
IDecryptOptions,
IEvent,
MatrixEvent,
MatrixEventEvent,
MatrixEventHandlerMap,
} from "./models/event";
import { StubStore } from "./store/stub";
import { createNewMatrixCall, MatrixCall } from "./webrtc/call";
import { CallEvent, CallEventHandlerMap, createNewMatrixCall, MatrixCall } from "./webrtc/call";
import { Filter, IFilterDefinition } from "./filter";
import { CallEventHandler } from './webrtc/callEventHandler';
import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from './webrtc/callEventHandler';
import * as utils from './utils';
import { sleep } from './utils';
import { Group } from "./models/group";
@@ -37,12 +44,12 @@ import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery";
import * as olmlib from "./crypto/olmlib";
import { decodeBase64, encodeBase64 } from "./crypto/olmlib";
import { IExportedDevice as IOlmDevice } from "./crypto/OlmDevice";
import { ReEmitter } from './ReEmitter';
import { TypedReEmitter } from './ReEmitter';
import { IRoomEncryption, RoomList } from './crypto/RoomList';
import { logger } from './logger';
import { SERVICE_TYPES } from './service-types';
import {
FileType,
FileType, HttpApiEvent, HttpApiEventHandlerMap,
IHttpOpts,
IUpload,
MatrixError,
@@ -52,11 +59,14 @@ import {
PREFIX_MEDIA_R0,
PREFIX_R0,
PREFIX_UNSTABLE,
PREFIX_V1,
retryNetworkOperation,
UploadContentResponseType,
} from "./http-api";
import {
Crypto,
CryptoEvent,
CryptoEventHandlerMap,
fixBackupKey,
IBootstrapCrossSigningOpts,
ICheckOwnCrossSigningTrustOpts,
@@ -67,7 +77,7 @@ import {
import { DeviceInfo, IDevice } from "./crypto/deviceinfo";
import { decodeRecoveryKey } from './crypto/recoverykey';
import { keyFromAuthData } from './crypto/key_passphrase';
import { User } from "./models/user";
import { User, UserEvent, UserEventHandlerMap } from "./models/user";
import { getHttpUriForMxc } from "./content-repo";
import { SearchResult } from "./models/search-result";
import {
@@ -94,6 +104,12 @@ import {
IRoomEvent,
IStateEvent,
NotificationCountType,
RoomEvent,
RoomEventHandlerMap,
RoomMemberEvent,
RoomMemberEventHandlerMap,
RoomStateEvent,
RoomStateEventHandlerMap,
} from "./matrix";
import {
CrossSigningKey,
@@ -155,11 +171,13 @@ import {
SearchOrderBy,
} from "./@types/search";
import { ISynapseAdminDeactivateResponse, ISynapseAdminWhoisResponse } from "./@types/synapse";
import { IHierarchyRoom, ISpaceSummaryEvent, ISpaceSummaryRoom } from "./@types/spaces";
import { IHierarchyRoom } from "./@types/spaces";
import { IPusher, IPusherRequest, IPushRules, PushRuleAction, PushRuleKind, RuleId } from "./@types/PushRules";
import { IThreepid } from "./@types/threepids";
import { CryptoStore } from "./crypto/store/base";
import { MediaHandler } from "./webrtc/mediaHandler";
import { IRefreshTokenResponse } from "./@types/auth";
import { TypedEventEmitter } from "./models/typed-event-emitter";
export type Store = IStore;
export type SessionStore = WebStorageSessionStore;
@@ -458,7 +476,7 @@ export interface ISignedKey {
}
export type KeySignatures = Record<string, Record<string, ICrossSigningKey | ISignedKey>>;
interface IUploadKeySignaturesResponse {
export interface IUploadKeySignaturesResponse {
failures: Record<string, Record<string, {
errcode: string;
error: string;
@@ -492,7 +510,7 @@ interface ITurnServer {
}
interface IServerVersions {
versions: string;
versions: string[];
unstable_features: Record<string, boolean>;
}
@@ -752,15 +770,109 @@ interface ITimestampToEventResponse {
// Probably not the most graceful solution but does a good enough job for now
const EVENT_ID_PREFIX = "$";
export enum ClientEvent {
Sync = "sync",
Event = "event",
ToDeviceEvent = "toDeviceEvent",
AccountData = "accountData",
Room = "Room",
DeleteRoom = "deleteRoom",
SyncUnexpectedError = "sync.unexpectedError",
ClientWellKnown = "WellKnown.client",
/* @deprecated */
Group = "Group",
// The following enum members are both deprecated and in the wrong place, Groups haven't been TSified
GroupProfile = "Group.profile",
GroupMyMembership = "Group.myMembership",
}
type RoomEvents = RoomEvent.Name
| RoomEvent.Redaction
| RoomEvent.RedactionCancelled
| RoomEvent.Receipt
| RoomEvent.Tags
| RoomEvent.LocalEchoUpdated
| RoomEvent.AccountData
| RoomEvent.MyMembership
| RoomEvent.Timeline
| RoomEvent.TimelineReset;
type RoomStateEvents = RoomStateEvent.Events
| RoomStateEvent.Members
| RoomStateEvent.NewMember
| RoomStateEvent.Update
;
type CryptoEvents = CryptoEvent.KeySignatureUploadFailure
| CryptoEvent.KeyBackupStatus
| CryptoEvent.KeyBackupFailed
| CryptoEvent.KeyBackupSessionsRemaining
| CryptoEvent.RoomKeyRequest
| CryptoEvent.RoomKeyRequestCancellation
| CryptoEvent.VerificationRequest
| CryptoEvent.DeviceVerificationChanged
| CryptoEvent.UserTrustStatusChanged
| CryptoEvent.KeysChanged
| CryptoEvent.Warning
| CryptoEvent.DevicesUpdated
| CryptoEvent.WillUpdateDevices;
type MatrixEventEvents = MatrixEventEvent.Decrypted | MatrixEventEvent.Replaced | MatrixEventEvent.VisibilityChange;
type RoomMemberEvents = RoomMemberEvent.Name
| RoomMemberEvent.Typing
| RoomMemberEvent.PowerLevel
| RoomMemberEvent.Membership;
type UserEvents = UserEvent.AvatarUrl
| UserEvent.DisplayName
| UserEvent.Presence
| UserEvent.CurrentlyActive
| UserEvent.LastPresenceTs;
type EmittedEvents = ClientEvent
| RoomEvents
| RoomStateEvents
| CryptoEvents
| MatrixEventEvents
| RoomMemberEvents
| UserEvents
| CallEvent // re-emitted by call.ts using Object.values
| CallEventHandlerEvent.Incoming
| HttpApiEvent.SessionLoggedOut
| HttpApiEvent.NoConsent;
export type ClientEventHandlerMap = {
[ClientEvent.Sync]: (state: SyncState, lastState?: SyncState, data?: ISyncStateData) => void;
[ClientEvent.Event]: (event: MatrixEvent) => void;
[ClientEvent.ToDeviceEvent]: (event: MatrixEvent) => void;
[ClientEvent.AccountData]: (event: MatrixEvent, lastEvent?: MatrixEvent) => void;
[ClientEvent.Room]: (room: Room) => void;
[ClientEvent.DeleteRoom]: (roomId: string) => void;
[ClientEvent.SyncUnexpectedError]: (error: Error) => void;
[ClientEvent.ClientWellKnown]: (data: IClientWellKnown) => void;
[ClientEvent.Group]: (group: Group) => void;
[ClientEvent.GroupProfile]: (group: Group) => void;
[ClientEvent.GroupMyMembership]: (group: Group) => void;
} & RoomEventHandlerMap
& RoomStateEventHandlerMap
& CryptoEventHandlerMap
& MatrixEventHandlerMap
& RoomMemberEventHandlerMap
& UserEventHandlerMap
& CallEventHandlerEventHandlerMap
& CallEventHandlerMap
& HttpApiEventHandlerMap;
/**
* Represents a Matrix Client. Only directly construct this if you want to use
* custom modules. Normally, {@link createClient} should be used
* as it specifies 'sensible' defaults for these modules.
*/
export class MatrixClient extends EventEmitter {
export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHandlerMap> {
public static readonly RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY';
public reEmitter = new ReEmitter(this);
public reEmitter = new TypedReEmitter<EmittedEvents, ClientEventHandlerMap>(this);
public olmVersion: [number, number, number] = null; // populated after initCrypto
public usingExternalCrypto = false;
public store: Store;
@@ -822,7 +934,7 @@ export class MatrixClient extends EventEmitter {
protected checkTurnServersIntervalID: number;
protected exportedOlmDeviceToImport: IOlmDevice;
protected txnCtr = 0;
protected mediaHandler = new MediaHandler();
protected mediaHandler = new MediaHandler(this);
protected pendingEventEncryption = new Map<string, Promise<void>>();
constructor(opts: IMatrixClientCreateOpts) {
@@ -841,7 +953,7 @@ export class MatrixClient extends EventEmitter {
const userId = opts.userId || null;
this.credentials = { userId };
this.http = new MatrixHttpApi(this, {
this.http = new MatrixHttpApi(this as ConstructorParameters<typeof MatrixHttpApi>[0], {
baseUrl: opts.baseUrl,
idBaseUrl: opts.idBaseUrl,
accessToken: opts.accessToken,
@@ -902,7 +1014,7 @@ export class MatrixClient extends EventEmitter {
// Start listening for calls after the initial sync is done
// We do not need to backfill the call event buffer
// with encrypted events that might never get decrypted
this.on("sync", this.startCallEventHandler);
this.on(ClientEvent.Sync, this.startCallEventHandler);
}
this.timelineSupport = Boolean(opts.timelineSupport);
@@ -927,7 +1039,7 @@ export class MatrixClient extends EventEmitter {
// actions for themselves, so we have to kinda help them out when they are encrypted.
// We do this so that push rules are correctly executed on events in their decrypted
// state, such as highlights when the user's name is mentioned.
this.on("Event.decrypted", (event) => {
this.on(MatrixEventEvent.Decrypted, (event) => {
const oldActions = event.getPushActions();
const actions = this.getPushActionsForEvent(event, true);
@@ -962,7 +1074,7 @@ export class MatrixClient extends EventEmitter {
// Like above, we have to listen for read receipts from ourselves in order to
// correctly handle notification counts on encrypted rooms.
// This fixes https://github.com/vector-im/element-web/issues/9421
this.on("Room.receipt", (event, room) => {
this.on(RoomEvent.Receipt, (event, room) => {
if (room && this.isRoomEncrypted(room.roomId)) {
// Figure out if we've read something or if it's just informational
const content = event.getContent();
@@ -997,7 +1109,7 @@ export class MatrixClient extends EventEmitter {
// Note: we don't need to handle 'total' notifications because the counts
// will come from the server.
room.setUnreadNotificationCount("highlight", highlightCount);
room.setUnreadNotificationCount(NotificationCountType.Highlight, highlightCount);
}
});
}
@@ -1562,16 +1674,16 @@ export class MatrixClient extends EventEmitter {
);
this.reEmitter.reEmit(crypto, [
"crypto.keyBackupFailed",
"crypto.keyBackupSessionsRemaining",
"crypto.roomKeyRequest",
"crypto.roomKeyRequestCancellation",
"crypto.warning",
"crypto.devicesUpdated",
"crypto.willUpdateDevices",
"deviceVerificationChanged",
"userTrustStatusChanged",
"crossSigning.keysChanged",
CryptoEvent.KeyBackupFailed,
CryptoEvent.KeyBackupSessionsRemaining,
CryptoEvent.RoomKeyRequest,
CryptoEvent.RoomKeyRequestCancellation,
CryptoEvent.Warning,
CryptoEvent.DevicesUpdated,
CryptoEvent.WillUpdateDevices,
CryptoEvent.DeviceVerificationChanged,
CryptoEvent.UserTrustStatusChanged,
CryptoEvent.KeysChanged,
]);
logger.log("Crypto: initialising crypto object...");
@@ -1583,9 +1695,8 @@ export class MatrixClient extends EventEmitter {
this.olmVersion = Crypto.getOlmVersion();
// if crypto initialisation was successful, tell it to attach its event
// handlers.
crypto.registerEventHandlers(this);
// if crypto initialisation was successful, tell it to attach its event handlers.
crypto.registerEventHandlers(this as Parameters<Crypto["registerEventHandlers"]>[0]);
this.crypto = crypto;
}
@@ -1825,7 +1936,7 @@ export class MatrixClient extends EventEmitter {
* @returns {Verification} a verification object
* @deprecated Use `requestVerification` instead.
*/
public beginKeyVerification(method: string, userId: string, deviceId: string): Verification {
public beginKeyVerification(method: string, userId: string, deviceId: string): Verification<any, any> {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
@@ -3159,9 +3270,9 @@ export class MatrixClient extends EventEmitter {
* has been emitted. Note in particular that other events, eg. RoomState.members
* will be emitted for a room before this function will return the given room.
* @param {string} roomId The room ID
* @return {Room} The Room or null if it doesn't exist or there is no data store.
* @return {Room|null} The Room or null if it doesn't exist or there is no data store.
*/
public getRoom(roomId: string): Room {
public getRoom(roomId: string): Room | null {
return this.store.getRoom(roomId);
}
@@ -3594,17 +3705,20 @@ export class MatrixClient extends EventEmitter {
threadId = null;
}
if (threadId && content["m.relates_to"]?.rel_type !== RelationType.Thread) {
// If we expect that an event is part of a thread but is missing the relation
// we need to add it manually, as well as the reply fallback
if (threadId && !content["m.relates_to"]?.rel_type) {
content["m.relates_to"] = {
...content["m.relates_to"],
"rel_type": RelationType.Thread,
"event_id": threadId,
};
const thread = this.getRoom(roomId)?.threads.get(threadId);
if (thread) {
content["m.relates_to"]["m.in_reply_to"] = {
"event_id": thread.replyToEvent.getId(),
"event_id": thread.lastReply((ev: MatrixEvent) => {
return ev.isThreadRelation && !ev.status;
})?.getId(),
};
}
}
@@ -3652,6 +3766,7 @@ export class MatrixClient extends EventEmitter {
const thread = room?.threads.get(threadId);
if (thread) {
localEvent.setThread(thread);
localEvent.setThreadId(thread.id);
}
// if this is a relation or redaction of an event
@@ -3659,9 +3774,9 @@ export class MatrixClient extends EventEmitter {
// then listen for the remote echo of that event so that by the time
// this event does get sent, we have the correct event_id
const targetId = localEvent.getAssociatedId();
if (targetId && targetId.startsWith("~")) {
if (targetId?.startsWith("~")) {
const target = room.getPendingEvents().find(e => e.getId() === targetId);
target.once("Event.localEventIdReplaced", () => {
target.once(MatrixEventEvent.LocalEventIdReplaced, () => {
localEvent.updateAssociatedId(target.getId());
});
}
@@ -4759,7 +4874,7 @@ export class MatrixClient extends EventEmitter {
}
return promise.then((response) => {
this.store.removeRoom(roomId);
this.emit("deleteRoom", roomId);
this.emit(ClientEvent.DeleteRoom, roomId);
return response;
});
}
@@ -4912,7 +5027,7 @@ export class MatrixClient extends EventEmitter {
const user = this.getUser(this.getUserId());
if (user) {
user.displayName = name;
user.emit("User.displayName", user.events.presence, user);
user.emit(UserEvent.DisplayName, user.events.presence, user);
}
return prom;
}
@@ -4929,7 +5044,7 @@ export class MatrixClient extends EventEmitter {
const user = this.getUser(this.getUserId());
if (user) {
user.avatarUrl = url;
user.emit("User.avatarUrl", user.events.presence, user);
user.emit(UserEvent.AvatarUrl, user.events.presence, user);
}
return prom;
}
@@ -5082,7 +5197,7 @@ export class MatrixClient extends EventEmitter {
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(matrixEvents);
room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
await this.processThreadEvents(room, threadedEvents);
await this.processThreadEvents(room, threadedEvents, true);
room.oldState.paginationToken = res.end;
if (res.chunk.length === 0) {
@@ -5193,7 +5308,7 @@ export class MatrixClient extends EventEmitter {
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(matrixEvents);
timelineSet.addEventsToTimeline(timelineEvents, true, timeline, res.start);
await this.processThreadEvents(timelineSet.room, threadedEvents);
await this.processThreadEvents(timelineSet.room, threadedEvents, true);
// there is no guarantee that the event ended up in "timeline" (we
// might have switched to a neighbouring timeline) - so check the
@@ -5326,7 +5441,7 @@ export class MatrixClient extends EventEmitter {
const timelineSet = eventTimeline.getTimelineSet();
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
await this.processThreadEvents(timelineSet.room, threadedEvents);
await this.processThreadEvents(timelineSet.room, threadedEvents, backwards);
// if we've hit the end of the timeline, we need to stop trying to
// paginate. We need to keep the 'forwards' token though, to make sure
@@ -5364,7 +5479,7 @@ export class MatrixClient extends EventEmitter {
eventTimeline.getTimelineSet()
.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
await this.processThreadEvents(room, threadedEvents);
await this.processThreadEvents(room, threadedEvents, backwards);
// if we've hit the end of the timeline, we need to stop trying to
// paginate. We need to keep the 'forwards' token though, to make sure
@@ -6099,7 +6214,7 @@ export class MatrixClient extends EventEmitter {
private startCallEventHandler = (): void => {
if (this.isInitialSyncComplete()) {
this.callEventHandler.start();
this.off("sync", this.startCallEventHandler);
this.off(ClientEvent.Sync, this.startCallEventHandler);
}
};
@@ -6247,7 +6362,7 @@ export class MatrixClient extends EventEmitter {
// it absorbs errors and returns `{}`.
this.clientWellKnownPromise = AutoDiscovery.getRawClientConfig(this.getDomain());
this.clientWellKnown = await this.clientWellKnownPromise;
this.emit("WellKnown.client", this.clientWellKnown);
this.emit(ClientEvent.ClientWellKnown, this.clientWellKnown);
}
public getClientWellKnown(): IClientWellKnown {
@@ -6485,8 +6600,8 @@ export class MatrixClient extends EventEmitter {
public async relations(
roomId: string,
eventId: string,
relationType: RelationType | string | null,
eventType: EventType | string | null,
relationType?: RelationType | string | null,
eventType?: EventType | string | null,
opts: IRelationsRequestOpts = {},
): Promise<{
originalEvent: MatrixEvent;
@@ -6509,12 +6624,10 @@ export class MatrixClient extends EventEmitter {
let events = result.chunk.map(mapper);
if (fetchedEventType === EventType.RoomMessageEncrypted) {
const allEvents = originalEvent ? events.concat(originalEvent) : events;
await Promise.all(allEvents.map(e => {
if (e.isEncrypted()) {
return new Promise(resolve => e.once("Event.decrypted", resolve));
}
}));
events = events.filter(e => e.getType() === eventType);
await Promise.all(allEvents.map(e => this.decryptEventIfNeeded(e)));
if (eventType !== null) {
events = events.filter(e => e.getType() === eventType);
}
}
if (originalEvent && relationType === RelationType.Replace) {
events = events.filter(e => e.getSender() === originalEvent.getSender());
@@ -6615,6 +6728,14 @@ export class MatrixClient extends EventEmitter {
return this.http.opts.accessToken || null;
}
/**
* Set the access token associated with this account.
* @param {string} token The new access token.
*/
public setAccessToken(token: string) {
this.http.opts.accessToken = token;
}
/**
* @return {boolean} true if there is a valid access_token for this client.
*/
@@ -6691,6 +6812,7 @@ export class MatrixClient extends EventEmitter {
const params: any = {
auth: auth,
refresh_token: true, // always ask for a refresh token - does nothing if unsupported
};
if (username !== undefined && username !== null) {
params.username = username;
@@ -6768,6 +6890,31 @@ export class MatrixClient extends EventEmitter {
return this.http.request(callback, Method.Post, "/register", params, data);
}
/**
* Refreshes an access token using a provided refresh token. The refresh token
* must be valid for the current access token known to the client instance.
*
* Note that this function will not cause a logout if the token is deemed
* unknown by the server - the caller is responsible for managing logout
* actions on error.
* @param {string} refreshToken The refresh token.
* @return {Promise<IRefreshTokenResponse>} Resolves to the new token.
* @return {module:http-api.MatrixError} Rejects with an error response.
*/
public refreshToken(refreshToken: string): Promise<IRefreshTokenResponse> {
return this.http.authedRequest(
undefined,
Method.Post,
"/refresh",
undefined,
{ refresh_token: refreshToken },
{
prefix: PREFIX_V1,
inhibitLogoutEmit: true, // we don't want to cause logout loops
},
);
}
/**
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: TODO
@@ -6881,7 +7028,17 @@ export class MatrixClient extends EventEmitter {
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: On success, the empty object
*/
public logout(callback?: Callback): Promise<{}> {
public async logout(callback?: Callback): Promise<{}> {
if (this.crypto?.backupManager?.getKeyBackupEnabled()) {
try {
while (await this.crypto.backupManager.backupPendingKeys(200) > 0);
} catch (err) {
logger.error(
"Key backup request failed when logging out. Some keys may be missing from backup",
err,
);
}
}
return this.http.authedRequest(
callback, Method.Post, '/logout',
);
@@ -6991,8 +7148,16 @@ export class MatrixClient extends EventEmitter {
const queryString = utils.encodeParams(opts as Record<string, string | number>);
let templatedUrl = "/rooms/$roomId/relations/$eventId";
if (relationType !== null) templatedUrl += "/$relationType";
if (eventType !== null) templatedUrl += "/$eventType";
if (relationType !== null) {
templatedUrl += "/$relationType";
if (eventType !== null) {
templatedUrl += "/$eventType";
}
} else if (eventType !== null) {
logger.warn(`eventType: ${eventType} ignored when fetching
relations as relationType is null`);
eventType = null;
}
const path = utils.encodeUri(
templatedUrl + "?" + queryString, {
@@ -8530,40 +8695,6 @@ export class MatrixClient extends EventEmitter {
return this.http.authedRequest(undefined, Method.Post, path, null, { score, reason });
}
/**
* Fetches or paginates a summary of a space as defined by an initial version of MSC2946
* @param {string} roomId The ID of the space-room to use as the root of the summary.
* @param {number?} maxRoomsPerSpace The maximum number of rooms to return per subspace.
* @param {boolean?} suggestedOnly Whether to only return rooms with suggested=true.
* @param {boolean?} autoJoinOnly Whether to only return rooms with auto_join=true.
* @param {number?} limit The maximum number of rooms to return in total.
* @param {string?} batch The opaque token to paginate a previous summary request.
* @returns {Promise} the response, with next_token, rooms fields.
* @deprecated in favour of `getRoomHierarchy` due to the MSC changing paths.
*/
public getSpaceSummary(
roomId: string,
maxRoomsPerSpace?: number,
suggestedOnly?: boolean,
autoJoinOnly?: boolean,
limit?: number,
batch?: string,
): Promise<{rooms: ISpaceSummaryRoom[], events: ISpaceSummaryEvent[]}> {
const path = utils.encodeUri("/rooms/$roomId/spaces", {
$roomId: roomId,
});
return this.http.authedRequest(undefined, Method.Post, path, null, {
max_rooms_per_space: maxRoomsPerSpace,
suggested_only: suggestedOnly,
auto_join_only: autoJoinOnly,
limit,
batch,
}, {
prefix: "/_matrix/client/unstable/org.matrix.msc2946",
});
}
/**
* Fetches or paginates a room hierarchy as defined by MSC2946.
* Falls back gracefully to sourcing its data from `getSpaceSummary` if this API is not yet supported by the server.
@@ -8585,30 +8716,21 @@ export class MatrixClient extends EventEmitter {
$roomId: roomId,
});
return this.http.authedRequest<IRoomHierarchy>(undefined, Method.Get, path, {
const queryParams: Record<string, string | string[]> = {
suggested_only: String(suggestedOnly),
max_depth: maxDepth?.toString(),
from: fromToken,
limit: limit?.toString(),
}, undefined, {
prefix: "/_matrix/client/unstable/org.matrix.msc2946",
};
return this.http.authedRequest<IRoomHierarchy>(undefined, Method.Get, path, queryParams, undefined, {
prefix: PREFIX_V1,
}).catch(e => {
if (e.errcode === "M_UNRECOGNIZED") {
// fall back to the older space summary API as it exposes the same data just in a different shape.
return this.getSpaceSummary(roomId, undefined, suggestedOnly, undefined, limit)
.then(({ rooms, events }) => {
// Translate response from `/spaces` to that we expect in this API.
const roomMap = new Map(rooms.map(r => {
return [r.room_id, <IHierarchyRoom>{ ...r, children_state: [] }];
}));
events.forEach(e => {
roomMap.get(e.room_id)?.children_state.push(e);
});
return {
rooms: Array.from(roomMap.values()),
};
});
// fall back to the prefixed hierarchy API.
return this.http.authedRequest<IRoomHierarchy>(undefined, Method.Get, path, queryParams, undefined, {
prefix: "/_matrix/client/unstable/org.matrix.msc2946",
});
}
throw e;
@@ -9079,6 +9201,60 @@ export class MatrixClient extends EventEmitter {
return threadRoots;
}
private eventShouldLiveIn(event: MatrixEvent, room: Room, events: MatrixEvent[], roots: Set<string>): {
shouldLiveInRoom: boolean;
shouldLiveInThread: boolean;
threadId?: string;
} {
if (event.isThreadRoot) {
return {
shouldLiveInRoom: true,
shouldLiveInThread: true,
threadId: event.getId(),
};
}
// A thread relation is always only shown in a thread
if (event.isThreadRelation) {
return {
shouldLiveInRoom: false,
shouldLiveInThread: true,
threadId: event.relationEventId,
};
}
const parentEventId = event.getAssociatedId();
const parentEvent = room?.findEventById(parentEventId) || events.find((mxEv: MatrixEvent) => (
mxEv.getId() === parentEventId
));
// A reaction targetting the thread root needs to be routed to both the
// the main timeline and the associated thread
const targetingThreadRoot = parentEvent?.isThreadRoot || roots.has(event.relationEventId);
if (targetingThreadRoot) {
return {
shouldLiveInRoom: true,
shouldLiveInThread: true,
threadId: event.relationEventId,
};
}
// If the parent event also has an associated ID we want to re-run the
// computation for that parent event.
// In the case of the redaction of a reaction that targets a root event
// we want that redaction to be pushed to both timeline
if (parentEvent?.getAssociatedId()) {
return this.eventShouldLiveIn(parentEvent, room, events, roots);
} else {
// We've exhausted all scenarios, can safely assume that this event
// should live in the room timeline
return {
shouldLiveInRoom: true,
shouldLiveInThread: false,
};
}
}
public partitionThreadedEvents(events: MatrixEvent[]): [MatrixEvent[], MatrixEvent[]] {
// Indices to the events array, for readibility
const ROOM = 0;
@@ -9087,35 +9263,22 @@ export class MatrixClient extends EventEmitter {
const threadRoots = this.findThreadRoots(events);
return events.reduce((memo, event: MatrixEvent) => {
const room = this.getRoom(event.getRoomId());
// An event should live in the thread timeline if
// - It's a reply in thread event
// - It's related to a reply in thread event
let shouldLiveInThreadTimeline = event.isThreadRelation;
if (!shouldLiveInThreadTimeline) {
const parentEventId = event.parentEventId;
const parentEvent = room?.findEventById(parentEventId) || events.find((mxEv: MatrixEvent) => {
return mxEv.getId() === parentEventId;
});
const targetingThreadRoot = parentEvent?.isThreadRoot || threadRoots.has(event.relationEventId);
if (targetingThreadRoot && !event.isThreadRelation && event.relationEventId) {
// If we refer to the thread root, we should be copied
// into the thread as well as the main timeline.
// This happens for reactions, annotations, poll votes etc.
const copiedEvent = event.toSnapshot();
const {
shouldLiveInRoom,
shouldLiveInThread,
threadId,
} = this.eventShouldLiveIn(event, room, events, threadRoots);
// The copied event is in this thread:
copiedEvent.setThreadId(parentEventId);
memo[THREAD].push(copiedEvent);
} else if (parentEvent?.isThreadRelation) {
// If our parent is in a thread, we are in that
// same thread too. (E.g. if I reply within a thread.)
shouldLiveInThreadTimeline = true;
event.setThreadId(parentEvent.threadRootId);
}
if (shouldLiveInRoom) {
memo[ROOM].push(event);
}
const targetTimeline = shouldLiveInThreadTimeline ? THREAD : ROOM;
memo[targetTimeline].push(event);
if (shouldLiveInThread) {
event.setThreadId(threadId);
memo[THREAD].push(event);
}
return memo;
}, [[], []]);
} else {
@@ -9131,10 +9294,13 @@ export class MatrixClient extends EventEmitter {
/**
* @experimental
*/
public async processThreadEvents(room: Room, threadedEvents: MatrixEvent[]): Promise<void> {
threadedEvents.sort((a, b) => a.getTs() - b.getTs());
public async processThreadEvents(
room: Room,
threadedEvents: MatrixEvent[],
toStartOfTimeline: boolean,
): Promise<void> {
for (const event of threadedEvents) {
await room.addThreadedEvent(event);
await room.addThreadedEvent(event, toStartOfTimeline);
}
}
+3 -3
View File
@@ -20,8 +20,8 @@ import { MsgType } from "./@types/event";
import { TEXT_NODE_TYPE } from "./@types/extensible_events";
import {
ASSET_NODE_TYPE,
ASSET_TYPE_SELF,
ILocationContent,
LocationAssetType,
LOCATION_EVENT_TYPE,
TIMESTAMP_NODE_TYPE,
} from "./@types/location";
@@ -121,7 +121,7 @@ export function makeLocationContent(
uri: string,
ts: number,
description?: string,
assetType?: string,
assetType?: LocationAssetType,
): ILocationContent {
return {
"body": text,
@@ -132,7 +132,7 @@ export function makeLocationContent(
description,
},
[ASSET_NODE_TYPE.name]: {
type: assetType ?? ASSET_TYPE_SELF,
type: assetType ?? LocationAssetType.Self,
},
[TEXT_NODE_TYPE.name]: text,
[TIMESTAMP_NODE_TYPE.name]: ts,
+2 -5
View File
@@ -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);
+8 -6
View File
@@ -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);
}
}
}
+16 -5
View File
@@ -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 {};
});
}
+9 -9
View File
@@ -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
View File
@@ -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 -60
View File
@@ -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;
}
@@ -3045,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: ` +
@@ -3070,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);
});
}
@@ -3219,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);
@@ -3254,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);
}
}
@@ -3555,7 +3611,7 @@ export class Crypto extends EventEmitter {
return;
}
this.emit("crypto.roomKeyRequest", req);
this.emit(CryptoEvent.RoomKeyRequest, req);
}
/**
@@ -3574,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);
}
/**
+14 -4
View File
@@ -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);
}
}
+2 -2
View File
@@ -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,
+17 -7
View File
@@ -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
+12 -4
View File
@@ -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
View File
@@ -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", "Event.visibilityChange"]);
client.reEmitter.reEmit(event, [
MatrixEventEvent.Replaced,
MatrixEventEvent.VisibilityChange,
]);
}
return event;
}
+12 -7
View File
@@ -15,7 +15,10 @@ limitations under the License.
*/
import { RelationType } from "./@types/event";
import { UNSTABLE_FILTER_RELATION_SENDERS, UNSTABLE_FILTER_RELATION_TYPES } from "./filter";
import {
UNSTABLE_FILTER_RELATED_BY_REL_TYPES,
UNSTABLE_FILTER_RELATED_BY_SENDERS,
} from "./filter";
import { MatrixEvent } from "./models/event";
/**
@@ -48,7 +51,8 @@ export interface IFilterComponent {
not_senders?: string[];
contains_url?: boolean;
limit?: number;
[UNSTABLE_FILTER_RELATION_TYPES.name]?: Array<RelationType | string>;
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]?: string[];
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]?: Array<RelationType | string>;
}
/* eslint-enable camelcase */
@@ -80,9 +84,10 @@ export class FilterComponent {
// of performance
// This should be improved when bundled relationships solve that problem
const relationSenders = [];
if (this.userId && relations?.[RelationType.Thread]?.current_user_participated) {
if (this.userId && bundledRelationships?.[RelationType.Thread]?.current_user_participated) {
relationSenders.push(this.userId);
}
return this.checkFields(
event.getRoomId(),
event.getSender(),
@@ -105,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),
};
}
@@ -160,14 +165,14 @@ export class FilterComponent {
return false;
}
const relationTypesFilter = this.filterJson[UNSTABLE_FILTER_RELATION_TYPES.name];
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_RELATION_SENDERS.name];
const relationSendersFilter = this.filterJson[UNSTABLE_FILTER_RELATED_BY_SENDERS.name];
if (relationSendersFilter !== undefined) {
if (!this.arrayMatchesFilter(relationSendersFilter, relationSenders)) {
return false;
+6 -6
View File
@@ -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 {}
+35 -10
View File
@@ -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,
},
@@ -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);
}
});
+4
View File
@@ -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 {
+1
View File
@@ -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';
+5 -2
View File
@@ -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;
}
/**
+40
View File
@@ -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",
}
+30 -19
View File
@@ -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;
@@ -58,7 +56,15 @@ export interface IRoomTimelineData {
liveEvent?: boolean;
}
export class EventTimelineSet extends EventEmitter {
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;
@@ -159,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.
@@ -253,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);
}
/**
@@ -603,8 +604,7 @@ export class EventTimelineSet extends EventEmitter {
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);
}
/**
@@ -658,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;
}
@@ -756,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) {
@@ -774,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
*
@@ -814,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;
+45 -52
View File
@@ -20,49 +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,
EVENT_VISIBILITY_CHANGE_TYPE,
} 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 {
@@ -209,7 +182,29 @@ export interface IMessageVisibilityHidden {
// A singleton implementing `IMessageVisibilityVisible`.
const MESSAGE_VISIBLE: IMessageVisibilityVisible = Object.freeze({ visible: true });
export class MatrixEvent extends EventEmitter {
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;
@@ -292,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
@@ -343,7 +338,7 @@ export class MatrixEvent extends EventEmitter {
this.txnId = event.txn_id || null;
this.localTimestamp = Date.now() - (this.getAge() ?? 0);
this.reEmitter = new ReEmitter(this);
this.reEmitter = new TypedReEmitter(this);
}
/**
@@ -512,8 +507,7 @@ export class MatrixEvent extends EventEmitter {
if (relatesTo?.rel_type === RelationType.Thread) {
return relatesTo.event_id;
} else {
return this.threadId
|| this.getThread()?.id;
return this.getThread()?.id || this.threadId;
}
}
@@ -537,10 +531,6 @@ export class MatrixEvent extends EventEmitter {
return !!threadDetails || (this.getThread()?.id === this.getId());
}
public get parentEventId(): string {
return this.replyEventId || this.relationEventId;
}
public get replyEventId(): string {
// We're prefer ev.getContent() over ev.getWireContent() to make sure
// we grab the latest edit with potentially new relations. But we also
@@ -876,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;
@@ -1035,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 = {};
@@ -1073,7 +1063,7 @@ export class MatrixEvent extends EventEmitter {
});
}
if (change) {
this.emit("Event.visibilityChange", this, visible);
this.emit(MatrixEventEvent.VisibilityChange, this, visible);
}
}
}
@@ -1105,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
@@ -1268,7 +1258,7 @@ 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();
@@ -1291,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);
}
/**
@@ -1345,7 +1335,7 @@ export class MatrixEvent extends EventEmitter {
}
if (this._replacingEvent !== newEvent) {
this._replacingEvent = newEvent;
this.emit("Event.replaced", this);
this.emit(MatrixEventEvent.Replaced, this);
this.invalidateExtensibleEvent();
}
}
@@ -1427,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;
@@ -1561,7 +1553,8 @@ 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]);
}
/**
+1
View File
@@ -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";
+4 -3
View File
@@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { Relations } from "./relations";
import { Relations, RelationsEvent, EventHandlerMap } from "./relations";
import { MatrixEvent } from "./event";
import { Listener } from "./typed-event-emitter";
export class RelatedRelations {
private relations: Relations[];
@@ -28,11 +29,11 @@ export class RelatedRelations {
return this.relations.reduce((c, p) => [...c, ...p.getRelations()], []);
}
public on(ev: string, fn: (...params) => void) {
public on<T extends RelationsEvent>(ev: T, fn: Listener<RelationsEvent, EventHandlerMap, T>) {
this.relations.forEach(r => r.on(ev, fn));
}
public off(ev: string, fn: (...params) => void) {
public off<T extends RelationsEvent>(ev: T, fn: Listener<RelationsEvent, EventHandlerMap, T>) {
this.relations.forEach(r => r.off(ev, fn));
}
}
+28 -17
View File
@@ -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, IAggregatedRelation } 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);
};
/**
@@ -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
View File
@@ -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);
}
}
+25 -10
View File
@@ -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);
}
/**
+101 -63
View File
@@ -18,18 +18,17 @@ 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 { TypedReEmitter } from '../ReEmitter';
import {
EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS,
EVENT_VISIBILITY_CHANGE_TYPE,
@@ -38,8 +37,9 @@ import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersio
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
@@ -143,15 +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[] }
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
@@ -287,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;
@@ -297,7 +333,8 @@ export class Room extends EventEmitter {
// 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",
RoomEvent.Timeline,
RoomEvent.TimelineReset,
]);
this.fixUpLegacyTimelineFields();
@@ -513,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;
}
/**
@@ -714,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);
}
}
@@ -1287,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);
@@ -1359,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);
}
}
@@ -1368,10 +1406,10 @@ 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);
@@ -1397,19 +1435,18 @@ export class Room extends EventEmitter {
}
}
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;
}
}
this.emit(ThreadEvent.Update, thread);
}
public createThread(rootEvent: MatrixEvent, events?: MatrixEvent[]): Thread | undefined {
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,
@@ -1421,12 +1458,11 @@ export class Room extends EventEmitter {
this.threads.set(thread.id, thread);
this.reEmitter.reEmit(thread, [
ThreadEvent.Update,
ThreadEvent.Ready,
"Room.timeline",
"Room.timelineReset",
RoomEvent.Timeline,
RoomEvent.TimelineReset,
]);
if (!this.lastThread || this.lastThread.rootEvent.localTimestamp < rootEvent.localTimestamp) {
if (!this.lastThread || this.lastThread.rootEvent?.localTimestamp < rootEvent?.localTimestamp) {
this.lastThread = thread;
}
@@ -1465,7 +1501,7 @@ 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
@@ -1565,8 +1601,7 @@ export class Room extends EventEmitter {
EventTimeline.setEventMetadata(event, this.getLiveTimeline().getState(EventTimeline.FORWARDS), false);
this.txnToEvent[txnId] = event;
const thread = this.findThreadForEvent(event);
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);
@@ -1582,36 +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.addEvent(event, 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);
}
/**
@@ -1667,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++) {
@@ -1683,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.
*
@@ -1697,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;
@@ -1722,7 +1758,9 @@ export class Room extends EventEmitter {
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];
@@ -1731,8 +1769,7 @@ export class Room extends EventEmitter {
}
}
this.emit("Room.localEchoUpdated", localEvent, this,
oldEventId, oldStatus);
this.emit(RoomEvent.LocalEchoUpdated, localEvent, this, oldEventId, oldStatus);
}
/**
@@ -1792,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.
@@ -1815,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 {
@@ -1828,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);
@@ -1968,7 +2006,7 @@ export class Room extends EventEmitter {
});
if (oldName !== this.name) {
this.emit("Room.name", this);
this.emit(RoomEvent.Name, this);
}
}
@@ -2061,7 +2099,7 @@ export class Room extends EventEmitter {
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("Room.receipt", event, this);
this.emit(RoomEvent.Receipt, event, this);
}
/**
@@ -2195,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);
}
/**
@@ -2210,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);
}
}
+109 -63
View File
@@ -14,25 +14,34 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClient } from "../matrix";
import { ReEmitter } from "../ReEmitter";
import { MatrixClient, RoomEvent } from "../matrix";
import { TypedReEmitter } from "../ReEmitter";
import { RelationType } from "../@types/event";
import { IRelationsRequestOpts } from "../@types/requests";
import { MatrixEvent, IThreadBundledRelationship } from "./event";
import { IThreadBundledRelationship, MatrixEvent } from "./event";
import { Direction, EventTimeline } from "./event-timeline";
import { EventTimelineSet } from './event-timeline-set';
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;
@@ -42,15 +51,18 @@ interface IThreadOpts {
/**
* @experimental
*/
export class Thread extends TypedEventEmitter<ThreadEvent> {
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;
@@ -75,13 +87,22 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
timelineSupport: true,
pendingEvents: true,
});
this.reEmitter = new ReEmitter(this);
this.reEmitter = new TypedReEmitter(this);
this.reEmitter.reEmit(this.timelineSet, [
"Room.timeline",
"Room.timelineReset",
RoomEvent.Timeline,
RoomEvent.TimelineReset,
]);
if (Thread.hasServerSideSupport === undefined) {
Thread.serverSupportPromise = this.client.doesServerSupportUnstableFeature("org.matrix.msc3440");
Thread.serverSupportPromise.then((serverSupportsThread) => {
Thread.hasServerSideSupport = serverSupportsThread;
}).catch(() => {
Thread.serverSupportPromise = null;
});
}
// 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) {
@@ -92,18 +113,13 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
}
this.initialiseThread(this.rootEvent);
opts?.initialEvents?.forEach(event => this.addEvent(event));
opts?.initialEvents?.forEach(event => this.addEvent(event, false));
this.room.on("Room.localEchoUpdated", this.onEcho);
this.room.on("Room.timeline", this.onEcho);
this.room.on(RoomEvent.LocalEchoUpdated, this.onEcho);
this.room.on(RoomEvent.Timeline, this.onEcho);
}
public get hasServerSideSupport(): boolean {
return this.client.cachedCapabilities
?.capabilities?.[RelationType.Thread]?.enabled;
}
onEcho = (event: MatrixEvent) => {
private onEcho = (event: MatrixEvent) => {
if (this.timelineSet.eventIdToTimeline(event.getId())) {
this.emit(ThreadEvent.Update, this);
}
@@ -113,26 +129,17 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
return this.room.getLiveTimeline().getState(EventTimeline.FORWARDS);
}
/**
* Add an event to the thread and updates
* the tail/root references if needed
* Will fire "Thread.update"
* @param event The event to add
*/
public async addEvent(event: MatrixEvent, toStartOfTimeline = false): Promise<void> {
// Add all incoming events to the thread's timeline set when there's
// no server support
if (!this.hasServerSideSupport) {
if (this.timelineSet.findEventById(event.getId())) {
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;
}
}
// 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);
if (!this.findEventById(event.getId())) {
this.timelineSet.addEventToTimeline(
event,
this.liveTimeline,
@@ -140,19 +147,38 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
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: boolean): Promise<void> {
if (Thread.hasServerSideSupport === undefined) {
await Thread.serverSupportPromise;
}
// 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 (this.hasServerSideSupport && this.initialEventsFetched) {
if (event.localTimestamp > this.lastReply().localTimestamp && !this.findEventById(event.getId())) {
this.timelineSet.addEventToTimeline(
event,
this.liveTimeline,
false,
false,
this.roomState,
);
if (Thread.hasServerSideSupport && this.initialEventsFetched) {
if (event.localTimestamp > this.lastReply().localTimestamp) {
this.addEventToTimeline(event, false);
}
}
@@ -163,7 +189,7 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
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 (!this.hasServerSideSupport && isThreadReply) {
if (!Thread.hasServerSideSupport && isThreadReply) {
this.replyCount++;
}
@@ -176,7 +202,7 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
// This counting only works when server side support is enabled
// as we started the counting from the value returned in the
// bundled relationship
if (this.hasServerSideSupport) {
if (Thread.hasServerSideSupport) {
this.replyCount++;
}
@@ -188,10 +214,17 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
}
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 (this.hasServerSideSupport && bundledRelationship) {
if (Thread.hasServerSideSupport && bundledRelationship) {
this.replyCount = bundledRelationship.count;
this._currentUserParticipated = bundledRelationship.current_user_participated;
@@ -199,19 +232,28 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
this.setEventMetadata(event);
this.lastEvent = event;
}
if (!bundledRelationship && rootEvent) {
this.addEvent(rootEvent);
}
}
public async fetchInitialEvents(): Promise<boolean> {
try {
await this.fetchEvents();
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 true;
return null;
}
try {
const response = await this.fetchEvents();
this.initialEventsFetched = true;
return response;
} catch (e) {
return false;
return null;
}
}
@@ -281,6 +323,10 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
nextBatch?: string;
prevBatch?: string;
}> {
if (Thread.hasServerSideSupport === undefined) {
await Thread.serverSupportPromise;
}
let {
originalEvent,
events,
@@ -297,13 +343,13 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
// 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 = [originalEvent, ...events];
events = [...events, originalEvent];
}
for (const event of events) {
await this.client.decryptEventIfNeeded(event);
await Promise.all(events.map(event => {
this.setEventMetadata(event);
}
return this.client.decryptEventIfNeeded(event);
}));
const prependEvents = !opts.direction || opts.direction === Direction.Backward;
+53 -11
View File
@@ -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
View File
@@ -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);
}
}
+3 -2
View File
@@ -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];
+6 -3
View File
@@ -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.
+10 -1
View File
@@ -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
View File
@@ -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];
}
+74 -57
View File
@@ -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,16 +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",
"Room.visibilityChange",
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;
@@ -214,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;
}
@@ -229,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,
]);
});
}
@@ -249,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);
}
/**
@@ -310,11 +320,11 @@ export class SyncApi {
EventTimeline.BACKWARDS);
this.processRoomEvents(room, stateEvents, timelineEvents);
await 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);
});
@@ -362,7 +372,7 @@ export class SyncApi {
user.setPresenceEvent(presenceEvent);
client.store.storeUser(user);
}
client.emit("event", presenceEvent);
client.emit(ClientEvent.Event, presenceEvent);
});
}
@@ -388,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;
@@ -445,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)
@@ -840,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
@@ -1073,7 +1083,7 @@ export class SyncApi {
user.setPresenceEvent(presenceEvent);
client.store.storeUser(user);
}
client.emit("event", presenceEvent);
client.emit(ClientEvent.Event, presenceEvent);
});
}
@@ -1096,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;
},
);
@@ -1149,7 +1159,7 @@ export class SyncApi {
}
}
client.emit("toDeviceEvent", toDeviceEvent);
client.emit(ClientEvent.ToDeviceEvent, toDeviceEvent);
},
);
} else {
@@ -1201,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");
});
@@ -1307,7 +1317,7 @@ export class SyncApi {
const [timelineEvents, threadedEvents] = this.client.partitionThreadedEvents(events);
this.processRoomEvents(room, stateEvents, timelineEvents, syncEventData.fromCache);
await this.processThreadEvents(room, threadedEvents);
await this.processThreadEvents(room, threadedEvents, false);
// set summary after processing events,
// because it will trigger a name calculation
@@ -1325,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);
}
@@ -1351,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");
@@ -1375,28 +1385,28 @@ export class SyncApi {
const [timelineEvents, threadedEvents] = this.client.partitionThreadedEvents(events);
this.processRoomEvents(room, stateEvents, timelineEvents);
await 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");
@@ -1551,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);
}
}
}
@@ -1720,14 +1730,18 @@ export class SyncApi {
/**
* @experimental
*/
private processThreadEvents(room: Room, threadedEvents: MatrixEvent[]): Promise<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) {
@@ -1778,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);
}
/**
@@ -1796,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
View File
@@ -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>;
}
+98 -33
View File
@@ -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, false);
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);
@@ -1943,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);
}
@@ -1973,7 +2032,7 @@ export class MatrixCall extends EventEmitter {
this.peerConn.close();
}
if (shouldEmit) {
this.emit(CallEvent.Hangup, this);
this.emit(CallEvent.Hangup);
}
}
@@ -1995,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() {})",
);
@@ -2064,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,
+25 -11
View File
@@ -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;
+9 -3
View File
@@ -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;
+86 -16
View File
@@ -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> {
@@ -65,20 +106,44 @@ export class MediaHandler {
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);
}
}
}
if (reusable) {
@@ -103,6 +168,10 @@ 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;
}
}
/**
@@ -174,6 +243,7 @@ export class MediaHandler {
this.userMediaStreams = [];
this.screensharingStreams = [];
this.localUserMediaStream = undefined;
}
private getUserMediaContraints(audio: boolean, video: boolean): MediaStreamConstraints {
+290 -298
View File
File diff suppressed because it is too large Load Diff