Compare commits

...

89 Commits

Author SHA1 Message Date
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
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
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
RiotRobot 83d1a6c046 v15.4.0-rc.1 2022-01-11 14:56:46 +00:00
RiotRobot 87393c7db8 Prepare changelog for v15.4.0-rc.1 2022-01-11 14:56:45 +00:00
Germain bd47667e63 Remove getCapabilities call for guest users (#2100) 2022-01-11 12:50:10 +00:00
Germain cfd865bf8b Fetch server capabilities during client initialisation (#2093) 2022-01-11 11:53:30 +00:00
Michael Telatynski 9b54df7b2b Don't consider alt_aliases when calculating room name (#2094) 2022-01-10 13:42:10 -07:00
Michael Telatynski 5da562fa6f Stop encrypting redactions as it isn't spec compliant (#2098) 2022-01-10 17:02:11 +00:00
J. Ryan Stinnett 83d952eae8 Fix spacing errors (#2096) 2022-01-10 12:57:11 +00:00
Hugh Nimmo-Smith f30be87879 Fix more function typings relating to key backup (#2086)
* Fix more function typings relating to key backup

* Use function overloads to specify allowed params
2021-12-23 17:23:31 +00:00
Hugh Nimmo-Smith ab19480cc5 Fix timeline search in MSC3089 getFileEvent() (#2085) 2021-12-23 17:23:19 +00:00
Šimon Brandner 99451698a4 Set a deviceId for VoIP example and use const/let (#2090) 2021-12-23 13:37:55 +00:00
Germain 48dbed8001 Make threads events indexed by the index manager (#2089) 2021-12-23 09:58:11 +00:00
Germain bae883a891 Update filters to reflect MSC3440 (threads) (#2065) 2021-12-22 13:58:21 +00:00
Hugh Nimmo-Smith 2aae2362e3 Fix incorrect TS return type for secret storage and key backup functions (#2082) 2021-12-20 16:52:50 +00:00
RiotRobot f780e1dbc3 Resetting package fields for development 2021-12-20 14:01:19 +00:00
RiotRobot fdc11b5e53 Merge branch 'master' into develop 2021-12-20 14:01:19 +00:00
RiotRobot 1a0bdb0f98 v15.3.0 2021-12-20 13:58:07 +00:00
RiotRobot e870bebd3d Prepare changelog for v15.3.0 2021-12-20 13:58:07 +00:00
Michael Telatynski dd0ca91aa9 Stop using v1-prefixed /hierarchy API due to Synapse bugs (#2080) 2021-12-17 15:33:55 +00:00
Hugh Nimmo-Smith 36906e2ddb Load room history if necessary when searching for MSC3089 getFileEvent() (#2066) 2021-12-16 21:50:33 -07:00
Michael Telatynski 3eaed30446 Iterate typing around sync, usage limits and errors (#2077) 2021-12-16 09:57:07 +00:00
Andy Balaam 72643026a3 Remove now-unused makePollContent from its old location (#2078) 2021-12-15 17:08:21 +00:00
Andy Balaam d6980cb8fa Provide a makeLocationContent to create a location content event (#2076) 2021-12-15 16:32:24 +00:00
Andy Balaam dcd24fd516 Add consts for location and extensible events types (#2074) 2021-12-15 13:49:26 +00:00
Andy Balaam 75cc873d77 Copy the polls constants to matrix-js-sdk from matrix-react-sdk (#2073) 2021-12-15 12:15:38 +00:00
Eric Eastwood dd23a1a401 Add support for MSC3030 /timestamp_to_event (#2072)
- `/jumptodate` slash command is being worked on in https://github.com/matrix-org/matrix-react-sdk/pull/7372
 - Jump to date headers are being worked on in https://github.com/matrix-org/matrix-react-sdk/pull/7339

Related to https://github.com/vector-im/element-web/issues/7677

Part of MSC3030: https://github.com/matrix-org/matrix-doc/pull/3030

Experimental Synapse implementation added in https://github.com/matrix-org/synapse/pull/9445
2021-12-15 05:06:26 -06:00
Michael Telatynski 6ac84a2465 Use v1 prefix for /hierarchy API, falling back to both previous variants (#2022) 2021-12-15 09:56:07 +00:00
Hugh Nimmo-Smith e4d3124e72 Move Room properties JSDoc into more usable comments on actual props (#2067) 2021-12-15 08:57:54 +00:00
Michael Telatynski df11a9e832 Fix sending undefined query args to Synapse (#2070) 2021-12-14 10:38:28 -07:00
David Baker e4703989fe Add git archive learnings to release script comment (#2069) 2021-12-14 15:46:34 +00:00
Michael Telatynski feb83ba161 Convert http-api to Typescript (#2063) 2021-12-14 15:34:50 +00:00
Michael Telatynski 963c7690b6 Iterate typing to work towards noImplicitAny (#2061) 2021-12-14 14:32:35 +00:00
68 changed files with 4318 additions and 2164 deletions
+97
View File
@@ -1,3 +1,100 @@
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)
============================================================================================================
## ✨ 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.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.3.0) (2021-12-20)
==================================================================================================
## ✨ Features
* Improve fallback key behaviour ([\#2037](https://github.com/matrix-org/matrix-js-sdk/pull/2037)).
* Add new room event filter fields ([\#2051](https://github.com/matrix-org/matrix-js-sdk/pull/2051)).
* Add method to fetch /account/whoami ([\#2046](https://github.com/matrix-org/matrix-js-sdk/pull/2046)).
## 🐛 Bug Fixes
* Filter out falsey opts in /relations API hits ([\#2059](https://github.com/matrix-org/matrix-js-sdk/pull/2059)). Fixes vector-im/element-web#20137.
* Fix paginateEventTimeline resolve to boolean ([\#2054](https://github.com/matrix-org/matrix-js-sdk/pull/2054)).
* Fix incorrect MSC3089 typings and add null checks ([\#2049](https://github.com/matrix-org/matrix-js-sdk/pull/2049)).
Changes in [15.3.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.3.0-rc.1) (2021-12-14)
============================================================================================================
+10 -9
View File
@@ -1,16 +1,17 @@
console.log("Loading browser sdk");
var BASE_URL = "https://matrix.org";
var TOKEN = "accesstokengoeshere";
var USER_ID = "@username:localhost";
var ROOM_ID = "!room:id";
const BASE_URL = "https://matrix.org";
const TOKEN = "accesstokengoeshere";
const USER_ID = "@username:localhost";
const ROOM_ID = "!room:id";
const DEVICE_ID = "some_device_id";
var client = matrixcs.createClient({
const client = matrixcs.createClient({
baseUrl: BASE_URL,
accessToken: TOKEN,
userId: USER_ID
userId: USER_ID,
deviceId: DEVICE_ID
});
var call;
let call;
function disableButtons(place, answer, hangup) {
document.getElementById("hangup").disabled = hangup;
@@ -19,7 +20,7 @@ function disableButtons(place, answer, hangup) {
}
function addListeners(call) {
var lastError = "";
let lastError = "";
call.on("hangup", function() {
disableButtons(false, true, true);
document.getElementById("result").innerHTML = (
+6 -4
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "15.3.0-rc.1",
"version": "15.5.2",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepublishOnly": "yarn build",
@@ -15,7 +15,7 @@
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
"gendoc": "jsdoc -c jsdoc.json -P package.json",
"lint": "yarn lint:types && yarn lint:js",
"lint:js": "eslint --max-warnings 4 src spec",
"lint:js": "eslint --max-warnings 0 src spec",
"lint:js-fix": "eslint --fix src spec",
"lint:types": "tsc --noEmit",
"test": "jest",
@@ -55,8 +55,8 @@
"browser-request": "^0.3.3",
"bs58": "^4.0.1",
"content-type": "^1.0.4",
"eslint-plugin-import": "^2.25.2",
"loglevel": "^1.7.1",
"matrix-events-sdk": "^0.0.1-beta.6",
"p-retry": "^4.5.0",
"qs": "^6.9.6",
"request": "^2.88.2",
@@ -77,6 +77,7 @@
"@babel/register": "^7.12.10",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
"@types/bs58": "^4.0.1",
"@types/content-type": "^1.1.5",
"@types/jest": "^26.0.20",
"@types/node": "12",
"@types/request": "^2.48.5",
@@ -90,7 +91,8 @@
"docdash": "^1.2.0",
"eslint": "7.18.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#48ec1e6af2cfb8310b9a6e23edf2dc7a26ddd580",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-matrix-org": "^0.4.0",
"exorcist": "^1.0.1",
"fake-indexeddb": "^3.1.2",
"jest": "^26.6.3",
+6
View File
@@ -255,6 +255,12 @@ if [ -n "$signing_id" ]; then
# the easiest way to check the validity of the tarball from git is to unzip
# it and compare it with our own idea of what the tar should look like.
# This uses git archive which seems to be what github uses. Specifically,
# the header fields are set in the same way: same file mode, uid & gid
# both zero and mtime set to the timestamp of the commit that the tag
# references. Also note that this puts the commit into the tar headers
# and can be extracted with gunzip -c foo.tar.gz | git get-tar-commit-id
# the name of the sig file we want to create
source_sigfile="${tag}-src.tar.gz.asc"
+1
View File
@@ -86,6 +86,7 @@ TestClient.prototype.toString = function() {
*/
TestClient.prototype.start = function() {
logger.log(this + ': starting');
this.httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
this.httpBackend.when("GET", "/pushrules").respond(200, {});
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
this.expectDeviceKeyUpload();
+10 -33
View File
@@ -17,55 +17,34 @@ limitations under the License.
// load XmlHttpRequest mock
import "./setupTests";
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
import MockHttpBackend from "matrix-mock-request";
import { MockStorageApi } from "../MockStorageApi";
import { WebStorageSessionStore } from "../../src/store/session/webstorage";
import { LocalStorageCryptoStore } from "../../src/crypto/store/localStorage-crypto-store";
import * as utils from "../test-utils";
import { TestClient } from "../TestClient";
const USER_ID = "@user:test.server";
const DEVICE_ID = "device_id";
const ACCESS_TOKEN = "access_token";
const ROOM_ID = "!room_id:server.test";
/* global matrixcs */
describe("Browserify Test", function() {
let client;
let httpBackend;
async function createTestClient() {
const sessionStoreBackend = new MockStorageApi();
const sessionStore = new WebStorageSessionStore(sessionStoreBackend);
const httpBackend = new MockHttpBackend();
beforeEach(() => {
const testClient = new TestClient(USER_ID, DEVICE_ID, ACCESS_TOKEN);
const options = {
baseUrl: "http://" + USER_ID + ".test.server",
userId: USER_ID,
accessToken: ACCESS_TOKEN,
deviceId: DEVICE_ID,
sessionStore: sessionStore,
request: httpBackend.requestFn,
cryptoStore: new LocalStorageCryptoStore(sessionStoreBackend),
};
const client = matrixcs.createClient(options);
client = testClient.client;
httpBackend = testClient.httpBackend;
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
return { client, httpBackend };
}
beforeEach(async () => {
({ client, httpBackend } = await createTestClient());
await client.startClient();
client.startClient();
});
afterEach(async () => {
client.stopClient();
await httpBackend.stop();
httpBackend.stop();
});
it("Sync", async function() {
@@ -92,10 +71,8 @@ describe("Browserify Test", function() {
};
httpBackend.when("GET", "/sync").respond(200, syncData);
await Promise.race([
Promise.all([
httpBackend.flushAllExpected(),
]),
return await Promise.race([
httpBackend.flushAllExpected(),
new Promise((_, reject) => {
client.once("sync.unexpectedError", reject);
}),
+2 -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",
});
@@ -722,6 +722,7 @@ describe("MatrixClient crypto", function() {
return Promise.resolve()
.then(() => {
logger.log(aliTestClient + ': starting');
httpBackend.when("GET", "/capabilities").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
aliTestClient.expectDeviceKeyUpload();
@@ -13,6 +13,7 @@ describe("MatrixClient events", function() {
httpBackend = testClient.httpBackend;
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
});
afterEach(function() {
@@ -71,6 +71,7 @@ const EVENTS = [
// start the client, and wait for it to initialise
function startClient(httpBackend, client) {
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA);
@@ -502,7 +503,7 @@ describe("MatrixClient event timelines", function() {
const params = req.queryParams;
expect(params.dir).toEqual("b");
expect(params.from).toEqual("start_token0");
expect(params.limit).toEqual(30);
expect(params.limit).toEqual("30");
}).respond(200, function() {
return {
chunk: [EVENTS[1], EVENTS[2]],
@@ -553,7 +554,7 @@ describe("MatrixClient event timelines", function() {
const params = req.queryParams;
expect(params.dir).toEqual("f");
expect(params.from).toEqual("end_token0");
expect(params.limit).toEqual(20);
expect(params.limit).toEqual("20");
}).respond(200, function() {
return {
chunk: [EVENTS[1], EVENTS[2]],
+488
View File
@@ -1,7 +1,9 @@
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";
import { Thread } from "../../src/models/thread";
describe("MatrixClient", function() {
let client = null;
@@ -392,6 +394,492 @@ 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 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 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 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-thread vote events onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true };
// Events for this test only, because we hack around with them
const eventMessageInThread2 = new MatrixEvent({
"age": 80098509,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
"device_id": "XISFUZSKHH",
"m.relates_to": {
"event_id": "$AAA2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
"m.in_reply_to": {
"event_id": "$AAA2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
},
"rel_type": "io.element.thread",
},
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
},
"event_id": "$AAAhKIGYowtBblVLkRimeIg8TcdjETnxhDPGfi6NpDg",
"origin_server_ts": 1643815466378,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"type": "m.room.encrypted",
"unsigned": { "age": 80098509 },
"user_id": "@andybalaam-test1:matrix.org",
});
const eventPollStartThreadRoot2 = new MatrixEvent({
"age": 80108647,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
"device_id": "XISFUZSKHH",
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
},
"event_id": "$AAA2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
"origin_server_ts": 1643815456240,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"type": "m.room.encrypted",
"unsigned": { "age": 80108647 },
"user_id": "@andybalaam-test1:matrix.org",
});
const eventPollResponseReference2 = new MatrixEvent({
"age": 80098509,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
"device_id": "XISFUZSKHH",
"m.relates_to": {
"event_id": "$AAA2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
"rel_type": "m.reference",
},
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
},
"event_id": "$AAAvpezvsF0cKgav3g8W-uEVS4WkDHgxbJZvL3uMR1g",
"origin_server_ts": 1643815458650,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"type": "m.room.encrypted",
"unsigned": { "age": 80106237 },
"user_id": "@andybalaam-test1:matrix.org",
});
// When we react within a thread, sometimes the thread root
// has isThreadRelation === true, because thread is set on it,
// but threadId is not.
eventPollStartThreadRoot2.setThread(
new Thread(
eventPollStartThreadRoot2,
{
client,
room: new Room(),
},
),
);
const events = [
eventPollResponseReference2,
eventMessageInThread2,
eventPollStartThreadRoot2,
];
const [timeline, threaded] = client.partitionThreadedEvents(events);
expect(timeline).toEqual([
eventPollResponseReference2,
// eventPollStartThreadRoot2,
// This is weird: by hacking the thread root to have an inconsistency
// between thread and threadId (which is what I have observed in the
// wild), we have persuaded the code that the thread root is actually
// within the thread, so it is not provided to the main timeline.
//
// This should go away when we fix this inconsistency. When that
// happens, we should probably delete this test.
]);
expect(threaded).toEqual([
withThreadId(
eventPollResponseReference2, eventPollStartThreadRoot2.getId(),
),
eventMessageInThread2,
eventPollStartThreadRoot2, // See note above for why this appears here.
]);
});
it("copies post-thread in-timeline reactions onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true };
const 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 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 eventMessageInThread = 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 eventPollResponseReference = 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 eventReaction = 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 eventPollStartThreadRoot = 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 eventRoomName = 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 eventEncryption = 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 eventGuestAccess = 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 eventHistoryVisibility = 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 eventJoinRules = 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 eventPowerLevels = 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 eventMember = 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 eventCreate = 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) {
+3 -1
View File
@@ -105,10 +105,12 @@ describe("MatrixClient opts", function() {
expectedEventTypes.indexOf(event.getType()), 1,
);
});
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "foo" });
httpBackend.when("GET", "/sync").respond(200, syncData);
await client.startClient();
client.startClient();
await httpBackend.flush("/capabilities", 1);
await httpBackend.flush("/pushrules", 1);
await httpBackend.flush("/filter", 1);
await Promise.all([
@@ -96,7 +96,7 @@ describe("MatrixClient room timelines", function() {
});
}
beforeEach(function() {
beforeEach(async function() {
// these tests should work with or without timelineSupport
const testClient = new TestClient(
userId,
@@ -109,6 +109,7 @@ describe("MatrixClient room timelines", function() {
client = testClient.client;
setNextSyncData();
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
@@ -116,9 +117,10 @@ describe("MatrixClient room timelines", function() {
return NEXT_SYNC_DATA;
});
client.startClient();
return httpBackend.flush("/pushrules").then(function() {
return httpBackend.flush("/filter");
});
await httpBackend.flush("/capabilities");
await httpBackend.flush("/pushrules");
await httpBackend.flush("/filter");
});
afterEach(function() {
+1
View File
@@ -19,6 +19,7 @@ describe("MatrixClient syncing", function() {
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
httpBackend = testClient.httpBackend;
client = testClient.client;
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
});
+15 -14
View File
@@ -341,8 +341,15 @@ HttpResponse.SYNC_RESPONSE = {
data: HttpResponse.SYNC_DATA,
};
HttpResponse.CAPABILITIES_RESPONSE = {
method: "GET",
path: "/capabilities",
data: { capabilities: {} },
};
HttpResponse.defaultResponses = function(userId) {
return [
HttpResponse.CAPABILITIES_RESPONSE,
HttpResponse.PUSH_RULES_RESPONSE,
HttpResponse.filterResponse(userId),
HttpResponse.SYNC_RESPONSE,
@@ -350,19 +357,13 @@ HttpResponse.defaultResponses = function(userId) {
};
export function setHttpResponses(
client, responses, acceptKeepalives, ignoreUnhandledSyncs,
httpBackend, responses,
) {
const httpResponseObj = new HttpResponse(
responses, acceptKeepalives, ignoreUnhandledSyncs,
);
const httpReq = httpResponseObj.request.bind(httpResponseObj);
client.http = [
"authedRequest", "authedRequestWithPrefix", "getContentUri",
"request", "requestWithPrefix", "uploadContent",
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
client.http.authedRequest.mockImplementation(httpReq);
client.http.authedRequestWithPrefix.mockImplementation(httpReq);
client.http.requestWithPrefix.mockImplementation(httpReq);
client.http.request.mockImplementation(httpReq);
responses.forEach(response => {
httpBackend
.when(response.method, response.path)
.respond(200, response.data);
});
}
export const emitPromise = (e, k) => new Promise(r => e.once(k, r));
+24 -20
View File
@@ -40,13 +40,14 @@ async function makeTestClient(userInfo, options, keys) {
options.cryptoCallbacks = Object.assign(
{}, { getCrossSigningKey, saveCrossSigningKeys }, options.cryptoCallbacks || {},
);
const client = (new TestClient(
const testClient = new TestClient(
userInfo.userId, userInfo.deviceId, undefined, undefined, options,
)).client;
);
const client = testClient.client;
await client.initCrypto();
return client;
return { client, httpBackend: testClient.httpBackend };
}
describe("Cross Signing", function() {
@@ -60,7 +61,7 @@ describe("Cross Signing", function() {
});
it("should sign the master key with the device key", async function() {
const alice = await makeTestClient(
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = jest.fn(async (auth, keys) => {
@@ -80,7 +81,7 @@ describe("Cross Signing", function() {
});
it("should abort bootstrap if device signing auth fails", async function() {
const alice = await makeTestClient(
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async (auth, keys) => {
@@ -131,7 +132,7 @@ describe("Cross Signing", function() {
});
it("should upload a signature when a user is verified", async function() {
const alice = await makeTestClient(
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async () => {};
@@ -161,7 +162,7 @@ describe("Cross Signing", function() {
await promise;
});
it("should get cross-signing keys from sync", async function() {
it.skip("should get cross-signing keys from sync", async function() {
const masterKey = new Uint8Array([
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82,
0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef,
@@ -175,7 +176,7 @@ describe("Cross Signing", function() {
0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f,
]);
const alice = await makeTestClient(
const { client: alice, httpBackend } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
{
cryptoCallbacks: {
@@ -236,6 +237,7 @@ describe("Cross Signing", function() {
// feed sync result that includes master key, ssk, device key
const responses = [
HttpResponse.CAPABILITIES_RESPONSE,
HttpResponse.PUSH_RULES_RESPONSE,
{
method: "POST",
@@ -311,9 +313,10 @@ describe("Cross Signing", function() {
},
},
];
setHttpResponses(alice, responses, true, true);
setHttpResponses(httpBackend, responses);
await alice.startClient();
alice.startClient();
httpBackend.flushAllExpected();
// once ssk is confirmed, device key should be trusted
await keyChangePromise;
@@ -332,7 +335,7 @@ describe("Cross Signing", function() {
});
it("should use trust chain to determine device verification", async function() {
const alice = await makeTestClient(
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async () => {};
@@ -415,9 +418,9 @@ describe("Cross Signing", function() {
expect(bobDeviceTrust2.isTofu()).toBeTruthy();
});
it("should trust signatures received from other devices", async function() {
it.skip("should trust signatures received from other devices", async function() {
const aliceKeys = {};
const alice = await makeTestClient(
const { client: alice, httpBackend } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
null,
aliceKeys,
@@ -491,6 +494,7 @@ describe("Cross Signing", function() {
// - master key signed by her usk (pretend that it was signed by another
// of Alice's devices)
const responses = [
HttpResponse.CAPABILITIES_RESPONSE,
HttpResponse.PUSH_RULES_RESPONSE,
{
method: "POST",
@@ -561,10 +565,10 @@ describe("Cross Signing", function() {
},
},
];
setHttpResponses(alice, responses);
await alice.startClient();
setHttpResponses(httpBackend, responses);
alice.startClient();
httpBackend.flushAllExpected();
await keyChangePromise;
// Bob's device key should be trusted
@@ -579,7 +583,7 @@ describe("Cross Signing", function() {
});
it("should dis-trust an unsigned device", async function() {
const alice = await makeTestClient(
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async () => {};
@@ -648,7 +652,7 @@ describe("Cross Signing", function() {
});
it("should dis-trust a user when their ssk changes", async function() {
const alice = await makeTestClient(
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async () => {};
@@ -786,7 +790,7 @@ describe("Cross Signing", function() {
it("should offer to upgrade device verifications to cross-signing", async function() {
let upgradeResolveFunc;
const alice = await makeTestClient(
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
{
cryptoCallbacks: {
@@ -798,7 +802,7 @@ describe("Cross Signing", function() {
},
},
);
const bob = await makeTestClient(
const { client: bob } = await makeTestClient(
{ userId: "@bob:example.com", deviceId: "Dynabook" },
);
+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,
ASSET_TYPE_SELF,
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: ASSET_TYPE_SELF });
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual("txt");
expect(TIMESTAMP_NODE_TYPE.findIn(loc)).toEqual(134235435);
});
it("should create a valid location with explicit properties", function() {
const loc = makeLocationContent(
"txxt", "geo:bar", 134235436, "desc", "m.something");
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: "m.something" });
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual("txxt");
expect(TIMESTAMP_NODE_TYPE.findIn(loc)).toEqual(134235436);
});
});
+225 -5
View File
@@ -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();
@@ -52,6 +53,12 @@ describe("MatrixClient", function() {
data: SYNC_DATA,
};
const CAPABILITIES_RESPONSE = {
method: "GET",
path: "/capabilities",
data: { capabilities: {} },
};
let httpLookups = [
// items are objects which look like:
// {
@@ -167,6 +174,7 @@ describe("MatrixClient", function() {
acceptKeepalives = true;
pendingLookup = null;
httpLookups = [];
httpLookups.push(CAPABILITIES_RESPONSE);
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
@@ -364,9 +372,11 @@ describe("MatrixClient", function() {
});
it("should not POST /filter if a matching filter already exists", async function() {
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
httpLookups = [
CAPABILITIES_RESPONSE,
PUSH_RULES_RESPONSE,
SYNC_RESPONSE,
];
const filterId = "ehfewf";
store.getFilterIdByName.mockReturnValue(filterId);
const filter = new Filter(0, filterId);
@@ -447,12 +457,15 @@ describe("MatrixClient", function() {
describe("retryImmediately", function() {
it("should return false if there is no request waiting", async function() {
httpLookups = [];
httpLookups.push(CAPABILITIES_RESPONSE);
await client.startClient();
expect(client.retryImmediately()).toBe(false);
});
it("should work on /filter", function(done) {
httpLookups = [];
httpLookups.push(CAPABILITIES_RESPONSE);
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push({
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" },
@@ -503,6 +516,7 @@ describe("MatrixClient", function() {
it("should work on /pushrules", function(done) {
httpLookups = [];
httpLookups.push(CAPABILITIES_RESPONSE);
httpLookups.push({
method: "GET", path: "/pushrules/", error: { errcode: "NOPE_NOPE_NOPE" },
});
@@ -559,6 +573,7 @@ describe("MatrixClient", function() {
it("should transition null -> ERROR after a failed /filter", function(done) {
const expectedStates = [];
httpLookups = [];
httpLookups.push(CAPABILITIES_RESPONSE);
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push({
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" },
@@ -573,6 +588,7 @@ describe("MatrixClient", function() {
const expectedStates = [];
acceptKeepalives = false;
httpLookups = [];
httpLookups.push(CAPABILITIES_RESPONSE);
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
httpLookups.push({
@@ -697,7 +713,8 @@ describe("MatrixClient", function() {
describe("guest rooms", function() {
it("should only do /sync calls (without filter/pushrules)", function(done) {
httpLookups = []; // no /pushrules or /filter
httpLookups = []; // no /pushrules or /filterw
httpLookups.push(CAPABILITIES_RESPONSE);
httpLookups.push({
method: "GET",
path: "/sync",
@@ -728,4 +745,207 @@ describe("MatrixClient", function() {
expect(httpLookups.length).toEqual(0);
});
});
describe("sendEvent", () => {
const roomId = "!room:example.org";
const body = "This is the body";
const content = { body };
it("overload without threadId works", async () => {
const eventId = "$eventId:example.org";
const txnId = client.makeTxnId();
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
data: { event_id: eventId },
expectBody: content,
}];
await client.sendEvent(roomId, EventType.RoomMessage, content, txnId);
});
it("overload with null threadId works", async () => {
const eventId = "$eventId:example.org";
const txnId = client.makeTxnId();
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
data: { event_id: eventId },
expectBody: content,
}];
await client.sendEvent(roomId, null, EventType.RoomMessage, content, txnId);
});
it("overload with threadId works", async () => {
const eventId = "$eventId:example.org";
const txnId = client.makeTxnId();
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
data: { event_id: eventId },
expectBody: content,
}];
await client.sendEvent(roomId, "$threadId:server", EventType.RoomMessage, content, txnId);
});
});
describe("redactEvent", () => {
const roomId = "!room:example.org";
const mockRoom = {
getMyMembership: () => "join",
currentState: {
getStateEvents: (eventType, stateKey) => {
if (eventType === EventType.RoomEncryption) {
expect(stateKey).toEqual("");
return new MatrixEvent({ content: {} });
} else {
throw new Error("Unexpected event type or state key");
}
},
},
threads: {
get: jest.fn(),
},
addPendingEvent: jest.fn(),
updatePendingEvent: jest.fn(),
};
beforeEach(() => {
client.getRoom = (getRoomId) => {
expect(getRoomId).toEqual(roomId);
return mockRoom;
};
});
it("overload without threadId works", async () => {
const eventId = "$eventId:example.org";
const txnId = client.makeTxnId();
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${txnId}`,
data: { event_id: eventId },
}];
await client.redactEvent(roomId, eventId, txnId);
});
it("overload with null threadId works", async () => {
const eventId = "$eventId:example.org";
const txnId = client.makeTxnId();
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${txnId}`,
data: { event_id: eventId },
}];
await client.redactEvent(roomId, null, eventId, txnId);
});
it("overload with threadId works", async () => {
const eventId = "$eventId:example.org";
const txnId = client.makeTxnId();
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${txnId}`,
data: { event_id: eventId },
}];
await client.redactEvent(roomId, "$threadId:server", eventId, txnId);
});
it("does not get wrongly encrypted", async () => {
const eventId = "$eventId:example.org";
const txnId = client.makeTxnId();
const reason = "This is the redaction reason";
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${txnId}`,
expectBody: { reason }, // NOT ENCRYPTED
data: { event_id: eventId },
}];
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);
});
});
});
+1 -1
View File
@@ -312,7 +312,7 @@ describe("MSC3089Branch", () => {
} as MatrixEvent);
const events = [await branch.getFileEvent(), await branch2.getFileEvent(), {
replacingEventId: () => null,
replacingEventId: (): string => null,
getId: () => "$unknown",
}];
staticRoom.getLiveTimeline = () => ({ getEvents: () => events }) as EventTimeline;
+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",
});
});
+61 -3
View File
@@ -622,6 +622,11 @@ describe("Room", function() {
}, event: true,
})]);
};
const setAlias = function(alias) {
room.addLiveEvents([utils.mkEvent({
type: "m.room.canonical_alias", room: roomId, skey: "", content: { alias }, event: true,
})]);
};
const setRoomName = function(name) {
room.addLiveEvents([utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, content: {
@@ -857,7 +862,7 @@ describe("Room", function() {
"(invite join_rules) rooms if a room name doesn't exist.", function() {
const alias = "#room_alias:here";
setJoinRule("invite");
setAltAliases([alias, "#another:here"]);
setAlias(alias);
room.recalculate();
const name = room.name;
expect(name).toEqual(alias);
@@ -867,12 +872,20 @@ describe("Room", function() {
"(public join_rules) rooms if a room name doesn't exist.", function() {
const alias = "#room_alias:here";
setJoinRule("public");
setAltAliases([alias, "#another:here"]);
setAlias(alias);
room.recalculate();
const name = room.name;
expect(name).toEqual(alias);
});
it("should not show alt aliases if a room name does not exist", () => {
const alias = "#room_alias:here";
setAltAliases([alias, "#another:here"]);
room.recalculate();
const name = room.name;
expect(name).not.toEqual(alias);
});
it("should show the room name if one exists for private " +
"(invite join_rules) rooms.", function() {
const roomName = "A mighty name indeed";
@@ -1130,6 +1143,51 @@ describe("Room", function() {
]));
expect(room.getEventReadUpTo(userB)).toEqual(events[2].getId());
});
it("should prioritise the most recent event even if it is synthetic", () => {
const events = [
utils.mkMessage({
room: roomId, user: userA, msg: "1111",
event: true,
}),
utils.mkMessage({
room: roomId, user: userA, msg: "2222",
event: true,
}),
utils.mkMessage({
room: roomId, user: userA, msg: "3333",
event: true,
}),
];
room.addLiveEvents(events);
const ts = 13787898424;
// check it initialises correctly
room.addReceipt(mkReceipt(roomId, [
mkRecord(events[0].getId(), "m.read", userB, ts),
]));
expect(room.getEventReadUpTo(userB)).toEqual(events[0].getId());
// 2>0, so it should move forward
room.addReceipt(mkReceipt(roomId, [
mkRecord(events[2].getId(), "m.read", userB, ts),
]), true);
expect(room.getEventReadUpTo(userB)).toEqual(events[2].getId());
expect(room.getReceiptsForEvent(events[2])).toEqual([
{ data: { ts }, type: "m.read", userId: userB },
]);
// 1<2, so it should stay put
room.addReceipt(mkReceipt(roomId, [
mkRecord(events[1].getId(), "m.read", userB, ts),
]));
expect(room.getEventReadUpTo(userB)).toEqual(events[2].getId());
expect(room.getEventReadUpTo(userB, true)).toEqual(events[1].getId());
expect(room.getReceiptsForEvent(events[2])).toEqual([
{ data: { ts }, type: "m.read", userId: userB },
]);
});
});
describe("getUsersReadUpTo", function() {
@@ -1448,7 +1506,7 @@ describe("Room", function() {
describe("maySendMessage", function() {
it("should return false if synced membership not join",
function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, { isRoomEncrypted: () => false }, userA);
room.updateMyMembership("invite");
expect(room.maySendMessage()).toEqual(false);
room.updateMyMembership("leave");
+10
View File
@@ -177,6 +177,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;
@@ -14,13 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// TODO: Merge this with sync.js once converted
// Types for MSC1767: Extensible events in Matrix
export enum SyncState {
Error = "ERROR",
Prepared = "PREPARED",
Stopped = "STOPPED",
Syncing = "SYNCING",
Catchup = "CATCHUP",
Reconnecting = "RECONNECTING",
}
import { UnstableValue } from "../NamespacedValue";
export const TEXT_NODE_TYPE = new UnstableValue("m.text", "org.matrix.msc1767.text");
-6
View File
@@ -62,12 +62,6 @@ declare global {
};
}
interface HTMLAudioElement {
// sinkId & setSinkId are experimental and typescript doesn't know about them
sinkId: string;
setSinkId(outputId: string);
}
interface DummyInterfaceWeShouldntBeUsingThis {}
interface Navigator {
+68
View File
@@ -0,0 +1,68 @@
/*
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.
*/
// Types for MSC3488 - m.location: Extending events with location data
import { UnstableValue } from "../NamespacedValue";
import { IContent } from "../models/event";
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 const ASSET_TYPE_SELF = "m.self";
/* From the spec at:
* https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md
{
"type": "m.room.message",
"content": {
"body": "Matthew was at geo:51.5008,0.1247;u=35 as of Sat Nov 13 18:50:58 2021",
"msgtype": "m.location",
"geo_uri": "geo:51.5008,0.1247;u=35",
"m.location": {
"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,
}
}
*/
/* eslint-disable camelcase */
export interface ILocationContent extends IContent {
body: string;
msgtype: string;
geo_uri: string;
[LOCATION_EVENT_TYPE.name]: {
uri: string;
description?: string;
};
[ASSET_NODE_TYPE.name]: {
type: string;
};
[TEXT_NODE_TYPE.name]: string;
[TIMESTAMP_NODE_TYPE.name]: number;
}
/* eslint-enable camelcase */
+9
View File
@@ -82,3 +82,12 @@ export enum HistoryVisibility {
Shared = "shared",
WorldReadable = "world_readable",
}
export interface IUsageLimit {
// "hs_disabled" is NOT a specced string, but is used in Synapse
// This is tracked over at https://github.com/matrix-org/synapse/issues/9237
// eslint-disable-next-line camelcase
limit_type: "monthly_active_user" | "hs_disabled" | string;
// eslint-disable-next-line camelcase
admin_contact?: 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 {
-1
View File
@@ -44,7 +44,6 @@ export interface ISpaceSummaryEvent {
}
export interface IHierarchyRelation extends IStrippedState {
room_id: string;
origin_server_ts: number;
content: {
order?: string;
+546 -278
View File
File diff suppressed because it is too large Load Diff
+42 -2
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.
@@ -18,6 +17,14 @@ limitations under the License.
/** @module ContentHelpers */
import { MsgType } from "./@types/event";
import { TEXT_NODE_TYPE } from "./@types/extensible_events";
import {
ASSET_NODE_TYPE,
ASSET_TYPE_SELF,
ILocationContent,
LOCATION_EVENT_TYPE,
TIMESTAMP_NODE_TYPE,
} from "./@types/location";
/**
* Generates the content for a HTML Message event
@@ -99,3 +106,36 @@ export function makeEmoteMessage(body: string) {
body: body,
};
}
/**
* Generates the content for a Location event
* @param text a text for of our location
* @param uri a geo:// uri for the location
* @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?: string,
): ILocationContent {
return {
"body": text,
"msgtype": MsgType.Location,
"geo_uri": uri,
[LOCATION_EVENT_TYPE.name]: {
uri,
description,
},
[ASSET_NODE_TYPE.name]: {
type: assetType ?? ASSET_TYPE_SELF,
},
[TEXT_NODE_TYPE.name]: text,
[TIMESTAMP_NODE_TYPE.name]: ts,
// TODO: MSC1767 fallbacks m.image thumbnail
};
}
+2 -1
View File
@@ -33,6 +33,7 @@ import { OlmDevice } from "./OlmDevice";
import { ICryptoCallbacks } from "../matrix";
import { ISignatures } from "../@types/signed";
import { CryptoStore } from "./store/base";
import { ISecretStorageKeyInfo } from "./api";
const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;
@@ -175,7 +176,7 @@ export class CrossSigningInfo extends EventEmitter {
// check what SSSS keys have encrypted the master key (if any)
const stored = await secretStorage.isStored("m.cross_signing.master", false) || {};
// then check which of those SSSS keys have also encrypted the SSK and USK
function intersect(s) {
function intersect(s: Record<string, ISecretStorageKeyInfo>) {
for (const k of Object.keys(stored)) {
if (!s[k]) {
delete stored[k];
+18 -10
View File
@@ -28,7 +28,7 @@ import { CrossSigningInfo, ICrossSigningInfo } from './CrossSigning';
import * as olmlib from './olmlib';
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { chunkPromises, defer, IDeferred, sleep } from '../utils';
import { MatrixClient } from "../client";
import { IDownloadKeyResult, MatrixClient } from "../client";
import { OlmDevice } from "./OlmDevice";
import { CryptoStore } from "./store/base";
@@ -756,17 +756,21 @@ class DeviceListUpdateSerialiser {
opts.token = this.syncToken;
}
const factories = [];
const factories: Array<() => Promise<IDownloadKeyResult>> = [];
for (let i = 0; i < downloadUsers.length; i += this.deviceList.keyDownloadChunkSize) {
const userSlice = downloadUsers.slice(i, i + this.deviceList.keyDownloadChunkSize);
factories.push(() => this.baseApis.downloadKeysForUsers(userSlice, opts));
}
chunkPromises(factories, 3).then(async (responses: any[]) => {
const dk = Object.assign({}, ...(responses.map(res => res.device_keys || {})));
const masterKeys = Object.assign({}, ...(responses.map(res => res.master_keys || {})));
const ssks = Object.assign({}, ...(responses.map(res => res.self_signing_keys || {})));
const usks = Object.assign({}, ...(responses.map(res => res.user_signing_keys || {})));
chunkPromises(factories, 3).then(async (responses: IDownloadKeyResult[]) => {
const dk: IDownloadKeyResult["device_keys"]
= Object.assign({}, ...(responses.map(res => res.device_keys || {})));
const masterKeys: IDownloadKeyResult["master_keys"]
= Object.assign({}, ...(responses.map(res => res.master_keys || {})));
const ssks: IDownloadKeyResult["self_signing_keys"]
= Object.assign({}, ...(responses.map(res => res.self_signing_keys || {})));
const usks: IDownloadKeyResult["user_signing_keys"]
= Object.assign({}, ...(responses.map(res => res.user_signing_keys || {})));
// yield to other things that want to execute in between users, to
// avoid wedging the CPU
@@ -811,8 +815,12 @@ class DeviceListUpdateSerialiser {
private async processQueryResponseForUser(
userId: string,
dkResponse: object,
crossSigningResponse: any, // TODO types
dkResponse: IDownloadKeyResult["device_keys"]["user_id"],
crossSigningResponse: {
master: IDownloadKeyResult["master_keys"]["user_id"];
self_signing: IDownloadKeyResult["master_keys"]["user_id"]; // eslint-disable-line camelcase
user_signing: IDownloadKeyResult["user_signing_keys"]["user_id"]; // eslint-disable-line camelcase
},
): Promise<void> {
logger.log('got device keys for ' + userId + ':', dkResponse);
logger.log('got cross-signing keys for ' + userId + ':', crossSigningResponse);
@@ -869,7 +877,7 @@ async function updateStoredDeviceKeysForUser(
olmDevice: OlmDevice,
userId: string,
userStore: Record<string, DeviceInfo>,
userResult: object,
userResult: IDownloadKeyResult["device_keys"]["user_id"],
localUserId: string,
localDeviceId: string,
): Promise<boolean> {
+4 -10
View File
@@ -20,15 +20,9 @@ import { logger } from "../logger";
import { MatrixEvent } from "../models/event";
import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning";
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { PREFIX_UNSTABLE } from "../http-api";
import { Method, PREFIX_UNSTABLE } from "../http-api";
import { Crypto, IBootstrapCrossSigningOpts } from "./index";
import {
CrossSigningKeys,
ICrossSigningKey,
ICryptoCallbacks,
ISignedKey,
KeySignatures,
} from "../matrix";
import { CrossSigningKeys, ICrossSigningKey, ICryptoCallbacks, ISignedKey, KeySignatures } from "../matrix";
import { ISecretStorageKeyInfo } from "./api";
import { IKeyBackupInfo } from "./keybackup";
@@ -239,7 +233,7 @@ export class EncryptionSetupOperation {
// Sign the backup with the cross signing key so the key backup can
// be trusted via cross-signing.
await baseApis.http.authedRequest(
undefined, "PUT", "/room_keys/version/" + this.keyBackupInfo.version,
undefined, Method.Put, "/room_keys/version/" + this.keyBackupInfo.version,
undefined, {
algorithm: this.keyBackupInfo.algorithm,
auth_data: this.keyBackupInfo.auth_data,
@@ -249,7 +243,7 @@ export class EncryptionSetupOperation {
} else {
// add new key backup
await baseApis.http.authedRequest(
undefined, "POST", "/room_keys/version",
undefined, Method.Post, "/room_keys/version",
undefined, this.keyBackupInfo,
{ prefix: PREFIX_UNSTABLE },
);
+2 -2
View File
@@ -83,7 +83,7 @@ export class SecretStorage {
private readonly baseApis?: MatrixClient,
) {}
public async getDefaultKeyId(): Promise<string> {
public async getDefaultKeyId(): Promise<string | null> {
const defaultKey = await this.accountDataAdapter.getAccountDataFromServer<{ key: string }>(
'm.secret_storage.default_key',
);
@@ -345,7 +345,7 @@ export class SecretStorage {
* with, or null if it is not present or not encrypted with a trusted
* key
*/
public async isStored(name: string, checkKey: boolean): Promise<Record<string, ISecretStorageKeyInfo>> {
public async isStored(name: string, checkKey: boolean): Promise<Record<string, ISecretStorageKeyInfo> | null> {
// check if secret exists
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name);
if (!secretInfo) return null;
+2 -2
View File
@@ -1114,8 +1114,8 @@ class MegolmEncryption extends EncryptionAlgorithm {
private checkForUnknownDevices(devicesInRoom: DeviceInfoMap): void {
const unknownDevices: Record<string, Record<string, DeviceInfo>> = {};
Object.keys(devicesInRoom).forEach((userId)=>{
Object.keys(devicesInRoom[userId]).forEach((deviceId)=>{
Object.keys(devicesInRoom).forEach((userId) => {
Object.keys(devicesInRoom[userId]).forEach((deviceId) => {
const device = devicesInRoom[userId][deviceId];
if (device.isUnverified() && !device.isKnown()) {
if (!unknownDevices[userId]) {
+8 -10
View File
@@ -33,6 +33,7 @@ import { encryptAES, decryptAES, calculateKeyCheck } from './aes';
import { getCrypto } from '../utils';
import { ICurve25519AuthData, IAes256AuthData, IKeyBackupInfo, IKeyBackupSession } from "./keybackup";
import { UnstableValue } from "../NamespacedValue";
import { IMegolmSessionData } from "./index";
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
@@ -87,7 +88,7 @@ interface BackupAlgorithmClass {
interface BackupAlgorithm {
untrusted: boolean;
encryptSession(data: Record<string, any>): Promise<any>;
decryptSessions(ciphertexts: Record<string, IKeyBackupSession>): Promise<Record<string, any>[]>;
decryptSessions(ciphertexts: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]>;
authData: AuthData;
keyMatches(key: ArrayLike<number>): Promise<boolean>;
free(): void;
@@ -185,7 +186,6 @@ export class BackupManager {
public async prepareKeyBackupVersion(
key?: string | Uint8Array | null,
algorithm?: string | undefined,
// eslint-disable-next-line camelcase
): Promise<IPreparedKeyBackupVersion> {
const Algorithm = algorithm ? algorithmsByName[algorithm] : DefaultAlgorithm;
if (!Algorithm) {
@@ -300,7 +300,7 @@ export class BackupManager {
const ret = {
usable: false,
trusted_locally: false,
sigs: [],
sigs: [] as SigInfo[],
};
if (
@@ -320,7 +320,7 @@ export class BackupManager {
ret.trusted_locally = true;
}
const mySigs = backupInfo.auth_data.signatures[this.baseApis.getUserId()] || [];
const mySigs = backupInfo.auth_data.signatures[this.baseApis.getUserId()] || {};
for (const keyId of Object.keys(mySigs)) {
const keyIdParts = keyId.split(':');
@@ -645,9 +645,7 @@ export class Curve25519 implements BackupAlgorithm {
return this.publicKey.encrypt(JSON.stringify(plainText));
}
public async decryptSessions(
sessions: Record<string, IKeyBackupSession>,
): Promise<Record<string, any>[]> {
public async decryptSessions(sessions: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]> {
const privKey = await this.getKey();
const decryption = new global.Olm.PkDecryption();
try {
@@ -658,7 +656,7 @@ export class Curve25519 implements BackupAlgorithm {
throw { errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY };
}
const keys = [];
const keys: IMegolmSessionData[] = [];
for (const [sessionId, sessionData] of Object.entries(sessions)) {
try {
@@ -777,8 +775,8 @@ export class Aes256 implements BackupAlgorithm {
return await encryptAES(JSON.stringify(plainText), this.key, data.session_id);
}
async decryptSessions(sessions: Record<string, IKeyBackupSession>): Promise<Record<string, any>[]> {
const keys = [];
async decryptSessions(sessions: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]> {
const keys: IMegolmSessionData[] = [];
for (const [sessionId, sessionData] of Object.entries(sessions)) {
try {
+9 -9
View File
@@ -22,9 +22,8 @@ import { decryptAES, encryptAES } from './aes';
import { logger } from '../logger';
import { ISecretStorageKeyInfo } from "./api";
import { Crypto } from "./index";
// FIXME: these types should eventually go in a different file
type Signatures = Record<string, Record<string, string>>;
import { Method } from "../http-api";
import { ISignatures } from "../@types/signed";
export interface IDehydratedDevice {
device_id: string; // eslint-disable-line camelcase
@@ -43,13 +42,13 @@ export interface IDeviceKeys {
device_id: string; // eslint-disable-line camelcase
user_id: string; // eslint-disable-line camelcase
keys: Record<string, string>;
signatures?: Signatures;
signatures?: ISignatures;
}
export interface IOneTimeKey {
key: string;
fallback?: boolean;
signatures?: Signatures;
signatures?: ISignatures;
}
export const DEHYDRATION_ALGORITHM = "org.matrix.msc2697.v1.olm.libolm_pickle";
@@ -207,9 +206,10 @@ export class DehydrationManager {
}
logger.log("Uploading account to server");
const dehydrateResult = await this.crypto.baseApis.http.authedRequest(
// eslint-disable-next-line camelcase
const dehydrateResult = await this.crypto.baseApis.http.authedRequest<{ device_id: string }>(
undefined,
"PUT",
Method.Put,
"/dehydrated_device",
undefined,
{
@@ -244,7 +244,7 @@ export class DehydrationManager {
}
logger.log("Preparing one-time keys");
const oneTimeKeys = {};
const oneTimeKeys: Record<string, IOneTimeKey> = {};
for (const [keyId, key] of Object.entries(otks.curve25519)) {
const k: IOneTimeKey = { key };
const signature = account.sign(anotherjson.stringify(k));
@@ -272,7 +272,7 @@ export class DehydrationManager {
logger.log("Uploading keys to server");
await this.crypto.baseApis.http.authedRequest(
undefined,
"POST",
Method.Post,
"/keys/upload/" + encodeURI(deviceId),
undefined,
{
+32 -46
View File
@@ -179,6 +179,13 @@ export interface IEventDecryptionResult {
untrusted?: boolean;
}
export interface IRequestsMap {
getRequest(event: MatrixEvent): VerificationRequest;
getRequestByChannel(channel: IVerificationChannel): VerificationRequest;
setRequest(event: MatrixEvent, request: VerificationRequest): void;
setRequestByChannel(channel: IVerificationChannel, request: VerificationRequest): void;
}
export class Crypto extends EventEmitter {
/**
* @return {string} The version of Olm.
@@ -808,7 +815,7 @@ export class Crypto extends EventEmitter {
}
};
const signKeyBackupWithCrossSigning = async (keyBackupAuthData) => {
const signKeyBackupWithCrossSigning = async (keyBackupAuthData: IKeyBackupInfo["auth_data"]) => {
if (
this.crossSigningInfo.getId() &&
await this.crossSigningInfo.isStoredInKeyCache("master")
@@ -1017,7 +1024,7 @@ export class Crypto extends EventEmitter {
public isSecretStored(
name: string,
checkKey?: boolean,
): Promise<Record<string, ISecretStorageKeyInfo>> {
): Promise<Record<string, ISecretStorageKeyInfo> | null> {
return this.secretStorage.isStored(name, checkKey);
}
@@ -1028,7 +1035,7 @@ export class Crypto extends EventEmitter {
return this.secretStorage.request(name, devices);
}
public getDefaultSecretStorageKeyId(): Promise<string> {
public getDefaultSecretStorageKeyId(): Promise<string | null> {
return this.secretStorage.getDefaultKeyId();
}
@@ -1148,7 +1155,7 @@ export class Crypto extends EventEmitter {
const signedDevice = await this.crossSigningInfo.signDevice(this.userId, device);
logger.info(`Starting background key sig upload for ${this.deviceId}`);
const upload = ({ shouldEmit }) => {
const upload = ({ shouldEmit = false }) => {
return this.baseApis.uploadKeySignatures({
[this.userId]: {
[this.deviceId]: signedDevice,
@@ -1184,7 +1191,7 @@ export class Crypto extends EventEmitter {
// Check all users for signatures if upgrade callback present
// FIXME: do this in batches
const users = {};
const users: Record<string, IDeviceVerificationUpgrade> = {};
for (const [userId, crossSigningInfo]
of Object.entries(this.deviceList.crossSigningInfo)) {
const upgradeInfo = await this.checkForDeviceVerificationUpgrade(
@@ -1482,7 +1489,7 @@ export class Crypto extends EventEmitter {
!crossSigningPrivateKeys.has("user_signing")
);
const keySignatures = {};
const keySignatures: Record<string, ISignedKey> = {};
if (selfSigningChanged) {
logger.info("Got new self-signing key", newCrossSigning.getId("self_signing"));
@@ -1537,7 +1544,7 @@ export class Crypto extends EventEmitter {
// We may have existing signatures from deleted devices, which will cause
// the entire upload to fail.
keySignatures[this.crossSigningInfo.getId()] = Object.assign(
{},
{} as ISignedKey,
masterKey,
{
signatures: {
@@ -1551,7 +1558,7 @@ export class Crypto extends EventEmitter {
const keysToUpload = Object.keys(keySignatures);
if (keysToUpload.length) {
const upload = ({ shouldEmit }) => {
const upload = ({ shouldEmit = false }) => {
logger.info(`Starting background key sig upload for ${keysToUpload}`);
return this.baseApis.uploadKeySignatures({ [this.userId]: keySignatures })
.then((response) => {
@@ -1927,7 +1934,7 @@ export class Crypto extends EventEmitter {
}
const oneTimeKeys = await this.olmDevice.getOneTimeKeys();
const oneTimeJson = {};
const oneTimeJson: Record<string, { key: string }> = {};
for (const keyId in oneTimeKeys.curve25519) {
if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) {
@@ -2076,7 +2083,7 @@ export class Crypto extends EventEmitter {
);
const device = await this.crossSigningInfo.signUser(xsk);
if (device) {
const upload = async ({ shouldEmit }) => {
const upload = async ({ shouldEmit = false }) => {
logger.info("Uploading signature for " + userId + "...");
const response = await this.baseApis.uploadKeySignatures({
[userId]: {
@@ -2149,7 +2156,7 @@ export class Crypto extends EventEmitter {
logger.info("Own device " + deviceId + " marked verified: signing");
// Signing only needed if other device not already signed
let device;
let device: ISignedKey;
const deviceTrust = this.checkDeviceTrust(userId, deviceId);
if (deviceTrust.isCrossSigningVerified()) {
logger.log(`Own device ${deviceId} already cross-signing verified`);
@@ -2160,7 +2167,7 @@ export class Crypto extends EventEmitter {
}
if (device) {
const upload = async ({ shouldEmit }) => {
const upload = async ({ shouldEmit = false }) => {
logger.info("Uploading signature for " + deviceId);
const response = await this.baseApis.uploadKeySignatures({
[userId]: {
@@ -2204,11 +2211,7 @@ export class Crypto extends EventEmitter {
return Promise.resolve(existingRequest);
}
const channel = new InRoomChannel(this.baseApis, roomId, userId);
return this.requestVerificationWithChannel(
userId,
channel,
this.inRoomVerificationRequests,
);
return this.requestVerificationWithChannel(userId, channel, this.inRoomVerificationRequests);
}
public requestVerification(userId: string, devices: string[]): Promise<VerificationRequest> {
@@ -2220,17 +2223,13 @@ export class Crypto extends EventEmitter {
return Promise.resolve(existingRequest);
}
const channel = new ToDeviceChannel(this.baseApis, userId, devices, ToDeviceChannel.makeTransactionId());
return this.requestVerificationWithChannel(
userId,
channel,
this.toDeviceVerificationRequests,
);
return this.requestVerificationWithChannel(userId, channel, this.toDeviceVerificationRequests);
}
private async requestVerificationWithChannel(
userId: string,
channel: IVerificationChannel,
requestsMap: any, // TODO types
requestsMap: IRequestsMap,
): Promise<VerificationRequest> {
let request = new VerificationRequest(channel, this.verificationMethods, this.baseApis);
// if transaction id is already known, add request
@@ -2618,7 +2617,7 @@ export class Crypto extends EventEmitter {
users: string[],
force?: boolean,
): Promise<Record<string, Record<string, olmlib.IOlmSessionResult>>> {
const devicesByUser = {};
const devicesByUser: Record<string, DeviceInfo[]> = {};
for (let i = 0; i < users.length; ++i) {
const userId = users[i];
@@ -2651,7 +2650,7 @@ export class Crypto extends EventEmitter {
* @return {module:crypto/OlmDevice.MegolmSessionData[]} a list of session export objects
*/
public async exportRoomKeys(): Promise<IMegolmSessionData[]> {
const exportedSessions = [];
const exportedSessions: IMegolmSessionData[] = [];
await this.cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
this.cryptoStore.getAllEndToEndInboundGroupSessions(txn, (s) => {
@@ -2729,7 +2728,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.
*
@@ -2740,8 +2738,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");
@@ -2783,8 +2779,7 @@ export class Crypto extends EventEmitter {
delete content['io.element.performance_metrics'];
}
const encryptedContent = await alg.encryptMessage(
room, event.getType(), content);
const encryptedContent = await alg.encryptMessage(room, event.getType(), content);
if (mRelatesTo) {
encryptedContent['m.relates_to'] = mRelatesTo;
@@ -3154,7 +3149,7 @@ export class Crypto extends EventEmitter {
if (!ToDeviceChannel.validateEvent(event, this.baseApis)) {
return;
}
const createRequest = event => {
const createRequest = (event: MatrixEvent) => {
if (!ToDeviceChannel.canCreateRequest(ToDeviceChannel.getEventType(event))) {
return;
}
@@ -3172,11 +3167,7 @@ export class Crypto extends EventEmitter {
return new VerificationRequest(
channel, this.verificationMethods, this.baseApis);
};
this.handleVerificationEvent(
event,
this.toDeviceVerificationRequests,
createRequest,
);
this.handleVerificationEvent(event, this.toDeviceVerificationRequests, createRequest);
}
/**
@@ -3199,7 +3190,7 @@ export class Crypto extends EventEmitter {
if (!InRoomChannel.validateEvent(event, this.baseApis)) {
return;
}
const createRequest = event => {
const createRequest = (event: MatrixEvent) => {
const channel = new InRoomChannel(
this.baseApis,
event.getRoomId(),
@@ -3207,18 +3198,13 @@ export class Crypto extends EventEmitter {
return new VerificationRequest(
channel, this.verificationMethods, this.baseApis);
};
this.handleVerificationEvent(
event,
this.inRoomVerificationRequests,
createRequest,
liveEvent,
);
this.handleVerificationEvent(event, this.inRoomVerificationRequests, createRequest, liveEvent);
};
private async handleVerificationEvent(
event: MatrixEvent,
requestsMap: any, // TODO types
createRequest: any, // TODO types
requestsMap: IRequestsMap,
createRequest: (event: MatrixEvent) => VerificationRequest,
isLiveEvent = true,
): Promise<void> {
// Wait for event to get its final ID with pendingEventOrdering: "chronological", since DM channels depend on it.
@@ -3332,7 +3318,7 @@ export class Crypto extends EventEmitter {
return;
}
}
const devicesByUser = {};
const devicesByUser: Record<string, DeviceInfo[]> = {};
devicesByUser[sender] = [device];
await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser, true);
+18 -14
View File
@@ -28,7 +28,8 @@ import { OlmDevice } from "./OlmDevice";
import { DeviceInfo } from "./deviceinfo";
import { logger } from '../logger';
import { IOneTimeKey } from "./dehydration";
import { MatrixClient } from "../client";
import { IClaimOTKsResult, MatrixClient } from "../client";
import { ISignatures } from "../@types/signed";
enum Algorithm {
Olm = "m.olm.v1.curve25519-aes-sha2",
@@ -132,6 +133,11 @@ export async function encryptMessageForDevice(
);
}
interface IExistingOlmSession {
device: DeviceInfo;
sessionId?: string;
}
/**
* Get the existing olm sessions for the given devices, and the devices that
* don't have olm sessions.
@@ -152,11 +158,11 @@ export async function getExistingOlmSessions(
olmDevice: OlmDevice,
baseApis: MatrixClient,
devicesByUser: Record<string, DeviceInfo[]>,
) {
const devicesWithoutSession = {};
const sessions = {};
): Promise<[Record<string, DeviceInfo[]>, Record<string, Record<string, IExistingOlmSession>>]> {
const devicesWithoutSession: {[userId: string]: DeviceInfo[]} = {};
const sessions: {[userId: string]: {[deviceId: string]: IExistingOlmSession}} = {};
const promises = [];
const promises: Promise<void>[] = [];
for (const [userId, devices] of Object.entries(devicesByUser)) {
for (const deviceInfo of devices) {
@@ -230,10 +236,10 @@ export async function ensureOlmSessionsForDevices(
force = false;
}
const devicesWithoutSession = [
const devicesWithoutSession: [string, string][] = [
// [userId, deviceId], ...
];
const result = {};
const result: {[userId: string]: {[deviceId: string]: IExistingOlmSession}} = {};
const resolveSession: Record<string, (sessionId?: string) => void> = {};
// Mark all sessions this task intends to update as in progress. It is
@@ -321,9 +327,7 @@ export async function ensureOlmSessionsForDevices(
let taskDetail = `one-time keys for ${devicesWithoutSession.length} devices`;
try {
log.debug(`Claiming ${taskDetail}`);
res = await baseApis.claimOneTimeKeys(
devicesWithoutSession, oneTimeKeyAlgorithm, otkTimeout,
);
res = await baseApis.claimOneTimeKeys(devicesWithoutSession, oneTimeKeyAlgorithm, otkTimeout);
log.debug(`Claimed ${taskDetail}`);
} catch (e) {
for (const resolver of Object.values(resolveSession)) {
@@ -337,8 +341,8 @@ export async function ensureOlmSessionsForDevices(
failedServers.push(...Object.keys(res.failures));
}
const otkResult = res.one_time_keys || {};
const promises = [];
const otkResult = res.one_time_keys || {} as IClaimOTKsResult["one_time_keys"];
const promises: Promise<void>[] = [];
for (const [userId, devices] of Object.entries(devicesByUser)) {
const userRes = otkResult[userId] || {};
for (let j = 0; j < devices.length; j++) {
@@ -359,7 +363,7 @@ export async function ensureOlmSessionsForDevices(
}
const deviceRes = userRes[deviceId] || {};
let oneTimeKey = null;
let oneTimeKey: IOneTimeKey = null;
for (const keyId in deviceRes) {
if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) {
oneTimeKey = deviceRes[keyId];
@@ -441,7 +445,7 @@ async function _verifyKeyAndStartSession(
export interface IObject {
unsigned?: object;
signatures?: object;
signatures?: ISignatures;
}
/**
@@ -200,10 +200,10 @@ export class Backend implements CryptoStore {
// index into the wantedStates array
let stateIndex = 0;
let result;
let result: OutgoingRoomKeyRequest;
function onsuccess(ev) {
const cursor = ev.target.result;
function onsuccess(this: IDBRequest<IDBCursorWithValue>) {
const cursor = this.result;
if (cursor) {
// got a match
result = cursor.value;
@@ -218,7 +218,7 @@ export class Backend implements CryptoStore {
}
const wantedState = wantedStates[stateIndex];
const cursorReq = ev.target.source.openCursor(wantedState);
const cursorReq = (this.source as IDBIndex).openCursor(wantedState);
cursorReq.onsuccess = onsuccess;
}
@@ -255,10 +255,10 @@ export class Backend implements CryptoStore {
wantedStates: number[],
): Promise<OutgoingRoomKeyRequest[]> {
let stateIndex = 0;
const results = [];
const results: OutgoingRoomKeyRequest[] = [];
function onsuccess(ev) {
const cursor = ev.target.result;
function onsuccess(this: IDBRequest<IDBCursorWithValue>) {
const cursor = this.result;
if (cursor) {
const keyReq = cursor.value;
if (keyReq.recipients.includes({ userId, deviceId })) {
@@ -274,7 +274,7 @@ export class Backend implements CryptoStore {
}
const wantedState = wantedStates[stateIndex];
const cursorReq = ev.target.source.openCursor(wantedState);
const cursorReq = (this.source as IDBIndex).openCursor(wantedState);
cursorReq.onsuccess = onsuccess;
}
}
@@ -306,10 +306,10 @@ export class Backend implements CryptoStore {
expectedState: number,
updates: Partial<OutgoingRoomKeyRequest>,
): Promise<OutgoingRoomKeyRequest | null> {
let result = null;
let result: OutgoingRoomKeyRequest = null;
function onsuccess(ev) {
const cursor = ev.target.result;
function onsuccess(this: IDBRequest<IDBCursorWithValue>) {
const cursor = this.result;
if (!cursor) {
return;
}
@@ -444,7 +444,7 @@ export class Backend implements CryptoStore {
const objectStore = txn.objectStore("sessions");
const idx = objectStore.index("deviceKey");
const getReq = idx.openCursor(deviceKey);
const results = {};
const results: Parameters<Parameters<Backend["getEndToEndSessions"]>[2]>[0] = {};
getReq.onsuccess = function() {
const cursor = getReq.result;
if (cursor) {
@@ -734,7 +734,7 @@ export class Backend implements CryptoStore {
}
public getEndToEndRooms(txn: IDBTransaction, func: (rooms: Record<string, IRoomEncryption>) => void): void {
const rooms = {};
const rooms: Parameters<Parameters<Backend["getEndToEndRooms"]>[1]>[0] = {};
const objectStore = txn.objectStore("rooms");
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
@@ -756,7 +756,7 @@ export class Backend implements CryptoStore {
public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
return new Promise((resolve, reject) => {
const sessions = [];
const sessions: ISession[] = [];
const txn = this.db.transaction(
["sessions_needing_backup", "inbound_group_sessions"],
@@ -877,8 +877,8 @@ export class Backend implements CryptoStore {
func: (txn: IDBTransaction) => T,
log: PrefixedLogger = logger,
): Promise<T> {
let startTime;
let description;
let startTime: number;
let description: string;
if (PROFILE_TRANSACTIONS) {
const txnId = this.nextTxnId++;
startTime = Date.now();
@@ -175,8 +175,10 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
}
public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
const notifiedErrorDevices = getJsonItem<string[]>(this.store, KEY_NOTIFIED_ERROR_DEVICES) || {};
const ret = [];
const notifiedErrorDevices = getJsonItem<MemoryCryptoStore["notifiedErrorDevices"]>(
this.store, KEY_NOTIFIED_ERROR_DEVICES,
) || {};
const ret: IOlmDevice[] = [];
for (const device of devices) {
const { userId, deviceInfo } = device;
@@ -291,7 +293,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
}
public getEndToEndRooms(txn: unknown, func: (rooms: Record<string, IRoomEncryption>) => void): void {
const result = {};
const result: Record<string, IRoomEncryption> = {};
const prefix = keyEndToEndRoomsPrefix('');
for (let i = 0; i < this.store.length; ++i) {
@@ -306,7 +308,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
const sessionsNeedingBackup = getJsonItem<string[]>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
const sessions = [];
const sessions: ISession[] = [];
for (const session in sessionsNeedingBackup) {
if (Object.prototype.hasOwnProperty.call(sessionsNeedingBackup, session)) {
@@ -30,4 +30,5 @@ export interface IVerificationChannel {
sendCompleted(type: string, content: Record<string, any>): Promise<void>;
completedContentFromEvent(event: MatrixEvent): Record<string, any>;
canCreateRequest(type: string): boolean;
handleEvent(event: MatrixEvent, request: VerificationRequest, isLiveEvent: boolean): Promise<void>;
}
@@ -26,6 +26,7 @@ import { IVerificationChannel } from "./Channel";
import { EventType } from "../../../@types/event";
import { MatrixClient } from "../../../client";
import { MatrixEvent } from "../../../models/event";
import { IRequestsMap } from "../..";
const MESSAGE_TYPE = EventType.RoomMessage;
const M_REFERENCE = "m.reference";
@@ -304,7 +305,7 @@ export class InRoomChannel implements IVerificationChannel {
}
}
export class InRoomRequests {
export class InRoomRequests implements IRequestsMap {
private requestsByRoomId = new Map<string, Map<string, VerificationRequest>>();
public getRequest(event: MatrixEvent): VerificationRequest {
@@ -328,7 +329,7 @@ export class InRoomRequests {
this.doSetRequest(event.getRoomId(), InRoomChannel.getTransactionId(event), request);
}
public setRequestByChannel(channel: InRoomChannel, request: VerificationRequest): void {
public setRequestByChannel(channel: IVerificationChannel, request: VerificationRequest): void {
this.doSetRequest(channel.roomId, channel.transactionId, request);
}
@@ -30,8 +30,9 @@ import { errorFromEvent, newUnexpectedMessageError } from "../Error";
import { MatrixEvent } from "../../../models/event";
import { IVerificationChannel } from "./Channel";
import { MatrixClient } from "../../../client";
import { IRequestsMap } from '../..';
type Request = VerificationRequest<ToDeviceChannel>;
export type Request = VerificationRequest<ToDeviceChannel>;
/**
* A key verification channel that sends verification events over to_device messages.
@@ -276,7 +277,7 @@ export class ToDeviceChannel implements IVerificationChannel {
private async sendToDevices(type: string, content: Record<string, any>, devices: string[]): Promise<void> {
if (devices.length) {
const msgMap = {};
const msgMap: Record<string, Record<string, any>> = {};
for (const deviceId of devices) {
msgMap[deviceId] = content;
}
@@ -294,7 +295,7 @@ export class ToDeviceChannel implements IVerificationChannel {
}
}
export class ToDeviceRequests {
export class ToDeviceRequests implements IRequestsMap {
private requestsByUserId = new Map<string, Map<string, Request>>();
public getRequest(event: MatrixEvent): Request {
@@ -582,7 +582,9 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
// get common methods
if (phase === PHASE_REQUESTED || phase === PHASE_READY) {
if (!this.wasSentByOwnDevice(event)) {
const content = event.getContent();
const content = event.getContent<{
methods: string[];
}>();
this.commonMethods =
content.methods.filter(m => this.verificationMethods.has(m));
}
+1 -1
View File
@@ -41,7 +41,7 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
}
}
if (!preventReEmit) {
client.reEmitter.reEmit(event, ["Event.replaced"]);
client.reEmitter.reEmit(event, ["Event.replaced", "Event.visibilityChange"]);
}
return event;
}
+51 -4
View File
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { RelationType } from "./@types/event";
import { UNSTABLE_FILTER_RELATION_SENDERS, UNSTABLE_FILTER_RELATION_TYPES } from "./filter";
import { MatrixEvent } from "./models/event";
/**
@@ -46,6 +48,7 @@ export interface IFilterComponent {
not_senders?: string[];
contains_url?: boolean;
limit?: number;
[UNSTABLE_FILTER_RELATION_TYPES.name]?: Array<RelationType | string>;
}
/* eslint-enable camelcase */
@@ -61,7 +64,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
@@ -69,11 +72,24 @@ 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 && relations?.[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,
);
}
@@ -89,6 +105,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),
};
}
@@ -98,9 +116,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;
@@ -133,15 +160,35 @@ export class FilterComponent {
return false;
}
const relationTypesFilter = this.filterJson[UNSTABLE_FILTER_RELATION_TYPES.name];
if (relationTypesFilter !== undefined) {
if (!this.arrayMatchesFilter(relationTypesFilter, relationTypes)) {
return false;
}
}
const relationSendersFilter = this.filterJson[UNSTABLE_FILTER_RELATION_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);
}
@@ -150,7 +197,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;
}
}
+3 -3
View File
@@ -65,7 +65,7 @@ export interface IFilterDefinition {
export interface IRoomEventFilter extends IFilterComponent {
lazy_load_members?: boolean;
include_redundant_members?: boolean;
types?: EventType[] | string[];
types?: Array<EventType | string>;
[UNSTABLE_FILTER_RELATION_TYPES.name]?: Array<RelationType | string>;
[UNSTABLE_FILTER_RELATION_SENDERS.name]?: string[];
}
@@ -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 =
+328 -202
View File
@@ -1,6 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019 - 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.
@@ -20,14 +20,21 @@ limitations under the License.
* @module http-api
*/
import { parse as parseContentType } from "content-type";
import { parse as parseContentType, ParsedMediaType } from "content-type";
import EventEmitter from "events";
import * as utils from "./utils";
import { logger } from './logger';
import type { IncomingHttpHeaders, IncomingMessage } from "http";
import type { Request as _Request, CoreOptions } from "request";
// we use our own implementation of setTimeout, so that if we get suspended in
// the middle of a /sync, we cancel the sync as soon as we awake, rather than
// waiting for the delay to elapse.
import * as callbacks from "./realtime-callbacks";
import { IUploadOpts } from "./@types/requests";
import { IAbortablePromise, IUsageLimit } from "./@types/partials";
import { IDeferred } from "./utils";
import { Callback } from "./client";
import * as utils from "./utils";
import { logger } from './logger';
/*
TODO:
@@ -40,6 +47,11 @@ TODO:
*/
export const PREFIX_R0 = "/_matrix/client/r0";
/**
* A constant representing the URI path for release v1 of the Client-Server HTTP API.
*/
export const PREFIX_V1 = "/_matrix/client/v1";
/**
* A constant representing the URI path for as-yet unspecified Client-Server HTTP APIs.
*/
@@ -61,10 +73,95 @@ export const PREFIX_IDENTITY_V2 = "/_matrix/identity/v2";
*/
export const PREFIX_MEDIA_R0 = "/_matrix/media/r0";
type RequestProps = "method"
| "withCredentials"
| "json"
| "headers"
| "qs"
| "body"
| "qsStringifyOptions"
| "useQuerystring"
| "timeout";
export interface IHttpOpts {
baseUrl: string;
idBaseUrl?: string;
prefix: string;
onlyData: boolean;
accessToken?: string;
extraParams?: Record<string, string>;
localTimeoutMs?: number;
useAuthorizationHeader?: boolean;
request(opts: Pick<CoreOptions, RequestProps> & {
uri: string;
method: Method;
// eslint-disable-next-line camelcase
_matrix_opts: IHttpOpts;
}, callback: RequestCallback): IRequest;
}
interface IRequest extends _Request {
onprogress?(e: unknown): void;
}
interface IRequestOpts<T> {
prefix?: string;
localTimeoutMs?: number;
headers?: Record<string, string>;
json?: boolean; // defaults to true
qsStringifyOptions?: CoreOptions["qsStringifyOptions"];
bodyParser?(body: string): T;
}
export interface IUpload {
loaded: number;
total: number;
promise: IAbortablePromise<unknown>;
}
interface IContentUri {
base: string;
path: string;
params: {
// eslint-disable-next-line camelcase
access_token: string;
};
}
type ResponseType<T, O extends IRequestOpts<T> | void = void> =
O extends { bodyParser: (body: string) => T } ? T :
O extends { json: false } ? string :
T;
interface IUploadResponse {
// eslint-disable-next-line camelcase
content_uri: string;
}
// This type's defaults only work for the Browser
// in the Browser we default rawResponse = false & onlyContentUri = true
// in Node we default rawResponse = true & onlyContentUri = false
export type UploadContentResponseType<O extends IUploadOpts> =
O extends undefined ? string :
O extends { rawResponse: true } ? string :
O extends { onlyContentUri: true } ? string :
O extends { rawResponse: false } ? IUploadResponse :
O extends { onlyContentUri: false } ? IUploadResponse :
string;
export enum Method {
Get = "GET",
Put = "PUT",
Post = "POST",
Delete = "DELETE",
}
export type FileType = Document | XMLHttpRequestBodyInit;
/**
* Construct a MatrixHttpApi.
* @constructor
* @param {EventEmitter} event_emitter The event emitter to use for emitting events
* @param {EventEmitter} eventEmitter The event emitter to use for emitting events
* @param {Object} opts The options to use for this HTTP API.
* @param {string} opts.baseUrl Required. The base client-server URL e.g.
* 'http://localhost:8008'.
@@ -77,7 +174,7 @@ export const PREFIX_MEDIA_R0 = "/_matrix/media/r0";
* response (e.g. the parsed HTTP body). If false, requests will return an
* object with the properties <tt>code</tt>, <tt>headers</tt> and <tt>data</tt>.
*
* @param {string} opts.accessToken The access_token to send with requests. Can be
* @param {string=} opts.accessToken The access_token to send with requests. Can be
* null to not send an access token.
* @param {Object=} opts.extraParams Optional. Extra query parameters to send on
* requests.
@@ -86,39 +183,37 @@ export const PREFIX_MEDIA_R0 = "/_matrix/media/r0";
* @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use
* Authorization header instead of query param to send the access token to the server.
*/
export function MatrixHttpApi(event_emitter, opts) {
utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]);
opts.onlyData = opts.onlyData || false;
this.event_emitter = event_emitter;
this.opts = opts;
this.useAuthorizationHeader = Boolean(opts.useAuthorizationHeader);
this.uploads = [];
}
export class MatrixHttpApi {
private uploads: IUpload[] = [];
constructor(private eventEmitter: EventEmitter, public readonly opts: IHttpOpts) {
utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]);
opts.onlyData = !!opts.onlyData;
opts.useAuthorizationHeader = !!opts.useAuthorizationHeader;
}
MatrixHttpApi.prototype = {
/**
* Sets the baase URL for the identity server
* Sets the base URL for the identity server
* @param {string} url The new base url
*/
setIdBaseUrl: function(url) {
public setIdBaseUrl(url: string): void {
this.opts.idBaseUrl = url;
},
}
/**
* Get the content repository url with query parameters.
* @return {Object} An object with a 'base', 'path' and 'params' for base URL,
* path and query parameters respectively.
*/
getContentUri: function() {
const params = {
access_token: this.opts.accessToken,
};
public getContentUri(): IContentUri {
return {
base: this.opts.baseUrl,
path: "/_matrix/media/r0/upload",
params: params,
params: {
access_token: this.opts.accessToken,
},
};
},
}
/**
* Upload content to the homeserver
@@ -160,14 +255,17 @@ MatrixHttpApi.prototype = {
* determined by this.opts.onlyData, opts.rawResponse, and
* opts.onlyContentUri. Rejects with an error (usually a MatrixError).
*/
uploadContent: function(file, opts) {
public uploadContent<O extends IUploadOpts>(
file: FileType,
opts?: O,
): IAbortablePromise<UploadContentResponseType<O>> {
if (utils.isFunction(opts)) {
// opts used to be callback
// opts used to be callback, backwards compatibility
opts = {
callback: opts,
};
} else if (opts === undefined) {
opts = {};
callback: opts as unknown as IUploadOpts["callback"],
} as O;
} else if (!opts) {
opts = {} as O;
}
// default opts.includeFilename to true (ignoring falsey values)
@@ -175,8 +273,8 @@ MatrixHttpApi.prototype = {
// if the file doesn't have a mime type, use a default since
// the HS errors if we don't supply one.
const contentType = opts.type || file.type || 'application/octet-stream';
const fileName = opts.name || file.name;
const contentType = opts.type || (file as File).type || 'application/octet-stream';
const fileName = opts.name || (file as File).name;
// We used to recommend setting file.stream to the thing to upload on
// Node.js. As of 2019-06-11, this is still in widespread use in various
@@ -185,13 +283,14 @@ MatrixHttpApi.prototype = {
// the browser now define a `stream` method, which leads to trouble
// here, so we also check the type of `stream`.
let body = file;
if (body.stream && typeof body.stream !== "function") {
const bodyStream = (body as File | Blob).stream; // this type is wrong but for legacy reasons is good enough
if (bodyStream && typeof bodyStream !== "function") {
logger.warn(
"Using `file.stream` as the content to upload. Future " +
"versions of the js-sdk will change this to expect `file` to " +
"be the content directly.",
);
body = body.stream;
body = bodyStream;
}
// backwards-compatibility hacks where we used to do different things
@@ -234,8 +333,8 @@ MatrixHttpApi.prototype = {
// (browser-request doesn't support progress either, which is also kind
// of important here)
const upload = { loaded: 0, total: 0 };
let promise;
const upload = { loaded: 0, total: 0 } as IUpload;
let promise: IAbortablePromise<UploadContentResponseType<O>>;
// XMLHttpRequest doesn't parse JSON for us. request normally does, but
// we're setting opts.json=false so that it doesn't JSON-encode the
@@ -243,7 +342,7 @@ MatrixHttpApi.prototype = {
// way, we have to JSON-parse the response ourselves.
let bodyParser = null;
if (!rawResponse) {
bodyParser = function(rawBody) {
bodyParser = function(rawBody: string) {
let body = JSON.parse(rawBody);
if (onlyContentUri) {
body = body.content_uri;
@@ -256,25 +355,23 @@ MatrixHttpApi.prototype = {
}
if (global.XMLHttpRequest) {
const defer = utils.defer();
const defer = utils.defer<UploadContentResponseType<O>>();
const xhr = new global.XMLHttpRequest();
upload.xhr = xhr;
const cb = requestCallback(defer, opts.callback, this.opts.onlyData);
const timeout_fn = function() {
const timeoutFn = function() {
xhr.abort();
cb(new Error('Timeout'));
};
// set an initial timeout of 30s; we'll advance it each time we get
// a progress notification
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
// set an initial timeout of 30s; we'll advance it each time we get a progress notification
let timeoutTimer = callbacks.setTimeout(timeoutFn, 30000);
xhr.onreadystatechange = function() {
let resp;
let resp: string;
switch (xhr.readyState) {
case global.XMLHttpRequest.DONE:
callbacks.clearTimeout(xhr.timeout_timer);
callbacks.clearTimeout(timeoutTimer);
try {
if (xhr.status === 0) {
throw new AbortError();
@@ -296,10 +393,10 @@ MatrixHttpApi.prototype = {
}
};
xhr.upload.addEventListener("progress", function(ev) {
callbacks.clearTimeout(xhr.timeout_timer);
callbacks.clearTimeout(timeoutTimer);
upload.loaded = ev.loaded;
upload.total = ev.total;
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
timeoutTimer = callbacks.setTimeout(timeoutFn, 30000);
if (opts.progressHandler) {
opts.progressHandler({
loaded: ev.loaded,
@@ -315,9 +412,8 @@ MatrixHttpApi.prototype = {
queryArgs.push("filename=" + encodeURIComponent(fileName));
}
if (!this.useAuthorizationHeader) {
queryArgs.push("access_token="
+ encodeURIComponent(this.opts.accessToken));
if (!this.opts.useAuthorizationHeader) {
queryArgs.push("access_token=" + encodeURIComponent(this.opts.accessToken));
}
if (queryArgs.length > 0) {
@@ -325,73 +421,69 @@ MatrixHttpApi.prototype = {
}
xhr.open("POST", url);
if (this.useAuthorizationHeader) {
if (this.opts.useAuthorizationHeader) {
xhr.setRequestHeader("Authorization", "Bearer " + this.opts.accessToken);
}
xhr.setRequestHeader("Content-Type", contentType);
xhr.send(body);
promise = defer.promise;
promise = defer.promise as IAbortablePromise<UploadContentResponseType<O>>;
// dirty hack (as per _request) to allow the upload to be cancelled.
// dirty hack (as per doRequest) to allow the upload to be cancelled.
promise.abort = xhr.abort.bind(xhr);
} else {
const queryParams = {};
const queryParams: Record<string, string> = {};
if (includeFilename && fileName) {
queryParams.filename = fileName;
}
promise = this.authedRequest(
opts.callback, "POST", "/upload", queryParams, body, {
opts.callback, Method.Post, "/upload", queryParams, body, {
prefix: "/_matrix/media/r0",
headers: { "Content-Type": contentType },
json: false,
bodyParser: bodyParser,
bodyParser,
},
);
}
const self = this;
// remove the upload from the list on completion
const promise0 = promise.finally(function() {
for (let i = 0; i < self.uploads.length; ++i) {
if (self.uploads[i] === upload) {
self.uploads.splice(i, 1);
upload.promise = promise.finally(() => {
for (let i = 0; i < this.uploads.length; ++i) {
if (this.uploads[i] === upload) {
this.uploads.splice(i, 1);
return;
}
}
});
}) as IAbortablePromise<UploadContentResponseType<O>>;
// copy our dirty abort() method to the new promise
promise0.abort = promise.abort;
upload.promise = promise0;
upload.promise.abort = promise.abort;
this.uploads.push(upload);
return promise0;
},
return upload.promise as IAbortablePromise<UploadContentResponseType<O>>;
}
cancelUpload: function(promise) {
public cancelUpload(promise: IAbortablePromise<unknown>): boolean {
if (promise.abort) {
promise.abort();
return true;
}
return false;
},
}
getCurrentUploads: function() {
public getCurrentUploads(): IUpload[] {
return this.uploads;
},
}
idServerRequest: function(
callback,
method,
path,
params,
prefix,
accessToken,
) {
public idServerRequest<T>(
callback: Callback<T>,
method: Method,
path: string,
params: Record<string, string | string[]>,
prefix: string,
accessToken: string,
): Promise<T> {
if (!this.opts.idBaseUrl) {
throw new Error("No identity server base URL set");
}
@@ -406,28 +498,27 @@ MatrixHttpApi.prototype = {
const opts = {
uri: fullUri,
method: method,
method,
withCredentials: false,
json: true, // we want a JSON response if we can
_matrix_opts: this.opts,
headers: {},
};
if (method === 'GET') {
} as Parameters<IHttpOpts["request"]>[0];
if (method === Method.Get) {
opts.qs = params;
} else if (typeof params === "object") {
opts.json = params;
}
if (accessToken) {
opts.headers['Authorization'] = `Bearer ${accessToken}`;
}
const defer = utils.defer();
this.opts.request(
opts,
requestCallback(defer, callback, this.opts.onlyData),
);
const defer = utils.defer<T>();
this.opts.request(opts, requestCallback(defer, callback, this.opts.onlyData));
return defer.promise;
},
}
/**
* Perform an authorised request to the homeserver.
@@ -448,7 +539,7 @@ MatrixHttpApi.prototype = {
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
*
* @param {sting=} opts.prefix The full prefix to use e.g.
* @param {string=} opts.prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
*
* @param {Object=} opts.headers map of additional request headers
@@ -460,45 +551,45 @@ MatrixHttpApi.prototype = {
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
authedRequest: function(callback, method, path, queryParams, data, opts) {
if (!queryParams) {
queryParams = {};
}
if (this.useAuthorizationHeader) {
if (isFinite(opts)) {
public authedRequest<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
callback: Callback<T>,
method: Method,
path: string,
queryParams?: Record<string, string | string[]>,
data?: CoreOptions["body"],
opts?: O | number, // number is legacy
): IAbortablePromise<ResponseType<T, O>> {
if (!queryParams) queryParams = {};
let requestOpts = (opts || {}) as O;
if (this.opts.useAuthorizationHeader) {
if (isFinite(opts as number)) {
// opts used to be localTimeoutMs
opts = {
localTimeoutMs: opts,
};
requestOpts = {
localTimeoutMs: opts as number,
} as O;
}
if (!opts) {
opts = {};
if (!requestOpts.headers) {
requestOpts.headers = {};
}
if (!opts.headers) {
opts.headers = {};
}
if (!opts.headers.Authorization) {
opts.headers.Authorization = "Bearer " + this.opts.accessToken;
if (!requestOpts.headers.Authorization) {
requestOpts.headers.Authorization = "Bearer " + this.opts.accessToken;
}
if (queryParams.access_token) {
delete queryParams.access_token;
}
} else {
if (!queryParams.access_token) {
queryParams.access_token = this.opts.accessToken;
}
} else if (!queryParams.access_token) {
queryParams.access_token = this.opts.accessToken;
}
const requestPromise = this.request(
callback, method, path, queryParams, data, opts,
);
const requestPromise = this.request<T, O>(callback, method, path, queryParams, data, requestOpts);
const self = this;
requestPromise.catch(function(err) {
requestPromise.catch((err: MatrixError) => {
if (err.errcode == 'M_UNKNOWN_TOKEN') {
self.event_emitter.emit("Session.logged_out", err);
this.eventEmitter.emit("Session.logged_out", err);
} else if (err.errcode == 'M_CONSENT_NOT_GIVEN') {
self.event_emitter.emit(
this.eventEmitter.emit(
"no_consent",
err.message,
err.data.consent_uri,
@@ -509,7 +600,7 @@ MatrixHttpApi.prototype = {
// return the original promise, otherwise tests break due to it having to
// go around the event loop one more time to process the result of the request
return requestPromise;
},
}
/**
* Perform a request to the homeserver without any credentials.
@@ -529,7 +620,7 @@ MatrixHttpApi.prototype = {
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
*
* @param {sting=} opts.prefix The full prefix to use e.g.
* @param {string=} opts.prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
*
* @param {Object=} opts.headers map of additional request headers
@@ -541,15 +632,19 @@ MatrixHttpApi.prototype = {
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
request: function(callback, method, path, queryParams, data, opts) {
opts = opts || {};
const prefix = opts.prefix !== undefined ? opts.prefix : this.opts.prefix;
public request<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
callback: Callback<T>,
method: Method,
path: string,
queryParams?: CoreOptions["qs"],
data?: CoreOptions["body"],
opts?: O,
): IAbortablePromise<ResponseType<T, O>> {
const prefix = opts?.prefix ?? this.opts.prefix;
const fullUri = this.opts.baseUrl + prefix + path;
return this.requestOtherUrl(
callback, method, fullUri, queryParams, data, opts,
);
},
return this.requestOtherUrl<T, O>(callback, method, fullUri, queryParams, data, opts);
}
/**
* Perform a request to an arbitrary URL.
@@ -568,7 +663,7 @@ MatrixHttpApi.prototype = {
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
*
* @param {sting=} opts.prefix The full prefix to use e.g.
* @param {string=} opts.prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
*
* @param {Object=} opts.headers map of additional request headers
@@ -580,21 +675,24 @@ MatrixHttpApi.prototype = {
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
requestOtherUrl: function(callback, method, uri, queryParams, data,
opts) {
if (opts === undefined || opts === null) {
opts = {};
} else if (isFinite(opts)) {
public requestOtherUrl<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
callback: Callback<T>,
method: Method,
uri: string,
queryParams?: CoreOptions["qs"],
data?: CoreOptions["body"],
opts?: O | number, // number is legacy
): IAbortablePromise<ResponseType<T, O>> {
let requestOpts = (opts || {}) as O;
if (isFinite(opts as number)) {
// opts used to be localTimeoutMs
opts = {
localTimeoutMs: opts,
};
requestOpts = {
localTimeoutMs: opts as number,
} as O;
}
return this._request(
callback, method, uri, queryParams, data, opts,
);
},
return this.doRequest<T, O>(callback, method, uri, queryParams, data, requestOpts);
}
/**
* Form and return a homeserver request URL based on the given path
@@ -607,13 +705,13 @@ MatrixHttpApi.prototype = {
* "/_matrix/client/v2_alpha".
* @return {string} URL
*/
getUrl: function(path, queryParams, prefix) {
public getUrl(path: string, queryParams: CoreOptions["qs"], prefix: string): string {
let queryString = "";
if (queryParams) {
queryString = "?" + utils.encodeParams(queryParams);
}
return this.opts.baseUrl + prefix + path + queryString;
},
}
/**
* @private
@@ -640,25 +738,32 @@ MatrixHttpApi.prototype = {
* @return {Promise} a promise which resolves to either the
* response object (if this.opts.onlyData is truthy), or the parsed
* body. Rejects
*
* Generic T is the callback/promise resolve type
* Generic O should be inferred
*/
_request: function(callback, method, uri, queryParams, data, opts) {
private doRequest<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
callback: Callback<T>,
method: Method,
uri: string,
queryParams?: Record<string, string>,
data?: CoreOptions["body"],
opts?: O,
): IAbortablePromise<ResponseType<T, O>> {
if (callback !== undefined && !utils.isFunction(callback)) {
throw Error(
"Expected callback to be a function but got " + typeof callback,
);
throw Error("Expected callback to be a function but got " + typeof callback);
}
opts = opts || {};
const self = this;
if (this.opts.extraParams) {
queryParams = {
...queryParams,
...this.opts.extraParams,
...(queryParams || {}),
...this.opts.extraParams,
};
}
const headers = Object.assign({}, opts.headers || {});
const json = opts.json === undefined ? true : opts.json;
if (!opts) opts = {} as O;
const json = opts.json ?? true;
let bodyParser = opts.bodyParser;
// we handle the json encoding/decoding here, because request and
@@ -677,17 +782,17 @@ MatrixHttpApi.prototype = {
}
if (bodyParser === undefined) {
bodyParser = function(rawBody) {
bodyParser = function(rawBody: string) {
return JSON.parse(rawBody);
};
}
}
const defer = utils.defer();
const defer = utils.defer<T>();
let timeoutId;
let timeoutId: number;
let timedOut = false;
let req;
let req: IRequest;
const localTimeoutMs = opts.localTimeoutMs || this.opts.localTimeoutMs;
const resetTimeout = () => {
@@ -697,9 +802,7 @@ MatrixHttpApi.prototype = {
}
timeoutId = callbacks.setTimeout(function() {
timedOut = true;
if (req && req.abort) {
req.abort();
}
req?.abort?.();
defer.reject(new MatrixError({
error: "Locally timed out waiting for a response",
errcode: "ORG.MATRIX.JSSDK_TIMEOUT",
@@ -710,7 +813,7 @@ MatrixHttpApi.prototype = {
};
resetTimeout();
const reqPromise = defer.promise;
const reqPromise = defer.promise as IAbortablePromise<ResponseType<T, O>>;
try {
req = this.opts.request(
@@ -727,7 +830,7 @@ MatrixHttpApi.prototype = {
headers: headers || {},
_matrix_opts: this.opts,
},
function(err, response, body) {
(err, response, body) => {
if (localTimeoutMs) {
callbacks.clearTimeout(timeoutId);
if (timedOut) {
@@ -735,16 +838,13 @@ MatrixHttpApi.prototype = {
}
}
const handlerFn = requestCallback(
defer, callback, self.opts.onlyData,
bodyParser,
);
const handlerFn = requestCallback(defer, callback, this.opts.onlyData, bodyParser);
handlerFn(err, response, body);
},
);
if (req) {
// This will only work in a browser, where opts.request is the
// `browser-request` import. Currently `request` does not support progress
// `browser-request` import. Currently, `request` does not support progress
// updates - see https://github.com/request/request/pull/2346.
// `browser-request` returns an XHRHttpRequest which exposes `onprogress`
if ('onprogress' in req) {
@@ -757,7 +857,9 @@ MatrixHttpApi.prototype = {
// FIXME: This is EVIL, but I can't think of a better way to expose
// abort() operations on underlying HTTP requests :(
if (req.abort) reqPromise.abort = req.abort.bind(req);
if (req.abort) {
reqPromise.abort = req.abort.bind(req);
}
}
} catch (ex) {
defer.reject(ex);
@@ -766,8 +868,21 @@ MatrixHttpApi.prototype = {
}
}
return reqPromise;
},
};
}
}
type RequestCallback = (err?: Error, response?: XMLHttpRequest | IncomingMessage, body?: string) => void;
// if using onlyData=false then wrap your expected data type in this generic
export interface IResponse<T> {
code: number;
data: T;
headers?: IncomingHttpHeaders;
}
function getStatusCode(response: XMLHttpRequest | IncomingMessage): number {
return (response as XMLHttpRequest).status || (response as IncomingMessage).statusCode;
}
/*
* Returns a callback that can be invoked by an HTTP request on completion,
@@ -783,17 +898,17 @@ MatrixHttpApi.prototype = {
* response, otherwise the result object (with `code` and `data` fields)
*
*/
const requestCallback = function(
defer, userDefinedCallback, onlyData,
bodyParser,
) {
userDefinedCallback = userDefinedCallback || function() {};
return function(err, response, body) {
function requestCallback<T>(
defer: IDeferred<T>,
userDefinedCallback?: Callback<T>,
onlyData = false,
bodyParser?: (body: string) => T,
): RequestCallback {
return function(err: Error, response: XMLHttpRequest | IncomingMessage, body: string): void {
if (err) {
// the unit tests use matrix-mock-request, which throw the string "aborted" when aborting a request.
// See https://github.com/matrix-org/matrix-mock-request/blob/3276d0263a561b5b8326b47bae720578a2c7473a/src/index.js#L48
const aborted = err.name === "AbortError" || err === "aborted";
const aborted = err.name === "AbortError" || (err as any as string) === "aborted";
if (!aborted && !(err instanceof MatrixError)) {
// browser-request just throws normal Error objects,
// not `TypeError`s like fetch does. So just assume any
@@ -801,13 +916,15 @@ const requestCallback = function(
err = new ConnectionError("request failed", err);
}
}
let data: T | string = body;
if (!err) {
try {
const httpStatus = response.status || response.statusCode; // XMLHttpRequest vs http.IncomingMessage
if (httpStatus >= 400) {
if (getStatusCode(response) >= 400) {
err = parseErrorResponse(response, body);
} else if (bodyParser) {
body = bodyParser(body);
data = bodyParser(body);
}
} catch (e) {
err = new Error(`Error parsing server response: ${e}`);
@@ -816,21 +933,26 @@ const requestCallback = function(
if (err) {
defer.reject(err);
userDefinedCallback(err);
userDefinedCallback?.(err);
} else if (onlyData) {
defer.resolve(data as T);
userDefinedCallback?.(null, data as T);
} else {
const res = {
code: response.status || response.statusCode, // XMLHttpRequest vs http.IncomingMessage
const res: IResponse<T> = {
code: getStatusCode(response),
// XXX: why do we bother with this? it doesn't work for
// XMLHttpRequest, so clearly we don't use it.
headers: response.headers,
data: body,
headers: (response as IncomingMessage).headers,
data: data as T,
};
defer.resolve(onlyData ? body : res);
userDefinedCallback(null, onlyData ? body : res);
// XXX: the variations in caller-expected types here are horrible,
// typescript doesn't do conditional types based on runtime values
defer.resolve(res as any as T);
userDefinedCallback?.(null, res as any as T);
}
};
};
}
/**
* Attempt to turn an HTTP error response into a Javascript Error.
@@ -842,8 +964,8 @@ const requestCallback = function(
* @param {String} body raw body of the response
* @returns {Error}
*/
function parseErrorResponse(response, body) {
const httpStatus = response.status || response.statusCode; // XMLHttpRequest vs http.IncomingMessage
function parseErrorResponse(response: XMLHttpRequest | IncomingMessage, body?: string) {
const httpStatus = getStatusCode(response);
const contentType = getResponseContentType(response);
let err;
@@ -872,14 +994,14 @@ function parseErrorResponse(response, body) {
* @param {XMLHttpRequest|http.IncomingMessage} response response object
* @returns {{type: String, parameters: Object}?} parsed content-type header, or null if not found
*/
function getResponseContentType(response) {
function getResponseContentType(response: XMLHttpRequest | IncomingMessage): ParsedMediaType {
let contentType;
if (response.getResponseHeader) {
if ((response as XMLHttpRequest).getResponseHeader) {
// XMLHttpRequest provides getResponseHeader
contentType = response.getResponseHeader("Content-Type");
} else if (response.headers) {
contentType = (response as XMLHttpRequest).getResponseHeader("Content-Type");
} else if ((response as IncomingMessage).headers) {
// request provides http.IncomingMessage which has a message.headers map
contentType = response.headers['content-type'] || null;
contentType = (response as IncomingMessage).headers['content-type'] || null;
}
if (!contentType) {
@@ -893,6 +1015,12 @@ function getResponseContentType(response) {
}
}
interface IErrorJson extends Partial<IUsageLimit> {
[key: string]: any; // extensible
errcode?: string;
error?: string;
}
/**
* Construct a Matrix error. This is a JavaScript Error with additional
* information specific to the standard Matrix error response.
@@ -905,8 +1033,11 @@ function getResponseContentType(response) {
* @prop {integer} httpStatus The numeric HTTP status code given
*/
export class MatrixError extends Error {
constructor(errorJson) {
errorJson = errorJson || {};
public readonly errcode: string;
public readonly data: IErrorJson;
public httpStatus?: number; // set by http-api
constructor(errorJson: IErrorJson = {}) {
super(`MatrixError: ${errorJson.errcode}`);
this.errcode = errorJson.errcode;
this.name = errorJson.errcode || "Unknown error code";
@@ -923,18 +1054,13 @@ export class MatrixError extends Error {
* @constructor
*/
export class ConnectionError extends Error {
constructor(message, cause = undefined) {
constructor(message: string, private readonly cause: Error = undefined) {
super(message + (cause ? `: ${cause.message}` : ""));
this._cause = cause;
}
get name() {
return "ConnectionError";
}
get cause() {
return this._cause;
}
}
export class AbortError extends Error {
@@ -954,7 +1080,7 @@ export class AbortError extends Error {
* @return {any} the result of the network operation
* @throws {ConnectionError} If after maxAttempts the callback still throws ConnectionError
*/
export async function retryNetworkOperation(maxAttempts, callback) {
export async function retryNetworkOperation<T>(maxAttempts: number, callback: () => T): Promise<T> {
let attempts = 0;
let lastConnectionError = null;
while (attempts < maxAttempts) {
+1 -2
View File
@@ -21,7 +21,6 @@ limitations under the License.
import { logger } from './logger';
import { MatrixClient } from "./client";
import { defer, IDeferred } from "./utils";
import { MatrixError } from "./http-api";
const EMAIL_STAGE_TYPE = "m.login.email.identity";
const MSISDN_STAGE_TYPE = "m.login.msisdn";
@@ -49,7 +48,7 @@ export interface IAuthData {
flows?: IFlow[];
params?: Record<string, Record<string, any>>;
errcode?: string;
error?: MatrixError;
error?: string;
}
export enum AuthType {
+11 -3
View File
@@ -18,7 +18,8 @@ import { MatrixClient } from "../client";
import { IEncryptedFile, RelationType, UNSTABLE_MSC3089_BRANCH } from "../@types/event";
import { IContent, MatrixEvent } from "./event";
import { MSC3089TreeSpace } from "./MSC3089TreeSpace";
import type { ReadStream } from "fs";
import { EventTimeline } from "./event-timeline";
import { FileType } from "../http-api";
/**
* Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) branch - a reference
@@ -140,7 +141,14 @@ export class MSC3089Branch {
const room = this.client.getRoom(this.roomId);
if (!room) throw new Error("Unknown room");
const event = room.getUnfilteredTimelineSet().findEventById(this.id);
let event: MatrixEvent | undefined = room.getUnfilteredTimelineSet().findEventById(this.id);
// keep scrolling back if needed until we find the event or reach the start of the room:
while (!event && room.getLiveTimeline().getState(EventTimeline.BACKWARDS).paginationToken) {
await this.client.scrollback(room, 100);
event = room.getUnfilteredTimelineSet().findEventById(this.id);
}
if (!event) throw new Error("Failed to find event");
// Sometimes the event isn't decrypted for us, so do that. We specifically set `emit: true`
@@ -160,7 +168,7 @@ export class MSC3089Branch {
*/
public async createNewVersion(
name: string,
encryptedContents: File | String | Buffer | ReadStream | Blob,
encryptedContents: FileType,
info: Partial<IEncryptedFile>,
additionalContent?: IContent,
): Promise<void> {
+3 -3
View File
@@ -32,7 +32,7 @@ import {
import { MSC3089Branch } from "./MSC3089Branch";
import { isRoomSharedHistory } from "../crypto/algorithms/megolm";
import { ISendEventResponse } from "../@types/requests";
import type { ReadStream } from "fs";
import { FileType } from "../http-api";
/**
* The recommended defaults for a tree space's power levels. Note that this
@@ -282,7 +282,7 @@ export class MSC3089TreeSpace {
const members = this.room.currentState.getStateEvents(EventType.RoomMember);
for (const member of members) {
const isNotUs = member.getStateKey() !== this.client.getUserId();
if (isNotUs && kickMemberships.includes(member.getContent<string>()['membership'])) {
if (isNotUs && kickMemberships.includes(member.getContent().membership)) {
const stateKey = member.getStateKey();
if (!stateKey) {
throw new Error("State key not found for branch");
@@ -472,7 +472,7 @@ export class MSC3089TreeSpace {
*/
public async createFile(
name: string,
encryptedContents: File | String | Buffer | ReadStream | Blob,
encryptedContents: FileType,
info: Partial<IEncryptedFile>,
additionalContent?: IContent,
): Promise<ISendEventResponse> {
+7 -2
View File
@@ -53,6 +53,11 @@ export enum DuplicateStrategy {
Replace = "replace",
}
export interface IRoomTimelineData {
timeline: EventTimeline;
liveEvent?: boolean;
}
export class EventTimelineSet extends EventEmitter {
private readonly timelineSupport: boolean;
private unstableClientRelationAggregation: boolean;
@@ -209,7 +214,7 @@ export class EventTimelineSet extends EventEmitter {
*
* @fires module:client~MatrixClient#event:"Room.timelineReset"
*/
public resetLiveTimeline(backPaginationToken: string, forwardPaginationToken?: string): void {
public resetLiveTimeline(backPaginationToken?: string, forwardPaginationToken?: string): void {
// Each EventTimeline has RoomState objects tracking the state at the start
// and end of that timeline. The copies at the end of the live timeline are
// special because they will have listeners attached to monitor changes to
@@ -594,7 +599,7 @@ export class EventTimelineSet extends EventEmitter {
this.setRelationsTarget(event);
this.aggregateRelations(event);
const data = {
const data: IRoomTimelineData = {
timeline: timeline,
liveEvent: !toStartOfTimeline && timeline == this.liveTimeline && !fromCache,
};
+2 -2
View File
@@ -34,13 +34,13 @@ export class EventTimeline {
* Symbolic constant for methods which take a 'direction' argument:
* refers to the start of the timeline, or backwards in time.
*/
static BACKWARDS = Direction.Backward;
public static readonly BACKWARDS = Direction.Backward;
/**
* Symbolic constant for methods which take a 'direction' argument:
* refers to the end of the timeline, or forwards in time.
*/
static FORWARDS = Direction.Forward;
public static readonly FORWARDS = Direction.Forward;
/**
* Static helper method to set sender and target properties
+254 -33
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.
@@ -21,6 +21,7 @@ limitations under the License.
*/
import { EventEmitter } from 'events';
import { ExtensibleEvent, ExtensibleEvents, Optional } from "matrix-events-sdk";
import { logger } from '../logger';
import { VerificationRequest } from "../crypto/verification/request/VerificationRequest";
@@ -28,6 +29,7 @@ import {
EventType,
MsgType,
RelationType,
EVENT_VISIBILITY_CHANGE_TYPE,
} from "../@types/event";
import { Crypto, IEventDecryptionResult } from "../crypto";
import { deepSortedObjectEntries } from "../utils";
@@ -89,6 +91,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 +119,7 @@ export interface IEvent {
age?: number;
}
interface IAggregatedRelation {
export interface IAggregatedRelation {
origin_server_ts: number;
event_id?: string;
sender?: string;
@@ -122,9 +131,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,6 +185,30 @@ export interface IDecryptOptions {
isRetry?: boolean;
}
/**
* 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 class MatrixEvent extends EventEmitter {
private pushActions: IActionsObject = null;
private _replacingEvent: MatrixEvent = null;
@@ -150,6 +216,18 @@ export class MatrixEvent extends EventEmitter {
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 +269,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 +277,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;
@@ -263,10 +342,29 @@ export class MatrixEvent extends EventEmitter {
});
this.txnId = event.txn_id || null;
this.localTimestamp = Date.now() - this.getAge();
this.localTimestamp = Date.now() - (this.getAge() ?? 0);
this.reEmitter = new ReEmitter(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;
}
/**
* Gets the event as though it would appear unencrypted. If the event is already not
* encrypted, it is simply returned as-is.
@@ -339,10 +437,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 +507,13 @@ 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.threadId
|| this.getThread()?.id;
}
}
@@ -420,17 +521,20 @@ 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);
// 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 parentEventId(): string {
@@ -438,8 +542,13 @@ export class MatrixEvent extends EventEmitter {
}
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 +584,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 +680,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;
}
/**
@@ -771,7 +886,7 @@ export class MatrixEvent extends EventEmitter {
private badEncryptedMessage(reason: string): IEventDecryptionResult {
return {
clearEvent: {
type: "m.room.message",
type: EventType.RoomMessage,
content: {
msgtype: "m.bad.encrypted",
body: "** Unable to decrypt: " + reason + " **",
@@ -801,6 +916,7 @@ export class MatrixEvent extends EventEmitter {
this.forwardingCurve25519KeyChain =
decryptionResult.forwardingCurve25519KeyChain || [];
this.untrusted = decryptionResult.untrusted || false;
this.invalidateExtensibleEvent();
}
/**
@@ -818,7 +934,7 @@ export class MatrixEvent extends EventEmitter {
* @return {boolean} True if this event is encrypted.
*/
public isEncrypted(): boolean {
return !this.isState() && this.event.type === "m.room.encrypted";
return !this.isState() && this.event.type === EventType.RoomMessageEncrypted;
}
/**
@@ -904,6 +1020,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;
@@ -923,6 +1043,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("Event.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
@@ -972,6 +1139,8 @@ export class MatrixEvent extends EventEmitter {
delete content[key];
}
}
this.invalidateExtensibleEvent();
}
/**
@@ -989,7 +1158,55 @@ export class MatrixEvent extends EventEmitter {
* @return {boolean} True if this event is a redaction
*/
public isRedaction(): boolean {
return this.getType() === "m.room.redaction";
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());
}
/**
@@ -1053,6 +1270,8 @@ export class MatrixEvent extends EventEmitter {
// emit the event if it changed
this.emit("Event.localEventIdReplaced", this);
}
this.localTimestamp = Date.now() - this.getAge();
}
/**
@@ -1127,6 +1346,7 @@ export class MatrixEvent extends EventEmitter {
if (this._replacingEvent !== newEvent) {
this._replacingEvent = newEvent;
this.emit("Event.replaced", this);
this.invalidateExtensibleEvent();
}
}
@@ -1146,11 +1366,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 +1376,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 +1401,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)) {
@@ -1350,9 +1567,13 @@ export class MatrixEvent extends EventEmitter {
/**
* @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
@@ -1371,15 +1592,15 @@ const REDACT_KEEP_KEYS = new Set([
// a map from event type to the .content keys we keep when an event is redacted
const REDACT_KEEP_CONTENT_MAP = {
'm.room.member': { 'membership': 1 },
'm.room.create': { 'creator': 1 },
'm.room.join_rules': { 'join_rule': 1 },
'm.room.power_levels': {
[EventType.RoomMember]: { 'membership': 1 },
[EventType.RoomCreate]: { 'creator': 1 },
[EventType.RoomJoinRules]: { 'join_rule': 1 },
[EventType.RoomPowerLevels]: {
'ban': 1, 'events': 1, 'events_default': 1,
'kick': 1, 'redact': 1, 'state_default': 1,
'users': 1, 'users_default': 1,
},
'm.room.aliases': { 'aliases': 1 },
[EventType.RoomAliases]: { 'aliases': 1 },
};
/**
+38
View File
@@ -0,0 +1,38 @@
/*
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 } from "./relations";
import { MatrixEvent } from "./event";
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(ev: string, fn: (...params) => void) {
this.relations.forEach(r => r.on(ev, fn));
}
public off(ev: string, fn: (...params) => void) {
this.relations.forEach(r => r.off(ev, fn));
}
}
+2 -2
View File
@@ -16,7 +16,7 @@ limitations under the License.
import { EventEmitter } from 'events';
import { EventStatus, MatrixEvent } from './event';
import { EventStatus, MatrixEvent, IAggregatedRelation } from './event';
import { Room } from './room';
import { logger } from '../logger';
import { RelationType } from "../@types/event";
@@ -319,7 +319,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) => {
+8 -8
View File
@@ -618,14 +618,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;
+441 -135
View File
@@ -21,7 +21,7 @@ limitations under the License.
import { EventEmitter } from "events";
import { EventTimelineSet, DuplicateStrategy } from "./event-timeline-set";
import { EventTimeline } from "./event-timeline";
import { Direction, EventTimeline } from "./event-timeline";
import { getHttpUriForMxc } from "../content-repo";
import * as utils from "../utils";
import { normalize } from "../utils";
@@ -30,12 +30,16 @@ 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 {
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 { Method } from "../http-api";
// 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
@@ -92,7 +96,7 @@ interface ICachedReceipt {
data: IReceipt;
}
type ReceiptCache = Record<string, ICachedReceipt[]>;
type ReceiptCache = {[eventId: string]: ICachedReceipt[]};
interface IReceiptContent {
[eventId: string]: {
@@ -102,13 +106,43 @@ 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",
Total = "total",
}
export interface ICreateFilterOpts {
// Populate the filtered timeline with already loaded events in the room
// timeline. Useful to disable for some filters that can't be achieved by the
// client in an efficient manner
prepopulateTimeline?: boolean;
}
export class Room extends EventEmitter {
private readonly reEmitter: ReEmitter;
private txnToEvent: Record<string, MatrixEvent> = {}; // Pending in-flight requests { string: MatrixEvent }
@@ -118,8 +152,6 @@ export class Room extends EventEmitter {
// 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 notificationCounts: Partial<Record<NotificationCountType, number>> = {};
private readonly timelineSets: EventTimelineSet[];
// any filtered timeline sets we're maintaining for this room
@@ -135,21 +167,74 @@ export class Room extends EventEmitter {
private membersPromise?: Promise<boolean>;
// XXX: These should be read-only
/**
* The human-readable display name for this room.
*/
public name: string;
/**
* The un-homoglyphed name for this room.
*/
public normalizedName: string;
/**
* Dict of room tags; the keys are the tag name and the values
* are any metadata associated with the tag - e.g. { "fav" : { order: 1 } }
*/
public tags: Record<string, Record<string, any>> = {}; // $tagName: { $metadata: $value }
/**
* accountData Dict of per-room account_data events; the keys are the
* event type and the values are the events.
*/
public accountData: Record<string, MatrixEvent> = {}; // $eventType: $event
/**
* The room summary.
*/
public summary: RoomSummary = null;
/**
* A token which a data store can use to remember the state of the room.
*/
public readonly storageToken?: string;
// legacy fields
/**
* The live event timeline for this room, with the oldest event at index 0.
* Present for backwards compatibility - prefer getLiveTimeline().getEvents()
*/
public timeline: MatrixEvent[];
/**
* oldState The state of the room at the time of the oldest
* event in the live timeline. Present for backwards compatibility -
* prefer getLiveTimeline().getState(EventTimeline.BACKWARDS).
*/
public oldState: RoomState;
/**
* currentState The state of the room at the time of the
* newest event in the timeline. Present for backwards compatibility -
* prefer getLiveTimeline().getState(EventTimeline.FORWARDS).
*/
public currentState: RoomState;
/**
* @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.
@@ -191,26 +276,6 @@ export class Room extends EventEmitter {
* Optional. Set to true to enable client-side aggregation of event relations
* via `EventTimelineSet#getRelationsForEvent`.
* This feature is currently unstable and the API may change without notice.
*
* @prop {string} roomId The ID of this room.
* @prop {string} name The human-readable display name for this room.
* @prop {string} normalizedName The un-homoglyphed name for this room.
* @prop {Array<MatrixEvent>} timeline The live event timeline for this room,
* with the oldest event at index 0. Present for backwards compatibility -
* prefer getLiveTimeline().getEvents().
* @prop {object} tags Dict of room tags; the keys are the tag name and the values
* are any metadata associated with the tag - e.g. { "fav" : { order: 1 } }
* @prop {object} accountData Dict of per-room account_data events; the keys are the
* event type and the values are the events.
* @prop {RoomState} oldState The state of the room at the time of the oldest
* event in the live timeline. Present for backwards compatibility -
* prefer getLiveTimeline().getState(EventTimeline.BACKWARDS).
* @prop {RoomState} currentState The state of the room at the time of the
* newest event in the timeline. Present for backwards compatibility -
* prefer getLiveTimeline().getState(EventTimeline.FORWARDS).
* @prop {RoomSummary} summary The room summary.
* @prop {*} storageToken A token which a data store can use to remember
* the state of the room.
*/
constructor(
public readonly roomId: string,
@@ -231,7 +296,9 @@ 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(), [
"Room.timeline", "Room.timelineReset",
]);
this.fixUpLegacyTimelineFields();
@@ -660,7 +727,7 @@ export class Room extends EventEmitter {
const path = utils.encodeUri("/rooms/$roomId/members?" + queryString,
{ $roomId: this.roomId });
const http = this.client.http;
const response = await http.authedRequest(undefined, "GET", path);
const response = await http.authedRequest<{ chunk: IEvent[] }>(undefined, Method.Get, path);
return response.chunk;
}
@@ -778,7 +845,7 @@ export class Room extends EventEmitter {
* timeline which would otherwise be unable to paginate forwards without this token).
* Removing just the old live timeline whilst preserving previous ones is not supported.
*/
public resetLiveTimeline(backPaginationToken: string, forwardPaginationToken: string): void {
public resetLiveTimeline(backPaginationToken: string | null, forwardPaginationToken: string | null): void {
for (let i = 0; i < this.timelineSets.length; i++) {
this.timelineSets[i].resetLiveTimeline(
backPaginationToken, forwardPaginationToken,
@@ -1207,9 +1274,14 @@ export class Room extends EventEmitter {
/**
* Add a timelineSet for this room with the given filter
* @param {Filter} filter The filter to be applied to this timelineSet
* @param {Object=} opts Configuration options
* @param {*} opts.storageToken Optional.
* @return {EventTimelineSet} The timelineSet
*/
public getOrCreateFilteredTimelineSet(filter: Filter): EventTimelineSet {
public getOrCreateFilteredTimelineSet(
filter: Filter,
{ prepopulateTimeline = true }: ICreateFilterOpts = {},
): EventTimelineSet {
if (this.filteredTimelineSets[filter.filterId]) {
return this.filteredTimelineSets[filter.filterId];
}
@@ -1219,30 +1291,39 @@ export class Room extends EventEmitter {
this.filteredTimelineSets[filter.filterId] = timelineSet;
this.timelineSets.push(timelineSet);
// populate up the new timelineSet with filtered events from our live
// unfiltered timeline.
//
// XXX: This is risky as our timeline
// may have grown huge and so take a long time to filter.
// see https://github.com/vector-im/vector-web/issues/2109
const unfilteredLiveTimeline = this.getLiveTimeline();
// Not all filter are possible to replicate client-side only
// When that's the case we do not want to prepopulate from the live timeline
// as we would get incorrect results compared to what the server would send back
if (prepopulateTimeline) {
// populate up the new timelineSet with filtered events from our live
// unfiltered timeline.
//
// XXX: This is risky as our timeline
// may have grown huge and so take a long time to filter.
// see https://github.com/vector-im/vector-web/issues/2109
unfilteredLiveTimeline.getEvents().forEach(function(event) {
timelineSet.addLiveEvent(event);
});
unfilteredLiveTimeline.getEvents().forEach(function(event) {
timelineSet.addLiveEvent(event);
});
// find the earliest unfiltered timeline
let timeline = unfilteredLiveTimeline;
while (timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)) {
timeline = timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS);
// find the earliest unfiltered timeline
let timeline = unfilteredLiveTimeline;
while (timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)) {
timeline = timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS);
}
timelineSet.getLiveTimeline().setPaginationToken(
timeline.getPaginationToken(EventTimeline.BACKWARDS),
EventTimeline.BACKWARDS,
);
} else {
const livePaginationToken = unfilteredLiveTimeline.getPaginationToken(Direction.Forward);
timelineSet
.getLiveTimeline()
.setPaginationToken(livePaginationToken, Direction.Backward);
}
timelineSet.getLiveTimeline().setPaginationToken(
timeline.getPaginationToken(EventTimeline.BACKWARDS),
EventTimeline.BACKWARDS,
);
// alternatively, we could try to do something like this to try and re-paginate
// in the filtered events from nothing, but Mark says it's an abuse of the API
// to do so:
@@ -1268,7 +1349,7 @@ export class Room extends EventEmitter {
}
}
public findThreadForEvent(event: MatrixEvent): Thread {
public findThreadForEvent(event: MatrixEvent): Thread | null {
if (!event) {
return null;
}
@@ -1296,15 +1377,24 @@ export class Room extends EventEmitter {
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);
try {
let eventData;
if (event.threadRootId) {
eventData = await this.client.fetchRoomEvent(this.roomId, event.threadRootId);
}
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);
}
events.unshift(rootEvent);
thread = new Thread(events, this, this.client);
this.threads.set(thread.id, thread);
this.reEmitter.reEmit(thread, [ThreadEvent.Update, ThreadEvent.Ready]);
this.emit(ThreadEvent.New, thread);
}
if (event.getUnsigned().transaction_id) {
@@ -1319,6 +1409,32 @@ export class Room extends EventEmitter {
this.emit(ThreadEvent.Update, thread);
}
public createThread(rootEvent: MatrixEvent, events?: MatrixEvent[]): Thread | undefined {
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,
ThreadEvent.Ready,
"Room.timeline",
"Room.timelineReset",
]);
if (!this.lastThread || this.lastThread.rootEvent.localTimestamp < rootEvent.localTimestamp) {
this.lastThread = thread;
}
this.emit(ThreadEvent.New, thread);
return thread;
}
}
/**
* Add an event to the end of this room's live timelines. Will fire
* "Room.timeline".
@@ -1356,6 +1472,15 @@ export class Room extends EventEmitter {
// (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
@@ -1365,6 +1490,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) {
@@ -1431,7 +1565,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);
const thread = this.findThreadForEvent(event);
if (this.opts.pendingEventOrdering === PendingEventOrdering.Detached && !thread) {
if (this.pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) {
logger.warn("Setting event as NOT_SENT due to messages in the same state");
@@ -1460,8 +1594,7 @@ export class Room extends EventEmitter {
}
} else {
if (thread) {
thread.timelineSet.addEventToTimeline(event,
thread.timelineSet.getLiveTimeline(), false);
thread.addEvent(event, false);
} else {
for (let i = 0; i < this.timelineSets.length; i++) {
const timelineSet = this.timelineSets[i];
@@ -1586,7 +1719,7 @@ 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 {
@@ -1747,7 +1880,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);
}
@@ -1853,19 +1986,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;
}
/**
@@ -1929,84 +2055,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
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("Room.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;
}
/**
@@ -2072,8 +2229,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));
}
/**
@@ -2159,15 +2317,7 @@ export class Room extends EventEmitter {
}
}
let alias = this.getCanonicalAlias();
if (!alias) {
const aliases = this.getAltAliases();
if (aliases.length) {
alias = aliases[0];
}
}
const alias = this.getCanonicalAlias();
if (alias) {
return alias;
}
@@ -2254,6 +2404,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);
}
}
/**
@@ -2264,12 +2569,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,
@@ -2422,3 +2727,4 @@ function memberNamesToRoomName(names: string[], count = (names.length + 1)) {
* @param {string} membership The new membership value
* @param {string} prevMembership The previous membership value
*/
+207 -74
View File
@@ -15,11 +15,15 @@ limitations under the License.
*/
import { MatrixClient } from "../matrix";
import { MatrixEvent } from "./event";
import { EventTimeline } from "./event-timeline";
import { ReEmitter } from "../ReEmitter";
import { RelationType } from "../@types/event";
import { IRelationsRequestOpts } from "../@types/requests";
import { MatrixEvent, IThreadBundledRelationship } from "./event";
import { Direction, EventTimeline } from "./event-timeline";
import { EventTimelineSet } 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",
@@ -29,14 +33,16 @@ export enum ThreadEvent {
ViewThread = "Thred.viewThread",
}
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;
/**
* A reference to all the events ID at the bottom of the threads
*/
@@ -44,25 +50,57 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
private _currentUserParticipated = false;
private reEmitter: ReEmitter;
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.room = opts.room;
this.client = opts.client;
this.timelineSet = new EventTimelineSet(this.room, {
unstableClientRelationAggregation: true,
timelineSupport: true,
pendingEvents: true,
});
events.forEach(event => this.addEvent(event));
this.reEmitter = new ReEmitter(this);
room.on("Room.localEchoUpdated", this.onEcho);
room.on("Room.timeline", this.onEcho);
this.reEmitter.reEmit(this.timelineSet, [
"Room.timeline",
"Room.timelineReset",
]);
// 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));
this.room.on("Room.localEchoUpdated", this.onEcho);
this.room.on("Room.timeline", this.onEcho);
}
public get hasServerSideSupport(): boolean {
return this.client.cachedCapabilities
?.capabilities?.[RelationType.Thread]?.enabled;
}
onEcho = (event: MatrixEvent) => {
@@ -71,6 +109,10 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
}
};
public get roomState(): RoomState {
return this.room.getLiveTimeline().getState(EventTimeline.FORWARDS);
}
/**
* Add an event to the thread and updates
* the tail/root references if needed
@@ -78,43 +120,104 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
* @param event The event to add
*/
public async addEvent(event: MatrixEvent, toStartOfTimeline = false): Promise<void> {
if (this.timelineSet.findEventById(event.getId()) || event.status !== null) {
return;
// Add all incoming events to the thread's timeline set when there's
// no server support
if (!this.hasServerSideSupport) {
if (this.timelineSet.findEventById(event.getId())) {
return;
}
// all the relevant membership info to hydrate events with a sender
// is held in the main room timeline
// We want to fetch the room state from there and pass it down to this thread
// timeline set to let it reconcile an event with its relevant RoomMember
event.setThread(this);
this.timelineSet.addEventToTimeline(
event,
this.liveTimeline,
toStartOfTimeline,
false,
this.roomState,
);
await this.client.decryptEventIfNeeded(event, {});
}
if (!this.root) {
if (event.isThreadRelation) {
this.root = event.threadRootId;
} else {
this.root = event.getId();
if (this.hasServerSideSupport && this.initialEventsFetched) {
if (event.localTimestamp > this.lastReply().localTimestamp && !this.findEventById(event.getId())) {
this.timelineSet.addEventToTimeline(
event,
this.liveTimeline,
false,
false,
this.roomState,
);
}
}
// 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 (!this.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 (this.hasServerSideSupport) {
this.replyCount++;
}
this.emit(ThreadEvent.NewReply, this, event);
}
}
this.emit(ThreadEvent.Update, this);
}
private initialiseThread(rootEvent: MatrixEvent | undefined): void {
const bundledRelationship = rootEvent
?.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
if (this.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;
}
if (!bundledRelationship && rootEvent) {
this.addEvent(rootEvent);
}
}
public async fetchInitialEvents(): Promise<boolean> {
try {
await this.fetchEvents();
this.initialEventsFetched = true;
return true;
} catch (e) {
return false;
}
}
private setEventMetadata(event: MatrixEvent): void {
EventTimeline.setEventMetadata(event, this.roomState, false);
event.setThread(this);
}
/**
@@ -127,28 +230,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;
}
/**
@@ -157,28 +249,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 {
@@ -188,4 +270,55 @@ 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;
}> {
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 = [originalEvent, ...events];
}
for (const event of events) {
await this.client.decryptEventIfNeeded(event);
this.setEventMetadata(event);
}
const prependEvents = !opts.direction || opts.direction === Direction.Backward;
this.timelineSet.addEventsToTimeline(
events,
prependEvents,
this.liveTimeline,
prependEvents ? nextBatch : prevBatch,
);
return {
originalEvent,
events,
prevBatch,
nextBatch,
};
}
}
+1 -1
View File
@@ -112,7 +112,7 @@ export class RoomHierarchy {
if (!this.backRefs.has(childRoomId)) {
this.backRefs.set(childRoomId, []);
}
this.backRefs.get(childRoomId).push(ev.room_id);
this.backRefs.get(childRoomId).push(room.room_id);
// fill viaMap
if (Array.isArray(ev.content.via)) {
+1 -1
View File
@@ -70,7 +70,7 @@ export class MatrixScheduler<T = ISendEventResponse> {
}
// we ship with browser-request which returns { cors: rejected } when trying
// with no connection, so if we match that, give up since they have no conn.
if (err.cors === "rejected") {
if (err["cors"] === "rejected") {
return -1;
}
+1 -1
View File
@@ -231,7 +231,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
* @returns {null} in case the members for this room haven't been stored yet
*/
public getOutOfBandMembers(roomId: string): Promise<IEvent[] | null> {
return new Promise<IEvent[] | null>((resolve, reject) =>{
return new Promise<IEvent[] | null>((resolve, reject) => {
const tx = this.db.transaction(["oob_membership_events"], "readonly");
const store = tx.objectStore("oob_membership_events");
const roomIndex = store.index("room");
+2 -2
View File
@@ -256,8 +256,8 @@ function removeByPrefix(store, prefix) {
}
}
function debuglog() {
function debuglog(...args) {
if (DEBUG) {
logger.log(...arguments);
logger.log(...args);
}
}
+1
View File
@@ -32,6 +32,7 @@ interface IOpts {
export interface IMinimalEvent {
content: IContent;
type: EventType | string;
unsigned?: IUnsigned;
}
export interface IEphemeral {
+40 -24
View File
@@ -34,23 +34,22 @@ import { PushProcessor } from "./pushprocessor";
import { logger } from './logger';
import { InvalidStoreError } from './errors';
import { IStoredClientOpts, MatrixClient, PendingEventOrdering } from "./client";
import { SyncState } from "./sync.api";
import {
Category,
IEphemeral,
IInvitedRoom,
IInviteState,
IJoinedRoom,
ILeftRoom,
IStateEvent,
IMinimalEvent,
IRoomEvent,
IStateEvent,
IStrippedState,
ISyncResponse,
ITimeline,
IEphemeral,
IMinimalEvent,
} from "./sync-accumulator";
import { MatrixEvent } from "./models/event";
import { MatrixError } from "./http-api";
import { MatrixError, Method } from "./http-api";
import { ISavedSync } from "./store";
import { EventType } from "./@types/event";
import { IPushRules } from "./@types/PushRules";
@@ -68,6 +67,15 @@ const BUFFER_PERIOD_MS = 80 * 1000;
// keepAlive is successful but the server /sync fails.
const FAILED_SYNC_ERROR_THRESHOLD = 3;
export enum SyncState {
Error = "ERROR",
Prepared = "PREPARED",
Stopped = "STOPPED",
Syncing = "SYNCING",
Catchup = "CATCHUP",
Reconnecting = "RECONNECTING",
}
function getFilterName(userId: string, suffix?: string): string {
// scope this on the user ID because people may login on many accounts
// and they all need to be stored!
@@ -87,13 +95,19 @@ interface ISyncOptions {
}
export interface ISyncStateData {
error?: Error;
error?: MatrixError;
oldSyncToken?: string;
nextSyncToken?: string;
catchingUp?: boolean;
fromCache?: boolean;
}
enum SetPresence {
Offline = "offline",
Online = "online",
Unavailable = "unavailable",
}
interface ISyncParams {
filter?: string;
timeout: number;
@@ -101,7 +115,7 @@ interface ISyncParams {
// eslint-disable-next-line camelcase
full_state?: boolean;
// eslint-disable-next-line camelcase
set_presence?: "offline" | "online" | "unavailable";
set_presence?: SetPresence;
_cacheBuster?: string | number; // not part of the API itself
}
@@ -187,6 +201,7 @@ export class SyncApi {
"Room.accountData",
"Room.myMembership",
"Room.replaceEvent",
"Room.visibilityChange",
]);
this.registerStateListeners(room);
return room;
@@ -260,16 +275,16 @@ export class SyncApi {
getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter,
).then(function(filterId) {
qps.filter = filterId;
return client.http.authedRequest(
undefined, "GET", "/sync", qps, undefined, localTimeoutMs,
return client.http.authedRequest<any>( // TODO types
undefined, Method.Get, "/sync", qps as any, undefined, localTimeoutMs,
);
}).then((data) => {
let leaveRooms = [];
if (data.rooms && data.rooms.leave) {
if (data.rooms?.leave) {
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) {
@@ -295,7 +310,7 @@ export class SyncApi {
EventTimeline.BACKWARDS);
this.processRoomEvents(room, stateEvents, timelineEvents);
this.processThreadEvents(room, threadedEvents);
await this.processThreadEvents(room, threadedEvents);
room.recalculate();
client.store.storeRoom(room);
@@ -400,9 +415,10 @@ export class SyncApi {
}
// FIXME: gut wrenching; hard-coded timeout values
this.client.http.authedRequest(undefined, "GET", "/events", {
// TODO types
this.client.http.authedRequest<any>(undefined, Method.Get, "/events", {
room_id: peekRoom.roomId,
timeout: 30 * 1000,
timeout: String(30 * 1000),
from: token,
}, undefined, 50 * 1000).then((res) => {
if (this._peekRoom !== peekRoom) {
@@ -470,7 +486,7 @@ export class SyncApi {
return this.syncStateData;
}
public async recoverFromSyncStartupError(savedSyncPromise: Promise<void>, err: Error): Promise<void> {
public async recoverFromSyncStartupError(savedSyncPromise: Promise<void>, err: MatrixError): Promise<void> {
// Wait for the saved sync to complete - we send the pushrules and filter requests
// before the saved sync has finished so they can run in parallel, but only process
// the results after the saved sync is done. Equivalently, we wait for it to finish
@@ -865,8 +881,8 @@ export class SyncApi {
private doSyncRequest(syncOptions: ISyncOptions, syncToken: string): IRequestPromise<ISyncResponse> {
const qps = this.getSyncParams(syncOptions, syncToken);
return this.client.http.authedRequest(
undefined, "GET", "/sync", qps, undefined,
return this.client.http.authedRequest( // TODO types
undefined, Method.Get, "/sync", qps as any, undefined,
qps.timeout + BUFFER_PERIOD_MS,
);
}
@@ -901,7 +917,7 @@ export class SyncApi {
};
if (this.opts.disablePresence) {
qps.set_presence = "offline";
qps.set_presence = SetPresence.Offline;
}
if (syncToken) {
@@ -924,7 +940,7 @@ export class SyncApi {
return qps;
}
private onSyncError(err: Error, syncOptions: ISyncOptions): void {
private onSyncError(err: MatrixError, syncOptions: ISyncOptions): void {
if (!this.running) {
debuglog("Sync no longer running: exiting");
if (this.connectionReturnedDefer) {
@@ -1291,7 +1307,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);
// set summary after processing events,
// because it will trigger a name calculation
@@ -1350,7 +1366,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);
@@ -1359,7 +1375,7 @@ export class SyncApi {
const [timelineEvents, threadedEvents] = this.client.partitionThreadedEvents(events);
this.processRoomEvents(room, stateEvents, timelineEvents);
this.processThreadEvents(room, threadedEvents);
await this.processThreadEvents(room, threadedEvents);
room.addAccountData(accountDataEvents);
room.recalculate();
@@ -1477,7 +1493,7 @@ export class SyncApi {
this.client.http.request(
undefined, // callback
"GET", "/_matrix/client/versions",
Method.Get, "/_matrix/client/versions",
undefined, // queryParams
undefined, // data
{
@@ -1704,7 +1720,7 @@ export class SyncApi {
/**
* @experimental
*/
private processThreadEvents(room: Room, threadedEvents: MatrixEvent[]): void {
private processThreadEvents(room: Room, threadedEvents: MatrixEvent[]): Promise<void> {
return this.client.processThreadEvents(room, threadedEvents);
}
+9 -3
View File
@@ -937,7 +937,7 @@ export class MatrixCall extends EventEmitter {
const upgradeVideo = video && !this.hasLocalUserMediaVideoTrack;
logger.debug(`Upgrading call: audio?=${upgradeAudio} video?=${upgradeVideo}`);
const stream = await this.client.getMediaHandler().getUserMediaStream(upgradeAudio, upgradeVideo);
const stream = await this.client.getMediaHandler().getUserMediaStream(upgradeAudio, upgradeVideo, false);
if (upgradeAudio && upgradeVideo) {
if (this.hasLocalUserMediaAudioTrack) return;
if (this.hasLocalUserMediaVideoTrack) return;
@@ -1717,7 +1717,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 => {
@@ -1973,7 +1978,7 @@ export class MatrixCall extends EventEmitter {
}
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 +1986,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();
}
+13 -4
View File
@@ -54,9 +54,12 @@ 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();
@@ -78,7 +81,9 @@ export class MediaHandler {
stream = await navigator.mediaDevices.getUserMedia(constraints);
}
this.userMediaStreams.push(stream);
if (reusable) {
this.userMediaStreams.push(stream);
}
return stream;
}
@@ -101,9 +106,11 @@ export class MediaHandler {
}
/**
* @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 +132,9 @@ export class MediaHandler {
stream = matchingStream.clone();
}
this.screensharingStreams.push(stream);
if (reusable) {
this.screensharingStreams.push(stream);
}
return stream;
}
+1067 -1130
View File
File diff suppressed because it is too large Load Diff