Compare commits

..

116 Commits

Author SHA1 Message Date
RiotRobot f1db4dc668 v16.0.0 2022-03-15 14:17:54 +00:00
RiotRobot 98c1710ac1 Prepare changelog for v16.0.0 2022-03-15 14:17:54 +00:00
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
RiotRobot 78dcae9143 v15.5.2 2022-02-17 11:45:23 +00:00
RiotRobot 0db640a679 Prepare changelog for v15.5.2 2022-02-17 11:45:22 +00:00
David Baker a3ddfd519b Fix synthetic read receipt handling (#2174) (#2183)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-02-17 11:29:28 +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
RiotRobot bf8ee39a3f v15.5.1 2022-02-14 15:14:04 +00:00
RiotRobot cc1d1d76d0 Prepare changelog for v15.5.1 2022-02-14 15:14:04 +00:00
David Baker 30fdd96168 Revert event-mapper optimisations (#2171) (#2172)
Co-authored-by: Germain <germains@element.io>
2022-02-14 13:53:12 +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
RiotRobot d473a2e095 v15.5.1-rc.1 2022-02-08 15:32:28 +00:00
RiotRobot d398ffd6df Prepare changelog for v15.5.1-rc.1 2022-02-08 15:32:27 +00:00
Michael Telatynski ffab55452a Fix issue with rooms not getting marked as unread (#2163) 2022-02-08 12:32:14 +00:00
Germain 9120b1dfd9 Expose room state helper (#2162) 2022-02-08 12:08:43 +00:00
Šimon Brandner 5d4e3183aa Don't store streams that are only used once (#2157)
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-02-08 10:13:58 +00:00
Michael Telatynski 07171a95c4 Fix edge cases around RR calculations (#2160) 2022-02-07 17:49:14 +00:00
Šimon Brandner 12afcd3850 Account for encryption in maySendMessage() (#2159) 2022-02-07 15:32:21 +00:00
Andy Balaam 2d23330b74 Send references to thread root to threads, even out of order (#2156)
Co-authored-by: Germain <germains@element.io>
2022-02-04 15:50:06 +00:00
Germain 7faff66006 Out of sync thread summary in main timeline (#2153) 2022-02-04 11:57:42 +00:00
Michael Telatynski b07457726b Avoid re-doing as much work when processing m.receipt event (#2151) 2022-02-03 11:06:19 +00:00
Germain 6bf8142ff6 Fix initial sync fail when event fetching unsuccessful (#2150) 2022-02-02 17:56:43 +00:00
Christian Paul cf0ccaf93d TypeScript: opts parameter of client.startClient is optional (#2106) 2022-02-02 15:29:58 +00:00
Christian Paul 34a3955b60 MatrixEvent.getRoomId can return undefined (#2036) 2022-02-02 16:22:57 +01:00
Germain 82122872bf Add thread relation in sendEvent if it's missing (#2149) 2022-02-02 14:44:38 +00:00
Germain 9c242a9ce6 Keep a reference to the latest thread created (#2148) 2022-02-01 14:50:42 +00:00
Germain 51f3fac87b Add thread relation to sticker message (#2147) 2022-02-01 10:36:04 +00:00
Germain 66b98844a2 Refactor thread model to be created from the root event (#2142) 2022-02-01 08:58:39 +00:00
David Baker d03db00e4c Yarn upgrade (#2146)
Apologies if this means everyone has to `rm -rf node_modules`: react-ace
decided to turn some paths that were regular files into directories
and it turns out this really confuses yarn: https://github.com/securingsincity/react-ace/issues/1048
2022-01-31 16:45:42 +00:00
RiotRobot cf8d2bf6ef Resetting package fields for development 2022-01-31 14:44:13 +00:00
RiotRobot 913a0b51a6 Merge branch 'master' into develop 2022-01-31 14:44:13 +00:00
RiotRobot ede0f696ee v15.5.0 2022-01-31 14:41:10 +00:00
RiotRobot d05214a169 Prepare changelog for v15.5.0 2022-01-31 14:41:09 +00:00
David Baker 1e93d0b19f Bump node-fetch from 2.6.6 to 2.6.7 (#2140) (#2145)
Bumps [node-fetch](https://github.com/node-fetch/node-fetch) from 2.6.6 to 2.6.7.
- [Release notes](https://github.com/node-fetch/node-fetch/releases)
- [Commits](https://github.com/node-fetch/node-fetch/compare/v2.6.6...v2.6.7)

---
updated-dependencies:
- dependency-name: node-fetch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-31 14:24:05 +00:00
Šimon Brandner 5e125e646c Don't decrypt redacted messages (#2143) 2022-01-31 11:21:12 +00:00
dependabot[bot] 1643201363 Bump node-fetch from 2.6.6 to 2.6.7 (#2140)
Bumps [node-fetch](https://github.com/node-fetch/node-fetch) from 2.6.6 to 2.6.7.
- [Release notes](https://github.com/node-fetch/node-fetch/releases)
- [Commits](https://github.com/node-fetch/node-fetch/compare/v2.6.6...v2.6.7)

---
updated-dependencies:
- dependency-name: node-fetch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-27 21:59:03 +00:00
dependabot[bot] 8e99e084a8 Bump cached-path-relative from 1.0.2 to 1.1.0 (#2138)
Bumps [cached-path-relative](https://github.com/ashaffer/cached-path-relative) from 1.0.2 to 1.1.0.
- [Release notes](https://github.com/ashaffer/cached-path-relative/releases)
- [Commits](https://github.com/ashaffer/cached-path-relative/commits)

---
updated-dependencies:
- dependency-name: cached-path-relative
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-27 14:52:23 -07:00
Germain 67d362fdd9 Implement local relation types filter (#2139) 2022-01-27 17:47:05 +00:00
RiotRobot e66430ada6 v15.5.0-rc.1 2022-01-26 16:56:34 +00:00
RiotRobot 52ca60142a Prepare changelog for v15.5.0-rc.1 2022-01-26 16:56:33 +00:00
Michael Telatynski 7d2a5afa6d Fix http-api butchering idServer requests (#2134) 2022-01-26 15:43:24 +00:00
Germain 02ba233644 Improved threads reliability with/without server side support (#2132) 2022-01-26 13:54:00 +00:00
Michael Telatynski 50493a3330 Improve typing for Room.timeline data parameter (#2131) 2022-01-26 13:24:10 +00:00
Germain 8434f29c54 Make threads reply chain filter out local event ID (#2129) 2022-01-25 10:51:12 +00:00
Michael Telatynski a34426a7f6 Improve signature of MatrixClient::isUsernameAvailable to not rely on throwing (#2130) 2022-01-25 10:45:39 +00:00
Germain 033693283d Fix local echo for reactions in threads (#2128) 2022-01-24 17:53:05 +00:00
Germain fab7b7f26a Add default IContent value in makeContentExtensible (#2126) 2022-01-24 09:28:58 +00:00
David Baker 230e3b4ace Add eslint camelcase ignores & remove unnecessary casts (#2110)
Presumbaly casts left over from typescript migration.
2022-01-19 18:19:08 +00:00
Andy Balaam a50a627300 Support m.asset in m.location event content (#2109) 2022-01-19 09:08:41 +00:00
Germain 80930f6690 Extend IEventRelation to define m.in_reply_to (#2108)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-01-19 08:57:32 +00:00
Michael Telatynski 652b3a9208 Revert "Remove getCapabilities call for guest users (#2100)" (#2107) 2022-01-18 09:55:54 +00:00
Travis Ralston 0a8b825702 Move polls to events-sdk, and invent a RelatedRelations type (#2102)
* Move polls handling to events-sdk & invent a multi-relation handler

Polls are now fully handled by the events-sdk instead of by the js-sdk.

The multi-relation handler (RelatedRelations) is primarily meant to handle unstable & stable support in polls when it becomes stable.

* update events-sdk for polls

* Update events-sdk for polls bugfix

* Update events-sdk for maintenance
2022-01-17 10:06:42 -07:00
RiotRobot f4e2a38f4b Resetting package fields for development 2022-01-17 14:10:20 +00:00
RiotRobot b3474531c9 Merge branch 'master' into develop 2022-01-17 14:10:20 +00:00
RiotRobot f0597e0124 v15.4.0 2022-01-17 14:06:06 +00:00
RiotRobot 2fdc91bd04 Prepare changelog for v15.4.0 2022-01-17 14:06:05 +00:00
David Baker 16ca09eed8 Don't remove streams that still have tracks (#2104)
If a renogotiation ends up with one track being removed, we removed
the whole stream, which would cause us to lose, for example, audio
rather than just video.
2022-01-14 09:43:50 +00:00
Travis Ralston 129fb7f10f Keep unknown fields when adding extensible formats (#2105)
* Keep unknown fields when adding extensible formats

This also properly handles `m.new_content` to also be extensible.

* appease the linter
2022-01-13 20:17:46 +00:00
Travis Ralston 016e24472a Send extensible events structure and support on-demand parsing (#2091)
* Parse extensible events on demand

* Decorate messages with MSC1767 when appropriate

We do this automatically to force a pool of messages in the wild we can use for testing.

* Include the SDK

* Appease linter and tests

* Change property name to appease linter

* Update SDK
2022-01-13 09:56:11 -07:00
David Teller 4acb98c496 [Fix] Nullcheck redactedEvent (#2103)
We should only check whether a redacted event is a visibility event *if the redacted event is not null*.
2022-01-12 13:27:03 +01:00
David Teller 96d1b30012 MSC3531: Hiding messages during moderation (#2041) 2022-01-12 11:27:33 +01:00
David Baker 6fc586598a Yarn upgrade (#2101) 2022-01-11 17:17:01 +00:00
Michael Telatynski 2d9c938765 Support cancelling events whilst they are in status = ENCRYPTING (#2095) 2022-01-11 15:03:33 +00:00
74 changed files with 5473 additions and 2890 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
+144
View File
@@ -1,3 +1,147 @@
Changes in [16.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.0) (2022-03-15)
==================================================================================================
## 🚨 BREAKING CHANGES
* Improve typing around event emitter handlers ([\#2180](https://github.com/matrix-org/matrix-js-sdk/pull/2180)).
## ✨ Features
* Fix defer not supporting resolving with a Promise<T> ([\#2216](https://github.com/matrix-org/matrix-js-sdk/pull/2216)).
* add LocationAssetType enum ([\#2214](https://github.com/matrix-org/matrix-js-sdk/pull/2214)).
* Support for mid-call devices changes ([\#2154](https://github.com/matrix-org/matrix-js-sdk/pull/2154)). Contributed by @SimonBrandner.
* Add new room state emit RoomStateEvent.Update for lower-frequency hits ([\#2192](https://github.com/matrix-org/matrix-js-sdk/pull/2192)).
## 🐛 Bug Fixes
* Fix wrong event_id being sent for m.in_reply_to of threads ([\#2213](https://github.com/matrix-org/matrix-js-sdk/pull/2213)).
* Fix wrongly asserting that PushRule::conditions is non-null ([\#2217](https://github.com/matrix-org/matrix-js-sdk/pull/2217)).
* Make createThread more resilient when missing rootEvent ([\#2207](https://github.com/matrix-org/matrix-js-sdk/pull/2207)). Fixes vector-im/element-web#21130.
* Fix bug with the /hierarchy API sending invalid requests ([\#2201](https://github.com/matrix-org/matrix-js-sdk/pull/2201)). Fixes vector-im/element-web#21170.
* fix relation sender filter ([\#2196](https://github.com/matrix-org/matrix-js-sdk/pull/2196)). Fixes vector-im/element-web#20877.
* Fix bug with one-way audio after a transfer ([\#2193](https://github.com/matrix-org/matrix-js-sdk/pull/2193)).
Changes in [16.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.0-rc.1) (2022-03-08)
============================================================================================================
## 🚨 BREAKING CHANGES
* Improve typing around event emitter handlers ([\#2180](https://github.com/matrix-org/matrix-js-sdk/pull/2180)).
## ✨ Features
* Fix defer not supporting resolving with a Promise<T> ([\#2216](https://github.com/matrix-org/matrix-js-sdk/pull/2216)).
* add LocationAssetType enum ([\#2214](https://github.com/matrix-org/matrix-js-sdk/pull/2214)).
* Support for mid-call devices changes ([\#2154](https://github.com/matrix-org/matrix-js-sdk/pull/2154)). Contributed by @SimonBrandner.
* Add new room state emit RoomStateEvent.Update for lower-frequency hits ([\#2192](https://github.com/matrix-org/matrix-js-sdk/pull/2192)).
## 🐛 Bug Fixes
* Fix wrong event_id being sent for m.in_reply_to of threads ([\#2213](https://github.com/matrix-org/matrix-js-sdk/pull/2213)).
* Fix wrongly asserting that PushRule::conditions is non-null ([\#2217](https://github.com/matrix-org/matrix-js-sdk/pull/2217)).
* Make createThread more resilient when missing rootEvent ([\#2207](https://github.com/matrix-org/matrix-js-sdk/pull/2207)). Fixes vector-im/element-web#21130.
* Fix bug with the /hierarchy API sending invalid requests ([\#2201](https://github.com/matrix-org/matrix-js-sdk/pull/2201)). Fixes vector-im/element-web#21170.
* fix relation sender filter ([\#2196](https://github.com/matrix-org/matrix-js-sdk/pull/2196)). Fixes vector-im/element-web#20877.
* Fix bug with one-way audio after a transfer ([\#2193](https://github.com/matrix-org/matrix-js-sdk/pull/2193)).
Changes in [15.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.6.0) (2022-02-28)
==================================================================================================
## ✨ Features
* Return send event response from MSC3089Branch.createNewVersion() ([\#2186](https://github.com/matrix-org/matrix-js-sdk/pull/2186)).
* Add functions to support refresh tokens ([\#2178](https://github.com/matrix-org/matrix-js-sdk/pull/2178)).
## 🐛 Bug Fixes
* [Release] Fix bug with the /hierarchy API sending invalid requests ([\#2202](https://github.com/matrix-org/matrix-js-sdk/pull/2202)).
* Fix bug where calls could break if rejected from somewhere else ([\#2189](https://github.com/matrix-org/matrix-js-sdk/pull/2189)).
* Fix camera stuck on after call transfer ([\#2188](https://github.com/matrix-org/matrix-js-sdk/pull/2188)).
* Fix synthetic read receipt handling ([\#2174](https://github.com/matrix-org/matrix-js-sdk/pull/2174)). Fixes vector-im/element-web#21016.
* Revert "Sign backup with cross-signing key when we reset it." ([\#2175](https://github.com/matrix-org/matrix-js-sdk/pull/2175)).
* Sign backup with cross-signing key when we reset it. ([\#2170](https://github.com/matrix-org/matrix-js-sdk/pull/2170)).
* Fix error in uploadContent() when file is empty under Node.js ([\#2155](https://github.com/matrix-org/matrix-js-sdk/pull/2155)).
* Check the backup info against the stored private key when determining trust. ([\#2167](https://github.com/matrix-org/matrix-js-sdk/pull/2167)).
* Back up keys before logging out ([\#2158](https://github.com/matrix-org/matrix-js-sdk/pull/2158)). Fixes vector-im/element-web#13151.
Changes in [15.6.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.6.0-rc.1) (2022-02-22)
============================================================================================================
## ✨ Features
* Return send event response from MSC3089Branch.createNewVersion() ([\#2186](https://github.com/matrix-org/matrix-js-sdk/pull/2186)).
* Add functions to support refresh tokens ([\#2178](https://github.com/matrix-org/matrix-js-sdk/pull/2178)).
## 🐛 Bug Fixes
* Fix bug where calls could break if rejected from somewhere else ([\#2189](https://github.com/matrix-org/matrix-js-sdk/pull/2189)).
* Fix camera stuck on after call transfer ([\#2188](https://github.com/matrix-org/matrix-js-sdk/pull/2188)).
* Fix synthetic read receipt handling ([\#2174](https://github.com/matrix-org/matrix-js-sdk/pull/2174)). Fixes vector-im/element-web#21016.
* Revert "Sign backup with cross-signing key when we reset it." ([\#2175](https://github.com/matrix-org/matrix-js-sdk/pull/2175)).
* Sign backup with cross-signing key when we reset it. ([\#2170](https://github.com/matrix-org/matrix-js-sdk/pull/2170)).
* Fix error in uploadContent() when file is empty under Node.js ([\#2155](https://github.com/matrix-org/matrix-js-sdk/pull/2155)).
* Check the backup info against the stored private key when determining trust. ([\#2167](https://github.com/matrix-org/matrix-js-sdk/pull/2167)).
* Back up keys before logging out ([\#2158](https://github.com/matrix-org/matrix-js-sdk/pull/2158)). Fixes vector-im/element-web#13151.
Changes in [15.5.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.2) (2022-02-17)
==================================================================================================
## 🐛 Bug Fixes
* Fix synthetic read receipt handling
Changes in [15.5.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.1) (2022-02-14)
==================================================================================================
## 🐛 Bug Fixes
* Fix issue with rooms not getting marked as unread ([\#2163](https://github.com/matrix-org/matrix-js-sdk/pull/2163)). Fixes vector-im/element-web#20971.
* Don't store streams that are only used once ([\#2157](https://github.com/matrix-org/matrix-js-sdk/pull/2157)). Fixes vector-im/element-web#20932. Contributed by @SimonBrandner.
* Fix edge cases around RR calculations ([\#2160](https://github.com/matrix-org/matrix-js-sdk/pull/2160)). Fixes vector-im/element-web#20922.
* Account for encryption in `maySendMessage()` ([\#2159](https://github.com/matrix-org/matrix-js-sdk/pull/2159)). Contributed by @SimonBrandner.
* Send references to thread root to threads, even out of order ([\#2156](https://github.com/matrix-org/matrix-js-sdk/pull/2156)).
* Fix initial sync fail when event fetching unsuccessful ([\#2150](https://github.com/matrix-org/matrix-js-sdk/pull/2150)). Fixes vector-im/element-web#20862.
* Don't decrypt redacted messages ([\#2143](https://github.com/matrix-org/matrix-js-sdk/pull/2143)). Contributed by @SimonBrandner.
Changes in [15.5.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.1-rc.1) (2022-02-08)
============================================================================================================
## 🐛 Bug Fixes
* Fix issue with rooms not getting marked as unread ([\#2163](https://github.com/matrix-org/matrix-js-sdk/pull/2163)). Fixes vector-im/element-web#20971.
* Don't store streams that are only used once ([\#2157](https://github.com/matrix-org/matrix-js-sdk/pull/2157)). Fixes vector-im/element-web#20932. Contributed by @SimonBrandner.
* Fix edge cases around RR calculations ([\#2160](https://github.com/matrix-org/matrix-js-sdk/pull/2160)). Fixes vector-im/element-web#20922.
* Account for encryption in `maySendMessage()` ([\#2159](https://github.com/matrix-org/matrix-js-sdk/pull/2159)). Contributed by @SimonBrandner.
* Send references to thread root to threads, even out of order ([\#2156](https://github.com/matrix-org/matrix-js-sdk/pull/2156)).
* Fix initial sync fail when event fetching unsuccessful ([\#2150](https://github.com/matrix-org/matrix-js-sdk/pull/2150)). Fixes vector-im/element-web#20862.
* Don't decrypt redacted messages ([\#2143](https://github.com/matrix-org/matrix-js-sdk/pull/2143)). Contributed by @SimonBrandner.
Changes in [15.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.0) (2022-01-31)
==================================================================================================
## ✨ Features
* Support m.asset in m.location event content ([\#2109](https://github.com/matrix-org/matrix-js-sdk/pull/2109)).
* Send extensible events structure and support on-demand parsing ([\#2091](https://github.com/matrix-org/matrix-js-sdk/pull/2091)).
* Support cancelling events whilst they are in status = ENCRYPTING ([\#2095](https://github.com/matrix-org/matrix-js-sdk/pull/2095)).
## 🐛 Bug Fixes
* Fix http-api butchering idServer requests ([\#2134](https://github.com/matrix-org/matrix-js-sdk/pull/2134)). Fixes vector-im/element-web#20680.
* Don't remove streams that still have tracks ([\#2104](https://github.com/matrix-org/matrix-js-sdk/pull/2104)).
Changes in [15.5.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.0-rc.1) (2022-01-26)
============================================================================================================
## ✨ Features
* Support m.asset in m.location event content ([\#2109](https://github.com/matrix-org/matrix-js-sdk/pull/2109)).
* Send extensible events structure and support on-demand parsing ([\#2091](https://github.com/matrix-org/matrix-js-sdk/pull/2091)).
* Support cancelling events whilst they are in status = ENCRYPTING ([\#2095](https://github.com/matrix-org/matrix-js-sdk/pull/2095)).
## 🐛 Bug Fixes
* Fix http-api butchering idServer requests ([\#2134](https://github.com/matrix-org/matrix-js-sdk/pull/2134)). Fixes vector-im/element-web#20680.
* Don't remove streams that still have tracks ([\#2104](https://github.com/matrix-org/matrix-js-sdk/pull/2104)).
Changes in [15.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.4.0) (2022-01-17)
==================================================================================================
## ✨ Features
* Don't consider alt_aliases when calculating room name ([\#2094](https://github.com/matrix-org/matrix-js-sdk/pull/2094)). Fixes vector-im/element-web#13887.
* Load room history if necessary when searching for MSC3089 getFileEvent() ([\#2066](https://github.com/matrix-org/matrix-js-sdk/pull/2066)).
* Add support for MSC3030 `/timestamp_to_event` ([\#2072](https://github.com/matrix-org/matrix-js-sdk/pull/2072)).
## 🐛 Bug Fixes
* Stop encrypting redactions as it isn't spec compliant ([\#2098](https://github.com/matrix-org/matrix-js-sdk/pull/2098)). Fixes vector-im/element-web#20460.
* Fix more function typings relating to key backup ([\#2086](https://github.com/matrix-org/matrix-js-sdk/pull/2086)).
* Fix timeline search in MSC3089 getFileEvent() ([\#2085](https://github.com/matrix-org/matrix-js-sdk/pull/2085)).
* Set a `deviceId` for VoIP example and use `const`/`let` ([\#2090](https://github.com/matrix-org/matrix-js-sdk/pull/2090)). Fixes #2083. Contributed by @SimonBrandner.
* Fix incorrect TS return type for secret storage and key backup functions ([\#2082](https://github.com/matrix-org/matrix-js-sdk/pull/2082)).
Changes in [15.4.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.4.0-rc.1) (2022-01-11)
============================================================================================================
+5 -3
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "15.4.0-rc.1",
"version": "16.0.0",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepublishOnly": "yarn build",
@@ -56,6 +56,7 @@
"bs58": "^4.0.1",
"content-type": "^1.0.4",
"loglevel": "^1.7.1",
"matrix-events-sdk": "^0.0.1-beta.7",
"p-retry": "^4.5.0",
"qs": "^6.9.6",
"request": "^2.88.2",
@@ -88,7 +89,7 @@
"better-docs": "^2.4.0-beta.9",
"browserify": "^17.0.0",
"docdash": "^1.2.0",
"eslint": "7.18.0",
"eslint": "8.9.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-matrix-org": "^0.4.0",
@@ -112,7 +113,8 @@
"<rootDir>/src/**/*.{js,ts}"
],
"coverageReporters": [
"text"
"text",
"json"
]
},
"typings": "./lib/index.d.ts"
+1 -1
View File
@@ -348,7 +348,7 @@ function recvMessage(httpBackend, client, sender, message) {
return testUtils.awaitDecryption(event);
}).then((event) => {
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent()).toEqual({
expect(event.getContent()).toMatchObject({
msgtype: "m.text",
body: "Hello, World",
});
+409
View File
@@ -1,5 +1,6 @@
import * as utils from "../test-utils";
import { CRYPTO_ENABLED } from "../../src/client";
import { MatrixEvent } from "../../src/models/event";
import { Filter, MemoryStore, Room } from "../../src/matrix";
import { TestClient } from "../TestClient";
@@ -392,6 +393,414 @@ describe("MatrixClient", function() {
return prom;
});
});
describe("partitionThreadedEvents", function() {
it("returns empty arrays when given an empty arrays", function() {
const events = [];
const [timeline, threaded] = client.partitionThreadedEvents(events);
expect(timeline).toEqual([]);
expect(threaded).toEqual([]);
});
it("copies pre-thread in-timeline vote events onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true };
const eventMessageInThread = buildEventMessageInThread();
const eventPollResponseReference = buildEventPollResponseReference();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const events = [
eventMessageInThread,
eventPollResponseReference,
eventPollStartThreadRoot,
];
// Vote has no threadId yet
expect(eventPollResponseReference.threadId).toBeFalsy();
const [timeline, threaded] = client.partitionThreadedEvents(events);
expect(timeline).toEqual([
// The message that was sent in a thread is missing
eventPollResponseReference,
eventPollStartThreadRoot,
]);
// The vote event has been copied into the thread
const eventRefWithThreadId = withThreadId(
eventPollResponseReference, eventPollStartThreadRoot.getId());
expect(eventRefWithThreadId.threadId).toBeTruthy();
expect(threaded).toEqual([
eventMessageInThread,
eventRefWithThreadId,
// Thread does not see thread root
]);
});
it("copies pre-thread in-timeline reactions onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true };
const eventMessageInThread = buildEventMessageInThread();
const eventReaction = buildEventReaction();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const events = [
eventMessageInThread,
eventReaction,
eventPollStartThreadRoot,
];
const [timeline, threaded] = client.partitionThreadedEvents(events);
expect(timeline).toEqual([
eventReaction,
eventPollStartThreadRoot,
]);
expect(threaded).toEqual([
eventMessageInThread,
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
]);
});
it("copies post-thread in-timeline vote events onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true };
const eventPollResponseReference = buildEventPollResponseReference();
const eventMessageInThread = buildEventMessageInThread();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const events = [
eventPollResponseReference,
eventMessageInThread,
eventPollStartThreadRoot,
];
const [timeline, threaded] = client.partitionThreadedEvents(events);
expect(timeline).toEqual([
eventPollResponseReference,
eventPollStartThreadRoot,
]);
expect(threaded).toEqual([
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
eventMessageInThread,
]);
});
it("copies post-thread in-timeline reactions onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true };
const eventReaction = buildEventReaction();
const eventMessageInThread = buildEventMessageInThread();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const events = [
eventReaction,
eventMessageInThread,
eventPollStartThreadRoot,
];
const [timeline, threaded] = client.partitionThreadedEvents(events);
expect(timeline).toEqual([
eventReaction,
eventPollStartThreadRoot,
]);
expect(threaded).toEqual([
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
eventMessageInThread,
]);
});
it("sends room state events to the main timeline only", function() {
client.clientOpts = { experimentalThreadSupport: true };
// This is based on recording the events in a real room:
const eventMessageInThread = buildEventMessageInThread();
const eventPollResponseReference = buildEventPollResponseReference();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const eventRoomName = buildEventRoomName();
const eventEncryption = buildEventEncryption();
const eventGuestAccess = buildEventGuestAccess();
const eventHistoryVisibility = buildEventHistoryVisibility();
const eventJoinRules = buildEventJoinRules();
const eventPowerLevels = buildEventPowerLevels();
const eventMember = buildEventMember();
const eventCreate = buildEventCreate();
const events = [
eventMessageInThread,
eventPollResponseReference,
eventPollStartThreadRoot,
eventRoomName,
eventEncryption,
eventGuestAccess,
eventHistoryVisibility,
eventJoinRules,
eventPowerLevels,
eventMember,
eventCreate,
];
const [timeline, threaded] = client.partitionThreadedEvents(events);
expect(timeline).toEqual([
// The message that was sent in a thread is missing
eventPollResponseReference,
eventPollStartThreadRoot,
eventRoomName,
eventEncryption,
eventGuestAccess,
eventHistoryVisibility,
eventJoinRules,
eventPowerLevels,
eventMember,
eventCreate,
]);
// Thread should contain only stuff that happened in the thread -
// no thread root, and no room state events
expect(threaded).toEqual([
eventMessageInThread,
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
]);
});
});
});
function withThreadId(event, newThreadId) {
const ret = event.toSnapshot();
ret.setThreadId(newThreadId);
return ret;
}
const buildEventMessageInThread = () => new MatrixEvent({
"age": 80098509,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
"device_id": "XISFUZSKHH",
"m.relates_to": {
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
"m.in_reply_to": {
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
},
"rel_type": "io.element.thread",
},
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
},
"event_id": "$W4chKIGYowtBblVLkRimeIg8TcdjETnxhDPGfi6NpDg",
"origin_server_ts": 1643815466378,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"type": "m.room.encrypted",
"unsigned": { "age": 80098509 },
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventPollResponseReference = () => new MatrixEvent({
"age": 80098509,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
"device_id": "XISFUZSKHH",
"m.relates_to": {
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
"rel_type": "m.reference",
},
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
},
"event_id": "$91JvpezvsF0cKgav3g8W-uEVS4WkDHgxbJZvL3uMR1g",
"origin_server_ts": 1643815458650,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"type": "m.room.encrypted",
"unsigned": { "age": 80106237 },
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventReaction = () => new MatrixEvent({
"content": {
"m.relates_to": {
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
"key": "🤗",
"rel_type": "m.annotation",
},
},
"origin_server_ts": 1643977249238,
"sender": "@andybalaam-test1:matrix.org",
"type": "m.reaction",
"unsigned": {
"age": 22598,
"transaction_id": "m1643977249073.16",
},
"event_id": "$86B2b-x3LgE4DlV4y24b7UHnt72LIA3rzjvMysTtAfA",
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
});
const buildEventPollStartThreadRoot = () => new MatrixEvent({
"age": 80108647,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
"device_id": "XISFUZSKHH",
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
},
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
"origin_server_ts": 1643815456240,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"type": "m.room.encrypted",
"unsigned": { "age": 80108647 },
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventRoomName = () => new MatrixEvent({
"age": 80123249,
"content": {
"name": "1 poll, 1 vote, 1 thread",
},
"event_id": "$QAdyNJtKnl1j7or2yMycbOCvb6bCgvHs5lg3ZMd5xWk",
"origin_server_ts": 1643815441638,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"state_key": "",
"type": "m.room.name",
"unsigned": { "age": 80123249 },
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventEncryption = () => new MatrixEvent({
"age": 80123383,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
},
"event_id": "$1hGykogKQkXbHw8bVuyE3BjHnFBEJBcUWnakd0ck2K0",
"origin_server_ts": 1643815441504,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"state_key": "",
"type": "m.room.encryption",
"unsigned": { "age": 80123383 },
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventGuestAccess = () => new MatrixEvent({
"age": 80123473,
"content": {
"guest_access": "can_join",
},
"event_id": "$4_2n-H6K9-0nPbnjjtIue2SU44tGJsnuTmi6UuSrh-U",
"origin_server_ts": 1643815441414,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"state_key": "",
"type": "m.room.guest_access",
"unsigned": { "age": 80123473 },
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventHistoryVisibility = () => new MatrixEvent({
"age": 80123556,
"content": {
"history_visibility": "shared",
},
"event_id": "$W6kp44CTnvciOiHSPyhp8dh4n2v1_9kclUPddeaQj0E",
"origin_server_ts": 1643815441331,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"state_key": "",
"type": "m.room.history_visibility",
"unsigned": { "age": 80123556 },
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventJoinRules = () => new MatrixEvent({
"age": 80123696,
"content": {
"join_rule": "invite",
},
"event_id": "$6JDDeDp7fEc0F6YnTWMruNcKWFltR3e9wk7wWDDJrAU",
"origin_server_ts": 1643815441191,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"state_key": "",
"type": "m.room.join_rules",
"unsigned": { "age": 80123696 },
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventPowerLevels = () => new MatrixEvent({
"age": 80124105,
"content": {
"ban": 50,
"events": {
"m.room.avatar": 50,
"m.room.canonical_alias": 50,
"m.room.encryption": 100,
"m.room.history_visibility": 100,
"m.room.name": 50,
"m.room.power_levels": 100,
"m.room.server_acl": 100,
"m.room.tombstone": 100,
},
"events_default": 0,
"historical": 100,
"invite": 0,
"kick": 50,
"redact": 50,
"state_default": 50,
"users": {
"@andybalaam-test1:matrix.org": 100,
},
"users_default": 0,
},
"event_id": "$XZY2YgQhXskpc7gmJJG3S0VmS9_QjjCUVeeFTfgfC2E",
"origin_server_ts": 1643815440782,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"state_key": "",
"type": "m.room.power_levels",
"unsigned": { "age": 80124105 },
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventMember = () => new MatrixEvent({
"age": 80125279,
"content": {
"avatar_url": "mxc://matrix.org/aNtbVcFfwotudypZcHsIcPOc",
"displayname": "andybalaam-test1",
"membership": "join",
},
"event_id": "$Ex5eVmMs_ti784mo8bgddynbwLvy6231lCycJr7Cl9M",
"origin_server_ts": 1643815439608,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"state_key": "@andybalaam-test1:matrix.org",
"type": "m.room.member",
"unsigned": { "age": 80125279 },
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventCreate = () => new MatrixEvent({
"age": 80126105,
"content": {
"creator": "@andybalaam-test1:matrix.org",
"room_version": "6",
},
"event_id": "$e7j2Gt37k5NPwB6lz2N3V9lO5pUdNK8Ai7i2FPEK-oI",
"origin_server_ts": 1643815438782,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"state_key": "",
"type": "m.room.create",
"unsigned": { "age": 80126105 },
"user_id": "@andybalaam-test1:matrix.org",
});
function assertObjectContains(obj, expected) {
+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();
}
+3
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) {
@@ -365,3 +366,5 @@ export function setHttpResponses(
.respond(200, response.data);
});
}
export const emitPromise = (e, k) => new Promise(r => e.once(k, r));
+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);
});
});
});
+56
View File
@@ -0,0 +1,56 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { makeLocationContent } from "../../src/content-helpers";
import {
ASSET_NODE_TYPE,
LocationAssetType,
LOCATION_EVENT_TYPE,
TIMESTAMP_NODE_TYPE,
} from "../../src/@types/location";
import { TEXT_NODE_TYPE } from "../../src/@types/extensible_events";
describe("Location", function() {
it("should create a valid location with defaults", function() {
const loc = makeLocationContent("txt", "geo:foo", 134235435);
expect(loc.body).toEqual("txt");
expect(loc.msgtype).toEqual("m.location");
expect(loc.geo_uri).toEqual("geo:foo");
expect(LOCATION_EVENT_TYPE.findIn(loc)).toEqual({
uri: "geo:foo",
description: undefined,
});
expect(ASSET_NODE_TYPE.findIn(loc)).toEqual({ type: LocationAssetType.Self });
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual("txt");
expect(TIMESTAMP_NODE_TYPE.findIn(loc)).toEqual(134235435);
});
it("should create a valid location with explicit properties", function() {
const loc = makeLocationContent(
"txxt", "geo:bar", 134235436, "desc", LocationAssetType.Pin);
expect(loc.body).toEqual("txxt");
expect(loc.msgtype).toEqual("m.location");
expect(loc.geo_uri).toEqual("geo:bar");
expect(LOCATION_EVENT_TYPE.findIn(loc)).toEqual({
uri: "geo:bar",
description: "desc",
});
expect(ASSET_NODE_TYPE.findIn(loc)).toEqual({ type: LocationAssetType.Pin });
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual("txxt");
expect(TIMESTAMP_NODE_TYPE.findIn(loc)).toEqual(134235436);
});
});
@@ -11,8 +11,9 @@ import {
UNSTABLE_MSC3089_TREE_SUBTYPE,
} from "../../src/@types/event";
import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib";
import { MatrixEvent } from "../../src/models/event";
import { EventStatus, MatrixEvent } from "../../src/models/event";
import { Preset } from "../../src/@types/partials";
import * as testUtils from "../test-utils";
jest.useFakeTimers();
@@ -90,11 +91,7 @@ describe("MatrixClient", function() {
return pendingLookup.promise;
}
// >1 pending thing, and they are different, whine.
expect(false).toBe(
true, ">1 pending request. You should probably handle them. " +
"PENDING: " + JSON.stringify(pendingLookup) + " JUST GOT: " +
method + " " + path,
);
expect(false).toBe(true);
}
pendingLookup = {
promise: new Promise(() => {}),
@@ -122,6 +119,7 @@ describe("MatrixClient", function() {
}
if (next.error) {
// eslint-disable-next-line
return Promise.reject({
errcode: next.error.errcode,
httpStatus: next.error.httpStatus,
@@ -132,7 +130,7 @@ describe("MatrixClient", function() {
}
return Promise.resolve(next.data);
}
expect(true).toBe(false, "Expected different request. " + logLine);
expect(true).toBe(false);
return new Promise(() => {});
}
@@ -157,7 +155,7 @@ describe("MatrixClient", function() {
baseUrl: "https://my.home.server",
idBaseUrl: identityServerUrl,
accessToken: "my.access.token",
request: function() {}, // NOP
request: function() {} as any, // NOP
store: store,
scheduler: scheduler,
userId: userId,
@@ -378,10 +376,10 @@ describe("MatrixClient", function() {
];
const filterId = "ehfewf";
store.getFilterIdByName.mockReturnValue(filterId);
const filter = new Filter(0, filterId);
const filter = new Filter("0", filterId);
filter.setDefinition({ "room": { "timeline": { "limit": 8 } } });
store.getFilter.mockReturnValue(filter);
const syncPromise = new Promise((resolve, reject) => {
const syncPromise = new Promise<void>((resolve, reject) => {
client.on("sync", function syncListener(state) {
if (state === "SYNCING") {
expect(httpLookups.length).toEqual(0);
@@ -402,7 +400,7 @@ describe("MatrixClient", function() {
});
it("should return the same sync state as emitted sync events", async function() {
const syncingPromise = new Promise((resolve) => {
const syncingPromise = new Promise<void>((resolve) => {
client.on("sync", function syncListener(state) {
expect(state).toEqual(client.getSyncState());
if (state === "SYNCING") {
@@ -422,7 +420,7 @@ describe("MatrixClient", function() {
it("should use an existing filter if id is present in localStorage", function() {
});
it("should handle localStorage filterId missing from the server", function(done) {
function getFilterName(userId, suffix) {
function getFilterName(userId, suffix?: string) {
// scope this on the user ID because people may login on many accounts
// and they all need to be stored!
return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : "");
@@ -500,7 +498,7 @@ describe("MatrixClient", function() {
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(1);
expect(client.retryImmediately()).toBe(
true, "retryImmediately returned false",
true,
);
jest.advanceTimersByTime(1);
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
@@ -583,33 +581,33 @@ describe("MatrixClient", function() {
});
it("should transition ERROR -> CATCHUP after /sync if prev failed",
function(done) {
const expectedStates = [];
acceptKeepalives = false;
httpLookups = [];
httpLookups.push(CAPABILITIES_RESPONSE);
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH,
error: { errcode: "KEEPALIVE_FAIL" },
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, data: {},
});
httpLookups.push({
method: "GET", path: "/sync", data: SYNC_DATA,
});
function(done) {
const expectedStates = [];
acceptKeepalives = false;
httpLookups = [];
httpLookups.push(CAPABILITIES_RESPONSE);
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH,
error: { errcode: "KEEPALIVE_FAIL" },
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, data: {},
});
httpLookups.push({
method: "GET", path: "/sync", data: SYNC_DATA,
});
expectedStates.push(["RECONNECTING", null]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["CATCHUP", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
expectedStates.push(["RECONNECTING", null]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["CATCHUP", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition PREPARED -> SYNCING after /sync", function(done) {
const expectedStates = [];
@@ -639,32 +637,32 @@ describe("MatrixClient", function() {
});
xit("should transition ERROR -> SYNCING after /sync if prev failed",
function(done) {
const expectedStates = [];
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
});
httpLookups.push(SYNC_RESPONSE);
function(done) {
const expectedStates = [];
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
});
httpLookups.push(SYNC_RESPONSE);
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["ERROR", "SYNCING"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["ERROR", "SYNCING"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition SYNCING -> SYNCING on subsequent /sync successes",
function(done) {
const expectedStates = [];
httpLookups.push(SYNC_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
function(done) {
const expectedStates = [];
httpLookups.push(SYNC_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["SYNCING", "SYNCING"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["SYNCING", "SYNCING"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition ERROR -> ERROR if keepalive keeps failing", function(done) {
acceptKeepalives = false;
@@ -713,6 +711,7 @@ describe("MatrixClient", function() {
describe("guest rooms", function() {
it("should only do /sync calls (without filter/pushrules)", function(done) {
httpLookups = []; // no /pushrules or /filterw
httpLookups.push(CAPABILITIES_RESPONSE);
httpLookups.push({
method: "GET",
path: "/sync",
@@ -867,4 +866,118 @@ describe("MatrixClient", function() {
await client.redactEvent(roomId, eventId, txnId, { reason });
});
});
describe("cancelPendingEvent", () => {
const roomId = "!room:server";
const txnId = "m12345";
const mockRoom = {
getMyMembership: () => "join",
updatePendingEvent: (event, status) => event.setStatus(status),
currentState: {
getStateEvents: (eventType, stateKey) => {
if (eventType === EventType.RoomCreate) {
expect(stateKey).toEqual("");
return new MatrixEvent({
content: {
[RoomCreateTypeField]: RoomType.Space,
},
});
} else if (eventType === EventType.RoomEncryption) {
expect(stateKey).toEqual("");
return new MatrixEvent({ content: {} });
} else {
throw new Error("Unexpected event type or state key");
}
},
},
};
let event;
beforeEach(async () => {
event = new MatrixEvent({
event_id: "~" + roomId + ":" + txnId,
user_id: client.credentials.userId,
sender: client.credentials.userId,
room_id: roomId,
origin_server_ts: new Date().getTime(),
});
event.setTxnId(txnId);
client.getRoom = (getRoomId) => {
expect(getRoomId).toEqual(roomId);
return mockRoom;
};
client.crypto = { // mock crypto
encryptEvent: (event, room) => new Promise(() => {}),
};
});
function assertCancelled() {
expect(event.status).toBe(EventStatus.CANCELLED);
expect(client.scheduler.removeEventFromQueue(event)).toBeFalsy();
expect(httpLookups.filter(h => h.path.includes("/send/")).length).toBe(0);
}
it("should cancel an event which is queued", () => {
event.setStatus(EventStatus.QUEUED);
client.scheduler.queueEvent(event);
client.cancelPendingEvent(event);
assertCancelled();
});
it("should cancel an event which is encrypting", async () => {
client.encryptAndSendEvent(null, event);
await testUtils.emitPromise(event, "Event.status");
client.cancelPendingEvent(event);
assertCancelled();
});
it("should cancel an event which is not sent", () => {
event.setStatus(EventStatus.NOT_SENT);
client.cancelPendingEvent(event);
assertCancelled();
});
it("should error when given any other event status", () => {
event.setStatus(EventStatus.SENDING);
expect(() => client.cancelPendingEvent(event)).toThrow("cannot cancel an event with status sending");
expect(event.status).toBe(EventStatus.SENDING);
});
});
describe("threads", () => {
it("partitions root events to room timeline and thread timeline", () => {
const supportsExperimentalThreads = client.supportsExperimentalThreads;
client.supportsExperimentalThreads = () => true;
const rootEvent = new MatrixEvent({
"content": {},
"origin_server_ts": 1,
"room_id": "!room1:matrix.org",
"sender": "@alice:matrix.org",
"type": "m.room.message",
"unsigned": {
"m.relations": {
"io.element.thread": {
"latest_event": {},
"count": 33,
"current_user_participated": false,
},
},
},
"event_id": "$ev1",
"user_id": "@alice:matrix.org",
});
expect(rootEvent.isThreadRoot).toBe(true);
const [room, threads] = client.partitionThreadedEvents([rootEvent]);
expect(room).toHaveLength(1);
expect(threads).toHaveLength(1);
// Restore method
client.supportsExperimentalThreads = supportsExperimentalThreads;
});
});
});
+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, {
+1 -1
View File
@@ -120,7 +120,7 @@ describe("RoomState", function() {
it("should return a single MatrixEvent if a state_key was specified",
function() {
const event = state.getStateEvents("m.room.member", userA);
expect(event.getContent()).toEqual({
expect(event.getContent()).toMatchObject({
membership: "join",
});
});
File diff suppressed because it is too large Load Diff
+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 */
+11
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
@@ -177,6 +178,16 @@ export const UNSTABLE_ELEMENT_FUNCTIONAL_USERS = new UnstableValue(
"io.element.functional_members",
"io.element.functional_members");
/**
* A type of message that affects visibility of a message,
* as per https://github.com/matrix-org/matrix-doc/pull/3531
*
* @experimental
*/
export const EVENT_VISIBILITY_CHANGE_TYPE = new UnstableValue(
"m.visibility",
"org.matrix.msc3531.visibility");
export interface IEncryptedFile {
url: string;
mimetype?: string;
+13
View File
@@ -23,8 +23,15 @@ import { TEXT_NODE_TYPE } from "./extensible_events";
export const LOCATION_EVENT_TYPE = new UnstableValue(
"m.location", "org.matrix.msc3488.location");
export const ASSET_NODE_TYPE = new UnstableValue("m.asset", "org.matrix.msc3488.asset");
export const TIMESTAMP_NODE_TYPE = new UnstableValue("m.ts", "org.matrix.msc3488.ts");
export enum LocationAssetType {
Self = "m.self",
Pin = "m.pin",
}
/* From the spec at:
* https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md
{
@@ -37,6 +44,9 @@ export const TIMESTAMP_NODE_TYPE = new UnstableValue("m.ts", "org.matrix.msc3488
"uri": "geo:51.5008,0.1247;u=35",
"description": "Matthew's whereabouts",
},
"m.asset": {
"type": "m.self"
},
"m.text": "Matthew was at geo:51.5008,0.1247;u=35 as of Sat Nov 13 18:50:58 2021",
"m.ts": 1636829458432,
}
@@ -52,6 +62,9 @@ export interface ILocationContent extends IContent {
uri: string;
description?: string;
};
[ASSET_NODE_TYPE.name]: {
type: LocationAssetType;
};
[TEXT_NODE_TYPE.name]: string;
[TIMESTAMP_NODE_TYPE.name]: number;
}
-68
View File
@@ -1,68 +0,0 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { UnstableValue } from "../NamespacedValue";
import { IContent } from "../models/event";
import { TEXT_NODE_TYPE } from "./extensible_events";
export const POLL_START_EVENT_TYPE = new UnstableValue(
"m.poll.start", "org.matrix.msc3381.poll.start");
export const POLL_RESPONSE_EVENT_TYPE = new UnstableValue(
"m.poll.response", "org.matrix.msc3381.poll.response");
export const POLL_END_EVENT_TYPE = new UnstableValue(
"m.poll.end", "org.matrix.msc3381.poll.end");
export const POLL_KIND_DISCLOSED = new UnstableValue(
"m.poll.disclosed", "org.matrix.msc3381.poll.disclosed");
export const POLL_KIND_UNDISCLOSED = new UnstableValue(
"m.poll.undisclosed", "org.matrix.msc3381.poll.undisclosed");
export interface IPollAnswer extends IContent {
id: string;
[TEXT_NODE_TYPE.name]: string;
}
export interface IPollContent extends IContent {
[POLL_START_EVENT_TYPE.name]: {
kind: string; // disclosed or undisclosed (untypeable for now)
question: {
[TEXT_NODE_TYPE.name]: string;
};
answers: IPollAnswer[];
};
[TEXT_NODE_TYPE.name]: string;
}
export interface IPollResponseContent extends IContent {
[POLL_RESPONSE_EVENT_TYPE.name]: {
answers: string[];
};
"m.relates_to": {
"event_id": string;
"rel_type": string;
};
}
export interface IPollEndContent extends IContent {
[POLL_END_EVENT_TYPE.name]: {};
"m.relates_to": {
"event_id": string;
"rel_type": string;
};
}
+2
View File
@@ -19,6 +19,7 @@ import { IContent, IEvent } from "../models/event";
import { Preset, Visibility } from "./partials";
import { SearchKey } from "./search";
import { IRoomEventFilter } from "../filter";
import { Direction } from "../models/event-timeline";
// allow camelcase as these are things that go onto the wire
/* eslint-disable camelcase */
@@ -144,6 +145,7 @@ export interface IRelationsRequestOpts {
from?: string;
to?: string;
limit?: number;
direction?: Direction;
}
export interface IRelationsResponse {
-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
+430 -162
View File
File diff suppressed because it is too large Load Diff
+14 -34
View File
@@ -1,6 +1,5 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2018 - 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -19,8 +18,13 @@ limitations under the License.
import { MsgType } from "./@types/event";
import { TEXT_NODE_TYPE } from "./@types/extensible_events";
import { ILocationContent, LOCATION_EVENT_TYPE, TIMESTAMP_NODE_TYPE } from "./@types/location";
import { IPollContent, POLL_START_EVENT_TYPE } from "./@types/polls";
import {
ASSET_NODE_TYPE,
ILocationContent,
LocationAssetType,
LOCATION_EVENT_TYPE,
TIMESTAMP_NODE_TYPE,
} from "./@types/location";
/**
* Generates the content for a HTML Message event
@@ -103,35 +107,6 @@ export function makeEmoteMessage(body: string) {
};
}
/**
* Generates the content for a Poll Start event
* @param question the poll question
* @param answers the possible answers
* @param kind whether the poll is disclosed or undisclosed. Allowed values are
* "m.poll.disclosed" or "m.poll.undisclosed", or the unstable equivalents
*/
export function makePollContent(
question: string,
answers: string[],
kind: string,
): IPollContent {
question = question.trim();
answers = answers.map(a => a.trim()).filter(a => !!a);
return {
[TEXT_NODE_TYPE.name]:
`${question}\n${answers.map((a, i) => `${i + 1}. ${a}`).join('\n')}`,
[POLL_START_EVENT_TYPE.name]: {
kind: kind,
question: {
[TEXT_NODE_TYPE.name]: question,
},
answers: answers.map(
(a, i) => ({ id: `${i}-${a}`, [TEXT_NODE_TYPE.name]: a }),
),
},
};
}
/**
* Generates the content for a Location event
* @param text a text for of our location
@@ -139,12 +114,14 @@ export function makePollContent(
* @param ts the timestamp when the location was correct (milliseconds since
* the UNIX epoch)
* @param description the (optional) label for this location on the map
* @param asset_type the (optional) asset type of this location e.g. "m.self"
*/
export function makeLocationContent(
text: string,
uri: string,
ts: number,
description?: string,
assetType?: LocationAssetType,
): ILocationContent {
return {
"body": text,
@@ -154,8 +131,11 @@ export function makeLocationContent(
uri,
description,
},
[TIMESTAMP_NODE_TYPE.name]: ts,
[ASSET_NODE_TYPE.name]: {
type: assetType ?? LocationAssetType.Self,
},
[TEXT_NODE_TYPE.name]: text,
[TIMESTAMP_NODE_TYPE.name]: ts,
// TODO: MSC1767 fallbacks m.image thumbnail
};
}
+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 -63
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;
}
@@ -2728,7 +2776,6 @@ export class Crypto extends EventEmitter {
}
}
/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307
/**
* Encrypt an event according to the configuration of the room.
*
@@ -2739,8 +2786,6 @@ export class Crypto extends EventEmitter {
* @return {Promise?} Promise which resolves when the event has been
* encrypted, or null if nothing was needed
*/
/* eslint-enable valid-jsdoc */
// TODO this return type lies
public async encryptEvent(event: MatrixEvent, room: Room): Promise<void> {
if (!room) {
throw new Error("Cannot send encrypted messages in unknown rooms");
@@ -3048,6 +3093,14 @@ export class Crypto extends EventEmitter {
});
}
private onMembership = (event: MatrixEvent, member: RoomMember, oldMembership?: string) => {
try {
this.onRoomMembership(event, member, oldMembership);
} catch (e) {
logger.error("Error handling membership change:", e);
}
};
private onToDeviceEvent = (event: MatrixEvent): void => {
try {
logger.log(`received to_device ${event.getType()} from: ` +
@@ -3073,7 +3126,7 @@ export class Crypto extends EventEmitter {
event.attemptDecryption(this);
}
// once the event has been decrypted, try again
event.once('Event.decrypted', (ev) => {
event.once(MatrixEventEvent.Decrypted, (ev) => {
this.onToDeviceEvent(ev);
});
}
@@ -3222,15 +3275,15 @@ export class Crypto extends EventEmitter {
reject(new Error("Event status set to CANCELLED."));
}
};
event.once("Event.localEventIdReplaced", eventIdListener);
event.on("Event.status", statusListener);
event.once(MatrixEventEvent.LocalEventIdReplaced, eventIdListener);
event.on(MatrixEventEvent.Status, statusListener);
});
} catch (err) {
logger.error("error while waiting for the verification event to be sent: " + err.message);
return;
} finally {
event.removeListener("Event.localEventIdReplaced", eventIdListener);
event.removeListener("Event.status", statusListener);
event.removeListener(MatrixEventEvent.LocalEventIdReplaced, eventIdListener);
event.removeListener(MatrixEventEvent.Status, statusListener);
}
}
let request = requestsMap.getRequest(event);
@@ -3257,7 +3310,7 @@ export class Crypto extends EventEmitter {
!request.invalid && // check it has enough events to pass the UNSENT stage
!request.observeOnly;
if (shouldEmit) {
this.baseApis.emit("crypto.verification.request", request);
this.baseApis.emit(CryptoEvent.VerificationRequest, request);
}
}
@@ -3558,7 +3611,7 @@ export class Crypto extends EventEmitter {
return;
}
this.emit("crypto.roomKeyRequest", req);
this.emit(CryptoEvent.RoomKeyRequest, req);
}
/**
@@ -3577,7 +3630,7 @@ export class Crypto extends EventEmitter {
// we should probably only notify the app of cancellations we told it
// about, but we don't currently have a record of that, so we just pass
// everything through.
this.emit("crypto.roomKeyRequestCancellation", cancellation);
this.emit(CryptoEvent.RoomKeyRequestCancellation, cancellation);
}
/**
+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"]);
client.reEmitter.reEmit(event, [
MatrixEventEvent.Replaced,
MatrixEventEvent.VisibilityChange,
]);
}
return event;
}
+56 -7
View File
@@ -14,7 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { UNSTABLE_FILTER_RELATION_SENDERS, UNSTABLE_FILTER_RELATION_TYPES } from "./filter";
import { RelationType } from "./@types/event";
import {
UNSTABLE_FILTER_RELATED_BY_REL_TYPES,
UNSTABLE_FILTER_RELATED_BY_SENDERS,
} from "./filter";
import { MatrixEvent } from "./models/event";
/**
@@ -47,6 +51,8 @@ export interface IFilterComponent {
not_senders?: string[];
contains_url?: boolean;
limit?: number;
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]?: string[];
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]?: Array<RelationType | string>;
}
/* eslint-enable camelcase */
@@ -62,7 +68,7 @@ export interface IFilterComponent {
* @param {Object} filterJson the definition of this filter JSON, e.g. { 'contains_url': true }
*/
export class FilterComponent {
constructor(private filterJson: IFilterComponent) {}
constructor(private filterJson: IFilterComponent, public readonly userId?: string) {}
/**
* Checks with the filter component matches the given event
@@ -70,11 +76,25 @@ export class FilterComponent {
* @return {boolean} true if the event matches the filter
*/
public check(event: MatrixEvent): boolean {
const bundledRelationships = event.getUnsigned()?.["m.relations"] || {};
const relations: Array<string | RelationType> = Object.keys(bundledRelationships);
// Relation senders allows in theory a look-up of any senders
// however clients can only know about the current user participation status
// as sending a whole list of participants could be proven problematic in terms
// of performance
// This should be improved when bundled relationships solve that problem
const relationSenders = [];
if (this.userId && bundledRelationships?.[RelationType.Thread]?.current_user_participated) {
relationSenders.push(this.userId);
}
return this.checkFields(
event.getRoomId(),
event.getSender(),
event.getType(),
event.getContent() ? event.getContent().url !== undefined : false,
relations,
relationSenders,
);
}
@@ -90,8 +110,8 @@ export class FilterComponent {
senders: this.filterJson.senders || null,
not_senders: this.filterJson.not_senders || [],
contains_url: this.filterJson.contains_url || null,
[UNSTABLE_FILTER_RELATION_SENDERS.name]: UNSTABLE_FILTER_RELATION_SENDERS.findIn(this.filterJson),
[UNSTABLE_FILTER_RELATION_TYPES.name]: UNSTABLE_FILTER_RELATION_TYPES.findIn(this.filterJson),
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]: UNSTABLE_FILTER_RELATED_BY_SENDERS.findIn(this.filterJson),
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]: UNSTABLE_FILTER_RELATED_BY_REL_TYPES.findIn(this.filterJson),
};
}
@@ -101,9 +121,18 @@ export class FilterComponent {
* @param {String} sender the sender of the event being checked
* @param {String} eventType the type of the event being checked
* @param {boolean} containsUrl whether the event contains a content.url field
* @param {boolean} relationTypes whether has aggregated relation of the given type
* @param {boolean} relationSenders whether one of the relation is sent by the user listed
* @return {boolean} true if the event fields match the filter
*/
private checkFields(roomId: string, sender: string, eventType: string, containsUrl: boolean): boolean {
private checkFields(
roomId: string,
sender: string,
eventType: string,
containsUrl: boolean,
relationTypes: Array<RelationType | string>,
relationSenders: string[],
): boolean {
const literalKeys = {
"rooms": function(v: string): boolean {
return roomId === v;
@@ -136,15 +165,35 @@ export class FilterComponent {
return false;
}
const relationTypesFilter = this.filterJson[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name];
if (relationTypesFilter !== undefined) {
if (!this.arrayMatchesFilter(relationTypesFilter, relationTypes)) {
return false;
}
}
const relationSendersFilter = this.filterJson[UNSTABLE_FILTER_RELATED_BY_SENDERS.name];
if (relationSendersFilter !== undefined) {
if (!this.arrayMatchesFilter(relationSendersFilter, relationSenders)) {
return false;
}
}
return true;
}
private arrayMatchesFilter(filter: any[], values: any[]): boolean {
return values.length > 0 && values.every(value => {
return filter.includes(value);
});
}
/**
* Filters a list of events down to those which match this filter component
* @param {MatrixEvent[]} events Events to be checked against the filter component
* @return {MatrixEvent[]} events which matched the filter component
*/
filter(events: MatrixEvent[]): MatrixEvent[] {
public filter(events: MatrixEvent[]): MatrixEvent[] {
return events.filter(this.check, this);
}
@@ -153,7 +202,7 @@ export class FilterComponent {
* 10 if none is otherwise specified. Cargo-culted from Synapse.
* @return {Number} the limit for this filter component.
*/
limit(): number {
public limit(): number {
return this.filterJson.limit !== undefined ? this.filterJson.limit : 10;
}
}
+8 -8
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 {}
@@ -185,8 +185,8 @@ export class Filter {
}
}
this.roomFilter = new FilterComponent(roomFilterFields);
this.roomTimelineFilter = new FilterComponent(roomFilterJson?.timeline || {});
this.roomFilter = new FilterComponent(roomFilterFields, this.userId);
this.roomTimelineFilter = new FilterComponent(roomFilterJson?.timeline || {}, this.userId);
// don't bother porting this from synapse yet:
// this._room_state_filter =
+36 -11
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,
},
@@ -508,7 +537,7 @@ export class MatrixHttpApi {
if (method === Method.Get) {
opts.qs = params;
} else if (typeof params === "object") {
opts.json = !!params; // XXX: this feels strange
opts.json = params;
}
if (accessToken) {
@@ -586,14 +615,10 @@ export class MatrixHttpApi {
const requestPromise = this.request<T, O>(callback, method, path, queryParams, data, requestOpts);
requestPromise.catch((err: MatrixError) => {
if (err.errcode == 'M_UNKNOWN_TOKEN') {
this.eventEmitter.emit("Session.logged_out", err);
if (err.errcode == 'M_UNKNOWN_TOKEN' && !requestOpts?.inhibitLogoutEmit) {
this.eventEmitter.emit(HttpApiEvent.SessionLoggedOut, err);
} else if (err.errcode == 'M_CONSENT_NOT_GIVEN') {
this.eventEmitter.emit(
"no_consent",
err.message,
err.data.consent_uri,
);
this.eventEmitter.emit(HttpApiEvent.NoConsent, err.message, err.data.consent_uri);
}
});
+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",
}
+36 -20
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;
@@ -53,7 +51,20 @@ export enum DuplicateStrategy {
Replace = "replace",
}
export class EventTimelineSet extends EventEmitter {
export interface IRoomTimelineData {
timeline: EventTimeline;
liveEvent?: boolean;
}
type EmittedEvents = RoomEvent.Timeline | RoomEvent.TimelineReset;
export type EventTimelineSetHandlerMap = {
[RoomEvent.Timeline]:
(event: MatrixEvent, room: Room, toStartOfTimeline: boolean, removed: boolean, data: IRoomTimelineData) => void;
[RoomEvent.TimelineReset]: (room: Room, eventTimelineSet: EventTimelineSet, resetAllTimelines: boolean) => void;
};
export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTimelineSetHandlerMap> {
private readonly timelineSupport: boolean;
private unstableClientRelationAggregation: boolean;
private displayPendingEvents: boolean;
@@ -154,17 +165,12 @@ export class EventTimelineSet extends EventEmitter {
*
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
*/
public getPendingEvents(thread?: Thread): MatrixEvent[] {
public getPendingEvents(): MatrixEvent[] {
if (!this.room || !this.displayPendingEvents) {
return [];
}
const pendingEvents = this.room.getPendingEvents(thread);
if (this.filter) {
return this.filter.filterRoomTimeline(pendingEvents);
} else {
return pendingEvents;
}
return this.room.getPendingEvents();
}
/**
* Get the live timeline for this room.
@@ -248,7 +254,7 @@ export class EventTimelineSet extends EventEmitter {
// Now we can swap the live timeline to the new one.
this.liveTimeline = newTimeline;
this.emit("Room.timelineReset", this.room, this, resetAllTimelines);
this.emit(RoomEvent.TimelineReset, this.room, this, resetAllTimelines);
}
/**
@@ -594,12 +600,11 @@ export class EventTimelineSet extends EventEmitter {
this.setRelationsTarget(event);
this.aggregateRelations(event);
const data = {
const data: IRoomTimelineData = {
timeline: timeline,
liveEvent: !toStartOfTimeline && timeline == this.liveTimeline && !fromCache,
};
this.emit("Room.timeline", event, this.room,
Boolean(toStartOfTimeline), false, data);
this.emit(RoomEvent.Timeline, event, this.room, Boolean(toStartOfTimeline), false, data);
}
/**
@@ -653,7 +658,7 @@ export class EventTimelineSet extends EventEmitter {
const data = {
timeline: timeline,
};
this.emit("Room.timeline", removed, this.room, undefined, true, data);
this.emit(RoomEvent.Timeline, removed, this.room, undefined, true, data);
}
return removed;
}
@@ -751,7 +756,7 @@ export class EventTimelineSet extends EventEmitter {
*/
public getRelationsForEvent(
eventId: string,
relationType: RelationType,
relationType: RelationType | string,
eventType: EventType | string,
): Relations | undefined {
if (!this.unstableClientRelationAggregation) {
@@ -769,6 +774,17 @@ export class EventTimelineSet extends EventEmitter {
return relationsWithRelType[eventType];
}
public getAllRelationsEventForEvent(eventId: string): MatrixEvent[] {
const relationsForEvent = this.relations[eventId] || {};
const events = [];
for (const relationsRecord of Object.values(relationsForEvent)) {
for (const relations of Object.values(relationsRecord)) {
events.push(...relations.getRelations());
}
}
return events;
}
/**
* Set an event as the target event if any Relations exist for it already
*
@@ -809,7 +825,7 @@ export class EventTimelineSet extends EventEmitter {
// If the event is currently encrypted, wait until it has been decrypted.
if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) {
event.once("Event.decrypted", () => {
event.once(MatrixEventEvent.Decrypted, () => {
this.aggregateRelations(event);
});
return;
+286 -72
View File
@@ -1,5 +1,5 @@
/*
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2015 - 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -20,47 +20,22 @@ limitations under the License.
* @module models/event
*/
import { EventEmitter } from 'events';
import { ExtensibleEvent, ExtensibleEvents, Optional } from "matrix-events-sdk";
import { logger } from '../logger';
import { VerificationRequest } from "../crypto/verification/request/VerificationRequest";
import {
EventType,
MsgType,
RelationType,
} from "../@types/event";
import { EVENT_VISIBILITY_CHANGE_TYPE, EventType, MsgType, RelationType } from "../@types/event";
import { Crypto, IEventDecryptionResult } from "../crypto";
import { deepSortedObjectEntries } from "../utils";
import { RoomMember } from "./room-member";
import { Thread, ThreadEvent } from "./thread";
import { Thread, ThreadEvent, EventHandlerMap as ThreadEventHandlerMap } from "./thread";
import { IActionsObject } from '../pushprocessor';
import { ReEmitter } from '../ReEmitter';
import { TypedReEmitter } from '../ReEmitter';
import { MatrixError } from "../http-api";
import { TypedEventEmitter } from "./typed-event-emitter";
import { EventStatus } from "./event-status";
/**
* Enum for event statuses.
* @readonly
* @enum {string}
*/
export enum EventStatus {
/** The event was not sent and will no longer be retried. */
NOT_SENT = "not_sent",
/** The message is being encrypted */
ENCRYPTING = "encrypting",
/** The event is in the process of being sent. */
SENDING = "sending",
/** The event is in a queue waiting to be sent. */
QUEUED = "queued",
/** The event has been sent to the server, but we have not yet received the echo. */
SENT = "sent",
/** The event was cancelled before it was successfully sent. */
CANCELLED = "cancelled",
}
export { EventStatus } from "./event-status";
const interns: Record<string, string> = {};
function intern(str: string): string {
@@ -89,6 +64,13 @@ export interface IUnsigned {
redacted_because?: IEvent;
transaction_id?: string;
invite_room_state?: StrippedState[];
"m.relations"?: Record<RelationType | string, any>; // No common pattern for aggregated relations
}
export interface IThreadBundledRelationship {
latest_event: IEvent;
count: number;
current_user_participated?: boolean;
}
export interface IEvent {
@@ -110,7 +92,7 @@ export interface IEvent {
age?: number;
}
interface IAggregatedRelation {
export interface IAggregatedRelation {
origin_server_ts: number;
event_id?: string;
sender?: string;
@@ -122,9 +104,42 @@ interface IAggregatedRelation {
export interface IEventRelation {
rel_type: RelationType | string;
event_id: string;
"m.in_reply_to"?: {
event_id: string;
"m.render_in"?: string[];
};
key?: string;
}
export interface IVisibilityEventRelation extends IEventRelation {
visibility: "visible" | "hidden";
reason?: string;
}
/**
* When an event is a visibility change event, as per MSC3531,
* the visibility change implied by the event.
*/
export interface IVisibilityChange {
/**
* If `true`, the target event should be made visible.
* Otherwise, it should be hidden.
*/
visible: boolean;
/**
* The event id affected.
*/
eventId: string;
/**
* Optionally, a human-readable reason explaining why
* the event was hidden. Ignored if the event was made
* visible.
*/
reason: string | null;
}
export interface IClearEvent {
room_id?: string;
type: string;
@@ -143,13 +158,71 @@ export interface IDecryptOptions {
isRetry?: boolean;
}
export class MatrixEvent extends EventEmitter {
/**
* Message hiding, as specified by https://github.com/matrix-org/matrix-doc/pull/3531.
*/
export type MessageVisibility = IMessageVisibilityHidden | IMessageVisibilityVisible;
/**
* Variant of `MessageVisibility` for the case in which the message should be displayed.
*/
export interface IMessageVisibilityVisible {
readonly visible: true;
}
/**
* Variant of `MessageVisibility` for the case in which the message should be hidden.
*/
export interface IMessageVisibilityHidden {
readonly visible: false;
/**
* Optionally, a human-readable reason to show to the user indicating why the
* message has been hidden (e.g. "Message Pending Moderation").
*/
readonly reason: string | null;
}
// A singleton implementing `IMessageVisibilityVisible`.
const MESSAGE_VISIBLE: IMessageVisibilityVisible = Object.freeze({ visible: true });
export enum MatrixEventEvent {
Decrypted = "Event.decrypted",
BeforeRedaction = "Event.beforeRedaction",
VisibilityChange = "Event.visibilityChange",
LocalEventIdReplaced = "Event.localEventIdReplaced",
Status = "Event.status",
Replaced = "Event.replaced",
RelationsCreated = "Event.relationsCreated",
}
type EmittedEvents = MatrixEventEvent | ThreadEvent.Update;
export type MatrixEventHandlerMap = {
[MatrixEventEvent.Decrypted]: (event: MatrixEvent, err?: Error) => void;
[MatrixEventEvent.BeforeRedaction]: (event: MatrixEvent, redactionEvent: MatrixEvent) => void;
[MatrixEventEvent.VisibilityChange]: (event: MatrixEvent, visible: boolean) => void;
[MatrixEventEvent.LocalEventIdReplaced]: (event: MatrixEvent) => void;
[MatrixEventEvent.Status]: (event: MatrixEvent, status: EventStatus) => void;
[MatrixEventEvent.Replaced]: (event: MatrixEvent) => void;
[MatrixEventEvent.RelationsCreated]: (relationType: string, eventType: string) => void;
} & ThreadEventHandlerMap;
export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHandlerMap> {
private pushActions: IActionsObject = null;
private _replacingEvent: MatrixEvent = null;
private _localRedactionEvent: MatrixEvent = null;
private _isCancelled = false;
private clearEvent?: IClearEvent;
/* Message hiding, as specified by https://github.com/matrix-org/matrix-doc/pull/3531.
Note: We're returning this object, so any value stored here MUST be frozen.
*/
private visibility: MessageVisibility = MESSAGE_VISIBLE;
// Not all events will be extensible-event compatible, so cache a flag in
// addition to a falsy cached event value. We check the flag later on in
// a public getter to decide if the cache is valid.
private _hasCachedExtEv = false;
private _cachedExtEv: Optional<ExtensibleEvent> = undefined;
/* curve25519 key which we believe belongs to the sender of the event. See
* getSenderKey()
*/
@@ -191,6 +264,7 @@ export class MatrixEvent extends EventEmitter {
* A reference to the thread this event belongs to
*/
private thread: Thread = null;
private threadId: string;
/* Set an approximate timestamp for the event relative the local clock.
* This will inherently be approximate because it doesn't take into account
@@ -198,7 +272,7 @@ export class MatrixEvent extends EventEmitter {
* it to us and the time we're now constructing this event, but that's better
* than assuming the local clock is in sync with the origin HS's clock.
*/
private readonly localTimestamp: number;
public localTimestamp: number;
// XXX: these should be read-only
public sender: RoomMember = null;
@@ -213,7 +287,7 @@ export class MatrixEvent extends EventEmitter {
*/
public verificationRequest: VerificationRequest = null;
private readonly reEmitter: ReEmitter;
private readonly reEmitter: TypedReEmitter<EmittedEvents, MatrixEventHandlerMap>;
/**
* Construct a Matrix Event object
@@ -263,8 +337,27 @@ export class MatrixEvent extends EventEmitter {
});
this.txnId = event.txn_id || null;
this.localTimestamp = Date.now() - this.getAge();
this.reEmitter = new ReEmitter(this);
this.localTimestamp = Date.now() - (this.getAge() ?? 0);
this.reEmitter = new TypedReEmitter(this);
}
/**
* Unstable getter to try and get an extensible event. Note that this might
* return a falsy value if the event could not be parsed as an extensible
* event.
*
* @deprecated Use stable functions where possible.
*/
public get unstableExtensibleEvent(): Optional<ExtensibleEvent> {
if (!this._hasCachedExtEv) {
this._cachedExtEv = ExtensibleEvents.parse(this.getEffectiveEvent());
}
return this._cachedExtEv;
}
private invalidateExtensibleEvent() {
// just reset the flag - that'll trick the getter into parsing a new event
this._hasCachedExtEv = false;
}
/**
@@ -339,10 +432,10 @@ export class MatrixEvent extends EventEmitter {
/**
* Get the room_id for this event. This will return <code>undefined</code>
* for <code>m.presence</code> events.
* @return {string} The room ID, e.g. <code>!cURbafjkfsMDVwdRDQ:matrix.org
* @return {string?} The room ID, e.g. <code>!cURbafjkfsMDVwdRDQ:matrix.org
* </code>
*/
public getRoomId(): string {
public getRoomId(): string | undefined {
return this.event.room_id;
}
@@ -409,10 +502,12 @@ export class MatrixEvent extends EventEmitter {
* @experimental
* Get the event ID of the thread head
*/
public get threadRootId(): string {
public get threadRootId(): string | undefined {
const relatesTo = this.getWireContent()?.["m.relates_to"];
if (relatesTo?.rel_type === RelationType.Thread) {
return relatesTo.event_id;
} else {
return this.getThread()?.id || this.threadId;
}
}
@@ -420,26 +515,30 @@ export class MatrixEvent extends EventEmitter {
* @experimental
*/
public get isThreadRelation(): boolean {
return !!this.threadRootId;
return !!this.threadRootId && this.threadId !== this.getId();
}
/**
* @experimental
*/
public get isThreadRoot(): boolean {
// TODO, change the inner working of this getter for it to use the
// bundled relationship return on the event, view MSC3440
const thread = this.getThread();
return thread?.id === this.getId();
}
const threadDetails = this
.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
public get parentEventId(): string {
return this.replyEventId || this.relationEventId;
// Bundled relationships only returned when the sync response is limited
// hence us having to check both bundled relation and inspect the thread
// model
return !!threadDetails || (this.getThread()?.id === this.getId());
}
public get replyEventId(): string {
const relations = this.getWireContent()["m.relates_to"];
return relations?.["m.in_reply_to"]?.["event_id"];
// We're prefer ev.getContent() over ev.getWireContent() to make sure
// we grab the latest edit with potentially new relations. But we also
// can't just rely on ev.getContent() by itself because historically we
// still show the reply from the original message even though the edit
// event does not include the relation reply.
const mRelatesTo = this.getContent()['m.relates_to'] || this.getWireContent()['m.relates_to'];
return mRelatesTo?.['m.in_reply_to']?.event_id;
}
public get relationEventId(): string {
@@ -475,9 +574,10 @@ export class MatrixEvent extends EventEmitter {
* Get the age of this event. This represents the age of the event when the
* event arrived at the device, and not the age of the event when this
* function was called.
* @return {Number} The age of this event in milliseconds.
* Can only be returned once the server has echo'ed back
* @return {Number|undefined} The age of this event in milliseconds.
*/
public getAge(): number {
public getAge(): number | undefined {
return this.getUnsigned().age || this.event.age; // v2 / v1
}
@@ -570,7 +670,12 @@ export class MatrixEvent extends EventEmitter {
}
public shouldAttemptDecryption() {
return this.isEncrypted() && !this.isBeingDecrypted() && !this.clearEvent;
if (this.isRedacted()) return false;
if (this.isBeingDecrypted()) return false;
if (this.clearEvent) return false;
if (!this.isEncrypted()) return false;
return true;
}
/**
@@ -761,7 +866,7 @@ export class MatrixEvent extends EventEmitter {
this.setPushActions(null);
if (options.emit !== false) {
this.emit("Event.decrypted", this, err);
this.emit(MatrixEventEvent.Decrypted, this, err);
}
return;
@@ -801,6 +906,7 @@ export class MatrixEvent extends EventEmitter {
this.forwardingCurve25519KeyChain =
decryptionResult.forwardingCurve25519KeyChain || [];
this.untrusted = decryptionResult.untrusted || false;
this.invalidateExtensibleEvent();
}
/**
@@ -904,6 +1010,10 @@ export class MatrixEvent extends EventEmitter {
return this.event.unsigned || {};
}
public setUnsigned(unsigned: IUnsigned): void {
this.event.unsigned = unsigned;
}
public unmarkLocallyRedacted(): boolean {
const value = this._localRedactionEvent;
this._localRedactionEvent = null;
@@ -915,7 +1025,7 @@ export class MatrixEvent extends EventEmitter {
public markLocallyRedacted(redactionEvent: MatrixEvent): void {
if (this._localRedactionEvent) return;
this.emit("Event.beforeRedaction", this, redactionEvent);
this.emit(MatrixEventEvent.BeforeRedaction, this, redactionEvent);
this._localRedactionEvent = redactionEvent;
if (!this.event.unsigned) {
this.event.unsigned = {};
@@ -923,6 +1033,53 @@ export class MatrixEvent extends EventEmitter {
this.event.unsigned.redacted_because = redactionEvent.event as IEvent;
}
/**
* Change the visibility of an event, as per https://github.com/matrix-org/matrix-doc/pull/3531 .
*
* @fires module:models/event.MatrixEvent#"Event.visibilityChange" if `visibilityEvent`
* caused a change in the actual visibility of this event, either by making it
* visible (if it was hidden), by making it hidden (if it was visible) or by
* changing the reason (if it was hidden).
* @param visibilityEvent event holding a hide/unhide payload, or nothing
* if the event is being reset to its original visibility (presumably
* by a visibility event being redacted).
*/
public applyVisibilityEvent(visibilityChange?: IVisibilityChange): void {
const visible = visibilityChange ? visibilityChange.visible : true;
const reason = visibilityChange ? visibilityChange.reason : null;
let change = false;
if (this.visibility.visible !== visibilityChange.visible) {
change = true;
} else if (!this.visibility.visible && this.visibility["reason"] !== reason) {
change = true;
}
if (change) {
if (visible) {
this.visibility = MESSAGE_VISIBLE;
} else {
this.visibility = Object.freeze({
visible: false,
reason: reason,
});
}
if (change) {
this.emit(MatrixEventEvent.VisibilityChange, this, visible);
}
}
}
/**
* Return instructions to display or hide the message.
*
* @returns Instructions determining whether the message
* should be displayed.
*/
public messageVisibility(): MessageVisibility {
// Note: We may return `this.visibility` without fear, as
// this is a shallow frozen object.
return this.visibility;
}
/**
* Update the content of an event in the same way it would be by the server
* if it were redacted before it was sent to us
@@ -938,7 +1095,7 @@ export class MatrixEvent extends EventEmitter {
this._localRedactionEvent = null;
this.emit("Event.beforeRedaction", this, redactionEvent);
this.emit(MatrixEventEvent.BeforeRedaction, this, redactionEvent);
this._replacingEvent = null;
// we attempt to replicate what we would see from the server if
@@ -972,6 +1129,8 @@ export class MatrixEvent extends EventEmitter {
delete content[key];
}
}
this.invalidateExtensibleEvent();
}
/**
@@ -992,6 +1151,54 @@ export class MatrixEvent extends EventEmitter {
return this.getType() === EventType.RoomRedaction;
}
/**
* Return the visibility change caused by this event,
* as per https://github.com/matrix-org/matrix-doc/pull/3531.
*
* @returns If the event is a well-formed visibility change event,
* an instance of `IVisibilityChange`, otherwise `null`.
*/
public asVisibilityChange(): IVisibilityChange | null {
if (!EVENT_VISIBILITY_CHANGE_TYPE.matches(this.getType())) {
// Not a visibility change event.
return null;
}
const relation = this.getRelation();
if (!relation || relation.rel_type != "m.reference") {
// Ill-formed, ignore this event.
return null;
}
const eventId = relation.event_id;
if (!eventId) {
// Ill-formed, ignore this event.
return null;
}
const content = this.getWireContent();
const visible = !!content.visible;
const reason = content.reason;
if (reason && typeof reason != "string") {
// Ill-formed, ignore this event.
return null;
}
// Well-formed visibility change event.
return {
visible,
reason,
eventId,
};
}
/**
* Check if this event alters the visibility of another event,
* as per https://github.com/matrix-org/matrix-doc/pull/3531.
*
* @returns {boolean} True if this event alters the visibility
* of another event.
*/
public isVisibilityEvent(): boolean {
return EVENT_VISIBILITY_CHANGE_TYPE.matches(this.getType());
}
/**
* Get the (decrypted, if necessary) redaction event JSON
* if event was redacted
@@ -1051,8 +1258,10 @@ export class MatrixEvent extends EventEmitter {
this.setStatus(null);
if (this.getId() !== oldId) {
// emit the event if it changed
this.emit("Event.localEventIdReplaced", this);
this.emit(MatrixEventEvent.LocalEventIdReplaced, this);
}
this.localTimestamp = Date.now() - this.getAge();
}
/**
@@ -1072,12 +1281,12 @@ export class MatrixEvent extends EventEmitter {
*/
public setStatus(status: EventStatus): void {
this.status = status;
this.emit("Event.status", this, status);
this.emit(MatrixEventEvent.Status, this, status);
}
public replaceLocalEventId(eventId: string): void {
this.event.event_id = eventId;
this.emit("Event.localEventIdReplaced", this);
this.emit(MatrixEventEvent.LocalEventIdReplaced, this);
}
/**
@@ -1126,7 +1335,8 @@ export class MatrixEvent extends EventEmitter {
}
if (this._replacingEvent !== newEvent) {
this._replacingEvent = newEvent;
this.emit("Event.replaced", this);
this.emit(MatrixEventEvent.Replaced, this);
this.invalidateExtensibleEvent();
}
}
@@ -1146,11 +1356,8 @@ export class MatrixEvent extends EventEmitter {
return this.status;
}
public getServerAggregatedRelation(relType: RelationType): IAggregatedRelation {
const relations = this.getUnsigned()["m.relations"];
if (relations) {
return relations[relType];
}
public getServerAggregatedRelation<T>(relType: RelationType): T | undefined {
return this.getUnsigned()["m.relations"]?.[relType];
}
/**
@@ -1159,7 +1366,7 @@ export class MatrixEvent extends EventEmitter {
* @return {string?}
*/
public replacingEventId(): string | undefined {
const replaceRelation = this.getServerAggregatedRelation(RelationType.Replace);
const replaceRelation = this.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
if (replaceRelation) {
return replaceRelation.event_id;
} else if (this._replacingEvent) {
@@ -1184,7 +1391,7 @@ export class MatrixEvent extends EventEmitter {
* @return {Date?}
*/
public replacingEventDate(): Date | undefined {
const replaceRelation = this.getServerAggregatedRelation(RelationType.Replace);
const replaceRelation = this.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
if (replaceRelation) {
const ts = replaceRelation.origin_server_ts;
if (Number.isFinite(ts)) {
@@ -1210,7 +1417,9 @@ export class MatrixEvent extends EventEmitter {
*/
public getAssociatedId(): string | undefined {
const relation = this.getRelation();
if (relation) {
if (this.replyEventId) {
return this.replyEventId;
} else if (relation) {
return relation.event_id;
} else if (this.isRedaction()) {
return this.event.redacts;
@@ -1344,15 +1553,20 @@ export class MatrixEvent extends EventEmitter {
*/
public setThread(thread: Thread): void {
this.thread = thread;
this.reEmitter.reEmit(thread, [ThreadEvent.Ready, ThreadEvent.Update]);
this.setThreadId(thread.id);
this.reEmitter.reEmit(thread, [ThreadEvent.Update]);
}
/**
* @experimental
*/
public getThread(): Thread {
public getThread(): Thread | undefined {
return this.thread;
}
public setThreadId(threadId: string): void {
this.threadId = threadId;
}
}
/* REDACT_KEEP_KEYS gives the keys we keep when an event is redacted
+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";
+39
View File
@@ -0,0 +1,39 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Relations, RelationsEvent, EventHandlerMap } from "./relations";
import { MatrixEvent } from "./event";
import { Listener } from "./typed-event-emitter";
export class RelatedRelations {
private relations: Relations[];
public constructor(relations: Relations[]) {
this.relations = relations.filter(r => !!r);
}
public getRelations(): MatrixEvent[] {
return this.relations.reduce((c, p) => [...c, ...p.getRelations()], []);
}
public on<T extends RelationsEvent>(ev: T, fn: Listener<RelationsEvent, EventHandlerMap, T>) {
this.relations.forEach(r => r.on(ev, fn));
}
public off<T extends RelationsEvent>(ev: T, fn: Listener<RelationsEvent, EventHandlerMap, T>) {
this.relations.forEach(r => r.off(ev, fn));
}
}
+29 -18
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 } from './event';
import { EventStatus, IAggregatedRelation, MatrixEvent, MatrixEventEvent } from './event';
import { Room } from './room';
import { logger } from '../logger';
import { RelationType } from "../@types/event";
import { TypedEventEmitter } from "./typed-event-emitter";
export enum RelationsEvent {
Add = "Relations.add",
Remove = "Relations.remove",
Redaction = "Relations.redaction",
}
export type EventHandlerMap = {
[RelationsEvent.Add]: (event: MatrixEvent) => void;
[RelationsEvent.Remove]: (event: MatrixEvent) => void;
[RelationsEvent.Redaction]: (event: MatrixEvent) => void;
};
/**
* A container for relation events that supports easy access to common ways of
@@ -29,7 +40,7 @@ import { RelationType } from "../@types/event";
* The typical way to get one of these containers is via
* EventTimelineSet#getRelationsForEvent.
*/
export class Relations extends EventEmitter {
export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap> {
private relationEventIds = new Set<string>();
private relations = new Set<MatrixEvent>();
private annotationsByKey: Record<string, Set<MatrixEvent>> = {};
@@ -84,7 +95,7 @@ export class Relations extends EventEmitter {
// If the event is in the process of being sent, listen for cancellation
// so we can remove the event from the collection.
if (event.isSending()) {
event.on("Event.status", this.onEventStatus);
event.on(MatrixEventEvent.Status, this.onEventStatus);
}
this.relations.add(event);
@@ -97,9 +108,9 @@ export class Relations extends EventEmitter {
this.targetEvent.makeReplaced(lastReplacement);
}
event.on("Event.beforeRedaction", this.onBeforeRedaction);
event.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
this.emit("Relations.add", event);
this.emit(RelationsEvent.Add, event);
this.maybeEmitCreated();
}
@@ -138,7 +149,7 @@ export class Relations extends EventEmitter {
this.targetEvent.makeReplaced(lastReplacement);
}
this.emit("Relations.remove", event);
this.emit(RelationsEvent.Remove, event);
}
/**
@@ -150,14 +161,14 @@ export class Relations extends EventEmitter {
private onEventStatus = (event: MatrixEvent, status: EventStatus) => {
if (!event.isSending()) {
// Sending is done, so we don't need to listen anymore
event.removeListener("Event.status", this.onEventStatus);
event.removeListener(MatrixEventEvent.Status, this.onEventStatus);
return;
}
if (status !== EventStatus.CANCELLED) {
return;
}
// Event was cancelled, remove from the collection
event.removeListener("Event.status", this.onEventStatus);
event.removeListener(MatrixEventEvent.Status, this.onEventStatus);
this.removeEvent(event);
};
@@ -171,11 +182,11 @@ export class Relations extends EventEmitter {
* @return {Array}
* Relation events in insertion order.
*/
public getRelations() {
public getRelations(): MatrixEvent[] {
return [...this.relations];
}
private addAnnotationToAggregation(event: MatrixEvent) {
private addAnnotationToAggregation(event: MatrixEvent): void {
const { key } = event.getRelation();
if (!key) {
return;
@@ -204,7 +215,7 @@ export class Relations extends EventEmitter {
eventsFromSender.add(event);
}
private removeAnnotationFromAggregation(event: MatrixEvent) {
private removeAnnotationFromAggregation(event: MatrixEvent): void {
const { key } = event.getRelation();
if (!key) {
return;
@@ -240,7 +251,7 @@ export class Relations extends EventEmitter {
* @param {MatrixEvent} redactedEvent
* The original relation event that is about to be redacted.
*/
private onBeforeRedaction = async (redactedEvent: MatrixEvent) => {
private onBeforeRedaction = async (redactedEvent: MatrixEvent): Promise<void> => {
if (!this.relations.has(redactedEvent)) {
return;
}
@@ -255,9 +266,9 @@ export class Relations extends EventEmitter {
this.targetEvent.makeReplaced(lastReplacement);
}
redactedEvent.removeListener("Event.beforeRedaction", this.onBeforeRedaction);
redactedEvent.removeListener(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
this.emit("Relations.redaction", redactedEvent);
this.emit(RelationsEvent.Redaction, redactedEvent);
};
/**
@@ -319,7 +330,7 @@ export class Relations extends EventEmitter {
// the all-knowning server tells us that the event at some point had
// this timestamp for its replacement, so any following replacement should definitely not be less
const replaceRelation = this.targetEvent.getServerAggregatedRelation(RelationType.Replace);
const replaceRelation = this.targetEvent.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
const minTs = replaceRelation && replaceRelation.origin_server_ts;
const lastReplacement = this.getRelations().reduce((last, event) => {
@@ -375,6 +386,6 @@ export class Relations extends EventEmitter {
return;
}
this.creationEmitted = true;
this.targetEvent.emit("Event.relationsCreated", this.relationType, this.eventType);
this.targetEvent.emit(MatrixEventEvent.RelationsCreated, this.relationType, this.eventType);
}
}
+34 -10
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);
}
}
+33 -18
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);
}
/**
@@ -618,14 +633,14 @@ export class RoomState extends EventEmitter {
}
/**
* Returns true if the given MatrixClient has permission to send a state
* event of type `stateEventType` into this room.
* @param {string} stateEventType The type of state events to test
* @param {MatrixClient} cli The client to test permission for
* @return {boolean} true if the given client should be permitted to send
* the given type of state event into this room,
* according to the room's state.
*/
* Returns true if the given MatrixClient has permission to send a state
* event of type `stateEventType` into this room.
* @param {string} stateEventType The type of state events to test
* @param {MatrixClient} cli The client to test permission for
* @return {boolean} true if the given client should be permitted to send
* the given type of state event into this room,
* according to the room's state.
*/
public mayClientSendStateEvent(stateEventType: EventType | string, cli: MatrixClient): boolean {
if (cli.isGuest()) {
return false;
+451 -145
View File
@@ -18,25 +18,28 @@ limitations under the License.
* @module models/room
*/
import { EventEmitter } from "events";
import { EventTimelineSet, DuplicateStrategy } from "./event-timeline-set";
import { Direction, EventTimeline } from "./event-timeline";
import { getHttpUriForMxc } from "../content-repo";
import * as utils from "../utils";
import { normalize } from "../utils";
import { EventStatus, IEvent, MatrixEvent } from "./event";
import { IEvent, MatrixEvent } from "./event";
import { EventStatus } from "./event-status";
import { RoomMember } from "./room-member";
import { IRoomSummary, RoomSummary } from "./room-summary";
import { logger } from '../logger';
import { ReEmitter } from '../ReEmitter';
import { EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../@types/event";
import { TypedReEmitter } from '../ReEmitter';
import {
EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS,
EVENT_VISIBILITY_CHANGE_TYPE,
} from "../@types/event";
import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials";
import { Filter } from "../filter";
import { RoomState } from "./room-state";
import { Thread, ThreadEvent } from "./thread";
import { Thread, ThreadEvent, EventHandlerMap as ThreadHandlerMap } from "./thread";
import { Method } from "../http-api";
import { TypedEventEmitter } from "./typed-event-emitter";
// These constants are used as sane defaults when the homeserver doesn't support
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
@@ -93,7 +96,7 @@ interface ICachedReceipt {
data: IReceipt;
}
type ReceiptCache = Record<string, ICachedReceipt[]>;
type ReceiptCache = {[eventId: string]: ICachedReceipt[]};
interface IReceiptContent {
[eventId: string]: {
@@ -103,7 +106,30 @@ interface IReceiptContent {
};
}
type Receipts = Record<string, Record<string, IWrappedReceipt>>;
const ReceiptPairRealIndex = 0;
const ReceiptPairSyntheticIndex = 1;
// We will only hold a synthetic receipt if we do not have a real receipt or the synthetic is newer.
type Receipts = {
[receiptType: string]: {
[userId: string]: [IWrappedReceipt, IWrappedReceipt]; // Pair<real receipt, synthetic receipt> (both nullable)
};
};
// When inserting a visibility event affecting event `eventId`, we
// need to scan through existing visibility events for `eventId`.
// In theory, this could take an unlimited amount of time if:
//
// - the visibility event was sent by a moderator; and
// - `eventId` already has many visibility changes (usually, it should
// be 2 or less); and
// - for some reason, the visibility changes are received out of order
// (usually, this shouldn't happen at all).
//
// For this reason, we limit the number of events to scan through,
// expecting that a broken visibility change for a single event in
// an extremely uncommon case (possibly a DoS) is a small
// price to pay to keep matrix-js-sdk responsive.
const MAX_NUMBER_OF_VISIBILITY_EVENTS_TO_SCAN_THROUGH = 30;
export enum NotificationCountType {
Highlight = "highlight",
@@ -117,17 +143,51 @@ export interface ICreateFilterOpts {
prepopulateTimeline?: boolean;
}
export class Room extends EventEmitter {
private readonly reEmitter: ReEmitter;
export enum RoomEvent {
MyMembership = "Room.myMembership",
Tags = "Room.tags",
AccountData = "Room.accountData",
Receipt = "Room.receipt",
Name = "Room.name",
Redaction = "Room.redaction",
RedactionCancelled = "Room.redactionCancelled",
LocalEchoUpdated = "Room.localEchoUpdated",
Timeline = "Room.timeline",
TimelineReset = "Room.timelineReset",
}
type EmittedEvents = RoomEvent
| ThreadEvent.New
| ThreadEvent.Update
| RoomEvent.Timeline
| RoomEvent.TimelineReset;
export type RoomEventHandlerMap = {
[RoomEvent.MyMembership]: (room: Room, membership: string, prevMembership?: string) => void;
[RoomEvent.Tags]: (event: MatrixEvent, room: Room) => void;
[RoomEvent.AccountData]: (event: MatrixEvent, room: Room, lastEvent?: MatrixEvent) => void;
[RoomEvent.Receipt]: (event: MatrixEvent, room: Room) => void;
[RoomEvent.Name]: (room: Room) => void;
[RoomEvent.Redaction]: (event: MatrixEvent, room: Room) => void;
[RoomEvent.RedactionCancelled]: (event: MatrixEvent, room: Room) => void;
[RoomEvent.LocalEchoUpdated]: (
event: MatrixEvent,
room: Room,
oldEventId?: string,
oldStatus?: EventStatus,
) => void;
[ThreadEvent.New]: (thread: Thread) => void;
} & ThreadHandlerMap;
export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap> {
private readonly reEmitter: TypedReEmitter<EmittedEvents, RoomEventHandlerMap>;
private txnToEvent: Record<string, MatrixEvent> = {}; // Pending in-flight requests { string: MatrixEvent }
// receipts should clobber based on receipt_type and user_id pairs hence
// the form of this structure. This is sub-optimal for the exposed APIs
// which pass in an event ID and get back some receipts, so we also store
// a pre-cached list for this purpose.
private receipts: Receipts = {}; // { receipt_type: { user_id: IReceipt } }
private receiptCacheByEventId: ReceiptCache = {}; // { event_id: IReceipt2[] }
// only receipts that came from the server, not synthesized ones
private realReceipts: Receipts = {};
private receiptCacheByEventId: ReceiptCache = {}; // { event_id: ICachedReceipt[] }
private notificationCounts: Partial<Record<NotificationCountType, number>> = {};
private readonly timelineSets: EventTimelineSet[];
// any filtered timeline sets we're maintaining for this room
@@ -192,6 +252,25 @@ export class Room extends EventEmitter {
* @experimental
*/
public threads = new Map<string, Thread>();
public lastThread: Thread;
/**
* A mapping of eventId to all visibility changes to apply
* to the event, by chronological order, as per
* https://github.com/matrix-org/matrix-doc/pull/3531
*
* # Invariants
*
* - within each list, all events are classed by
* chronological order;
* - all events are events such that
* `asVisibilityEvent()` returns a non-null `IVisibilityChange`;
* - within each list with key `eventId`, all events
* are in relation to `eventId`.
*
* @experimental
*/
private visibilityEvents = new Map<string, MatrixEvent[]>();
/**
* Construct a new Room.
@@ -244,7 +323,7 @@ export class Room extends EventEmitter {
// In some cases, we add listeners for every displayed Matrix event, so it's
// common to have quite a few more than the default limit.
this.setMaxListeners(100);
this.reEmitter = new ReEmitter(this);
this.reEmitter = new TypedReEmitter(this);
opts.pendingEventOrdering = opts.pendingEventOrdering || PendingEventOrdering.Chronological;
@@ -253,7 +332,10 @@ export class Room extends EventEmitter {
// all our per-room timeline sets. the first one is the unfiltered ones;
// the subsequent ones are the filtered ones in no particular order.
this.timelineSets = [new EventTimelineSet(this, opts)];
this.reEmitter.reEmit(this.getUnfilteredTimelineSet(), ["Room.timeline", "Room.timelineReset"]);
this.reEmitter.reEmit(this.getUnfilteredTimelineSet(), [
RoomEvent.Timeline,
RoomEvent.TimelineReset,
]);
this.fixUpLegacyTimelineFields();
@@ -468,16 +550,14 @@ export class Room extends EventEmitter {
*
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
*/
public getPendingEvents(thread?: Thread): MatrixEvent[] {
public getPendingEvents(): MatrixEvent[] {
if (this.opts.pendingEventOrdering !== PendingEventOrdering.Detached) {
throw new Error(
"Cannot call getPendingEvents with pendingEventOrdering == " +
this.opts.pendingEventOrdering);
}
return this.pendingEventList.filter(event => {
return !thread || thread.id === event.threadRootId;
});
return this.pendingEventList;
}
/**
@@ -669,7 +749,7 @@ export class Room extends EventEmitter {
if (membership === "leave") {
this.cleanupAfterLeaving();
}
this.emit("Room.myMembership", this, membership, prevMembership);
this.emit(RoomEvent.MyMembership, this, membership, prevMembership);
}
}
@@ -1242,7 +1322,10 @@ export class Room extends EventEmitter {
}
const opts = Object.assign({ filter: filter }, this.opts);
const timelineSet = new EventTimelineSet(this, opts);
this.reEmitter.reEmit(timelineSet, ["Room.timeline", "Room.timelineReset"]);
this.reEmitter.reEmit(timelineSet, [
RoomEvent.Timeline,
RoomEvent.TimelineReset,
]);
this.filteredTimelineSets[filter.filterId] = timelineSet;
this.timelineSets.push(timelineSet);
@@ -1304,7 +1387,7 @@ export class Room extends EventEmitter {
}
}
public findThreadForEvent(event: MatrixEvent): Thread {
public findThreadForEvent(event: MatrixEvent): Thread | null {
if (!event) {
return null;
}
@@ -1314,7 +1397,7 @@ export class Room extends EventEmitter {
} else if (event.isThreadRoot) {
return this.threads.get(event.getId());
} else {
const parentEvent = this.findEventById(event.parentEventId);
const parentEvent = this.findEventById(event.getAssociatedId());
return this.findThreadForEvent(parentEvent);
}
}
@@ -1323,46 +1406,69 @@ export class Room extends EventEmitter {
* Add an event to a thread's timeline. Will fire "Thread.update"
* @experimental
*/
public async addThreadedEvent(event: MatrixEvent): Promise<void> {
public async addThreadedEvent(event: MatrixEvent, toStartOfTimeline: boolean): Promise<void> {
let thread = this.findThreadForEvent(event);
if (thread) {
thread.addEvent(event);
thread.addEvent(event, toStartOfTimeline);
} else {
const events = [event];
let rootEvent = this.findEventById(event.threadRootId);
// If the rootEvent does not exist in the current sync, then look for
// it over the network
if (!rootEvent) {
const eventData = await this.client.fetchRoomEvent(this.roomId, event.threadRootId);
rootEvent = new MatrixEvent(eventData);
}
events.unshift(rootEvent);
thread = this.createThread(events);
}
try {
let eventData;
if (event.threadRootId) {
eventData = await this.client.fetchRoomEvent(this.roomId, event.threadRootId);
}
if (event.getUnsigned().transaction_id) {
const existingEvent = this.txnToEvent[event.getUnsigned().transaction_id];
if (existingEvent) {
// remote echo of an event we sent earlier
this.handleRemoteEcho(event, existingEvent);
return;
if (!rootEvent) {
rootEvent = new MatrixEvent(eventData);
} else {
rootEvent.setUnsigned(eventData.unsigned);
}
} finally {
// The root event might be not be visible to the person requesting
// it. If it wasn't fetched successfully the thread will work
// in "limited" mode and won't benefit from all the APIs a homeserver
// can provide to enhance the thread experience
thread = this.createThread(rootEvent, events);
}
}
this.emit(ThreadEvent.Update, thread);
}
public createThread(events: MatrixEvent[]): Thread {
const thread = new Thread(events, this, this.client);
this.threads.set(thread.id, thread);
this.reEmitter.reEmit(thread, [
ThreadEvent.Update,
ThreadEvent.Ready,
"Room.timeline",
"Room.timelineReset",
]);
this.emit(ThreadEvent.New, thread);
return thread;
public createThread(rootEvent: MatrixEvent | undefined, events: MatrixEvent[] = []): Thread | undefined {
if (rootEvent) {
const tl = this.getTimelineForEvent(rootEvent.getId());
const relatedEvents = tl?.getTimelineSet().getAllRelationsEventForEvent(rootEvent.getId());
if (relatedEvents) {
events = events.concat(relatedEvents);
}
}
const thread = new Thread(rootEvent, {
initialEvents: events,
room: this,
client: this.client,
});
// If we managed to create a thread and figure out its `id`
// then we can use it
if (thread.id) {
this.threads.set(thread.id, thread);
this.reEmitter.reEmit(thread, [
ThreadEvent.Update,
RoomEvent.Timeline,
RoomEvent.TimelineReset,
]);
if (!this.lastThread || this.lastThread.rootEvent?.localTimestamp < rootEvent?.localTimestamp) {
this.lastThread = thread;
}
this.emit(ThreadEvent.New, thread);
return thread;
}
}
/**
@@ -1395,13 +1501,22 @@ export class Room extends EventEmitter {
}
}
this.emit("Room.redaction", event, this);
this.emit(RoomEvent.Redaction, event, this);
// TODO: we stash user displaynames (among other things) in
// RoomMember objects which are then attached to other events
// (in the sender and target fields). We should get those
// RoomMember objects to update themselves when the events that
// they are based on are changed.
// Remove any visibility change on this event.
this.visibilityEvents.delete(redactId);
// If this event is a visibility change event, remove it from the
// list of visibility changes and update any event affected by it.
if (redactedEvent.isVisibilityEvent()) {
this.redactVisibilityChangeEvent(event);
}
}
// FIXME: apply redactions to notification list
@@ -1411,6 +1526,15 @@ export class Room extends EventEmitter {
// this may be needed to trigger an update.
}
// Implement MSC3531: hiding messages.
if (event.isVisibilityEvent()) {
// This event changes the visibility of another event, record
// the visibility change, inform clients if necessary.
this.applyNewVisibilityEvent(event);
}
// If any pending visibility change is waiting for this (older) event,
this.applyPendingVisibilityEvents(event);
if (event.getUnsigned().transaction_id) {
const existingEvent = this.txnToEvent[event.getUnsigned().transaction_id];
if (existingEvent) {
@@ -1477,8 +1601,7 @@ export class Room extends EventEmitter {
EventTimeline.setEventMetadata(event, this.getLiveTimeline().getState(EventTimeline.FORWARDS), false);
this.txnToEvent[txnId] = event;
const thread = this.threads.get(event.threadRootId);
if (this.opts.pendingEventOrdering === PendingEventOrdering.Detached && !thread) {
if (this.opts.pendingEventOrdering === PendingEventOrdering.Detached) {
if (this.pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) {
logger.warn("Setting event as NOT_SENT due to messages in the same state");
event.setStatus(EventStatus.NOT_SENT);
@@ -1494,37 +1617,31 @@ export class Room extends EventEmitter {
if (event.isRedaction()) {
const redactId = event.event.redacts;
let redactedEvent = this.pendingEventList &&
this.pendingEventList.find(e => e.getId() === redactId);
let redactedEvent = this.pendingEventList?.find(e => e.getId() === redactId);
if (!redactedEvent) {
redactedEvent = this.findEventById(redactId);
}
if (redactedEvent) {
redactedEvent.markLocallyRedacted(event);
this.emit("Room.redaction", event, this);
this.emit(RoomEvent.Redaction, event, this);
}
}
} else {
if (thread) {
thread.timelineSet.addEventToTimeline(event,
thread.timelineSet.getLiveTimeline(), false);
} else {
for (let i = 0; i < this.timelineSets.length; i++) {
const timelineSet = this.timelineSets[i];
if (timelineSet.getFilter()) {
if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
timelineSet.addEventToTimeline(event,
timelineSet.getLiveTimeline(), false);
}
} else {
for (let i = 0; i < this.timelineSets.length; i++) {
const timelineSet = this.timelineSets[i];
if (timelineSet.getFilter()) {
if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
timelineSet.addEventToTimeline(event,
timelineSet.getLiveTimeline(), false);
}
} else {
timelineSet.addEventToTimeline(event,
timelineSet.getLiveTimeline(), false);
}
}
}
this.emit("Room.localEchoUpdated", event, this, null, null);
this.emit(RoomEvent.LocalEchoUpdated, event, this, null, null);
}
/**
@@ -1580,7 +1697,9 @@ export class Room extends EventEmitter {
const thread = this.findThreadForEvent(event);
if (thread) {
thread.timelineSet.aggregateRelations(event);
} else {
}
if (thread?.id === event.getAssociatedId() || !thread) {
// TODO: We should consider whether this means it would be a better
// design to lift the relations handling up to the room instead.
for (let i = 0; i < this.timelineSets.length; i++) {
@@ -1596,6 +1715,10 @@ export class Room extends EventEmitter {
}
}
public getEventForTxnId(txnId: string): MatrixEvent {
return this.txnToEvent[txnId];
}
/**
* Deal with the echo of a message we sent.
*
@@ -1610,7 +1733,7 @@ export class Room extends EventEmitter {
* @fires module:client~MatrixClient#event:"Room.localEchoUpdated"
* @private
*/
private handleRemoteEcho(remoteEvent: MatrixEvent, localEvent: MatrixEvent): void {
public handleRemoteEcho(remoteEvent: MatrixEvent, localEvent: MatrixEvent): void {
const oldEventId = localEvent.getId();
const newEventId = remoteEvent.getId();
const oldStatus = localEvent.status;
@@ -1632,10 +1755,12 @@ export class Room extends EventEmitter {
// any, which is good, because we don't want to try decoding it again).
localEvent.handleRemoteEcho(remoteEvent.event);
const thread = this.threads.get(remoteEvent.threadRootId);
const thread = this.findThreadForEvent(remoteEvent);
if (thread) {
thread.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
} else {
}
if (thread?.id === remoteEvent.getAssociatedId() || !thread) {
for (let i = 0; i < this.timelineSets.length; i++) {
const timelineSet = this.timelineSets[i];
@@ -1644,8 +1769,7 @@ export class Room extends EventEmitter {
}
}
this.emit("Room.localEchoUpdated", localEvent, this,
oldEventId, oldStatus);
this.emit(RoomEvent.LocalEchoUpdated, localEvent, this, oldEventId, oldStatus);
}
/**
@@ -1705,7 +1829,8 @@ export class Room extends EventEmitter {
const thread = this.findThreadForEvent(event);
if (thread) {
thread.timelineSet.replaceEventId(oldEventId, newEventId);
} else {
}
if (thread?.id === event.getAssociatedId() || !thread) {
// if the event was already in the timeline (which will be the case if
// opts.pendingEventOrdering==chronological), we need to update the
// timeline map.
@@ -1728,7 +1853,7 @@ export class Room extends EventEmitter {
}
this.savePendingEvents();
this.emit("Room.localEchoUpdated", event, this, oldEventId, oldStatus);
this.emit(RoomEvent.LocalEchoUpdated, event, this, oldEventId, oldStatus);
}
private revertRedactionLocalEcho(redactionEvent: MatrixEvent): void {
@@ -1741,7 +1866,7 @@ export class Room extends EventEmitter {
if (redactedEvent) {
redactedEvent.unmarkLocallyRedacted();
// re-render after undoing redaction
this.emit("Room.redactionCancelled", redactionEvent, this);
this.emit(RoomEvent.RedactionCancelled, redactionEvent, this);
// reapply relation now redaction failed
if (redactedEvent.isRelation()) {
this.aggregateNonLiveRelation(redactedEvent);
@@ -1793,7 +1918,7 @@ export class Room extends EventEmitter {
// TODO: We should have a filter to say "only add state event
// types X Y Z to the timeline".
this.addLiveEvent(events[i], duplicateStrategy, fromCache);
const thread = this.threads.get(events[i].getId());
const thread = this.findThreadForEvent(events[i]);
if (thread) {
thread.addEvent(events[i], true);
}
@@ -1881,7 +2006,7 @@ export class Room extends EventEmitter {
});
if (oldName !== this.name) {
this.emit("Room.name", this);
this.emit(RoomEvent.Name, this);
}
}
@@ -1899,19 +2024,12 @@ export class Room extends EventEmitter {
}
public getReadReceiptForUserId(userId: string, ignoreSynthesized = false): IWrappedReceipt | null {
let receipts = this.receipts;
const [realReceipt, syntheticReceipt] = this.receipts["m.read"]?.[userId] ?? [];
if (ignoreSynthesized) {
receipts = this.realReceipts;
return realReceipt;
}
if (
receipts["m.read"] === undefined ||
receipts["m.read"][userId] === undefined
) {
return null;
}
return receipts["m.read"][userId];
return syntheticReceipt ?? realReceipt;
}
/**
@@ -1975,84 +2093,115 @@ export class Room extends EventEmitter {
/**
* Add a receipt event to the room.
* @param {MatrixEvent} event The m.receipt event.
* @param {Boolean} fake True if this event is implicit
* @param {Boolean} synthetic True if this event is implicit.
*/
public addReceipt(event: MatrixEvent, fake = false): void {
if (!fake) {
this.addReceiptsToStructure(event, this.realReceipts);
// we don't bother caching real receipts by event ID
// as there's nothing that would read it.
}
this.addReceiptsToStructure(event, this.receipts);
this.receiptCacheByEventId = this.buildReceiptCache(this.receipts);
// send events after we've regenerated the cache, otherwise things that
// listened for the event would read from a stale cache
this.emit("Room.receipt", event, this);
public addReceipt(event: MatrixEvent, synthetic = false): void {
this.addReceiptsToStructure(event, synthetic);
// send events after we've regenerated the structure & cache, otherwise things that
// listened for the event would read stale data.
this.emit(RoomEvent.Receipt, event, this);
}
/**
* Add a receipt event to the room.
* @param {MatrixEvent} event The m.receipt event.
* @param {Object} receipts The object to add receipts to
* @param {Boolean} synthetic True if this event is implicit.
*/
private addReceiptsToStructure(event: MatrixEvent, receipts: Receipts): void {
private addReceiptsToStructure(event: MatrixEvent, synthetic: boolean): void {
const content = event.getContent<IReceiptContent>();
Object.keys(content).forEach((eventId) => {
Object.keys(content[eventId]).forEach((receiptType) => {
Object.keys(content[eventId][receiptType]).forEach((userId) => {
const receipt = content[eventId][receiptType][userId];
if (!receipts[receiptType]) {
receipts[receiptType] = {};
if (!this.receipts[receiptType]) {
this.receipts[receiptType] = {};
}
if (!this.receipts[receiptType][userId]) {
this.receipts[receiptType][userId] = [null, null];
}
const existingReceipt = receipts[receiptType][userId];
const pair = this.receipts[receiptType][userId];
if (!existingReceipt) {
receipts[receiptType][userId] = {} as IWrappedReceipt;
} else {
// we only want to add this receipt if we think it is later
// than the one we already have. (This is managed
// server-side, but because we synthesize RRs locally we
// have to do it here too.)
let existingReceipt = pair[ReceiptPairRealIndex];
if (synthetic) {
existingReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
}
if (existingReceipt) {
// we only want to add this receipt if we think it is later than the one we already have.
// This is managed server-side, but because we synthesize RRs locally we have to do it here too.
const ordering = this.getUnfilteredTimelineSet().compareEventOrdering(
existingReceipt.eventId, eventId);
existingReceipt.eventId,
eventId,
);
if (ordering !== null && ordering >= 0) {
return;
}
}
receipts[receiptType][userId] = {
eventId: eventId,
const wrappedReceipt: IWrappedReceipt = {
eventId,
data: receipt,
};
});
});
});
}
/**
* Build and return a map of receipts by event ID
* @param {Object} receipts A map of receipts
* @return {Object} Map of receipts by event ID
*/
private buildReceiptCache(receipts: Receipts): ReceiptCache {
const receiptCacheByEventId: ReceiptCache = {};
Object.keys(receipts).forEach(function(receiptType) {
Object.keys(receipts[receiptType]).forEach(function(userId) {
const receipt = receipts[receiptType][userId];
if (!receiptCacheByEventId[receipt.eventId]) {
receiptCacheByEventId[receipt.eventId] = [];
}
receiptCacheByEventId[receipt.eventId].push({
userId: userId,
type: receiptType,
data: receipt.data,
const realReceipt = synthetic ? pair[ReceiptPairRealIndex] : wrappedReceipt;
const syntheticReceipt = synthetic ? wrappedReceipt : pair[ReceiptPairSyntheticIndex];
let ordering: number | null = null;
if (realReceipt && syntheticReceipt) {
ordering = this.getUnfilteredTimelineSet().compareEventOrdering(
realReceipt.eventId,
syntheticReceipt.eventId,
);
}
const preferSynthetic = ordering === null || ordering < 0;
// we don't bother caching just real receipts by event ID as there's nothing that would read it.
// Take the current cached receipt before we overwrite the pair elements.
const cachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
if (synthetic && preferSynthetic) {
pair[ReceiptPairSyntheticIndex] = wrappedReceipt;
} else if (!synthetic) {
pair[ReceiptPairRealIndex] = wrappedReceipt;
if (!preferSynthetic) {
pair[ReceiptPairSyntheticIndex] = null;
}
}
const newCachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
if (cachedReceipt === newCachedReceipt) return;
// clean up any previous cache entry
if (cachedReceipt && this.receiptCacheByEventId[cachedReceipt.eventId]) {
const previousEventId = cachedReceipt.eventId;
// Remove the receipt we're about to clobber out of existence from the cache
this.receiptCacheByEventId[previousEventId] = (
this.receiptCacheByEventId[previousEventId].filter(r => {
return r.type !== receiptType || r.userId !== userId;
})
);
if (this.receiptCacheByEventId[previousEventId].length < 1) {
delete this.receiptCacheByEventId[previousEventId]; // clean up the cache keys
}
}
// cache the new one
if (!this.receiptCacheByEventId[eventId]) {
this.receiptCacheByEventId[eventId] = [];
}
this.receiptCacheByEventId[eventId].push({
userId: userId,
type: receiptType,
data: receipt,
});
});
});
});
return receiptCacheByEventId;
}
/**
@@ -2084,7 +2233,7 @@ export class Room extends EventEmitter {
// XXX: we could do a deep-comparison to see if the tags have really
// changed - but do we want to bother?
this.emit("Room.tags", event, this);
this.emit(RoomEvent.Tags, event, this);
}
/**
@@ -2099,7 +2248,7 @@ export class Room extends EventEmitter {
}
const lastEvent = this.accountData[event.getType()];
this.accountData[event.getType()] = event;
this.emit("Room.accountData", event, this, lastEvent);
this.emit(RoomEvent.AccountData, event, this, lastEvent);
}
}
@@ -2118,8 +2267,9 @@ export class Room extends EventEmitter {
* message events into the room.
*/
public maySendMessage(): boolean {
return this.getMyMembership() === 'join' &&
this.currentState.maySendEvent(EventType.RoomMessage, this.myUserId);
return this.getMyMembership() === 'join' && (this.client.isRoomEncrypted(this.roomId)
? this.currentState.maySendEvent(EventType.RoomMessageEncrypted, this.myUserId)
: this.currentState.maySendEvent(EventType.RoomMessage, this.myUserId));
}
/**
@@ -2292,6 +2442,161 @@ export class Room extends EventEmitter {
return "Empty room";
}
}
/**
* When we receive a new visibility change event:
*
* - store this visibility change alongside the timeline, in case we
* later need to apply it to an event that we haven't received yet;
* - if we have already received the event whose visibility has changed,
* patch it to reflect the visibility change and inform listeners.
*/
private applyNewVisibilityEvent(event: MatrixEvent): void {
const visibilityChange = event.asVisibilityChange();
if (!visibilityChange) {
// The event is ill-formed.
return;
}
// Ignore visibility change events that are not emitted by moderators.
const userId = event.getSender();
if (!userId) {
return;
}
const isPowerSufficient =
(
EVENT_VISIBILITY_CHANGE_TYPE.name
&& this.currentState.maySendStateEvent(EVENT_VISIBILITY_CHANGE_TYPE.name, userId)
)
|| (
EVENT_VISIBILITY_CHANGE_TYPE.altName
&& this.currentState.maySendStateEvent(EVENT_VISIBILITY_CHANGE_TYPE.altName, userId)
);
if (!isPowerSufficient) {
// Powerlevel is insufficient.
return;
}
// Record this change in visibility.
// If the event is not in our timeline and we only receive it later,
// we may need to apply the visibility change at a later date.
const visibilityEventsOnOriginalEvent = this.visibilityEvents.get(visibilityChange.eventId);
if (visibilityEventsOnOriginalEvent) {
// It would be tempting to simply erase the latest visibility change
// but we need to record all of the changes in case the latest change
// is ever redacted.
//
// In practice, linear scans through `visibilityEvents` should be fast.
// However, to protect against a potential DoS attack, we limit the
// number of iterations in this loop.
let index = visibilityEventsOnOriginalEvent.length - 1;
const min = Math.max(0,
visibilityEventsOnOriginalEvent.length - MAX_NUMBER_OF_VISIBILITY_EVENTS_TO_SCAN_THROUGH);
for (; index >= min; --index) {
const target = visibilityEventsOnOriginalEvent[index];
if (target.getTs() < event.getTs()) {
break;
}
}
if (index === -1) {
visibilityEventsOnOriginalEvent.unshift(event);
} else {
visibilityEventsOnOriginalEvent.splice(index + 1, 0, event);
}
} else {
this.visibilityEvents.set(visibilityChange.eventId, [event]);
}
// Finally, let's check if the event is already in our timeline.
// If so, we need to patch it and inform listeners.
const originalEvent = this.findEventById(visibilityChange.eventId);
if (!originalEvent) {
return;
}
originalEvent.applyVisibilityEvent(visibilityChange);
}
private redactVisibilityChangeEvent(event: MatrixEvent) {
// Sanity checks.
if (!event.isVisibilityEvent) {
throw new Error("expected a visibility change event");
}
const relation = event.getRelation();
const originalEventId = relation.event_id;
const visibilityEventsOnOriginalEvent = this.visibilityEvents.get(originalEventId);
if (!visibilityEventsOnOriginalEvent) {
// No visibility changes on the original event.
// In particular, this change event was not recorded,
// most likely because it was ill-formed.
return;
}
const index = visibilityEventsOnOriginalEvent.findIndex(change => change.getId() === event.getId());
if (index === -1) {
// This change event was not recorded, most likely because
// it was ill-formed.
return;
}
// Remove visibility change.
visibilityEventsOnOriginalEvent.splice(index, 1);
// If we removed the latest visibility change event, propagate changes.
if (index === visibilityEventsOnOriginalEvent.length) {
const originalEvent = this.findEventById(originalEventId);
if (!originalEvent) {
return;
}
if (index === 0) {
// We have just removed the only visibility change event.
this.visibilityEvents.delete(originalEventId);
originalEvent.applyVisibilityEvent();
} else {
const newEvent = visibilityEventsOnOriginalEvent[visibilityEventsOnOriginalEvent.length - 1];
const newVisibility = newEvent.asVisibilityChange();
if (!newVisibility) {
// Event is ill-formed.
// This breaks our invariant.
throw new Error("at this stage, visibility changes should be well-formed");
}
originalEvent.applyVisibilityEvent(newVisibility);
}
}
}
/**
* When we receive an event whose visibility has been altered by
* a (more recent) visibility change event, patch the event in
* place so that clients now not to display it.
*
* @param event Any matrix event. If this event has at least one a
* pending visibility change event, apply the latest visibility
* change event.
*/
private applyPendingVisibilityEvents(event: MatrixEvent): void {
const visibilityEvents = this.visibilityEvents.get(event.getId());
if (!visibilityEvents || visibilityEvents.length == 0) {
// No pending visibility change in store.
return;
}
const visibilityEvent = visibilityEvents[visibilityEvents.length - 1];
const visibilityChange = visibilityEvent.asVisibilityChange();
if (!visibilityChange) {
return;
}
if (visibilityChange.visible) {
// Events are visible by default, no need to apply a visibility change.
// Note that we need to keep the visibility changes in `visibilityEvents`,
// in case we later fetch an older visibility change event that is superseded
// by `visibilityChange`.
}
if (visibilityEvent.getTs() < event.getTs()) {
// Something is wrong, the visibility change cannot happen before the
// event. Presumably an ill-formed event.
return;
}
event.applyVisibilityEvent(visibilityChange);
}
}
/**
@@ -2302,12 +2607,12 @@ function pendingEventsKey(roomId: string): string {
return `mx_pending_events_${roomId}`;
}
/* a map from current event status to a list of allowed next statuses
*/
// a map from current event status to a list of allowed next statuses
const ALLOWED_TRANSITIONS: Record<EventStatus, EventStatus[]> = {
[EventStatus.ENCRYPTING]: [
EventStatus.SENDING,
EventStatus.NOT_SENT,
EventStatus.CANCELLED,
],
[EventStatus.SENDING]: [
EventStatus.ENCRYPTING,
@@ -2460,3 +2765,4 @@ function memberNamesToRoomName(names: string[], count = (names.length + 1)) {
* @param {string} membership The new membership value
* @param {string} prevMembership The previous membership value
*/
+256 -88
View File
@@ -14,118 +14,252 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClient } from "../matrix";
import { ReEmitter } from "../ReEmitter";
import { MatrixEvent } from "./event";
import { EventTimeline } from "./event-timeline";
import { EventTimelineSet } from './event-timeline-set';
import { MatrixClient, RoomEvent } from "../matrix";
import { TypedReEmitter } from "../ReEmitter";
import { RelationType } from "../@types/event";
import { IRelationsRequestOpts } from "../@types/requests";
import { IThreadBundledRelationship, MatrixEvent } from "./event";
import { Direction, EventTimeline } from "./event-timeline";
import { EventTimelineSet, EventTimelineSetHandlerMap } from './event-timeline-set';
import { Room } from './room';
import { TypedEventEmitter } from "./typed-event-emitter";
import { RoomState } from "./room-state";
export enum ThreadEvent {
New = "Thread.new",
Ready = "Thread.ready",
Update = "Thread.update",
NewReply = "Thread.newReply",
ViewThread = "Thred.viewThread",
ViewThread = "Thread.viewThread",
}
type EmittedEvents = Exclude<ThreadEvent, ThreadEvent.New>
| RoomEvent.Timeline
| RoomEvent.TimelineReset;
export type EventHandlerMap = {
[ThreadEvent.Update]: (thread: Thread) => void;
[ThreadEvent.NewReply]: (thread: Thread, event: MatrixEvent) => void;
[ThreadEvent.ViewThread]: () => void;
} & EventTimelineSetHandlerMap;
interface IThreadOpts {
initialEvents?: MatrixEvent[];
room: Room;
client: MatrixClient;
}
/**
* @experimental
*/
export class Thread extends TypedEventEmitter<ThreadEvent> {
/**
* A reference to the event ID at the top of the thread
*/
private root: string;
export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
public static hasServerSideSupport: boolean;
private static serverSupportPromise: Promise<boolean> | null;
/**
* A reference to all the events ID at the bottom of the threads
*/
public readonly timelineSet;
public readonly timelineSet: EventTimelineSet;
private _currentUserParticipated = false;
private reEmitter: ReEmitter;
private reEmitter: TypedReEmitter<EmittedEvents, EventHandlerMap>;
private lastEvent: MatrixEvent;
private replyCount = 0;
public readonly room: Room;
public readonly client: MatrixClient;
public initialEventsFetched = false;
public readonly id: string;
constructor(
events: MatrixEvent[] = [],
public readonly room: Room,
public readonly client: MatrixClient,
public readonly rootEvent: MatrixEvent | undefined,
opts: IThreadOpts,
) {
super();
if (events.length === 0) {
throw new Error("Can't create an empty thread");
}
this.reEmitter = new ReEmitter(this);
this.room = opts.room;
this.client = opts.client;
this.timelineSet = new EventTimelineSet(this.room, {
unstableClientRelationAggregation: true,
timelineSupport: true,
pendingEvents: true,
});
this.reEmitter = new TypedReEmitter(this);
this.reEmitter.reEmit(this.timelineSet, [
"Room.timeline",
"Room.timelineReset",
RoomEvent.Timeline,
RoomEvent.TimelineReset,
]);
events.forEach(event => this.addEvent(event));
if (Thread.hasServerSideSupport === undefined) {
Thread.serverSupportPromise = this.client.doesServerSupportUnstableFeature("org.matrix.msc3440");
Thread.serverSupportPromise.then((serverSupportsThread) => {
Thread.hasServerSideSupport = serverSupportsThread;
}).catch(() => {
Thread.serverSupportPromise = null;
});
}
room.on("Room.localEchoUpdated", this.onEcho);
room.on("Room.timeline", this.onEcho);
// If we weren't able to find the root event, it's probably missing
// and we define the thread ID from one of the thread relation
if (!rootEvent) {
this.id = opts?.initialEvents
?.find(event => event.isThreadRelation)?.relationEventId;
} else {
this.id = rootEvent.getId();
}
this.initialiseThread(this.rootEvent);
opts?.initialEvents?.forEach(event => this.addEvent(event, false));
this.room.on(RoomEvent.LocalEchoUpdated, this.onEcho);
this.room.on(RoomEvent.Timeline, this.onEcho);
}
onEcho = (event: MatrixEvent) => {
private onEcho = (event: MatrixEvent) => {
if (this.timelineSet.eventIdToTimeline(event.getId())) {
this.emit(ThreadEvent.Update, this);
}
};
public get roomState(): RoomState {
return this.room.getLiveTimeline().getState(EventTimeline.FORWARDS);
}
private addEventToTimeline(event: MatrixEvent, toStartOfTimeline: boolean): void {
if (event.getUnsigned().transaction_id) {
const existingEvent = this.room.getEventForTxnId(event.getUnsigned().transaction_id);
if (existingEvent) {
// remote echo of an event we sent earlier
this.room.handleRemoteEcho(event, existingEvent);
return;
}
}
if (!this.findEventById(event.getId())) {
this.timelineSet.addEventToTimeline(
event,
this.liveTimeline,
toStartOfTimeline,
false,
this.roomState,
);
}
}
/**
* Add an event to the thread and updates
* the tail/root references if needed
* Will fire "Thread.update"
* @param event The event to add
* @param {boolean} toStartOfTimeline whether the event is being added
* to the start (and not the end) of the timeline.
*/
public async addEvent(event: MatrixEvent, toStartOfTimeline = false): Promise<void> {
if (this.timelineSet.findEventById(event.getId()) || event.status !== null) {
return;
public async addEvent(event: MatrixEvent, toStartOfTimeline: boolean): Promise<void> {
if (Thread.hasServerSideSupport === undefined) {
await Thread.serverSupportPromise;
}
if (!this.root) {
if (event.isThreadRelation) {
this.root = event.threadRootId;
} else {
this.root = event.getId();
// Add all incoming events to the thread's timeline set when there's no server support
if (!Thread.hasServerSideSupport) {
// all the relevant membership info to hydrate events with a sender
// is held in the main room timeline
// We want to fetch the room state from there and pass it down to this thread
// timeline set to let it reconcile an event with its relevant RoomMember
event.setThread(this);
this.addEventToTimeline(event, toStartOfTimeline);
await this.client.decryptEventIfNeeded(event, {});
}
if (Thread.hasServerSideSupport && this.initialEventsFetched) {
if (event.localTimestamp > this.lastReply().localTimestamp) {
this.addEventToTimeline(event, false);
}
}
// all the relevant membership info to hydrate events with a sender
// is held in the main room timeline
// We want to fetch the room state from there and pass it down to this thread
// timeline set to let it reconcile an event with its relevant RoomMember
const roomState = this.room.getLiveTimeline().getState(EventTimeline.FORWARDS);
event.setThread(this);
this.timelineSet.addEventToTimeline(
event,
this.timelineSet.getLiveTimeline(),
toStartOfTimeline,
false,
roomState,
);
if (!this._currentUserParticipated && event.getSender() === this.client.getUserId()) {
this._currentUserParticipated = true;
}
await this.client.decryptEventIfNeeded(event, {});
this.emit(ThreadEvent.Update, this);
if (event.isThreadRelation) {
this.emit(ThreadEvent.NewReply, this, event);
const isThreadReply = event.getRelation()?.rel_type === RelationType.Thread;
// If no thread support exists we want to count all thread relation
// added as a reply. We can't rely on the bundled relationships count
if (!Thread.hasServerSideSupport && isThreadReply) {
this.replyCount++;
}
// There is a risk that the `localTimestamp` approximation will not be accurate
// when threads are used over federation. That could results in the reply
// count value drifting away from the value returned by the server
if (!this.lastEvent || (isThreadReply && event.localTimestamp > this.replyToEvent.localTimestamp)) {
this.lastEvent = event;
if (this.lastEvent.getId() !== this.id) {
// This counting only works when server side support is enabled
// as we started the counting from the value returned in the
// bundled relationship
if (Thread.hasServerSideSupport) {
this.replyCount++;
}
this.emit(ThreadEvent.NewReply, this, event);
}
}
this.emit(ThreadEvent.Update, this);
}
private initialiseThread(rootEvent: MatrixEvent | undefined): void {
if (Thread.hasServerSideSupport === undefined) {
Thread.serverSupportPromise.then(() => {
this.initialiseThread(rootEvent);
});
return;
}
const bundledRelationship = rootEvent
?.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
if (Thread.hasServerSideSupport && bundledRelationship) {
this.replyCount = bundledRelationship.count;
this._currentUserParticipated = bundledRelationship.current_user_participated;
const event = new MatrixEvent(bundledRelationship.latest_event);
this.setEventMetadata(event);
this.lastEvent = event;
}
}
public async fetchInitialEvents(): Promise<{
originalEvent: MatrixEvent;
events: MatrixEvent[];
nextBatch?: string;
prevBatch?: string;
} | null> {
if (Thread.hasServerSideSupport === undefined) {
await Thread.serverSupportPromise;
}
if (!Thread.hasServerSideSupport) {
this.initialEventsFetched = true;
return null;
}
try {
const response = await this.fetchEvents();
this.initialEventsFetched = true;
return response;
} catch (e) {
return null;
}
}
private setEventMetadata(event: MatrixEvent): void {
EventTimeline.setEventMetadata(event, this.roomState, false);
event.setThread(this);
}
/**
@@ -138,28 +272,17 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
/**
* Return last reply to the thread
*/
public get lastReply(): MatrixEvent {
const threadReplies = this.events
.filter(event => event.isThreadRelation);
return threadReplies[threadReplies.length - 1];
}
/**
* The thread ID, which is the same as the root event ID
*/
public get id(): string {
return this.root;
}
/**
* The thread root event
*/
public get rootEvent(): MatrixEvent {
return this.findEventById(this.root);
public lastReply(matches: (ev: MatrixEvent) => boolean = () => true): MatrixEvent {
for (let i = this.events.length - 1; i >= 0; i--) {
const event = this.events[i];
if (matches(event)) {
return event;
}
}
}
public get roomId(): string {
return this.rootEvent.getRoomId();
return this.room.roomId;
}
/**
@@ -168,28 +291,18 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
* exclude annotations from that number
*/
public get length(): number {
return this.events
.filter(event => event.isThreadRelation)
.length;
return this.replyCount;
}
/**
* A getter for the last event added to the thread
*/
public get replyToEvent(): MatrixEvent {
const events = this.events;
return events[events.length -1];
return this.lastEvent;
}
public get events(): MatrixEvent[] {
return this.timelineSet.getLiveTimeline().getEvents();
}
public merge(thread: Thread): void {
thread.events.forEach(event => {
this.addEvent(event);
});
this.events.forEach(event => event.setThread(this));
return this.liveTimeline.getEvents();
}
public has(eventId: string): boolean {
@@ -199,4 +312,59 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
public get hasCurrentUserParticipated(): boolean {
return this._currentUserParticipated;
}
public get liveTimeline(): EventTimeline {
return this.timelineSet.getLiveTimeline();
}
public async fetchEvents(opts: IRelationsRequestOpts = { limit: 20 }): Promise<{
originalEvent: MatrixEvent;
events: MatrixEvent[];
nextBatch?: string;
prevBatch?: string;
}> {
if (Thread.hasServerSideSupport === undefined) {
await Thread.serverSupportPromise;
}
let {
originalEvent,
events,
prevBatch,
nextBatch,
} = await this.client.relations(
this.room.roomId,
this.id,
RelationType.Thread,
null,
opts,
);
// When there's no nextBatch returned with a `from` request we have reached
// the end of the thread, and therefore want to return an empty one
if (!opts.to && !nextBatch) {
events = [...events, originalEvent];
}
await Promise.all(events.map(event => {
this.setEventMetadata(event);
return this.client.decryptEventIfNeeded(event);
}));
const prependEvents = !opts.direction || opts.direction === Direction.Backward;
this.timelineSet.addEventsToTimeline(
events,
prependEvents,
this.liveTimeline,
prependEvents ? nextBatch : prevBatch,
);
return {
originalEvent,
events,
prevBatch,
nextBatch,
};
}
}
+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];
}
+1
View File
@@ -32,6 +32,7 @@ interface IOpts {
export interface IMinimalEvent {
content: IContent;
type: EventType | string;
unsigned?: IUnsigned;
}
export interface IEphemeral {
+76 -58
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,15 +196,17 @@ export class SyncApi {
timelineSupport,
unstableClientRelationAggregation,
});
client.reEmitter.reEmit(room, ["Room.name", "Room.timeline",
"Room.redaction",
"Room.redactionCancelled",
"Room.receipt", "Room.tags",
"Room.timelineReset",
"Room.localEchoUpdated",
"Room.accountData",
"Room.myMembership",
"Room.replaceEvent",
client.reEmitter.reEmit(room, [
RoomEvent.Name,
RoomEvent.Redaction,
RoomEvent.RedactionCancelled,
RoomEvent.Receipt,
RoomEvent.Tags,
RoomEvent.LocalEchoUpdated,
RoomEvent.AccountData,
RoomEvent.MyMembership,
RoomEvent.Timeline,
RoomEvent.TimelineReset,
]);
this.registerStateListeners(room);
return room;
@@ -213,7 +219,10 @@ export class SyncApi {
public createGroup(groupId: string): Group {
const client = this.client;
const group = new Group(groupId);
client.reEmitter.reEmit(group, ["Group.profile", "Group.myMembership"]);
client.reEmitter.reEmit(group, [
ClientEvent.GroupProfile,
ClientEvent.GroupMyMembership,
]);
client.store.storeGroup(group);
return group;
}
@@ -228,17 +237,19 @@ export class SyncApi {
// to the client now. We need to add a listener for RoomState.members in
// order to hook them correctly. (TODO: find a better way?)
client.reEmitter.reEmit(room.currentState, [
"RoomState.events", "RoomState.members", "RoomState.newMember",
RoomStateEvent.Events,
RoomStateEvent.Members,
RoomStateEvent.NewMember,
RoomStateEvent.Update,
]);
room.currentState.on("RoomState.newMember", function(event, state, member) {
room.currentState.on(RoomStateEvent.NewMember, function(event, state, member) {
member.user = client.getUser(member.userId);
client.reEmitter.reEmit(
member,
[
"RoomMember.name", "RoomMember.typing", "RoomMember.powerLevel",
"RoomMember.membership",
],
);
client.reEmitter.reEmit(member, [
RoomMemberEvent.Name,
RoomMemberEvent.Typing,
RoomMemberEvent.PowerLevel,
RoomMemberEvent.Membership,
]);
});
}
@@ -248,9 +259,9 @@ export class SyncApi {
*/
private deregisterStateListeners(room: Room): void {
// could do with a better way of achieving this.
room.currentState.removeAllListeners("RoomState.events");
room.currentState.removeAllListeners("RoomState.members");
room.currentState.removeAllListeners("RoomState.newMember");
room.currentState.removeAllListeners(RoomStateEvent.Events);
room.currentState.removeAllListeners(RoomStateEvent.Members);
room.currentState.removeAllListeners(RoomStateEvent.NewMember);
}
/**
@@ -283,7 +294,7 @@ export class SyncApi {
leaveRooms = this.mapSyncResponseToRoomArray(data.rooms.leave);
}
const rooms = [];
leaveRooms.forEach((leaveObj) => {
leaveRooms.forEach(async (leaveObj) => {
const room = leaveObj.room;
rooms.push(room);
if (!leaveObj.isBrandNewRoom) {
@@ -309,11 +320,11 @@ export class SyncApi {
EventTimeline.BACKWARDS);
this.processRoomEvents(room, stateEvents, timelineEvents);
this.processThreadEvents(room, threadedEvents);
await this.processThreadEvents(room, threadedEvents, false);
room.recalculate();
client.store.storeRoom(room);
client.emit("Room", room);
client.emit(ClientEvent.Room, room);
this.processEventsForNotifs(room, events);
});
@@ -361,7 +372,7 @@ export class SyncApi {
user.setPresenceEvent(presenceEvent);
client.store.storeUser(user);
}
client.emit("event", presenceEvent);
client.emit(ClientEvent.Event, presenceEvent);
});
}
@@ -387,7 +398,7 @@ export class SyncApi {
response.messages.start);
client.store.storeRoom(this._peekRoom);
client.emit("Room", this._peekRoom);
client.emit(ClientEvent.Room, this._peekRoom);
this.peekPoll(this._peekRoom);
return this._peekRoom;
@@ -444,7 +455,7 @@ export class SyncApi {
user.setPresenceEvent(presenceEvent);
this.client.store.storeUser(user);
}
this.client.emit("event", presenceEvent);
this.client.emit(ClientEvent.Event, presenceEvent);
});
// strip out events which aren't for the given room_id (e.g presence)
@@ -839,7 +850,7 @@ export class SyncApi {
logger.error("Caught /sync error", e.stack || e);
// Emit the exception for client handling
this.client.emit("sync.unexpectedError", e);
this.client.emit(ClientEvent.SyncUnexpectedError, e);
}
// update this as it may have changed
@@ -1072,7 +1083,7 @@ export class SyncApi {
user.setPresenceEvent(presenceEvent);
client.store.storeUser(user);
}
client.emit("event", presenceEvent);
client.emit(ClientEvent.Event, presenceEvent);
});
}
@@ -1095,7 +1106,7 @@ export class SyncApi {
client.pushRules = PushProcessor.rewriteDefaultRules(rules);
}
const prevEvent = prevEventsMap[accountDataEvent.getId()];
client.emit("accountData", accountDataEvent, prevEvent);
client.emit(ClientEvent.AccountData, accountDataEvent, prevEvent);
return accountDataEvent;
},
);
@@ -1148,7 +1159,7 @@ export class SyncApi {
}
}
client.emit("toDeviceEvent", toDeviceEvent);
client.emit(ClientEvent.ToDeviceEvent, toDeviceEvent);
},
);
} else {
@@ -1200,10 +1211,10 @@ export class SyncApi {
if (inviteObj.isBrandNewRoom) {
room.recalculate();
client.store.storeRoom(room);
client.emit("Room", room);
client.emit(ClientEvent.Room, room);
}
stateEvents.forEach(function(e) {
client.emit("event", e);
client.emit(ClientEvent.Event, e);
});
room.updateMyMembership("invite");
});
@@ -1306,7 +1317,7 @@ export class SyncApi {
const [timelineEvents, threadedEvents] = this.client.partitionThreadedEvents(events);
this.processRoomEvents(room, stateEvents, timelineEvents, syncEventData.fromCache);
this.processThreadEvents(room, threadedEvents);
await this.processThreadEvents(room, threadedEvents, false);
// set summary after processing events,
// because it will trigger a name calculation
@@ -1324,13 +1335,13 @@ export class SyncApi {
room.recalculate();
if (joinObj.isBrandNewRoom) {
client.store.storeRoom(room);
client.emit("Room", room);
client.emit(ClientEvent.Room, room);
}
this.processEventsForNotifs(room, events);
const processRoomEvent = async (e) => {
client.emit("event", e);
client.emit(ClientEvent.Event, e);
if (e.isState() && e.getType() == "m.room.encryption" && this.opts.crypto) {
await this.opts.crypto.onCryptoEvent(e);
}
@@ -1350,10 +1361,10 @@ export class SyncApi {
await utils.promiseMapSeries(timelineEvents, processRoomEvent);
await utils.promiseMapSeries(threadedEvents, processRoomEvent);
ephemeralEvents.forEach(function(e) {
client.emit("event", e);
client.emit(ClientEvent.Event, e);
});
accountDataEvents.forEach(function(e) {
client.emit("event", e);
client.emit(ClientEvent.Event, e);
});
room.updateMyMembership("join");
@@ -1365,7 +1376,7 @@ export class SyncApi {
});
// Handle leaves (e.g. kicked rooms)
leaveRooms.forEach((leaveObj) => {
leaveRooms.forEach(async (leaveObj) => {
const room = leaveObj.room;
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
@@ -1374,28 +1385,28 @@ export class SyncApi {
const [timelineEvents, threadedEvents] = this.client.partitionThreadedEvents(events);
this.processRoomEvents(room, stateEvents, timelineEvents);
this.processThreadEvents(room, threadedEvents);
await this.processThreadEvents(room, threadedEvents, false);
room.addAccountData(accountDataEvents);
room.recalculate();
if (leaveObj.isBrandNewRoom) {
client.store.storeRoom(room);
client.emit("Room", room);
client.emit(ClientEvent.Room, room);
}
this.processEventsForNotifs(room, events);
stateEvents.forEach(function(e) {
client.emit("event", e);
client.emit(ClientEvent.Event, e);
});
timelineEvents.forEach(function(e) {
client.emit("event", e);
client.emit(ClientEvent.Event, e);
});
threadedEvents.forEach(function(e) {
client.emit("event", e);
client.emit(ClientEvent.Event, e);
});
accountDataEvents.forEach(function(e) {
client.emit("event", e);
client.emit(ClientEvent.Event, e);
});
room.updateMyMembership("leave");
@@ -1550,7 +1561,7 @@ export class SyncApi {
group.setMyMembership(sectionName);
if (isBrandNew) {
// Now we've filled in all the fields, emit the Group event
this.client.emit("Group", group);
this.client.emit(ClientEvent.Group, group);
}
}
}
@@ -1719,14 +1730,18 @@ export class SyncApi {
/**
* @experimental
*/
private processThreadEvents(room: Room, threadedEvents: MatrixEvent[]): void {
return this.client.processThreadEvents(room, threadedEvents);
private processThreadEvents(
room: Room,
threadedEvents: MatrixEvent[],
toStartOfTimeline: boolean,
): Promise<void> {
return this.client.processThreadEvents(room, threadedEvents, toStartOfTimeline);
}
// extractRelatedEvents(event: MatrixEvent, events: MatrixEvent[], relatedEvents: MatrixEvent[] = []): MatrixEvent[] {
// relatedEvents.push(event);
// const parentEventId = event.parentEventId;
// const parentEventId = event.getAssociatedId();
// const parentEventIndex = events.findIndex(event => event.getId() === parentEventId);
// if (parentEventIndex > -1) {
@@ -1777,7 +1792,7 @@ export class SyncApi {
const old = this.syncState;
this.syncState = newState;
this.syncStateData = data;
this.client.emit("sync", this.syncState, old, data);
this.client.emit(ClientEvent.Sync, this.syncState, old, data);
}
/**
@@ -1795,8 +1810,11 @@ export class SyncApi {
function createNewUser(client: MatrixClient, userId: string): User {
const user = new User(userId);
client.reEmitter.reEmit(user, [
"User.avatarUrl", "User.displayName", "User.presence",
"User.currentlyActive", "User.lastPresenceTs",
UserEvent.AvatarUrl,
UserEvent.DisplayName,
UserEvent.Presence,
UserEvent.CurrentlyActive,
UserEvent.LastPresenceTs,
]);
return user;
}
+1 -1
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>;
}
+106 -35
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);
if (upgradeAudio && upgradeVideo) {
if (this.hasLocalUserMediaAudioTrack) return;
if (this.hasLocalUserMediaVideoTrack) return;
this.pushNewLocalFeed(stream, SDPStreamMetadataPurpose.Usermedia);
} else if (upgradeAudio) {
if (this.hasLocalUserMediaAudioTrack) return;
const audioTrack = stream.getAudioTracks()[0];
this.localUsermediaStream.addTrack(audioTrack);
this.peerConn.addTrack(audioTrack, this.localUsermediaStream);
} else if (upgradeVideo) {
if (this.hasLocalUserMediaVideoTrack) return;
const videoTrack = stream.getVideoTracks()[0];
this.localUsermediaStream.addTrack(videoTrack);
this.peerConn.addTrack(videoTrack, this.localUsermediaStream);
}
// updateLocalUsermediaStream() will take the tracks, use them as
// replacement and throw the stream away, so it isn't reusable
const stream = await this.client.getMediaHandler().getUserMediaStream(getAudio, getVideo, false);
await this.updateLocalUsermediaStream(stream, audio, video);
} catch (error) {
logger.error("Failed to upgrade the call", error);
this.emit(CallEvent.Error,
@@ -1071,6 +1073,63 @@ export class MatrixCall extends EventEmitter {
}
}
/**
* Replaces/adds the tracks from the passed stream to the localUsermediaStream
* @param {MediaStream} stream to use a replacement for the local usermedia stream
*/
public async updateLocalUsermediaStream(
stream: MediaStream, forceAudio = false, forceVideo = false,
): Promise<void> {
const callFeed = this.localUsermediaFeed;
const audioEnabled = forceAudio || (!callFeed.isAudioMuted() && !this.remoteOnHold);
const videoEnabled = forceVideo || (!callFeed.isVideoMuted() && !this.remoteOnHold);
setTracksEnabled(stream.getAudioTracks(), audioEnabled);
setTracksEnabled(stream.getVideoTracks(), videoEnabled);
// We want to keep the same stream id, so we replace the tracks rather than the whole stream
for (const track of this.localUsermediaStream.getTracks()) {
this.localUsermediaStream.removeTrack(track);
track.stop();
}
for (const track of stream.getTracks()) {
this.localUsermediaStream.addTrack(track);
}
const newSenders = [];
for (const track of stream.getTracks()) {
const oldSender = this.usermediaSenders.find((sender) => sender.track?.kind === track.kind);
let newSender: RTCRtpSender;
if (oldSender) {
logger.info(
`Replacing track (` +
`id="${track.id}", ` +
`kind="${track.kind}", ` +
`streamId="${stream.id}", ` +
`streamPurpose="${callFeed.purpose}"` +
`) to peer connection`,
);
await oldSender.replaceTrack(track);
newSender = oldSender;
} else {
logger.info(
`Adding track (` +
`id="${track.id}", ` +
`kind="${track.kind}", ` +
`streamId="${stream.id}", ` +
`streamPurpose="${callFeed.purpose}"` +
`) to peer connection`,
);
newSender = this.peerConn.addTrack(track, this.localUsermediaStream);
}
newSenders.push(newSender);
}
this.usermediaSenders = newSenders;
}
/**
* Set whether our outbound video should be muted or not.
* @param {boolean} muted True to mute the outbound video.
@@ -1199,8 +1258,8 @@ export class MatrixCall extends EventEmitter {
[SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(),
});
const micShouldBeMuted = this.localUsermediaFeed?.isAudioMuted() || this.remoteOnHold;
const vidShouldBeMuted = this.localUsermediaFeed?.isVideoMuted() || this.remoteOnHold;
const micShouldBeMuted = this.isMicrophoneMuted() || this.remoteOnHold;
const vidShouldBeMuted = this.isLocalVideoMuted() || this.remoteOnHold;
setTracksEnabled(this.localUsermediaStream.getAudioTracks(), !micShouldBeMuted);
setTracksEnabled(this.localUsermediaStream.getVideoTracks(), !vidShouldBeMuted);
@@ -1717,7 +1776,12 @@ export class MatrixCall extends EventEmitter {
const stream = ev.streams[0];
this.pushRemoteFeed(stream);
stream.addEventListener("removetrack", () => this.deleteFeedByStream(stream));
stream.addEventListener("removetrack", () => {
if (stream.getTracks().length === 0) {
logger.info(`Stream ID ${stream.id} has no tracks remaining - removing`);
this.deleteFeedByStream(stream);
}
});
};
private onDataChannel = (ev: RTCDataChannelEvent): void => {
@@ -1938,7 +2002,7 @@ export class MatrixCall extends EventEmitter {
await this.sendVoipEvent(EventType.CallReplaces, bodyToTransferee);
await this.terminate(CallParty.Local, CallErrorCode.Replaced, true);
await this.terminate(CallParty.Local, CallErrorCode.Transfered, true);
await transferTargetCall.terminate(CallParty.Local, CallErrorCode.Transfered, true);
}
@@ -1968,12 +2032,12 @@ export class MatrixCall extends EventEmitter {
this.peerConn.close();
}
if (shouldEmit) {
this.emit(CallEvent.Hangup, this);
this.emit(CallEvent.Hangup);
}
}
private stopAllMedia(): void {
logger.debug(`stopAllMedia (stream=${this.localUsermediaStream})`);
logger.debug("Stopping all media for call", this.callId);
for (const feed of this.feeds) {
if (feed.isLocal() && feed.purpose === SDPStreamMetadataPurpose.Usermedia) {
@@ -1981,6 +2045,7 @@ export class MatrixCall extends EventEmitter {
} else if (feed.isLocal() && feed.purpose === SDPStreamMetadataPurpose.Screenshare) {
this.client.getMediaHandler().stopScreensharingStream(feed.stream);
} else {
logger.debug("Stopping remote stream", feed.stream.id);
for (const track of feed.stream.getTracks()) {
track.stop();
}
@@ -1989,7 +2054,7 @@ export class MatrixCall extends EventEmitter {
}
private checkForErrorListener(): void {
if (this.listeners("error").length === 0) {
if (this.listeners(EventEmitterEvents.Error).length === 0) {
throw new Error(
"You MUST attach an error listener using call.on('error', function() {})",
);
@@ -2058,6 +2123,12 @@ export class MatrixCall extends EventEmitter {
try {
const stream = await this.client.getMediaHandler().getUserMediaStream(audio, video);
// make sure all the tracks are enabled (same as pushNewLocalFeed -
// we probably ought to just have one code path for adding streams)
setTracksEnabled(stream.getAudioTracks(), true);
setTracksEnabled(stream.getVideoTracks(), true);
const callFeed = new CallFeed({
client: this.client,
roomId: this.roomId,
+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;
+99 -20
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> {
@@ -54,31 +95,60 @@ export class MediaHandler {
}
/**
* @param audio should have an audio track
* @param video should have a video track
* @param reusable is allowed to be reused by the MediaHandler
* @returns {MediaStream} based on passed parameters
*/
public async getUserMediaStream(audio: boolean, video: boolean): Promise<MediaStream> {
public async getUserMediaStream(audio: boolean, video: boolean, reusable = true): Promise<MediaStream> {
const shouldRequestAudio = audio && await this.hasAudioDevice();
const shouldRequestVideo = video && await this.hasVideoDevice();
let stream: MediaStream;
// Find a stream with matching tracks
const matchingStream = this.userMediaStreams.find((stream) => {
if (shouldRequestAudio !== (stream.getAudioTracks().length > 0)) return false;
if (shouldRequestVideo !== (stream.getVideoTracks().length > 0)) return false;
return true;
});
if (matchingStream) {
logger.log("Cloning user media stream", matchingStream.id);
stream = matchingStream.clone();
} else {
if (
!this.localUserMediaStream ||
(this.localUserMediaStream.getAudioTracks().length === 0 && shouldRequestAudio) ||
(this.localUserMediaStream.getVideoTracks().length === 0 && shouldRequestVideo) ||
(this.localUserMediaStream.getAudioTracks()[0]?.getSettings()?.deviceId !== this.audioInput) ||
(this.localUserMediaStream.getVideoTracks()[0]?.getSettings()?.deviceId !== this.videoInput)
) {
const constraints = this.getUserMediaContraints(shouldRequestAudio, shouldRequestVideo);
logger.log("Getting user media with constraints", constraints);
stream = await navigator.mediaDevices.getUserMedia(constraints);
for (const track of stream.getTracks()) {
const settings = track.getSettings();
if (track.kind === "audio") {
this.audioInput = settings.deviceId;
} else if (track.kind === "video") {
this.videoInput = settings.deviceId;
}
}
if (reusable) {
this.localUserMediaStream = stream;
}
} else {
stream = this.localUserMediaStream.clone();
if (!shouldRequestAudio) {
for (const track of stream.getAudioTracks()) {
stream.removeTrack(track);
}
}
if (!shouldRequestVideo) {
for (const track of stream.getVideoTracks()) {
stream.removeTrack(track);
}
}
}
this.userMediaStreams.push(stream);
if (reusable) {
this.userMediaStreams.push(stream);
}
return stream;
}
@@ -98,12 +168,18 @@ export class MediaHandler {
logger.debug("Splicing usermedia stream out stream array", mediaStream.id);
this.userMediaStreams.splice(index, 1);
}
if (this.localUserMediaStream === mediaStream) {
this.localUserMediaStream = undefined;
}
}
/**
* @param desktopCapturerSourceId sourceId for Electron DesktopCapturer
* @param reusable is allowed to be reused by the MediaHandler
* @returns {MediaStream} based on passed parameters
*/
public async getScreensharingStream(desktopCapturerSourceId: string): Promise<MediaStream | null> {
public async getScreensharingStream(desktopCapturerSourceId: string, reusable = true): Promise<MediaStream | null> {
let stream: MediaStream;
if (this.screensharingStreams.length === 0) {
@@ -125,7 +201,9 @@ export class MediaHandler {
stream = matchingStream.clone();
}
this.screensharingStreams.push(stream);
if (reusable) {
this.screensharingStreams.push(stream);
}
return stream;
}
@@ -165,6 +243,7 @@ export class MediaHandler {
this.userMediaStreams = [];
this.screensharingStreams = [];
this.localUserMediaStream = undefined;
}
private getUserMediaContraints(audio: boolean, video: boolean): MediaStreamConstraints {
+1180 -1270
View File
File diff suppressed because it is too large Load Diff