Compare commits

..

602 Commits

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

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

* Make sure to enable tracks after a call upgrade

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

* Simplify `updateMuteStatus()`

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

* Add copyright for 2022

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

* Add `updateLocalUsermediaStream()`

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

* Support mid-call device changes

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

* Use `updateLocalUsermediaStream()` for call upgrades

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

* Improve mock classes

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

* Add new tests

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

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

* lint

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

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

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

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

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

* docs: update JSDoc to match
2022-02-21 15:45:43 +00:00
Germain 4e72290d53 Null-guard for undefined rootEvent when creating a thread (#2187) 2022-02-21 15:06:01 +00:00
Patrick Cloke 7a7318b636 Fix incorrect type referenced in receiptCacheByEventId. (#2185) 2022-02-17 17:39:17 +00:00
RiotRobot 74d24f38f7 Merge branch 'master' into develop 2022-02-17 11:48:50 +00:00
RiotRobot 78dcae9143 v15.5.2 2022-02-17 11:45:23 +00:00
RiotRobot 0db640a679 Prepare changelog for v15.5.2 2022-02-17 11:45:22 +00:00
David Baker a3ddfd519b Fix synthetic read receipt handling (#2174) (#2183)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-02-17 11:29:28 +00:00
Travis Ralston e86d8861b9 Add functions to support refresh tokens (#2178)
* Add functions for refreshing access tokens

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

* Appease the linter

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

* Make type safe check work

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

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

* May as well have a stack trace too
2022-02-10 22:33:49 +00:00
Germain 6b822ccd61 Improve thread partitioning for 2nd degree relations (#2165) 2022-02-10 15:09:46 +00:00
Hubert Chathi 47c5c4645e Check the backup info against the stored private key when determining trust. (#2167) 2022-02-10 08:34:21 -05:00
Hubert Chathi ea0eaff212 Back up keys before logging out (#2158) 2022-02-09 08:36:43 -05:00
David Baker 41bf8c2d5f Yarn upgrade (#2164) 2022-02-08 19:17:28 +00:00
RiotRobot d473a2e095 v15.5.1-rc.1 2022-02-08 15:32:28 +00:00
RiotRobot d398ffd6df Prepare changelog for v15.5.1-rc.1 2022-02-08 15:32:27 +00:00
Michael Telatynski ffab55452a Fix issue with rooms not getting marked as unread (#2163) 2022-02-08 12:32:14 +00:00
Germain 9120b1dfd9 Expose room state helper (#2162) 2022-02-08 12:08:43 +00:00
Šimon Brandner 5d4e3183aa Don't store streams that are only used once (#2157)
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-02-08 10:13:58 +00:00
Michael Telatynski 07171a95c4 Fix edge cases around RR calculations (#2160) 2022-02-07 17:49:14 +00:00
Šimon Brandner 12afcd3850 Account for encryption in maySendMessage() (#2159) 2022-02-07 15:32:21 +00:00
Andy Balaam 2d23330b74 Send references to thread root to threads, even out of order (#2156)
Co-authored-by: Germain <germains@element.io>
2022-02-04 15:50:06 +00:00
Germain 7faff66006 Out of sync thread summary in main timeline (#2153) 2022-02-04 11:57:42 +00:00
Michael Telatynski b07457726b Avoid re-doing as much work when processing m.receipt event (#2151) 2022-02-03 11:06:19 +00:00
Germain 6bf8142ff6 Fix initial sync fail when event fetching unsuccessful (#2150) 2022-02-02 17:56:43 +00:00
Christian Paul cf0ccaf93d TypeScript: opts parameter of client.startClient is optional (#2106) 2022-02-02 15:29:58 +00:00
Christian Paul 34a3955b60 MatrixEvent.getRoomId can return undefined (#2036) 2022-02-02 16:22:57 +01:00
Germain 82122872bf Add thread relation in sendEvent if it's missing (#2149) 2022-02-02 14:44:38 +00:00
Germain 9c242a9ce6 Keep a reference to the latest thread created (#2148) 2022-02-01 14:50:42 +00:00
Germain 51f3fac87b Add thread relation to sticker message (#2147) 2022-02-01 10:36:04 +00:00
Germain 66b98844a2 Refactor thread model to be created from the root event (#2142) 2022-02-01 08:58:39 +00:00
David Baker d03db00e4c Yarn upgrade (#2146)
Apologies if this means everyone has to `rm -rf node_modules`: react-ace
decided to turn some paths that were regular files into directories
and it turns out this really confuses yarn: https://github.com/securingsincity/react-ace/issues/1048
2022-01-31 16:45:42 +00:00
RiotRobot cf8d2bf6ef Resetting package fields for development 2022-01-31 14:44:13 +00:00
RiotRobot 913a0b51a6 Merge branch 'master' into develop 2022-01-31 14:44:13 +00:00
RiotRobot ede0f696ee v15.5.0 2022-01-31 14:41:10 +00:00
RiotRobot d05214a169 Prepare changelog for v15.5.0 2022-01-31 14:41:09 +00:00
David Baker 1e93d0b19f Bump node-fetch from 2.6.6 to 2.6.7 (#2140) (#2145)
Bumps [node-fetch](https://github.com/node-fetch/node-fetch) from 2.6.6 to 2.6.7.
- [Release notes](https://github.com/node-fetch/node-fetch/releases)
- [Commits](https://github.com/node-fetch/node-fetch/compare/v2.6.6...v2.6.7)

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

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

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

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

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

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

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

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

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

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

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

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

* update events-sdk for polls

* Update events-sdk for polls bugfix

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

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

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

* Decorate messages with MSC1767 when appropriate

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

* Include the SDK

* Appease linter and tests

* Change property name to appease linter

* Update SDK
2022-01-13 09:56:11 -07:00
David Teller 4acb98c496 [Fix] Nullcheck redactedEvent (#2103)
We should only check whether a redacted event is a visibility event *if the redacted event is not null*.
2022-01-12 13:27:03 +01:00
David Teller 96d1b30012 MSC3531: Hiding messages during moderation (#2041) 2022-01-12 11:27:33 +01:00
David Baker 6fc586598a Yarn upgrade (#2101) 2022-01-11 17:17:01 +00:00
Michael Telatynski 2d9c938765 Support cancelling events whilst they are in status = ENCRYPTING (#2095) 2022-01-11 15:03:33 +00:00
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
RiotRobot 22a98b3e4f v15.3.0-rc.1 2021-12-14 14:37:58 +00:00
RiotRobot 934a0e8943 Prepare changelog for v15.3.0-rc.1 2021-12-14 14:37:58 +00:00
Michael Telatynski 963c7690b6 Iterate typing to work towards noImplicitAny (#2061) 2021-12-14 14:32:35 +00:00
Michael Telatynski aeec4aa4a8 Update typescript-eslint plugin & parser (#2064) 2021-12-13 23:28:54 +00:00
Michael Telatynski 169b6b5572 Filter out falsey opts in /relations API hits (#2059) 2021-12-13 15:38:03 +00:00
RiotRobot 6244d77d44 Merge branch 'master' into develop 2021-12-13 15:22:07 +00:00
RiotRobot f4839a3b4f v15.2.1 2021-12-13 15:17:37 +00:00
RiotRobot 4b21b67a45 Prepare changelog for v15.2.1 2021-12-13 15:17:37 +00:00
David Baker 799a83a115 Update olm to 3.2.8 (#2062) 2021-12-13 14:59:03 +00:00
Michael Telatynski f3ebb2a314 Update Typescript to 4.5 (#2060) 2021-12-13 11:22:50 +00:00
Michael Telatynski c0fbed914e Update olm in yarn.lock (#2058) 2021-12-10 10:49:35 +00:00
Hubert Chathi dd8c157bb9 Improve fallback key behaviour (#2037) 2021-12-09 19:00:23 -05:00
Travis Ralston 8bd2d640f2 Update eslint 2021-12-09 16:01:07 -07:00
Aaron R 80aaa6c32b Fix imports to work with new lint rules (#2003)
* Add eslint-plugin-import

Signed-off-by: Aaron Raimist <aaron@raim.ist>

* Autofix

Signed-off-by: Aaron Raimist <aaron@raim.ist>

* Manual fix

Signed-off-by: Aaron Raimist <aaron@raim.ist>
2021-12-09 15:57:43 -07:00
Michael Telatynski 4e9fe8f53f Fix types (#2056) 2021-12-09 15:44:07 +00:00
Ajay Bura f926a2f777 Fix paginateEventTimeline resolve to boolean (#2054)
* Fix paginateEventTimeline resolve to boolean

Earlier paginateEventTimeline used to resolve to  chunk instead of returning boolean. Now it return boolean as specified in doc.

* added type
2021-12-09 20:00:55 +05:30
Michael Telatynski f8097221e6 Improve typing (#2055) 2021-12-09 14:22:58 +00:00
Callum Brown 95e7a76ba9 Finish adding registration token UIAA type (#2048)
Signed-off-by: Callum Brown <callum@calcuode.com>
2021-12-08 19:38:22 -07:00
Germain 72bc13eb39 Allow to omit optional params for /relations calls (#2053) 2021-12-08 08:23:53 +00:00
Germain 2206b80e65 Threads notifications after app startup (#2043) 2021-12-07 10:58:34 +00:00
Germain 39a8399977 Add new room event filter fields (#2051) 2021-12-07 09:42:52 +00:00
RiotRobot b7247fcedc Resetting package fields for development 2021-12-06 15:14:16 +00:00
RiotRobot e4494707fa Merge branch 'master' into develop 2021-12-06 15:14:16 +00:00
RiotRobot 45b7cab203 v15.2.0 2021-12-06 15:10:51 +00:00
RiotRobot 4dfc3b9dbb Prepare changelog for v15.2.0 2021-12-06 15:10:50 +00:00
Hugh Nimmo-Smith 15d05cd92f Fix incorrect MSC3089 typings and add null checks (#2049)
* Fix MSC3089TreeSpace typings to match actual code

* Implement strict null checks to resolve TS strictNullChecks=true issues
2021-12-06 10:54:43 +00:00
skyka13711 ba1b1cc6fa fix exports (#2047) 2021-12-02 19:40:10 -07:00
Michael Telatynski b716cde9b5 Add method to fetch /account/whoami (#2046) 2021-12-02 13:43:15 +00:00
RiotRobot 46a0645865 v15.2.0-rc.1 2021-11-30 18:16:24 +00:00
RiotRobot 2b30f45f60 Prepare changelog for v15.2.0-rc.1 2021-11-30 18:16:23 +00:00
J. Ryan Stinnett 87b920698f Upgrade allchange to 1.0.6 (#2042) 2021-11-30 17:55:38 +00:00
Germain 4f4811e1d9 Emit thread update after event decryption (#2040) 2021-11-30 14:40:59 +00:00
Hugh Nimmo-Smith db9936e07c Standardise content type handling in MSC3089 createFile() and createNewVersion() (#2014)
* Provide cross platform compatible versions of createFile() and createNewVersion()

The exist implementations are deprecated as they only work in a browser and support a different type of contents from MatrixClient.uploadContent()

* Fix MSC3089 content upload meta data in NodeJS runtime

* Break unstable createFile() and createNewVersion() instead of deprecating

Test using NodeJS types instead of mocked browser Blob

* chore: remove incorrect comment
2021-11-30 09:02:41 +00:00
Hugh Nimmo-Smith 7dcee2eb40 Implementation of deriveKey() for NodeJS (#2021)
Based on approach used by aes.ts
2021-11-30 09:02:26 +00:00
skyka13711 6b34ea6fe5 Exporting missing types (#2023)
* fix types export

* fix lint

* index.ts fix exports

* revert yarn.lock

Co-authored-by: Alexey Avramenko <2822698@gmail.com>
Co-authored-by: nikita <nikita@42px.ru>
2021-11-29 19:54:08 -07:00
Germain b33b01df0f Handle remote echo when creating a thread (#2038) 2021-11-25 08:27:13 +00:00
Germain 1c895432ed Fix backwards compatibility for redactEvent (#2039) 2021-11-24 15:42:10 +00:00
Germain ddd6a05198 Make local echo work for threads (#2026) 2021-11-24 08:19:17 +00:00
Germain 0ccde7807f Comprehensive events emitter typing (#2034) 2021-11-23 15:16:35 +00:00
Will Hunt 9f97992196 getStateEvent should return any (#2032)
* getStateEvent returns any

getStateEvent returns the content of an event, not the whole state event as per https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-rooms-roomid-state-eventtype-statekey

* Make the return type Record<string, any>

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2021-11-23 13:54:54 +00:00
Germain 67b8da719f Add a flag to track user participation in a thread (#2030) 2021-11-23 08:20:27 +00:00
Šimon Brandner 04fad564f7 Fix call upgrades (#2024)
* Init call feeds with non-muted state

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

* Improve CallFeed docs

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-11-22 18:17:35 -07:00
Germain aee698c285 Add KeyVerificationRequest msgType (#2033) 2021-11-22 17:08:24 +00:00
RiotRobot 55b489b17a Resetting package fields for development 2021-11-22 13:32:08 +00:00
RiotRobot c5d188c30a Merge branch 'master' into develop 2021-11-22 13:32:08 +00:00
RiotRobot 6fff3d6db5 v15.1.1 2021-11-22 13:28:39 +00:00
RiotRobot 8043d83921 Prepare changelog for v15.1.1 2021-11-22 13:28:39 +00:00
Michael Telatynski b2d83c1f80 Iterate type definitions (#2029) 2021-11-19 17:35:09 +00:00
Travis Ralston 1b91cd7037 Add optional force parameter when ensuring Olm sessions (#2027)
This is useful for debugging crypto where Element's `/discardsession` doesn't reset the Olm session.
2021-11-19 07:33:45 -07:00
skyka13711 0baf926c84 TimelineWindow fix load function signature (#2028) 2021-11-19 09:00:08 +00:00
Robert Long 763e25e031 Update sync state to error when aborting (#2025) 2021-11-18 12:16:57 -08:00
RiotRobot 861023b3f6 v15.1.1-rc.1 2021-11-17 13:52:47 +00:00
RiotRobot 1f5a83994b Prepare changelog for v15.1.1-rc.1 2021-11-17 13:52:47 +00:00
Dariusz Niemczyk af523522de Add LocalStorageErrorEventListener (#2019)
Add a way to listen to LocalStorage error events from element-web
and matrix-react-sdk in the same manner.

Related: https://github.com/vector-im/element-web/issues/18423
2021-11-15 18:08:20 +00:00
Dariusz Niemczyk c3a266b3e7 Implement TypedEventEmitter for better TS support (#2018)
We're using stringly typed events everywhere, this is the first step for
better typescript support with our event emitters before we replace it
with something much better for React support.
2021-11-15 14:47:21 +00:00
Callum Brown f41d815aa6 Add registration token UIA type (#2020) 2021-11-15 09:16:23 +00:00
Germain ad8a93dde8 Fix check supportExperimentalThreads (#2017) 2021-11-10 10:54:13 +00:00
Germain b07d44a6c0 Getter for last thread reply (#2015) 2021-11-09 14:46:48 +00:00
RiotRobot 1dc899ba6e Resetting package fields for development 2021-11-08 17:37:26 +00:00
RiotRobot 0b1e3edaff Merge branch 'master' into develop 2021-11-08 17:37:25 +00:00
RiotRobot 9fb4fed044 v15.1.0 2021-11-08 17:34:08 +00:00
RiotRobot 730b0b121d Prepare changelog for v15.1.0 2021-11-08 17:34:07 +00:00
Aaron R 9d9d9e2cfa Fix edit history being broken after editing an unencrypted event with an encrypted event (#2013) 2021-11-08 08:55:41 +00:00
Germain 43bc09f392 Copy relations to thread root in the thread timeline (#2012) 2021-11-05 14:16:45 +00:00
Germain 195498e9db Make events pagination responses parse threads (#2011) 2021-11-04 11:29:14 +00:00
Faye Duxovni 50332c4999 End authentication attempt immediately if we couldn't find an appropriate flow (#2008)
If `doRequest()` in `interactive-auth.ts` fails to obtain an appropriate authentication flow, it should return immediately after rejecting the promise.  If it continues, it'll attempt to check `chosenFlow.stages`, which will cause an error because `chosenFlow` is `null`.  This was breaking the interactive auth spec tests with Node 16.
2021-11-02 10:40:46 -04:00
RiotRobot 9e46646b16 v15.1.0-rc.1 2021-11-02 14:09:01 +00:00
RiotRobot a74a03e414 Prepare changelog for v15.1.0-rc.1 2021-11-02 14:09:00 +00:00
David Baker fe8b9eca8e Update allchange to 1.0.5 (#2010) 2021-11-02 13:30:49 +00:00
Germain a7cc1ae630 Fetch thread root event if it's missing from the sync (#2009) 2021-11-02 11:56:43 +00:00
Šimon Brandner 14e008bbad Don't emit CallError when we fail to get screen-sharing stream (#2005)
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-28 16:23:06 +01:00
Travis Ralston 6a12c265cf PSFD-455: Try to set a sender on search result events if possible (#2004)
This is to ensure that search results have aesthetic information such as display name and avatar. Though the membership event won't be in context for when the event was sent, it'll at least be something better than a bare user ID.
2021-10-28 07:48:27 -06:00
Šimon Brandner c35cb57a79 Port some changes from group calls branch to develop (#2001)
* Add some useful getters

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

Co-authored-by: Robert Long <robert@robertlong.me>

* Add removeLocalFeed()

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

Co-authored-by: Robert Long <robert@robertlong.me>

* Don't updateStream() if they're the same

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

Co-authored-by: Robert Long <robert@robertlong.me>

* Add isSpeaking()

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

Co-authored-by: Robert Long <robert@robertlong.me>

* Improve speaking detection using history

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

Co-authored-by: Robert Long <robert@robertlong.me>

* Make code for placing and answering calls more flexible

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

Co-authored-by: Robert Long <robert@robertlong.me>

* Correctly log stream id

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

* Remove mistaken parameter

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

* Add a unit

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

Co-authored-by: Robert Long <robert@robertlong.me>
2021-10-26 11:59:15 -07:00
Christian Paul 51c776f51e Correct TypeScript signature of client.sendCompleteEvent (#1997)
* Correct TypeScript signature of client.sendCompleteEvent

* Revert renaming txnId to txnIdOrCallback
2021-10-25 14:25:38 -06:00
Hubert Chathi 7ce0eb26d4 Fetch room membership from server rather than relying on stored data (#1998)
To work around the possibility that the persisted sync data may be out of date.
2021-10-25 10:29:46 -04:00
RiotRobot f0091fa811 Resetting package fields for development 2021-10-25 11:25:30 +01:00
RiotRobot 25c88bc986 Merge branch 'master' into develop 2021-10-25 11:25:29 +01:00
RiotRobot ab06f57965 v15.0.0 2021-10-25 11:22:22 +01:00
RiotRobot c46209a159 Prepare changelog for v15.0.0 2021-10-25 11:22:21 +01:00
Germain 0c47e27f9f Add explicit options to disable pending events (#1993) 2021-10-22 09:23:12 +01:00
Hubert Chathi 0385f265e8 Mark old verification methods as deprecated (#1994)
The old verification methods start a verification by sending an
`m.key.verification.start` message, rather than an `m.key.verification.request`
message.  This behaviour is deprecated in the
spec (https://github.com/matrix-org/matrix-doc/pull/3122), so these functions
should be deprecated too.
2021-10-21 16:10:13 -04:00
David Baker 59b3960a42 Strip direction override characters from display names (#1992)
Strip RLO and LRO characters from name and rawDisplayName so they
can safely be embedded into other text without messing up the text
ordering.

Fixes https://github.com/vector-im/element-web/issues/1712
2021-10-20 17:08:40 +01:00
Germain d5ab4ba2ef Update thread replies count to only include m.thread (#1991) 2021-10-20 16:49:54 +01:00
Travis Ralston f59b81f46b Update allchange (#1989) 2021-10-19 18:21:52 +01:00
Michael Telatynski 4c39ce8c96 Add method to fetch the MSC3266 Room Summary of a Room (#1988) 2021-10-19 16:13:29 +01:00
Germain Souquet 94ff0ea4cd Upgrade yarn dependencies 2021-10-19 11:24:53 +01:00
RiotRobot 526758a07f v15.0.0-rc.1 2021-10-19 10:50:42 +01:00
RiotRobot 9686fba81e Prepare changelog for v15.0.0-rc.1 2021-10-19 10:50:41 +01:00
Germain bbc547fd7f Merge pull request #1982 from matrix-org/gsouquet/thread-event-new 2021-10-15 14:32:19 +01:00
Germain 7d5dbeaf2d Merge pull request #1981 from matrix-org/gsouquet/threads-root-guard 2021-10-15 14:32:13 +01:00
Germain Souquet 8c19cf45e2 Dispatch a Thread.New event 2021-10-15 11:09:14 +01:00
Germain Souquet c9277de8ee Remove console statement 2021-10-15 10:31:43 +01:00
Germain Souquet b5baca51a6 Null-guard for thread root event not loaded yet 2021-10-15 10:22:32 +01:00
Germain fc800821f9 Merge pull request #1980 from matrix-org/gsouquet/threads-relations 2021-10-15 08:53:42 +01:00
Germain Souquet efbc46937f Null-guard for event that do not have a thread 2021-10-14 17:37:33 +01:00
Germain Souquet 9ceeb2d220 Delete UnstableValue and use hardcoded value for m.thread 2021-10-14 17:09:00 +01:00
Germain Souquet 7b89862ed0 Make threads use 'm.thread' relation 2021-10-14 16:56:48 +01:00
David Baker 6804e4290a Merge pull request #1972 from SimonBrandner/feature/answer-no-cam
Try to answer a call without video if we can't access the camera
2021-10-14 12:15:35 +01:00
David Baker 0a3d820fda Merge pull request #1943 from freaktechnik/develop
Fix `requestVerificationDM` with chronological `pendingEventOrdering`
2021-10-14 12:14:20 +01:00
Michael Telatynski e2a9c95ea8 Merge pull request #1974 from SimonBrandner/task/security-ts 2021-10-14 10:02:14 +01:00
Šimon Brandner 3aefc9f02e Add tests for falling back to answering with no video
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-14 08:46:36 +02:00
Šimon Brandner 0042cb5292 Fix importRoomKeys() definition
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-13 16:50:27 +02:00
Denis Kasak b288c97372 Merge pull request #1975 from matrix-org/dkasak/more-detailed-room-key-sharing-logging
More detailed logging when sharing room keys
2021-10-12 13:26:43 +00:00
Denis Kasak a0de1740a4 Add comma per review
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2021-10-12 13:23:22 +00:00
Denis Kasak 8e2e3018d8 Log device objects for existing/new devices when sharing room keys. 2021-10-12 13:48:41 +02:00
Denis Kasak 60e93024e7 Fix comment regarding marking blocked devices as notified. 2021-10-12 13:43:28 +02:00
Denis Kasak d89aa365f9 More detailed logging of blocked devices.
Separately log how many blocked devices there are in total and list them
out.

Then, log how many devices were notified because they were newly
blocked and list those out too.
2021-10-12 13:43:28 +02:00
Denis Kasak 4241263f2c Make log message regarding notifying unnotified failed devices clearer. 2021-10-12 13:43:26 +02:00
Denis Kasak b009f9c21b Log more clearly when we're sharing room keys with *new* Olm sessions. 2021-10-12 13:41:04 +02:00
Denis Kasak 04ccec6de3 Log which devices room keys were shared with. 2021-10-12 13:41:04 +02:00
RiotRobot e7b41fadd0 Merge branch 'master' into develop 2021-10-12 08:50:43 +01:00
RiotRobot 2dc1acef2e Resetting package fields for development 2021-10-12 08:36:13 +01:00
RiotRobot b977b629f4 Merge branch 'release-v14.0.1' 2021-10-12 08:35:42 +01:00
RiotRobot 6bbd493e20 v14.0.1 2021-10-12 08:29:30 +01:00
RiotRobot 38ad435af5 Prepare changelog for v14.0.1 2021-10-12 08:29:30 +01:00
RiotRobot 8f245af317 Merge branch 'master' into develop 2021-10-11 11:48:22 +01:00
RiotRobot 9b2bfbe7d0 v14.0.0 2021-10-11 11:44:54 +01:00
RiotRobot f69ea85e55 Prepare changelog for v14.0.0 2021-10-11 11:44:53 +01:00
Šimon Brandner 7559a10526 Make opts in importRoomKeys optional
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-10 08:36:11 +02:00
Šimon Brandner 68dbe959bb Add a logline
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-10 08:19:29 +02:00
Šimon Brandner 74a875aa23 Add MockMediaDeviceInfo
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-10 08:19:29 +02:00
Šimon Brandner 4b679c5056 Try to answer without video if we can't access it
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-10 08:19:22 +02:00
Šimon Brandner cb8a260b9c Improve handling when we don't have a camera
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-09 08:36:19 +02:00
Robert Long 7d7975b8bb Fix connecting to a call without a webcam
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-09 08:05:59 +02:00
Travis Ralston f12513da96 Merge pull request #1971 from matrix-org/travis/spam-log
Remove excess logging from getRoomUpgradeHistory()
2021-10-08 10:33:07 -06:00
Travis Ralston 785a3e3381 Remove excess logging from getRoomUpgradeHistory()
On large accounts this can now cause a massive amount of spam. It's not really providing any value in day-to-day debugging either.
2021-10-08 10:28:20 -06:00
RiotRobot f5fcc20840 v14.0.0-rc.2 2021-10-07 17:42:40 +01:00
RiotRobot 17e20c17b3 Prepare changelog for v14.0.0-rc.2 2021-10-07 17:42:40 +01:00
Michael Telatynski 39fd350ffe Merge pull request #1969 from matrix-org/t3chguy/cp/space-hierarchy 2021-10-06 16:51:32 +01:00
Michael Telatynski b4d100baff Tweak Room Hierarchy root visibility and add loading getter 2021-10-06 13:29:13 +01:00
Michael Telatynski 29bc83f4a7 Merge pull request #1968 from matrix-org/t3chguy/fix/19276 2021-10-06 13:27:12 +01:00
Michael Telatynski 13c8774d45 Tweak Room Hierarchy root visibility and add loading getter 2021-10-06 10:25:27 +01:00
Travis Ralston c9212e770d Merge pull request #1966 from Alexendoo/declaration-map
Enable TypeScript declaration maps
2021-10-05 13:39:47 -06:00
Germain 08aa7803e3 Merge pull request #1964 from SimonBrandner/feature/call-feed-opts 2021-10-04 15:02:49 +01:00
Alex Macleod 770858c255 Enable TypeScript declaration maps
This allows go-to-definition to reach the original .ts files for
consumers of the package. It emits .d.ts.map files alongside the regular
.d.ts files

e.g. in sdk.createClient go-to-definition on createClient will place the
cursor at the function in src/matrix.ts rather than inside a definition
file lib/matrix.d.ts

Signed-off-by: Alex Macleod <alex@macleod.io>
2021-10-04 14:36:50 +01:00
Germain Souquet c73bb7d408 Update dependencies 2021-10-04 12:30:14 +01:00
RiotRobot 15cd675354 v14.0.0-rc.1 2021-10-04 11:55:19 +01:00
RiotRobot e8137d3f88 Prepare changelog for v14.0.0-rc.1 2021-10-04 11:55:19 +01:00
Šimon Brandner c63abe9988 Use ICallFeedOpts in the CallFeed constructor
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-02 08:12:43 +02:00
Germain a7a08fd760 Merge pull request #1962 from matrix-org/gsouquet/fix-19229 2021-10-01 15:46:30 +01:00
Germain df51f95fbd Merge pull request #1963 from matrix-org/gsouquet/fix-19225 2021-10-01 12:11:35 +01:00
Germain Souquet a14cf1a339 Remove unused DuplicateStrategy 2021-10-01 12:07:16 +01:00
Germain Souquet 30758600f0 Prepend events to thread when fetching the reply chain 2021-10-01 10:49:46 +01:00
Germain 823242a93c Merge pull request #1959 from psrpinto/fix/joined-rooms-response 2021-09-30 17:54:53 +01:00
Martin Giger 482e3c4cb7 Skip waiting if already sent and abort on cancel 2021-09-30 16:58:08 +02:00
Germain Souquet 1dbd7158ad Prevent redactions to be sent as pending events
Co-authored-by: Dariusz Niemczyk <Palid@users.noreply.github.com>
2021-09-30 13:56:52 +01:00
Michael Telatynski 46b3f0babd Merge pull request #1958 from SimonBrandner/fix/call-length 2021-09-29 15:04:02 +01:00
Šimon Brandner b4bc554d7a Make sure to callLengthInterval only once
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-29 15:46:22 +02:00
Paulo Pinto 1485969493 Return IJoinedRoomsResponse from getJoinedRooms(), instead of string[]
Signed-off-by: Paulo Pinto <paulo.pinto@automattic.com>
2021-09-29 11:45:01 +01:00
Travis Ralston 92f7ddcf3e Merge pull request #1954 from SimonBrandner/task/src-ts
Changes for TS migration
2021-09-27 13:45:06 -06:00
Travis Ralston 90d480b4cf Merge pull request #1952 from matrix-org/travis/fsdk/versions
Implement file versioning for tree spaces
2021-09-27 10:04:22 -06:00
RiotRobot 2515d07c8f Resetting package fields for development 2021-09-27 14:23:47 +01:00
RiotRobot 7acb9416c2 Merge branch 'master' into develop 2021-09-27 14:23:47 +01:00
RiotRobot 095e3a8a88 v13.0.0 2021-09-27 14:20:00 +01:00
RiotRobot 92623a246a Prepare changelog for v13.0.0 2021-09-27 14:20:00 +01:00
Michael Telatynski 0c4212ca90 Merge pull request #1935 from matrix-org/t3chguy/fix/18969 2021-09-27 11:08:07 +01:00
Germain 9a06a05a75 Merge pull request #1953 from SimonBrandner/fix/audio-upgrade 2021-09-27 10:18:13 +01:00
Šimon Brandner f05a0615f7 Export IRequestMsisdnTokenResponse
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 09:45:07 +02:00
Šimon Brandner 6b93687ff0 Pass correct params to upgradeCall() to upgrade audio
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-26 13:10:00 +02:00
Travis Ralston 8d910d5e26 Merge pull request #1950 from SimonBrandner/feature/flexible-answer
Allow answering calls without audio/video
2021-09-25 23:39:20 -06:00
Šimon Brandner 9264050f41 Update src/webrtc/call.ts
Co-authored-by: Travis Ralston <travpc@gmail.com>
2021-09-26 07:26:08 +02:00
Travis Ralston eab0c54663 Appease the linter 2021-09-25 20:47:59 -06:00
Travis Ralston 82a254b7bd Implement file versioning for tree spaces
The diff isn't super clear on how this works, but the general idea is that the MSC3089Branches (files) now know which directory they came from, and the directories (MSC3089TreeSpace) can tell when files are triggering uploads referencing themselves so it can add all the replacement metadata onto the event.

There's a few challenges with how relations work in the js-sdk which has shaped the API surface exposed by this change. Specifically, there is a `getVersionHistory()` function instead of a function to get a certain prior version: the js-sdk doesn't track parent events in the relation structures so cannot determine what the event could have replaced. 

In order to trigger the built-in relations structures, we must trigger decryption with `emit: true`. This is so an internal listener in the relations system can pick up `Event.decrypted`.
2021-09-25 20:44:17 -06:00
Šimon Brandner 0f6a59ed98 Allow answering calls without audio/video
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-25 10:00:51 +02:00
Travis Ralston afb4682987 Merge pull request #1949 from SimonBrandner/fix/mic-state
Fix not returning new mic mute state
2021-09-25 01:14:11 -06:00
Šimon Brandner a18326519b Add missing return statement
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-25 08:26:13 +02:00
Germain b033540f95 Merge pull request #1948 from matrix-org/gsouquet/fix-sync-partitioning 2021-09-24 15:43:27 +01:00
Germain Souquet ff529d733d Fix event partitioning from non threading ready clients 2021-09-24 14:54:40 +01:00
Germain 5951958c05 Merge pull request #1945 from matrix-org/gsouquet/thread-serialise-18720 2021-09-24 11:17:39 +01:00
David Baker 886f2fb95d Merge pull request #1947 from matrix-org/dbkr/we_like_tests
Add tests requirements to CONTRIBUTING
2021-09-23 18:15:32 +01:00
David Baker f6ce0c581e Add tests requirements to CONTRIBUTING
Fixes https://github.com/vector-im/element-web/issues/19136
2021-09-23 14:09:01 +01:00
Germain 5ce68b6c12 Merge pull request #1944 from matrix-org/gsouquet/thread-state-events-18719 2021-09-23 09:17:02 +01:00
Germain Souquet c748aebfc5 Add context to explain why we fetch roomstate in threads 2021-09-22 17:36:12 +01:00
Germain Souquet f8ec445ff0 Fix addLiveEvent with 'replace' strategy 2021-09-22 17:14:18 +01:00
Germain Souquet 5d4f347eaa Add thread serialisation helper 2021-09-22 11:35:19 +01:00
Germain Souquet 1359c0574e Share room state events with thread's timelines 2021-09-22 11:19:08 +01:00
Travis Ralston b1d239b292 Merge pull request #1934 from aaronraimist/kick-api
Switch to /kick API
2021-09-21 23:30:23 -06:00
Travis Ralston 70031415e8 Merge pull request #1938 from matrix-org/travis/fix-widgets-relations
Ensure unencrypted fields get exposed by getEffectiveEvent()
2021-09-21 23:16:18 -06:00
Martin Giger 3c97d17770 Fix requestVerificationDM with chronological pendingEventOrdering
Signed-off-by: Martin Giger <martin@humanoids.be>
2021-09-21 20:04:07 +02:00
Travis Ralston 5a73a9758a Merge pull request #1941 from SimonBrandner/feature/bound
Add `bound` to `IThreepid`
2021-09-21 11:36:36 -06:00
Travis Ralston 6848e96244 Merge pull request #1942 from SimonBrandner/feature/trusted_locally
Add `trusted_locally` to `TrustInfo`
2021-09-21 11:26:35 -06:00
Travis Ralston b53566e074 Merge pull request #1827 from SimonBrandner/feature/call-upgrades
Support for call upgrades
2021-09-21 09:10:02 -06:00
Šimon Brandner 012b914a97 Add trusted_locally to TrustInfo
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-21 16:20:39 +02:00
Šimon Brandner 3c6eed0135 Add bound to IThreepid
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-21 14:56:33 +02:00
RiotRobot f2a2e7f5dd v13.0.0-rc.1 2021-09-21 09:24:22 +01:00
RiotRobot 15b6a9a257 Prepare changelog for v13.0.0-rc.1 2021-09-21 09:24:21 +01:00
Šimon Brandner b83977e72f Add @param docs
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-20 16:40:46 +02:00
Travis Ralston 8f5082cd2f Merge pull request #1940 from SimonBrandner/feature/getHistoryVisibility
Add `getHistoryVisibility()` and `getGuestAccess()`
2021-09-20 08:34:14 -06:00
David Baker 50c877cb60 Merge pull request #1939 from SimonBrandner/fix/stream-stopping
Fix stream stopping and improve logging
2021-09-20 11:56:36 +01:00
Šimon Brandner e3e7fd5900 Add getGuestAccess()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-19 10:47:18 +02:00
Šimon Brandner 69823f9cae Add getHistoryVisibility()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-19 10:36:45 +02:00
Šimon Brandner e5dcdf109a Fix stream stopping and improve logging
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-19 09:29:16 +02:00
Šimon Brandner 5c6a643436 Merge branch 'feature/call-upgrades' of https://github.com/SimonBrandner/matrix-js-sdk into feature/call-upgrades 2021-09-19 07:44:40 +02:00
Šimon Brandner 1ca1a69eb8 Fix docs
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-19 07:44:28 +02:00
Šimon Brandner 76ea1ba192 Improve log
Co-authored-by: Travis Ralston <travpc@gmail.com>
2021-09-19 07:43:09 +02:00
Šimon Brandner 8cd8ec134e Merge remote-tracking branch 'upstream/develop' into feature/call-upgrades 2021-09-19 07:42:04 +02:00
Travis Ralston 6c649e2bfe Merge pull request #1937 from SimonBrandner/fix/getUserMediaStream
Fix `getUserMediaStream()` not being able to handle streams of different track combinations
2021-09-18 16:29:15 -06:00
Travis Ralston 9f42647a75 Ensure unencrypted fields get exposed by getEffectiveEvent()
Fixes https://github.com/vector-im/element-web/issues/19062
2021-09-18 16:26:08 -06:00
Šimon Brandner 9747e5e0d5 Fix getUserMediaStream() not being able to handle streams of different track combinations
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-18 13:18:28 +02:00
Šimon Brandner a4cacbc73a Add call upgrade support
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-18 12:32:13 +02:00
Šimon Brandner f24840d09c Make CallFeed handle addTrack events
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-18 12:31:54 +02:00
Šimon Brandner acb7991cc5 Fix getUserMediaStream() not being able to handle streams of different track combinations
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-18 12:27:24 +02:00
Šimon Brandner 6addc7dd3d Add MockMediaStream
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-18 12:27:08 +02:00
Eric Eastwood 2bd60f4160 Merge pull request #1933 from matrix-org/madlittlemods/more-better-pr-descriptions
Update PR guidelines for easier review
2021-09-17 11:54:13 -05:00
David Baker e71362a6df Merge pull request #1936 from matrix-org/dbkr/await_save_backup_key
Fix race on automatic backup restore
2021-09-17 17:06:43 +01:00
David Baker 673c09cd77 Fix race on automatic backup restore
We forgot to await the saving of the backup key, so there was a race
where it would fail to get the backup key because it hadn't been saved
yet, so the backup wouldn't be restored automatically.

Fixes https://github.com/vector-im/element-web/issues/17781
2021-09-17 15:53:14 +01:00
Michael Telatynski 4606422e57 Make RoomState::hasSufficientPowerLevelFor public 2021-09-17 15:35:03 +01:00
Dariusz Niemczyk 4bc8f968e0 Merge pull request #1932 from matrix-org/palid/threads-typescriptify
Fix PR comments
2021-09-17 08:11:36 +02:00
Aaron Raimist 35ee58dfe1 Switch to /kick API
Signed-off-by: Aaron Raimist <aaron@raim.ist>
2021-09-17 01:05:21 -05:00
Eric Eastwood 3705426b6d Make PR's easier to review 2021-09-16 16:05:49 -05:00
Dariusz Niemczyk 5813aea27f Properly rename ThreadEvents to ThreadEvent 2021-09-16 17:49:31 +02:00
Dariusz Niemczyk 5ba836a31b Fix PR comments 2021-09-16 17:12:57 +02:00
Dariusz Niemczyk 4f387d15b9 Merge pull request #1930 from matrix-org/palid/threads-typescriptify
Typescriptify thread events a bit
2021-09-16 17:12:02 +02:00
Dariusz Niemczyk 8ab0246ca2 Fix PR comments 2021-09-16 16:03:47 +02:00
Dariusz Niemczyk ed68bd4108 Typescriptify thread events a bit 2021-09-16 15:59:41 +02:00
Dariusz Niemczyk 3d39e8ec26 Merge pull request #1880 from matrix-org/palid/fix/dont-persist-log-level
Palid/fix/dont persist log level
2021-09-16 14:52:52 +02:00
Robert Long 53bdab7cc8 Merge pull request #1929 from matrix-org/robertlong/datachannels
Add WebRTC DataChannels to MatrixCall
2021-09-15 14:25:13 -07:00
Robert Long fafa55ba1d Merge pull request #1928 from matrix-org/robertlong/clone-streams
Clone and stop local media streams from mediaHandler
2021-09-15 14:25:00 -07:00
Travis Ralston 3a818c1419 Merge pull request #1927 from SimonBrandner/feature/getBuffer
Add `getBuffer()` to `QRCodeData`
2021-09-15 14:42:11 -06:00
Robert Long 422853298c Reset media stream arrays 2021-09-15 13:20:52 -07:00
Robert Long f3a2bd6b40 Clean up stopAllMedia 2021-09-15 12:38:12 -07:00
Robert Long 6834137f50 Add datachannels to MatrixCall 2021-09-15 12:28:09 -07:00
Robert Long c4d7fef0cd Clone and stop local media streams from mediaHandler 2021-09-15 12:08:28 -07:00
Šimon Brandner 556b5ff628 Add getBuffer() to QRCodeData
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-15 20:27:13 +02:00
David Baker 2daa1ec4ce Merge pull request #1916 from matrix-org/dbkr/verify_key_share_tests
Tests for key sharing security fix
2021-09-15 18:38:03 +01:00
Travis Ralston 348ea72b70 Merge pull request #1925 from SimonBrandner/task/setCodecPreferences
Add `setCodecPreferences()` to `global.d.ts`
2021-09-15 09:48:27 -06:00
Šimon Brandner 4e14d810c5 Add setCodecPreferences()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-15 17:44:16 +02:00
Travis Ralston f84905b003 Merge pull request #1924 from SimonBrandner/revert-1923-task/die-rtx-hack
Revert "Remove RTX codec hack "
2021-09-15 09:23:27 -06:00
Šimon Brandner 9bb87145f6 Revert "Remove RTX codec hack " 2021-09-15 17:17:56 +02:00
Travis Ralston c7675570aa Merge pull request #1923 from SimonBrandner/task/die-rtx-hack
Remove RTX codec hack
2021-09-15 09:16:55 -06:00
Šimon Brandner be942fd66a Remove RTX codec hack as the issue is no longer present on Chromium 90
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-15 16:38:59 +02:00
James Salter 8544aca362 Merge pull request #1922 from matrix-org/revert-errors-ts
Revert "Migrate errors to TypeScript"
2021-09-15 12:08:32 +01:00
James Salter c3308e1de9 Revert "Migrate errors to TypeScript"
This reverts commit ab906faa17.
2021-09-15 12:01:34 +01:00
David Baker 047d7125a1 Merge pull request #1920 from matrix-org/dbkr/fix_autodiscovery_types
Fix types
2021-09-15 11:29:10 +01:00
David Baker cf4d3117c1 Fix types
This is 'any' (defined as any by IWellKnownConfig.raw) rather than
string (which the jsdoc claimed). We were calling this method ourselves
but with an object that was also 'any', so it was fine, but react-sdk
calls it with an object literal, which typescript knows it not a string.
2021-09-15 10:29:14 +01:00
David Baker 88c2844e48 Merge pull request #1919 from matrix-org/dbkr/yarnupgrade_20210914
yarn upgrade
2021-09-15 10:01:18 +01:00
Germain 6ca4694fe6 Merge pull request #1907 from matrix-org/gsouquet/ts-last-push 2021-09-15 08:30:52 +01:00
Germain 062287e270 Merge pull request #1913 from matrix-org/gsouquet/ts-security 2021-09-15 08:27:29 +01:00
Germain Souquet 50e0d76f10 remove erroneous comment related to typings 2021-09-15 09:26:25 +02:00
Travis Ralston df039d3d8c Merge pull request #1911 from SimonBrandner/feature/media-handling
Improve `MatrixCall` media handling and internally remove `CallType`s
2021-09-14 14:40:45 -06:00
Travis Ralston 799606b73c Merge pull request #1909 from matrix-org/travis/fsdk/locking
Add file locking to MSC3089 branches
2021-09-14 12:39:41 -06:00
Travis Ralston c048f5d357 Merge pull request #1910 from matrix-org/travis/fsdk/tests
Add missing tests for MSC3089 implementation
2021-09-14 12:37:54 -06:00
Robert Long f712a8cecb Merge pull request #1903 from matrix-org/robertlong/fix-register-types
Add types for the MatrixClient register method
2021-09-14 10:56:12 -07:00
David Baker e1d8d6bf4e Not typescript again
because this time they just removed setCodecPreferences from webrtc?
2021-09-14 18:52:35 +01:00
Robert Long 14ed8a110c Update src/client.ts
Co-authored-by: Travis Ralston <travisr@matrix.org>
2021-09-14 10:50:50 -07:00
David Baker cb6700f21b yarn upgrade 2021-09-14 18:41:14 +01:00
Travis Ralston d4f35bf07a fix main package? 2021-09-14 10:03:23 -06:00
RiotRobot b4eb29138b Resetting package fields for development 2021-09-14 15:52:38 +01:00
RiotRobot 8ca3a071f9 Merge branch 'master' into develop 2021-09-14 15:51:45 +01:00
RiotRobot 5abd211436 Merge branch 'release-v12.5.0' 2021-09-14 15:51:13 +01:00
RiotRobot 99f428e091 v12.5.0 2021-09-14 15:46:09 +01:00
RiotRobot a734fdf9b0 Prepare changelog for v12.5.0 2021-09-14 15:46:08 +01:00
David Baker 6f91b2bc80 Merge pull request #1917 from matrix-org/dbkr/verify_keyshare_tests_125rel
Tests for key sharing security fix
2021-09-14 15:33:05 +01:00
David Baker 32d7272939 Tests for key sharing security fix
Tests for 894c24880d
2021-09-14 15:24:15 +01:00
David Baker c8d85452bf Merge remote-tracking branch 'origin/develop' into dbkr/verify_key_share_tests 2021-09-14 15:16:49 +01:00
RiotRobot 557f800919 Verify target device key on reshare 2021-09-14 15:14:46 +01:00
David Baker 2119e88c78 Tests for key sharing security fix
Tests for 894c24880d
2021-09-14 13:11:18 +01:00
Travis Ralston 24e7a7e6bf Merge pull request #1915 from SimonBrandner/feature/has-been-cancelled
Add `hasBeenCancelled` to `VerificationBase`
2021-09-13 20:54:03 -06:00
Šimon Brandner 4e5847f356 Add hasBeenCancelled to VerificationBase
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-13 18:01:24 +02:00
RiotRobot 72d580edfd Merge branch 'master' into develop 2021-09-13 12:50:02 +01:00
RiotRobot e878b7683b v12.4.1 2021-09-13 12:47:04 +01:00
RiotRobot fac700bf4d Changelog for 12.4.1 2021-09-13 12:43:13 +01:00
RiotRobot 894c24880d Verify target device key on reshare 2021-09-13 12:34:48 +01:00
Germain d742249fd1 Merge pull request #1914 from matrix-org/gsouquet/ts-misc
Types adjustment for TS migration
2021-09-13 10:00:26 +01:00
Germain Souquet 9c100fea48 Types update for UserView TS migration 2021-09-12 10:12:59 +02:00
Germain Souquet 7daab62850 Types fixes for CreateKeyBackupDialog TS migration 2021-09-12 09:21:04 +02:00
Germain Souquet fed0ced89d Type fixes for CreateSecretStorageDialog TS migration 2021-09-12 08:56:31 +02:00
Germain Souquet df9cfaed1d Merge branch 'develop' into gsouquet/ts-last-push 2021-09-11 16:18:25 +01:00
Germain Souquet edd614dd0c fix cyclic dependencies for AutoDiscoveryActionEnum 2021-09-11 16:17:40 +01:00
Germain Souquet 375b228bb2 fix missing types and linting errors 2021-09-11 16:06:29 +01:00
Šimon Brandner 7143ef8a32 Internally remove call types
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-11 08:21:58 +02:00
Šimon Brandner 1bd7de5a18 Use MediaHandler
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-11 08:21:57 +02:00
Šimon Brandner a1f73eee86 Add MediaHandler
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-11 08:21:51 +02:00
Travis Ralston d9f8710758 Add missing tests for MSC3089 implementation
Fixes https://github.com/vector-im/element-web/issues/18461
2021-09-10 14:44:50 -06:00
Travis Ralston 3a9d5439a2 Add tests 2021-09-10 14:18:11 -06:00
Travis Ralston 5d1be6e8be Add file locking to MSC3089 branches
This isn't hooked up to versioning, yet, but will be eventually.
2021-09-10 14:15:32 -06:00
Germain 775e85a2e8 Merge pull request #1906 from matrix-org/gsouquet/threads-relations-18721 2021-09-10 19:22:41 +01:00
Travis Ralston dc8f403c44 Merge pull request #1908 from SimonBrandner/task/dialogs-ts
Add `ISasEvent`
2021-09-10 12:17:41 -06:00
Šimon Brandner cf9fa991d1 Add ISasEvent
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-10 19:18:19 +02:00
Robert Long d26dc49755 Merge pull request #1904 from matrix-org/robertlong/callfeed-dispose
Properly dispose of CallFeeds
2021-09-10 10:07:10 -07:00
Germain be04559a66 Merge pull request #1878 from SimonBrandner/task/i-hate-my-code 2021-09-10 16:16:51 +01:00
Germain Souquet 0722dafe58 Migrate AutoDiscovery to TypeScript 2021-09-10 16:04:55 +01:00
Germain Souquet ab906faa17 Migrate errors to TypeScript 2021-09-10 15:39:36 +01:00
Germain Souquet 9d63a12469 Migrate realtime-callbacks to TypeScript 2021-09-10 15:31:21 +01:00
Germain Souquet f40d0d24c7 Move relations and redactions to thread timeline 2021-09-10 09:21:25 +01:00
Germain Souquet 2cdea36abf Merge branch 'develop' into gsouquet/threads-relations-18721 2021-09-10 08:18:03 +01:00
Travis Ralston 8d4f4c8832 Merge pull request #1901 from matrix-org/travis/rl/perf-metrics
[Release] Exclude opt-in Element performance metrics from encryption
2021-09-09 18:27:25 -06:00
Robert Long 3b4dcbb01d Properly dispose of CallFeeds 2021-09-09 17:15:41 -07:00
Robert Long b52f3b1c57 Add types for the MatrixClient register method 2021-09-09 17:00:45 -07:00
Germain 2cdf5629e5 Merge pull request #1900 from matrix-org/gsouquet/thread-disable-echo 2021-09-09 18:56:02 +01:00
Germain Souquet 97b4f0e369 restore correct local echo 2021-09-09 18:51:12 +01:00
Germain Souquet a508c8d236 Make supportsExperimentalThreads calls more resilient 2021-09-09 18:40:15 +01:00
Travis Ralston 7911e6371a Exclude opt-in Element performance metrics from encryption
This is needed for the system which is meant to be monitoring this metadata. See https://github.com/matrix-org/matrix-react-sdk/pull/6766
2021-09-09 10:41:09 -06:00
Travis Ralston bbace01e9a Merge pull request #1897 from matrix-org/travis/measure-encryption-perf
Exclude opt-in Element performance metrics from encryption
2021-09-09 10:38:18 -06:00
Germain Souquet 87469fea63 Disable pending events for threads
There's a fair few things to update to make them work with Threads
Will get back to it when the plan is to build a more polished UI ready for production
2021-09-09 17:24:55 +01:00
Germain Souquet 48a9db0315 Add helper to check threads support 2021-09-09 17:24:34 +01:00
Germain Souquet 33c9471112 Make timelineSet public readonly 2021-09-09 17:23:45 +01:00
Travis Ralston 04fdcbb672 Exclude opt-in Element performance metrics from encryption
This is needed for the system which is meant to be monitoring this metadata. See https://github.com/matrix-org/matrix-react-sdk/pull/6766
2021-09-08 11:34:01 -06:00
Michael Telatynski 4d2286c5be Merge pull request #1888 from matrix-org/t3chguy/ts1234 2021-09-08 16:59:17 +01:00
Michael Telatynski 620e45e4dc fix typing mishap 2021-09-08 16:56:19 +01:00
Michael Telatynski fb97e74344 iterate PR based on feedback 2021-09-08 16:49:17 +01:00
Michael Telatynski 3bbb7df72d fix tests private field access 2021-09-08 12:57:12 +01:00
Michael Telatynski 58b08821bf Fix bugs identified by TS 2021-09-08 12:49:30 +01:00
Michael Telatynski 19bdfd5b84 Add missing interface 2021-09-08 12:49:22 +01:00
Michael Telatynski b6c857e2a2 Convert crypto/verification/* to Typescript 2021-09-08 12:34:44 +01:00
Michael Telatynski 0991626e85 Merge pull request #1884 from matrix-org/t3chguy/ts/123 2021-09-08 10:03:25 +01:00
Germain 7b10d4fac0 Merge pull request #1886 from janpawellek/janpawellek-fix-removeEventListener
Fix calling removeEventListener if it does not exist
2021-09-08 09:13:27 +01:00
Jan Pawellek 980dacaa87 Add explaination why the check for global.window.removeEventListener is necessary 2021-09-08 09:59:09 +02:00
Travis Ralston 3790e8abb4 Merge pull request #1887 from matrix-org/revert-1872-fix/call-notifs
Revert "Count notifications in encrypted rooms client-side"
2021-09-07 14:15:15 -06:00
Travis Ralston 14ba851daf Revert "Count notifications in encrypted rooms client-side" 2021-09-07 14:15:01 -06:00
Travis Ralston 95d80772d7 Merge pull request #1872 from SimonBrandner/fix/call-notifs
Count notifications in encrypted rooms client-side
2021-09-07 12:31:49 -06:00
RiotRobot 341e936082 v12.5.0-rc.1 2021-09-07 18:20:07 +01:00
RiotRobot e87399c4a1 Prepare changelog for v12.5.0-rc.1 2021-09-07 18:20:06 +01:00
Jan Pawellek a7678ea1e0 Fix calling removeEventListener if it does not exist 2021-09-07 17:12:21 +02:00
Germain 0388bd3b48 Merge pull request #1885 from matrix-org/gsouquet/improve-threads-dedupe 2021-09-07 15:31:56 +01:00
Germain Souquet f27327164b Merge branch 'develop' into gsouquet/improve-threads-dedupe 2021-09-07 15:28:05 +01:00
Germain a0f85075c1 Merge pull request #1882 from matrix-org/gsouquet/fix-replyinthread 2021-09-07 15:26:35 +01:00
Germain Souquet e97d18a03b Improve thread deduplication thread process
Some threads were still holding a reference to the original thread models they were assigned to, leading to some unexpected timeline rendering
2021-09-07 15:05:45 +01:00
Michael Telatynski f07cdb43b1 Fix dehydration Crypto type 2021-09-07 14:39:49 +01:00
Michael Telatynski da8c8f166d Improve jsdoc 2021-09-07 14:36:20 +01:00
Michael Telatynski 0830a30498 Fix tests 2021-09-07 14:36:10 +01:00
Michael Telatynski 68a2762b75 fix forwarding_curve25519_key_chain in OlmDevice::exportInboundGroupSession 2021-09-07 13:29:42 +01:00
Michael Telatynski 666e471369 Convert OlmDevice to Typescript 2021-09-07 13:22:27 +01:00
Germain Souquet d89f4a74f9 Fix replyInThread getter to match event m.in_reply_to structure 2021-09-07 10:20:01 +01:00
Germain Souquet 191535d01a Fix replyInThread getter to match event m.in_reply_to structure 2021-09-07 10:17:24 +01:00
Šimon Brandner 241203f804 Merge remote-tracking branch 'upstream/develop' into fix/call-notifs 2021-09-03 18:28:34 +02:00
Šimon Brandner 26cc7a0f64 Return a TODO
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-03 18:24:37 +02:00
Travis Ralston 324f9e58ea Merge pull request #1873 from SimonBrandner/feature/call-timer/18566
Give `MatrixCall` the capability to emit `LengthChanged` events
2021-09-02 22:12:31 -06:00
Dariusz Niemczyk e71519c638 Remove persisted log levels 2021-09-02 20:17:32 +02:00
Šimon Brandner 9b6cee0cab Merge remote-tracking branch 'upstream/develop' into fix/call-notifs 2021-09-02 16:32:26 +02:00
Germain Souquet 2a5f6a7b3d Move all relations event in thread timeline
If an event is related to another event who's in a thread, we want to make sure to move that event to the thread's timeline
2021-09-02 14:14:30 +01:00
Šimon Brandner cae03817cb Use source id directly
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-02 14:10:43 +02:00
Germain 37736184a0 Merge pull request #1876 from matrix-org/gsouquet/replies-fix-18717
Add reply events rendering hint
2021-09-02 08:37:11 +01:00
Šimon Brandner 9431a52abe Move DesktopCapturerSource out of global.d.ts
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-01 18:22:51 +02:00
Šimon Brandner 13c664ad34 Remove Element-specifc screen-sharing code out of the js-sdk
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-01 18:22:45 +02:00
Šimon Brandner d7640d9e15 Don't use a callback in setScreensharingEnabled()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-01 18:08:59 +02:00
Michael Telatynski 3c36be9839 Merge pull request #1877 from matrix-org/t3chguy/fix/18798 2021-09-01 16:08:37 +01:00
Michael Telatynski 9dce7bbb97 Improve typing around join rules 2021-09-01 15:54:04 +01:00
Germain Souquet fa44300abc Add reply events rendering hint
Adds an unstable hint for clients to know whether to render an event in the room timeline or in a thread

There are currently no MSC associated with that unstable "io.element.in_thread" property as I am only experimenting before settling on a specific approach
2021-09-01 12:12:01 +01:00
RiotRobot 2783d162b7 Resetting package fields for development 2021-08-31 13:32:29 +01:00
RiotRobot 39566dbd4f Merge branch 'master' into develop 2021-08-31 13:32:28 +01:00
RiotRobot f8186add92 v12.4.0 2021-08-31 13:29:19 +01:00
RiotRobot c15f22e81d Prepare changelog for v12.4.0 2021-08-31 13:29:18 +01:00
Michael Telatynski 052ac8f95b Merge pull request #1874 from SimonBrandner/task/call-types
Improve TypeScript in `MatrixCall`
2021-08-31 13:06:23 +01:00
Šimon Brandner c37f8ba4c7 Make placeCalls public
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-31 14:00:37 +02:00
Germain 7e2458d5b5 Merge pull request #1828 from matrix-org/gsouquet/threaded-messaging-2349 2021-08-31 09:19:48 +01:00
Germain fe989177bb Merge pull request #1875 from psrpinto/fix/browser-example 2021-08-31 09:15:39 +01:00
Paulo Pinto 412645f025 Use HTTPS
Signed-off-by: Paulo Pinto <paulo.pinto@automattic.com>
2021-08-30 13:32:30 +01:00
Paulo Pinto e527c46f34 Add empty favicon
Signed-off-by: Paulo Pinto <paulo.pinto@automattic.com>
2021-08-30 13:32:30 +01:00
Paulo Pinto da471b57ad Define document encoding
Signed-off-by: Paulo Pinto <paulo.pinto@automattic.com>
2021-08-30 13:32:30 +01:00
Paulo Pinto a4ff97c8df Add required lang attribute
Signed-off-by: Paulo Pinto <paulo.pinto@automattic.com>
2021-08-30 13:32:30 +01:00
Šimon Brandner 411fc47f28 Improve TypeScript in MatrixCall
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-28 09:27:47 +02:00
Šimon Brandner b97e3113d1 Add/improve call event content types
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-28 09:27:34 +02:00
Šimon Brandner f107d63fab Give MatrixCall the capability to emit LengthChanged events
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-27 15:06:55 +02:00
Šimon Brandner a65ac5a82b Count notifications in encrypted rooms client-side
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-27 08:38:59 +02:00
Šimon Brandner c7c1db00e8 Use getWirePushActions()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-26 20:01:40 +02:00
Šimon Brandner 824f93daa3 Add wirePushRules
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-26 20:01:39 +02:00
Šimon Brandner 9fec2f8c58 Simplifie some code
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-26 20:01:39 +02:00
Šimon Brandner 0b46389176 currentCount -> currentHighlightCount
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-26 20:01:38 +02:00
Šimon Brandner db03f16b8e Add a type
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-26 20:01:37 +02:00
David Baker f83fcb1a18 Merge pull request #1870 from matrix-org/dbkr/allchange102
Update changelog generator
2021-08-26 09:42:37 +01:00
David Baker ba0f1f86b9 Merge pull request #1871 from matrix-org/dbkr/verification_request_cancel
Fix verification request cancellation
2021-08-26 09:40:53 +01:00
David Baker 81d349d993 Clarify operator precedence 2021-08-25 20:55:03 +01:00
David Baker d65d2b94b9 Fix verification request cancellation
If you started trying to verify against your other devices and
then cancelled the dialog, the device destination in the cancellation
to-device message would be 'null', so the cancellation wouldn't go
anywhere.

If we haven't yet picked a specific device to talk to, send the
cancellation to all devices (like we did the request).
2021-08-25 18:37:45 +01:00
David Baker acd52bc070 Update changelog generator 2021-08-25 17:19:25 +01:00
David Baker a64339763f Merge pull request #1869 from matrix-org/dbkr/yarn_upgrade_20210824
yarn upgrade
2021-08-25 16:39:45 +01:00
David Baker 4f21b95bbf Merge pull request #1865 from SimonBrandner/feature/voice-activity
Give `CallFeed` the capability to emit on volume changes
2021-08-25 14:40:24 +01:00
David Baker 03e4dcf1ed yarn upgrade 2021-08-25 10:57:37 +01:00
Germain Souquet 3c56d1372d Appease the linter 2021-08-25 09:15:07 +01:00
Germain Souquet 27ecdef09b Merge branch 'develop' into gsouquet/threaded-messaging-2349 2021-08-25 08:53:29 +01:00
RiotRobot 121e8a51c1 v12.4.0-rc.1 2021-08-24 17:04:11 +01:00
RiotRobot 88e3ada701 Prepare changelog for v12.4.0-rc.1 2021-08-24 17:04:10 +01:00
Dariusz Niemczyk 92d822d494 Merge pull request #1847 from matrix-org/jaller94-patch-2
SSSSCryptoCallbacks.getSecretStorageKey: Test if delegateCryptoCallbacks has getSecretStorageKey
2021-08-24 15:24:54 +02:00
Travis Ralston 62c93f54cc Merge pull request #1818 from matrix-org/t3chguy/fix/18089
Add support for /hierarchy API and fall back to /spaces API if needed
2021-08-23 15:51:28 -06:00
Germain Souquet 500c2ed4f2 Remove thread spec 2021-08-23 15:03:10 +01:00
Šimon Brandner 425362edfe Emit Speaking events from CallFeed
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-19 14:48:26 +02:00
Germain Souquet 9c554f9b3d Add experimentalThreadSupport flag 2021-08-19 11:18:46 +01:00
Germain Souquet b7a5f81f95 Add experimental indicators for all threading related features 2021-08-19 11:10:20 +01:00
Šimon Brandner 0746a64c83 Add some docs
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-19 12:03:57 +02:00
Šimon Brandner 22cd475d22 Add missing types
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-19 11:49:26 +02:00
Šimon Brandner 6904f6b36f Emit volume changes
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-19 11:49:19 +02:00
Šimon Brandner e25a649e1a Add webkitAudioContext to window
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-19 11:49:18 +02:00
Germain Souquet 0eb8cbee4d Merge branch 'develop' into gsouquet/threaded-messaging-2349 2021-08-19 10:45:23 +01:00
Dariusz Niemczyk 96e8f65af7 Merge pull request #1864 from matrix-org/palid/dx/typescriptify-password-reset
Add necessary changes for PasswordReset migration
2021-08-19 10:42:59 +02:00
Dariusz Niemczyk 8fb036ba2d Make eslint happy 2021-08-19 09:46:54 +02:00
Dariusz Niemczyk 0a10fa12ef Add necessary changes for PasswordReset migration 2021-08-19 08:59:55 +02:00
David Baker e30dad7913 Merge pull request #1859 from SimonBrandner/task/revert-quick-voip-fix
Revert quick VoIP fix
2021-08-18 18:50:37 +01:00
Travis Ralston c2a0c02898 Merge pull request #1850 from matrix-org/travis/fsdk/filemeta
Add method for including extra fields when uploading to a tree space
2021-08-18 07:29:49 -06:00
Dariusz Niemczyk 7c1e89f86a Merge pull request #1849 from matrix-org/jaller94-patch-3
Make delegateCryptoCallbacks an optional parameter of EncryptionSetupBuilder and SSSSCryptoCallbacks
2021-08-18 14:01:44 +02:00
Germain 29f8a4cba2 Merge pull request #1853 from matrix-org/gsouquet/ts-components-migration
Expose IMyDevice interface
2021-08-18 12:35:57 +01:00
Germain Souquet b884accc99 Dispatch thread events 2021-08-17 17:43:00 +01:00
Germain Souquet d56540320e Merge branch 'develop' into gsouquet/threaded-messaging-2349 2021-08-17 11:14:10 +01:00
Germain Souquet 526fe7e9a4 use timeline set instead of an array of events 2021-08-17 11:10:20 +01:00
Germain Souquet aa696c4c15 Move threading logic to the Sync API 2021-08-17 10:37:34 +01:00
RiotRobot 3216d7e5a7 Merge branch 'master' into develop 2021-08-17 09:27:42 +01:00
David Baker 4f48554cd2 Merge pull request #1861 from matrix-org/dbkr/release_script_handle_no_signing_id_rel
Fix release script to handle no signing ID
2021-08-17 09:13:59 +01:00
Šimon Brandner c0e15b206a Revert "Fix regressions"
This reverts commit bd8690de57.
2021-08-17 10:03:46 +02:00
David Baker 290575ba65 Merge pull request #1858 from SimonBrandner/fix/voip
Fix broken voice calls, no ringing and broken call notifications
2021-08-17 08:54:11 +01:00
Šimon Brandner bd8690de57 Fix regressions
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-17 08:09:14 +02:00
Šimon Brandner 0d09f87777 Revert "Revert "Fix glare related regressions""
This reverts commit 4c552cc350.
2021-08-17 07:33:40 +02:00
David Baker 0f45b7e516 Merge pull request #1857 from matrix-org/revert-1851-fix/glare/18538
Revert "Fix glare related regressions"
2021-08-16 23:18:13 +01:00
David Baker 4c552cc350 Revert "Fix glare related regressions" 2021-08-16 23:06:57 +01:00
David Baker 6a87f54292 Merge pull request #1856 from matrix-org/dbkr/release_script_handle_no_signing_id
Fix release script to handle no signing ID
2021-08-16 15:32:09 +01:00
David Baker bfb2c5aad0 Fix release script to handle no signing ID
We now use the file for stuff other than just the signing ID, so
the file may be present but with no signing ID: handle this case.
2021-08-16 14:38:42 +01:00
RiotRobot db898e7bcf Resetting package fields for development 2021-08-16 13:55:37 +01:00
RiotRobot dbebbe3a19 Merge branch 'master' into develop 2021-08-16 13:55:37 +01:00
David Baker ccf296ea92 Merge pull request #1851 from SimonBrandner/fix/glare/18538
Fix glare related regressions
2021-08-16 12:27:51 +01:00
Šimon Brandner d65b48204b Await handleCallEvent()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-16 13:14:04 +02:00
J. Ryan Stinnett 977720cee4 Merge pull request #1852 from matrix-org/jryans/comment-to-json
Clarify usages of event's toJSON
2021-08-16 10:30:50 +01:00
Christian Paul 3c7cdb1da8 Update src/crypto/EncryptionSetup.ts 2021-08-16 10:36:21 +02:00
Germain Souquet dd80e744ff Expose IMyDevice interface 2021-08-16 09:06:52 +01:00
J. Ryan Stinnett 0b12e37459 Clarify usages of event's toJSON 2021-08-16 09:04:14 +01:00
Šimon Brandner 51e815f0cb Fix glare
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-15 13:03:18 +02:00
Šimon Brandner f3856d569d Reformat some code
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-15 13:03:08 +02:00
Šimon Brandner e6d1909f0b Remove weird method call
If we can get localUsermediaStream gotUserMediaForAnswer() has alredy been called before

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-15 13:02:39 +02:00
Šimon Brandner 408976a199 Simplifie some code
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-15 12:58:38 +02:00
Šimon Brandner 4da49d926b Remove unnecessary logs
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-15 12:57:27 +02:00
Šimon Brandner 75750ed760 Add a comment
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-15 12:56:48 +02:00
Travis Ralston 2f74779bf1 Merge pull request #1772 from matrix-org/t3chguy/ts/12
Convert SearchResult, InteractiveAuth, PushProcessor and Scheduler to TS
2021-08-13 10:08:54 -06:00
Michael Telatynski 7b038393b1 Merge pull request #1792 from matrix-org/t3chguy/kill-groups
Deprecate groups APIs
2021-08-13 16:39:47 +01:00
Travis Ralston 768c0e7f77 Add method for including extra fields when uploading to a tree space
We ultimately need this to include things like `info` and other metadata in a specific environment.
2021-08-13 09:25:19 -06:00
Christian Paul c6009b1056 Make delegateCryptoCallbacks parameter optional on EncryptionSetupBuilder 2021-08-13 16:04:47 +02:00
Christian Paul 2a280afa88 Make SSSSCryptoCallbacks.delegateCryptoCallbacks optional 2021-08-13 15:58:44 +02:00
Christian Paul be980f4bc9 Update src/crypto/EncryptionSetup.ts 2021-08-13 15:38:22 +02:00
Dariusz Niemczyk 78118e9442 Merge pull request #1846 from matrix-org/jaller94-patch-1
SSSSCryptoCallbacks.getSecretStorageKey return Promise<null> if no key was found
2021-08-13 15:36:53 +02:00
Christian Paul a2c10a7913 SSSSCryptoCallbacks.getSecretStorageKey: Test if delegateCryptoCallbacks has getSecretStorageKey 2021-08-13 15:28:01 +02:00
Christian Paul 7c4f7c9dad SSSSCryptoCallbacks.getSecretStorageKey: Return null, if no key found 2021-08-13 15:23:47 +02:00
Christian Paul 62058e6d48 SSSSCryptoCallbacks.getSecretStorageKey may return Promise<void>
If no result is found, the function may reach the last line without `return` ever being called.
2021-08-13 15:15:20 +02:00
Dariusz Niemczyk a2f514b544 Merge pull request #1834 from matrix-org/palid/fix/signaling-state-errors
Fix temporary call messages being handled without call
2021-08-13 14:17:30 +02:00
Michael Telatynski 14b424ee94 Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3chguy/fix/18089 2021-08-12 11:55:04 +01:00
Travis Ralston 696b3ef1ce Merge pull request #1841 from matrix-org/travis/fsdk/fix-delete
Fix conditional on returning file tree spaces
2021-08-11 15:21:20 -06:00
Travis Ralston 4e2ee3b3a8 It helps to fix the other tests too 2021-08-11 15:18:24 -06:00
Travis Ralston 1f9fab9a0c Fix conditional on returning file tree spaces
If a tree space was deleted then it'll still hold a Room object for us, which is not much help if we're effectively trying to get the joined trees.
2021-08-11 15:11:13 -06:00
Michael Telatynski d91d1cea34 Apply suggestions from code review
Co-authored-by: Travis Ralston <travisr@matrix.org>
2021-08-11 21:47:51 +01:00
Michael Telatynski 69ba32683c Iterate PR based on feedback 2021-08-11 21:45:51 +01:00
Michael Telatynski fc78e63ad1 Fix RoomHierarchy comments 2021-08-11 20:57:40 +01:00
Michael Telatynski db212a555d Update to latest version of MSC2946 API s/next_token/next_batch/ 2021-08-11 20:49:23 +01:00
Dariusz Niemczyk 4607f0177c Fix invalid string quotes 2021-08-11 14:39:00 +02:00
Dariusz Niemczyk d7dbaeba46 Update src/webrtc/callEventHandler.ts
Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-11 14:38:20 +02:00
Dariusz Niemczyk 3e94db1837 Update src/webrtc/call.ts
Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-11 14:38:13 +02:00
Dariusz Niemczyk 4fd77c2f05 Update src/webrtc/callEventHandler.ts
Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-11 14:38:08 +02:00
Dariusz Niemczyk 9fe05e7d40 Fix temporary call messages being handled without call
In cases where a rogue client, or just a simple bug, sends a temporary
call message, like CallNegotiate the messages should just be discarded
if there's no actual p2p connection created.
2021-08-11 14:28:10 +02:00
Germain Souquet e5a67500e4 Stop checking for event type as it is sometimes encrypted 2021-08-10 16:21:59 +02:00
Michael Telatynski 9b50455049 Iterate PR, merge types with @types/PushRules 2021-08-10 11:03:05 +01:00
Michael Telatynski a531446396 tidy a bit more 2021-08-10 10:46:13 +01:00
Michael Telatynski f876c35283 Fix some bad conversion artifacts 2021-08-10 10:34:05 +01:00
Michael Telatynski 1bcec53c6b delint 2021-08-10 10:22:40 +01:00
Michael Telatynski 0ad5ef4e9b Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3chguy/ts/12 2021-08-10 10:21:13 +01:00
Germain Souquet 18113900fe Implementation of event threading 2021-08-04 17:28:08 +02:00
Michael Telatynski 88b8b24629 Merge room hierarchy pages 2021-07-30 11:01:08 +01:00
Michael Telatynski 58c60d85e7 Refactor Space Hierarchy stuff in preparation for pagination 2021-07-29 17:35:17 +01:00
Michael Telatynski 21a357c433 fix typo 2021-07-29 15:00:39 +01:00
Michael Telatynski d49d6936a5 Add support for /children API and fall back to /spaces API if M_UNRECOGNIZED 2021-07-29 12:16:34 +01:00
Michael Telatynski c2b5b14d26 Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3chguy/ts/12
 Conflicts:
	src/client.ts
	src/interactive-auth.ts
	src/models/search-result.ts
2021-07-23 23:46:15 +01:00
Michael Telatynski f1a0c46a29 Deprecate groups APIs 2021-07-16 17:45:07 +01:00
Michael Telatynski e775bcac3c Convert SearchResult, InteractiveAuth, PushProcessor and Scheduler to Typescript 2021-07-07 11:08:54 +01:00
143 changed files with 14101 additions and 7595 deletions
+3
View File
@@ -31,6 +31,9 @@ module.exports = {
"no-async-promise-executor": "off",
// We use a `logger` intermediary module
"no-console": "error",
// restrict EventEmitters to force callers to use TypedEventEmitter
"no-restricted-imports": ["error", "events"],
},
overrides: [{
files: [
+6
View File
@@ -0,0 +1,6 @@
comment:
layout: "diff, files"
behavior: default
require_changes: false
require_base: no
require_head: no
+19
View File
@@ -0,0 +1,19 @@
name: Test coverage
on:
pull_request: {}
push:
branches: [develop, main, master]
jobs:
test-coverage:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Run tests with coverage
run: "yarn install && yarn build && yarn coverage"
- name: Upload coverage
uses: codecov/codecov-action@v2
with:
verbose: true
+392
View File
@@ -1,3 +1,395 @@
Changes in [16.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.0) (2022-03-15)
==================================================================================================
## 🚨 BREAKING CHANGES
* Improve typing around event emitter handlers ([\#2180](https://github.com/matrix-org/matrix-js-sdk/pull/2180)).
## ✨ Features
* Fix defer not supporting resolving with a Promise<T> ([\#2216](https://github.com/matrix-org/matrix-js-sdk/pull/2216)).
* add LocationAssetType enum ([\#2214](https://github.com/matrix-org/matrix-js-sdk/pull/2214)).
* Support for mid-call devices changes ([\#2154](https://github.com/matrix-org/matrix-js-sdk/pull/2154)). Contributed by @SimonBrandner.
* Add new room state emit RoomStateEvent.Update for lower-frequency hits ([\#2192](https://github.com/matrix-org/matrix-js-sdk/pull/2192)).
## 🐛 Bug Fixes
* Fix wrong event_id being sent for m.in_reply_to of threads ([\#2213](https://github.com/matrix-org/matrix-js-sdk/pull/2213)).
* Fix wrongly asserting that PushRule::conditions is non-null ([\#2217](https://github.com/matrix-org/matrix-js-sdk/pull/2217)).
* Make createThread more resilient when missing rootEvent ([\#2207](https://github.com/matrix-org/matrix-js-sdk/pull/2207)). Fixes vector-im/element-web#21130.
* Fix bug with the /hierarchy API sending invalid requests ([\#2201](https://github.com/matrix-org/matrix-js-sdk/pull/2201)). Fixes vector-im/element-web#21170.
* fix relation sender filter ([\#2196](https://github.com/matrix-org/matrix-js-sdk/pull/2196)). Fixes vector-im/element-web#20877.
* Fix bug with one-way audio after a transfer ([\#2193](https://github.com/matrix-org/matrix-js-sdk/pull/2193)).
Changes in [16.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.0-rc.1) (2022-03-08)
============================================================================================================
## 🚨 BREAKING CHANGES
* Improve typing around event emitter handlers ([\#2180](https://github.com/matrix-org/matrix-js-sdk/pull/2180)).
## ✨ Features
* Fix defer not supporting resolving with a Promise<T> ([\#2216](https://github.com/matrix-org/matrix-js-sdk/pull/2216)).
* add LocationAssetType enum ([\#2214](https://github.com/matrix-org/matrix-js-sdk/pull/2214)).
* Support for mid-call devices changes ([\#2154](https://github.com/matrix-org/matrix-js-sdk/pull/2154)). Contributed by @SimonBrandner.
* Add new room state emit RoomStateEvent.Update for lower-frequency hits ([\#2192](https://github.com/matrix-org/matrix-js-sdk/pull/2192)).
## 🐛 Bug Fixes
* Fix wrong event_id being sent for m.in_reply_to of threads ([\#2213](https://github.com/matrix-org/matrix-js-sdk/pull/2213)).
* Fix wrongly asserting that PushRule::conditions is non-null ([\#2217](https://github.com/matrix-org/matrix-js-sdk/pull/2217)).
* Make createThread more resilient when missing rootEvent ([\#2207](https://github.com/matrix-org/matrix-js-sdk/pull/2207)). Fixes vector-im/element-web#21130.
* Fix bug with the /hierarchy API sending invalid requests ([\#2201](https://github.com/matrix-org/matrix-js-sdk/pull/2201)). Fixes vector-im/element-web#21170.
* fix relation sender filter ([\#2196](https://github.com/matrix-org/matrix-js-sdk/pull/2196)). Fixes vector-im/element-web#20877.
* Fix bug with one-way audio after a transfer ([\#2193](https://github.com/matrix-org/matrix-js-sdk/pull/2193)).
Changes in [15.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.6.0) (2022-02-28)
==================================================================================================
## ✨ Features
* Return send event response from MSC3089Branch.createNewVersion() ([\#2186](https://github.com/matrix-org/matrix-js-sdk/pull/2186)).
* Add functions to support refresh tokens ([\#2178](https://github.com/matrix-org/matrix-js-sdk/pull/2178)).
## 🐛 Bug Fixes
* [Release] Fix bug with the /hierarchy API sending invalid requests ([\#2202](https://github.com/matrix-org/matrix-js-sdk/pull/2202)).
* Fix bug where calls could break if rejected from somewhere else ([\#2189](https://github.com/matrix-org/matrix-js-sdk/pull/2189)).
* Fix camera stuck on after call transfer ([\#2188](https://github.com/matrix-org/matrix-js-sdk/pull/2188)).
* Fix synthetic read receipt handling ([\#2174](https://github.com/matrix-org/matrix-js-sdk/pull/2174)). Fixes vector-im/element-web#21016.
* Revert "Sign backup with cross-signing key when we reset it." ([\#2175](https://github.com/matrix-org/matrix-js-sdk/pull/2175)).
* Sign backup with cross-signing key when we reset it. ([\#2170](https://github.com/matrix-org/matrix-js-sdk/pull/2170)).
* Fix error in uploadContent() when file is empty under Node.js ([\#2155](https://github.com/matrix-org/matrix-js-sdk/pull/2155)).
* Check the backup info against the stored private key when determining trust. ([\#2167](https://github.com/matrix-org/matrix-js-sdk/pull/2167)).
* Back up keys before logging out ([\#2158](https://github.com/matrix-org/matrix-js-sdk/pull/2158)). Fixes vector-im/element-web#13151.
Changes in [15.6.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.6.0-rc.1) (2022-02-22)
============================================================================================================
## ✨ Features
* Return send event response from MSC3089Branch.createNewVersion() ([\#2186](https://github.com/matrix-org/matrix-js-sdk/pull/2186)).
* Add functions to support refresh tokens ([\#2178](https://github.com/matrix-org/matrix-js-sdk/pull/2178)).
## 🐛 Bug Fixes
* Fix bug where calls could break if rejected from somewhere else ([\#2189](https://github.com/matrix-org/matrix-js-sdk/pull/2189)).
* Fix camera stuck on after call transfer ([\#2188](https://github.com/matrix-org/matrix-js-sdk/pull/2188)).
* Fix synthetic read receipt handling ([\#2174](https://github.com/matrix-org/matrix-js-sdk/pull/2174)). Fixes vector-im/element-web#21016.
* Revert "Sign backup with cross-signing key when we reset it." ([\#2175](https://github.com/matrix-org/matrix-js-sdk/pull/2175)).
* Sign backup with cross-signing key when we reset it. ([\#2170](https://github.com/matrix-org/matrix-js-sdk/pull/2170)).
* Fix error in uploadContent() when file is empty under Node.js ([\#2155](https://github.com/matrix-org/matrix-js-sdk/pull/2155)).
* Check the backup info against the stored private key when determining trust. ([\#2167](https://github.com/matrix-org/matrix-js-sdk/pull/2167)).
* Back up keys before logging out ([\#2158](https://github.com/matrix-org/matrix-js-sdk/pull/2158)). Fixes vector-im/element-web#13151.
Changes in [15.5.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.2) (2022-02-17)
==================================================================================================
## 🐛 Bug Fixes
* Fix synthetic read receipt handling
Changes in [15.5.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.1) (2022-02-14)
==================================================================================================
## 🐛 Bug Fixes
* Fix issue with rooms not getting marked as unread ([\#2163](https://github.com/matrix-org/matrix-js-sdk/pull/2163)). Fixes vector-im/element-web#20971.
* Don't store streams that are only used once ([\#2157](https://github.com/matrix-org/matrix-js-sdk/pull/2157)). Fixes vector-im/element-web#20932. Contributed by @SimonBrandner.
* Fix edge cases around RR calculations ([\#2160](https://github.com/matrix-org/matrix-js-sdk/pull/2160)). Fixes vector-im/element-web#20922.
* Account for encryption in `maySendMessage()` ([\#2159](https://github.com/matrix-org/matrix-js-sdk/pull/2159)). Contributed by @SimonBrandner.
* Send references to thread root to threads, even out of order ([\#2156](https://github.com/matrix-org/matrix-js-sdk/pull/2156)).
* Fix initial sync fail when event fetching unsuccessful ([\#2150](https://github.com/matrix-org/matrix-js-sdk/pull/2150)). Fixes vector-im/element-web#20862.
* Don't decrypt redacted messages ([\#2143](https://github.com/matrix-org/matrix-js-sdk/pull/2143)). Contributed by @SimonBrandner.
Changes in [15.5.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.1-rc.1) (2022-02-08)
============================================================================================================
## 🐛 Bug Fixes
* Fix issue with rooms not getting marked as unread ([\#2163](https://github.com/matrix-org/matrix-js-sdk/pull/2163)). Fixes vector-im/element-web#20971.
* Don't store streams that are only used once ([\#2157](https://github.com/matrix-org/matrix-js-sdk/pull/2157)). Fixes vector-im/element-web#20932. Contributed by @SimonBrandner.
* Fix edge cases around RR calculations ([\#2160](https://github.com/matrix-org/matrix-js-sdk/pull/2160)). Fixes vector-im/element-web#20922.
* Account for encryption in `maySendMessage()` ([\#2159](https://github.com/matrix-org/matrix-js-sdk/pull/2159)). Contributed by @SimonBrandner.
* Send references to thread root to threads, even out of order ([\#2156](https://github.com/matrix-org/matrix-js-sdk/pull/2156)).
* Fix initial sync fail when event fetching unsuccessful ([\#2150](https://github.com/matrix-org/matrix-js-sdk/pull/2150)). Fixes vector-im/element-web#20862.
* Don't decrypt redacted messages ([\#2143](https://github.com/matrix-org/matrix-js-sdk/pull/2143)). Contributed by @SimonBrandner.
Changes in [15.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.0) (2022-01-31)
==================================================================================================
## ✨ Features
* Support m.asset in m.location event content ([\#2109](https://github.com/matrix-org/matrix-js-sdk/pull/2109)).
* Send extensible events structure and support on-demand parsing ([\#2091](https://github.com/matrix-org/matrix-js-sdk/pull/2091)).
* Support cancelling events whilst they are in status = ENCRYPTING ([\#2095](https://github.com/matrix-org/matrix-js-sdk/pull/2095)).
## 🐛 Bug Fixes
* Fix http-api butchering idServer requests ([\#2134](https://github.com/matrix-org/matrix-js-sdk/pull/2134)). Fixes vector-im/element-web#20680.
* Don't remove streams that still have tracks ([\#2104](https://github.com/matrix-org/matrix-js-sdk/pull/2104)).
Changes in [15.5.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.0-rc.1) (2022-01-26)
============================================================================================================
## ✨ Features
* Support m.asset in m.location event content ([\#2109](https://github.com/matrix-org/matrix-js-sdk/pull/2109)).
* Send extensible events structure and support on-demand parsing ([\#2091](https://github.com/matrix-org/matrix-js-sdk/pull/2091)).
* Support cancelling events whilst they are in status = ENCRYPTING ([\#2095](https://github.com/matrix-org/matrix-js-sdk/pull/2095)).
## 🐛 Bug Fixes
* Fix http-api butchering idServer requests ([\#2134](https://github.com/matrix-org/matrix-js-sdk/pull/2134)). Fixes vector-im/element-web#20680.
* Don't remove streams that still have tracks ([\#2104](https://github.com/matrix-org/matrix-js-sdk/pull/2104)).
Changes in [15.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.4.0) (2022-01-17)
==================================================================================================
## ✨ Features
* Don't consider alt_aliases when calculating room name ([\#2094](https://github.com/matrix-org/matrix-js-sdk/pull/2094)). Fixes vector-im/element-web#13887.
* Load room history if necessary when searching for MSC3089 getFileEvent() ([\#2066](https://github.com/matrix-org/matrix-js-sdk/pull/2066)).
* Add support for MSC3030 `/timestamp_to_event` ([\#2072](https://github.com/matrix-org/matrix-js-sdk/pull/2072)).
## 🐛 Bug Fixes
* Stop encrypting redactions as it isn't spec compliant ([\#2098](https://github.com/matrix-org/matrix-js-sdk/pull/2098)). Fixes vector-im/element-web#20460.
* Fix more function typings relating to key backup ([\#2086](https://github.com/matrix-org/matrix-js-sdk/pull/2086)).
* Fix timeline search in MSC3089 getFileEvent() ([\#2085](https://github.com/matrix-org/matrix-js-sdk/pull/2085)).
* Set a `deviceId` for VoIP example and use `const`/`let` ([\#2090](https://github.com/matrix-org/matrix-js-sdk/pull/2090)). Fixes #2083. Contributed by @SimonBrandner.
* Fix incorrect TS return type for secret storage and key backup functions ([\#2082](https://github.com/matrix-org/matrix-js-sdk/pull/2082)).
Changes in [15.4.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.4.0-rc.1) (2022-01-11)
============================================================================================================
## ✨ 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)
============================================================================================================
## ✨ 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.2.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.2.1) (2021-12-13)
==================================================================================================
* Security release with updated version of Olm to fix https://matrix.org/blog/2021/12/03/pre-disclosure-upcoming-security-release-of-libolm-and-matrix-js-sdk
Changes in [15.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.2.0) (2021-12-06)
==================================================================================================
## ✨ Features
* Remove support for `ArrayBuffer` in unstable MSC3089 `createFile()` and `createNewVersion()` and instead use same content types as handled by `MatrixClient.uploadContent()`. This enables support for Node.js. ([\#2014](https://github.com/matrix-org/matrix-js-sdk/pull/2014)).
* Support for password-based backup on Node.js ([\#2021](https://github.com/matrix-org/matrix-js-sdk/pull/2021)).
* Add optional force parameter when ensuring Olm sessions ([\#2027](https://github.com/matrix-org/matrix-js-sdk/pull/2027)).
## 🐛 Bug Fixes
* Fix call upgrades ([\#2024](https://github.com/matrix-org/matrix-js-sdk/pull/2024)). Contributed by @SimonBrandner.
Changes in [15.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.2.0-rc.1) (2021-11-30)
============================================================================================================
## ✨ Features
* Remove support for `ArrayBuffer` in unstable MSC3089 `createFile()` and `createNewVersion()` and instead use same content types as handled by `MatrixClient.uploadContent()`. This enables support for Node.js. ([\#2014](https://github.com/matrix-org/matrix-js-sdk/pull/2014)).
* Support for password-based backup on Node.js ([\#2021](https://github.com/matrix-org/matrix-js-sdk/pull/2021)).
* Add optional force parameter when ensuring Olm sessions ([\#2027](https://github.com/matrix-org/matrix-js-sdk/pull/2027)).
## 🐛 Bug Fixes
* Fix call upgrades ([\#2024](https://github.com/matrix-org/matrix-js-sdk/pull/2024)). Contributed by @SimonBrandner.
Changes in [15.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.1.1) (2021-11-22)
==================================================================================================
## 🐛 Bug Fixes
* Fix edit history being broken after editing an unencrypted event with an encrypted event ([\#2013](https://github.com/matrix-org/matrix-js-sdk/pull/2013)). Fixes vector-im/element-web#19651 and vector-im/element-web#19651. Contributed by @aaronraimist.
* Make events pagination responses parse threads ([\#2011](https://github.com/matrix-org/matrix-js-sdk/pull/2011)). Fixes vector-im/element-web#19587 and vector-im/element-web#19587.
Changes in [15.1.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.1.1-rc.1) (2021-11-17)
============================================================================================================
## 🐛 Bug Fixes
* Fix edit history being broken after editing an unencrypted event with an encrypted event ([\#2013](https://github.com/matrix-org/matrix-js-sdk/pull/2013)). Fixes vector-im/element-web#19651 and vector-im/element-web#19651. Contributed by @aaronraimist.
* Make events pagination responses parse threads ([\#2011](https://github.com/matrix-org/matrix-js-sdk/pull/2011)). Fixes vector-im/element-web#19587 and vector-im/element-web#19587.
Changes in [15.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.1.0) (2021-11-08)
==================================================================================================
## 🦖 Deprecations
* Mark old verification methods as deprecated ([\#1994](https://github.com/matrix-org/matrix-js-sdk/pull/1994)).
## ✨ Features
* Try to set a sender on search result events if possible ([\#2004](https://github.com/matrix-org/matrix-js-sdk/pull/2004)).
* Port some changes from group calls branch to develop ([\#2001](https://github.com/matrix-org/matrix-js-sdk/pull/2001)). Contributed by @SimonBrandner.
* Fetch room membership from server rather than relying on stored data ([\#1998](https://github.com/matrix-org/matrix-js-sdk/pull/1998)).
* Add method to fetch the MSC3266 Room Summary of a Room ([\#1988](https://github.com/matrix-org/matrix-js-sdk/pull/1988)).
## 🐛 Bug Fixes
* Don't show `Unable to access microphone` when cancelling screensharing dialog ([\#2005](https://github.com/matrix-org/matrix-js-sdk/pull/2005)). Fixes vector-im/element-web#19533 and vector-im/element-web#19533. Contributed by @SimonBrandner.
* Strip direction override characters from display names ([\#1992](https://github.com/matrix-org/matrix-js-sdk/pull/1992)). Fixes vector-im/element-web#1712 and vector-im/element-web#1712.
Changes in [15.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.1.0-rc.1) (2021-11-02)
============================================================================================================
## 🦖 Deprecations
* Mark old verification methods as deprecated ([\#1994](https://github.com/matrix-org/matrix-js-sdk/pull/1994)).
## ✨ Features
* Try to set a sender on search result events if possible ([\#2004](https://github.com/matrix-org/matrix-js-sdk/pull/2004)).
* Port some changes from group calls branch to develop ([\#2001](https://github.com/matrix-org/matrix-js-sdk/pull/2001)). Contributed by @SimonBrandner.
* Fetch room membership from server rather than relying on stored data ([\#1998](https://github.com/matrix-org/matrix-js-sdk/pull/1998)).
* Add method to fetch the MSC3266 Room Summary of a Room ([\#1988](https://github.com/matrix-org/matrix-js-sdk/pull/1988)).
## 🐛 Bug Fixes
* Don't show `Unable to access microphone` when cancelling screensharing dialog ([\#2005](https://github.com/matrix-org/matrix-js-sdk/pull/2005)). Fixes vector-im/element-web#19533 and vector-im/element-web#19533. Contributed by @SimonBrandner.
* Strip direction override characters from display names ([\#1992](https://github.com/matrix-org/matrix-js-sdk/pull/1992)). Fixes vector-im/element-web#1712 and vector-im/element-web#1712.
Changes in [15.0.0](https://github.com/vector-im/element-desktop/releases/tag/v15.0.0) (2021-10-25)
===================================================================================================
## 🚨 BREAKING CHANGES
* Use `ICallFeedOpts` in the `CallFeed` constructor. To construct a new `CallFeed` object you have to pass `ICallFeedOpts` e.g. `const callFeed = new CallFeed({client ([\#1964](https://github.com/matrix-org/matrix-js-sdk/pull/1964)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
## ✨ Features
* Make threads use 'm.thread' relation ([\#1980](https://github.com/matrix-org/matrix-js-sdk/pull/1980)).
* Try to answer a call without video if we can't access the camera ([\#1972](https://github.com/matrix-org/matrix-js-sdk/pull/1972)). Fixes vector-im/element-web#17975 and vector-im/element-web#17975. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Make `opts` in `importRoomKeys()` optional ([\#1974](https://github.com/matrix-org/matrix-js-sdk/pull/1974)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Enable TypeScript declaration maps ([\#1966](https://github.com/matrix-org/matrix-js-sdk/pull/1966)). Contributed by [Alexendoo](https://github.com/Alexendoo).
## 🐛 Bug Fixes
* Fix `requestVerificationDM` with chronological `pendingEventOrdering` ([\#1943](https://github.com/matrix-org/matrix-js-sdk/pull/1943)). Contributed by [freaktechnik](https://github.com/freaktechnik).
Changes in [15.0.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v15.0.0-rc.1) (2021-10-19)
=============================================================================================================
## 🚨 BREAKING CHANGES
* Use `ICallFeedOpts` in the `CallFeed` constructor. To construct a new `CallFeed` object you have to pass `ICallFeedOpts` e.g. `const callFeed = new CallFeed({client ([\#1964](https://github.com/matrix-org/matrix-js-sdk/pull/1964)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
## ✨ Features
* Make threads use 'm.thread' relation ([\#1980](https://github.com/matrix-org/matrix-js-sdk/pull/1980)).
* Try to answer a call without video if we can't access the camera ([\#1972](https://github.com/matrix-org/matrix-js-sdk/pull/1972)). Fixes vector-im/element-web#17975 and vector-im/element-web#17975. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Make `opts` in `importRoomKeys()` optional ([\#1974](https://github.com/matrix-org/matrix-js-sdk/pull/1974)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Enable TypeScript declaration maps ([\#1966](https://github.com/matrix-org/matrix-js-sdk/pull/1966)). Contributed by [Alexendoo](https://github.com/Alexendoo).
## 🐛 Bug Fixes
* Fix `requestVerificationDM` with chronological `pendingEventOrdering` ([\#1943](https://github.com/matrix-org/matrix-js-sdk/pull/1943)). Contributed by [freaktechnik](https://github.com/freaktechnik).
Changes in [14.0.1](https://github.com/vector-im/element-desktop/releases/tag/v14.0.1) (2021-10-12)
===================================================================================================
## 🚨 BREAKING CHANGES
* Support for call upgrades. `setLocalVideoMuted()` and `setMicrophoneMuted()` are now `async` and return the new mute state ([\#1827](https://github.com/matrix-org/matrix-js-sdk/pull/1827)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
## ✨ Features
* Implement file versioning for tree spaces ([\#1952](https://github.com/matrix-org/matrix-js-sdk/pull/1952)).
* Allow answering calls without audio/video ([\#1950](https://github.com/matrix-org/matrix-js-sdk/pull/1950)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Add `bound` to `IThreepid` ([\#1941](https://github.com/matrix-org/matrix-js-sdk/pull/1941)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Add `trusted_locally` to `TrustInfo` ([\#1942](https://github.com/matrix-org/matrix-js-sdk/pull/1942)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
## 🐛 Bug Fixes
* Fix incorrect return value type in getJoinedRooms() ([\#1959](https://github.com/matrix-org/matrix-js-sdk/pull/1959)). Contributed by [psrpinto](https://github.com/psrpinto).
* Make sure to set `callLengthInterval` only once ([\#1958](https://github.com/matrix-org/matrix-js-sdk/pull/1958)). Fixes vector-im/element-web#19221 and vector-im/element-web#19221. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Fix event partitioning from non threading ready clients ([\#1948](https://github.com/matrix-org/matrix-js-sdk/pull/1948)).
* Ensure unencrypted fields get exposed by getEffectiveEvent() ([\#1938](https://github.com/matrix-org/matrix-js-sdk/pull/1938)). Fixes vector-im/element-web#19062 and vector-im/element-web#19062.
Changes in [14.0.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v14.0.0-rc.1) (2021-10-04)
=============================================================================================================
## 🚨 BREAKING CHANGES
* Support for call upgrades. `setLocalVideoMuted()` and `setMicrophoneMuted()` are now `async` and return the new mute state ([\#1827](https://github.com/matrix-org/matrix-js-sdk/pull/1827)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
## ✨ Features
* Implement file versioning for tree spaces ([\#1952](https://github.com/matrix-org/matrix-js-sdk/pull/1952)).
* Allow answering calls without audio/video ([\#1950](https://github.com/matrix-org/matrix-js-sdk/pull/1950)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Add `bound` to `IThreepid` ([\#1941](https://github.com/matrix-org/matrix-js-sdk/pull/1941)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Add `trusted_locally` to `TrustInfo` ([\#1942](https://github.com/matrix-org/matrix-js-sdk/pull/1942)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
## 🐛 Bug Fixes
* Fix incorrect return value type in getJoinedRooms() ([\#1959](https://github.com/matrix-org/matrix-js-sdk/pull/1959)). Contributed by [psrpinto](https://github.com/psrpinto).
* Make sure to set `callLengthInterval` only once ([\#1958](https://github.com/matrix-org/matrix-js-sdk/pull/1958)). Fixes vector-im/element-web#19221 and vector-im/element-web#19221. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Fix event partitioning from non threading ready clients ([\#1948](https://github.com/matrix-org/matrix-js-sdk/pull/1948)).
* Ensure unencrypted fields get exposed by getEffectiveEvent() ([\#1938](https://github.com/matrix-org/matrix-js-sdk/pull/1938)). Fixes vector-im/element-web#19062 and vector-im/element-web#19062.
Changes in [13.0.0](https://github.com/vector-im/element-desktop/releases/tag/v13.0.0) (2021-09-27)
===================================================================================================
## ✨ Features
* Add `getHistoryVisibility()` and `getGuestAccess()` ([\#1940](https://github.com/matrix-org/matrix-js-sdk/pull/1940)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Add `getBuffer()` to `QRCodeData` ([\#1927](https://github.com/matrix-org/matrix-js-sdk/pull/1927)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Added `createDataChannel()` and `CallEvent.DataChannel` to `MatrixCall` for creating and listening for WebRTC datachannels. ([\#1929](https://github.com/matrix-org/matrix-js-sdk/pull/1929)). Contributed by [robertlong](https://github.com/robertlong).
* Add file locking to MSC3089 branches ([\#1909](https://github.com/matrix-org/matrix-js-sdk/pull/1909)).
* Add `hasBeenCancelled` to `VerificationBase` ([\#1915](https://github.com/matrix-org/matrix-js-sdk/pull/1915)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Add `ISasEvent` ([\#1908](https://github.com/matrix-org/matrix-js-sdk/pull/1908)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Count notifications in encrypted rooms client-side ([\#1872](https://github.com/matrix-org/matrix-js-sdk/pull/1872)). Fixes vector-im/element-web#15393 and vector-im/element-web#15393. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Exclude opt-in Element performance metrics from encryption ([\#1897](https://github.com/matrix-org/matrix-js-sdk/pull/1897)).
## 🐛 Bug Fixes
* Fix race on automatic backup restore ([\#1936](https://github.com/matrix-org/matrix-js-sdk/pull/1936)). Fixes vector-im/element-web#17781 and vector-im/element-web#17781.
Changes in [13.0.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v13.0.0-rc.1) (2021-09-21)
=============================================================================================================
## ✨ Features
* Add `getHistoryVisibility()` and `getGuestAccess()` ([\#1940](https://github.com/matrix-org/matrix-js-sdk/pull/1940)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Add `getBuffer()` to `QRCodeData` ([\#1927](https://github.com/matrix-org/matrix-js-sdk/pull/1927)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Added `createDataChannel()` and `CallEvent.DataChannel` to `MatrixCall` for creating and listening for WebRTC datachannels. ([\#1929](https://github.com/matrix-org/matrix-js-sdk/pull/1929)). Contributed by [robertlong](https://github.com/robertlong).
* Add file locking to MSC3089 branches ([\#1909](https://github.com/matrix-org/matrix-js-sdk/pull/1909)).
* Add `hasBeenCancelled` to `VerificationBase` ([\#1915](https://github.com/matrix-org/matrix-js-sdk/pull/1915)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Add `ISasEvent` ([\#1908](https://github.com/matrix-org/matrix-js-sdk/pull/1908)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Count notifications in encrypted rooms client-side ([\#1872](https://github.com/matrix-org/matrix-js-sdk/pull/1872)). Fixes vector-im/element-web#15393 and vector-im/element-web#15393. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Exclude opt-in Element performance metrics from encryption ([\#1897](https://github.com/matrix-org/matrix-js-sdk/pull/1897)).
## 🐛 Bug Fixes
* Fix race on automatic backup restore ([\#1936](https://github.com/matrix-org/matrix-js-sdk/pull/1936)). Fixes vector-im/element-web#17781 and vector-im/element-web#17781.
Changes in [12.5.0](https://github.com/vector-im/element-desktop/releases/tag/v12.5.0) (2021-09-14)
===================================================================================================
## ✨ Features
* [Release] Exclude opt-in Element performance metrics from encryption ([\#1901](https://github.com/matrix-org/matrix-js-sdk/pull/1901)).
* Give `MatrixCall` the capability to emit `LengthChanged` events ([\#1873](https://github.com/matrix-org/matrix-js-sdk/pull/1873)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Improve browser example ([\#1875](https://github.com/matrix-org/matrix-js-sdk/pull/1875)). Contributed by [psrpinto](https://github.com/psrpinto).
* Give `CallFeed` the capability to emit on volume changes ([\#1865](https://github.com/matrix-org/matrix-js-sdk/pull/1865)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
## 🐛 Bug Fixes
* Fix verification request cancellation ([\#1871](https://github.com/matrix-org/matrix-js-sdk/pull/1871)).
Changes in [12.4.1](https://github.com/vector-im/element-desktop/releases/tag/v12.4.1) (2021-09-13)
===================================================================================================
## 🔒 SECURITY FIXES
* Fix a security issue with message key sharing. See https://matrix.org/blog/2021/09/13/vulnerability-disclosure-key-sharing
for details.
Changes in [12.4.0](https://github.com/vector-im/element-desktop/releases/tag/v12.4.0) (2021-08-31)
===================================================================================================
## 🦖 Deprecations
* Deprecate groups APIs. Groups are no longer supported, only Synapse has support. They are being replaced by Spaces which build off of Rooms and are far more flexible. ([\#1792](https://github.com/matrix-org/matrix-js-sdk/pull/1792)).
## ✨ Features
* Add method for including extra fields when uploading to a tree space ([\#1850](https://github.com/matrix-org/matrix-js-sdk/pull/1850)).
## 🐛 Bug Fixes
* Fix broken voice calls, no ringing and broken call notifications ([\#1858](https://github.com/matrix-org/matrix-js-sdk/pull/1858)). Fixes vector-im/element-web#18578 vector-im/element-web#18538 and vector-im/element-web#18578. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Revert "Fix glare related regressions" ([\#1857](https://github.com/matrix-org/matrix-js-sdk/pull/1857)).
* Fix glare related regressions ([\#1851](https://github.com/matrix-org/matrix-js-sdk/pull/1851)). Fixes vector-im/element-web#18538 and vector-im/element-web#18538. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Fix temporary call messages being handled without call ([\#1834](https://github.com/matrix-org/matrix-js-sdk/pull/1834)). Contributed by [Palid](https://github.com/Palid).
* Fix conditional on returning file tree spaces ([\#1841](https://github.com/matrix-org/matrix-js-sdk/pull/1841)).
Changes in [12.3.1](https://github.com/vector-im/element-desktop/releases/tag/v12.3.1) (2021-08-17)
===================================================================================================
+23 -2
View File
@@ -22,8 +22,14 @@ ask you to make any refinements needed or merge it and make them ourselves.
Things that should go into your PR description:
* A changelog entry in the `Notes` section (see below)
* References to any bugs fixed by the change (in GitHub's `Fixes` notation)
* Notes for the reviewer that might help them to understand why the change is
necessary or how they might better review it.
* Describe the why and what is changing in the PR description so it's easy for
onlookers and reviewers to onboard and context switch.
* Include both **before** and **after** screenshots to easily compare and discuss
what's changing.
* Include a step-by-step testing strategy so that a reviewer can check out the
code locally and easily get to the point of testing your change.
* Add comments to the diff for the reviewer that might help them to understand
why the change is necessary or how they might better understand and review it.
Things that should *not* go into your PR description:
* Any information on how the code works or why you chose to do it the way
@@ -92,6 +98,21 @@ We use continuous integration, and all pull requests get automatically tested:
if your change breaks the build, then the PR will show that there are failed
checks, so please check back after a few minutes.
Tests
-----
If your PR is a feature (ie. if it's being labelled with the 'T-Enhancement'
label) then we require that the PR also includes tests. These need to test that
your feature works as expected and ideally test edge cases too. For the js-sdk
itself, your tests should generally be unit tests. matrix-react-sdk also uses
these guidelines, so for that your tests can be unit tests using
react-test-utils, snapshot tests or screenshot tests.
We don't require tests for bug fixes (T-Defect) but strongly encourage regression
tests for the bug itself wherever possible.
In the future we may formalise this more with a minimum test coverage
percentage for the diff.
Code style
----------
The js-sdk aims to target TypeScript/ES6. All new files should be written in
+1 -1
View File
@@ -1,6 +1,6 @@
console.log("Loading browser sdk");
var client = matrixcs.createClient("http://matrix.org");
var client = matrixcs.createClient("https://matrix.org");
client.publicRooms(function (err, data) {
if (err) {
console.error("err %s", JSON.stringify(err));
+3 -1
View File
@@ -1,6 +1,8 @@
<html>
<html lang="en">
<head>
<title>Test</title>
<meta charset="utf-8"/>
<link rel="icon" href="data:,">
<script src="lib/matrix.js"></script>
<script src="browserTest.js"></script>
</head>
+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 = (
+14 -10
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "12.3.1",
"version": "16.0.0",
"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 7 src spec",
"lint:js": "eslint --max-warnings 0 src spec",
"lint:js-fix": "eslint --fix src spec",
"lint:types": "tsc --noEmit",
"test": "jest",
@@ -56,6 +56,7 @@
"bs58": "^4.0.1",
"content-type": "^1.0.4",
"loglevel": "^1.7.1",
"matrix-events-sdk": "^0.0.1-beta.7",
"p-retry": "^4.5.0",
"qs": "^6.9.6",
"request": "^2.88.2",
@@ -74,22 +75,24 @@
"@babel/preset-env": "^7.12.11",
"@babel/preset-typescript": "^7.12.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.3.tgz",
"@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",
"@typescript-eslint/eslint-plugin": "^4.17.0",
"@typescript-eslint/parser": "^4.17.0",
"allchange": "^1.0.0",
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",
"allchange": "^1.0.6",
"babel-jest": "^26.6.3",
"babelify": "^10.0.0",
"better-docs": "^2.4.0-beta.9",
"browserify": "^17.0.0",
"docdash": "^1.2.0",
"eslint": "7.18.0",
"eslint": "8.9.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#2306b3d4da4eba908b256014b979f1d3d43d2945",
"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",
@@ -99,7 +102,7 @@
"rimraf": "^3.0.2",
"terser": "^5.5.1",
"tsify": "^5.0.2",
"typescript": "^4.1.3"
"typescript": "^4.5.3"
},
"jest": {
"testEnvironment": "node",
@@ -110,7 +113,8 @@
"<rootDir>/src/**/*.{js,ts}"
],
"coverageReporters": [
"text"
"text",
"json"
]
},
"typings": "./lib/index.d.ts"
+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"
+2
View File
@@ -20,6 +20,7 @@ limitations under the License.
import './olm-loader';
import MockHttpBackend from 'matrix-mock-request';
import { LocalStorageCryptoStore } from '../src/crypto/store/localStorage-crypto-store';
import { logger } from '../src/logger';
import { WebStorageSessionStore } from "../src/store/session/webstorage";
@@ -85,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 -32
View File
@@ -17,54 +17,34 @@ limitations under the License.
// load XmlHttpRequest mock
import "./setupTests";
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
import { MockStorageApi } from "../MockStorageApi";
import { WebStorageSessionStore } from "../../src/store/session/webstorage";
import MockHttpBackend from "matrix-mock-request";
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() {
@@ -91,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]],
+409
View File
@@ -1,5 +1,6 @@
import * as utils from "../test-utils";
import { CRYPTO_ENABLED } from "../../src/client";
import { MatrixEvent } from "../../src/models/event";
import { Filter, MemoryStore, Room } from "../../src/matrix";
import { TestClient } from "../TestClient";
@@ -392,6 +393,414 @@ describe("MatrixClient", function() {
return prom;
});
});
describe("partitionThreadedEvents", function() {
it("returns empty arrays when given an empty arrays", function() {
const events = [];
const [timeline, threaded] = client.partitionThreadedEvents(events);
expect(timeline).toEqual([]);
expect(threaded).toEqual([]);
});
it("copies pre-thread in-timeline vote events onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true };
const eventMessageInThread = buildEventMessageInThread();
const eventPollResponseReference = buildEventPollResponseReference();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const events = [
eventMessageInThread,
eventPollResponseReference,
eventPollStartThreadRoot,
];
// Vote has no threadId yet
expect(eventPollResponseReference.threadId).toBeFalsy();
const [timeline, threaded] = client.partitionThreadedEvents(events);
expect(timeline).toEqual([
// The message that was sent in a thread is missing
eventPollResponseReference,
eventPollStartThreadRoot,
]);
// The vote event has been copied into the thread
const eventRefWithThreadId = withThreadId(
eventPollResponseReference, eventPollStartThreadRoot.getId());
expect(eventRefWithThreadId.threadId).toBeTruthy();
expect(threaded).toEqual([
eventMessageInThread,
eventRefWithThreadId,
// Thread does not see thread root
]);
});
it("copies pre-thread in-timeline reactions onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true };
const eventMessageInThread = buildEventMessageInThread();
const eventReaction = buildEventReaction();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const events = [
eventMessageInThread,
eventReaction,
eventPollStartThreadRoot,
];
const [timeline, threaded] = client.partitionThreadedEvents(events);
expect(timeline).toEqual([
eventReaction,
eventPollStartThreadRoot,
]);
expect(threaded).toEqual([
eventMessageInThread,
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
]);
});
it("copies post-thread in-timeline vote events onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true };
const eventPollResponseReference = buildEventPollResponseReference();
const eventMessageInThread = buildEventMessageInThread();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const events = [
eventPollResponseReference,
eventMessageInThread,
eventPollStartThreadRoot,
];
const [timeline, threaded] = client.partitionThreadedEvents(events);
expect(timeline).toEqual([
eventPollResponseReference,
eventPollStartThreadRoot,
]);
expect(threaded).toEqual([
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
eventMessageInThread,
]);
});
it("copies post-thread in-timeline reactions onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true };
const eventReaction = buildEventReaction();
const eventMessageInThread = buildEventMessageInThread();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const events = [
eventReaction,
eventMessageInThread,
eventPollStartThreadRoot,
];
const [timeline, threaded] = client.partitionThreadedEvents(events);
expect(timeline).toEqual([
eventReaction,
eventPollStartThreadRoot,
]);
expect(threaded).toEqual([
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
eventMessageInThread,
]);
});
it("sends room state events to the main timeline only", function() {
client.clientOpts = { experimentalThreadSupport: true };
// This is based on recording the events in a real room:
const eventMessageInThread = buildEventMessageInThread();
const eventPollResponseReference = buildEventPollResponseReference();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const eventRoomName = buildEventRoomName();
const eventEncryption = buildEventEncryption();
const eventGuestAccess = buildEventGuestAccess();
const eventHistoryVisibility = buildEventHistoryVisibility();
const eventJoinRules = buildEventJoinRules();
const eventPowerLevels = buildEventPowerLevels();
const eventMember = buildEventMember();
const eventCreate = buildEventCreate();
const events = [
eventMessageInThread,
eventPollResponseReference,
eventPollStartThreadRoot,
eventRoomName,
eventEncryption,
eventGuestAccess,
eventHistoryVisibility,
eventJoinRules,
eventPowerLevels,
eventMember,
eventCreate,
];
const [timeline, threaded] = client.partitionThreadedEvents(events);
expect(timeline).toEqual([
// The message that was sent in a thread is missing
eventPollResponseReference,
eventPollStartThreadRoot,
eventRoomName,
eventEncryption,
eventGuestAccess,
eventHistoryVisibility,
eventJoinRules,
eventPowerLevels,
eventMember,
eventCreate,
]);
// Thread should contain only stuff that happened in the thread -
// no thread root, and no room state events
expect(threaded).toEqual([
eventMessageInThread,
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
]);
});
});
});
function withThreadId(event, newThreadId) {
const ret = event.toSnapshot();
ret.setThreadId(newThreadId);
return ret;
}
const buildEventMessageInThread = () => new MatrixEvent({
"age": 80098509,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
"device_id": "XISFUZSKHH",
"m.relates_to": {
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
"m.in_reply_to": {
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
},
"rel_type": "io.element.thread",
},
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
},
"event_id": "$W4chKIGYowtBblVLkRimeIg8TcdjETnxhDPGfi6NpDg",
"origin_server_ts": 1643815466378,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"type": "m.room.encrypted",
"unsigned": { "age": 80098509 },
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventPollResponseReference = () => new MatrixEvent({
"age": 80098509,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
"device_id": "XISFUZSKHH",
"m.relates_to": {
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
"rel_type": "m.reference",
},
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
},
"event_id": "$91JvpezvsF0cKgav3g8W-uEVS4WkDHgxbJZvL3uMR1g",
"origin_server_ts": 1643815458650,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"type": "m.room.encrypted",
"unsigned": { "age": 80106237 },
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventReaction = () => new MatrixEvent({
"content": {
"m.relates_to": {
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
"key": "🤗",
"rel_type": "m.annotation",
},
},
"origin_server_ts": 1643977249238,
"sender": "@andybalaam-test1:matrix.org",
"type": "m.reaction",
"unsigned": {
"age": 22598,
"transaction_id": "m1643977249073.16",
},
"event_id": "$86B2b-x3LgE4DlV4y24b7UHnt72LIA3rzjvMysTtAfA",
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
});
const buildEventPollStartThreadRoot = () => new MatrixEvent({
"age": 80108647,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
"device_id": "XISFUZSKHH",
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
},
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
"origin_server_ts": 1643815456240,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"type": "m.room.encrypted",
"unsigned": { "age": 80108647 },
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventRoomName = () => new MatrixEvent({
"age": 80123249,
"content": {
"name": "1 poll, 1 vote, 1 thread",
},
"event_id": "$QAdyNJtKnl1j7or2yMycbOCvb6bCgvHs5lg3ZMd5xWk",
"origin_server_ts": 1643815441638,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"state_key": "",
"type": "m.room.name",
"unsigned": { "age": 80123249 },
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventEncryption = () => new MatrixEvent({
"age": 80123383,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
},
"event_id": "$1hGykogKQkXbHw8bVuyE3BjHnFBEJBcUWnakd0ck2K0",
"origin_server_ts": 1643815441504,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"state_key": "",
"type": "m.room.encryption",
"unsigned": { "age": 80123383 },
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventGuestAccess = () => new MatrixEvent({
"age": 80123473,
"content": {
"guest_access": "can_join",
},
"event_id": "$4_2n-H6K9-0nPbnjjtIue2SU44tGJsnuTmi6UuSrh-U",
"origin_server_ts": 1643815441414,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"state_key": "",
"type": "m.room.guest_access",
"unsigned": { "age": 80123473 },
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventHistoryVisibility = () => new MatrixEvent({
"age": 80123556,
"content": {
"history_visibility": "shared",
},
"event_id": "$W6kp44CTnvciOiHSPyhp8dh4n2v1_9kclUPddeaQj0E",
"origin_server_ts": 1643815441331,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"state_key": "",
"type": "m.room.history_visibility",
"unsigned": { "age": 80123556 },
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventJoinRules = () => new MatrixEvent({
"age": 80123696,
"content": {
"join_rule": "invite",
},
"event_id": "$6JDDeDp7fEc0F6YnTWMruNcKWFltR3e9wk7wWDDJrAU",
"origin_server_ts": 1643815441191,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"state_key": "",
"type": "m.room.join_rules",
"unsigned": { "age": 80123696 },
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventPowerLevels = () => new MatrixEvent({
"age": 80124105,
"content": {
"ban": 50,
"events": {
"m.room.avatar": 50,
"m.room.canonical_alias": 50,
"m.room.encryption": 100,
"m.room.history_visibility": 100,
"m.room.name": 50,
"m.room.power_levels": 100,
"m.room.server_acl": 100,
"m.room.tombstone": 100,
},
"events_default": 0,
"historical": 100,
"invite": 0,
"kick": 50,
"redact": 50,
"state_default": 50,
"users": {
"@andybalaam-test1:matrix.org": 100,
},
"users_default": 0,
},
"event_id": "$XZY2YgQhXskpc7gmJJG3S0VmS9_QjjCUVeeFTfgfC2E",
"origin_server_ts": 1643815440782,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"state_key": "",
"type": "m.room.power_levels",
"unsigned": { "age": 80124105 },
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventMember = () => new MatrixEvent({
"age": 80125279,
"content": {
"avatar_url": "mxc://matrix.org/aNtbVcFfwotudypZcHsIcPOc",
"displayname": "andybalaam-test1",
"membership": "join",
},
"event_id": "$Ex5eVmMs_ti784mo8bgddynbwLvy6231lCycJr7Cl9M",
"origin_server_ts": 1643815439608,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"state_key": "@andybalaam-test1:matrix.org",
"type": "m.room.member",
"unsigned": { "age": 80125279 },
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventCreate = () => new MatrixEvent({
"age": 80126105,
"content": {
"creator": "@andybalaam-test1:matrix.org",
"room_version": "6",
},
"event_id": "$e7j2Gt37k5NPwB6lz2N3V9lO5pUdNK8Ai7i2FPEK-oI",
"origin_server_ts": 1643815438782,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"state_key": "",
"type": "m.room.create",
"unsigned": { "age": 80126105 },
"user_id": "@andybalaam-test1:matrix.org",
});
function assertObjectContains(obj, expected) {
+5 -2
View File
@@ -1,5 +1,6 @@
import * as utils from "../test-utils";
import HttpBackend from "matrix-mock-request";
import * as utils from "../test-utils";
import { MatrixClient } from "../../src/matrix";
import { MatrixScheduler } from "../../src/scheduler";
import { MemoryStore } from "../../src/store/memory";
@@ -104,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([
@@ -1,16 +1,16 @@
import { EventStatus } from "../../src/matrix";
import { EventStatus, RoomEvent } from "../../src/matrix";
import { MatrixScheduler } from "../../src/scheduler";
import { Room } from "../../src/models/room";
import { TestClient } from "../TestClient";
describe("MatrixClient retrying", function() {
let client = null;
let httpBackend = null;
let client: TestClient = null;
let httpBackend: TestClient["httpBackend"] = null;
let scheduler;
const userId = "@alice:localhost";
const accessToken = "aseukfgwef";
const roomId = "!room:here";
let room;
let room: Room;
beforeEach(function() {
scheduler = new MatrixScheduler();
@@ -23,7 +23,7 @@ describe("MatrixClient retrying", function() {
);
httpBackend = testClient.httpBackend;
client = testClient.client;
room = new Room(roomId);
room = new Room(roomId, client, userId);
client.store.storeRoom(room);
});
@@ -50,17 +50,23 @@ describe("MatrixClient retrying", function() {
it("should mark events as EventStatus.CANCELLED when cancelled", function() {
// send a couple of events; the second will be queued
const p1 = client.sendMessage(roomId, "m1").then(function(ev) {
const p1 = client.sendMessage(roomId, {
"msgtype": "m.text",
"body": "m1",
}).then(function() {
// we expect the first message to fail
throw new Error('Message 1 unexpectedly sent successfully');
}, (e) => {
}, () => {
// this is expected
});
// XXX: it turns out that the promise returned by this message
// never gets resolved.
// https://github.com/matrix-org/matrix-js-sdk/issues/496
client.sendMessage(roomId, "m2");
client.sendMessage(roomId, {
"msgtype": "m.text",
"body": "m2",
});
// both events should be in the timeline at this point
const tl = room.getLiveTimeline().getEvents();
@@ -72,7 +78,7 @@ describe("MatrixClient retrying", function() {
expect(ev2.status).toEqual(EventStatus.SENDING);
// the first message should get sent, and the second should get queued
httpBackend.when("PUT", "/send/m.room.message/").check(function(rq) {
httpBackend.when("PUT", "/send/m.room.message/").check(function() {
// ev2 should now have been queued
expect(ev2.status).toEqual(EventStatus.QUEUED);
@@ -88,8 +94,8 @@ describe("MatrixClient retrying", function() {
}).respond(400); // fail the first message
// wait for the localecho of ev1 to be updated
const p3 = new Promise((resolve, reject) => {
room.on("Room.localEchoUpdated", (ev0) => {
const p3 = new Promise<void>((resolve, reject) => {
room.on(RoomEvent.LocalEchoUpdated, (ev0) => {
if (ev0 === ev1) {
resolve();
}
@@ -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" });
});
+1
View File
@@ -16,6 +16,7 @@ limitations under the License.
*/
import anotherjson from "another-json";
import * as testUtils from "../test-utils";
import { TestClient } from "../TestClient";
import { logger } from "../../src/logger";
+16 -14
View File
@@ -85,6 +85,7 @@ export function mkEvent(opts) {
room_id: opts.room,
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: opts.content,
unsigned: opts.unsigned,
event_id: "$" + Math.random() + "-" + Math.random(),
};
if (opts.skey !== undefined) {
@@ -341,8 +342,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 +358,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));
+2
View File
@@ -14,7 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// eslint-disable-next-line no-restricted-imports
import { EventEmitter } from "events";
import { ReEmitter } from "../../src/ReEmitter";
const EVENTNAME = "UnknownEntry";
+1
View File
@@ -16,6 +16,7 @@ limitations under the License.
*/
import MockHttpBackend from "matrix-mock-request";
import * as sdk from "../../src";
import { AutoDiscovery } from "../../src/autodiscovery";
+3 -1
View File
@@ -1,4 +1,7 @@
import '../olm-loader';
// eslint-disable-next-line no-restricted-imports
import { EventEmitter } from "events";
import { Crypto } from "../../src/crypto";
import { WebStorageSessionStore } from "../../src/store/session/webstorage";
import { MemoryCryptoStore } from "../../src/crypto/store/memory-crypto-store";
@@ -8,7 +11,6 @@ import { MatrixEvent } from "../../src/models/event";
import { Room } from "../../src/models/room";
import * as olmlib from "../../src/crypto/olmlib";
import { sleep } from "../../src/utils";
import { EventEmitter } from "events";
import { CRYPTO_ENABLED } from "../../src/client";
import { DeviceInfo } from "../../src/crypto/deviceinfo";
+121 -73
View File
@@ -256,97 +256,145 @@ describe("MegolmDecryption", function() {
});
});
it("re-uses sessions for sequential messages", async function() {
mockCrypto.backupManager = {
backupGroupSession: () => {},
};
const mockStorage = new MockStorageApi();
const cryptoStore = new MemoryCryptoStore(mockStorage);
describe("session reuse and key reshares", () => {
let megolmEncryption;
let aliceDeviceInfo;
let mockRoom;
let olmDevice;
const olmDevice = new OlmDevice(cryptoStore);
olmDevice.verifySignature = jest.fn();
await olmDevice.init();
beforeEach(async () => {
mockCrypto.backupManager = {
backupGroupSession: () => {},
};
const mockStorage = new MockStorageApi();
const cryptoStore = new MemoryCryptoStore(mockStorage);
mockBaseApis.claimOneTimeKeys = jest.fn().mockReturnValue(Promise.resolve({
one_time_keys: {
'@alice:home.server': {
aliceDevice: {
'signed_curve25519:flooble': {
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
signatures: {
'@alice:home.server': {
'ed25519:aliceDevice': 'totally valid',
olmDevice = new OlmDevice(cryptoStore);
olmDevice.verifySignature = jest.fn();
await olmDevice.init();
mockBaseApis.claimOneTimeKeys = jest.fn().mockReturnValue(Promise.resolve({
one_time_keys: {
'@alice:home.server': {
aliceDevice: {
'signed_curve25519:flooble': {
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
signatures: {
'@alice:home.server': {
'ed25519:aliceDevice': 'totally valid',
},
},
},
},
},
},
},
}));
mockBaseApis.sendToDevice = jest.fn().mockResolvedValue(undefined);
}));
mockBaseApis.sendToDevice = jest.fn().mockResolvedValue(undefined);
mockCrypto.downloadKeys.mockReturnValue(Promise.resolve({
'@alice:home.server': {
aliceDevice: {
deviceId: 'aliceDevice',
isBlocked: jest.fn().mockReturnValue(false),
isUnverified: jest.fn().mockReturnValue(false),
getIdentityKey: jest.fn().mockReturnValue(
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE',
),
getFingerprint: jest.fn().mockReturnValue(''),
aliceDeviceInfo = {
deviceId: 'aliceDevice',
isBlocked: jest.fn().mockReturnValue(false),
isUnverified: jest.fn().mockReturnValue(false),
getIdentityKey: jest.fn().mockReturnValue(
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE',
),
getFingerprint: jest.fn().mockReturnValue(''),
};
mockCrypto.downloadKeys.mockReturnValue(Promise.resolve({
'@alice:home.server': {
aliceDevice: aliceDeviceInfo,
},
},
}));
}));
mockCrypto.checkDeviceTrust.mockReturnValue({
isVerified: () => false,
mockCrypto.checkDeviceTrust.mockReturnValue({
isVerified: () => false,
});
megolmEncryption = new MegolmEncryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: mockBaseApis,
roomId: ROOM_ID,
config: {
rotation_period_ms: 9999999999999,
},
});
mockRoom = {
getEncryptionTargetMembers: jest.fn().mockReturnValue(
[{ userId: "@alice:home.server" }],
),
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
};
});
const megolmEncryption = new MegolmEncryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: mockBaseApis,
roomId: ROOM_ID,
config: {
rotation_period_ms: 9999999999999,
},
});
const mockRoom = {
getEncryptionTargetMembers: jest.fn().mockReturnValue(
[{ userId: "@alice:home.server" }],
),
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
};
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some text",
});
expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
it("re-uses sessions for sequential messages", async function() {
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some text",
});
expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
// this should have claimed a key for alice as it's starting a new session
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
);
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
['@alice:home.server'], false,
);
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
);
// this should have claimed a key for alice as it's starting a new session
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
);
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
['@alice:home.server'], false,
);
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
);
mockBaseApis.claimOneTimeKeys.mockReset();
mockBaseApis.claimOneTimeKeys.mockReset();
const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some more text",
const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some more text",
});
// this should *not* have claimed a key as it should be using the same session
expect(mockBaseApis.claimOneTimeKeys).not.toHaveBeenCalled();
// likewise they should show the same session ID
expect(ct2.session_id).toEqual(ct1.session_id);
});
// this should *not* have claimed a key as it should be using the same session
expect(mockBaseApis.claimOneTimeKeys).not.toHaveBeenCalled();
it("re-shares keys to devices it's already sent to", async function() {
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some text",
});
// likewise they should show the same session ID
expect(ct2.session_id).toEqual(ct1.session_id);
mockBaseApis.sendToDevice.mockClear();
await megolmEncryption.reshareKeyWithDevice(
olmDevice.deviceCurve25519Key,
ct1.session_id,
'@alice:home.server',
aliceDeviceInfo,
);
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
});
it("does not re-share keys to devices whose keys have changed", async function() {
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some text",
});
aliceDeviceInfo.getIdentityKey = jest.fn().mockReturnValue(
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWI',
);
mockBaseApis.sendToDevice.mockClear();
await megolmEncryption.reshareKeyWithDevice(
olmDevice.deviceCurve25519Key,
ct1.session_id,
'@alice:home.server',
aliceDeviceInfo,
);
expect(mockBaseApis.sendToDevice).not.toHaveBeenCalled();
});
});
});
+2 -2
View File
@@ -246,7 +246,7 @@ describe("OlmDevice", function() {
// After a single tick through the first task, it should have
// claimed ownership of all devices to avoid deadlocking others.
expect(Object.keys(aliceOlmDevice._sessionsInProgress).length).toBe(2);
expect(Object.keys(aliceOlmDevice.sessionsInProgress).length).toBe(2);
const task2 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
aliceOlmDevice, baseApis, devicesByUserBA,
@@ -254,7 +254,7 @@ describe("OlmDevice", function() {
// The second task should not have changed the ownership count, as
// it's waiting on the first task.
expect(Object.keys(aliceOlmDevice._sessionsInProgress).length).toBe(2);
expect(Object.keys(aliceOlmDevice.sessionsInProgress).length).toBe(2);
// Track the tasks, but don't await them yet.
const promises = Promise.all([
+3 -3
View File
@@ -288,7 +288,7 @@ describe("MegolmBackup", function() {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice._pickleKey),
session: ibGroupSession.pickle(olmDevice.pickleKey),
},
txn);
});
@@ -369,7 +369,7 @@ describe("MegolmBackup", function() {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice._pickleKey),
session: ibGroupSession.pickle(olmDevice.pickleKey),
},
txn);
});
@@ -546,7 +546,7 @@ describe("MegolmBackup", function() {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice._pickleKey),
session: ibGroupSession.pickle(olmDevice.pickleKey),
},
txn);
});
+25 -20
View File
@@ -17,6 +17,7 @@ limitations under the License.
import '../../olm-loader';
import anotherjson from 'another-json';
import * as olmlib from "../../../src/crypto/olmlib";
import { TestClient } from '../../TestClient';
import { HttpResponse, setHttpResponses } from '../../test-utils';
@@ -39,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() {
@@ -59,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) => {
@@ -79,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) => {
@@ -130,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 () => {};
@@ -160,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,
@@ -174,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: {
@@ -235,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",
@@ -310,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;
@@ -331,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 () => {};
@@ -414,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,
@@ -490,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",
@@ -560,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
@@ -578,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 () => {};
@@ -647,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 () => {};
@@ -785,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: {
@@ -797,7 +802,7 @@ describe("Cross Signing", function() {
},
},
);
const bob = await makeTestClient(
const { client: bob } = await makeTestClient(
{ userId: "@bob:example.com", deviceId: "Dynabook" },
);
+1 -1
View File
@@ -26,7 +26,7 @@ export async function resetCrossSigningKeys(client, {
crypto.crossSigningInfo.keys = oldKeys;
throw e;
}
crypto.baseApis.emit("crossSigning.keysChanged", {});
crypto.emit("crossSigning.keysChanged", {});
await crypto.afterCrossSigningLocalKeyChange();
}
@@ -18,11 +18,11 @@ import {
IndexedDBCryptoStore,
} from '../../../src/crypto/store/indexeddb-crypto-store';
import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store';
import { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager';
import 'fake-indexeddb/auto';
import 'jest-localstorage-mock';
import { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager';
const requests = [
{
requestId: "A",
-1
View File
@@ -23,7 +23,6 @@ import { makeTestClients } from './verification/util';
import { encryptAES } from "../../../src/crypto/aes";
import { resetCrossSigningKeys, createSecretStorageKey } from "./crypto-utils";
import { logger } from '../../../src/logger';
import * as utils from "../../../src/utils";
try {
@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
"../../../../src/crypto/verification/request/ToDeviceChannel";
import { MatrixEvent } from "../../../../src/models/event";
"../../../../src/crypto/verification/request/ToDeviceChannel";
describe("InRoomChannel tests", function() {
const ALICE = "@alice:hs.tld";
@@ -69,7 +69,7 @@ describe("verification request integration tests with crypto layer", function()
bobVerifier.verify();
// XXX: Private function access (but it's a test, so we're okay)
bobVerifier._endTimer();
bobVerifier.endTimer();
});
const aliceRequest = await alice.client.requestVerification("@bob:example.com");
await aliceRequest.waitFor(r => r.started);
@@ -77,6 +77,6 @@ describe("verification request integration tests with crypto layer", function()
expect(aliceVerifier).toBeInstanceOf(SAS);
// XXX: Private function access (but it's a test, so we're okay)
aliceVerifier._endTimer();
aliceVerifier.endTimer();
});
});
@@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { VerificationBase } from '../../../../src/crypto/verification/Base';
import { CrossSigningInfo } from '../../../../src/crypto/CrossSigning';
import { encodeBase64 } from "../../../../src/crypto/olmlib";
import { setupWebcrypto, teardownWebcrypto } from './util';
import { VerificationBase } from '../../../../src/crypto/verification/Base';
jest.useFakeTimers();
@@ -93,7 +93,7 @@ describe("self-verifications", () => {
undefined, // startEvent
request,
);
verification._resolve = () => undefined;
verification.resolve = () => undefined;
const result = await verification.done();
+2 -1
View File
@@ -15,9 +15,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import nodeCrypto from "crypto";
import { TestClient } from '../../../TestClient';
import { MatrixEvent } from "../../../../src/models/event";
import nodeCrypto from "crypto";
import { logger } from '../../../../src/logger';
export async function makeTestClients(userInfos, options) {
-34
View File
@@ -1,34 +0,0 @@
import { FilterComponent } from "../../src/filter-component";
import { mkEvent } from '../test-utils';
describe("Filter Component", function() {
describe("types", function() {
it("should filter out events with other types", function() {
const filter = new FilterComponent({ types: ['m.room.message'] });
const event = mkEvent({
type: 'm.room.member',
content: { },
room: 'roomId',
event: true,
});
const checkResult = filter.check(event);
expect(checkResult).toBe(false);
});
it("should validate events with the same type", function() {
const filter = new FilterComponent({ types: ['m.room.message'] });
const event = mkEvent({
type: 'm.room.message',
content: { },
room: 'roomId',
event: true,
});
const checkResult = filter.check(event);
expect(checkResult).toBe(true);
});
});
});
+132
View File
@@ -0,0 +1,132 @@
import {
RelationType,
UNSTABLE_FILTER_RELATED_BY_REL_TYPES,
UNSTABLE_FILTER_RELATED_BY_SENDERS,
} from "../../src";
import { FilterComponent } from "../../src/filter-component";
import { mkEvent } from '../test-utils';
describe("Filter Component", function() {
describe("types", function() {
it("should filter out events with other types", function() {
const filter = new FilterComponent({ types: ['m.room.message'] });
const event = mkEvent({
type: 'm.room.member',
content: { },
room: 'roomId',
event: true,
});
const checkResult = filter.check(event);
expect(checkResult).toBe(false);
});
it("should validate events with the same type", function() {
const filter = new FilterComponent({ types: ['m.room.message'] });
const event = mkEvent({
type: 'm.room.message',
content: { },
room: 'roomId',
event: true,
});
const checkResult = filter.check(event);
expect(checkResult).toBe(true);
});
it("should filter out events by relation participation", function() {
const currentUserId = '@me:server.org';
const filter = new FilterComponent({
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]: [currentUserId],
}, currentUserId);
const threadRootNotParticipated = mkEvent({
type: 'm.room.message',
content: {},
room: 'roomId',
user: '@someone-else:server.org',
event: true,
unsigned: {
"m.relations": {
[RelationType.Thread]: {
count: 2,
current_user_participated: false,
},
},
},
});
expect(filter.check(threadRootNotParticipated)).toBe(false);
});
it("should keep events by relation participation", function() {
const currentUserId = '@me:server.org';
const filter = new FilterComponent({
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]: [currentUserId],
}, currentUserId);
const threadRootParticipated = mkEvent({
type: 'm.room.message',
content: {},
unsigned: {
"m.relations": {
[RelationType.Thread]: {
count: 2,
current_user_participated: true,
},
},
},
user: '@someone-else:server.org',
room: 'roomId',
event: true,
});
expect(filter.check(threadRootParticipated)).toBe(true);
});
it("should filter out events by relation type", function() {
const filter = new FilterComponent({
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]: [RelationType.Thread],
});
const referenceRelationEvent = mkEvent({
type: 'm.room.message',
content: {},
room: 'roomId',
event: true,
unsigned: {
"m.relations": {
[RelationType.Reference]: {},
},
},
});
expect(filter.check(referenceRelationEvent)).toBe(false);
});
it("should keep events by relation type", function() {
const filter = new FilterComponent({
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]: [RelationType.Thread],
});
const threadRootEvent = mkEvent({
type: 'm.room.message',
content: {},
unsigned: {
"m.relations": {
[RelationType.Thread]: {
count: 2,
current_user_participated: true,
},
},
},
room: 'roomId',
event: true,
});
expect(filter.check(threadRootEvent)).toBe(true);
});
});
});
+56
View File
@@ -0,0 +1,56 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { makeLocationContent } from "../../src/content-helpers";
import {
ASSET_NODE_TYPE,
LocationAssetType,
LOCATION_EVENT_TYPE,
TIMESTAMP_NODE_TYPE,
} from "../../src/@types/location";
import { TEXT_NODE_TYPE } from "../../src/@types/extensible_events";
describe("Location", function() {
it("should create a valid location with defaults", function() {
const loc = makeLocationContent("txt", "geo:foo", 134235435);
expect(loc.body).toEqual("txt");
expect(loc.msgtype).toEqual("m.location");
expect(loc.geo_uri).toEqual("geo:foo");
expect(LOCATION_EVENT_TYPE.findIn(loc)).toEqual({
uri: "geo:foo",
description: undefined,
});
expect(ASSET_NODE_TYPE.findIn(loc)).toEqual({ type: LocationAssetType.Self });
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual("txt");
expect(TIMESTAMP_NODE_TYPE.findIn(loc)).toEqual(134235435);
});
it("should create a valid location with explicit properties", function() {
const loc = makeLocationContent(
"txxt", "geo:bar", 134235436, "desc", LocationAssetType.Pin);
expect(loc.body).toEqual("txxt");
expect(loc.msgtype).toEqual("m.location");
expect(loc.geo_uri).toEqual("geo:bar");
expect(LOCATION_EVENT_TYPE.findIn(loc)).toEqual({
uri: "geo:bar",
description: "desc",
});
expect(ASSET_NODE_TYPE.findIn(loc)).toEqual({ type: LocationAssetType.Pin });
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual("txxt");
expect(TIMESTAMP_NODE_TYPE.findIn(loc)).toEqual(134235436);
});
});
@@ -11,8 +11,9 @@ import {
UNSTABLE_MSC3089_TREE_SUBTYPE,
} from "../../src/@types/event";
import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib";
import { MatrixEvent } from "../../src/models/event";
import { EventStatus, MatrixEvent } from "../../src/models/event";
import { Preset } from "../../src/@types/partials";
import * as testUtils from "../test-utils";
jest.useFakeTimers();
@@ -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:
// {
@@ -84,11 +91,7 @@ describe("MatrixClient", function() {
return pendingLookup.promise;
}
// >1 pending thing, and they are different, whine.
expect(false).toBe(
true, ">1 pending request. You should probably handle them. " +
"PENDING: " + JSON.stringify(pendingLookup) + " JUST GOT: " +
method + " " + path,
);
expect(false).toBe(true);
}
pendingLookup = {
promise: new Promise(() => {}),
@@ -116,6 +119,7 @@ describe("MatrixClient", function() {
}
if (next.error) {
// eslint-disable-next-line
return Promise.reject({
errcode: next.error.errcode,
httpStatus: next.error.httpStatus,
@@ -126,7 +130,7 @@ describe("MatrixClient", function() {
}
return Promise.resolve(next.data);
}
expect(true).toBe(false, "Expected different request. " + logLine);
expect(true).toBe(false);
return new Promise(() => {});
}
@@ -151,7 +155,7 @@ describe("MatrixClient", function() {
baseUrl: "https://my.home.server",
idBaseUrl: identityServerUrl,
accessToken: "my.access.token",
request: function() {}, // NOP
request: function() {} as any, // NOP
store: store,
scheduler: scheduler,
userId: userId,
@@ -167,6 +171,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);
@@ -237,6 +242,7 @@ describe("MatrixClient", function() {
it("should get (unstable) file trees with valid state", async () => {
const roomId = "!room:example.org";
const mockRoom = {
getMyMembership: () => "join",
currentState: {
getStateEvents: (eventType, stateKey) => {
if (eventType === EventType.RoomCreate) {
@@ -270,9 +276,33 @@ describe("MatrixClient", function() {
expect(tree.room).toBe(mockRoom);
});
it("should not get (unstable) file trees if not joined", async () => {
const roomId = "!room:example.org";
const mockRoom = {
getMyMembership: () => "leave", // "not join"
};
client.getRoom = (getRoomId) => {
expect(getRoomId).toEqual(roomId);
return mockRoom;
};
const tree = client.unstableGetFileTreeSpace(roomId);
expect(tree).toBeFalsy();
});
it("should not get (unstable) file trees for unknown rooms", async () => {
const roomId = "!room:example.org";
client.getRoom = (getRoomId) => {
expect(getRoomId).toEqual(roomId);
return null; // imply unknown
};
const tree = client.unstableGetFileTreeSpace(roomId);
expect(tree).toBeFalsy();
});
it("should not get (unstable) file trees with invalid create contents", async () => {
const roomId = "!room:example.org";
const mockRoom = {
getMyMembership: () => "join",
currentState: {
getStateEvents: (eventType, stateKey) => {
if (eventType === EventType.RoomCreate) {
@@ -307,6 +337,7 @@ describe("MatrixClient", function() {
it("should not get (unstable) file trees with invalid purpose/subtype contents", async () => {
const roomId = "!room:example.org";
const mockRoom = {
getMyMembership: () => "join",
currentState: {
getStateEvents: (eventType, stateKey) => {
if (eventType === EventType.RoomCreate) {
@@ -338,15 +369,17 @@ 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);
const filter = new Filter("0", filterId);
filter.setDefinition({ "room": { "timeline": { "limit": 8 } } });
store.getFilter.mockReturnValue(filter);
const syncPromise = new Promise((resolve, reject) => {
const syncPromise = new Promise<void>((resolve, reject) => {
client.on("sync", function syncListener(state) {
if (state === "SYNCING") {
expect(httpLookups.length).toEqual(0);
@@ -367,7 +400,7 @@ describe("MatrixClient", function() {
});
it("should return the same sync state as emitted sync events", async function() {
const syncingPromise = new Promise((resolve) => {
const syncingPromise = new Promise<void>((resolve) => {
client.on("sync", function syncListener(state) {
expect(state).toEqual(client.getSyncState());
if (state === "SYNCING") {
@@ -387,7 +420,7 @@ describe("MatrixClient", function() {
it("should use an existing filter if id is present in localStorage", function() {
});
it("should handle localStorage filterId missing from the server", function(done) {
function getFilterName(userId, suffix) {
function getFilterName(userId, suffix?: string) {
// scope this on the user ID because people may login on many accounts
// and they all need to be stored!
return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : "");
@@ -421,12 +454,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" },
@@ -462,7 +498,7 @@ describe("MatrixClient", function() {
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(1);
expect(client.retryImmediately()).toBe(
true, "retryImmediately returned false",
true,
);
jest.advanceTimersByTime(1);
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
@@ -477,6 +513,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" },
});
@@ -533,6 +570,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" },
@@ -543,32 +581,33 @@ describe("MatrixClient", function() {
});
it("should transition ERROR -> CATCHUP after /sync if prev failed",
function(done) {
const expectedStates = [];
acceptKeepalives = false;
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH,
error: { errcode: "KEEPALIVE_FAIL" },
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, data: {},
});
httpLookups.push({
method: "GET", path: "/sync", data: SYNC_DATA,
});
function(done) {
const expectedStates = [];
acceptKeepalives = false;
httpLookups = [];
httpLookups.push(CAPABILITIES_RESPONSE);
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH,
error: { errcode: "KEEPALIVE_FAIL" },
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, data: {},
});
httpLookups.push({
method: "GET", path: "/sync", data: SYNC_DATA,
});
expectedStates.push(["RECONNECTING", null]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["CATCHUP", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
expectedStates.push(["RECONNECTING", null]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["CATCHUP", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition PREPARED -> SYNCING after /sync", function(done) {
const expectedStates = [];
@@ -598,32 +637,32 @@ describe("MatrixClient", function() {
});
xit("should transition ERROR -> SYNCING after /sync if prev failed",
function(done) {
const expectedStates = [];
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
});
httpLookups.push(SYNC_RESPONSE);
function(done) {
const expectedStates = [];
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
});
httpLookups.push(SYNC_RESPONSE);
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["ERROR", "SYNCING"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["ERROR", "SYNCING"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition SYNCING -> SYNCING on subsequent /sync successes",
function(done) {
const expectedStates = [];
httpLookups.push(SYNC_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
function(done) {
const expectedStates = [];
httpLookups.push(SYNC_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["SYNCING", "SYNCING"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["SYNCING", "SYNCING"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition ERROR -> ERROR if keepalive keeps failing", function(done) {
acceptKeepalives = false;
@@ -671,7 +710,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",
@@ -702,4 +742,242 @@ 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);
});
});
describe("threads", () => {
it("partitions root events to room timeline and thread timeline", () => {
const supportsExperimentalThreads = client.supportsExperimentalThreads;
client.supportsExperimentalThreads = () => true;
const rootEvent = new MatrixEvent({
"content": {},
"origin_server_ts": 1,
"room_id": "!room1:matrix.org",
"sender": "@alice:matrix.org",
"type": "m.room.message",
"unsigned": {
"m.relations": {
"io.element.thread": {
"latest_event": {},
"count": 33,
"current_user_participated": false,
},
},
},
"event_id": "$ev1",
"user_id": "@alice:matrix.org",
});
expect(rootEvent.isThreadRoot).toBe(true);
const [room, threads] = client.partitionThreadedEvents([rootEvent]);
expect(room).toHaveLength(1);
expect(threads).toHaveLength(1);
// Restore method
client.supportsExperimentalThreads = supportsExperimentalThreads;
});
});
});
+191 -14
View File
@@ -14,21 +14,25 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClient } from "../../../src";
import { IContent, MatrixClient, MatrixEvent } from "../../../src";
import { Room } from "../../../src/models/room";
import { UNSTABLE_MSC3089_BRANCH } from "../../../src/@types/event";
import { IEncryptedFile, RelationType, UNSTABLE_MSC3089_BRANCH } from "../../../src/@types/event";
import { EventTimelineSet } from "../../../src/models/event-timeline-set";
import { EventTimeline } from "../../../src/models/event-timeline";
import { MSC3089Branch } from "../../../src/models/MSC3089Branch";
import { MSC3089TreeSpace } from "../../../src/models/MSC3089TreeSpace";
describe("MSC3089Branch", () => {
let client: MatrixClient;
// @ts-ignore - TS doesn't know that this is a type
let indexEvent: any;
let directory: MSC3089TreeSpace;
let branch: MSC3089Branch;
let branch2: MSC3089Branch;
const branchRoomId = "!room:example.org";
const fileEventId = "$file";
const fileEventId2 = "$second_file";
const staticTimelineSets = {} as EventTimelineSet;
const staticRoom = {
@@ -50,7 +54,12 @@ describe("MSC3089Branch", () => {
getRoomId: () => branchRoomId,
getStateKey: () => fileEventId,
});
branch = new MSC3089Branch(client, indexEvent);
directory = new MSC3089TreeSpace(client, branchRoomId);
branch = new MSC3089Branch(client, indexEvent, directory);
branch2 = new MSC3089Branch(client, {
getRoomId: () => branchRoomId,
getStateKey: () => fileEventId2,
} as MatrixEvent, directory);
});
it('should know the file event ID', () => {
@@ -69,13 +78,15 @@ describe("MSC3089Branch", () => {
});
it('should be able to delete the file', async () => {
const eventIdOrder = [fileEventId, fileEventId2];
const stateFn = jest.fn()
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
expect(roomId).toEqual(branchRoomId);
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
expect(content).toMatchObject({});
expect(content['active']).toBeUndefined();
expect(stateKey).toEqual(fileEventId);
expect(stateKey).toEqual(eventIdOrder[stateFn.mock.calls.length - 1]);
return Promise.resolve(); // return value not used
});
@@ -83,16 +94,19 @@ describe("MSC3089Branch", () => {
const redactFn = jest.fn().mockImplementation((roomId: string, eventId: string) => {
expect(roomId).toEqual(branchRoomId);
expect(eventId).toEqual(fileEventId);
expect(eventId).toEqual(eventIdOrder[stateFn.mock.calls.length - 1]);
return Promise.resolve(); // return value not used
});
client.redactEvent = redactFn;
branch.getVersionHistory = () => Promise.resolve([branch, branch2]);
branch2.getVersionHistory = () => Promise.resolve([branch2]);
await branch.delete();
expect(stateFn).toHaveBeenCalledTimes(1);
expect(redactFn).toHaveBeenCalledTimes(1);
expect(stateFn).toHaveBeenCalledTimes(2);
expect(redactFn).toHaveBeenCalledTimes(2);
});
it('should know its name', async () => {
@@ -127,16 +141,71 @@ describe("MSC3089Branch", () => {
expect(stateFn).toHaveBeenCalledTimes(1);
});
it('should be v1 by default', () => {
indexEvent.getContent = () => ({ active: true });
const res = branch.version;
expect(res).toEqual(1);
});
it('should be vN when set', () => {
indexEvent.getContent = () => ({ active: true, version: 3 });
const res = branch.version;
expect(res).toEqual(3);
});
it('should be unlocked by default', async () => {
indexEvent.getContent = () => ({ active: true });
const res = branch.isLocked();
expect(res).toEqual(false);
});
it('should use lock status from index event', async () => {
indexEvent.getContent = () => ({ active: true, locked: true });
const res = branch.isLocked();
expect(res).toEqual(true);
});
it('should be able to change its locked status', async () => {
const locked = true;
indexEvent.getContent = () => ({ active: true, retained: true });
const stateFn = jest.fn()
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
expect(roomId).toEqual(branchRoomId);
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
expect(content).toMatchObject({
retained: true, // canary for copying state
active: true,
locked: locked,
});
expect(stateKey).toEqual(fileEventId);
return Promise.resolve(); // return value not used
});
client.sendStateEvent = stateFn;
await branch.setLocked(locked);
expect(stateFn).toHaveBeenCalledTimes(1);
});
it('should be able to return event information', async () => {
const mxcLatter = "example.org/file";
const fileContent = { isFile: "not quite", url: "mxc://" + mxcLatter };
const eventsArr = [
{ getId: () => "$not-file", getContent: () => ({}) },
{ getId: () => fileEventId, getContent: () => ({ file: fileContent }) },
];
client.getEventTimeline = () => Promise.resolve({
getEvents: () => eventsArr,
}) as any as Promise<EventTimeline>; // partial
const fileEvent = { getId: () => fileEventId, getOriginalContent: () => ({ file: fileContent }) };
staticRoom.getUnfilteredTimelineSet = () => ({
findEventById: (eventId) => {
expect(eventId).toEqual(fileEventId);
return fileEvent;
},
}) as EventTimelineSet;
client.mxcUrlToHttp = (mxc: string) => {
expect(mxc).toEqual("mxc://" + mxcLatter);
return `https://example.org/_matrix/media/v1/download/${mxcLatter}`;
@@ -151,4 +220,112 @@ describe("MSC3089Branch", () => {
httpUrl: expect.stringMatching(`.+${mxcLatter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`),
});
});
it('should be able to return the event object', async () => {
const mxcLatter = "example.org/file";
const fileContent = { isFile: "not quite", url: "mxc://" + mxcLatter };
const fileEvent = { getId: () => fileEventId, getOriginalContent: () => ({ file: fileContent }) };
staticRoom.getUnfilteredTimelineSet = () => ({
findEventById: (eventId) => {
expect(eventId).toEqual(fileEventId);
return fileEvent;
},
}) as EventTimelineSet;
client.mxcUrlToHttp = (mxc: string) => {
expect(mxc).toEqual("mxc://" + mxcLatter);
return `https://example.org/_matrix/media/v1/download/${mxcLatter}`;
};
client.decryptEventIfNeeded = () => Promise.resolve();
const res = await branch.getFileEvent();
expect(res).toBeDefined();
expect(res).toBe(fileEvent);
});
it('should create new versions of itself', async () => {
const canaryName = "canary";
const canaryContents = "contents go here";
const canaryFile = {} as IEncryptedFile;
const canaryAddl = { canary: true };
indexEvent.getContent = () => ({ active: true, retained: true });
const stateKeyOrder = [fileEventId2, fileEventId];
const stateFn = jest.fn()
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
expect(roomId).toEqual(branchRoomId);
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
expect(stateKey).toEqual(stateKeyOrder[stateFn.mock.calls.length - 1]);
if (stateKey === fileEventId) {
expect(content).toMatchObject({
retained: true, // canary for copying state
active: false,
});
} else if (stateKey === fileEventId2) {
expect(content).toMatchObject({
active: true,
version: 2,
name: canaryName,
});
} else {
throw new Error("Unexpected state key: " + stateKey);
}
return Promise.resolve(); // return value not used
});
client.sendStateEvent = stateFn;
const createFn = jest.fn().mockImplementation((
name: string,
contents: ArrayBuffer,
info: Partial<IEncryptedFile>,
addl: IContent,
) => {
expect(name).toEqual(canaryName);
expect(contents).toBe(canaryContents);
expect(info).toBe(canaryFile);
expect(addl).toMatchObject({
...canaryAddl,
"m.new_content": true,
"m.relates_to": {
"rel_type": RelationType.Replace,
"event_id": fileEventId,
},
});
return Promise.resolve({ event_id: fileEventId2 });
});
directory.createFile = createFn;
await branch.createNewVersion(canaryName, canaryContents, canaryFile, canaryAddl);
expect(stateFn).toHaveBeenCalledTimes(2);
expect(createFn).toHaveBeenCalledTimes(1);
});
it('should fetch file history', async () => {
branch2.getFileEvent = () => Promise.resolve({
replacingEventId: () => undefined,
getId: () => fileEventId2,
} as MatrixEvent);
branch.getFileEvent = () => Promise.resolve({
replacingEventId: () => fileEventId2,
getId: () => fileEventId,
} as MatrixEvent);
const events = [await branch.getFileEvent(), await branch2.getFileEvent(), {
replacingEventId: (): string => null,
getId: () => "$unknown",
}];
staticRoom.getLiveTimeline = () => ({ getEvents: () => events }) as EventTimeline;
directory.getFile = (evId: string) => {
expect(evId).toEqual(fileEventId);
return branch;
};
const results = await branch2.getVersionHistory();
expect(results).toMatchObject([
branch2,
branch,
]);
});
});
+120 -17
View File
@@ -24,7 +24,6 @@ import {
TreePermissions,
} from "../../../src/models/MSC3089TreeSpace";
import { DEFAULT_ALPHABET } from "../../../src/utils";
import { MockBlob } from "../../MockBlob";
import { MatrixError } from "../../../src/http-api";
describe("MSC3089TreeSpace", () => {
@@ -227,40 +226,61 @@ describe("MSC3089TreeSpace", () => {
[targetUser]: expectedPl,
},
});
// Store new power levels so the `getPermissions()` test passes
makePowerLevels(content);
return Promise.resolve();
});
client.sendStateEvent = fn;
await tree.setPermissions(targetUser, role);
expect(fn.mock.calls.length).toBe(1);
const finalPermissions = tree.getPermissions(targetUser);
expect(finalPermissions).toEqual(role);
}
it('should support setting Viewer permissions', () => {
return evaluatePowerLevels({
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
users_default: 1024,
events_default: 1025,
events: {
[EventType.RoomPowerLevels]: 1026,
},
}, TreePermissions.Viewer, 1024);
});
it('should support setting Editor permissions', () => {
return evaluatePowerLevels({
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
events_default: 1024,
}, TreePermissions.Editor, 1024);
users_default: 1024,
events_default: 1025,
events: {
[EventType.RoomPowerLevels]: 1026,
},
}, TreePermissions.Editor, 1025);
});
it('should support setting Owner permissions', () => {
return evaluatePowerLevels({
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
users_default: 1024,
events_default: 1025,
events: {
[EventType.RoomPowerLevels]: 1024,
[EventType.RoomPowerLevels]: 1026,
},
}, TreePermissions.Owner, 1024);
}, TreePermissions.Owner, 1026);
});
it('should support demoting permissions', () => {
return evaluatePowerLevels({
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
users_default: 1024,
events_default: 1025,
events: {
[EventType.RoomPowerLevels]: 1026,
},
users: {
[targetUser]: 2222,
},
@@ -270,11 +290,15 @@ describe("MSC3089TreeSpace", () => {
it('should support promoting permissions', () => {
return evaluatePowerLevels({
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
events_default: 1024,
users_default: 1024,
events_default: 1025,
events: {
[EventType.RoomPowerLevels]: 1026,
},
users: {
[targetUser]: 5,
},
}, TreePermissions.Editor, 1024);
}, TreePermissions.Editor, 1025);
});
it('should support defaults: Viewer', () => {
@@ -862,12 +886,8 @@ describe("MSC3089TreeSpace", () => {
const fileName = "My File.txt";
const fileContents = "This is a test file";
// Mock out Blob for the test environment
(<any>global).Blob = MockBlob;
const uploadFn = jest.fn().mockImplementation((contents: Blob, opts: any) => {
expect(contents).toBeInstanceOf(Blob);
expect(contents.size).toEqual(fileContents.length);
const uploadFn = jest.fn().mockImplementation((contents: Buffer, opts: any) => {
expect(contents.length).toEqual(fileContents.length);
expect(opts).toMatchObject({
includeFilename: false,
onlyContentUri: true, // because the tests rely on this - we shouldn't really be testing for this.
@@ -883,6 +903,7 @@ describe("MSC3089TreeSpace", () => {
body: fileName,
url: mxc,
file: fileInfo,
metadata: true, // additional content from test
[UNSTABLE_MSC3089_LEAF.unstable]: {}, // test to ensure we're definitely using unstable
});
@@ -900,14 +921,78 @@ describe("MSC3089TreeSpace", () => {
name: fileName,
});
return Promise.resolve(); // return value not used.
return Promise.resolve({ event_id: "wrong" }); // return value shouldn't be used
});
client.sendStateEvent = sendStateFn;
const buf = Uint8Array.from(Array.from(fileContents).map((_, i) => fileContents.charCodeAt(i)));
const buf = Buffer.from(fileContents);
// We clone the file info just to make sure it doesn't get mutated for the test.
await tree.createFile(fileName, buf, Object.assign({}, fileInfo));
const result = await tree.createFile(fileName, buf, Object.assign({}, fileInfo), { metadata: true });
expect(result).toMatchObject({ event_id: fileEventId });
expect(uploadFn).toHaveBeenCalledTimes(1);
expect(sendMsgFn).toHaveBeenCalledTimes(1);
expect(sendStateFn).toHaveBeenCalledTimes(1);
});
it('should upload file versions', async () => {
const mxc = "mxc://example.org/file";
const fileInfo = {
mimetype: "text/plain",
// other fields as required by encryption, but ignored here
};
const fileEventId = "$file";
const fileName = "My File.txt";
const fileContents = "This is a test file";
const uploadFn = jest.fn().mockImplementation((contents: Buffer, opts: any) => {
expect(contents.length).toEqual(fileContents.length);
expect(opts).toMatchObject({
includeFilename: false,
onlyContentUri: true, // because the tests rely on this - we shouldn't really be testing for this.
});
return Promise.resolve(mxc);
});
client.uploadContent = uploadFn;
const sendMsgFn = jest.fn().mockImplementation((roomId: string, contents: any) => {
expect(roomId).toEqual(tree.roomId);
const content = {
msgtype: MsgType.File,
body: fileName,
url: mxc,
file: fileInfo,
};
expect(contents).toMatchObject({
...content,
"m.new_content": content,
[UNSTABLE_MSC3089_LEAF.unstable]: {}, // test to ensure we're definitely using unstable
});
return Promise.resolve({ event_id: fileEventId }); // eslint-disable-line camelcase
});
client.sendMessage = sendMsgFn;
const sendStateFn = jest.fn()
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
expect(roomId).toEqual(tree.roomId);
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
expect(stateKey).toEqual(fileEventId);
expect(content).toMatchObject({
active: true,
name: fileName,
});
return Promise.resolve({ event_id: "wrong" }); // return value shouldn't be used
});
client.sendStateEvent = sendStateFn;
const buf = Buffer.from(fileContents);
// We clone the file info just to make sure it doesn't get mutated for the test.
const result = await tree.createFile(fileName, buf, Object.assign({}, fileInfo), { "m.new_content": true });
expect(result).toMatchObject({ event_id: fileEventId });
expect(uploadFn).toHaveBeenCalledTimes(1);
expect(sendMsgFn).toHaveBeenCalledTimes(1);
@@ -933,7 +1018,7 @@ describe("MSC3089TreeSpace", () => {
it('should return falsy for unknown files', () => {
const fileEventId = "$file";
room.currentState = {
getStateEvents: (eventType: string, stateKey?: string) => {
getStateEvents: (eventType: string, stateKey?: string): MatrixEvent[] | MatrixEvent | null => {
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
expect(stateKey).toEqual(fileEventId);
return null;
@@ -960,4 +1045,22 @@ describe("MSC3089TreeSpace", () => {
expect(files.length).toEqual(1);
expect(files[0].indexEvent).toBe(firstFile);
});
it('should list all files', () => {
const firstFile = { getContent: () => ({ active: true }) };
const secondFile = { getContent: () => ({ active: false }) }; // deliberately inactive
room.currentState = {
getStateEvents: (eventType: string, stateKey?: string) => {
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
expect(stateKey).toBeUndefined();
return [firstFile, secondFile];
},
};
const files = tree.listAllFiles();
expect(files).toBeDefined();
expect(files.length).toEqual(2);
expect(files[0].indexEvent).toBe(firstFile);
expect(files[1].indexEvent).toBe(secondFile);
});
});
+16
View File
@@ -302,4 +302,20 @@ describe('NotificationService', function() {
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(false);
});
it("a rule with no conditions matches every event.", function() {
expect(pushProcessor.ruleMatchesEvent({
rule_id: "rule1",
actions: [],
conditions: [],
default: false,
enabled: true,
}, testEvent)).toBe(true);
expect(pushProcessor.ruleMatchesEvent({
rule_id: "rule1",
actions: [],
default: false,
enabled: true,
}, testEvent)).toBe(true);
});
});
+3 -3
View File
@@ -15,7 +15,7 @@ limitations under the License.
*/
import { EventTimelineSet } from "../../src/models/event-timeline-set";
import { MatrixEvent } from "../../src/models/event";
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { Room } from "../../src/models/room";
import { Relations } from "../../src/models/relations";
@@ -103,7 +103,7 @@ describe("Relations", function() {
// Add the target event first, then the relation event
{
const relationsCreated = new Promise(resolve => {
targetEvent.once("Event.relationsCreated", resolve);
targetEvent.once(MatrixEventEvent.RelationsCreated, resolve);
});
const timelineSet = new EventTimelineSet(room, {
@@ -118,7 +118,7 @@ describe("Relations", function() {
// Add the relation event first, then the target event
{
const relationsCreated = new Promise(resolve => {
targetEvent.once("Event.relationsCreated", resolve);
targetEvent.once(MatrixEventEvent.RelationsCreated, resolve);
});
const timelineSet = new EventTimelineSet(room, {
+1 -1
View File
@@ -120,7 +120,7 @@ describe("RoomState", function() {
it("should return a single MatrixEvent if a state_key was specified",
function() {
const event = state.getStateEvents("m.room.member", userA);
expect(event.getContent()).toEqual({
expect(event.getContent()).toMatchObject({
membership: "join",
});
});
File diff suppressed because it is too large Load Diff
+15 -66
View File
@@ -26,6 +26,15 @@ describe("utils", function() {
"foo=bar&baz=beer%40",
);
});
it("should handle boolean and numeric values", function() {
const params = {
string: "foobar",
number: 12345,
boolean: false,
};
expect(utils.encodeParams(params)).toEqual("string=foobar&number=12345&boolean=false");
});
});
describe("encodeUri", function() {
@@ -111,10 +120,10 @@ describe("utils", function() {
describe("deepCompare", function() {
const assert = {
isTrue: function(x) {
isTrue: function(x: any) {
expect(x).toBe(true);
},
isFalse: function(x) {
isFalse: function(x: any) {
expect(x).toBe(false);
},
};
@@ -176,10 +185,10 @@ describe("utils", function() {
// no two different function is equal really, they capture their
// context variables so even if they have same toString(), they
// won't have same functionality
const func = function(x) {
const func = function() {
return true;
};
const func2 = function(x) {
const func2 = function() {
return true;
};
assert.isTrue(utils.deepCompare(func, func));
@@ -189,66 +198,6 @@ describe("utils", function() {
});
});
describe("extend", function() {
const SOURCE = { "prop2": 1, "string2": "x", "newprop": "new" };
it("should extend", function() {
const target = {
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
};
const merged = {
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
"newprop": "new",
};
const sourceOrig = JSON.stringify(SOURCE);
utils.extend(target, SOURCE);
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
// check the originial wasn't modified
expect(JSON.stringify(SOURCE)).toEqual(sourceOrig);
});
it("should ignore null", function() {
const target = {
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
};
const merged = {
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
"newprop": "new",
};
const sourceOrig = JSON.stringify(SOURCE);
utils.extend(target, null, SOURCE);
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
// check the originial wasn't modified
expect(JSON.stringify(SOURCE)).toEqual(sourceOrig);
});
it("should handle properties created with defineProperties", function() {
const source = Object.defineProperties({}, {
"enumerableProp": {
get: function() {
return true;
},
enumerable: true,
},
"nonenumerableProp": {
get: function() {
return true;
},
},
});
// TODO: Fix type
const target: any = {};
utils.extend(target, source);
expect(target.enumerableProp).toBe(true);
expect(target.nonenumerableProp).toBe(undefined);
});
});
describe("chunkPromises", function() {
it("should execute promises in chunks", async function() {
let promiseCount = 0;
@@ -273,7 +222,7 @@ describe("utils", function() {
it('should retry', async () => {
let count = 0;
const val = {};
const fn = (attempt) => {
const fn = (attempt: any) => {
count++;
// If this expectation fails then it can appear as a Jest Timeout due to
@@ -480,7 +429,7 @@ describe("utils", function() {
},
[72]: "test",
};
const output = [
const output: any = [
["72", "test"],
["a", 42],
["b", [
+154 -12
View File
@@ -17,6 +17,7 @@ limitations under the License.
import { TestClient } from '../../TestClient';
import { MatrixCall, CallErrorCode, CallEvent } from '../../../src/webrtc/call';
import { SDPStreamMetadataKey, SDPStreamMetadataPurpose } from '../../../src/webrtc/callEventTypes';
import { RoomMember } from "../../../src";
const DUMMY_SDP = (
"v=0\r\n" +
@@ -81,6 +82,51 @@ class MockRTCPeerConnection {
}
close() {}
getStats() { return []; }
addTrack(track: MockMediaStreamTrack) {return new MockRTCRtpSender(track);}
}
class MockRTCRtpSender {
constructor(public track: MockMediaStreamTrack) {}
replaceTrack(track: MockMediaStreamTrack) {this.track = track;}
}
class MockMediaStreamTrack {
constructor(public readonly id: string, public readonly kind: "audio" | "video", public enabled = true) {}
stop() {}
}
class MockMediaStream {
constructor(
public id: string,
private tracks: MockMediaStreamTrack[] = [],
) {}
getTracks() { return this.tracks; }
getAudioTracks() { return this.tracks.filter((track) => track.kind === "audio"); }
getVideoTracks() { return this.tracks.filter((track) => track.kind === "video"); }
addEventListener() {}
removeEventListener() { }
addTrack(track: MockMediaStreamTrack) {this.tracks.push(track);}
removeTrack(track: MockMediaStreamTrack) {this.tracks.splice(this.tracks.indexOf(track), 1);}
}
class MockMediaDeviceInfo {
constructor(
public kind: "audio" | "video",
) {}
}
class MockMediaHandler {
getUserMediaStream(audio: boolean, video: boolean) {
const tracks = [];
if (audio) tracks.push(new MockMediaStreamTrack("audio_track", "audio"));
if (video) tracks.push(new MockMediaStreamTrack("video_track", "video"));
return new MockMediaStream("mock_stream_from_media_handler", tracks);
}
stopUserMediaStream() {}
}
describe('Call', function() {
@@ -98,13 +144,9 @@ describe('Call', function() {
global.navigator = {
mediaDevices: {
// @ts-ignore Mock
getUserMedia: () => {
return {
getTracks: () => [],
getAudioTracks: () => [],
getVideoTracks: () => [],
};
},
getUserMedia: () => new MockMediaStream("local_stream"),
// @ts-ignore Mock
enumerateDevices: async () => [new MockMediaDeviceInfo("audio"), new MockMediaDeviceInfo("video")],
},
};
@@ -115,7 +157,7 @@ describe('Call', function() {
RTCSessionDescription: {},
// @ts-ignore Mock
RTCIceCandidate: {},
getUserMedia: {},
getUserMedia: () => new MockMediaStream("local_stream"),
};
// @ts-ignore Mock
global.document = {};
@@ -124,6 +166,8 @@ describe('Call', function() {
// We just stub out sendEvent: we're not interested in testing the client's
// event sending code here
client.client.sendEvent = () => {};
client.client.mediaHandler = new MockMediaHandler;
client.client.getMediaHandler = () => client.client.mediaHandler;
client.httpBackend.when("GET", "/voip/turnServer").respond(200, {});
call = new MatrixCall({
client: client.client,
@@ -319,7 +363,7 @@ describe('Call', function() {
sdp: DUMMY_SDP,
},
[SDPStreamMetadataKey]: {
"stream_id": {
"remote_stream": {
purpose: SDPStreamMetadataPurpose.Usermedia,
audio_muted: true,
video_muted: false,
@@ -329,8 +373,16 @@ describe('Call', function() {
},
});
call.pushRemoteFeed({ id: "stream_id", getAudioTracks: () => ["track1"], getVideoTracks: () => ["track1"] });
const feed = call.getFeeds().find((feed) => feed.stream.id === "stream_id");
call.pushRemoteFeed(
new MockMediaStream(
"remote_stream",
[
new MockMediaStreamTrack("remote_audio_track", "audio"),
new MockMediaStreamTrack("remote_video_track", "video"),
],
),
);
const feed = call.getFeeds().find((feed) => feed.stream.id === "remote_stream");
expect(feed?.purpose).toBe(SDPStreamMetadataPurpose.Usermedia);
expect(feed?.isAudioMuted()).toBeTruthy();
expect(feed?.isVideoMuted()).not.toBeTruthy();
@@ -342,7 +394,7 @@ describe('Call', function() {
await callPromise;
call.getOpponentMember = () => {
return { userId: "@bob:bar.uk" };
return { userId: "@bob:bar.uk" } as RoomMember;
};
await call.onAnswerReceived({
@@ -363,4 +415,94 @@ describe('Call', function() {
call.setScreensharingEnabled(true);
expect(call.setScreensharingEnabledWithoutMetadataSupport).toHaveBeenCalled();
});
it("should fallback to answering with no video", async () => {
await client.httpBackend.flush();
call.shouldAnswerWithMediaType = (wantedValue: boolean) => wantedValue;
client.client.mediaHandler.getUserMediaStream = jest.fn().mockRejectedValue("reject");
await call.answer(true, true);
expect(client.client.mediaHandler.getUserMediaStream).toHaveBeenNthCalledWith(1, true, true);
expect(client.client.mediaHandler.getUserMediaStream).toHaveBeenNthCalledWith(2, true, false);
});
it("should handle mid-call device changes", async () => {
client.client.mediaHandler.getUserMediaStream = jest.fn().mockReturnValue(
new MockMediaStream(
"stream", [
new MockMediaStreamTrack("audio_track", "audio"),
new MockMediaStreamTrack("video_track", "video"),
],
),
);
const callPromise = call.placeVideoCall();
await client.httpBackend.flush();
await callPromise;
await call.onAnswerReceived({
getContent: () => {
return {
version: 1,
call_id: call.callId,
party_id: 'party_id',
answer: {
sdp: DUMMY_SDP,
},
};
},
});
await call.updateLocalUsermediaStream(
new MockMediaStream(
"replacement_stream",
[
new MockMediaStreamTrack("new_audio_track", "audio"),
new MockMediaStreamTrack("video_track", "video"),
],
),
);
expect(call.localUsermediaStream.id).toBe("stream");
expect(call.localUsermediaStream.getAudioTracks()[0].id).toBe("new_audio_track");
expect(call.localUsermediaStream.getVideoTracks()[0].id).toBe("video_track");
expect(call.usermediaSenders.find((sender) => {
return sender?.track?.kind === "audio";
}).track.id).toBe("new_audio_track");
expect(call.usermediaSenders.find((sender) => {
return sender?.track?.kind === "video";
}).track.id).toBe("video_track");
});
it("should handle upgrade to video call", async () => {
const callPromise = call.placeVoiceCall();
await client.httpBackend.flush();
await callPromise;
await call.onAnswerReceived({
getContent: () => {
return {
version: 1,
call_id: call.callId,
party_id: 'party_id',
answer: {
sdp: DUMMY_SDP,
},
[SDPStreamMetadataKey]: {},
};
},
});
await call.upgradeCall(false, true);
expect(call.localUsermediaStream.getAudioTracks()[0].id).toBe("audio_track");
expect(call.localUsermediaStream.getVideoTracks()[0].id).toBe("video_track");
expect(call.usermediaSenders.find((sender) => {
return sender?.track?.kind === "audio";
}).track.id).toBe("audio_track");
expect(call.usermediaSenders.find((sender) => {
return sender?.track?.kind === "video";
}).track.id).toBe("video_track");
});
});
+6 -5
View File
@@ -90,8 +90,9 @@ export interface ISenderNotificationPermissionCondition
key: string;
}
export type PushRuleCondition = IPushRuleCondition<string>
| IEventMatchCondition
// XXX: custom conditions are possible but always fail, and break the typescript discriminated union so ignore them here
// IPushRuleCondition<Exclude<string, ConditionKind>> unfortunately does not resolve this at the time of writing.
export type PushRuleCondition = IEventMatchCondition
| IContainsDisplayNameCondition
| IRoomMemberCountCondition
| ISenderNotificationPermissionCondition;
@@ -146,12 +147,12 @@ export interface IPusher {
app_display_name: string;
app_id: string;
data: {
format?: string; // TODO: Types
format?: string;
url?: string; // TODO: Required if kind==http
brand?: string; // TODO: For email notifications only?
brand?: string; // TODO: For email notifications only? Unspecced field
};
device_display_name: string;
kind: string; // TODO: Types
kind: "http" | string;
lang: string;
profile_tag?: string;
pushkey: string;
+12 -10
View File
@@ -1,5 +1,5 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
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.
@@ -14,14 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
export class MockBlob {
private contents: number[] = [];
// disable lint because these are wire responses
/* eslint-disable camelcase */
public constructor(private parts: ArrayLike<number>[]) {
parts.forEach(p => Array.from(p).forEach(e => this.contents.push(e)));
}
public get size(): number {
return this.contents.length;
}
/**
* Represents a response to the CSAPI `/refresh` endpoint.
*/
export interface IRefreshTokenResponse {
access_token: string;
expires_in_ms: number;
refresh_token: string;
}
/* eslint-enable camelcase */
+20
View File
@@ -92,6 +92,15 @@ export enum EventType {
export enum RelationType {
Annotation = "m.annotation",
Replace = "m.replace",
Reference = "m.reference",
/**
* Note, "io.element.thread" is hardcoded
* Should be replaced with "m.thread" once MSC3440 lands
* Can not use `UnstableValue` as TypeScript does not
* allow computed values in enums
* https://github.com/microsoft/TypeScript/issues/27976
*/
Thread = "io.element.thread",
}
export enum MsgType {
@@ -103,6 +112,7 @@ export enum MsgType {
Audio = "m.audio",
Location = "m.location",
Video = "m.video",
KeyVerificationRequest = "m.key.verification.request",
}
export const RoomCreateTypeField = "type";
@@ -168,6 +178,16 @@ export const UNSTABLE_ELEMENT_FUNCTIONAL_USERS = new UnstableValue(
"io.element.functional_members",
"io.element.functional_members");
/**
* A type of message that affects visibility of a message,
* as per https://github.com/matrix-org/matrix-doc/pull/3531
*
* @experimental
*/
export const EVENT_VISIBILITY_CHANGE_TYPE = new UnstableValue(
"m.visibility",
"org.matrix.msc3531.visibility");
export interface IEncryptedFile {
url: string;
mimetype?: string;
@@ -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");
+8 -26
View File
@@ -33,11 +33,7 @@ declare global {
}
interface Window {
electron?: Electron;
}
interface Electron {
getDesktopCapturerSources(options: GetSourcesOptions): Promise<Array<DesktopCapturerSource>>;
webkitAudioContext: typeof AudioContext;
}
interface Crypto {
@@ -66,27 +62,6 @@ declare global {
};
}
interface DesktopCapturerSource {
id: string;
name: string;
thumbnailURL: string;
}
interface GetSourcesOptions {
types: Array<string>;
thumbnailSize?: {
height: number;
width: number;
};
fetchWindowIcons?: boolean;
}
interface HTMLAudioElement {
// sinkId & setSinkId are experimental and typescript doesn't know about them
sinkId: string;
setSinkId(outputId: string);
}
interface DummyInterfaceWeShouldntBeUsingThis {}
interface Navigator {
@@ -107,4 +82,11 @@ declare global {
interface PromiseConstructor {
allSettled<T>(promises: Promise<T>[]): Promise<Array<ISettledFulfilled<T> | ISettledRejected>>;
}
interface RTCRtpTransceiver {
// This has been removed from TS
// (https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1029),
// but we still need this for MatrixCall::getRidOfRTXCodecs()
setCodecPreferences(codecs: RTCRtpCodecCapability[]): void;
}
}
+71
View File
@@ -0,0 +1,71 @@
/*
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 enum LocationAssetType {
Self = "m.self",
Pin = "m.pin",
}
/* From the spec at:
* https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md
{
"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: LocationAssetType;
};
[TEXT_NODE_TYPE.name]: string;
[TIMESTAMP_NODE_TYPE.name]: number;
}
/* eslint-enable camelcase */
+18 -9
View File
@@ -55,12 +55,20 @@ export enum JoinRule {
* @deprecated Reserved keyword. Should not be used. Not yet implemented.
*/
Private = "private",
Knock = "knock", // MSC2403 - only valid inside experimental room versions at this time.
Restricted = "restricted", // MSC3083 - only valid inside experimental room versions at this time.
Knock = "knock",
Restricted = "restricted",
}
export enum RestrictedAllowType {
RoomMembership = "m.room_membership", // MSC3083 - only valid inside experimental room versions at this time.
RoomMembership = "m.room_membership",
}
export interface IJoinRuleEventContent {
join_rule: JoinRule; // eslint-disable-line camelcase
allow?: {
type: RestrictedAllowType;
room_id: string; // eslint-disable-line camelcase
}[];
}
export enum GuestAccess {
@@ -75,10 +83,11 @@ export enum HistoryVisibility {
WorldReadable = "world_readable",
}
// XXX move to OlmDevice when converted
export interface InboundGroupSessionData {
room_id: string; // eslint-disable-line camelcase
session: string;
keysClaimed: Record<string, string>;
forwardingCurve25519KeyChain: string[];
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;
}
+17 -1
View File
@@ -15,10 +15,11 @@ limitations under the License.
*/
import { Callback } from "../client";
import { IContent } from "../models/event";
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 */
@@ -139,4 +140,19 @@ export interface IBindThreePidBody {
id_access_token: string;
sid: string;
}
export interface IRelationsRequestOpts {
from?: string;
to?: string;
limit?: number;
direction?: Direction;
}
export interface IRelationsResponse {
original_event: IEvent;
chunk: IEvent[];
next_batch?: string;
prev_batch?: string;
}
/* eslint-enable camelcase */
+8 -11
View File
@@ -15,26 +15,23 @@ limitations under the License.
*/
import { IPublicRoomsChunkRoom } from "../client";
import { RoomType } from "./event";
import { IStrippedState } from "../sync-accumulator";
// Types relating to Rooms of type `m.space` and related APIs
/* eslint-disable camelcase */
export interface ISpaceSummaryRoom extends IPublicRoomsChunkRoom {
num_refs: number;
room_type: string;
}
export interface ISpaceSummaryEvent {
room_id: string;
event_id: string;
export interface IHierarchyRelation extends IStrippedState {
origin_server_ts: number;
type: string;
state_key: string;
content: {
order?: string;
suggested?: boolean;
auto_join?: boolean;
via?: string[];
};
}
export interface IHierarchyRoom extends IPublicRoomsChunkRoom {
room_type?: RoomType | string;
children_state: IHierarchyRelation[];
}
/* eslint-enable camelcase */
+1
View File
@@ -25,4 +25,5 @@ export interface IThreepid {
address: string;
validated_at: number; // eslint-disable-line camelcase
added_at: number; // eslint-disable-line camelcase
bound?: boolean;
}
+22 -7
View File
@@ -16,21 +16,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// eslint-disable-next-line no-restricted-imports
import { EventEmitter } from "events";
import { ListenerMap, TypedEventEmitter } from "./models/typed-event-emitter";
export class ReEmitter {
private target: EventEmitter;
constructor(private readonly target: EventEmitter) {}
constructor(target: EventEmitter) {
this.target = target;
}
reEmit(source: EventEmitter, eventNames: string[]) {
public reEmit(source: EventEmitter, eventNames: string[]): void {
for (const eventName of eventNames) {
// We include the source as the last argument for event handlers which may need it,
// such as read receipt listeners on the client class which won't have the context
// of the room.
const forSource = (...args) => {
const forSource = (...args: any[]) => {
// EventEmitter special cases 'error' to make the emit function throw if no
// handler is attached, which sort of makes sense for making sure that something
// handles an error, but for re-emitting, there could be a listener on the original
@@ -48,3 +47,19 @@ export class ReEmitter {
}
}
}
export class TypedReEmitter<
Events extends string,
Arguments extends ListenerMap<Events>,
> extends ReEmitter {
constructor(target: TypedEventEmitter<Events, Arguments>) {
super(target);
}
public reEmit<ReEmittedEvents extends string, T extends Events & ReEmittedEvents>(
source: TypedEventEmitter<ReEmittedEvents, any>,
eventNames: T[],
): void {
super.reEmit(source, eventNames);
}
}
+56 -133
View File
@@ -17,79 +17,20 @@ limitations under the License.
/** @module auto-discovery */
import { logger } from './logger';
import { URL as NodeURL } from "url";
import { IClientWellKnown, IWellKnownConfig } from "./client";
import { logger } from './logger';
// Dev note: Auto discovery is part of the spec.
// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
/**
* Description for what an automatically discovered client configuration
* would look like. Although this is a class, it is recommended that it
* be treated as an interface definition rather than as a class.
*
* Additional properties than those defined here may be present, and
* should follow the Java package naming convention.
*/
class DiscoveredClientConfig { // eslint-disable-line no-unused-vars
// Dev note: this is basically a copy/paste of the .well-known response
// object as defined in the spec. It does have additional information,
// however. Overall, this exists to serve as a place for documentation
// and not functionality.
// See https://matrix.org/docs/spec/client_server/r0.4.0.html#get-well-known-matrix-client
constructor() {
/**
* The homeserver configuration the client should use. This will
* always be present on the object.
* @type {{state: string, base_url: string}} The configuration.
*/
this["m.homeserver"] = {
/**
* The lookup result state. If this is anything other than
* AutoDiscovery.SUCCESS then base_url may be falsey. Additionally,
* if this is not AutoDiscovery.SUCCESS then the client should
* assume the other properties in the client config (such as
* the identity server configuration) are not valid.
*/
state: AutoDiscovery.PROMPT,
/**
* If the state is AutoDiscovery.FAIL_ERROR or .FAIL_PROMPT
* then this will contain a human-readable (English) message
* for what went wrong. If the state is none of those previously
* mentioned, this will be falsey.
*/
error: "Something went wrong",
/**
* The base URL clients should use to talk to the homeserver,
* particularly for the login process. May be falsey if the
* state is not AutoDiscovery.SUCCESS.
*/
base_url: "https://matrix.org",
};
/**
* The identity server configuration the client should use. This
* will always be present on teh object.
* @type {{state: string, base_url: string}} The configuration.
*/
this["m.identity_server"] = {
/**
* The lookup result state. If this is anything other than
* AutoDiscovery.SUCCESS then base_url may be falsey.
*/
state: AutoDiscovery.PROMPT,
/**
* The base URL clients should use for interacting with the
* identity server. May be falsey if the state is not
* AutoDiscovery.SUCCESS.
*/
base_url: "https://vector.im",
};
}
export enum AutoDiscoveryAction {
SUCCESS = "SUCCESS",
IGNORE = "IGNORE",
PROMPT = "PROMPT",
FAIL_PROMPT = "FAIL_PROMPT",
FAIL_ERROR = "FAIL_ERROR",
}
/**
@@ -102,55 +43,36 @@ export class AutoDiscovery {
// translate the meaning of the states in the spec, but also
// support our own if needed.
static get ERROR_INVALID() {
return "Invalid homeserver discovery response";
}
public static readonly ERROR_INVALID = "Invalid homeserver discovery response";
static get ERROR_GENERIC_FAILURE() {
return "Failed to get autodiscovery configuration from server";
}
public static readonly ERROR_GENERIC_FAILURE = "Failed to get autodiscovery configuration from server";
static get ERROR_INVALID_HS_BASE_URL() {
return "Invalid base_url for m.homeserver";
}
public static readonly ERROR_INVALID_HS_BASE_URL = "Invalid base_url for m.homeserver";
static get ERROR_INVALID_HOMESERVER() {
return "Homeserver URL does not appear to be a valid Matrix homeserver";
}
public static readonly ERROR_INVALID_HOMESERVER = "Homeserver URL does not appear to be a valid Matrix homeserver";
static get ERROR_INVALID_IS_BASE_URL() {
return "Invalid base_url for m.identity_server";
}
public static readonly ERROR_INVALID_IS_BASE_URL = "Invalid base_url for m.identity_server";
static get ERROR_INVALID_IDENTITY_SERVER() {
return "Identity server URL does not appear to be a valid identity server";
}
// eslint-disable-next-line
public static readonly ERROR_INVALID_IDENTITY_SERVER = "Identity server URL does not appear to be a valid identity server";
static get ERROR_INVALID_IS() {
return "Invalid identity server discovery response";
}
public static readonly ERROR_INVALID_IS = "Invalid identity server discovery response";
static get ERROR_MISSING_WELLKNOWN() {
return "No .well-known JSON file found";
}
public static readonly ERROR_MISSING_WELLKNOWN = "No .well-known JSON file found";
static get ERROR_INVALID_JSON() {
return "Invalid JSON";
}
public static readonly ERROR_INVALID_JSON = "Invalid JSON";
static get ALL_ERRORS() {
return [
AutoDiscovery.ERROR_INVALID,
AutoDiscovery.ERROR_GENERIC_FAILURE,
AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
AutoDiscovery.ERROR_INVALID_HOMESERVER,
AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
AutoDiscovery.ERROR_INVALID_IS,
AutoDiscovery.ERROR_MISSING_WELLKNOWN,
AutoDiscovery.ERROR_INVALID_JSON,
];
}
public static readonly ALL_ERRORS = [
AutoDiscovery.ERROR_INVALID,
AutoDiscovery.ERROR_GENERIC_FAILURE,
AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
AutoDiscovery.ERROR_INVALID_HOMESERVER,
AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
AutoDiscovery.ERROR_INVALID_IS,
AutoDiscovery.ERROR_MISSING_WELLKNOWN,
AutoDiscovery.ERROR_INVALID_JSON,
];
/**
* The auto discovery failed. The client is expected to communicate
@@ -158,7 +80,7 @@ export class AutoDiscovery {
* @return {string}
* @constructor
*/
static get FAIL_ERROR() { return "FAIL_ERROR"; }
public static readonly FAIL_ERROR = AutoDiscoveryAction.FAIL_ERROR;
/**
* The auto discovery failed, however the client may still recover
@@ -169,7 +91,7 @@ export class AutoDiscovery {
* @return {string}
* @constructor
*/
static get FAIL_PROMPT() { return "FAIL_PROMPT"; }
public static readonly FAIL_PROMPT = AutoDiscoveryAction.FAIL_PROMPT;
/**
* The auto discovery didn't fail but did not find anything of
@@ -178,14 +100,14 @@ export class AutoDiscovery {
* @return {string}
* @constructor
*/
static get PROMPT() { return "PROMPT"; }
public static readonly PROMPT = AutoDiscoveryAction.PROMPT;
/**
* The auto discovery was successful.
* @return {string}
* @constructor
*/
static get SUCCESS() { return "SUCCESS"; }
public static readonly SUCCESS = AutoDiscoveryAction.SUCCESS;
/**
* Validates and verifies client configuration information for purposes
@@ -193,13 +115,13 @@ export class AutoDiscovery {
* and identity server URL the client would want. Additional details
* may also be included, and will be transparently brought into the
* response object unaltered.
* @param {string} wellknown The configuration object itself, as returned
* @param {object} wellknown The configuration object itself, as returned
* by the .well-known auto-discovery endpoint.
* @return {Promise<DiscoveredClientConfig>} Resolves to the verified
* configuration, which may include error states. Rejects on unexpected
* failure, not when verification fails.
*/
static async fromDiscoveryConfig(wellknown) {
public static async fromDiscoveryConfig(wellknown: any): Promise<IClientWellKnown> {
// Step 1 is to get the config, which is provided to us here.
// We default to an error state to make the first few checks easier to
@@ -240,7 +162,7 @@ export class AutoDiscovery {
// Step 2: Make sure the homeserver URL is valid *looking*. We'll make
// sure it points to a homeserver in Step 3.
const hsUrl = this._sanitizeWellKnownUrl(
const hsUrl = this.sanitizeWellKnownUrl(
wellknown["m.homeserver"]["base_url"],
);
if (!hsUrl) {
@@ -250,7 +172,7 @@ export class AutoDiscovery {
}
// Step 3: Make sure the homeserver URL points to a homeserver.
const hsVersions = await this._fetchWellKnownObject(
const hsVersions = await this.fetchWellKnownObject(
`${hsUrl}/_matrix/client/versions`,
);
if (!hsVersions || !hsVersions.raw["versions"]) {
@@ -272,7 +194,7 @@ export class AutoDiscovery {
};
// Step 5: Try to pull out the identity server configuration
let isUrl = "";
let isUrl: string | boolean = "";
if (wellknown["m.identity_server"]) {
// We prepare a failing identity server response to save lines later
// in this branch.
@@ -287,7 +209,7 @@ export class AutoDiscovery {
// Step 5a: Make sure the URL is valid *looking*. We'll make sure it
// points to an identity server in Step 5b.
isUrl = this._sanitizeWellKnownUrl(
isUrl = this.sanitizeWellKnownUrl(
wellknown["m.identity_server"]["base_url"],
);
if (!isUrl) {
@@ -299,10 +221,10 @@ export class AutoDiscovery {
// Step 5b: Verify there is an identity server listening on the provided
// URL.
const isResponse = await this._fetchWellKnownObject(
const isResponse = await this.fetchWellKnownObject(
`${isUrl}/_matrix/identity/api/v1`,
);
if (!isResponse || !isResponse.raw || isResponse.action !== "SUCCESS") {
if (!isResponse || !isResponse.raw || isResponse.action !== AutoDiscoveryAction.SUCCESS) {
logger.error("Invalid /api/v1 response");
failingClientConfig["m.identity_server"].error =
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER;
@@ -317,7 +239,7 @@ export class AutoDiscovery {
// Step 6: Now that the identity server is valid, or never existed,
// populate the IS section.
if (isUrl && isUrl.length > 0) {
if (isUrl && isUrl.toString().length > 0) {
clientConfig["m.identity_server"] = {
state: AutoDiscovery.SUCCESS,
error: null,
@@ -359,7 +281,7 @@ export class AutoDiscovery {
* configuration, which may include error states. Rejects on unexpected
* failure, not when discovery fails.
*/
static async findClientConfig(domain) {
public static async findClientConfig(domain: string): Promise<IClientWellKnown> {
if (!domain || typeof(domain) !== "string" || domain.length === 0) {
throw new Error("'domain' must be a string of non-zero length");
}
@@ -395,13 +317,13 @@ export class AutoDiscovery {
// Step 1: Actually request the .well-known JSON file and make sure it
// at least has a homeserver definition.
const wellknown = await this._fetchWellKnownObject(
const wellknown = await this.fetchWellKnownObject(
`https://${domain}/.well-known/matrix/client`,
);
if (!wellknown || wellknown.action !== "SUCCESS") {
if (!wellknown || wellknown.action !== AutoDiscoveryAction.SUCCESS) {
logger.error("No response or error when parsing .well-known");
if (wellknown.reason) logger.error(wellknown.reason);
if (wellknown.action === "IGNORE") {
if (wellknown.action === AutoDiscoveryAction.IGNORE) {
clientConfig["m.homeserver"] = {
state: AutoDiscovery.PROMPT,
error: null,
@@ -427,12 +349,12 @@ export class AutoDiscovery {
* @returns {Promise<object>} Resolves to the domain's client config. Can
* be an empty object.
*/
static async getRawClientConfig(domain) {
public static async getRawClientConfig(domain: string): Promise<IClientWellKnown> {
if (!domain || typeof(domain) !== "string" || domain.length === 0) {
throw new Error("'domain' must be a string of non-zero length");
}
const response = await this._fetchWellKnownObject(
const response = await this.fetchWellKnownObject(
`https://${domain}/.well-known/matrix/client`,
);
if (!response) return {};
@@ -447,7 +369,7 @@ export class AutoDiscovery {
* @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid.
* @private
*/
static _sanitizeWellKnownUrl(url) {
private static sanitizeWellKnownUrl(url: string): string | boolean {
if (!url) return false;
try {
@@ -495,8 +417,9 @@ export class AutoDiscovery {
* @return {Promise<object>} Resolves to the returned state.
* @private
*/
static async _fetchWellKnownObject(url) {
private static async fetchWellKnownObject(url: string): Promise<IWellKnownConfig> {
return new Promise(function(resolve, reject) {
// eslint-disable-next-line
const request = require("./matrix").getRequest();
if (!request) throw new Error("No request library available");
request(
@@ -505,10 +428,10 @@ export class AutoDiscovery {
if (err || response &&
(response.statusCode < 200 || response.statusCode >= 300)
) {
let action = "FAIL_PROMPT";
let action = AutoDiscoveryAction.FAIL_PROMPT;
let reason = (err ? err.message : null) || "General failure";
if (response && response.statusCode === 404) {
action = "IGNORE";
action = AutoDiscoveryAction.IGNORE;
reason = AutoDiscovery.ERROR_MISSING_WELLKNOWN;
}
resolve({ raw: {}, action: action, reason: reason, error: err });
@@ -516,7 +439,7 @@ export class AutoDiscovery {
}
try {
resolve({ raw: JSON.parse(body), action: "SUCCESS" });
resolve({ raw: JSON.parse(body), action: AutoDiscoveryAction.SUCCESS });
} catch (e) {
let reason = AutoDiscovery.ERROR_INVALID;
if (e.name === "SyntaxError") {
@@ -524,7 +447,7 @@ export class AutoDiscovery {
}
resolve({
raw: {},
action: "FAIL_PROMPT",
action: AutoDiscoveryAction.FAIL_PROMPT,
reason: reason,
error: e,
});
+6 -1
View File
@@ -14,10 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as matrixcs from "./matrix";
import request from "browser-request";
import queryString from "qs";
import * as matrixcs from "./matrix";
if (matrixcs.getRequest()) {
throw new Error("Multiple matrix-js-sdk entrypoints detected!");
}
matrixcs.request(function(opts, fn) {
// We manually fix the query string for browser-request because
// it doesn't correctly handle cases like ?via=one&via=two. Instead
+1376 -448
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,
ILocationContent,
LocationAssetType,
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?: LocationAssetType,
): ILocationContent {
return {
"body": text,
"msgtype": MsgType.Location,
"geo_uri": uri,
[LOCATION_EVENT_TYPE.name]: {
uri,
description,
},
[ASSET_NODE_TYPE.name]: {
type: assetType ?? LocationAssetType.Self,
},
[TEXT_NODE_TYPE.name]: text,
[TIMESTAMP_NODE_TYPE.name]: ts,
// TODO: MSC1767 fallbacks m.image thumbnail
};
}
+3 -3
View File
@@ -53,13 +53,13 @@ export function getHttpUriForMxc(
}
let serverAndMediaId = mxc.slice(6); // strips mxc://
let prefix = "/_matrix/media/r0/download/";
const params = {};
const params: Record<string, string> = {};
if (width) {
params["width"] = Math.round(width);
params["width"] = Math.round(width).toString();
}
if (height) {
params["height"] = Math.round(height);
params["height"] = Math.round(height).toString();
}
if (resizeMethod) {
params["method"] = resizeMethod;
+21 -17
View File
@@ -19,13 +19,12 @@ limitations under the License.
* @module crypto/CrossSigning
*/
import { EventEmitter } from 'events';
import { PkSigning } from "@matrix-org/olm";
import { decodeBase64, encodeBase64, pkSign, pkVerify } from './olmlib';
import { logger } from '../logger';
import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store';
import { decryptAES, encryptAES } from './aes';
import { PkSigning } from "@matrix-org/olm";
import { DeviceInfo } from "./deviceinfo";
import { SecretStorage } from "./SecretStorage";
import { ICrossSigningKey, ISignedKey, MatrixClient } from "../client";
@@ -33,6 +32,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;
@@ -54,7 +54,7 @@ export interface ICrossSigningInfo {
crossSigningVerifiedBefore: boolean;
}
export class CrossSigningInfo extends EventEmitter {
export class CrossSigningInfo {
public keys: Record<string, ICrossSigningKey> = {};
public firstUse = true;
// This tracks whether we've ever verified this user with any identity.
@@ -78,9 +78,7 @@ export class CrossSigningInfo extends EventEmitter {
public readonly userId: string,
private callbacks: ICryptoCallbacks = {},
private cacheCallbacks: ICacheCallbacks = {},
) {
super();
}
) {}
public static fromStorage(obj: ICrossSigningInfo, userId: string): CrossSigningInfo {
const res = new CrossSigningInfo(userId);
@@ -175,7 +173,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];
@@ -304,7 +302,7 @@ export class CrossSigningInfo extends EventEmitter {
}
const privateKeys: Record<string, Uint8Array> = {};
const keys: Record<string, any> = {}; // TODO types
const keys: Record<string, ICrossSigningKey> = {};
let masterSigning;
let masterPub;
@@ -713,7 +711,7 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
});
if (key && key.ciphertext) {
const pickleKey = Buffer.from(olmDevice._pickleKey);
const pickleKey = Buffer.from(olmDevice.pickleKey);
const decrypted = await decryptAES(key, pickleKey, type);
return decodeBase64(decrypted);
} else {
@@ -726,7 +724,7 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
`storeCrossSigningKeyCache expects Uint8Array, got ${key}`,
);
}
const pickleKey = Buffer.from(olmDevice._pickleKey);
const pickleKey = Buffer.from(olmDevice.pickleKey);
const encryptedKey = await encryptAES(encodeBase64(key), pickleKey, type);
return store.doTxn(
'readwrite',
@@ -739,6 +737,8 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
};
}
export type KeysDuringVerification = [[string, PkSigning], [string, PkSigning], [string, PkSigning], void];
/**
* Request cross-signing keys from another device during verification.
*
@@ -746,15 +746,19 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
* @param {string} userId The user ID being verified
* @param {string} deviceId The device ID being verified
*/
export async function requestKeysDuringVerification(baseApis: MatrixClient, userId: string, deviceId: string) {
export function requestKeysDuringVerification(
baseApis: MatrixClient,
userId: string,
deviceId: string,
): Promise<KeysDuringVerification | void> {
// If this is a self-verification, ask the other party for keys
if (baseApis.getUserId() !== userId) {
return;
}
logger.log("Cross-signing: Self-verification done; requesting keys");
// This happens asynchronously, and we're not concerned about waiting for
// it. We return here in order to test.
return new Promise((resolve, reject) => {
// it. We return here in order to test.
return new Promise<KeysDuringVerification | void>((resolve, reject) => {
const client = baseApis;
const original = client.crypto.crossSigningInfo;
@@ -781,7 +785,7 @@ export async function requestKeysDuringVerification(baseApis: MatrixClient, user
// https://github.com/vector-im/element-web/issues/12604
// then change here to reject on the timeout
// Requests can be ignored, so don't wait around forever
const timeout = new Promise((resolve, reject) => {
const timeout = new Promise<void>((resolve) => {
setTimeout(
resolve,
KEY_REQUEST_TIMEOUT_MS,
@@ -801,7 +805,7 @@ export async function requestKeysDuringVerification(baseApis: MatrixClient, user
logger.info("Got key backup key, decoding...");
const decodedKey = decodeBase64(base64Key);
logger.info("Decoded backup key, storing...");
client.crypto.storeSessionBackupPrivateKey(
await client.crypto.storeSessionBackupPrivateKey(
Uint8Array.from(decodedKey),
);
logger.info("Backup key stored. Starting backup restore...");
@@ -814,13 +818,13 @@ export async function requestKeysDuringVerification(baseApis: MatrixClient, user
})();
// We call getCrossSigningKey() for its side-effects
return Promise.race([
return Promise.race<KeysDuringVerification | void>([
Promise.all([
crossSigning.getCrossSigningKey("master"),
crossSigning.getCrossSigningKey("self_signing"),
crossSigning.getCrossSigningKey("user_signing"),
backupKeyPromise,
]),
]) as Promise<KeysDuringVerification>,
timeout,
]).then(resolve, reject);
}).catch((e) => {
+29 -19
View File
@@ -20,17 +20,17 @@ limitations under the License.
* Manages the list of other users' devices
*/
import { EventEmitter } from 'events';
import { logger } from '../logger';
import { DeviceInfo, IDevice } from './deviceinfo';
import { CrossSigningInfo, ICrossSigningInfo } from './CrossSigning';
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";
import { TypedEventEmitter } from "../models/typed-event-emitter";
import { CryptoEvent, CryptoEventHandlerMap } from "./index";
/* State transition diagram for DeviceList.deviceTrackingStatus
*
@@ -62,10 +62,12 @@ export enum TrackingStatus {
export type DeviceInfoMap = Record<string, Record<string, DeviceInfo>>;
type EmittedEvents = CryptoEvent.WillUpdateDevices | CryptoEvent.DevicesUpdated | CryptoEvent.UserCrossSigningUpdated;
/**
* @alias module:crypto/DeviceList
*/
export class DeviceList extends EventEmitter {
export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHandlerMap> {
private devices: { [userId: string]: { [deviceId: string]: IDevice } } = {};
public crossSigningInfo: { [userId: string]: ICrossSigningInfo } = {};
@@ -265,8 +267,8 @@ export class DeviceList extends EventEmitter {
* module:crypto/deviceinfo|DeviceInfo}.
*/
public downloadKeys(userIds: string[], forceDownload: boolean): Promise<DeviceInfoMap> {
const usersToDownload = [];
const promises = [];
const usersToDownload: string[] = [];
const promises: Promise<unknown>[] = [];
userIds.forEach((u) => {
const trackingStatus = this.deviceTrackingStatus[u];
@@ -633,8 +635,8 @@ export class DeviceList extends EventEmitter {
}
});
const finished = (success) => {
this.emit("crypto.willUpdateDevices", users, !this.hasFetched);
const finished = (success: boolean): void => {
this.emit(CryptoEvent.WillUpdateDevices, users, !this.hasFetched);
users.forEach((u) => {
this.dirty = true;
@@ -659,7 +661,7 @@ export class DeviceList extends EventEmitter {
}
});
this.saveIfDirty();
this.emit("crypto.devicesUpdated", users, !this.hasFetched);
this.emit(CryptoEvent.DevicesUpdated, users, !this.hasFetched);
this.hasFetched = true;
};
@@ -756,17 +758,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 +817,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);
@@ -859,7 +869,7 @@ class DeviceListUpdateSerialiser {
// NB. Unlike most events in the js-sdk, this one is internal to the
// js-sdk and is not re-emitted
this.deviceList.emit('userCrossSigningUpdated', userId);
this.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, userId);
}
}
}
@@ -869,7 +879,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> {
+17 -10
View File
@@ -16,13 +16,14 @@ limitations under the License.
import { logger } from "../logger";
import { MatrixEvent } from "../models/event";
import { EventEmitter } from "events";
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 {
ClientEvent,
CrossSigningKeys,
ClientEventHandlerMap,
ICrossSigningKey,
ICryptoCallbacks,
ISignedKey,
@@ -30,6 +31,8 @@ import {
} from "../matrix";
import { ISecretStorageKeyInfo } from "./api";
import { IKeyBackupInfo } from "./keybackup";
import { TypedEventEmitter } from "../models/typed-event-emitter";
import { IAccountDataClient } from "./SecretStorage";
interface ICrossSigningKeys {
authUpload: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"];
@@ -59,7 +62,7 @@ export class EncryptionSetupBuilder {
* @param {Object.<String, MatrixEvent>} accountData pre-existing account data, will only be read, not written.
* @param {CryptoCallbacks} delegateCryptoCallbacks crypto callbacks to delegate to if the key isn't in cache yet
*/
constructor(accountData: Record<string, MatrixEvent>, delegateCryptoCallbacks: ICryptoCallbacks) {
constructor(accountData: Record<string, MatrixEvent>, delegateCryptoCallbacks?: ICryptoCallbacks) {
this.accountDataClientAdapter = new AccountDataClientAdapter(accountData);
this.crossSigningCallbacks = new CrossSigningCallbacks();
this.ssssCryptoCallbacks = new SSSSCryptoCallbacks(delegateCryptoCallbacks);
@@ -238,7 +241,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,
@@ -248,7 +251,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 },
);
@@ -261,7 +264,10 @@ export class EncryptionSetupOperation {
* Catches account data set by SecretStorage during bootstrapping by
* implementing the methods related to account data in MatrixClient
*/
class AccountDataClientAdapter extends EventEmitter {
class AccountDataClientAdapter
extends TypedEventEmitter<ClientEvent.AccountData, ClientEventHandlerMap>
implements IAccountDataClient {
//
public readonly values = new Map<string, MatrixEvent>();
/**
@@ -308,7 +314,7 @@ class AccountDataClientAdapter extends EventEmitter {
// and it seems to rely on this.
return Promise.resolve().then(() => {
const event = new MatrixEvent({ type, content });
this.emit("accountData", event, lastEvent);
this.emit(ClientEvent.AccountData, event, lastEvent);
return {};
});
}
@@ -351,12 +357,12 @@ class CrossSigningCallbacks implements ICryptoCallbacks, ICacheCallbacks {
class SSSSCryptoCallbacks {
private readonly privateKeys = new Map<string, Uint8Array>();
constructor(private readonly delegateCryptoCallbacks: ICryptoCallbacks) {}
constructor(private readonly delegateCryptoCallbacks?: ICryptoCallbacks) {}
public async getSecretStorageKey(
{ keys }: { keys: Record<string, ISecretStorageKeyInfo> },
name: string,
): Promise<[string, Uint8Array]> {
): Promise<[string, Uint8Array]|null> {
for (const keyId of Object.keys(keys)) {
const privateKey = this.privateKeys.get(keyId);
if (privateKey) {
@@ -365,7 +371,7 @@ class SSSSCryptoCallbacks {
}
// if we don't have the key cached yet, ask
// for it to the general crypto callbacks and cache it
if (this.delegateCryptoCallbacks) {
if (this?.delegateCryptoCallbacks?.getSecretStorageKey) {
const result = await this.delegateCryptoCallbacks.
getSecretStorageKey({ keys }, name);
if (result) {
@@ -374,6 +380,7 @@ class SSSSCryptoCallbacks {
}
return result;
}
return null;
}
public addPrivateKey(keyId: string, keyInfo: ISecretStorageKeyInfo, privKey: Uint8Array): void {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+37 -27
View File
@@ -16,12 +16,13 @@ limitations under the License.
import { logger } from '../logger';
import * as olmlib from './olmlib';
import { encodeBase64 } from './olmlib';
import { randomString } from '../randomstring';
import { encryptAES, decryptAES, IEncryptedPayload, calculateKeyCheck } from './aes';
import { encodeBase64 } from "./olmlib";
import { ICryptoCallbacks, MatrixClient, MatrixEvent } from '../matrix';
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from './aes';
import { ClientEvent, ICryptoCallbacks, MatrixEvent } from '../matrix';
import { ClientEventHandlerMap, MatrixClient } from "../client";
import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from './api';
import { EventEmitter } from 'stream';
import { TypedEventEmitter } from '../models/typed-event-emitter';
export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2";
@@ -35,9 +36,9 @@ export interface ISecretRequest {
cancel: (reason: string) => void;
}
export interface IAccountDataClient extends EventEmitter {
export interface IAccountDataClient extends TypedEventEmitter<ClientEvent.AccountData, ClientEventHandlerMap> {
// Subset of MatrixClient (which also uses any for the event content)
getAccountDataFromServer: (eventType: string) => Promise<Record<string, any>>;
getAccountDataFromServer: <T extends {[k: string]: any}>(eventType: string) => Promise<T>;
getAccountData: (eventType: string) => MatrixEvent;
setAccountData: (eventType: string, content: any) => Promise<{}>;
}
@@ -54,6 +55,13 @@ interface IDecryptors {
decrypt: (ciphertext: IEncryptedPayload) => Promise<string>;
}
interface ISecretInfo {
encrypted: {
// eslint-disable-next-line camelcase
key_id: IEncryptedPayload;
};
}
/**
* Implements Secure Secret Storage and Sharing (MSC1946)
* @module crypto/SecretStorage
@@ -75,8 +83,8 @@ export class SecretStorage {
private readonly baseApis?: MatrixClient,
) {}
public async getDefaultKeyId(): Promise<string> {
const defaultKey = await this.accountDataAdapter.getAccountDataFromServer(
public async getDefaultKeyId(): Promise<string | null> {
const defaultKey = await this.accountDataAdapter.getAccountDataFromServer<{ key: string }>(
'm.secret_storage.default_key',
);
if (!defaultKey) return null;
@@ -90,17 +98,17 @@ export class SecretStorage {
ev.getType() === 'm.secret_storage.default_key' &&
ev.getContent().key === keyId
) {
this.accountDataAdapter.removeListener('accountData', listener);
this.accountDataAdapter.removeListener(ClientEvent.AccountData, listener);
resolve();
}
};
this.accountDataAdapter.on('accountData', listener);
this.accountDataAdapter.on(ClientEvent.AccountData, listener);
this.accountDataAdapter.setAccountData(
'm.secret_storage.default_key',
{ key: keyId },
).catch(e => {
this.accountDataAdapter.removeListener('accountData', listener);
this.accountDataAdapter.removeListener(ClientEvent.AccountData, listener);
reject(e);
});
});
@@ -149,7 +157,7 @@ export class SecretStorage {
do {
keyId = randomString(32);
} while (
await this.accountDataAdapter.getAccountDataFromServer(
await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
`m.secret_storage.key.${keyId}`,
)
);
@@ -182,9 +190,9 @@ export class SecretStorage {
return null;
}
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
"m.secret_storage.key." + keyId,
) as ISecretStorageKeyInfo;
);
return keyInfo ? [keyId, keyInfo] : null;
}
@@ -230,7 +238,7 @@ export class SecretStorage {
* or null/undefined to use the default key.
*/
public async store(name: string, secret: string, keys?: string[]): Promise<void> {
const encrypted = {};
const encrypted: Record<string, IEncryptedPayload> = {};
if (!keys) {
const defaultKeyId = await this.getDefaultKeyId();
@@ -246,9 +254,9 @@ export class SecretStorage {
for (const keyId of keys) {
// get key information from key storage
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
"m.secret_storage.key." + keyId,
) as ISecretStorageKeyInfo;
);
if (!keyInfo) {
throw new Error("Unknown key: " + keyId);
}
@@ -277,7 +285,7 @@ export class SecretStorage {
* @return {string} the contents of the secret
*/
public async get(name: string): Promise<string> {
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name);
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name);
if (!secretInfo) {
return;
}
@@ -286,11 +294,13 @@ export class SecretStorage {
}
// get possible keys to decrypt
const keys = {};
const keys: Record<string, ISecretStorageKeyInfo> = {};
for (const keyId of Object.keys(secretInfo.encrypted)) {
// get key information from key storage
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
"m.secret_storage.key." + keyId,
const keyInfo = (
await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
"m.secret_storage.key." + keyId,
)
);
const encInfo = secretInfo.encrypted[keyId];
// only use keys we understand the encryption algorithm of
@@ -306,7 +316,7 @@ export class SecretStorage {
`the keys it is encrypted with are for a supported algorithm`);
}
let keyId;
let keyId: string;
let decryption;
try {
// fetch private key from app
@@ -335,9 +345,9 @@ 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(name);
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name);
if (!secretInfo) return null;
if (!secretInfo.encrypted) {
return null;
@@ -350,7 +360,7 @@ export class SecretStorage {
// filter secret encryption keys with supported algorithm
for (const keyId of Object.keys(secretInfo.encrypted)) {
// get key information from key storage
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
"m.secret_storage.key." + keyId,
);
if (!keyInfo) continue;
@@ -375,8 +385,8 @@ export class SecretStorage {
public request(name: string, devices: string[]): ISecretRequest {
const requestId = this.baseApis.makeTxnId();
let resolve: (string) => void;
let reject: (Error) => void;
let resolve: (s: string) => void;
let reject: (e: Error) => void;
const promise = new Promise<string>((res, rej) => {
resolve = res;
reject = rej;
+4 -4
View File
@@ -15,7 +15,6 @@ limitations under the License.
*/
import type { BinaryLike } from "crypto";
import { getCrypto } from '../utils';
import { decodeBase64, encodeBase64 } from './olmlib';
@@ -26,9 +25,10 @@ const subtleCrypto = (typeof window !== "undefined" && window.crypto) ?
const zeroSalt = new Uint8Array(8);
export interface IEncryptedPayload {
iv: string;
ciphertext: string;
mac: string;
[key: string]: any; // extensible
iv?: string;
ciphertext?: string;
mac?: string;
}
/**
+1 -1
View File
@@ -46,7 +46,7 @@ type DecryptionClassParams = Omit<IParams, "deviceId" | "config">;
*/
export const DECRYPTION_CLASSES: Record<string, new (params: DecryptionClassParams) => DecryptionAlgorithm> = {};
interface IParams {
export interface IParams {
userId: string;
deviceId: string;
crypto: Crypto;
+79 -46
View File
@@ -26,6 +26,7 @@ import {
DecryptionAlgorithm,
DecryptionError,
EncryptionAlgorithm,
IParams,
registerAlgorithm,
UnknownDeviceError,
} from "./base";
@@ -67,7 +68,7 @@ export interface IOlmDevice<T = DeviceInfo> {
}
/* eslint-disable camelcase */
interface IOutboundGroupSessionKey {
export interface IOutboundGroupSessionKey {
chain_index: number;
key: string;
}
@@ -99,8 +100,21 @@ interface IPayload extends Partial<IMessage> {
algorithm?: string;
sender_key?: string;
}
interface IEncryptedContent {
algorithm: string;
sender_key: string;
ciphertext: Record<string, string>;
}
/* eslint-enable camelcase */
interface SharedWithData {
// The identity key of the device we shared with
deviceKey: string;
// The message index of the ratchet we shared with that device
messageIndex: number;
}
/**
* @private
* @constructor
@@ -115,12 +129,12 @@ interface IPayload extends Partial<IMessage> {
*
* @property {object} sharedWithDevices
* devices with which we have shared the session key
* userId -> {deviceId -> msgindex}
* userId -> {deviceId -> SharedWithData}
*/
class OutboundSessionInfo {
public useCount = 0;
public creationTime: number;
public sharedWithDevices: Record<string, Record<string, number>> = {};
public sharedWithDevices: Record<string, Record<string, SharedWithData>> = {};
public blockedDevicesNotified: Record<string, Record<string, boolean>> = {};
constructor(public readonly sessionId: string, public readonly sharedHistory = false) {
@@ -150,11 +164,11 @@ class OutboundSessionInfo {
return false;
}
public markSharedWithDevice(userId: string, deviceId: string, chainIndex: number): void {
public markSharedWithDevice(userId: string, deviceId: string, deviceKey: string, chainIndex: number): void {
if (!this.sharedWithDevices[userId]) {
this.sharedWithDevices[userId] = {};
}
this.sharedWithDevices[userId][deviceId] = chainIndex;
this.sharedWithDevices[userId][deviceId] = { deviceKey, messageIndex: chainIndex };
}
public markNotifiedBlockedDevice(userId: string, deviceId: string): void {
@@ -231,7 +245,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
startTime: number;
};
constructor(params) {
constructor(params: IParams) {
super(params);
this.sessionRotationPeriodMsgs = params.config?.rotation_period_msgs ?? 100;
@@ -256,7 +270,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
blocked: IBlockedMap,
singleOlmCreationPhase = false,
): Promise<OutboundSessionInfo> {
let session;
let session: OutboundSessionInfo;
// takes the previous OutboundSessionInfo, and considers whether to create
// a new one. Also shares the key with any (new) devices in the room.
@@ -295,7 +309,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
}
// now check if we need to share with any devices
const shareMap = {};
const shareMap: Record<string, DeviceInfo[]> = {};
for (const [userId, userDevices] of Object.entries(devicesInRoom)) {
for (const [deviceId, deviceInfo] of Object.entries(userDevices)) {
@@ -334,13 +348,16 @@ class MegolmEncryption extends EncryptionAlgorithm {
await Promise.all([
(async () => {
// share keys with devices that we already have a session for
logger.debug(`Sharing keys with existing Olm sessions in ${this.roomId}`);
logger.debug(`Sharing keys with existing Olm sessions in ${this.roomId}`, olmSessions);
await this.shareKeyWithOlmSessions(session, key, payload, olmSessions);
logger.debug(`Shared keys with existing Olm sessions in ${this.roomId}`);
})(),
(async () => {
logger.debug(`Sharing keys (start phase 1) with new Olm sessions in ${this.roomId}`);
const errorDevices = [];
logger.debug(
`Sharing keys (start phase 1) with new Olm sessions in ${this.roomId}`,
devicesWithoutSession,
);
const errorDevices: IOlmDevice[] = [];
// meanwhile, establish olm sessions for devices that we don't
// already have a session for, and share keys with them. If
@@ -348,7 +365,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
// shorter timeout when fetching one-time keys for the first
// phase.
const start = Date.now();
const failedServers = [];
const failedServers: string[] = [];
await this.shareKeyWithDevices(
session, key, payload, devicesWithoutSession, errorDevices,
singleOlmCreationPhase ? 10000 : 2000, failedServers,
@@ -364,7 +381,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
// do this in the background and don't block anything else while we
// do this. We only need to retry users from servers that didn't
// respond the first time.
const retryDevices = {};
const retryDevices: Record<string, DeviceInfo[]> = {};
const failedServerMap = new Set;
for (const server of failedServers) {
failedServerMap.add(server);
@@ -396,8 +413,11 @@ class MegolmEncryption extends EncryptionAlgorithm {
logger.debug(`Shared keys (all phases done) with new Olm sessions in ${this.roomId}`);
})(),
(async () => {
logger.debug(`Notifying blocked devices in ${this.roomId}`);
// also, notify blocked devices that they're blocked
logger.debug(`There are ${Object.entries(blocked).length} blocked devices in ${this.roomId}`,
Object.entries(blocked));
// also, notify newly blocked devices that they're blocked
logger.debug(`Notifying newly blocked devices in ${this.roomId}`);
const blockedMap: Record<string, Record<string, { device: IBlockedDevice }>> = {};
let blockedCount = 0;
for (const [userId, userBlockedDevices] of Object.entries(blocked)) {
@@ -414,7 +434,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
}
await this.notifyBlockedDevices(session, blockedMap);
logger.debug(`Notified ${blockedCount} blocked devices in ${this.roomId}`);
logger.debug(`Notified ${blockedCount} newly blocked devices in ${this.roomId}`, blockedMap);
})(),
]);
};
@@ -571,11 +591,12 @@ class MegolmEncryption extends EncryptionAlgorithm {
userDeviceMap: IOlmDevice[],
payload: IPayload,
): Promise<void> {
const contentMap = {};
const contentMap: Record<string, Record<string, IEncryptedContent>> = {};
const deviceInfoByDeviceId = new Map<string, DeviceInfo>();
const promises = [];
const promises: Promise<unknown>[] = [];
for (let i = 0; i < userDeviceMap.length; i++) {
const encryptedContent = {
const encryptedContent: IEncryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key,
ciphertext: {},
@@ -584,6 +605,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
const userId = val.userId;
const deviceInfo = val.deviceInfo;
const deviceId = deviceInfo.deviceId;
deviceInfoByDeviceId.set(deviceId, deviceInfo);
if (!contentMap[userId]) {
contentMap[userId] = {};
@@ -636,7 +658,10 @@ class MegolmEncryption extends EncryptionAlgorithm {
for (const userId of Object.keys(contentMap)) {
for (const deviceId of Object.keys(contentMap[userId])) {
session.markSharedWithDevice(
userId, deviceId, chainIndex,
userId,
deviceId,
deviceInfoByDeviceId.get(deviceId).getIdentityKey(),
chainIndex,
);
}
}
@@ -661,7 +686,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
userDeviceMap: IOlmDevice<IBlockedDevice>[],
payload: IPayload,
): Promise<void> {
const contentMap = {};
const contentMap: Record<string, Record<string, IPayload>> = {};
for (const val of userDeviceMap) {
const userId = val.userId;
@@ -685,7 +710,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
await this.baseApis.sendToDevice("org.matrix.room_key.withheld", contentMap);
// store that we successfully uploaded the keys of the current slice
// record the fact that we notified these blocked devices
for (const userId of Object.keys(contentMap)) {
for (const deviceId of Object.keys(contentMap[userId])) {
session.markNotifiedBlockedDevice(userId, deviceId);
@@ -719,8 +744,8 @@ class MegolmEncryption extends EncryptionAlgorithm {
logger.debug(`megolm session ${sessionId} never shared with user ${userId}`);
return;
}
const sentChainIndex = obSessionInfo.sharedWithDevices[userId][device.deviceId];
if (sentChainIndex === undefined) {
const sessionSharedData = obSessionInfo.sharedWithDevices[userId][device.deviceId];
if (sessionSharedData === undefined) {
logger.debug(
"megolm session ID " + sessionId + " never shared with device " +
userId + ":" + device.deviceId,
@@ -728,10 +753,18 @@ class MegolmEncryption extends EncryptionAlgorithm {
return;
}
if (sessionSharedData.deviceKey !== device.getIdentityKey()) {
logger.warn(
`Session has been shared with device ${device.deviceId} but with identity ` +
`key ${sessionSharedData.deviceKey}. Key is now ${device.getIdentityKey()}!`,
);
return;
}
// get the key from the inbound session: the outbound one will already
// have been ratcheted to the next chain index.
const key = await this.olmDevice.getInboundGroupSessionKey(
this.roomId, senderKey, sessionId, sentChainIndex,
this.roomId, senderKey, sessionId, sessionSharedData.messageIndex,
);
if (!key) {
@@ -826,9 +859,9 @@ class MegolmEncryption extends EncryptionAlgorithm {
this.getDevicesWithoutSessions(devicemap, devicesByUser, errorDevices);
logger.debug(`Sharing keys with Olm sessions in ${this.roomId}`);
logger.debug(`Sharing keys with newly created Olm sessions in ${this.roomId}`);
await this.shareKeyWithOlmSessions(session, key, payload, devicemap);
logger.debug(`Shared keys with Olm sessions in ${this.roomId}`);
logger.debug(`Shared keys with newly created Olm sessions in ${this.roomId}`);
}
private async shareKeyWithOlmSessions(
@@ -844,7 +877,8 @@ class MegolmEncryption extends EncryptionAlgorithm {
`megolm keys for ${session.sessionId} ` +
`in ${this.roomId} (slice ${i + 1}/${userDeviceMaps.length})`;
try {
logger.debug(`Sharing ${taskDetail}`);
logger.debug(`Sharing ${taskDetail}`,
userDeviceMaps[i].map((d) => `${d.userId}/${d.deviceInfo.deviceId}`));
await this.encryptAndSendKeysToDevices(
session, key.chain_index, userDeviceMaps[i], payload,
);
@@ -882,20 +916,20 @@ class MegolmEncryption extends EncryptionAlgorithm {
const deviceId = deviceInfo.deviceId;
session.markSharedWithDevice(
userId, deviceId, key.chain_index,
userId, deviceId, deviceInfo.getIdentityKey(), key.chain_index,
);
}
const filteredFailedDevices =
const unnotifiedFailedDevices =
await this.olmDevice.filterOutNotifiedErrorDevices(
failedDevices,
);
logger.debug(
`Filtered down to ${filteredFailedDevices.length} error devices ` +
`in ${this.roomId}`,
`Need to notify ${unnotifiedFailedDevices.length} failed devices ` +
`which haven't been notified before in ${this.roomId}`,
);
const blockedMap: Record<string, Record<string, { device: IBlockedDevice }>> = {};
for (const { userId, deviceInfo } of filteredFailedDevices) {
for (const { userId, deviceInfo } of unnotifiedFailedDevices) {
blockedMap[userId] = blockedMap[userId] || {};
// we use a similar format to what
// olmlib.ensureOlmSessionsForDevices returns, so that
@@ -912,7 +946,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
// send the notifications
await this.notifyBlockedDevices(session, blockedMap);
logger.debug(
`Notified ${filteredFailedDevices.length} devices we failed to ` +
`Notified ${unnotifiedFailedDevices.length} devices we failed to ` +
`create Olm sessions in ${this.roomId}`,
);
}
@@ -1078,10 +1112,10 @@ class MegolmEncryption extends EncryptionAlgorithm {
* devices we should shared the session with.
*/
private checkForUnknownDevices(devicesInRoom: DeviceInfoMap): void {
const unknownDevices = {};
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]) {
@@ -1277,8 +1311,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
content.sender_key, event.getTs() - 120000,
);
if (problem) {
let problemDescription = PROBLEM_DESCRIPTIONS[problem.type]
|| PROBLEM_DESCRIPTIONS.unknown;
let problemDescription = PROBLEM_DESCRIPTIONS[problem.type as "no_olm"] || PROBLEM_DESCRIPTIONS.unknown;
if (problem.fixed) {
problemDescription +=
" Trying to create a new secure channel and re-requesting the keys.";
@@ -1372,14 +1405,14 @@ class MegolmDecryption extends DecryptionAlgorithm {
const senderKey = content.sender_key;
const sessionId = content.session_id;
const senderPendingEvents = this.pendingEvents[senderKey];
const pendingEvents = senderPendingEvents && senderPendingEvents.get(sessionId);
const pendingEvents = senderPendingEvents?.get(sessionId);
if (!pendingEvents) {
return;
}
pendingEvents.delete(event);
if (pendingEvents.size === 0) {
senderPendingEvents.delete(senderKey);
senderPendingEvents.delete(sessionId);
}
if (senderPendingEvents.size === 0) {
delete this.pendingEvents[senderKey];
@@ -1391,7 +1424,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
*
* @param {module:models/event.MatrixEvent} event key event
*/
public onRoomKeyEvent(event: MatrixEvent): void {
public onRoomKeyEvent(event: MatrixEvent): Promise<void> {
const content = event.getContent();
const sessionId = content.session_id;
let senderKey = event.getSenderKey();
@@ -1733,7 +1766,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
}));
// If decrypted successfully, they'll have been removed from pendingEvents
return !((this.pendingEvents[senderKey] || {})[sessionId]);
return !this.pendingEvents[senderKey]?.has(sessionId);
}
public async retryDecryptionFromSender(senderKey: string): Promise<boolean> {
@@ -1767,12 +1800,12 @@ class MegolmDecryption extends DecryptionAlgorithm {
for (const [senderKey, sessionId] of sharedHistorySessions) {
const payload = await this.buildKeyForwardingMessage(this.roomId, senderKey, sessionId);
const promises = [];
const contentMap = {};
const promises: Promise<unknown>[] = [];
const contentMap: Record<string, Record<string, IEncryptedContent>> = {};
for (const [userId, devices] of Object.entries(devicesByUser)) {
contentMap[userId] = {};
for (const deviceInfo of devices) {
const encryptedContent = {
const encryptedContent: IEncryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key,
ciphertext: {},
+4 -4
View File
@@ -36,7 +36,7 @@ import { IEventDecryptionResult } from "../index";
const DeviceVerification = DeviceInfo.DeviceVerification;
interface IMessage {
type: number | string;
type: number;
body: string;
}
@@ -269,11 +269,11 @@ class OlmDecryption extends DecryptionAlgorithm {
// not a prekey message: we can safely just try & decrypt it
return this.reallyDecryptMessage(theirDeviceIdentityKey, message);
} else {
const myPromise = this.olmDevice._olmPrekeyPromise.then(() => {
const myPromise = this.olmDevice.olmPrekeyPromise.then(() => {
return this.reallyDecryptMessage(theirDeviceIdentityKey, message);
});
// we want the error, but don't propagate it to the next decryption
this.olmDevice._olmPrekeyPromise = myPromise.catch(() => {});
this.olmDevice.olmPrekeyPromise = myPromise.catch(() => {});
return await myPromise;
}
}
@@ -282,7 +282,7 @@ class OlmDecryption extends DecryptionAlgorithm {
const sessionIds = await this.olmDevice.getSessionIdsForDevice(theirDeviceIdentityKey);
// try each session in turn.
const decryptionErrors = {};
const decryptionErrors: Record<string, string> = {};
for (let i = 0; i < sessionIds.length; i++) {
const sessionId = sessionIds[i];
try {
+7 -13
View File
@@ -58,16 +58,9 @@ export interface IEncryptedEventInfo {
}
export interface IRecoveryKey {
keyInfo: {
pubkey: string;
passphrase?: {
algorithm: string;
iterations: number;
salt: string;
};
};
keyInfo?: IAddSecretStorageKeyOpts;
privateKey: Uint8Array;
encodedPrivateKey: string;
encodedPrivateKey?: string;
}
export interface ICreateSecretStorageOpts {
@@ -125,12 +118,13 @@ export interface IPassphraseInfo {
algorithm: "m.pbkdf2";
iterations: number;
salt: string;
bits: number;
bits?: number;
}
export interface IAddSecretStorageKeyOpts {
name: string;
passphrase: IPassphraseInfo;
pubkey: string;
passphrase?: IPassphraseInfo;
name?: string;
key: Uint8Array;
}
@@ -142,7 +136,7 @@ export interface IImportOpts {
}
export interface IImportRoomKeysOpts {
progressCallback: (stage: IImportOpts) => void;
progressCallback?: (stage: IImportOpts) => void;
untrusted?: boolean;
source?: string; // TODO: Enum
}
+37 -25
View File
@@ -26,13 +26,13 @@ import { MEGOLM_ALGORITHM, verifySignature } from "./olmlib";
import { DeviceInfo } from "./deviceinfo";
import { DeviceTrustLevel } from './CrossSigning';
import { keyFromPassphrase } from './key_passphrase';
import { sleep } from "../utils";
import { getCrypto, sleep } from "../utils";
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { encodeRecoveryKey } from './recoverykey';
import { encryptAES, decryptAES, calculateKeyCheck } from './aes';
import { getCrypto } from '../utils';
import { ICurve25519AuthData, IAes256AuthData, IKeyBackupInfo, IKeyBackupSession } from "./keybackup";
import { calculateKeyCheck, decryptAES, encryptAES } from './aes';
import { IAes256AuthData, ICurve25519AuthData, IKeyBackupInfo, IKeyBackupSession } from "./keybackup";
import { UnstableValue } from "../NamespacedValue";
import { CryptoEvent, IMegolmSessionData } from "./index";
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
@@ -49,6 +49,8 @@ type SigInfo = {
export type TrustInfo = {
usable: boolean; // is the backup trusted, true iff there is a sig that is valid & from a trusted device
sigs: SigInfo[];
// eslint-disable-next-line camelcase
trusted_locally?: boolean;
};
export interface IKeyBackupCheck {
@@ -85,7 +87,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;
@@ -152,7 +154,7 @@ export class BackupManager {
this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey);
this.baseApis.emit('crypto.keyBackupStatus', true);
this.baseApis.emit(CryptoEvent.KeyBackupStatus, true);
// There may be keys left over from a partially completed backup, so
// schedule a send to check.
@@ -170,7 +172,7 @@ export class BackupManager {
this.backupInfo = undefined;
this.baseApis.emit('crypto.keyBackupStatus', false);
this.baseApis.emit(CryptoEvent.KeyBackupStatus, false);
}
public getKeyBackupEnabled(): boolean | null {
@@ -183,7 +185,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) {
@@ -298,7 +299,7 @@ export class BackupManager {
const ret = {
usable: false,
trusted_locally: false,
sigs: [],
sigs: [] as SigInfo[],
};
if (
@@ -311,14 +312,27 @@ export class BackupManager {
return ret;
}
const trustedPubkey = this.baseApis.crypto.sessionStore.getLocalTrustedBackupPubKey();
const privKey = await this.baseApis.crypto.getSessionBackupPrivateKey();
if (privKey) {
let algorithm;
try {
algorithm = await BackupManager.makeAlgorithm(backupInfo, async () => privKey);
if ("public_key" in backupInfo.auth_data && backupInfo.auth_data.public_key === trustedPubkey) {
logger.info("Backup public key " + trustedPubkey + " is trusted locally");
ret.trusted_locally = true;
if (await algorithm.keyMatches(privKey)) {
logger.info("Backup is trusted locally");
ret.trusted_locally = true;
}
} catch {
// do nothing -- if we have an error, then we don't mark it as
// locally trusted
} finally {
if (algorithm) {
algorithm.free();
}
}
}
const mySigs = backupInfo.auth_data.signatures[this.baseApis.getUserId()] || [];
const mySigs = backupInfo.auth_data.signatures[this.baseApis.getUserId()] || {};
for (const keyId of Object.keys(mySigs)) {
const keyIdParts = keyId.split(':');
@@ -443,7 +457,7 @@ export class BackupManager {
await this.checkKeyBackup();
// Backup version has changed or this backup version
// has been deleted
this.baseApis.crypto.emit("crypto.keyBackupFailed", err.data.errcode);
this.baseApis.crypto.emit(CryptoEvent.KeyBackupFailed, err.data.errcode);
throw err;
}
}
@@ -465,14 +479,14 @@ export class BackupManager {
* @param {integer} limit Maximum number of keys to back up
* @returns {integer} Number of sessions backed up
*/
private async backupPendingKeys(limit: number): Promise<number> {
public async backupPendingKeys(limit: number): Promise<number> {
const sessions = await this.baseApis.crypto.cryptoStore.getSessionsNeedingBackup(limit);
if (!sessions.length) {
return 0;
}
let remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining);
this.baseApis.crypto.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
const rooms: IKeyBackup["rooms"] = {};
for (const session of sessions) {
@@ -509,7 +523,7 @@ export class BackupManager {
await this.baseApis.crypto.cryptoStore.unmarkSessionsNeedingBackup(sessions);
remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining);
this.baseApis.crypto.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
return sessions.length;
}
@@ -565,7 +579,7 @@ export class BackupManager {
);
const remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
this.baseApis.emit("crypto.keyBackupSessionsRemaining", remaining);
this.baseApis.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
return remaining;
}
@@ -643,9 +657,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 {
@@ -656,7 +668,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 {
@@ -775,8 +787,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 {
+15 -13
View File
@@ -14,15 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import anotherjson from "another-json";
import { decodeBase64, encodeBase64 } from './olmlib';
import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store';
import { decryptAES, encryptAES } from './aes';
import anotherjson from "another-json";
import { logger } from '../logger';
import { ISecretStorageKeyInfo } from "./api";
// FIXME: these types should eventually go in a different file
type Signatures = Record<string, Record<string, string>>;
import { Crypto } from "./index";
import { Method } from "../http-api";
import { ISignatures } from "../@types/signed";
export interface IDehydratedDevice {
device_id: string; // eslint-disable-line camelcase
@@ -41,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";
@@ -60,7 +61,7 @@ export class DehydrationManager {
private key: Uint8Array;
private keyInfo: {[props: string]: any};
private deviceDisplayName: string;
constructor(private crypto) {
constructor(private readonly crypto: Crypto) {
this.getDehydrationKeyFromCache();
}
async getDehydrationKeyFromCache(): Promise<void> {
@@ -73,7 +74,7 @@ export class DehydrationManager {
async (result) => {
if (result) {
const { key, keyInfo, deviceDisplayName, time } = result;
const pickleKey = Buffer.from(this.crypto.olmDevice._pickleKey);
const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey);
const decrypted = await decryptAES(key, pickleKey, DEHYDRATION_ALGORITHM);
this.key = decodeBase64(decrypted);
this.keyInfo = keyInfo;
@@ -158,7 +159,7 @@ export class DehydrationManager {
this.timeoutId = undefined;
}
try {
const pickleKey = Buffer.from(this.crypto.olmDevice._pickleKey);
const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey);
// update the crypto store with the timestamp
const key = await encryptAES(encodeBase64(this.key), pickleKey, DEHYDRATION_ALGORITHM);
@@ -205,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,
{
@@ -242,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));
@@ -270,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,
{
+1 -1
View File
@@ -68,7 +68,7 @@ export class DeviceInfo {
*
* @return {module:crypto~DeviceInfo} new DeviceInfo
*/
public static fromStorage(obj: IDevice, deviceId: string): DeviceInfo {
public static fromStorage(obj: Partial<IDevice>, deviceId: string): DeviceInfo {
const res = new DeviceInfo(deviceId);
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {
+258 -164
View File
@@ -22,27 +22,36 @@ limitations under the License.
*/
import anotherjson from "another-json";
import { EventEmitter } from 'events';
import { ReEmitter } from '../ReEmitter';
import { TypedReEmitter } from '../ReEmitter';
import { logger } from '../logger';
import { OlmDevice } from "./OlmDevice";
import { IExportedDevice, OlmDevice } from "./OlmDevice";
import * as olmlib from "./olmlib";
import { DeviceInfoMap, DeviceList } from "./DeviceList";
import { DeviceInfo, IDevice } from "./deviceinfo";
import type { DecryptionAlgorithm, EncryptionAlgorithm } from "./algorithms";
import * as algorithms from "./algorithms";
import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from './CrossSigning';
import { EncryptionSetupBuilder } from "./EncryptionSetup";
import {
IAccountDataClient,
ISecretRequest,
SECRET_STORAGE_ALGORITHM_V1_AES,
SecretStorage,
SecretStorageKeyTuple,
ISecretRequest,
SecretStorageKeyObject,
SecretStorageKeyTuple,
} from './SecretStorage';
import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from "./api";
import {
IAddSecretStorageKeyOpts,
ICreateSecretStorageOpts,
IEncryptedEventInfo,
IImportRoomKeysOpts,
IRecoveryKey,
ISecretStorageKeyInfo,
} from "./api";
import { OutgoingRoomKeyRequestManager } from './OutgoingRoomKeyRequestManager';
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { VerificationBase } from "./verification/Base";
import { ReciprocateQRCode, SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD } from './verification/QRCode';
import { SAS as SASVerification } from './verification/SAS';
import { keyFromPassphrase } from './key_passphrase';
@@ -52,20 +61,28 @@ import { InRoomChannel, InRoomRequests } from "./verification/request/InRoomChan
import { ToDeviceChannel, ToDeviceRequests } from "./verification/request/ToDeviceChannel";
import { IllegalMethod } from "./verification/IllegalMethod";
import { KeySignatureUploadError } from "../errors";
import { decryptAES, encryptAES, calculateKeyCheck } from './aes';
import { calculateKeyCheck, decryptAES, encryptAES } from './aes';
import { DehydrationManager, IDeviceKeys, IOneTimeKey } from './dehydration';
import { BackupManager } from "./backup";
import { IStore } from "../store";
import { Room } from "../models/room";
import { RoomMember } from "../models/room-member";
import { MatrixEvent } from "../models/event";
import { MatrixClient, IKeysUploadResponse, SessionStore, ISignedKey } from "../client";
import type { EncryptionAlgorithm, DecryptionAlgorithm } from "./algorithms/base";
import { Room, RoomEvent } from "../models/room";
import { RoomMember, RoomMemberEvent } from "../models/room-member";
import { EventStatus, IClearEvent, IEvent, MatrixEvent, MatrixEventEvent } from "../models/event";
import {
ClientEvent,
ICrossSigningKey,
IKeysUploadResponse,
ISignedKey,
IUploadKeySignaturesResponse,
MatrixClient,
SessionStore,
} from "../client";
import type { IRoomEncryption, RoomList } from "./RoomList";
import { IRecoveryKey, IEncryptedEventInfo } from "./api";
import { IKeyBackupInfo } from "./keybackup";
import { ISyncStateData } from "../sync";
import { CryptoStore } from "./store/base";
import { IVerificationChannel } from "./verification/request/Channel";
import { TypedEventEmitter } from "../models/typed-event-emitter";
const DeviceVerification = DeviceInfo.DeviceVerification;
@@ -84,12 +101,12 @@ const defaultVerificationMethods = {
* verification method names
*/
// legacy export identifier
export enum verificationMethods {
RECIPROCATE_QR_CODE = ReciprocateQRCode.NAME,
SAS = SASVerification.NAME,
}
export const verificationMethods = {
RECIPROCATE_QR_CODE: ReciprocateQRCode.NAME,
SAS: SASVerification.NAME,
};
export type VerificationMethod = verificationMethods;
export type VerificationMethod = keyof typeof verificationMethods | string;
export function isCryptoAvailable(): boolean {
return Boolean(global.Olm);
@@ -98,7 +115,7 @@ export function isCryptoAvailable(): boolean {
const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000;
interface IInitOpts {
exportedOlmDevice?: any; // TODO types
exportedOlmDevice?: IExportedDevice;
pickleKey?: string;
}
@@ -107,17 +124,6 @@ export interface IBootstrapCrossSigningOpts {
authUploadDeviceSigningKeys?(makeRequest: (authData: any) => {}): Promise<void>;
}
interface IBootstrapSecretStorageOpts {
keyBackupInfo?: any; // TODO types
setupNewKeyBackup?: boolean;
setupNewSecretStorage?: boolean;
createSecretStorageKey?(): Promise<{
keyInfo?: any; // TODO types
privateKey?: Uint8Array;
}>;
getKeyBackupPassphrase?(): Promise<Uint8Array | null>;
}
/* eslint-disable camelcase */
interface IRoomKey {
room_id: string;
@@ -130,13 +136,14 @@ export interface IRoomKeyRequestBody extends IRoomKey {
}
export interface IMegolmSessionData {
[key: string]: any; // extensible
sender_key: string;
forwarding_curve25519_key_chain: string[];
sender_claimed_keys: Record<string, string>;
room_id: string;
session_id: string;
session_key: string;
algorithm: string;
algorithm?: string;
untrusted?: boolean;
}
/* eslint-enable camelcase */
@@ -181,18 +188,63 @@ interface ISignableObject {
}
export interface IEventDecryptionResult {
clearEvent: object;
clearEvent: IClearEvent;
forwardingCurve25519KeyChain?: string[];
senderCurve25519Key?: string;
claimedEd25519Key?: string;
forwardingCurve25519KeyChain?: string[];
untrusted?: boolean;
}
export class Crypto extends EventEmitter {
export interface IRequestsMap {
getRequest(event: MatrixEvent): VerificationRequest;
getRequestByChannel(channel: IVerificationChannel): VerificationRequest;
setRequest(event: MatrixEvent, request: VerificationRequest): void;
setRequestByChannel(channel: IVerificationChannel, request: VerificationRequest): void;
}
export enum CryptoEvent {
DeviceVerificationChanged = "deviceVerificationChanged",
UserTrustStatusChanged = "userTrustStatusChanged",
UserCrossSigningUpdated = "userCrossSigningUpdated",
RoomKeyRequest = "crypto.roomKeyRequest",
RoomKeyRequestCancellation = "crypto.roomKeyRequestCancellation",
KeyBackupStatus = "crypto.keyBackupStatus",
KeyBackupFailed = "crypto.keyBackupFailed",
KeyBackupSessionsRemaining = "crypto.keyBackupSessionsRemaining",
KeySignatureUploadFailure = "crypto.keySignatureUploadFailure",
VerificationRequest = "crypto.verification.request",
Warning = "crypto.warning",
WillUpdateDevices = "crypto.willUpdateDevices",
DevicesUpdated = "crypto.devicesUpdated",
KeysChanged = "crossSigning.keysChanged",
}
export type CryptoEventHandlerMap = {
[CryptoEvent.DeviceVerificationChanged]: (userId: string, deviceId: string, device: DeviceInfo) => void;
[CryptoEvent.UserTrustStatusChanged]: (userId: string, trustLevel: UserTrustLevel) => void;
[CryptoEvent.RoomKeyRequest]: (request: IncomingRoomKeyRequest) => void;
[CryptoEvent.RoomKeyRequestCancellation]: (request: IncomingRoomKeyRequestCancellation) => void;
[CryptoEvent.KeyBackupStatus]: (enabled: boolean) => void;
[CryptoEvent.KeyBackupFailed]: (errcode: string) => void;
[CryptoEvent.KeyBackupSessionsRemaining]: (remaining: number) => void;
[CryptoEvent.KeySignatureUploadFailure]: (
failures: IUploadKeySignaturesResponse["failures"],
source: "checkOwnCrossSigningTrust" | "afterCrossSigningLocalKeyChange" | "setDeviceVerification",
upload: (opts: { shouldEmit: boolean }) => Promise<void>
) => void;
[CryptoEvent.VerificationRequest]: (request: VerificationRequest<any>) => void;
[CryptoEvent.Warning]: (type: string) => void;
[CryptoEvent.KeysChanged]: (data: {}) => void;
[CryptoEvent.WillUpdateDevices]: (users: string[], initialFetch: boolean) => void;
[CryptoEvent.DevicesUpdated]: (users: string[], initialFetch: boolean) => void;
[CryptoEvent.UserCrossSigningUpdated]: (userId: string) => void;
};
export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap> {
/**
* @return {string} The version of Olm.
*/
static getOlmVersion(): string {
static getOlmVersion(): [number, number, number] {
return OlmDevice.getOlmVersion();
}
@@ -203,9 +255,9 @@ export class Crypto extends EventEmitter {
public readonly dehydrationManager: DehydrationManager;
public readonly secretStorage: SecretStorage;
private readonly reEmitter: ReEmitter;
private readonly verificationMethods: any; // TODO types
private readonly supportedAlgorithms: string[];
private readonly reEmitter: TypedReEmitter<CryptoEvent, CryptoEventHandlerMap>;
private readonly verificationMethods: Map<VerificationMethod, typeof VerificationBase>;
public readonly supportedAlgorithms: string[];
private readonly outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager;
private readonly toDeviceVerificationRequests: ToDeviceRequests;
public readonly inRoomVerificationRequests: InRoomRequests;
@@ -257,6 +309,7 @@ export class Crypto extends EventEmitter {
private oneTimeKeyCount: number;
private needsNewFallback: boolean;
private fallbackCleanup?: number; // setTimeout ID
/**
* Cryptography bits
@@ -291,15 +344,15 @@ export class Crypto extends EventEmitter {
constructor(
public readonly baseApis: MatrixClient,
public readonly sessionStore: SessionStore,
private readonly userId: string,
public readonly userId: string,
private readonly deviceId: string,
private readonly clientStore: IStore,
public readonly cryptoStore: CryptoStore,
private readonly roomList: RoomList,
verificationMethods: any[], // TODO types
verificationMethods: Array<keyof typeof defaultVerificationMethods | typeof VerificationBase>,
) {
super();
this.reEmitter = new ReEmitter(this);
this.reEmitter = new TypedReEmitter(this);
if (verificationMethods) {
this.verificationMethods = new Map();
@@ -308,20 +361,21 @@ export class Crypto extends EventEmitter {
if (defaultVerificationMethods[method]) {
this.verificationMethods.set(
method,
defaultVerificationMethods[method],
<typeof VerificationBase>defaultVerificationMethods[method],
);
}
} else if (method.NAME) {
} else if (method["NAME"]) {
this.verificationMethods.set(
method.NAME,
method,
method["NAME"],
method as typeof VerificationBase,
);
} else {
logger.warn(`Excluding unknown verification method ${method}`);
}
}
} else {
this.verificationMethods = defaultVerificationMethods;
this.verificationMethods =
new Map(Object.entries(defaultVerificationMethods)) as Map<VerificationMethod, typeof VerificationBase>;
}
this.backupManager = new BackupManager(baseApis, async () => {
@@ -359,8 +413,8 @@ export class Crypto extends EventEmitter {
// XXX: This isn't removed at any point, but then none of the event listeners
// this class sets seem to be removed at any point... :/
this.deviceList.on('userCrossSigningUpdated', this.onDeviceListUserCrossSigningUpdated);
this.reEmitter.reEmit(this.deviceList, ["crypto.devicesUpdated", "crypto.willUpdateDevices"]);
this.deviceList.on(CryptoEvent.UserCrossSigningUpdated, this.onDeviceListUserCrossSigningUpdated);
this.reEmitter.reEmit(this.deviceList, [CryptoEvent.DevicesUpdated, CryptoEvent.WillUpdateDevices]);
this.supportedAlgorithms = Object.keys(algorithms.DECRYPTION_CLASSES);
@@ -376,7 +430,7 @@ export class Crypto extends EventEmitter {
this.crossSigningInfo = new CrossSigningInfo(userId, cryptoCallbacks, cacheCallbacks);
// Yes, we pass the client twice here: see SecretStorage
this.secretStorage = new SecretStorage(baseApis, cryptoCallbacks, baseApis);
this.secretStorage = new SecretStorage(baseApis as IAccountDataClient, cryptoCallbacks, baseApis);
this.dehydrationManager = new DehydrationManager(this);
// Assuming no app-supplied callback, default to getting from SSSS.
@@ -488,7 +542,7 @@ export class Crypto extends EventEmitter {
deviceTrust.isCrossSigningVerified()
) {
const deviceObj = this.deviceList.getStoredDevice(userId, deviceId);
this.emit("deviceVerificationChanged", userId, deviceId, deviceObj);
this.emit(CryptoEvent.DeviceVerificationChanged, userId, deviceId, deviceObj);
}
}
}
@@ -504,7 +558,7 @@ export class Crypto extends EventEmitter {
* recovery key which should be disposed of after displaying to the user,
* and raw private key to avoid round tripping if needed.
*/
public async createRecoveryKeyFromPassphrase(password: string): Promise<IRecoveryKey> {
public async createRecoveryKeyFromPassphrase(password?: string): Promise<IRecoveryKey> {
const decryption = new global.Olm.PkDecryption();
try {
const keyInfo: Partial<IRecoveryKey["keyInfo"]> = {};
@@ -760,12 +814,12 @@ export class Crypto extends EventEmitter {
*/
// TODO this does not resolve with what it says it does
public async bootstrapSecretStorage({
createSecretStorageKey = async () => ({ }),
createSecretStorageKey = async () => ({} as IRecoveryKey),
keyBackupInfo,
setupNewKeyBackup,
setupNewSecretStorage,
getKeyBackupPassphrase,
}: IBootstrapSecretStorageOpts = {}) {
}: ICreateSecretStorageOpts = {}) {
logger.log("Bootstrapping Secure Secret Storage");
const delegateCryptoCallbacks = this.baseApis.cryptoCallbacks;
const builder = new EncryptionSetupBuilder(
@@ -781,8 +835,7 @@ export class Crypto extends EventEmitter {
let newKeyId = null;
// create a new SSSS key and set it as default
const createSSSS = async (opts, privateKey: Uint8Array) => {
opts = opts || {};
const createSSSS = async (opts: IAddSecretStorageKeyOpts, privateKey: Uint8Array) => {
if (privateKey) {
opts.key = privateKey;
}
@@ -798,7 +851,7 @@ export class Crypto extends EventEmitter {
return keyId;
};
const ensureCanCheckPassphrase = async (keyId, keyInfo) => {
const ensureCanCheckPassphrase = async (keyId: string, keyInfo: ISecretStorageKeyInfo) => {
if (!keyInfo.mac) {
const key = await this.baseApis.cryptoCallbacks.getSecretStorageKey(
{ keys: { [keyId]: keyInfo } }, "",
@@ -817,7 +870,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")
@@ -878,7 +931,7 @@ export class Crypto extends EventEmitter {
const backupKey = await this.getSessionBackupPrivateKey() || await getKeyBackupPassphrase();
// create a new SSSS key and use the backup key as the new SSSS key
const opts: any = {}; // TODO types
const opts = {} as IAddSecretStorageKeyOpts;
if (
keyBackupInfo.auth_data.private_key_salt &&
@@ -896,9 +949,7 @@ export class Crypto extends EventEmitter {
newKeyId = await createSSSS(opts, backupKey);
// store the backup key in secret storage
await secretStorage.store(
"m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId],
);
await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId]);
// The backup is trusted because the user provided the private key.
// Sign the backup with the cross-signing key so the key backup can
@@ -1028,7 +1079,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);
}
@@ -1039,7 +1090,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();
}
@@ -1097,7 +1148,7 @@ export class Crypto extends EventEmitter {
await this.storeSessionBackupPrivateKey(key);
}
if (key && key.ciphertext) {
const pickleKey = Buffer.from(this.olmDevice._pickleKey);
const pickleKey = Buffer.from(this.olmDevice.pickleKey);
const decrypted = await decryptAES(key, pickleKey, "m.megolm_backup.v1");
key = olmlib.decodeBase64(decrypted);
}
@@ -1113,7 +1164,7 @@ export class Crypto extends EventEmitter {
if (!(key instanceof Uint8Array)) {
throw new Error(`storeSessionBackupPrivateKey expects Uint8Array, got ${key}`);
}
const pickleKey = Buffer.from(this.olmDevice._pickleKey);
const pickleKey = Buffer.from(this.olmDevice.pickleKey);
const encryptedKey = await encryptAES(olmlib.encodeBase64(key), pickleKey, "m.megolm_backup.v1");
return this.cryptoStore.doTxn(
'readwrite',
@@ -1159,7 +1210,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,
@@ -1169,7 +1220,7 @@ export class Crypto extends EventEmitter {
if (Object.keys(failures || []).length > 0) {
if (shouldEmit) {
this.baseApis.emit(
"crypto.keySignatureUploadFailure",
CryptoEvent.KeySignatureUploadFailure,
failures,
"afterCrossSigningLocalKeyChange",
upload, // continuation
@@ -1195,7 +1246,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(
@@ -1272,7 +1323,7 @@ export class Crypto extends EventEmitter {
*/
private async checkForValidDeviceSignature(
userId: string,
key: any, // TODO types
key: ICrossSigningKey,
devices: Record<string, IDevice>,
): Promise<string[]> {
const deviceIds: string[] = [];
@@ -1395,11 +1446,10 @@ export class Crypto extends EventEmitter {
// that reset the keys
this.storeTrustedSelfKeys(null);
// emit cross-signing has been disabled
this.emit("crossSigning.keysChanged", {});
this.emit(CryptoEvent.KeysChanged, {});
// as the trust for our own user has changed,
// also emit an event for this
this.emit("userTrustStatusChanged",
this.userId, this.checkUserTrust(userId));
this.emit(CryptoEvent.UserTrustStatusChanged, this.userId, this.checkUserTrust(userId));
}
} else {
await this.checkDeviceVerifications(userId);
@@ -1414,7 +1464,7 @@ export class Crypto extends EventEmitter {
this.deviceList.setRawStoredCrossSigningForUser(userId, crossSigning.toStorage());
}
this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId));
this.emit(CryptoEvent.UserTrustStatusChanged, userId, this.checkUserTrust(userId));
}
};
@@ -1493,7 +1543,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"));
@@ -1548,7 +1598,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: {
@@ -1562,7 +1612,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) => {
@@ -1571,7 +1621,7 @@ export class Crypto extends EventEmitter {
if (Object.keys(failures || []).length > 0) {
if (shouldEmit) {
this.baseApis.emit(
"crypto.keySignatureUploadFailure",
CryptoEvent.KeySignatureUploadFailure,
failures,
"checkOwnCrossSigningTrust",
upload,
@@ -1589,10 +1639,10 @@ export class Crypto extends EventEmitter {
upload({ shouldEmit: true });
}
this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId));
this.emit(CryptoEvent.UserTrustStatusChanged, userId, this.checkUserTrust(userId));
if (masterChanged) {
this.baseApis.emit("crossSigning.keysChanged", {});
this.emit(CryptoEvent.KeysChanged, {});
await this.afterCrossSigningLocalKeyChange();
}
@@ -1607,7 +1657,7 @@ export class Crypto extends EventEmitter {
*
* @param {object} keys The new trusted set of keys
*/
private async storeTrustedSelfKeys(keys: any): Promise<void> { // TODO types
private async storeTrustedSelfKeys(keys: Record<string, ICrossSigningKey>): Promise<void> {
if (keys) {
this.crossSigningInfo.setKeys(keys);
} else {
@@ -1679,18 +1729,14 @@ export class Crypto extends EventEmitter {
* @param {external:EventEmitter} eventEmitter event source where we can register
* for event notifications
*/
public registerEventHandlers(eventEmitter: EventEmitter): void {
eventEmitter.on("RoomMember.membership", (event: MatrixEvent, member: RoomMember, oldMembership?: string) => {
try {
this.onRoomMembership(event, member, oldMembership);
} catch (e) {
logger.error("Error handling membership change:", e);
}
});
eventEmitter.on("toDeviceEvent", this.onToDeviceEvent);
eventEmitter.on("Room.timeline", this.onTimelineEvent);
eventEmitter.on("Event.decrypted", this.onTimelineEvent);
public registerEventHandlers(eventEmitter: TypedEventEmitter<
RoomMemberEvent.Membership | ClientEvent.ToDeviceEvent | RoomEvent.Timeline | MatrixEventEvent.Decrypted,
any
>): void {
eventEmitter.on(RoomMemberEvent.Membership, this.onMembership);
eventEmitter.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
eventEmitter.on(RoomEvent.Timeline, this.onTimelineEvent);
eventEmitter.on(MatrixEventEvent.Decrypted, this.onTimelineEvent);
}
/** Start background processes related to crypto */
@@ -1862,8 +1908,23 @@ export class Crypto extends EventEmitter {
}
if (this.getNeedsNewFallback()) {
logger.info("generating fallback key");
await this.olmDevice.generateFallbackKey();
const fallbackKeys = await this.olmDevice.getFallbackKey();
// if fallbackKeys is non-empty, we've already generated a
// fallback key, but it hasn't been published yet, so we
// can use that instead of generating a new one
if (!fallbackKeys.curve25519 ||
Object.keys(fallbackKeys.curve25519).length == 0) {
logger.info("generating fallback key");
if (this.fallbackCleanup) {
// cancel any pending fallback cleanup because generating
// a new fallback key will already drop the old fallback
// that would have been dropped, and we don't want to kill
// the current key
clearTimeout(this.fallbackCleanup);
delete this.fallbackCleanup;
}
await this.olmDevice.generateFallbackKey();
}
}
logger.info("calling uploadOneTimeKeys");
@@ -1910,9 +1971,10 @@ export class Crypto extends EventEmitter {
private async uploadOneTimeKeys() {
const promises = [];
const fallbackJson: Record<string, IOneTimeKey> = {};
let fallbackJson: Record<string, IOneTimeKey>;
if (this.getNeedsNewFallback()) {
const fallbackKeys = await this.olmDevice.getFallbackKey() as Record<string, Record<string, string>>;
fallbackJson = {};
const fallbackKeys = await this.olmDevice.getFallbackKey();
for (const [keyId, key] of Object.entries(fallbackKeys.curve25519)) {
const k = { key, fallback: true };
fallbackJson["signed_curve25519:" + keyId] = k;
@@ -1922,7 +1984,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)) {
@@ -1936,10 +1998,23 @@ export class Crypto extends EventEmitter {
await Promise.all(promises);
const res = await this.baseApis.uploadKeysRequest({
const requestBody: Record<string, any> = {
"one_time_keys": oneTimeJson,
"org.matrix.msc2732.fallback_keys": fallbackJson,
});
};
if (fallbackJson) {
requestBody["org.matrix.msc2732.fallback_keys"] = fallbackJson;
requestBody["fallback_keys"] = fallbackJson;
}
const res = await this.baseApis.uploadKeysRequest(requestBody);
if (fallbackJson) {
this.fallbackCleanup = setTimeout(() => {
delete this.fallbackCleanup;
this.olmDevice.forgetOldFallbackKey();
}, 60*60*1000);
}
await this.olmDevice.markKeysAsPublished();
return res;
@@ -2045,9 +2120,7 @@ export class Crypto extends EventEmitter {
if (!this.crossSigningInfo.getId() && userId === this.crossSigningInfo.userId) {
this.storeTrustedSelfKeys(xsk.keys);
// This will cause our own user trust to change, so emit the event
this.emit(
"userTrustStatusChanged", this.userId, this.checkUserTrust(userId),
);
this.emit(CryptoEvent.UserTrustStatusChanged, this.userId, this.checkUserTrust(userId));
}
// Now sign the master key with our user signing key (unless it's ourself)
@@ -2058,7 +2131,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]: {
@@ -2069,7 +2142,7 @@ export class Crypto extends EventEmitter {
if (Object.keys(failures || []).length > 0) {
if (shouldEmit) {
this.baseApis.emit(
"crypto.keySignatureUploadFailure",
CryptoEvent.KeySignatureUploadFailure,
failures,
"setDeviceVerification",
upload,
@@ -2131,7 +2204,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`);
@@ -2142,7 +2215,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]: {
@@ -2153,7 +2226,7 @@ export class Crypto extends EventEmitter {
if (Object.keys(failures || []).length > 0) {
if (shouldEmit) {
this.baseApis.emit(
"crypto.keySignatureUploadFailure",
CryptoEvent.KeySignatureUploadFailure,
failures,
"setDeviceVerification",
upload, // continuation
@@ -2168,7 +2241,7 @@ export class Crypto extends EventEmitter {
}
const deviceObj = DeviceInfo.fromStorage(dev, deviceId);
this.emit("deviceVerificationChanged", userId, deviceId, deviceObj);
this.emit(CryptoEvent.DeviceVerificationChanged, userId, deviceId, deviceObj);
return deviceObj;
}
@@ -2176,24 +2249,20 @@ export class Crypto extends EventEmitter {
return this.inRoomVerificationRequests.findRequestInProgress(roomId);
}
public getVerificationRequestsToDeviceInProgress(userId: string): VerificationRequest {
public getVerificationRequestsToDeviceInProgress(userId: string): VerificationRequest[] {
return this.toDeviceVerificationRequests.getRequestsInProgress(userId);
}
public requestVerificationDM(userId: string, roomId: string): VerificationRequest {
public requestVerificationDM(userId: string, roomId: string): Promise<VerificationRequest> {
const existingRequest = this.inRoomVerificationRequests.findRequestInProgress(roomId);
if (existingRequest) {
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[]): VerificationRequest {
public requestVerification(userId: string, devices: string[]): Promise<VerificationRequest> {
if (!devices) {
devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(userId));
}
@@ -2202,18 +2271,14 @@ 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: any, // TODO types
requestsMap: any, // TODO types
): VerificationRequest {
channel: IVerificationChannel,
requestsMap: IRequestsMap,
): Promise<VerificationRequest> {
let request = new VerificationRequest(channel, this.verificationMethods, this.baseApis);
// if transaction id is already known, add request
if (channel.transactionId) {
@@ -2259,14 +2324,11 @@ export class Crypto extends EventEmitter {
userId: string,
deviceId: string,
method: VerificationMethod,
): VerificationRequest {
): Promise<VerificationRequest> {
const transactionId = ToDeviceChannel.makeTransactionId();
const channel = new ToDeviceChannel(
this.baseApis, userId, [deviceId], transactionId, deviceId);
const request = new VerificationRequest(
channel, this.verificationMethods, this.baseApis);
this.toDeviceVerificationRequests.setRequestBySenderAndTxnId(
userId, transactionId, request);
const channel = new ToDeviceChannel(this.baseApis, userId, [deviceId], transactionId, deviceId);
const request = new VerificationRequest(channel, this.verificationMethods, this.baseApis);
this.toDeviceVerificationRequests.setRequestBySenderAndTxnId(userId, transactionId, request);
const verifier = request.beginKeyVerification(method, { userId, deviceId });
// either reject by an error from verify() while sending .start
// or resolve when the request receives the
@@ -2593,13 +2655,17 @@ export class Crypto extends EventEmitter {
* the given users.
*
* @param {string[]} users list of user ids
* @param {boolean} force If true, force a new Olm session to be created. Default false.
*
* @return {Promise} resolves once the sessions are complete, to
* an Object mapping from userId to deviceId to
* {@link module:crypto~OlmSessionResult}
*/
ensureOlmSessionsForUsers(users: string[]): Promise<Record<string, Record<string, olmlib.IOlmSessionResult>>> {
const devicesByUser = {};
public ensureOlmSessionsForUsers(
users: string[],
force?: boolean,
): Promise<Record<string, Record<string, olmlib.IOlmSessionResult>>> {
const devicesByUser: Record<string, DeviceInfo[]> = {};
for (let i = 0; i < users.length; ++i) {
const userId = users[i];
@@ -2623,7 +2689,7 @@ export class Crypto extends EventEmitter {
}
}
return olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser);
return olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser, force);
}
/**
@@ -2632,7 +2698,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) => {
@@ -2659,7 +2725,7 @@ export class Crypto extends EventEmitter {
* @param {Function} opts.progressCallback called with an object which has a stage param
* @return {Promise} a promise which resolves once the keys have been imported
*/
public importRoomKeys(keys: IMegolmSessionData[], opts: any = {}): Promise<any> { // TODO types
public importRoomKeys(keys: IMegolmSessionData[], opts: IImportRoomKeysOpts = {}): Promise<void> {
let successes = 0;
let failures = 0;
const total = keys.length;
@@ -2686,7 +2752,7 @@ export class Crypto extends EventEmitter {
successes++;
if (opts.progressCallback) { updateProgress(); }
});
}));
})).then();
}
/**
@@ -2710,7 +2776,6 @@ export class Crypto extends EventEmitter {
}
}
/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307
/**
* Encrypt an event according to the configuration of the room.
*
@@ -2721,8 +2786,6 @@ export class Crypto extends EventEmitter {
* @return {Promise?} Promise which resolves when the event has been
* encrypted, or null if nothing was needed
*/
/* eslint-enable valid-jsdoc */
// TODO this return type lies
public async encryptEvent(event: MatrixEvent, room: Room): Promise<void> {
if (!room) {
throw new Error("Cannot send encrypted messages in unknown rooms");
@@ -2757,12 +2820,21 @@ export class Crypto extends EventEmitter {
delete content['m.relates_to'];
}
const encryptedContent = await alg.encryptMessage(
room, event.getType(), content);
// Treat element's performance metrics the same as `m.relates_to` (when present)
const elementPerfMetrics = content['io.element.performance_metrics'];
if (elementPerfMetrics) {
content = Object.assign({}, content);
delete content['io.element.performance_metrics'];
}
const encryptedContent = await alg.encryptMessage(room, event.getType(), content);
if (mRelatesTo) {
encryptedContent['m.relates_to'] = mRelatesTo;
}
if (elementPerfMetrics) {
encryptedContent['io.element.performance_metrics'] = elementPerfMetrics;
}
event.makeEncrypted(
"m.room.encrypted",
@@ -2792,7 +2864,7 @@ export class Crypto extends EventEmitter {
type: "m.room.message",
content: {},
unsigned: {
redacted_because: decryptedEvent.clearEvent,
redacted_because: decryptedEvent.clearEvent as IEvent,
},
},
};
@@ -3021,6 +3093,14 @@ export class Crypto extends EventEmitter {
});
}
private onMembership = (event: MatrixEvent, member: RoomMember, oldMembership?: string) => {
try {
this.onRoomMembership(event, member, oldMembership);
} catch (e) {
logger.error("Error handling membership change:", e);
}
};
private onToDeviceEvent = (event: MatrixEvent): void => {
try {
logger.log(`received to_device ${event.getType()} from: ` +
@@ -3046,7 +3126,7 @@ export class Crypto extends EventEmitter {
event.attemptDecryption(this);
}
// once the event has been decrypted, try again
event.once('Event.decrypted', (ev) => {
event.once(MatrixEventEvent.Decrypted, (ev) => {
this.onToDeviceEvent(ev);
});
}
@@ -3125,7 +3205,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;
}
@@ -3143,11 +3223,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);
}
/**
@@ -3170,7 +3246,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(),
@@ -3178,20 +3254,38 @@ 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.
if (event.isSending() && event.status != EventStatus.SENT) {
let eventIdListener;
let statusListener;
try {
await new Promise((resolve, reject) => {
eventIdListener = resolve;
statusListener = () => {
if (event.status == EventStatus.CANCELLED) {
reject(new Error("Event status set to CANCELLED."));
}
};
event.once(MatrixEventEvent.LocalEventIdReplaced, eventIdListener);
event.on(MatrixEventEvent.Status, statusListener);
});
} catch (err) {
logger.error("error while waiting for the verification event to be sent: " + err.message);
return;
} finally {
event.removeListener(MatrixEventEvent.LocalEventIdReplaced, eventIdListener);
event.removeListener(MatrixEventEvent.Status, statusListener);
}
}
let request = requestsMap.getRequest(event);
let isNewRequest = false;
if (!request) {
@@ -3216,7 +3310,7 @@ export class Crypto extends EventEmitter {
!request.invalid && // check it has enough events to pass the UNSENT stage
!request.observeOnly;
if (shouldEmit) {
this.baseApis.emit("crypto.verification.request", request);
this.baseApis.emit(CryptoEvent.VerificationRequest, request);
}
}
@@ -3280,7 +3374,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);
@@ -3517,7 +3611,7 @@ export class Crypto extends EventEmitter {
return;
}
this.emit("crypto.roomKeyRequest", req);
this.emit(CryptoEvent.RoomKeyRequest, req);
}
/**
@@ -3536,7 +3630,7 @@ export class Crypto extends EventEmitter {
// we should probably only notify the app of cancellations we told it
// about, but we don't currently have a record of that, so we just pass
// everything through.
this.emit("crypto.roomKeyRequestCancellation", cancellation);
this.emit(CryptoEvent.RoomKeyRequestCancellation, cancellation);
}
/**
+29 -1
View File
@@ -15,6 +15,10 @@ limitations under the License.
*/
import { randomString } from '../randomstring';
import { getCrypto } from '../utils';
const subtleCrypto = (typeof window !== "undefined" && window.crypto) ?
(window.crypto.subtle || window.crypto.webkitSubtle) : null;
const DEFAULT_ITERATIONS = 500000;
@@ -70,11 +74,21 @@ export async function deriveKey(
salt: string,
iterations: number,
numBits = DEFAULT_BITSIZE,
): Promise<Uint8Array> {
return subtleCrypto
? deriveKeyBrowser(password, salt, iterations, numBits)
: deriveKeyNode(password, salt, iterations, numBits);
}
async function deriveKeyBrowser(
password: string,
salt: string,
iterations: number,
numBits: number,
): Promise<Uint8Array> {
const subtleCrypto = global.crypto.subtle;
const TextEncoder = global.TextEncoder;
if (!subtleCrypto || !TextEncoder) {
// TODO: Implement this for node
throw new Error("Password-based backup is not avaiable on this platform");
}
@@ -99,3 +113,17 @@ export async function deriveKey(
return new Uint8Array(keybits);
}
async function deriveKeyNode(
password: string,
salt: string,
iterations: number,
numBits: number,
): Promise<Uint8Array> {
const crypto = getCrypto();
if (!crypto) {
throw new Error("No usable crypto implementation");
}
return crypto.pbkdf2Sync(password, Buffer.from(salt, 'binary'), iterations, numBits, 'sha512');
}
+1 -1
View File
@@ -67,5 +67,5 @@ export interface IKeyBackupRestoreResult {
export interface IKeyBackupRestoreOpts {
cacheCompleteCallback?: () => void;
progressCallback?: ({ stage: string }) => void;
progressCallback?: (progress: { stage: string }) => void;
}
+25 -24
View File
@@ -21,15 +21,15 @@ limitations under the License.
*/
import anotherjson from "another-json";
import type { PkSigning } from "@matrix-org/olm";
import { Logger } from "loglevel";
import OlmDevice from "./OlmDevice";
import type { PkSigning } from "@matrix-org/olm";
import { OlmDevice } from "./OlmDevice";
import { DeviceInfo } from "./deviceinfo";
import { logger } from '../logger';
import * as utils from "../utils";
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",
@@ -126,13 +126,18 @@ export async function encryptMessageForDevice(
// involved in the session. If we're looking to reduce data transfer in the
// future, we could elide them for subsequent messages.
utils.extend(payload, payloadFields);
Object.assign(payload, payloadFields);
resultsObject[deviceKey] = await olmDevice.encryptMessage(
deviceKey, sessionId, JSON.stringify(payload),
);
}
interface IExistingOlmSession {
device: DeviceInfo;
sessionId?: string;
}
/**
* Get the existing olm sessions for the given devices, and the devices that
* don't have olm sessions.
@@ -153,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) {
@@ -231,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
@@ -252,13 +257,13 @@ export async function ensureOlmSessionsForDevices(
continue;
}
if (!olmDevice._sessionsInProgress[key]) {
if (!olmDevice.sessionsInProgress[key]) {
// pre-emptively mark the session as in-progress to avoid race
// conditions. If we find that we already have a session, then
// we'll resolve
olmDevice._sessionsInProgress[key] = new Promise(resolve => {
olmDevice.sessionsInProgress[key] = new Promise(resolve => {
resolveSession[key] = (v: any) => {
delete olmDevice._sessionsInProgress[key];
delete olmDevice.sessionsInProgress[key];
resolve(v);
};
});
@@ -291,9 +296,7 @@ export async function ensureOlmSessionsForDevices(
}
const forWhom = `for ${key} (${userId}:${deviceId})`;
const sessionId = await olmDevice.getSessionIdForDevice(
key, resolveSession[key], log,
);
const sessionId = await olmDevice.getSessionIdForDevice(key, !!resolveSession[key], log);
if (sessionId !== null && resolveSession[key]) {
// we found a session, but we had marked the session as
// in-progress, so resolve it now, which will unmark it and
@@ -324,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)) {
@@ -340,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++) {
@@ -362,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];
@@ -444,7 +445,7 @@ async function _verifyKeyAndStartSession(
export interface IObject {
unsigned?: object;
signatures?: object;
signatures?: ISignatures;
}
/**
+2 -2
View File
@@ -23,7 +23,7 @@ import { IRoomEncryption } from "../RoomList";
import { IDevice } from "../deviceinfo";
import { ICrossSigningInfo } from "../CrossSigning";
import { PrefixedLogger } from "../../logger";
import { InboundGroupSessionData } from "../../@types/partials";
import { InboundGroupSessionData } from "../OlmDevice";
import { IEncryptedPayload } from "../aes";
/**
@@ -125,7 +125,7 @@ export interface CryptoStore {
addSharedHistoryInboundGroupSession(roomId: string, senderKey: string, sessionId: string, txn?: unknown): void;
getSharedHistoryInboundGroupSessions(
roomId: string,
txn?: IDBTransaction,
txn?: unknown,
): Promise<[senderKey: string, sessionId: string][]>;
// Session key backups
@@ -30,7 +30,7 @@ import { IRoomKeyRequestBody } from "../index";
import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { InboundGroupSessionData } from "../../@types/partials";
import { InboundGroupSessionData } from "../OlmDevice";
import { IEncryptedPayload } from "../aes";
export const VERSION = 10;
@@ -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();
+1 -1
View File
@@ -34,7 +34,7 @@ import { IRoomKeyRequestBody } from "../index";
import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { InboundGroupSessionData } from "../../@types/partials";
import { InboundGroupSessionData } from "../OlmDevice";
import { IEncryptedPayload } from "../aes";
/**
@@ -20,7 +20,7 @@ import { IDeviceData, IProblem, ISession, ISessionInfo, IWithheld, Mode } from "
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { ICrossSigningKey } from "../../client";
import { InboundGroupSessionData } from "../../@types/partials";
import { InboundGroupSessionData } from "../OlmDevice";
import { IEncryptedPayload } from "../aes";
/**
@@ -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)) {
+1 -1
View File
@@ -30,7 +30,7 @@ import { IRoomKeyRequestBody } from "../index";
import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { InboundGroupSessionData } from "../../@types/partials";
import { InboundGroupSessionData } from "../OlmDevice";
import { IEncryptedPayload } from "../aes";
/**
@@ -21,22 +21,48 @@ limitations under the License.
*/
import { MatrixEvent } from '../../models/event';
import { EventEmitter } from 'events';
import { logger } from '../../logger';
import { DeviceInfo } from '../deviceinfo';
import { newTimeoutError } from "./Error";
import { requestKeysDuringVerification } from "../CrossSigning";
import { KeysDuringVerification, requestKeysDuringVerification } from "../CrossSigning";
import { IVerificationChannel } from "./request/Channel";
import { MatrixClient } from "../../client";
import { VerificationRequest } from "./request/VerificationRequest";
import { ListenerMap, TypedEventEmitter } from "../../models/typed-event-emitter";
const timeoutException = new Error("Verification timed out");
export class SwitchStartEventError extends Error {
constructor(startEvent) {
constructor(public readonly startEvent: MatrixEvent) {
super();
this.startEvent = startEvent;
}
}
export class VerificationBase extends EventEmitter {
export type KeyVerifier = (keyId: string, device: DeviceInfo, keyInfo: string) => void;
export enum VerificationEvent {
Cancel = "cancel",
}
export type VerificationEventHandlerMap = {
[VerificationEvent.Cancel]: (e: Error | MatrixEvent) => void;
};
export class VerificationBase<
Events extends string,
Arguments extends ListenerMap<Events | VerificationEvent>,
> extends TypedEventEmitter<Events | VerificationEvent, Arguments, VerificationEventHandlerMap> {
private cancelled = false;
private _done = false;
private promise: Promise<void> = null;
private transactionTimeoutTimer: number = null;
protected expectedEvent: string;
private resolve: () => void;
private reject: (e: Error | MatrixEvent) => void;
private resolveEvent: (e: MatrixEvent) => void;
private rejectEvent: (e: Error) => void;
private started: boolean;
/**
* Base class for verification methods.
*
@@ -64,22 +90,18 @@ export class VerificationBase extends EventEmitter {
* @param {object} [request] the key verification request object related to
* this verification, if any
*/
constructor(channel, baseApis, userId, deviceId, startEvent, request) {
constructor(
public readonly channel: IVerificationChannel,
public readonly baseApis: MatrixClient,
public readonly userId: string,
public readonly deviceId: string,
public startEvent: MatrixEvent,
public readonly request: VerificationRequest,
) {
super();
this._channel = channel;
this._baseApis = baseApis;
this.userId = userId;
this.deviceId = deviceId;
this.startEvent = startEvent;
this.request = request;
this.cancelled = false;
this._done = false;
this._promise = null;
this._transactionTimeoutTimer = null;
}
get initiatedByMe() {
public get initiatedByMe(): boolean {
// if there is no start event yet,
// we probably want to send it,
// which happens if we initiate
@@ -88,16 +110,20 @@ export class VerificationBase extends EventEmitter {
}
const sender = this.startEvent.getSender();
const content = this.startEvent.getContent();
return sender === this._baseApis.getUserId() &&
content.from_device === this._baseApis.getDeviceId();
return sender === this.baseApis.getUserId() &&
content.from_device === this.baseApis.getDeviceId();
}
_resetTimer() {
public get hasBeenCancelled(): boolean {
return this.cancelled;
}
private resetTimer(): void {
logger.info("Refreshing/starting the verification transaction timeout timer");
if (this._transactionTimeoutTimer !== null) {
clearTimeout(this._transactionTimeoutTimer);
if (this.transactionTimeoutTimer !== null) {
clearTimeout(this.transactionTimeoutTimer);
}
this._transactionTimeoutTimer = setTimeout(() => {
this.transactionTimeoutTimer = setTimeout(() => {
if (!this._done && !this.cancelled) {
logger.info("Triggering verification timeout");
this.cancel(timeoutException);
@@ -105,18 +131,18 @@ export class VerificationBase extends EventEmitter {
}, 10 * 60 * 1000); // 10 minutes
}
_endTimer() {
if (this._transactionTimeoutTimer !== null) {
clearTimeout(this._transactionTimeoutTimer);
this._transactionTimeoutTimer = null;
private endTimer(): void {
if (this.transactionTimeoutTimer !== null) {
clearTimeout(this.transactionTimeoutTimer);
this.transactionTimeoutTimer = null;
}
}
_send(type, uncompletedContent) {
return this._channel.send(type, uncompletedContent);
protected send(type: string, uncompletedContent: Record<string, any>): Promise<void> {
return this.channel.send(type, uncompletedContent);
}
_waitForEvent(type) {
protected waitForEvent(type: string): Promise<MatrixEvent> {
if (this._done) {
return Promise.reject(new Error("Verification is already done"));
}
@@ -125,24 +151,24 @@ export class VerificationBase extends EventEmitter {
return Promise.resolve(existingEvent);
}
this._expectedEvent = type;
this.expectedEvent = type;
return new Promise((resolve, reject) => {
this._resolveEvent = resolve;
this._rejectEvent = reject;
this.resolveEvent = resolve;
this.rejectEvent = reject;
});
}
canSwitchStartEvent() {
public canSwitchStartEvent(event: MatrixEvent): boolean {
return false;
}
switchStartEvent(event) {
public switchStartEvent(event: MatrixEvent): void {
if (this.canSwitchStartEvent(event)) {
logger.log("Verification Base: switching verification start event",
{ restartingFlow: !!this._rejectEvent });
if (this._rejectEvent) {
const reject = this._rejectEvent;
this._rejectEvent = undefined;
{ restartingFlow: !!this.rejectEvent });
if (this.rejectEvent) {
const reject = this.rejectEvent;
this.rejectEvent = undefined;
reject(new SwitchStartEventError(event));
} else {
this.startEvent = event;
@@ -150,21 +176,21 @@ export class VerificationBase extends EventEmitter {
}
}
handleEvent(e) {
public handleEvent(e: MatrixEvent): void {
if (this._done) {
return;
} else if (e.getType() === this._expectedEvent) {
} else if (e.getType() === this.expectedEvent) {
// if we receive an expected m.key.verification.done, then just
// ignore it, since we don't need to do anything about it
if (this._expectedEvent !== "m.key.verification.done") {
this._expectedEvent = undefined;
this._rejectEvent = undefined;
this._resetTimer();
this._resolveEvent(e);
if (this.expectedEvent !== "m.key.verification.done") {
this.expectedEvent = undefined;
this.rejectEvent = undefined;
this.resetTimer();
this.resolveEvent(e);
}
} else if (e.getType() === "m.key.verification.cancel") {
const reject = this._reject;
this._reject = undefined;
const reject = this.reject;
this.reject = undefined;
// there is only promise to reject if verify has been called
if (reject) {
const content = e.getContent();
@@ -172,36 +198,36 @@ export class VerificationBase extends EventEmitter {
reject(new Error(`Other side cancelled verification ` +
`because ${reason} (${code})`));
}
} else if (this._expectedEvent) {
} else if (this.expectedEvent) {
// only cancel if there is an event expected.
// if there is no event expected, it means verify() wasn't called
// and we're just replaying the timeline events when syncing
// after a refresh when the events haven't been stored in the cache yet.
const exception = new Error(
"Unexpected message: expecting " + this._expectedEvent
"Unexpected message: expecting " + this.expectedEvent
+ " but got " + e.getType(),
);
this._expectedEvent = undefined;
if (this._rejectEvent) {
const reject = this._rejectEvent;
this._rejectEvent = undefined;
this.expectedEvent = undefined;
if (this.rejectEvent) {
const reject = this.rejectEvent;
this.rejectEvent = undefined;
reject(exception);
}
this.cancel(exception);
}
}
done() {
this._endTimer(); // always kill the activity timer
public done(): Promise<KeysDuringVerification | void> {
this.endTimer(); // always kill the activity timer
if (!this._done) {
this.request.onVerifierFinished();
this._resolve();
return requestKeysDuringVerification(this._baseApis, this.userId, this.deviceId);
this.resolve();
return requestKeysDuringVerification(this.baseApis, this.userId, this.deviceId);
}
}
cancel(e) {
this._endTimer(); // always kill the activity timer
public cancel(e: Error | MatrixEvent): void {
this.endTimer(); // always kill the activity timer
if (!this._done) {
this.cancelled = true;
this.request.onVerifierCancelled();
@@ -210,7 +236,7 @@ export class VerificationBase extends EventEmitter {
// cancelled by the other user)
if (e === timeoutException) {
const timeoutEvent = newTimeoutError();
this._send(timeoutEvent.getType(), timeoutEvent.getContent());
this.send(timeoutEvent.getType(), timeoutEvent.getContent());
} else if (e instanceof MatrixEvent) {
const sender = e.getSender();
if (sender !== this.userId) {
@@ -219,33 +245,33 @@ export class VerificationBase extends EventEmitter {
content.code = content.code || "m.unknown";
content.reason = content.reason || content.body
|| "Unknown reason";
this._send("m.key.verification.cancel", content);
this.send("m.key.verification.cancel", content);
} else {
this._send("m.key.verification.cancel", {
this.send("m.key.verification.cancel", {
code: "m.unknown",
reason: content.body || "Unknown reason",
});
}
}
} else {
this._send("m.key.verification.cancel", {
this.send("m.key.verification.cancel", {
code: "m.unknown",
reason: e.toString(),
});
}
}
if (this._promise !== null) {
if (this.promise !== null) {
// when we cancel without a promise, we end up with a promise
// but no reject function. If cancel is called again, we'd error.
if (this._reject) this._reject(e);
if (this.reject) this.reject(e);
} else {
// FIXME: this causes an "Uncaught promise" console message
// if nothing ends up chaining this promise.
this._promise = Promise.reject(e);
this.promise = Promise.reject(e);
}
// Also emit a 'cancel' event that the app can listen for to detect cancellation
// before calling verify()
this.emit('cancel', e);
this.emit(VerificationEvent.Cancel, e);
}
}
@@ -255,31 +281,32 @@ export class VerificationBase extends EventEmitter {
* @returns {Promise} Promise which resolves when the verification has
* completed.
*/
verify() {
if (this._promise) return this._promise;
public verify(): Promise<void> {
if (this.promise) return this.promise;
this._promise = new Promise((resolve, reject) => {
this._resolve = (...args) => {
this.promise = new Promise((resolve, reject) => {
this.resolve = (...args) => {
this._done = true;
this._endTimer();
this.endTimer();
resolve(...args);
};
this._reject = (...args) => {
this.reject = (e: Error) => {
this._done = true;
this._endTimer();
reject(...args);
this.endTimer();
reject(e);
};
});
if (this._doVerification && !this._started) {
this._started = true;
this._resetTimer(); // restart the timeout
Promise.resolve(this._doVerification())
.then(this.done.bind(this), this.cancel.bind(this));
if (this.doVerification && !this.started) {
this.started = true;
this.resetTimer(); // restart the timeout
Promise.resolve(this.doVerification()).then(this.done.bind(this), this.cancel.bind(this));
}
return this._promise;
return this.promise;
}
async _verifyKeys(userId, keys, verifier) {
protected doVerification?: () => Promise<void>;
protected async verifyKeys(userId: string, keys: Record<string, string>, verifier: KeyVerifier): Promise<void> {
// we try to verify all the keys that we're told about, but we might
// not know about all of them, so keep track of the keys that we know
// about, and ignore the rest
@@ -287,15 +314,14 @@ export class VerificationBase extends EventEmitter {
for (const [keyId, keyInfo] of Object.entries(keys)) {
const deviceId = keyId.split(':', 2)[1];
const device = this._baseApis.getStoredDevice(userId, deviceId);
const device = this.baseApis.getStoredDevice(userId, deviceId);
if (device) {
await verifier(keyId, device, keyInfo);
verifier(keyId, device, keyInfo);
verifiedDevices.push(deviceId);
} else {
const crossSigningInfo = this._baseApis.crypto.deviceList
.getStoredCrossSigningForUser(userId);
const crossSigningInfo = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(userId);
if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
await verifier(keyId, DeviceInfo.fromStorage({
verifier(keyId, DeviceInfo.fromStorage({
keys: {
[keyId]: deviceId,
},
@@ -323,7 +349,11 @@ export class VerificationBase extends EventEmitter {
// to upload each signature in a separate API call which is silly because the
// API supports as many signatures as you like.
for (const deviceId of verifiedDevices) {
await this._baseApis.setDeviceVerified(userId, deviceId);
await this.baseApis.setDeviceVerified(userId, deviceId);
}
}
public get events(): string[] | undefined {
return undefined;
}
}
@@ -1,5 +1,5 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2018 - 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.
@@ -22,17 +22,17 @@ limitations under the License.
import { MatrixEvent } from "../../models/event";
export function newVerificationError(code, reason, extradata) {
const content = Object.assign({}, { code, reason }, extradata);
export function newVerificationError(code: string, reason: string, extraData: Record<string, any>): MatrixEvent {
const content = Object.assign({}, { code, reason }, extraData);
return new MatrixEvent({
type: "m.key.verification.cancel",
content,
});
}
export function errorFactory(code, reason) {
return function(extradata) {
return newVerificationError(code, reason, extradata);
export function errorFactory(code: string, reason: string): (extraData?: Record<string, any>) => MatrixEvent {
return function(extraData?: Record<string, any>) {
return newVerificationError(code, reason, extraData);
};
}
@@ -84,7 +84,7 @@ export const newInvalidMessageError = errorFactory(
"m.invalid_message", "Invalid message",
);
export function errorFromEvent(event) {
export function errorFromEvent(event: MatrixEvent): { code: string, reason: string } {
const content = event.getContent();
if (content) {
const { code, reason } = content;
@@ -20,24 +20,36 @@ limitations under the License.
* @module crypto/verification/IllegalMethod
*/
import { VerificationBase as Base } from "./Base";
import { VerificationBase as Base, VerificationEvent, VerificationEventHandlerMap } from "./Base";
import { IVerificationChannel } from "./request/Channel";
import { MatrixClient } from "../../client";
import { MatrixEvent } from "../../models/event";
import { VerificationRequest } from "./request/VerificationRequest";
/**
* @class crypto/verification/IllegalMethod/IllegalMethod
* @extends {module:crypto/verification/Base}
*/
export class IllegalMethod extends Base {
static factory(...args) {
return new IllegalMethod(...args);
export class IllegalMethod extends Base<VerificationEvent, VerificationEventHandlerMap> {
public static factory(
channel: IVerificationChannel,
baseApis: MatrixClient,
userId: string,
deviceId: string,
startEvent: MatrixEvent,
request: VerificationRequest,
): IllegalMethod {
return new IllegalMethod(channel, baseApis, userId, deviceId, startEvent, request);
}
static get NAME() {
// eslint-disable-next-line @typescript-eslint/naming-convention
public static get NAME(): string {
// Typically the name will be something else, but to complete
// the contract we offer a default one here.
return "org.matrix.illegal_method";
}
async _doVerification() {
protected doVerification = async (): Promise<void> => {
throw new Error("Verification is not possible with this method");
}
};
}
@@ -1,6 +1,5 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2018 - 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,31 +19,55 @@ limitations under the License.
* @module crypto/verification/QRCode
*/
import { VerificationBase as Base } from "./Base";
import {
newKeyMismatchError,
newUserCancelledError,
} from './Error';
import { encodeUnpaddedBase64, decodeBase64 } from "../olmlib";
import { VerificationBase as Base, VerificationEventHandlerMap } from "./Base";
import { newKeyMismatchError, newUserCancelledError } from './Error';
import { decodeBase64, encodeUnpaddedBase64 } from "../olmlib";
import { logger } from '../../logger';
import { VerificationRequest } from "./request/VerificationRequest";
import { MatrixClient } from "../../client";
import { IVerificationChannel } from "./request/Channel";
import { MatrixEvent } from "../../models/event";
export const SHOW_QR_CODE_METHOD = "m.qr_code.show.v1";
export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1";
interface IReciprocateQr {
confirm(): void;
cancel(): void;
}
export enum QrCodeEvent {
ShowReciprocateQr = "show_reciprocate_qr",
}
type EventHandlerMap = {
[QrCodeEvent.ShowReciprocateQr]: (qr: IReciprocateQr) => void;
} & VerificationEventHandlerMap;
/**
* @class crypto/verification/QRCode/ReciprocateQRCode
* @extends {module:crypto/verification/Base}
*/
export class ReciprocateQRCode extends Base {
static factory(...args) {
return new ReciprocateQRCode(...args);
export class ReciprocateQRCode extends Base<QrCodeEvent, EventHandlerMap> {
public reciprocateQREvent: IReciprocateQr;
public static factory(
channel: IVerificationChannel,
baseApis: MatrixClient,
userId: string,
deviceId: string,
startEvent: MatrixEvent,
request: VerificationRequest,
): ReciprocateQRCode {
return new ReciprocateQRCode(channel, baseApis, userId, deviceId, startEvent, request);
}
static get NAME() {
// eslint-disable-next-line @typescript-eslint/naming-convention
public static get NAME(): string {
return "m.reciprocate.v1";
}
async _doVerification() {
protected doVerification = async (): Promise<void> => {
if (!this.startEvent) {
// TODO: Support scanning QR codes
throw new Error("It is not currently possible to start verification" +
@@ -58,30 +81,30 @@ export class ReciprocateQRCode extends Base {
}
// 2. ask if other user shows shield as well
await new Promise((resolve, reject) => {
await new Promise<void>((resolve, reject) => {
this.reciprocateQREvent = {
confirm: resolve,
cancel: () => reject(newUserCancelledError()),
};
this.emit("show_reciprocate_qr", this.reciprocateQREvent);
this.emit(QrCodeEvent.ShowReciprocateQr, this.reciprocateQREvent);
});
// 3. determine key to sign / mark as trusted
const keys = {};
const keys: Record<string, string> = {};
switch (qrCodeData.mode) {
case MODE_VERIFY_OTHER_USER: {
case Mode.VerifyOtherUser: {
// add master key to keys to be signed, only if we're not doing self-verification
const masterKey = qrCodeData.otherUserMasterKey;
keys[`ed25519:${masterKey}`] = masterKey;
break;
}
case MODE_VERIFY_SELF_TRUSTED: {
case Mode.VerifySelfTrusted: {
const deviceId = this.request.targetDevice.deviceId;
keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey;
break;
}
case MODE_VERIFY_SELF_UNTRUSTED: {
case Mode.VerifySelfUntrusted: {
const masterKey = qrCodeData.myMasterKey;
keys[`ed25519:${masterKey}`] = masterKey;
break;
@@ -89,7 +112,7 @@ export class ReciprocateQRCode extends Base {
}
// 4. sign the key (or mark own MSK as verified in case of MODE_VERIFY_SELF_TRUSTED)
await this._verifyKeys(this.userId, keys, (keyId, device, keyInfo) => {
await this.verifyKeys(this.userId, keys, (keyId, device, keyInfo) => {
// make sure the device has the expected keys
const targetKey = keys[keyId];
if (!targetKey) throw newKeyMismatchError();
@@ -108,103 +131,88 @@ export class ReciprocateQRCode extends Base {
}
}
});
}
};
}
const CODE_VERSION = 0x02; // the version of binary QR codes we support
const BINARY_PREFIX = "MATRIX"; // ASCII, used to prefix the binary format
const MODE_VERIFY_OTHER_USER = 0x00; // Verifying someone who isn't us
const MODE_VERIFY_SELF_TRUSTED = 0x01; // We trust the master key
const MODE_VERIFY_SELF_UNTRUSTED = 0x02; // We do not trust the master key
enum Mode {
VerifyOtherUser = 0x00, // Verifying someone who isn't us
VerifySelfTrusted = 0x01, // We trust the master key
VerifySelfUntrusted = 0x02, // We do not trust the master key
}
interface IQrData {
prefix: string;
version: number;
mode: Mode;
transactionId: string;
firstKeyB64: string;
secondKeyB64: string;
secretB64: string;
}
export class QRCodeData {
constructor(
mode, sharedSecret, otherUserMasterKey,
otherDeviceKey, myMasterKey, buffer,
) {
this._sharedSecret = sharedSecret;
this._mode = mode;
this._otherUserMasterKey = otherUserMasterKey;
this._otherDeviceKey = otherDeviceKey;
this._myMasterKey = myMasterKey;
this._buffer = buffer;
}
public readonly mode: Mode,
private readonly sharedSecret: string,
// only set when mode is MODE_VERIFY_OTHER_USER, master key of other party at time of generating QR code
public readonly otherUserMasterKey: string | undefined,
// only set when mode is MODE_VERIFY_SELF_TRUSTED, device key of other party at time of generating QR code
public readonly otherDeviceKey: string | undefined,
// only set when mode is MODE_VERIFY_SELF_UNTRUSTED, own master key at time of generating QR code
public readonly myMasterKey: string | undefined,
private readonly buffer: Buffer,
) {}
static async create(request, client) {
const sharedSecret = QRCodeData._generateSharedSecret();
const mode = QRCodeData._determineMode(request, client);
public static async create(request: VerificationRequest, client: MatrixClient): Promise<QRCodeData> {
const sharedSecret = QRCodeData.generateSharedSecret();
const mode = QRCodeData.determineMode(request, client);
let otherUserMasterKey = null;
let otherDeviceKey = null;
let myMasterKey = null;
if (mode === MODE_VERIFY_OTHER_USER) {
if (mode === Mode.VerifyOtherUser) {
const otherUserCrossSigningInfo =
client.getStoredCrossSigningForUser(request.otherUserId);
otherUserMasterKey = otherUserCrossSigningInfo.getId("master");
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
otherDeviceKey = await QRCodeData._getOtherDeviceKey(request, client);
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) {
} else if (mode === Mode.VerifySelfTrusted) {
otherDeviceKey = await QRCodeData.getOtherDeviceKey(request, client);
} else if (mode === Mode.VerifySelfUntrusted) {
const myUserId = client.getUserId();
const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
myMasterKey = myCrossSigningInfo.getId("master");
}
const qrData = QRCodeData._generateQrData(
const qrData = QRCodeData.generateQrData(
request, client, mode,
sharedSecret,
otherUserMasterKey,
otherDeviceKey,
myMasterKey,
);
const buffer = QRCodeData._generateBuffer(qrData);
const buffer = QRCodeData.generateBuffer(qrData);
return new QRCodeData(mode, sharedSecret,
otherUserMasterKey, otherDeviceKey, myMasterKey, buffer);
}
get buffer() {
return this._buffer;
}
get mode() {
return this._mode;
}
/**
* only set when mode is MODE_VERIFY_SELF_TRUSTED
* @return {string} device key of other party at time of generating QR code
*/
get otherDeviceKey() {
return this._otherDeviceKey;
}
/**
* only set when mode is MODE_VERIFY_OTHER_USER
* @return {string} master key of other party at time of generating QR code
*/
get otherUserMasterKey() {
return this._otherUserMasterKey;
}
/**
* only set when mode is MODE_VERIFY_SELF_UNTRUSTED
* @return {string} own master key at time of generating QR code
*/
get myMasterKey() {
return this._myMasterKey;
}
/**
* The unpadded base64 encoded shared secret.
*/
get encodedSharedSecret() {
return this._sharedSecret;
public get encodedSharedSecret(): string {
return this.sharedSecret;
}
static _generateSharedSecret() {
public getBuffer(): Buffer {
return this.buffer;
}
private static generateSharedSecret(): string {
const secretBytes = new Uint8Array(11);
global.crypto.getRandomValues(secretBytes);
return encodeUnpaddedBase64(secretBytes);
}
static async _getOtherDeviceKey(request, client) {
private static async getOtherDeviceKey(request: VerificationRequest, client: MatrixClient): Promise<string> {
const myUserId = client.getUserId();
const otherDevice = request.targetDevice;
const otherDeviceId = otherDevice ? otherDevice.deviceId : null;
@@ -212,31 +220,35 @@ export class QRCodeData {
if (!device) {
throw new Error("could not find device " + otherDeviceId);
}
const key = device.getFingerprint();
return key;
return device.getFingerprint();
}
static _determineMode(request, client) {
private static determineMode(request: VerificationRequest, client: MatrixClient): Mode {
const myUserId = client.getUserId();
const otherUserId = request.otherUserId;
let mode = MODE_VERIFY_OTHER_USER;
let mode = Mode.VerifyOtherUser;
if (myUserId === otherUserId) {
// Mode changes depending on whether or not we trust the master cross signing key
const myTrust = client.checkUserTrust(myUserId);
if (myTrust.isCrossSigningVerified()) {
mode = MODE_VERIFY_SELF_TRUSTED;
mode = Mode.VerifySelfTrusted;
} else {
mode = MODE_VERIFY_SELF_UNTRUSTED;
mode = Mode.VerifySelfUntrusted;
}
}
return mode;
}
static _generateQrData(request, client, mode,
encodedSharedSecret, otherUserMasterKey,
otherDeviceKey, myMasterKey,
) {
private static generateQrData(
request: VerificationRequest,
client: MatrixClient,
mode: Mode,
encodedSharedSecret: string,
otherUserMasterKey: string,
otherDeviceKey: string,
myMasterKey: string,
): IQrData {
const myUserId = client.getUserId();
const transactionId = request.channel.transactionId;
const qrData = {
@@ -251,16 +263,16 @@ export class QRCodeData {
const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
if (mode === MODE_VERIFY_OTHER_USER) {
if (mode === Mode.VerifyOtherUser) {
// First key is our master cross signing key
qrData.firstKeyB64 = myCrossSigningInfo.getId("master");
// Second key is the other user's master cross signing key
qrData.secondKeyB64 = otherUserMasterKey;
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
} else if (mode === Mode.VerifySelfTrusted) {
// First key is our master cross signing key
qrData.firstKeyB64 = myCrossSigningInfo.getId("master");
qrData.secondKeyB64 = otherDeviceKey;
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) {
} else if (mode === Mode.VerifySelfUntrusted) {
// First key is our device's key
qrData.firstKeyB64 = client.getDeviceEd25519Key();
// Second key is what we think our master cross signing key is
@@ -269,7 +281,7 @@ export class QRCodeData {
return qrData;
}
static _generateBuffer(qrData) {
private static generateBuffer(qrData: IQrData): Buffer {
let buf = Buffer.alloc(0); // we'll concat our way through life
const appendByte = (b) => {
@@ -1,5 +1,5 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2018 - 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.
@@ -19,8 +19,10 @@ limitations under the License.
* @module crypto/verification/SAS
*/
import { VerificationBase as Base, SwitchStartEventError } from "./Base";
import anotherjson from 'another-json';
import { Utility, SAS as OlmSAS } from "@matrix-org/olm";
import { VerificationBase as Base, SwitchStartEventError, VerificationEventHandlerMap } from "./Base";
import {
errorFactory,
newInvalidMessageError,
@@ -29,6 +31,7 @@ import {
newUserCancelledError,
} from './Error';
import { logger } from '../../logger';
import { IContent, MatrixEvent } from "../../models/event";
const START_TYPE = "m.key.verification.start";
@@ -38,7 +41,7 @@ const EVENTS = [
"m.key.verification.mac",
];
let olmutil;
let olmutil: Utility;
const newMismatchedSASError = errorFactory(
"m.mismatched_sas", "Mismatched short authentication string",
@@ -48,7 +51,7 @@ const newMismatchedCommitmentError = errorFactory(
"m.mismatched_commitment", "Mismatched commitment",
);
function generateDecimalSas(sasBytes) {
function generateDecimalSas(sasBytes: number[]): [number, number, number] {
/**
* +--------+--------+--------+--------+--------+
* | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 |
@@ -64,7 +67,9 @@ function generateDecimalSas(sasBytes) {
];
}
const emojiMapping = [
type EmojiMapping = [emoji: string, name: string];
const emojiMapping: EmojiMapping[] = [
["🐶", "dog"], // 0
["🐱", "cat"], // 1
["🦁", "lion"], // 2
@@ -131,7 +136,7 @@ const emojiMapping = [
["📌", "pin"], // 63
];
function generateEmojiSas(sasBytes) {
function generateEmojiSas(sasBytes: number[]): EmojiMapping[] {
const emojis = [
// just like base64 encoding
sasBytes[0] >> 2,
@@ -151,8 +156,20 @@ const sasGenerators = {
emoji: generateEmojiSas,
};
function generateSas(sasBytes, methods) {
const sas = {};
export interface IGeneratedSas {
decimal?: [number, number, number];
emoji?: EmojiMapping[];
}
export interface ISasEvent {
sas: IGeneratedSas;
confirm(): Promise<void>;
cancel(): void;
mismatch(): void;
}
function generateSas(sasBytes: number[], methods: string[]): IGeneratedSas {
const sas: IGeneratedSas = {};
for (const method of methods) {
if (method in sasGenerators) {
sas[method] = sasGenerators[method](sasBytes);
@@ -166,7 +183,7 @@ const macMethods = {
"hmac-sha256": "calculate_mac_long_kdf",
};
function calculateMAC(olmSAS, method) {
function calculateMAC(olmSAS: OlmSAS, method: string) {
return function(...args) {
const macFunction = olmSAS[macMethods[method]];
const mac = macFunction.apply(olmSAS, args);
@@ -176,23 +193,23 @@ function calculateMAC(olmSAS, method) {
}
const calculateKeyAgreement = {
"curve25519-hkdf-sha256": function(sas, olmSAS, bytes) {
const ourInfo = `${sas._baseApis.getUserId()}|${sas._baseApis.deviceId}|`
"curve25519-hkdf-sha256": function(sas: SAS, olmSAS: OlmSAS, bytes: number): Uint8Array {
const ourInfo = `${sas.baseApis.getUserId()}|${sas.baseApis.deviceId}|`
+ `${sas.ourSASPubKey}|`;
const theirInfo = `${sas.userId}|${sas.deviceId}|${sas.theirSASPubKey}|`;
const sasInfo =
"MATRIX_KEY_VERIFICATION_SAS|"
+ (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo)
+ sas._channel.transactionId;
+ sas.channel.transactionId;
return olmSAS.generate_bytes(sasInfo, bytes);
},
"curve25519": function(sas, olmSAS, bytes) {
const ourInfo = `${sas._baseApis.getUserId()}${sas._baseApis.deviceId}`;
"curve25519": function(sas: SAS, olmSAS: OlmSAS, bytes: number): Uint8Array {
const ourInfo = `${sas.baseApis.getUserId()}${sas.baseApis.deviceId}`;
const theirInfo = `${sas.userId}${sas.deviceId}`;
const sasInfo =
"MATRIX_KEY_VERIFICATION_SAS"
+ (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo)
+ sas._channel.transactionId;
+ sas.channel.transactionId;
return olmSAS.generate_bytes(sasInfo, bytes);
},
};
@@ -211,37 +228,51 @@ const HASHES_SET = new Set(HASHES_LIST);
const MAC_SET = new Set(MAC_LIST);
const SAS_SET = new Set(SAS_LIST);
function intersection(anArray, aSet) {
function intersection<T>(anArray: T[], aSet: Set<T>): T[] {
return anArray instanceof Array ? anArray.filter(x => aSet.has(x)) : [];
}
export enum SasEvent {
ShowSas = "show_sas",
}
type EventHandlerMap = {
[SasEvent.ShowSas]: (sas: ISasEvent) => void;
} & VerificationEventHandlerMap;
/**
* @alias module:crypto/verification/SAS
* @extends {module:crypto/verification/Base}
*/
export class SAS extends Base {
static get NAME() {
export class SAS extends Base<SasEvent, EventHandlerMap> {
private waitingForAccept: boolean;
public ourSASPubKey: string;
public theirSASPubKey: string;
public sasEvent: ISasEvent;
// eslint-disable-next-line @typescript-eslint/naming-convention
public static get NAME(): string {
return "m.sas.v1";
}
get events() {
public get events(): string[] {
return EVENTS;
}
async _doVerification() {
protected doVerification = async (): Promise<void> => {
await global.Olm.init();
olmutil = olmutil || new global.Olm.Utility();
// make sure user's keys are downloaded
await this._baseApis.downloadKeys([this.userId]);
await this.baseApis.downloadKeys([this.userId]);
let retry = false;
do {
try {
if (this.initiatedByMe) {
return await this._doSendVerification();
return await this.doSendVerification();
} else {
return await this._doRespondVerification();
return await this.doRespondVerification();
}
} catch (err) {
if (err instanceof SwitchStartEventError) {
@@ -253,38 +284,37 @@ export class SAS extends Base {
}
}
} while (retry);
}
};
canSwitchStartEvent(event) {
public canSwitchStartEvent(event: MatrixEvent): boolean {
if (event.getType() !== START_TYPE) {
return false;
}
const content = event.getContent();
return content && content.method === SAS.NAME &&
this._waitingForAccept;
return content && content.method === SAS.NAME && this.waitingForAccept;
}
async _sendStart() {
const startContent = this._channel.completeContent(START_TYPE, {
private async sendStart(): Promise<Record<string, any>> {
const startContent = this.channel.completeContent(START_TYPE, {
method: SAS.NAME,
from_device: this._baseApis.deviceId,
from_device: this.baseApis.deviceId,
key_agreement_protocols: KEY_AGREEMENT_LIST,
hashes: HASHES_LIST,
message_authentication_codes: MAC_LIST,
// FIXME: allow app to specify what SAS methods can be used
short_authentication_string: SAS_LIST,
});
await this._channel.sendCompleted(START_TYPE, startContent);
await this.channel.sendCompleted(START_TYPE, startContent);
return startContent;
}
async _doSendVerification() {
this._waitingForAccept = true;
private async doSendVerification(): Promise<void> {
this.waitingForAccept = true;
let startContent;
if (this.startEvent) {
startContent = this._channel.completedContentFromEvent(this.startEvent);
startContent = this.channel.completedContentFromEvent(this.startEvent);
} else {
startContent = await this._sendStart();
startContent = await this.sendStart();
}
// we might have switched to a different start event,
@@ -297,9 +327,9 @@ export class SAS extends Base {
let e;
try {
e = await this._waitForEvent("m.key.verification.accept");
e = await this.waitForEvent("m.key.verification.accept");
} finally {
this._waitingForAccept = false;
this.waitingForAccept = false;
}
let content = e.getContent();
const sasMethods
@@ -319,11 +349,11 @@ export class SAS extends Base {
const olmSAS = new global.Olm.SAS();
try {
this.ourSASPubKey = olmSAS.get_pubkey();
await this._send("m.key.verification.key", {
await this.send("m.key.verification.key", {
key: this.ourSASPubKey,
});
e = await this._waitForEvent("m.key.verification.key");
e = await this.waitForEvent("m.key.verification.key");
// FIXME: make sure event is properly formed
content = e.getContent();
const commitmentStr = content.key + anotherjson.stringify(startContent);
@@ -335,12 +365,12 @@ export class SAS extends Base {
olmSAS.set_their_key(content.key);
const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
const verifySAS = new Promise((resolve, reject) => {
const verifySAS = new Promise<void>((resolve, reject) => {
this.sasEvent = {
sas: generateSas(sasBytes, sasMethods),
confirm: async () => {
try {
await this._sendMAC(olmSAS, macMethod);
await this.sendMAC(olmSAS, macMethod);
resolve();
} catch (err) {
reject(err);
@@ -349,58 +379,49 @@ export class SAS extends Base {
cancel: () => reject(newUserCancelledError()),
mismatch: () => reject(newMismatchedSASError()),
};
this.emit("show_sas", this.sasEvent);
this.emit(SasEvent.ShowSas, this.sasEvent);
});
[e] = await Promise.all([
this._waitForEvent("m.key.verification.mac")
this.waitForEvent("m.key.verification.mac")
.then((e) => {
// we don't expect any more messages from the other
// party, and they may send a m.key.verification.done
// when they're done on their end
this._expectedEvent = "m.key.verification.done";
this.expectedEvent = "m.key.verification.done";
return e;
}),
verifySAS,
]);
content = e.getContent();
await this._checkMAC(olmSAS, content, macMethod);
await this.checkMAC(olmSAS, content, macMethod);
} finally {
olmSAS.free();
}
}
async _doRespondVerification() {
private async doRespondVerification(): Promise<void> {
// as m.related_to is not included in the encrypted content in e2e rooms,
// we need to make sure it is added
let content = this._channel.completedContentFromEvent(this.startEvent);
let content = this.channel.completedContentFromEvent(this.startEvent);
// Note: we intersect using our pre-made lists, rather than the sets,
// so that the result will be in our order of preference. Then
// fetching the first element from the array will give our preferred
// method out of the ones offered by the other party.
const keyAgreement
= intersection(
KEY_AGREEMENT_LIST, new Set(content.key_agreement_protocols),
)[0];
const hashMethod
= intersection(HASHES_LIST, new Set(content.hashes))[0];
const macMethod
= intersection(MAC_LIST, new Set(content.message_authentication_codes))[0];
const keyAgreement = intersection(KEY_AGREEMENT_LIST, new Set(content.key_agreement_protocols))[0];
const hashMethod = intersection(HASHES_LIST, new Set(content.hashes))[0];
const macMethod = intersection(MAC_LIST, new Set(content.message_authentication_codes))[0];
// FIXME: allow app to specify what SAS methods can be used
const sasMethods
= intersection(content.short_authentication_string, SAS_SET);
if (!(keyAgreement !== undefined
&& hashMethod !== undefined
&& macMethod !== undefined
&& sasMethods.length)) {
const sasMethods = intersection(content.short_authentication_string, SAS_SET);
if (!(keyAgreement !== undefined && hashMethod !== undefined && macMethod !== undefined && sasMethods.length)) {
throw newUnknownMethodError();
}
const olmSAS = new global.Olm.SAS();
try {
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
await this._send("m.key.verification.accept", {
await this.send("m.key.verification.accept", {
key_agreement_protocol: keyAgreement,
hash: hashMethod,
message_authentication_code: macMethod,
@@ -409,23 +430,23 @@ export class SAS extends Base {
commitment: olmutil.sha256(commitmentStr),
});
let e = await this._waitForEvent("m.key.verification.key");
let e = await this.waitForEvent("m.key.verification.key");
// FIXME: make sure event is properly formed
content = e.getContent();
this.theirSASPubKey = content.key;
olmSAS.set_their_key(content.key);
this.ourSASPubKey = olmSAS.get_pubkey();
await this._send("m.key.verification.key", {
await this.send("m.key.verification.key", {
key: this.ourSASPubKey,
});
const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
const verifySAS = new Promise((resolve, reject) => {
const verifySAS = new Promise<void>((resolve, reject) => {
this.sasEvent = {
sas: generateSas(sasBytes, sasMethods),
confirm: async () => {
try {
await this._sendMAC(olmSAS, macMethod);
await this.sendMAC(olmSAS, macMethod);
resolve();
} catch (err) {
reject(err);
@@ -434,43 +455,43 @@ export class SAS extends Base {
cancel: () => reject(newUserCancelledError()),
mismatch: () => reject(newMismatchedSASError()),
};
this.emit("show_sas", this.sasEvent);
this.emit(SasEvent.ShowSas, this.sasEvent);
});
[e] = await Promise.all([
this._waitForEvent("m.key.verification.mac")
this.waitForEvent("m.key.verification.mac")
.then((e) => {
// we don't expect any more messages from the other
// party, and they may send a m.key.verification.done
// when they're done on their end
this._expectedEvent = "m.key.verification.done";
this.expectedEvent = "m.key.verification.done";
return e;
}),
verifySAS,
]);
content = e.getContent();
await this._checkMAC(olmSAS, content, macMethod);
await this.checkMAC(olmSAS, content, macMethod);
} finally {
olmSAS.free();
}
}
_sendMAC(olmSAS, method) {
private sendMAC(olmSAS: OlmSAS, method: string): Promise<void> {
const mac = {};
const keyList = [];
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this.baseApis.getUserId() + this.baseApis.deviceId
+ this.userId + this.deviceId
+ this._channel.transactionId;
+ this.channel.transactionId;
const deviceKeyId = `ed25519:${this._baseApis.deviceId}`;
const deviceKeyId = `ed25519:${this.baseApis.deviceId}`;
mac[deviceKeyId] = calculateMAC(olmSAS, method)(
this._baseApis.getDeviceEd25519Key(),
this.baseApis.getDeviceEd25519Key(),
baseInfo + deviceKeyId,
);
keyList.push(deviceKeyId);
const crossSigningId = this._baseApis.getCrossSigningId();
const crossSigningId = this.baseApis.getCrossSigningId();
if (crossSigningId) {
const crossSigningKeyId = `ed25519:${crossSigningId}`;
mac[crossSigningKeyId] = calculateMAC(olmSAS, method)(
@@ -484,14 +505,14 @@ export class SAS extends Base {
keyList.sort().join(","),
baseInfo + "KEY_IDS",
);
return this._send("m.key.verification.mac", { mac, keys });
return this.send("m.key.verification.mac", { mac, keys });
}
async _checkMAC(olmSAS, content, method) {
private async checkMAC(olmSAS: OlmSAS, content: IContent, method: string): Promise<void> {
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
+ this.userId + this.deviceId
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this._channel.transactionId;
+ this.baseApis.getUserId() + this.baseApis.deviceId
+ this.channel.transactionId;
if (content.keys !== calculateMAC(olmSAS, method)(
Object.keys(content.mac).sort().join(","),
@@ -500,7 +521,7 @@ export class SAS extends Base {
throw newKeyMismatchError();
}
await this._verifyKeys(this.userId, content.mac, (keyId, device, keyInfo) => {
await this.verifyKeys(this.userId, content.mac, (keyId, device, keyInfo) => {
if (keyInfo !== calculateMAC(olmSAS, method)(
device.keys[keyId],
baseInfo + keyId,
@@ -0,0 +1,34 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixEvent } from "../../../models/event";
import { VerificationRequest } from "./VerificationRequest";
export interface IVerificationChannel {
request?: VerificationRequest;
readonly userId: string;
readonly roomId?: string;
readonly deviceId?: string;
readonly transactionId: string;
readonly receiveStartFromOtherDevices?: boolean;
getTimestamp(event: MatrixEvent): number;
send(type: string, uncompletedContent: Record<string, any>): Promise<void>;
completeContent(type: string, content: Record<string, any>): Record<string, any>;
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>;
}
@@ -22,8 +22,13 @@ import {
START_TYPE,
} from "./VerificationRequest";
import { logger } from '../../../logger';
import { IVerificationChannel } from "./Channel";
import { EventType } from "../../../@types/event";
import { MatrixClient } from "../../../client";
import { MatrixEvent } from "../../../models/event";
import { IRequestsMap } from "../..";
const MESSAGE_TYPE = "m.room.message";
const MESSAGE_TYPE = EventType.RoomMessage;
const M_REFERENCE = "m.reference";
const M_RELATES_TO = "m.relates_to";
@@ -31,36 +36,34 @@ const M_RELATES_TO = "m.relates_to";
* A key verification channel that sends verification events in the timeline of a room.
* Uses the event id of the initial m.key.verification.request event as a transaction id.
*/
export class InRoomChannel {
export class InRoomChannel implements IVerificationChannel {
private requestEventId: string = null;
/**
* @param {MatrixClient} client the matrix client, to send messages with and get current user & device from.
* @param {string} roomId id of the room where verification events should be posted in, should be a DM with the given user.
* @param {string} userId id of user that the verification request is directed at, should be present in the room.
*/
constructor(client, roomId, userId = null) {
this._client = client;
this._roomId = roomId;
this.userId = userId;
this._requestEventId = null;
constructor(
private readonly client: MatrixClient,
public readonly roomId: string,
public userId: string = null,
) {
}
get receiveStartFromOtherDevices() {
public get receiveStartFromOtherDevices(): boolean {
return true;
}
get roomId() {
return this._roomId;
}
/** The transaction id generated/used by this verification channel */
get transactionId() {
return this._requestEventId;
public get transactionId(): string {
return this.requestEventId;
}
static getOtherPartyUserId(event, client) {
public static getOtherPartyUserId(event: MatrixEvent, client: MatrixClient): string {
const type = InRoomChannel.getEventType(event);
if (type !== REQUEST_TYPE) {
return;
return;
}
const ownUserId = client.getUserId();
const sender = event.getSender();
@@ -78,25 +81,29 @@ export class InRoomChannel {
* @param {MatrixEvent} event the event to get the timestamp of
* @return {number} the timestamp when the event was sent
*/
getTimestamp(event) {
public getTimestamp(event: MatrixEvent): number {
return event.getTs();
}
/**
* Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel
* @param {string} type the event type to check
* @returns {bool} boolean flag
* @returns {boolean} boolean flag
*/
static canCreateRequest(type) {
public static canCreateRequest(type: string): boolean {
return type === REQUEST_TYPE;
}
public canCreateRequest(type: string): boolean {
return InRoomChannel.canCreateRequest(type);
}
/**
* Extract the transaction id used by a given key verification event, if any
* @param {MatrixEvent} event the event
* @returns {string} the transaction id
*/
static getTransactionId(event) {
public static getTransactionId(event: MatrixEvent): string {
if (InRoomChannel.getEventType(event) === REQUEST_TYPE) {
return event.getId();
} else {
@@ -114,9 +121,9 @@ export class InRoomChannel {
* `handleEvent` can do more checks and choose to ignore invalid events.
* @param {MatrixEvent} event the event to validate
* @param {MatrixClient} client the client to get the current user and device id from
* @returns {bool} whether the event is valid and should be passed to handleEvent
* @returns {boolean} whether the event is valid and should be passed to handleEvent
*/
static validateEvent(event, client) {
public static validateEvent(event: MatrixEvent, client: MatrixClient): boolean {
const txnId = InRoomChannel.getTransactionId(event);
if (typeof txnId !== "string" || txnId.length === 0) {
return false;
@@ -152,7 +159,7 @@ export class InRoomChannel {
* @param {MatrixEvent} event the event to get the type of
* @returns {string} the "symbolic" event type
*/
static getEventType(event) {
public static getEventType(event: MatrixEvent): string {
const type = event.getType();
if (type === MESSAGE_TYPE) {
const content = event.getContent();
@@ -174,10 +181,10 @@ export class InRoomChannel {
* Changes the state of the channel, request, and verifier in response to a key verification event.
* @param {MatrixEvent} event to handle
* @param {VerificationRequest} request the request to forward handling to
* @param {bool} isLiveEvent whether this is an even received through sync or not
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
* @param {boolean} isLiveEvent whether this is an even received through sync or not
* @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent.
*/
async handleEvent(event, request, isLiveEvent) {
public async handleEvent(event: MatrixEvent, request: VerificationRequest, isLiveEvent = false): Promise<void> {
// prevent processing the same event multiple times, as under
// some circumstances Room.timeline can get emitted twice for the same event
if (request.hasEventId(event.getId())) {
@@ -187,18 +194,18 @@ export class InRoomChannel {
// do validations that need state (roomId, userId),
// ignore if invalid
if (event.getRoomId() !== this._roomId) {
if (event.getRoomId() !== this.roomId) {
return;
}
// set userId if not set already
if (this.userId === null) {
const userId = InRoomChannel.getOtherPartyUserId(event, this._client);
const userId = InRoomChannel.getOtherPartyUserId(event, this.client);
if (userId) {
this.userId = userId;
}
}
// ignore events not sent by us or the other party
const ownUserId = this._client.getUserId();
const ownUserId = this.client.getUserId();
const sender = event.getSender();
if (this.userId !== null) {
if (sender !== ownUserId && sender !== this.userId) {
@@ -207,12 +214,12 @@ export class InRoomChannel {
return;
}
}
if (this._requestEventId === null) {
this._requestEventId = InRoomChannel.getTransactionId(event);
if (this.requestEventId === null) {
this.requestEventId = InRoomChannel.getTransactionId(event);
}
const isRemoteEcho = !!event.getUnsigned().transaction_id;
const isSentByUs = event.getSender() === this._client.getUserId();
const isSentByUs = event.getSender() === this.client.getUserId();
return await request.handleEvent(
type, event, isLiveEvent, isRemoteEcho, isSentByUs);
@@ -226,13 +233,14 @@ export class InRoomChannel {
* @param {MatrixEvent} event the received event
* @returns {Object} the content object with the relation added again
*/
completedContentFromEvent(event) {
public completedContentFromEvent(event: MatrixEvent): Record<string, any> {
// ensure m.related_to is included in e2ee rooms
// as the field is excluded from encryption
const content = Object.assign({}, event.getContent());
content[M_RELATES_TO] = event.getRelation();
return content;
}
/**
* Add all the fields to content needed for sending it over this channel.
* This is public so verification methods (SAS uses this) can get the exact
@@ -242,15 +250,15 @@ export class InRoomChannel {
* @param {object} content the (incomplete) content
* @returns {object} the complete content, as it will be sent.
*/
completeContent(type, content) {
public completeContent(type: string, content: Record<string, any>): Record<string, any> {
content = Object.assign({}, content);
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
content.from_device = this._client.getDeviceId();
content.from_device = this.client.getDeviceId();
}
if (type === REQUEST_TYPE) {
// type is mapped to m.room.message in the send method
content = {
body: this._client.getUserId() + " is requesting to verify " +
body: this.client.getUserId() + " is requesting to verify " +
"your key, but your client does not support in-chat key " +
"verification. You will need to use legacy key " +
"verification to verify keys.",
@@ -274,7 +282,7 @@ export class InRoomChannel {
* @param {object} uncompletedContent the (incomplete) content
* @returns {Promise} the promise of the request
*/
send(type, uncompletedContent) {
public send(type: string, uncompletedContent: Record<string, any>): Promise<void> {
const content = this.completeContent(type, uncompletedContent);
return this.sendCompleted(type, content);
}
@@ -285,74 +293,68 @@ export class InRoomChannel {
* @param {object} content
* @returns {Promise} the promise of the request
*/
async sendCompleted(type, content) {
public async sendCompleted(type: string, content: Record<string, any>): Promise<void> {
let sendType = type;
if (type === REQUEST_TYPE) {
sendType = MESSAGE_TYPE;
}
const response = await this._client.sendEvent(this._roomId, sendType, content);
const response = await this.client.sendEvent(this.roomId, sendType, content);
if (type === REQUEST_TYPE) {
this._requestEventId = response.event_id;
this.requestEventId = response.event_id;
}
}
}
export class InRoomRequests {
constructor() {
this._requestsByRoomId = new Map();
}
export class InRoomRequests implements IRequestsMap {
private requestsByRoomId = new Map<string, Map<string, VerificationRequest>>();
getRequest(event) {
public getRequest(event: MatrixEvent): VerificationRequest {
const roomId = event.getRoomId();
const txnId = InRoomChannel.getTransactionId(event);
return this._getRequestByTxnId(roomId, txnId);
return this.getRequestByTxnId(roomId, txnId);
}
getRequestByChannel(channel) {
return this._getRequestByTxnId(channel.roomId, channel.transactionId);
public getRequestByChannel(channel: InRoomChannel): VerificationRequest {
return this.getRequestByTxnId(channel.roomId, channel.transactionId);
}
_getRequestByTxnId(roomId, txnId) {
const requestsByTxnId = this._requestsByRoomId.get(roomId);
private getRequestByTxnId(roomId: string, txnId: string): VerificationRequest {
const requestsByTxnId = this.requestsByRoomId.get(roomId);
if (requestsByTxnId) {
return requestsByTxnId.get(txnId);
}
}
setRequest(event, request) {
this._setRequest(
event.getRoomId(),
InRoomChannel.getTransactionId(event),
request,
);
public setRequest(event: MatrixEvent, request: VerificationRequest): void {
this.doSetRequest(event.getRoomId(), InRoomChannel.getTransactionId(event), request);
}
setRequestByChannel(channel, request) {
this._setRequest(channel.roomId, channel.transactionId, request);
public setRequestByChannel(channel: IVerificationChannel, request: VerificationRequest): void {
this.doSetRequest(channel.roomId, channel.transactionId, request);
}
_setRequest(roomId, txnId, request) {
let requestsByTxnId = this._requestsByRoomId.get(roomId);
private doSetRequest(roomId: string, txnId: string, request: VerificationRequest): void {
let requestsByTxnId = this.requestsByRoomId.get(roomId);
if (!requestsByTxnId) {
requestsByTxnId = new Map();
this._requestsByRoomId.set(roomId, requestsByTxnId);
this.requestsByRoomId.set(roomId, requestsByTxnId);
}
requestsByTxnId.set(txnId, request);
}
removeRequest(event) {
public removeRequest(event: MatrixEvent): void {
const roomId = event.getRoomId();
const requestsByTxnId = this._requestsByRoomId.get(roomId);
const requestsByTxnId = this.requestsByRoomId.get(roomId);
if (requestsByTxnId) {
requestsByTxnId.delete(InRoomChannel.getTransactionId(event));
if (requestsByTxnId.size === 0) {
this._requestsByRoomId.delete(roomId);
this.requestsByRoomId.delete(roomId);
}
}
}
findRequestInProgress(roomId) {
const requestsByTxnId = this._requestsByRoomId.get(roomId);
public findRequestInProgress(roomId: string): VerificationRequest {
const requestsByTxnId = this.requestsByRoomId.get(roomId);
if (requestsByTxnId) {
for (const request of requestsByTxnId.values()) {
if (request.pending) {
@@ -28,26 +28,32 @@ import {
} from "./VerificationRequest";
import { errorFromEvent, newUnexpectedMessageError } from "../Error";
import { MatrixEvent } from "../../../models/event";
import { IVerificationChannel } from "./Channel";
import { MatrixClient } from "../../../client";
import { IRequestsMap } from '../..';
export type Request = VerificationRequest<ToDeviceChannel>;
/**
* A key verification channel that sends verification events over to_device messages.
* Generates its own transaction ids.
*/
export class ToDeviceChannel {
// userId and devices of user we're about to verify
constructor(client, userId, devices, transactionId = null, deviceId = null) {
this._client = client;
this.userId = userId;
this._devices = devices;
this.transactionId = transactionId;
this._deviceId = deviceId;
}
export class ToDeviceChannel implements IVerificationChannel {
public request?: VerificationRequest;
isToDevices(devices) {
if (devices.length === this._devices.length) {
// userId and devices of user we're about to verify
constructor(
private readonly client: MatrixClient,
public readonly userId: string,
private readonly devices: string[],
public transactionId: string = null,
public deviceId: string = null,
) {}
public isToDevices(devices: string[]): boolean {
if (devices.length === this.devices.length) {
for (const device of devices) {
const d = this._devices.find(d => d.deviceId === device.deviceId);
if (!d) {
if (!this.devices.includes(device)) {
return false;
}
}
@@ -57,11 +63,7 @@ export class ToDeviceChannel {
}
}
get deviceId() {
return this._deviceId;
}
static getEventType(event) {
public static getEventType(event: MatrixEvent): string {
return event.getType();
}
@@ -70,7 +72,7 @@ export class ToDeviceChannel {
* @param {MatrixEvent} event the event
* @returns {string} the transaction id
*/
static getTransactionId(event) {
public static getTransactionId(event: MatrixEvent): string {
const content = event.getContent();
return content && content.transaction_id;
}
@@ -78,12 +80,16 @@ export class ToDeviceChannel {
/**
* Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel
* @param {string} type the event type to check
* @returns {bool} boolean flag
* @returns {boolean} boolean flag
*/
static canCreateRequest(type) {
public static canCreateRequest(type: string): boolean {
return type === REQUEST_TYPE || type === START_TYPE;
}
public canCreateRequest(type: string): boolean {
return ToDeviceChannel.canCreateRequest(type);
}
/**
* Checks whether this event is a well-formed key verification event.
* This only does checks that don't rely on the current state of a potentially already channel
@@ -91,9 +97,9 @@ export class ToDeviceChannel {
* `handleEvent` can do more checks and choose to ignore invalid events.
* @param {MatrixEvent} event the event to validate
* @param {MatrixClient} client the client to get the current user and device id from
* @returns {bool} whether the event is valid and should be passed to handleEvent
* @returns {boolean} whether the event is valid and should be passed to handleEvent
*/
static validateEvent(event, client) {
public static validateEvent(event: MatrixEvent, client: MatrixClient): boolean {
if (event.isCancelled()) {
logger.warn("Ignoring flagged verification request from "
+ event.getSender());
@@ -134,7 +140,7 @@ export class ToDeviceChannel {
* @param {MatrixEvent} event the event to get the timestamp of
* @return {number} the timestamp when the event was sent
*/
getTimestamp(event) {
public getTimestamp(event: MatrixEvent): number {
const content = event.getContent();
return content && content.timestamp;
}
@@ -143,10 +149,10 @@ export class ToDeviceChannel {
* Changes the state of the channel, request, and verifier in response to a key verification event.
* @param {MatrixEvent} event to handle
* @param {VerificationRequest} request the request to forward handling to
* @param {bool} isLiveEvent whether this is an even received through sync or not
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
* @param {boolean} isLiveEvent whether this is an even received through sync or not
* @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent.
*/
async handleEvent(event, request, isLiveEvent) {
public async handleEvent(event: MatrixEvent, request: Request, isLiveEvent = false): Promise<void> {
const type = event.getType();
const content = event.getContent();
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
@@ -155,17 +161,16 @@ export class ToDeviceChannel {
}
const deviceId = content.from_device;
// adopt deviceId if not set before and valid
if (!this._deviceId && this._devices.includes(deviceId)) {
this._deviceId = deviceId;
if (!this.deviceId && this.devices.includes(deviceId)) {
this.deviceId = deviceId;
}
// if no device id or different from addopted one, cancel with sender
if (!this._deviceId || this._deviceId !== deviceId) {
// if no device id or different from adopted one, cancel with sender
if (!this.deviceId || this.deviceId !== deviceId) {
// also check that message came from the device we sent the request to earlier on
// and do send a cancel message to that device
// (but don't cancel the request for the device we should be talking to)
const cancelContent =
this.completeContent(errorFromEvent(newUnexpectedMessageError()));
return this._sendToDevices(CANCEL_TYPE, cancelContent, [deviceId]);
const cancelContent = this.completeContent(CANCEL_TYPE, errorFromEvent(newUnexpectedMessageError()));
return this.sendToDevices(CANCEL_TYPE, cancelContent, [deviceId]);
}
}
const wasStarted = request.phase === PHASE_STARTED ||
@@ -178,16 +183,16 @@ export class ToDeviceChannel {
const isAcceptingEvent = type === START_TYPE || type === READY_TYPE;
// the request has picked a ready or start event, tell the other devices about it
if (isAcceptingEvent && !wasStarted && isStarted && this._deviceId) {
const nonChosenDevices = this._devices.filter(
d => d !== this._deviceId && d !== this._client.getDeviceId(),
if (isAcceptingEvent && !wasStarted && isStarted && this.deviceId) {
const nonChosenDevices = this.devices.filter(
d => d !== this.deviceId && d !== this.client.getDeviceId(),
);
if (nonChosenDevices.length) {
const message = this.completeContent({
const message = this.completeContent(CANCEL_TYPE, {
code: "m.accepted",
reason: "Verification request accepted by another device",
});
await this._sendToDevices(CANCEL_TYPE, message, nonChosenDevices);
await this.sendToDevices(CANCEL_TYPE, message, nonChosenDevices);
}
}
}
@@ -197,7 +202,7 @@ export class ToDeviceChannel {
* @param {MatrixEvent} event the received event
* @returns {Object} the content object
*/
completedContentFromEvent(event) {
public completedContentFromEvent(event: MatrixEvent): Record<string, any> {
return event.getContent();
}
@@ -210,14 +215,14 @@ export class ToDeviceChannel {
* @param {object} content the (incomplete) content
* @returns {object} the complete content, as it will be sent.
*/
completeContent(type, content) {
public completeContent(type: string, content: Record<string, any>): Record<string, any> {
// make a copy
content = Object.assign({}, content);
if (this.transactionId) {
content.transaction_id = this.transactionId;
}
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
content.from_device = this._client.getDeviceId();
content.from_device = this.client.getDeviceId();
}
if (type === REQUEST_TYPE) {
content.timestamp = Date.now();
@@ -231,7 +236,7 @@ export class ToDeviceChannel {
* @param {object} uncompletedContent the (incomplete) content
* @returns {Promise} the promise of the request
*/
send(type, uncompletedContent = {}) {
public send(type: string, uncompletedContent: Record<string, any> = {}): Promise<void> {
// create transaction id when sending request
if ((type === REQUEST_TYPE || type === START_TYPE) && !this.transactionId) {
this.transactionId = ToDeviceChannel.makeTransactionId();
@@ -246,21 +251,21 @@ export class ToDeviceChannel {
* @param {object} content
* @returns {Promise} the promise of the request
*/
async sendCompleted(type, content) {
public async sendCompleted(type: string, content: Record<string, any>): Promise<void> {
let result;
if (type === REQUEST_TYPE) {
result = await this._sendToDevices(type, content, this._devices);
if (type === REQUEST_TYPE || (type === CANCEL_TYPE && !this.deviceId)) {
result = await this.sendToDevices(type, content, this.devices);
} else {
result = await this._sendToDevices(type, content, [this._deviceId]);
result = await this.sendToDevices(type, content, [this.deviceId]);
}
// the VerificationRequest state machine requires remote echos of the event
// the client sends itself, so we fake this for to_device messages
const remoteEchoEvent = new MatrixEvent({
sender: this._client.getUserId(),
sender: this.client.getUserId(),
content,
type,
});
await this._request.handleEvent(
await this.request.handleEvent(
type,
remoteEchoEvent,
/*isLiveEvent=*/true,
@@ -270,16 +275,14 @@ export class ToDeviceChannel {
return result;
}
_sendToDevices(type, content, devices) {
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;
}
return this._client.sendToDevice(type, { [this.userId]: msgMap });
} else {
return Promise.resolve();
await this.client.sendToDevice(type, { [this.userId]: msgMap });
}
}
@@ -287,68 +290,62 @@ export class ToDeviceChannel {
* Allow Crypto module to create and know the transaction id before the .start event gets sent.
* @returns {string} the transaction id
*/
static makeTransactionId() {
public static makeTransactionId(): string {
return randomString(32);
}
}
export class ToDeviceRequests {
constructor() {
this._requestsByUserId = new Map();
}
export class ToDeviceRequests implements IRequestsMap {
private requestsByUserId = new Map<string, Map<string, Request>>();
getRequest(event) {
public getRequest(event: MatrixEvent): Request {
return this.getRequestBySenderAndTxnId(
event.getSender(),
ToDeviceChannel.getTransactionId(event),
);
}
getRequestByChannel(channel) {
public getRequestByChannel(channel: ToDeviceChannel): Request {
return this.getRequestBySenderAndTxnId(channel.userId, channel.transactionId);
}
getRequestBySenderAndTxnId(sender, txnId) {
const requestsByTxnId = this._requestsByUserId.get(sender);
public getRequestBySenderAndTxnId(sender: string, txnId: string): Request {
const requestsByTxnId = this.requestsByUserId.get(sender);
if (requestsByTxnId) {
return requestsByTxnId.get(txnId);
}
}
setRequest(event, request) {
this.setRequestBySenderAndTxnId(
event.getSender(),
ToDeviceChannel.getTransactionId(event),
request,
);
public setRequest(event: MatrixEvent, request: Request): void {
this.setRequestBySenderAndTxnId(event.getSender(), ToDeviceChannel.getTransactionId(event), request);
}
setRequestByChannel(channel, request) {
public setRequestByChannel(channel: ToDeviceChannel, request: Request): void {
this.setRequestBySenderAndTxnId(channel.userId, channel.transactionId, request);
}
setRequestBySenderAndTxnId(sender, txnId, request) {
let requestsByTxnId = this._requestsByUserId.get(sender);
public setRequestBySenderAndTxnId(sender: string, txnId: string, request: Request): void {
let requestsByTxnId = this.requestsByUserId.get(sender);
if (!requestsByTxnId) {
requestsByTxnId = new Map();
this._requestsByUserId.set(sender, requestsByTxnId);
this.requestsByUserId.set(sender, requestsByTxnId);
}
requestsByTxnId.set(txnId, request);
}
removeRequest(event) {
public removeRequest(event: MatrixEvent): void {
const userId = event.getSender();
const requestsByTxnId = this._requestsByUserId.get(userId);
const requestsByTxnId = this.requestsByUserId.get(userId);
if (requestsByTxnId) {
requestsByTxnId.delete(ToDeviceChannel.getTransactionId(event));
if (requestsByTxnId.size === 0) {
this._requestsByUserId.delete(userId);
this.requestsByUserId.delete(userId);
}
}
}
findRequestInProgress(userId, devices) {
const requestsByTxnId = this._requestsByUserId.get(userId);
public findRequestInProgress(userId: string, devices: string[]): Request {
const requestsByTxnId = this.requestsByUserId.get(userId);
if (requestsByTxnId) {
for (const request of requestsByTxnId.values()) {
if (request.pending && request.channel.isToDevices(devices)) {
@@ -358,8 +355,8 @@ export class ToDeviceRequests {
}
}
getRequestsInProgress(userId) {
const requestsByTxnId = this._requestsByUserId.get(userId);
public getRequestsInProgress(userId: string): Request[] {
const requestsByTxnId = this.requestsByUserId.get(userId);
if (requestsByTxnId) {
return Array.from(requestsByTxnId.values()).filter(r => r.pending);
}
@@ -1,6 +1,5 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2018 - 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.
@@ -16,7 +15,6 @@ limitations under the License.
*/
import { logger } from '../../../logger';
import { EventEmitter } from 'events';
import {
errorFactory,
errorFromEvent,
@@ -24,6 +22,12 @@ import {
newUnknownMethodError,
} from "../Error";
import { QRCodeData, SCAN_QR_CODE_METHOD } from "../QRCode";
import { IVerificationChannel } from "./Channel";
import { MatrixClient } from "../../../client";
import { MatrixEvent } from "../../../models/event";
import { VerificationBase } from "../Base";
import { VerificationMethod } from "../../index";
import { TypedEventEmitter } from "../../../models/typed-event-emitter";
// How long after the event's timestamp that the request times out
const TIMEOUT_FROM_EVENT_TS = 10 * 60 * 1000; // 10 minutes
@@ -44,12 +48,40 @@ export const CANCEL_TYPE = EVENT_PREFIX + "cancel";
export const DONE_TYPE = EVENT_PREFIX + "done";
export const READY_TYPE = EVENT_PREFIX + "ready";
export const PHASE_UNSENT = 1;
export const PHASE_REQUESTED = 2;
export const PHASE_READY = 3;
export const PHASE_STARTED = 4;
export const PHASE_CANCELLED = 5;
export const PHASE_DONE = 6;
export enum Phase {
Unsent = 1,
Requested,
Ready,
Started,
Cancelled,
Done,
}
// Legacy export fields
export const PHASE_UNSENT = Phase.Unsent;
export const PHASE_REQUESTED = Phase.Requested;
export const PHASE_READY = Phase.Ready;
export const PHASE_STARTED = Phase.Started;
export const PHASE_CANCELLED = Phase.Cancelled;
export const PHASE_DONE = Phase.Done;
interface ITargetDevice {
userId?: string;
deviceId?: string;
}
interface ITransition {
phase: Phase;
event?: MatrixEvent;
}
export enum VerificationRequestEvent {
Change = "change",
}
type EventHandlerMap = {
[VerificationRequestEvent.Change]: () => void;
};
/**
* State machine for verification requests.
@@ -57,32 +89,40 @@ export const PHASE_DONE = 6;
* send and receive verification events are put in `InRoomChannel` or `ToDeviceChannel`.
* @event "change" whenever the state of the request object has changed.
*/
export class VerificationRequest extends EventEmitter {
constructor(channel, verificationMethods, client) {
super();
this.channel = channel;
this.channel._request = this;
this._verificationMethods = verificationMethods;
this._client = client;
this._commonMethods = [];
this._setPhase(PHASE_UNSENT, false);
this._eventsByUs = new Map();
this._eventsByThem = new Map();
this._observeOnly = false;
this._timeoutTimer = null;
this._accepting = false;
this._declining = false;
this._verifierHasFinished = false;
this._cancelled = false;
this._chosenMethod = null;
// we keep a copy of the QR Code data (including other user master key) around
// for QR reciprocate verification, to protect against
// cross-signing identity reset between the .ready and .start event
// and signing the wrong key after .start
this._qrCodeData = null;
export class VerificationRequest<
C extends IVerificationChannel = IVerificationChannel,
> extends TypedEventEmitter<VerificationRequestEvent, EventHandlerMap> {
private eventsByUs = new Map<string, MatrixEvent>();
private eventsByThem = new Map<string, MatrixEvent>();
private _observeOnly = false;
private timeoutTimer: number = null;
private _accepting = false;
private _declining = false;
private verifierHasFinished = false;
private _cancelled = false;
private _chosenMethod: VerificationMethod = null;
// we keep a copy of the QR Code data (including other user master key) around
// for QR reciprocate verification, to protect against
// cross-signing identity reset between the .ready and .start event
// and signing the wrong key after .start
private _qrCodeData: QRCodeData = null;
// The timestamp when we received the request event from the other side
this._requestReceivedAt = null;
// The timestamp when we received the request event from the other side
private requestReceivedAt: number = null;
private commonMethods: VerificationMethod[] = [];
private _phase: Phase;
private _cancellingUserId: string;
private _verifier: VerificationBase<any, any>;
constructor(
public readonly channel: C,
private readonly verificationMethods: Map<VerificationMethod, typeof VerificationBase>,
private readonly client: MatrixClient,
) {
super();
this.channel.request = this;
this.setPhase(PHASE_UNSENT, false);
}
/**
@@ -91,9 +131,9 @@ export class VerificationRequest extends EventEmitter {
* @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel.
* @param {MatrixEvent} event the event to validate. Don't call getType() on it but use the `type` parameter instead.
* @param {MatrixClient} client the client to get the current user and device id from
* @returns {bool} whether the event is valid and should be passed to handleEvent
* @returns {boolean} whether the event is valid and should be passed to handleEvent
*/
static validateEvent(type, event, client) {
public static validateEvent(type: string, event: MatrixEvent, client: MatrixClient): boolean {
const content = event.getContent();
if (!type || !type.startsWith(EVENT_PREFIX)) {
@@ -128,53 +168,53 @@ export class VerificationRequest extends EventEmitter {
return true;
}
get invalid() {
public get invalid(): boolean {
return this.phase === PHASE_UNSENT;
}
/** returns whether the phase is PHASE_REQUESTED */
get requested() {
public get requested(): boolean {
return this.phase === PHASE_REQUESTED;
}
/** returns whether the phase is PHASE_CANCELLED */
get cancelled() {
public get cancelled(): boolean {
return this.phase === PHASE_CANCELLED;
}
/** returns whether the phase is PHASE_READY */
get ready() {
public get ready(): boolean {
return this.phase === PHASE_READY;
}
/** returns whether the phase is PHASE_STARTED */
get started() {
public get started(): boolean {
return this.phase === PHASE_STARTED;
}
/** returns whether the phase is PHASE_DONE */
get done() {
public get done(): boolean {
return this.phase === PHASE_DONE;
}
/** once the phase is PHASE_STARTED (and !initiatedByMe) or PHASE_READY: common methods supported by both sides */
get methods() {
return this._commonMethods;
public get methods(): VerificationMethod[] {
return this.commonMethods;
}
/** the method picked in the .start event */
get chosenMethod() {
public get chosenMethod(): VerificationMethod {
return this._chosenMethod;
}
calculateEventTimeout(event) {
public calculateEventTimeout(event: MatrixEvent): number {
let effectiveExpiresAt = this.channel.getTimestamp(event)
+ TIMEOUT_FROM_EVENT_TS;
if (this._requestReceivedAt && !this.initiatedByMe &&
if (this.requestReceivedAt && !this.initiatedByMe &&
this.phase <= PHASE_REQUESTED
) {
const expiresAtByReceipt = this._requestReceivedAt
const expiresAtByReceipt = this.requestReceivedAt
+ TIMEOUT_FROM_EVENT_RECEIPT;
effectiveExpiresAt = Math.min(effectiveExpiresAt, expiresAtByReceipt);
}
@@ -183,8 +223,8 @@ export class VerificationRequest extends EventEmitter {
}
/** The current remaining amount of ms before the request should be automatically cancelled */
get timeout() {
const requestEvent = this._getEventByEither(REQUEST_TYPE);
public get timeout(): number {
const requestEvent = this.getEventByEither(REQUEST_TYPE);
if (requestEvent) {
return this.calculateEventTimeout(requestEvent);
}
@@ -195,41 +235,41 @@ export class VerificationRequest extends EventEmitter {
* The key verification request event.
* @returns {MatrixEvent} The request event, or falsey if not found.
*/
get requestEvent() {
return this._getEventByEither(REQUEST_TYPE);
public get requestEvent(): MatrixEvent {
return this.getEventByEither(REQUEST_TYPE);
}
/** current phase of the request. Some properties might only be defined in a current phase. */
get phase() {
public get phase(): Phase {
return this._phase;
}
/** The verifier to do the actual verification, once the method has been established. Only defined when the `phase` is PHASE_STARTED. */
get verifier() {
public get verifier(): VerificationBase<any, any> {
return this._verifier;
}
get canAccept() {
public get canAccept(): boolean {
return this.phase < PHASE_READY && !this._accepting && !this._declining;
}
get accepting() {
public get accepting(): boolean {
return this._accepting;
}
get declining() {
public get declining(): boolean {
return this._declining;
}
/** whether this request has sent it's initial event and needs more events to complete */
get pending() {
public get pending(): boolean {
return !this.observeOnly &&
this._phase !== PHASE_DONE &&
this._phase !== PHASE_CANCELLED;
}
/** Only set after a .ready if the other party can scan a QR code */
get qrCodeData() {
public get qrCodeData(): QRCodeData {
return this._qrCodeData;
}
@@ -239,19 +279,19 @@ export class VerificationRequest extends EventEmitter {
* For methods that need to be supported by both ends, use the `methods` property.
* @param {string} method the method to check
* @param {boolean} force to check even if the phase is not ready or started yet, internal usage
* @return {bool} whether or not the other party said the supported the method */
otherPartySupportsMethod(method, force = false) {
* @return {boolean} whether or not the other party said the supported the method */
public otherPartySupportsMethod(method: string, force = false): boolean {
if (!force && !this.ready && !this.started) {
return false;
}
const theirMethodEvent = this._eventsByThem.get(REQUEST_TYPE) ||
this._eventsByThem.get(READY_TYPE);
const theirMethodEvent = this.eventsByThem.get(REQUEST_TYPE) ||
this.eventsByThem.get(READY_TYPE);
if (!theirMethodEvent) {
// if we started straight away with .start event,
// we are assuming that the other side will support the
// chosen method, so return true for that.
if (this.started && this.initiatedByMe) {
const myStartEvent = this._eventsByUs.get(START_TYPE);
const myStartEvent = this.eventsByUs.get(START_TYPE);
const content = myStartEvent && myStartEvent.getContent();
const myStartMethod = content && content.method;
return method == myStartMethod;
@@ -274,22 +314,22 @@ export class VerificationRequest extends EventEmitter {
* For InRoomChannel, this is who sent the .request event.
* For ToDeviceChannel, this is who sent the .start event
*/
get initiatedByMe() {
public get initiatedByMe(): boolean {
// event created by us but no remote echo has been received yet
const noEventsYet = (this._eventsByUs.size + this._eventsByThem.size) === 0;
const noEventsYet = (this.eventsByUs.size + this.eventsByThem.size) === 0;
if (this._phase === PHASE_UNSENT && noEventsYet) {
return true;
}
const hasMyRequest = this._eventsByUs.has(REQUEST_TYPE);
const hasTheirRequest = this._eventsByThem.has(REQUEST_TYPE);
const hasMyRequest = this.eventsByUs.has(REQUEST_TYPE);
const hasTheirRequest = this.eventsByThem.has(REQUEST_TYPE);
if (hasMyRequest && !hasTheirRequest) {
return true;
}
if (!hasMyRequest && hasTheirRequest) {
return false;
}
const hasMyStart = this._eventsByUs.has(START_TYPE);
const hasTheirStart = this._eventsByThem.has(START_TYPE);
const hasMyStart = this.eventsByUs.has(START_TYPE);
const hasTheirStart = this.eventsByThem.has(START_TYPE);
if (hasMyStart && !hasTheirStart) {
return true;
}
@@ -297,39 +337,39 @@ export class VerificationRequest extends EventEmitter {
}
/** The id of the user that initiated the request */
get requestingUserId() {
public get requestingUserId(): string {
if (this.initiatedByMe) {
return this._client.getUserId();
return this.client.getUserId();
} else {
return this.otherUserId;
}
}
/** The id of the user that (will) receive(d) the request */
get receivingUserId() {
public get receivingUserId(): string {
if (this.initiatedByMe) {
return this.otherUserId;
} else {
return this._client.getUserId();
return this.client.getUserId();
}
}
/** The user id of the other party in this request */
get otherUserId() {
public get otherUserId(): string {
return this.channel.userId;
}
get isSelfVerification() {
return this._client.getUserId() === this.otherUserId;
public get isSelfVerification(): boolean {
return this.client.getUserId() === this.otherUserId;
}
/**
* The id of the user that cancelled the request,
* only defined when phase is PHASE_CANCELLED
*/
get cancellingUserId() {
const myCancel = this._eventsByUs.get(CANCEL_TYPE);
const theirCancel = this._eventsByThem.get(CANCEL_TYPE);
public get cancellingUserId(): string {
const myCancel = this.eventsByUs.get(CANCEL_TYPE);
const theirCancel = this.eventsByThem.get(CANCEL_TYPE);
if (myCancel && (!theirCancel || myCancel.getId() < theirCancel.getId())) {
return myCancel.getSender();
@@ -343,12 +383,12 @@ export class VerificationRequest extends EventEmitter {
/**
* The cancellation code e.g m.user which is responsible for cancelling this verification
*/
get cancellationCode() {
const ev = this._getEventByEither(CANCEL_TYPE);
public get cancellationCode(): string {
const ev = this.getEventByEither(CANCEL_TYPE);
return ev ? ev.getContent().code : null;
}
get observeOnly() {
public get observeOnly(): boolean {
return this._observeOnly;
}
@@ -359,11 +399,11 @@ export class VerificationRequest extends EventEmitter {
* verification to when no specific device is specified.
* @returns {{userId: *, deviceId: *}} The device information
*/
get targetDevice() {
public get targetDevice(): ITargetDevice {
const theirFirstEvent =
this._eventsByThem.get(REQUEST_TYPE) ||
this._eventsByThem.get(READY_TYPE) ||
this._eventsByThem.get(START_TYPE);
this.eventsByThem.get(REQUEST_TYPE) ||
this.eventsByThem.get(READY_TYPE) ||
this.eventsByThem.get(START_TYPE);
const theirFirstContent = theirFirstEvent.getContent();
const fromDevice = theirFirstContent.from_device;
return {
@@ -379,21 +419,23 @@ export class VerificationRequest extends EventEmitter {
* @param {string?} targetDevice.deviceId the id of the device to direct this request to
* @returns {VerifierBase} the verifier of the given method
*/
beginKeyVerification(method, targetDevice = null) {
public beginKeyVerification(
method: VerificationMethod,
targetDevice: ITargetDevice = null,
): VerificationBase<any, any> {
// need to allow also when unsent in case of to_device
if (!this.observeOnly && !this._verifier) {
const validStartPhase =
this.phase === PHASE_REQUESTED ||
this.phase === PHASE_READY ||
(this.phase === PHASE_UNSENT &&
this.channel.constructor.canCreateRequest(START_TYPE));
(this.phase === PHASE_UNSENT && this.channel.canCreateRequest(START_TYPE));
if (validStartPhase) {
// when called on a request that was initiated with .request event
// check the method is supported by both sides
if (this._commonMethods.length && !this._commonMethods.includes(method)) {
if (this.commonMethods.length && !this.commonMethods.includes(method)) {
throw newUnknownMethodError();
}
this._verifier = this._createVerifier(method, null, targetDevice);
this._verifier = this.createVerifier(method, null, targetDevice);
if (!this._verifier) {
throw newUnknownMethodError();
}
@@ -407,9 +449,9 @@ export class VerificationRequest extends EventEmitter {
* sends the initial .request event.
* @returns {Promise} resolves when the event has been sent.
*/
async sendRequest() {
public async sendRequest(): Promise<void> {
if (!this.observeOnly && this._phase === PHASE_UNSENT) {
const methods = [...this._verificationMethods.keys()];
const methods = [...this.verificationMethods.keys()];
await this.channel.send(REQUEST_TYPE, { methods });
}
}
@@ -420,14 +462,14 @@ export class VerificationRequest extends EventEmitter {
* @param {string?} error.code the error code to send the cancellation with
* @returns {Promise} resolves when the event has been sent.
*/
async cancel({ reason = "User declined", code = "m.user" } = {}) {
public async cancel({ reason = "User declined", code = "m.user" } = {}): Promise<void> {
if (!this.observeOnly && this._phase !== PHASE_CANCELLED) {
this._declining = true;
this.emit("change");
this.emit(VerificationRequestEvent.Change);
if (this._verifier) {
return this._verifier.cancel(errorFactory(code, reason)());
} else {
this._cancellingUserId = this._client.getUserId();
this._cancellingUserId = this.client.getUserId();
await this.channel.send(CANCEL_TYPE, { code, reason });
}
}
@@ -437,11 +479,11 @@ export class VerificationRequest extends EventEmitter {
* Accepts the request, sending a .ready event to the other party
* @returns {Promise} resolves when the event has been sent.
*/
async accept() {
public async accept(): Promise<void> {
if (!this.observeOnly && this.phase === PHASE_REQUESTED && !this.initiatedByMe) {
const methods = [...this._verificationMethods.keys()];
const methods = [...this.verificationMethods.keys()];
this._accepting = true;
this.emit("change");
this.emit(VerificationRequestEvent.Change);
await this.channel.send(READY_TYPE, { methods });
}
}
@@ -453,7 +495,7 @@ export class VerificationRequest extends EventEmitter {
* @returns {Promise} that resolves once the callback returns true
* @throws {Error} when the request is cancelled
*/
waitFor(fn) {
public waitFor(fn: (request: VerificationRequest) => boolean): Promise<VerificationRequest> {
return new Promise((resolve, reject) => {
const check = () => {
let handled = false;
@@ -465,56 +507,56 @@ export class VerificationRequest extends EventEmitter {
handled = true;
}
if (handled) {
this.off("change", check);
this.off(VerificationRequestEvent.Change, check);
}
return handled;
};
if (!check()) {
this.on("change", check);
this.on(VerificationRequestEvent.Change, check);
}
});
}
_setPhase(phase, notify = true) {
private setPhase(phase: Phase, notify = true): void {
this._phase = phase;
if (notify) {
this.emit("change");
this.emit(VerificationRequestEvent.Change);
}
}
_getEventByEither(type) {
return this._eventsByThem.get(type) || this._eventsByUs.get(type);
private getEventByEither(type: string): MatrixEvent {
return this.eventsByThem.get(type) || this.eventsByUs.get(type);
}
_getEventBy(type, byThem) {
private getEventBy(type: string, byThem = false): MatrixEvent {
if (byThem) {
return this._eventsByThem.get(type);
return this.eventsByThem.get(type);
} else {
return this._eventsByUs.get(type);
return this.eventsByUs.get(type);
}
}
_calculatePhaseTransitions() {
const transitions = [{ phase: PHASE_UNSENT }];
private calculatePhaseTransitions(): ITransition[] {
const transitions: ITransition[] = [{ phase: PHASE_UNSENT }];
const phase = () => transitions[transitions.length - 1].phase;
// always pass by .request first to be sure channel.userId has been set
const hasRequestByThem = this._eventsByThem.has(REQUEST_TYPE);
const requestEvent = this._getEventBy(REQUEST_TYPE, hasRequestByThem);
const hasRequestByThem = this.eventsByThem.has(REQUEST_TYPE);
const requestEvent = this.getEventBy(REQUEST_TYPE, hasRequestByThem);
if (requestEvent) {
transitions.push({ phase: PHASE_REQUESTED, event: requestEvent });
}
const readyEvent =
requestEvent && this._getEventBy(READY_TYPE, !hasRequestByThem);
requestEvent && this.getEventBy(READY_TYPE, !hasRequestByThem);
if (readyEvent && phase() === PHASE_REQUESTED) {
transitions.push({ phase: PHASE_READY, event: readyEvent });
}
let startEvent;
if (readyEvent || !requestEvent) {
const theirStartEvent = this._eventsByThem.get(START_TYPE);
const ourStartEvent = this._eventsByUs.get(START_TYPE);
const theirStartEvent = this.eventsByThem.get(START_TYPE);
const ourStartEvent = this.eventsByUs.get(START_TYPE);
// any party can send .start after a .ready or unsent
if (theirStartEvent && ourStartEvent) {
startEvent = theirStartEvent.getSender() < ourStartEvent.getSender() ?
@@ -523,24 +565,22 @@ export class VerificationRequest extends EventEmitter {
startEvent = theirStartEvent ? theirStartEvent : ourStartEvent;
}
} else {
startEvent = this._getEventBy(START_TYPE, !hasRequestByThem);
startEvent = this.getEventBy(START_TYPE, !hasRequestByThem);
}
if (startEvent) {
const fromRequestPhase = phase() === PHASE_REQUESTED &&
requestEvent.getSender() !== startEvent.getSender();
const fromUnsentPhase = phase() === PHASE_UNSENT &&
this.channel.constructor.canCreateRequest(START_TYPE);
const fromRequestPhase = phase() === PHASE_REQUESTED && requestEvent.getSender() !== startEvent.getSender();
const fromUnsentPhase = phase() === PHASE_UNSENT && this.channel.canCreateRequest(START_TYPE);
if (fromRequestPhase || phase() === PHASE_READY || fromUnsentPhase) {
transitions.push({ phase: PHASE_STARTED, event: startEvent });
}
}
const ourDoneEvent = this._eventsByUs.get(DONE_TYPE);
if (this._verifierHasFinished || (ourDoneEvent && phase() === PHASE_STARTED)) {
const ourDoneEvent = this.eventsByUs.get(DONE_TYPE);
if (this.verifierHasFinished || (ourDoneEvent && phase() === PHASE_STARTED)) {
transitions.push({ phase: PHASE_DONE });
}
const cancelEvent = this._getEventByEither(CANCEL_TYPE);
const cancelEvent = this.getEventByEither(CANCEL_TYPE);
if ((this._cancelled || cancelEvent) && phase() !== PHASE_DONE) {
transitions.push({ phase: PHASE_CANCELLED, event: cancelEvent });
return transitions;
@@ -549,14 +589,16 @@ export class VerificationRequest extends EventEmitter {
return transitions;
}
_transitionToPhase(transition) {
private transitionToPhase(transition: ITransition): void {
const { phase, event } = transition;
// get common methods
if (phase === PHASE_REQUESTED || phase === PHASE_READY) {
if (!this._wasSentByOwnDevice(event)) {
const content = event.getContent();
this._commonMethods =
content.methods.filter(m => this._verificationMethods.has(m));
if (!this.wasSentByOwnDevice(event)) {
const content = event.getContent<{
methods: string[];
}>();
this.commonMethods =
content.methods.filter(m => this.verificationMethods.has(m));
}
}
// detect if we're not a party in the request, and we should just observe
@@ -568,8 +610,8 @@ export class VerificationRequest extends EventEmitter {
) {
if (
this.channel.receiveStartFromOtherDevices &&
this._wasSentByOwnUser(event) &&
!this._wasSentByOwnDevice(event)
this.wasSentByOwnUser(event) &&
!this.wasSentByOwnDevice(event)
) {
this._observeOnly = true;
}
@@ -579,7 +621,7 @@ export class VerificationRequest extends EventEmitter {
if (phase === PHASE_STARTED) {
const { method } = event.getContent();
if (!this._verifier && !this.observeOnly) {
this._verifier = this._createVerifier(method, event);
this._verifier = this.createVerifier(method, event);
if (!this._verifier) {
this.cancel({
code: "m.unknown_method",
@@ -592,19 +634,19 @@ export class VerificationRequest extends EventEmitter {
}
}
_applyPhaseTransitions() {
const transitions = this._calculatePhaseTransitions();
private applyPhaseTransitions(): ITransition[] {
const transitions = this.calculatePhaseTransitions();
const existingIdx = transitions.findIndex(t => t.phase === this.phase);
// trim off phases we already went through, if any
const newTransitions = transitions.slice(existingIdx + 1);
// transition to all new phases
for (const transition of newTransitions) {
this._transitionToPhase(transition);
this.transitionToPhase(transition);
}
return newTransitions;
}
_isWinningStartRace(newEvent) {
private isWinningStartRace(newEvent: MatrixEvent): boolean {
if (newEvent.getType() !== START_TYPE) {
return false;
}
@@ -620,13 +662,13 @@ export class VerificationRequest extends EventEmitter {
const oldContent = oldEvent.getContent();
oldRaceIdentifier = oldContent && oldContent.from_device;
} else {
oldRaceIdentifier = this._client.getDeviceId();
oldRaceIdentifier = this.client.getDeviceId();
}
} else {
if (oldEvent) {
oldRaceIdentifier = oldEvent.getSender();
} else {
oldRaceIdentifier = this._client.getUserId();
oldRaceIdentifier = this.client.getUserId();
}
}
@@ -640,13 +682,13 @@ export class VerificationRequest extends EventEmitter {
return newRaceIdentifier < oldRaceIdentifier;
}
hasEventId(eventId) {
for (const event of this._eventsByUs.values()) {
public hasEventId(eventId: string): boolean {
for (const event of this.eventsByUs.values()) {
if (event.getId() === eventId) {
return true;
}
}
for (const event of this._eventsByThem.values()) {
for (const event of this.eventsByThem.values()) {
if (event.getId() === eventId) {
return true;
}
@@ -658,23 +700,29 @@ export class VerificationRequest extends EventEmitter {
* Changes the state of the request and verifier in response to a key verification event.
* @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel.
* @param {MatrixEvent} event the event to handle. Don't call getType() on it but use the `type` parameter instead.
* @param {bool} isLiveEvent whether this is an even received through sync or not
* @param {bool} isRemoteEcho whether this is the remote echo of an event sent by the same device
* @param {bool} isSentByUs whether this event is sent by a party that can accept and/or observe the request like one of our peers.
* @param {boolean} isLiveEvent whether this is an even received through sync or not
* @param {boolean} isRemoteEcho whether this is the remote echo of an event sent by the same device
* @param {boolean} isSentByUs whether this event is sent by a party that can accept and/or observe the request like one of our peers.
* For InRoomChannel this means any device for the syncing user. For ToDeviceChannel, just the syncing device.
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
* @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent.
*/
async handleEvent(type, event, isLiveEvent, isRemoteEcho, isSentByUs) {
public async handleEvent(
type: string,
event: MatrixEvent,
isLiveEvent: boolean,
isRemoteEcho: boolean,
isSentByUs: boolean,
): Promise<void> {
// if reached phase cancelled or done, ignore anything else that comes
if (this.done || this.cancelled) {
return;
}
const wasObserveOnly = this._observeOnly;
this._adjustObserveOnly(event, isLiveEvent);
this.adjustObserveOnly(event, isLiveEvent);
if (!this.observeOnly && !isRemoteEcho) {
if (await this._cancelOnError(type, event)) {
if (await this.cancelOnError(type, event)) {
return;
}
}
@@ -685,27 +733,26 @@ export class VerificationRequest extends EventEmitter {
// added here to prevent verification getting cancelled
// when the server duplicates an event (https://github.com/matrix-org/synapse/issues/3365)
const isDuplicateEvent = isSentByUs ?
this._eventsByUs.has(type) :
this._eventsByThem.has(type);
this.eventsByUs.has(type) :
this.eventsByThem.has(type);
if (isDuplicateEvent) {
return;
}
const oldPhase = this.phase;
this._addEvent(type, event, isSentByUs);
this.addEvent(type, event, isSentByUs);
// this will create if needed the verifier so needs to happen before calling it
const newTransitions = this._applyPhaseTransitions();
const newTransitions = this.applyPhaseTransitions();
try {
// only pass events from the other side to the verifier,
// no remote echos of our own events
if (this._verifier && !this.observeOnly) {
const newEventWinsRace = this._isWinningStartRace(event);
const newEventWinsRace = this.isWinningStartRace(event);
if (this._verifier.canSwitchStartEvent(event) && newEventWinsRace) {
this._verifier.switchStartEvent(event);
} else if (!isRemoteEcho) {
if (type === CANCEL_TYPE || (this._verifier.events
&& this._verifier.events.includes(type))) {
if (type === CANCEL_TYPE || this._verifier.events?.includes(type)) {
this._verifier.handleEvent(event);
}
}
@@ -722,18 +769,18 @@ export class VerificationRequest extends EventEmitter {
const shouldGenerateQrCode =
this.otherPartySupportsMethod(SCAN_QR_CODE_METHOD, true);
if (shouldGenerateQrCode) {
this._qrCodeData = await QRCodeData.create(this, this._client);
this._qrCodeData = await QRCodeData.create(this, this.client);
}
}
const lastTransition = newTransitions[newTransitions.length - 1];
const { phase } = lastTransition;
this._setupTimeout(phase);
this.setupTimeout(phase);
// set phase as last thing as this emits the "change" event
this._setPhase(phase);
this.setPhase(phase);
} else if (this._observeOnly !== wasObserveOnly) {
this.emit("change");
this.emit(VerificationRequestEvent.Change);
}
} finally {
// log events we processed so we can see from rageshakes what events were added to a request
@@ -748,26 +795,26 @@ export class VerificationRequest extends EventEmitter {
}
}
_setupTimeout(phase) {
const shouldTimeout = !this._timeoutTimer && !this.observeOnly &&
private setupTimeout(phase: Phase): void {
const shouldTimeout = !this.timeoutTimer && !this.observeOnly &&
phase === PHASE_REQUESTED;
if (shouldTimeout) {
this._timeoutTimer = setTimeout(this._cancelOnTimeout, this.timeout);
this.timeoutTimer = setTimeout(this.cancelOnTimeout, this.timeout);
}
if (this._timeoutTimer) {
if (this.timeoutTimer) {
const shouldClear = phase === PHASE_STARTED ||
phase === PHASE_READY ||
phase === PHASE_DONE ||
phase === PHASE_CANCELLED;
if (shouldClear) {
clearTimeout(this._timeoutTimer);
this._timeoutTimer = null;
clearTimeout(this.timeoutTimer);
this.timeoutTimer = null;
}
}
}
_cancelOnTimeout = () => {
private cancelOnTimeout = () => {
try {
if (this.initiatedByMe) {
this.cancel({
@@ -785,10 +832,10 @@ export class VerificationRequest extends EventEmitter {
}
};
async _cancelOnError(type, event) {
private async cancelOnError(type: string, event: MatrixEvent): Promise<boolean> {
if (type === START_TYPE) {
const method = event.getContent().method;
if (!this._verificationMethods.has(method)) {
if (!this.verificationMethods.has(method)) {
await this.cancel(errorFromEvent(newUnknownMethodError()));
return true;
}
@@ -811,7 +858,7 @@ export class VerificationRequest extends EventEmitter {
return false;
}
_adjustObserveOnly(event, isLiveEvent) {
private adjustObserveOnly(event: MatrixEvent, isLiveEvent = false): void {
// don't send out events for historical requests
if (!isLiveEvent) {
this._observeOnly = true;
@@ -821,83 +868,80 @@ export class VerificationRequest extends EventEmitter {
}
}
_addEvent(type, event, isSentByUs) {
private addEvent(type: string, event: MatrixEvent, isSentByUs = false): void {
if (isSentByUs) {
this._eventsByUs.set(type, event);
this.eventsByUs.set(type, event);
} else {
this._eventsByThem.set(type, event);
this.eventsByThem.set(type, event);
}
// once we know the userId of the other party (from the .request event)
// see if any event by anyone else crept into this._eventsByThem
// see if any event by anyone else crept into this.eventsByThem
if (type === REQUEST_TYPE) {
for (const [type, event] of this._eventsByThem.entries()) {
for (const [type, event] of this.eventsByThem.entries()) {
if (event.getSender() !== this.otherUserId) {
this._eventsByThem.delete(type);
this.eventsByThem.delete(type);
}
}
// also remember when we received the request event
this._requestReceivedAt = Date.now();
this.requestReceivedAt = Date.now();
}
}
_createVerifier(method, startEvent = null, targetDevice = null) {
private createVerifier(
method: VerificationMethod,
startEvent: MatrixEvent = null,
targetDevice: ITargetDevice = null,
): VerificationBase<any, any> {
if (!targetDevice) {
targetDevice = this.targetDevice;
}
const { userId, deviceId } = targetDevice;
const VerifierCtor = this._verificationMethods.get(method);
const VerifierCtor = this.verificationMethods.get(method);
if (!VerifierCtor) {
logger.warn("could not find verifier constructor for method", method);
return;
}
return new VerifierCtor(
this.channel,
this._client,
userId,
deviceId,
startEvent,
this,
);
return new VerifierCtor(this.channel, this.client, userId, deviceId, startEvent, this);
}
_wasSentByOwnUser(event) {
return event.getSender() === this._client.getUserId();
private wasSentByOwnUser(event: MatrixEvent): boolean {
return event.getSender() === this.client.getUserId();
}
// only for .request, .ready or .start
_wasSentByOwnDevice(event) {
if (!this._wasSentByOwnUser(event)) {
private wasSentByOwnDevice(event: MatrixEvent): boolean {
if (!this.wasSentByOwnUser(event)) {
return false;
}
const content = event.getContent();
if (!content || content.from_device !== this._client.getDeviceId()) {
if (!content || content.from_device !== this.client.getDeviceId()) {
return false;
}
return true;
}
onVerifierCancelled() {
public onVerifierCancelled(): void {
this._cancelled = true;
// move to cancelled phase
const newTransitions = this._applyPhaseTransitions();
const newTransitions = this.applyPhaseTransitions();
if (newTransitions.length) {
this._setPhase(newTransitions[newTransitions.length - 1].phase);
this.setPhase(newTransitions[newTransitions.length - 1].phase);
}
}
onVerifierFinished() {
public onVerifierFinished(): void {
this.channel.send("m.key.verification.done", {});
this._verifierHasFinished = true;
this.verifierHasFinished = true;
// move to .done phase
const newTransitions = this._applyPhaseTransitions();
const newTransitions = this.applyPhaseTransitions();
if (newTransitions.length) {
this._setPhase(newTransitions[newTransitions.length - 1].phase);
this.setPhase(newTransitions[newTransitions.length - 1].phase);
}
}
getEventFromOtherParty(type) {
return this._eventsByThem.get(type);
public getEventFromOtherParty(type: string): MatrixEvent {
return this.eventsByThem.get(type);
}
}
+12 -3
View File
@@ -15,7 +15,7 @@ limitations under the License.
*/
import { MatrixClient } from "./client";
import { IEvent, MatrixEvent } from "./models/event";
import { IEvent, MatrixEvent, MatrixEventEvent } from "./models/event";
export type EventMapper = (obj: Partial<IEvent>) => MatrixEvent;
@@ -30,10 +30,16 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
function mapper(plainOldJsObject: Partial<IEvent>) {
const event = new MatrixEvent(plainOldJsObject);
const room = client.getRoom(event.getRoomId());
if (room?.threads.has(event.getId())) {
event.setThread(room.threads.get(event.getId()));
}
if (event.isEncrypted()) {
if (!preventReEmit) {
client.reEmitter.reEmit(event, [
"Event.decrypted",
MatrixEventEvent.Decrypted,
]);
}
if (decrypt) {
@@ -41,7 +47,10 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
}
}
if (!preventReEmit) {
client.reEmitter.reEmit(event, ["Event.replaced"]);
client.reEmitter.reEmit(event, [
MatrixEventEvent.Replaced,
MatrixEventEvent.VisibilityChange,
]);
}
return event;
}
+56 -4
View File
@@ -14,6 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { RelationType } from "./@types/event";
import {
UNSTABLE_FILTER_RELATED_BY_REL_TYPES,
UNSTABLE_FILTER_RELATED_BY_SENDERS,
} from "./filter";
import { MatrixEvent } from "./models/event";
/**
@@ -46,6 +51,8 @@ export interface IFilterComponent {
not_senders?: string[];
contains_url?: boolean;
limit?: number;
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]?: string[];
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]?: Array<RelationType | string>;
}
/* eslint-enable camelcase */
@@ -61,7 +68,7 @@ export interface IFilterComponent {
* @param {Object} filterJson the definition of this filter JSON, e.g. { 'contains_url': true }
*/
export class FilterComponent {
constructor(private filterJson: IFilterComponent) {}
constructor(private filterJson: IFilterComponent, public readonly userId?: string) {}
/**
* Checks with the filter component matches the given event
@@ -69,11 +76,25 @@ export class FilterComponent {
* @return {boolean} true if the event matches the filter
*/
public check(event: MatrixEvent): boolean {
const bundledRelationships = event.getUnsigned()?.["m.relations"] || {};
const relations: Array<string | RelationType> = Object.keys(bundledRelationships);
// Relation senders allows in theory a look-up of any senders
// however clients can only know about the current user participation status
// as sending a whole list of participants could be proven problematic in terms
// of performance
// This should be improved when bundled relationships solve that problem
const relationSenders = [];
if (this.userId && bundledRelationships?.[RelationType.Thread]?.current_user_participated) {
relationSenders.push(this.userId);
}
return this.checkFields(
event.getRoomId(),
event.getSender(),
event.getType(),
event.getContent() ? event.getContent().url !== undefined : false,
relations,
relationSenders,
);
}
@@ -89,6 +110,8 @@ export class FilterComponent {
senders: this.filterJson.senders || null,
not_senders: this.filterJson.not_senders || [],
contains_url: this.filterJson.contains_url || null,
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]: UNSTABLE_FILTER_RELATED_BY_SENDERS.findIn(this.filterJson),
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]: UNSTABLE_FILTER_RELATED_BY_REL_TYPES.findIn(this.filterJson),
};
}
@@ -98,9 +121,18 @@ export class FilterComponent {
* @param {String} sender the sender of the event being checked
* @param {String} eventType the type of the event being checked
* @param {boolean} containsUrl whether the event contains a content.url field
* @param {boolean} relationTypes whether has aggregated relation of the given type
* @param {boolean} relationSenders whether one of the relation is sent by the user listed
* @return {boolean} true if the event fields match the filter
*/
private checkFields(roomId: string, sender: string, eventType: string, containsUrl: boolean): boolean {
private checkFields(
roomId: string,
sender: string,
eventType: string,
containsUrl: boolean,
relationTypes: Array<RelationType | string>,
relationSenders: string[],
): boolean {
const literalKeys = {
"rooms": function(v: string): boolean {
return roomId === v;
@@ -133,15 +165,35 @@ export class FilterComponent {
return false;
}
const relationTypesFilter = this.filterJson[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name];
if (relationTypesFilter !== undefined) {
if (!this.arrayMatchesFilter(relationTypesFilter, relationTypes)) {
return false;
}
}
const relationSendersFilter = this.filterJson[UNSTABLE_FILTER_RELATED_BY_SENDERS.name];
if (relationSendersFilter !== undefined) {
if (!this.arrayMatchesFilter(relationSendersFilter, relationSenders)) {
return false;
}
}
return true;
}
private arrayMatchesFilter(filter: any[], values: any[]): boolean {
return values.length > 0 && values.every(value => {
return filter.includes(value);
});
}
/**
* Filters a list of events down to those which match this filter component
* @param {MatrixEvent[]} events Events to be checked against the filter component
* @return {MatrixEvent[]} events which matched the filter component
*/
filter(events: MatrixEvent[]): MatrixEvent[] {
public filter(events: MatrixEvent[]): MatrixEvent[] {
return events.filter(this.check, this);
}
@@ -150,7 +202,7 @@ export class FilterComponent {
* 10 if none is otherwise specified. Cargo-culted from Synapse.
* @return {Number} the limit for this filter component.
*/
limit(): number {
public limit(): number {
return this.filterJson.limit !== undefined ? this.filterJson.limit : 10;
}
}
+20 -2
View File
@@ -18,8 +18,23 @@ limitations under the License.
* @module filter
*/
import {
EventType,
RelationType,
} from "./@types/event";
import { FilterComponent, IFilterComponent } from "./filter-component";
import { MatrixEvent } from "./models/event";
import { UnstableValue } from "./NamespacedValue";
export const UNSTABLE_FILTER_RELATED_BY_SENDERS = new UnstableValue(
"related_by_senders",
"io.element.relation_senders",
);
export const UNSTABLE_FILTER_RELATED_BY_REL_TYPES = new UnstableValue(
"related_by_rel_types",
"io.element.relation_types",
);
/**
* @param {Object} obj
@@ -50,6 +65,9 @@ export interface IFilterDefinition {
export interface IRoomEventFilter extends IFilterComponent {
lazy_load_members?: boolean;
include_redundant_members?: boolean;
types?: Array<EventType | string>;
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]?: Array<RelationType | string>;
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]?: string[];
}
interface IStateFilter extends IRoomEventFilter {}
@@ -167,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 =
+360 -209
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 * as utils from "./utils";
import { logger } from './logger';
import { parse as parseContentType, ParsedMediaType } from "content-type";
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';
import { TypedEventEmitter } from "./models/typed-event-emitter";
/*
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,111 @@ 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;
// Set to true to prevent the request function from emitting
// a Session.logged_out event. This is intended for use on
// endpoints where M_UNKNOWN_TOKEN is a valid/notable error
// response, such as with token refreshes.
inhibitLogoutEmit?: boolean;
}
export interface IUpload {
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;
export enum HttpApiEvent {
SessionLoggedOut = "Session.logged_out",
NoConsent = "no_consent",
}
export type HttpApiEventHandlerMap = {
[HttpApiEvent.SessionLoggedOut]: (err: MatrixError) => void;
[HttpApiEvent.NoConsent]: (message: string, consentUri: string) => void;
};
/**
* Construct a MatrixHttpApi.
* @constructor
* @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 +190,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 +199,40 @@ 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: TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>,
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 +274,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 +292,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 +302,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 +352,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 +361,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 +374,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 +412,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 +431,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 +440,79 @@ 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;
}
const headers: Record<string, string> = { "Content-Type": contentType };
// authedRequest uses `request` which is no longer maintained.
// `request` has a bug where if the body is zero bytes then you get an error: `Argument error, options.body`.
// See https://github.com/request/request/issues/920
// if body looks like a byte array and empty then set the Content-Length explicitly as a workaround:
if ((body as unknown as ArrayLike<number>).length === 0) {
headers["Content-Length"] = "0";
}
promise = this.authedRequest(
opts.callback, "POST", "/upload", queryParams, body, {
opts.callback, Method.Post, "/upload", queryParams, body, {
prefix: "/_matrix/media/r0",
headers: { "Content-Type": contentType },
headers,
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 +527,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 +568,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,56 +580,52 @@ 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) {
if (err.errcode == 'M_UNKNOWN_TOKEN') {
self.event_emitter.emit("Session.logged_out", err);
requestPromise.catch((err: MatrixError) => {
if (err.errcode == 'M_UNKNOWN_TOKEN' && !requestOpts?.inhibitLogoutEmit) {
this.eventEmitter.emit(HttpApiEvent.SessionLoggedOut, err);
} else if (err.errcode == 'M_CONSENT_NOT_GIVEN') {
self.event_emitter.emit(
"no_consent",
err.message,
err.data.consent_uri,
);
this.eventEmitter.emit(HttpApiEvent.NoConsent, err.message, err.data.consent_uri);
}
});
// 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 +645,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 +657,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 +688,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 +700,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 +730,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 +763,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 = utils.extend({}, opts.headers || {});
const json = opts.json === undefined ? true : opts.json;
const headers = Object.assign({}, opts.headers || {});
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 +807,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 +827,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 +838,7 @@ MatrixHttpApi.prototype = {
};
resetTimeout();
const reqPromise = defer.promise;
const reqPromise = defer.promise as IAbortablePromise<ResponseType<T, O>>;
try {
req = this.opts.request(
@@ -727,7 +855,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 +863,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 +882,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 +893,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 +923,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 +941,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 +958,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 +989,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 +1019,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 +1040,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 +1058,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 +1079,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 +1105,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) {

Some files were not shown because too many files have changed in this diff Show More