Compare commits

..

655 Commits

Author SHA1 Message Date
RiotRobot fe36dafcc7 v3.42.2-rc.3 2022-04-08 10:40:56 +01:00
RiotRobot 04a6dbbedf Prepare changelog for v3.42.2-rc.3 2022-04-08 10:40:56 +01:00
RiotRobot 29b427aab7 v17.0.0-rc.1 2022-04-08 10:36:14 +01:00
RiotRobot 643b783dec Prepare changelog for v17.0.0-rc.1 2022-04-08 10:36:13 +01:00
Germain 872033a552 Port #2283 to release (#2284) 2022-04-08 10:34:09 +01:00
RiotRobot d457fd6db0 v16.0.2-rc.1 2022-04-05 18:07:38 +01:00
RiotRobot 03f4700bd7 Prepare changelog for v16.0.2-rc.1 2022-04-05 18:07:37 +01:00
Robin b8321290f8 Add Element video room type (#2273) 2022-04-04 10:29:35 -04:00
Kerry 71b7521f42 Live location sharing - handle redacted beacons (#2269)
* emit beacon destroy event on destroy

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

* handle redacted beacon events in room-state

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

* empty line

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-04-04 10:17:49 +02:00
adamvy 106f7beb48 Fix getSessionsNeedingBackup() limit support (#2270) 2022-04-01 19:56:07 -06:00
Michael Telatynski d6f1c6cfdc Fix thread & main timeline partitioning logic (#2264) 2022-03-31 13:57:37 +01:00
Robin 4360ae7ff8 Fix coverage diffs for PRs that aren't up to date (#2263) 2022-03-30 08:02:14 -04:00
Michael Telatynski 26cbe02a7f Instantiate Thread objects when running fetchRoomThreads (#2262) 2022-03-29 15:28:08 +01:00
Michael Telatynski 85b8d4f83a Fix issues with /search and /context API handling for threads (#2261) 2022-03-29 09:24:45 +01:00
RiotRobot bdc3da1fac Resetting package fields for development 2022-03-28 14:38:44 +01:00
RiotRobot 95fcd5aa26 Merge branch 'master' into develop 2022-03-28 14:38:43 +01:00
RiotRobot 0ad83c43e4 v16.0.1 2022-03-28 14:35:08 +01:00
RiotRobot d6e4de4761 Prepare changelog for v16.0.1 2022-03-28 14:35:07 +01:00
Andy Balaam f03a391f80 Prevent exception 'Unable to set up secret storage' (#2260) 2022-03-28 11:48:34 +01:00
Andy Balaam e90f12ee32 Port codecov improvements matrix-react-sdk -> matrix-js-sdk (#2258) 2022-03-25 09:09:35 +00:00
Michael Telatynski c541b3f1ce Fix issues with duplicated MatrixEvent objects around threads (#2256) 2022-03-24 12:24:19 +00:00
Germain 6192325fe0 Thread list ordering by last reply (#2253) 2022-03-23 14:43:30 +00:00
Travis Ralston d0b964837f Remove groups (#2234)
This API is due for removal in Synapse and has been deprecated for a very long time. People should move away from it soon, but just in case we'll declare this as a breaking change.

There is no impact on sync storage here: we happen to store the data in a way that is backwards-compatible for group-supporting clients, and the code guards against missing data from the stores. So, if someone were to revert, they'd be "safe" (probably lose all their group info, but the app wouldn't crash).
2022-03-22 22:20:32 +00:00
Robin 65316ffb5c Voice rooms prototype (#2249)
* Support call room type from MSC3417

Signed-off-by: Robin Townsend <robin@robin.town>

* Make it more clear that call room type is unstable

Signed-off-by: Robin Townsend <robin@robin.town>
2022-03-22 16:14:23 -06:00
RiotRobot dadc19897c v16.0.1-rc.1 2022-03-22 21:40:04 +00:00
RiotRobot bd2f1858f4 Prepare changelog for v16.0.1-rc.1 2022-03-22 21:40:03 +00:00
Germain e32b8a75ee Lazy load thread list timeline set (#2254) 2022-03-22 21:34:20 +00:00
Kerry a6fe8797f0 Use beacon info event type as beacon identifier (#2251)
* use beacon info event type as beacon identifier

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

* test cases

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-03-22 11:14:43 +01:00
Michael Telatynski 29e54806a4 Improve typing for MatrixEvent::getContent generic (#2252) 2022-03-21 17:22:21 +01:00
Germain 75674d961a Create threads event timeline set in the room model (#2244) 2022-03-18 11:58:59 +00:00
Germain 779afbcb39 Apply redaction logic to threaded events (#2246) 2022-03-18 09:30:08 +00:00
Kerry a3f5ec1ba2 reemit beacon events (#2245)
* reemit beacon events

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

* use specific imports

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

* Update src/models/room-state.ts

Co-authored-by: Travis Ralston <travisr@matrix.org>

Co-authored-by: Travis Ralston <travisr@matrix.org>
2022-03-18 08:56:53 +00:00
Kerry 524322280b add upsert function for updating beacon events (#2247)
* add upsert function for updating beacon events

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

* expose event type on beacon model

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

* allow setting timestamp in beaconinfo content helper

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

* expose parsed beacon info

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-03-18 09:52:27 +01:00
James Salter 905a884f72 Formalise guidelines around writing tests (#2179) 2022-03-17 09:10:12 +00:00
Kerry 157635476b fix missed types for event emitter (#2243)
Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-03-16 13:50:07 +01:00
Kerry 18943d6519 emit aggregate room beacon liveness (#2241)
* emit aggregate room beacon liveness

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

* tidy and comment

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

* add export for models/beacon

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

* add owner and roomId

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

* copyright

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-03-16 08:54:13 +00:00
Germain 42b3b73551 Fix threads reply count sometimes off by one (#2240) 2022-03-15 18:59:12 -06:00
RiotRobot ee9eccb85a Resetting package fields for development 2022-03-15 14:22:43 +00:00
RiotRobot 03a8d9edb6 Merge branch 'master' into develop 2022-03-15 14:22:42 +00:00
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
Germain 510833b2f2 Use stable value for fallback (#2239) 2022-03-15 13:52:33 +00:00
Kerry c2fdb4478d Live location sharing - create m.beacon_info events (#2238)
* add content helpers

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

* stubbed Beacon class

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

* beacon test utils

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

* add beacon test utils

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

* copyrights

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

* add beacons to room state

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

* tidy comments

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

* unit test RoomState.setBeacon

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-03-15 10:31:32 +01:00
Kerry 57d71ccd0f Move test-utils into a directory (#2236)
* move test-utils.js into directory

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

* fix imports

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-03-14 14:55:11 +01:00
Kerry d064d82fcc Beacon event types from MSC3489 (#2230)
* ASSET_NODE_TYPE -> M_ASSET

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

* export const M_TIMESTAMP = new UnstableValue("m.ts", "org.matrix.msc3488.ts");

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

* LOCATION_EVENT_TYPE -> M_LOCATION

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

* extensible event types for location

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

* add locationevent parsing helpers

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

* rename

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

* comment

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

* revert makelocationcontent signature

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

* add beacon event types

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

* add variable* to type and comment

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

* add content helper functions for beacon_info and beacon

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

* copyright

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

* add m.beacon_info.live from msc3672

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-03-14 12:13:28 +00:00
Hubert Chathi 17f3920ddd Send and handle stable name for withheld codes (#2232)
since MSC2399 is finished FCP and it's in the spec, we can use the stable name
now
2022-03-11 16:21:06 -05:00
Germain 9fc8048c30 Fix incorrect toJSON for filter-component (#2231) 2022-03-11 10:46:18 +00:00
Germain 9058dbf289 Switch to using stable values for Threads (#2228) 2022-03-11 09:04:17 +00:00
Kerry e16e7bc098 Location event helper functions (#2229)
* ASSET_NODE_TYPE -> M_ASSET

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

* export const M_TIMESTAMP = new UnstableValue("m.ts", "org.matrix.msc3488.ts");

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

* LOCATION_EVENT_TYPE -> M_LOCATION

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

* extensible event types for location

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

* add locationevent parsing helpers

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

* rename

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

* comment

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

* revert makelocationcontent signature

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-03-10 18:40:13 +01:00
Germain dbcd01bb43 Fix missing threads in thread list (#2226)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-03-10 16:44:42 +00:00
Michael Telatynski 40d1674a5c Fix incorrect usage of unstable variant of is_falling_back (#2227) 2022-03-10 15:31:57 +00:00
Michael Telatynski 35a375e3d2 Update thread relation fields to match MSC3440 changes (#2218) 2022-03-09 16:15:44 +00:00
Germain 460f4f9254 fix thread fallback targeting only thread relations (#2224) 2022-03-09 11:25:13 +00:00
J. Ryan Stinnett dbd6af745c Add lint for unused locals (#2223) 2022-03-09 10:09:00 +00:00
Germain 2ac9448646 Disable pending events for thread list when server supports threads (#2222) 2022-03-08 15:51:19 +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
161 changed files with 16979 additions and 8828 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: [
+12
View File
@@ -0,0 +1,12 @@
codecov:
allow_coverage_offsets: True
coverage:
status:
project: off
patch: off
comment:
layout: "diff, files"
behavior: default
require_changes: false
require_base: no
require_head: no
+31
View File
@@ -0,0 +1,31 @@
name: Test coverage
on:
pull_request: {}
push:
branches: [develop, main, master]
jobs:
test-coverage:
runs-on: ubuntu-latest
env:
# This must be set for fetchdep.sh to get the right branch
PR_NUMBER: ${{github.event.number}}
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
# If this is a pull request, make sure we check out its head rather than the
# automatically generated merge commit, so that the coverage diff excludes
# unrelated changes in the base branch
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }}
- name: Yarn cache
uses: c-hive/gha-yarn-cache@v2
- name: Run tests with coverage
run: "yarn install && yarn build && yarn coverage"
- name: Upload coverage
uses: codecov/codecov-action@v2
with:
fail_ci_if_error: false
verbose: true
+430
View File
@@ -1,3 +1,427 @@
Changes in [17.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v17.0.0-rc.1) (2022-04-08)
============================================================================================================
Changes in [16.0.2-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.2-rc.1) (2022-04-05)
============================================================================================================
## 🚨 BREAKING CHANGES
* Remove groups and groups-related APIs ([\#2234](https://github.com/matrix-org/matrix-js-sdk/pull/2234)).
## ✨ Features
* Add Element video room type ([\#2273](https://github.com/matrix-org/matrix-js-sdk/pull/2273)).
* Live location sharing - handle redacted beacons ([\#2269](https://github.com/matrix-org/matrix-js-sdk/pull/2269)).
## 🐛 Bug Fixes
* Fix getSessionsNeedingBackup() limit support ([\#2270](https://github.com/matrix-org/matrix-js-sdk/pull/2270)). Contributed by @adamvy.
* Fix issues with /search and /context API handling for threads ([\#2261](https://github.com/matrix-org/matrix-js-sdk/pull/2261)). Fixes vector-im/element-web#21543.
* Prevent exception 'Unable to set up secret storage' ([\#2260](https://github.com/matrix-org/matrix-js-sdk/pull/2260)).
Changes in [16.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.1) (2022-03-28)
==================================================================================================
## ✨ Features
* emit aggregate room beacon liveness ([\#2241](https://github.com/matrix-org/matrix-js-sdk/pull/2241)).
* Live location sharing - create m.beacon_info events ([\#2238](https://github.com/matrix-org/matrix-js-sdk/pull/2238)).
* Beacon event types from MSC3489 ([\#2230](https://github.com/matrix-org/matrix-js-sdk/pull/2230)).
## 🐛 Bug Fixes
* Fix incorrect usage of unstable variant of `is_falling_back` ([\#2227](https://github.com/matrix-org/matrix-js-sdk/pull/2227)).
Changes in [16.0.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.1-rc.1) (2022-03-22)
============================================================================================================
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)
===================================================================================================
@@ -1582,6 +2006,12 @@ All Changes
* [BREAKING] Refactor the entire build process
[\#1113](https://github.com/matrix-org/matrix-js-sdk/pull/1113)
Changes in [3.42.2-rc.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v3.42.2-rc.3) (2022-04-08)
============================================================================================================
## 🐛 Bug Fixes
* Make self membership less prone to races ([\#2277](https://github.com/matrix-org/matrix-js-sdk/pull/2277)). Fixes vector-im/element-web#21661.
Changes in [3.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v3.0.0) (2020-01-13)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v3.0.0-rc.1...v3.0.0)
+53 -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,51 @@ 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
-----
Your PR should include tests.
For new user facing features in `matrix-react-sdk` or `element-web`, you
must include:
1. Comprehensive unit tests written in Jest. These are located in `/test`.
2. "happy path" end-to-end tests.
These are located in `/test/end-to-end-tests` in `matrix-react-sdk`, and
are run using `element-web`. Ideally, you would also include tests for edge
and error cases.
Unit tests are expected even when the feature is in labs. It's good practice
to write tests alongside the code as it ensures the code is testable from
the start, and gives you a fast feedback loop while you're developing the
functionality. End-to-end tests should be added prior to the feature
leaving labs, but don't have to be present from the start (although it might
be beneficial to have some running early, so you can test things faster).
For bugs in those repos, your change must include at least one unit test or
end-to-end test; which is best depends on what sort of test most concisely
exercises the area.
Changes to `matrix-js-sdk` must be accompanied by unit tests written in Jest.
These are located in `/spec/`.
When writing unit tests, please aim for a high level of test coverage
for new code - 80% or greater. If you cannot achieve that, please document
why it's not possible in your PR.
Tests validate that your change works as intended and also document
concisely what is being changed. Ideally, your new tests fail
prior to your change, and succeed once it has been applied. You may
find this simpler to achieve if you write the tests first.
If you're spiking some code that's experimental and not being used to support
production features, exceptions can be made to requirements for tests.
Note that tests will still be required in order to ship the feature, and it's
strongly encouraged to think about tests early in the process, as adding
tests later will become progressively more difficult.
If you're not sure how to approach writing tests for your change, ask for help
in [#element-dev](https://matrix.to/#/#element-dev:matrix.org).
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": "3.42.2-rc.3",
"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"
+3 -1
View File
@@ -20,10 +20,11 @@ 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";
import { syncPromise } from "./test-utils";
import { syncPromise } from "./test-utils/test-utils";
import { createClient } from "../src/matrix";
import { MockStorageApi } from "./MockStorageApi";
@@ -85,6 +86,7 @@ TestClient.prototype.toString = function() {
*/
TestClient.prototype.start = function() {
logger.log(this + ': starting');
this.httpBackend.when("GET", "/versions").respond(200, {});
this.httpBackend.when("GET", "/pushrules").respond(200, {});
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
this.expectDeviceKeyUpload();
+11 -33
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 * as utils from "../test-utils/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", "/versions").respond(200, {});
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);
}),
+1 -1
View File
@@ -17,7 +17,7 @@ limitations under the License.
*/
import { TestClient } from '../TestClient';
import * as testUtils from '../test-utils';
import * as testUtils from '../test-utils/test-utils';
import { logger } from '../../src/logger';
const ROOM_ID = "!room:id";
+3 -2
View File
@@ -29,7 +29,7 @@ limitations under the License.
import '../olm-loader';
import { logger } from '../../src/logger';
import * as testUtils from "../test-utils";
import * as testUtils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
import { CRYPTO_ENABLED } from "../../src/client";
@@ -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", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
aliTestClient.expectDeviceKeyUpload();
@@ -1,4 +1,4 @@
import * as utils from "../test-utils";
import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
describe("MatrixClient events", function() {
@@ -11,6 +11,7 @@ describe("MatrixClient events", function() {
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
client = testClient.client;
httpBackend = testClient.httpBackend;
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
});
+75 -14
View File
@@ -1,7 +1,8 @@
import * as utils from "../test-utils";
import * as utils from "../test-utils/test-utils";
import { EventTimeline } from "../../src/matrix";
import { logger } from "../../src/logger";
import { TestClient } from "../TestClient";
import { Thread, THREAD_RELATION_TYPE } from "../../src/models/thread";
const userId = "@alice:localhost";
const userName = "Alice";
@@ -69,8 +70,30 @@ const EVENTS = [
}),
];
const THREAD_ROOT = utils.mkMessage({
room: roomId,
user: userId,
msg: "thread root",
});
const THREAD_REPLY = utils.mkEvent({
room: roomId,
user: userId,
type: "m.room.message",
content: {
"body": "thread reply",
"msgtype": "m.text",
"m.relates_to": {
// We can't use the const here because we change server support mode for test
rel_type: "io.element.thread",
event_id: THREAD_ROOT.event_id,
},
},
});
// start the client, and wait for it to initialise
function startClient(httpBackend, client) {
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA);
@@ -115,9 +138,7 @@ describe("getEventTimeline support", function() {
return startClient(httpBackend, client).then(function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
expect(function() {
client.getEventTimeline(timelineSet, "event");
}).toThrow();
expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeTruthy();
});
});
@@ -135,16 +156,12 @@ describe("getEventTimeline support", function() {
return startClient(httpBackend, client).then(() => {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
expect(function() {
client.getEventTimeline(timelineSet, "event");
}).not.toThrow();
expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeFalsy();
});
});
it("scrollback should be able to scroll back to before a gappy /sync",
function() {
it("scrollback should be able to scroll back to before a gappy /sync", function() {
// need a client with timelineSupport disabled to make this work
let room;
return startClient(httpBackend, client).then(function() {
@@ -228,6 +245,7 @@ describe("MatrixClient event timelines", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
Thread.setServerSideSupport(false);
});
describe("getEventTimeline", function() {
@@ -354,8 +372,7 @@ describe("MatrixClient event timelines", function() {
]);
});
it("should join timelines where they overlap a previous /context",
function() {
it("should join timelines where they overlap a previous /context", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
@@ -477,6 +494,50 @@ describe("MatrixClient event timelines", function() {
httpBackend.flushAllExpected(),
]);
});
it("should handle thread replies with server support by fetching a contiguous thread timeline", async () => {
Thread.setServerSideSupport(true);
client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_REPLY.event_id))
.respond(200, function() {
return {
start: "start_token0",
events_before: [],
event: THREAD_REPLY,
events_after: [],
end: "end_token0",
state: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id))
.respond(200, function() {
return THREAD_ROOT;
});
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
encodeURIComponent(THREAD_ROOT.event_id) + "/" +
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?limit=20")
.respond(200, function() {
return {
original_event: THREAD_ROOT,
chunk: [THREAD_REPLY],
next_batch: "next_batch_token0",
prev_batch: "prev_batch_token0",
};
});
const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id);
await httpBackend.flushAllExpected();
const timeline = await timelinePromise;
expect(timeline.getEvents().find(e => e.getId() === THREAD_ROOT.event_id));
expect(timeline.getEvents().find(e => e.getId() === THREAD_REPLY.event_id));
});
});
describe("paginateEventTimeline", function() {
@@ -502,7 +563,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 +614,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]],
+714 -16
View File
@@ -1,7 +1,9 @@
import * as utils from "../test-utils";
import * as utils from "../test-utils/test-utils";
import { CRYPTO_ENABLED } from "../../src/client";
import { MatrixEvent } from "../../src/models/event";
import { Filter, MemoryStore, Room } from "../../src/matrix";
import { TestClient } from "../TestClient";
import { THREAD_RELATION_TYPE } from "../../src/models/thread";
describe("MatrixClient", function() {
let client = null;
@@ -13,9 +15,7 @@ describe("MatrixClient", function() {
beforeEach(function() {
store = new MemoryStore();
const testClient = new TestClient(userId, "aliceDevice", accessToken, undefined, {
store: store,
});
const testClient = new TestClient(userId, "aliceDevice", accessToken, undefined, { store });
httpBackend = testClient.httpBackend;
client = testClient.client;
});
@@ -243,14 +243,15 @@ describe("MatrixClient", function() {
});
describe("searching", function() {
const response = {
search_categories: {
room_events: {
count: 24,
results: {
"$flibble:localhost": {
it("searchMessageText should perform a /search for room_events", function() {
const response = {
search_categories: {
room_events: {
count: 24,
results: [{
rank: 0.1,
result: {
event_id: "$flibble:localhost",
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
@@ -259,13 +260,11 @@ describe("MatrixClient", function() {
msgtype: "m.text",
},
},
},
}],
},
},
},
};
};
it("searchMessageText should perform a /search for room_events", function(done) {
client.searchMessageText({
query: "monkeys",
});
@@ -279,8 +278,171 @@ describe("MatrixClient", function() {
});
}).respond(200, response);
httpBackend.flush().then(function() {
done();
return httpBackend.flush();
});
describe("should filter out context from different timelines (threads)", () => {
it("filters out thread replies when result is in the main timeline", async () => {
const response = {
search_categories: {
room_events: {
count: 24,
results: [{
rank: 0.1,
result: {
event_id: "$flibble:localhost",
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
content: {
body: "main timeline",
msgtype: "m.text",
},
},
context: {
events_after: [{
event_id: "$ev-after:server",
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
content: {
"body": "thread reply",
"msgtype": "m.text",
"m.relates_to": {
"event_id": "$some-thread:server",
"rel_type": THREAD_RELATION_TYPE.name,
},
},
}],
events_before: [{
event_id: "$ev-before:server",
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
content: {
body: "main timeline again",
msgtype: "m.text",
},
}],
},
}],
},
},
};
const data = {
results: [],
highlights: [],
};
client.processRoomEventsSearch(data, response);
expect(data.results).toHaveLength(1);
expect(data.results[0].context.timeline).toHaveLength(2);
expect(data.results[0].context.timeline.find(e => e.getId() === "$ev-after:server")).toBeFalsy();
});
it("filters out thread replies from threads other than the thread the result replied to", () => {
const response = {
search_categories: {
room_events: {
count: 24,
results: [{
rank: 0.1,
result: {
event_id: "$flibble:localhost",
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
content: {
"body": "thread 1 reply 1",
"msgtype": "m.text",
"m.relates_to": {
"event_id": "$thread1:server",
"rel_type": THREAD_RELATION_TYPE.name,
},
},
},
context: {
events_after: [{
event_id: "$ev-after:server",
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
content: {
"body": "thread 2 reply 2",
"msgtype": "m.text",
"m.relates_to": {
"event_id": "$thread2:server",
"rel_type": THREAD_RELATION_TYPE.name,
},
},
}],
events_before: [],
},
}],
},
},
};
const data = {
results: [],
highlights: [],
};
client.processRoomEventsSearch(data, response);
expect(data.results).toHaveLength(1);
expect(data.results[0].context.timeline).toHaveLength(1);
expect(data.results[0].context.timeline.find(e => e.getId() === "$flibble:localhost")).toBeTruthy();
});
it("filters out main timeline events when result is a thread reply", () => {
const response = {
search_categories: {
room_events: {
count: 24,
results: [{
rank: 0.1,
result: {
event_id: "$flibble:localhost",
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
content: {
"body": "thread 1 reply 1",
"msgtype": "m.text",
"m.relates_to": {
"event_id": "$thread1:server",
"rel_type": THREAD_RELATION_TYPE.name,
},
},
},
context: {
events_after: [{
event_id: "$ev-after:server",
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
content: {
"body": "main timeline",
"msgtype": "m.text",
},
}],
events_before: [],
},
}],
},
},
};
const data = {
results: [],
highlights: [],
};
client.processRoomEventsSearch(data, response);
expect(data.results).toHaveLength(1);
expect(data.results[0].context.timeline).toHaveLength(1);
expect(data.results[0].context.timeline.find(e => e.getId() === "$flibble:localhost")).toBeTruthy();
});
});
});
@@ -392,6 +554,542 @@ describe("MatrixClient", function() {
return prom;
});
});
describe("partitionThreadedEvents", function() {
const room = new Room("!STrMRsukXHtqQdSeHa:matrix.org", client, userId);
it("returns empty arrays when given an empty arrays", function() {
const events = [];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([]);
expect(threaded).toEqual([]);
});
it("copies pre-thread in-timeline vote events onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true };
const eventPollResponseReference = buildEventPollResponseReference();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
const events = [
eventPollStartThreadRoot,
eventMessageInThread,
eventPollResponseReference,
];
// Vote has no threadId yet
expect(eventPollResponseReference.threadId).toBeFalsy();
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([
// The message that was sent in a thread is missing
eventPollStartThreadRoot,
eventPollResponseReference,
]);
// The vote event has been copied into the thread
const eventRefWithThreadId = withThreadId(
eventPollResponseReference, eventPollStartThreadRoot.getId());
expect(eventRefWithThreadId.threadId).toBeTruthy();
expect(threaded).toEqual([
eventPollStartThreadRoot,
eventMessageInThread,
eventRefWithThreadId,
]);
});
it("copies pre-thread in-timeline reactions onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true };
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
const eventReaction = buildEventReaction(eventPollStartThreadRoot);
const events = [
eventPollStartThreadRoot,
eventMessageInThread,
eventReaction,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([
eventPollStartThreadRoot,
eventReaction,
]);
expect(threaded).toEqual([
eventPollStartThreadRoot,
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 eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
const events = [
eventPollStartThreadRoot,
eventPollResponseReference,
eventMessageInThread,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([
eventPollStartThreadRoot,
eventPollResponseReference,
]);
expect(threaded).toEqual([
eventPollStartThreadRoot,
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
eventMessageInThread,
]);
});
it("copies post-thread in-timeline reactions onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true };
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
const eventReaction = buildEventReaction(eventPollStartThreadRoot);
const events = [
eventPollStartThreadRoot,
eventMessageInThread,
eventReaction,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([
eventPollStartThreadRoot,
eventReaction,
]);
expect(threaded).toEqual([
eventPollStartThreadRoot,
eventMessageInThread,
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
]);
});
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 eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const eventPollResponseReference = buildEventPollResponseReference();
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
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 = [
eventPollStartThreadRoot,
eventPollResponseReference,
eventMessageInThread,
eventRoomName,
eventEncryption,
eventGuestAccess,
eventHistoryVisibility,
eventJoinRules,
eventPowerLevels,
eventMember,
eventCreate,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([
// The message that was sent in a thread is missing
eventPollStartThreadRoot,
eventPollResponseReference,
eventRoomName,
eventEncryption,
eventGuestAccess,
eventHistoryVisibility,
eventJoinRules,
eventPowerLevels,
eventMember,
eventCreate,
]);
// Thread should contain only stuff that happened in the thread - no room state events
expect(threaded).toEqual([
eventPollStartThreadRoot,
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
eventMessageInThread,
]);
});
it("sends redactions of reactions to thread responses to thread timeline only", () => {
client.clientOpts = { experimentalThreadSupport: true };
const threadRootEvent = buildEventPollStartThreadRoot();
const eventMessageInThread = buildEventMessageInThread(threadRootEvent);
const threadedReaction = buildEventReaction(eventMessageInThread);
const threadedReactionRedaction = buildEventRedaction(threadedReaction);
const events = [
threadRootEvent,
eventMessageInThread,
threadedReaction,
threadedReactionRedaction,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([
threadRootEvent,
]);
expect(threaded).toEqual([
threadRootEvent,
eventMessageInThread,
threadedReaction,
threadedReactionRedaction,
]);
});
it("sends reply to reply to thread root outside of thread to main timeline only", () => {
client.clientOpts = { experimentalThreadSupport: true };
const threadRootEvent = buildEventPollStartThreadRoot();
const eventMessageInThread = buildEventMessageInThread(threadRootEvent);
const directReplyToThreadRoot = buildEventReply(threadRootEvent);
const replyToReply = buildEventReply(directReplyToThreadRoot);
const events = [
threadRootEvent,
eventMessageInThread,
directReplyToThreadRoot,
replyToReply,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([
threadRootEvent,
directReplyToThreadRoot,
replyToReply,
]);
expect(threaded).toEqual([
threadRootEvent,
eventMessageInThread,
]);
});
it("sends reply to thread responses to thread timeline only", () => {
client.clientOpts = { experimentalThreadSupport: true };
const threadRootEvent = buildEventPollStartThreadRoot();
const eventMessageInThread = buildEventMessageInThread(threadRootEvent);
const replyToThreadResponse = buildEventReply(eventMessageInThread);
const events = [
threadRootEvent,
eventMessageInThread,
replyToThreadResponse,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([
threadRootEvent,
]);
expect(threaded).toEqual([
threadRootEvent,
eventMessageInThread,
replyToThreadResponse,
]);
});
});
});
function withThreadId(event, newThreadId) {
const ret = event.toSnapshot();
ret.setThreadId(newThreadId);
return ret;
}
const buildEventMessageInThread = (root) => new MatrixEvent({
"age": 80098509,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
"device_id": "XISFUZSKHH",
"m.relates_to": {
"event_id": root.getId(),
"m.in_reply_to": {
"event_id": root.getId(),
},
"rel_type": "m.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 = (event) => new MatrixEvent({
"content": {
"m.relates_to": {
"event_id": event.getId(),
"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 buildEventRedaction = (event) => new MatrixEvent({
"content": {
},
"origin_server_ts": 1643977249239,
"sender": "@andybalaam-test1:matrix.org",
"redacts": event.getId(),
"type": "m.room.redaction",
"unsigned": {
"age": 22597,
"transaction_id": "m1643977249073.17",
},
"event_id": "$86B2b-x3LgE4DlV4y24b7UHnt72LIA3rzjvMysTtAfB",
"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 buildEventReply = (target) => new MatrixEvent({
"age": 80098509,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
"device_id": "XISFUZSKHH",
"m.relates_to": {
"m.in_reply_to": {
"event_id": target.getId(),
},
},
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
},
"event_id": target.getId() + Math.random(),
"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 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/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", "/versions").respond(200, {});
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("/versions", 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();
}
@@ -1,4 +1,4 @@
import * as utils from "../test-utils";
import * as utils from "../test-utils/test-utils";
import { EventStatus } from "../../src/models/event";
import { TestClient } from "../TestClient";
@@ -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", "/versions").respond(200, {});
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("/versions");
await httpBackend.flush("/pushrules");
await httpBackend.flush("/filter");
});
afterEach(function() {
@@ -551,6 +553,7 @@ describe("MatrixClient room timelines", function() {
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
return Promise.all([
httpBackend.flush("/versions", 1),
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(() => {
+3 -3
View File
@@ -1,6 +1,6 @@
import { MatrixEvent } from "../../src/models/event";
import { EventTimeline } from "../../src/models/event-timeline";
import * as utils from "../test-utils";
import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
describe("MatrixClient syncing", function() {
@@ -19,6 +19,7 @@ describe("MatrixClient syncing", function() {
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
httpBackend = testClient.httpBackend;
client = testClient.client;
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
});
@@ -734,8 +735,7 @@ describe("MatrixClient syncing", function() {
expect(tok).toEqual("pagTok");
}),
// first flush the filter request; this will make syncLeftRooms
// make its /sync call
// first flush the filter request; this will make syncLeftRooms make its /sync call
httpBackend.flush("/filter").then(function() {
return httpBackend.flushAllExpected();
}),
+8 -1
View File
@@ -16,7 +16,8 @@ limitations under the License.
*/
import anotherjson from "another-json";
import * as testUtils from "../test-utils";
import * as testUtils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
import { logger } from "../../src/logger";
@@ -617,6 +618,9 @@ describe("megolm", function() {
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/org.matrix.room_key.withheld/',
).respond(200, {});
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/m.room_key.withheld/',
).respond(200, {});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
@@ -717,6 +721,9 @@ describe("megolm", function() {
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/org.matrix.room_key.withheld/',
).respond(200, {});
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/m.room_key.withheld/',
).respond(200, {});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test2'),
-368
View File
@@ -1,368 +0,0 @@
// load olm before the sdk if possible
import './olm-loader';
import { logger } from '../src/logger';
import { MatrixEvent } from "../src/models/event";
/**
* Return a promise that is resolved when the client next emits a
* SYNCING event.
* @param {Object} client The client
* @param {Number=} count Number of syncs to wait for (default 1)
* @return {Promise} Resolves once the client has emitted a SYNCING event
*/
export function syncPromise(client, count) {
if (count === undefined) {
count = 1;
}
if (count <= 0) {
return Promise.resolve();
}
const p = new Promise((resolve, reject) => {
const cb = (state) => {
logger.log(`${Date.now()} syncPromise(${count}): ${state}`);
if (state === 'SYNCING') {
resolve();
} else {
client.once('sync', cb);
}
};
client.once('sync', cb);
});
return p.then(() => {
return syncPromise(client, count-1);
});
}
/**
* Create a spy for an object and automatically spy its methods.
* @param {*} constr The class constructor (used with 'new')
* @param {string} name The name of the class
* @return {Object} An instantiated object with spied methods/properties.
*/
export function mock(constr, name) {
// Based on
// http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/
const HelperConstr = new Function(); // jshint ignore:line
HelperConstr.prototype = constr.prototype;
const result = new HelperConstr();
result.toString = function() {
return "mock" + (name ? " of " + name : "");
};
for (const key of Object.getOwnPropertyNames(constr.prototype)) { // eslint-disable-line guard-for-in
try {
if (constr.prototype[key] instanceof Function) {
result[key] = jest.fn();
}
} catch (ex) {
// Direct access to some non-function fields of DOM prototypes may
// cause exceptions.
// Overwriting will not work either in that case.
}
}
return result;
}
/**
* Create an Event.
* @param {Object} opts Values for the event.
* @param {string} opts.type The event.type
* @param {string} opts.room The event.room_id
* @param {string} opts.sender The event.sender
* @param {string} opts.skey Optional. The state key (auto inserts empty string)
* @param {Object} opts.content The event.content
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object} a JSON object representing this event.
*/
export function mkEvent(opts) {
if (!opts.type || !opts.content) {
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
}
const event = {
type: opts.type,
room_id: opts.room,
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: opts.content,
event_id: "$" + Math.random() + "-" + Math.random(),
};
if (opts.skey !== undefined) {
event.state_key = opts.skey;
} else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
"m.room.power_levels", "m.room.topic",
"com.example.state"].includes(opts.type)) {
event.state_key = "";
}
return opts.event ? new MatrixEvent(event) : event;
}
/**
* Create an m.presence event.
* @param {Object} opts Values for the presence.
* @return {Object|MatrixEvent} The event
*/
export function mkPresence(opts) {
if (!opts.user) {
throw new Error("Missing user");
}
const event = {
event_id: "$" + Math.random() + "-" + Math.random(),
type: "m.presence",
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: {
avatar_url: opts.url,
displayname: opts.name,
last_active_ago: opts.ago,
presence: opts.presence || "offline",
},
};
return opts.event ? new MatrixEvent(event) : event;
}
/**
* Create an m.room.member event.
* @param {Object} opts Values for the membership.
* @param {string} opts.room The room ID for the event.
* @param {string} opts.mship The content.membership for the event.
* @param {string} opts.sender The sender user ID for the event.
* @param {string} opts.skey The target user ID for the event if applicable
* e.g. for invites/bans.
* @param {string} opts.name The content.displayname for the event.
* @param {string} opts.url The content.avatar_url for the event.
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
*/
export function mkMembership(opts) {
opts.type = "m.room.member";
if (!opts.skey) {
opts.skey = opts.sender || opts.user;
}
if (!opts.mship) {
throw new Error("Missing .mship => " + JSON.stringify(opts));
}
opts.content = {
membership: opts.mship,
};
if (opts.name) {
opts.content.displayname = opts.name;
}
if (opts.url) {
opts.content.avatar_url = opts.url;
}
return mkEvent(opts);
}
/**
* Create an m.room.message event.
* @param {Object} opts Values for the message
* @param {string} opts.room The room ID for the event.
* @param {string} opts.user The user ID for the event.
* @param {string} opts.msg Optional. The content.body for the event.
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
*/
export function mkMessage(opts) {
opts.type = "m.room.message";
if (!opts.msg) {
opts.msg = "Random->" + Math.random();
}
if (!opts.room || !opts.user) {
throw new Error("Missing .room or .user from %s", opts);
}
opts.content = {
msgtype: "m.text",
body: opts.msg,
};
return mkEvent(opts);
}
/**
* A mock implementation of webstorage
*
* @constructor
*/
export function MockStorageApi() {
this.data = {};
}
MockStorageApi.prototype = {
get length() {
return Object.keys(this.data).length;
},
key: function(i) {
return Object.keys(this.data)[i];
},
setItem: function(k, v) {
this.data[k] = v;
},
getItem: function(k) {
return this.data[k] || null;
},
removeItem: function(k) {
delete this.data[k];
},
};
/**
* If an event is being decrypted, wait for it to finish being decrypted.
*
* @param {MatrixEvent} event
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
*/
export function awaitDecryption(event) {
// An event is not always decrypted ahead of time
// getClearContent is a good signal to know whether an event has been decrypted
// already
if (event.getClearContent() !== null) {
return event;
} else {
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
return new Promise((resolve, reject) => {
event.once('Event.decrypted', (ev) => {
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev);
});
});
}
}
export function HttpResponse(
httpLookups, acceptKeepalives, ignoreUnhandledSync,
) {
this.httpLookups = httpLookups;
this.acceptKeepalives = acceptKeepalives === undefined ? true : acceptKeepalives;
this.ignoreUnhandledSync = ignoreUnhandledSync;
this.pendingLookup = null;
}
HttpResponse.prototype.request = function(
cb, method, path, qp, data, prefix,
) {
if (path === HttpResponse.KEEP_ALIVE_PATH && this.acceptKeepalives) {
return Promise.resolve();
}
const next = this.httpLookups.shift();
const logLine = (
"MatrixClient[UT] RECV " + method + " " + path + " " +
"EXPECT " + (next ? next.method : next) + " " + (next ? next.path : next)
);
logger.log(logLine);
if (!next) { // no more things to return
if (method === "GET" && path === "/sync" && this.ignoreUnhandledSync) {
logger.log("MatrixClient[UT] Ignoring.");
return new Promise(() => {});
}
if (this.pendingLookup) {
if (this.pendingLookup.method === method
&& this.pendingLookup.path === path) {
return this.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(this.pendingLookup) + " JUST GOT: " +
method + " " + path,
);
}
this.pendingLookup = {
promise: new Promise(() => {}),
method: method,
path: path,
};
return this.pendingLookup.promise;
}
if (next.path === path && next.method === method) {
logger.log(
"MatrixClient[UT] Matched. Returning " +
(next.error ? "BAD" : "GOOD") + " response",
);
if (next.expectBody) {
expect(next.expectBody).toEqual(data);
}
if (next.expectQueryParams) {
Object.keys(next.expectQueryParams).forEach(function(k) {
expect(qp[k]).toEqual(next.expectQueryParams[k]);
});
}
if (next.thenCall) {
process.nextTick(next.thenCall, 0); // next tick so we return first.
}
if (next.error) {
return Promise.reject({
errcode: next.error.errcode,
httpStatus: next.error.httpStatus,
name: next.error.errcode,
message: "Expected testing error",
data: next.error,
});
}
return Promise.resolve(next.data);
} else if (method === "GET" && path === "/sync" && this.ignoreUnhandledSync) {
logger.log("MatrixClient[UT] Ignoring.");
this.httpLookups.unshift(next);
return new Promise(() => {});
}
expect(true).toBe(false, "Expected different request. " + logLine);
return new Promise(() => {});
};
HttpResponse.KEEP_ALIVE_PATH = "/_matrix/client/versions";
HttpResponse.PUSH_RULES_RESPONSE = {
method: "GET",
path: "/pushrules/",
data: {},
};
HttpResponse.USER_ID = "@alice:bar";
HttpResponse.filterResponse = function(userId) {
const filterPath = "/user/" + encodeURIComponent(userId) + "/filter";
return {
method: "POST",
path: filterPath,
data: { filter_id: "f1lt3r" },
};
};
HttpResponse.SYNC_DATA = {
next_batch: "s_5_3",
presence: { events: [] },
rooms: {},
};
HttpResponse.SYNC_RESPONSE = {
method: "GET",
path: "/sync",
data: HttpResponse.SYNC_DATA,
};
HttpResponse.defaultResponses = function(userId) {
return [
HttpResponse.PUSH_RULES_RESPONSE,
HttpResponse.filterResponse(userId),
HttpResponse.SYNC_RESPONSE,
];
};
export function setHttpResponses(
client, responses, acceptKeepalives, ignoreUnhandledSyncs,
) {
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);
}
+119
View File
@@ -0,0 +1,119 @@
/*
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 { MatrixEvent } from "../../src";
import { M_BEACON, M_BEACON_INFO } from "../../src/@types/beacon";
import { LocationAssetType } from "../../src/@types/location";
import {
makeBeaconContent,
makeBeaconInfoContent,
} from "../../src/content-helpers";
type InfoContentProps = {
timeout: number;
isLive?: boolean;
assetType?: LocationAssetType;
description?: string;
};
const DEFAULT_INFO_CONTENT_PROPS: InfoContentProps = {
timeout: 3600000,
};
/**
* Create an m.beacon_info event
* all required properties are mocked
* override with contentProps
*/
export const makeBeaconInfoEvent = (
sender: string,
roomId: string,
contentProps: Partial<InfoContentProps> = {},
eventId?: string,
eventTypeSuffix?: string,
): MatrixEvent => {
const {
timeout, isLive, description, assetType,
} = {
...DEFAULT_INFO_CONTENT_PROPS,
...contentProps,
};
const event = new MatrixEvent({
type: `${M_BEACON_INFO.name}.${sender}.${eventTypeSuffix || Date.now()}`,
room_id: roomId,
state_key: sender,
content: makeBeaconInfoContent(timeout, isLive, description, assetType),
});
// live beacons use the beacon_info event id
// set or default this
event.replaceLocalEventId(eventId || `$${Math.random()}-${Math.random()}`);
return event;
};
type ContentProps = {
uri: string;
timestamp: number;
beaconInfoId: string;
description?: string;
};
const DEFAULT_CONTENT_PROPS: ContentProps = {
uri: 'geo:-36.24484561954707,175.46884959563613;u=10',
timestamp: 123,
beaconInfoId: '$123',
};
/**
* Create an m.beacon event
* all required properties are mocked
* override with contentProps
*/
export const makeBeaconEvent = (
sender: string,
contentProps: Partial<ContentProps> = {},
): MatrixEvent => {
const { uri, timestamp, beaconInfoId, description } = {
...DEFAULT_CONTENT_PROPS,
...contentProps,
};
return new MatrixEvent({
type: M_BEACON.name,
sender,
content: makeBeaconContent(uri, timestamp, beaconInfoId, description),
});
};
/**
* Create a mock geolocation position
* defaults all required properties
*/
export const makeGeolocationPosition = (
{ timestamp, coords }:
{ timestamp?: number, coords: Partial<GeolocationCoordinates> },
): GeolocationPosition => ({
timestamp: timestamp ?? 1647256791840,
coords: {
accuracy: 1,
latitude: 54.001927,
longitude: -8.253491,
altitude: null,
altitudeAccuracy: null,
heading: null,
speed: null,
...coords,
},
});
+28
View File
@@ -0,0 +1,28 @@
/*
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.
*/
/**
* Filter emitter.emit mock calls to find relevant events
* eg:
* ```
* const emitSpy = jest.spyOn(state, 'emit');
* << actions >>
* const beaconLivenessEmits = emitCallsByEventType(BeaconEvent.New, emitSpy);
* expect(beaconLivenessEmits.length).toBe(1);
* ```
*/
export const filterEmitCallsByEventType = (eventType: string, spy: jest.SpyInstance<any, unknown[]>) =>
spy.mock.calls.filter((args) => args[0] === eventType);
+282
View File
@@ -0,0 +1,282 @@
// eslint-disable-next-line no-restricted-imports
import EventEmitter from "events";
// load olm before the sdk if possible
import '../olm-loader';
import { logger } from '../../src/logger';
import { IContent, IEvent, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { ClientEvent, EventType, MatrixClient } from "../../src";
import { SyncState } from "../../src/sync";
/**
* Return a promise that is resolved when the client next emits a
* SYNCING event.
* @param {Object} client The client
* @param {Number=} count Number of syncs to wait for (default 1)
* @return {Promise} Resolves once the client has emitted a SYNCING event
*/
export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
if (count <= 0) {
return Promise.resolve();
}
const p = new Promise<void>((resolve) => {
const cb = (state: SyncState) => {
logger.log(`${Date.now()} syncPromise(${count}): ${state}`);
if (state === SyncState.Syncing) {
resolve();
} else {
client.once(ClientEvent.Sync, cb);
}
};
client.once(ClientEvent.Sync, cb);
});
return p.then(() => {
return syncPromise(client, count - 1);
});
}
/**
* Create a spy for an object and automatically spy its methods.
* @param {*} constr The class constructor (used with 'new')
* @param {string} name The name of the class
* @return {Object} An instantiated object with spied methods/properties.
*/
export function mock<T>(constr: { new(...args: any[]): T }, name: string): T {
// Based on http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/
const HelperConstr = new Function(); // jshint ignore:line
HelperConstr.prototype = constr.prototype;
// @ts-ignore
const result = new HelperConstr();
result.toString = function() {
return "mock" + (name ? " of " + name : "");
};
for (const key of Object.getOwnPropertyNames(constr.prototype)) { // eslint-disable-line guard-for-in
try {
if (constr.prototype[key] instanceof Function) {
result[key] = jest.fn();
}
} catch (ex) {
// Direct access to some non-function fields of DOM prototypes may
// cause exceptions.
// Overwriting will not work either in that case.
}
}
return result;
}
interface IEventOpts {
type: EventType | string;
room: string;
sender?: string;
skey?: string;
content: IContent;
event?: boolean;
user?: string;
unsigned?: IUnsigned;
redacts?: string;
}
/**
* Create an Event.
* @param {Object} opts Values for the event.
* @param {string} opts.type The event.type
* @param {string} opts.room The event.room_id
* @param {string} opts.sender The event.sender
* @param {string} opts.skey Optional. The state key (auto inserts empty string)
* @param {Object} opts.content The event.content
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object} a JSON object representing this event.
*/
export function mkEvent(opts: IEventOpts): object | MatrixEvent {
if (!opts.type || !opts.content) {
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
}
const event: Partial<IEvent> = {
type: opts.type as string,
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(),
redacts: opts.redacts,
};
if (opts.skey !== undefined) {
event.state_key = opts.skey;
} else if ([
EventType.RoomName,
EventType.RoomTopic,
EventType.RoomCreate,
EventType.RoomJoinRules,
EventType.RoomPowerLevels,
EventType.RoomTopic,
"com.example.state",
].includes(opts.type)) {
event.state_key = "";
}
return opts.event ? new MatrixEvent(event) : event;
}
interface IPresenceOpts {
user?: string;
sender?: string;
url: string;
name: string;
ago: number;
presence?: string;
event?: boolean;
}
/**
* Create an m.presence event.
* @param {Object} opts Values for the presence.
* @return {Object|MatrixEvent} The event
*/
export function mkPresence(opts: IPresenceOpts): object | MatrixEvent {
const event = {
event_id: "$" + Math.random() + "-" + Math.random(),
type: "m.presence",
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: {
avatar_url: opts.url,
displayname: opts.name,
last_active_ago: opts.ago,
presence: opts.presence || "offline",
},
};
return opts.event ? new MatrixEvent(event) : event;
}
interface IMembershipOpts {
room: string;
mship: string;
sender?: string;
user?: string;
skey?: string;
name?: string;
url?: string;
event?: boolean;
}
/**
* Create an m.room.member event.
* @param {Object} opts Values for the membership.
* @param {string} opts.room The room ID for the event.
* @param {string} opts.mship The content.membership for the event.
* @param {string} opts.sender The sender user ID for the event.
* @param {string} opts.skey The target user ID for the event if applicable
* e.g. for invites/bans.
* @param {string} opts.name The content.displayname for the event.
* @param {string} opts.url The content.avatar_url for the event.
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
*/
export function mkMembership(opts: IMembershipOpts): object | MatrixEvent {
const eventOpts: IEventOpts = {
...opts,
type: EventType.RoomMember,
content: {
membership: opts.mship,
},
};
if (!opts.skey) {
eventOpts.skey = opts.sender || opts.user;
}
if (opts.name) {
eventOpts.content.displayname = opts.name;
}
if (opts.url) {
eventOpts.content.avatar_url = opts.url;
}
return mkEvent(eventOpts);
}
interface IMessageOpts {
room: string;
user: string;
msg?: string;
event?: boolean;
}
/**
* Create an m.room.message event.
* @param {Object} opts Values for the message
* @param {string} opts.room The room ID for the event.
* @param {string} opts.user The user ID for the event.
* @param {string} opts.msg Optional. The content.body for the event.
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
*/
export function mkMessage(opts: IMessageOpts): object | MatrixEvent {
const eventOpts: IEventOpts = {
...opts,
type: EventType.RoomMessage,
content: {
msgtype: "m.text",
body: opts.msg,
},
};
if (!eventOpts.content.body) {
eventOpts.content.body = "Random->" + Math.random();
}
return mkEvent(eventOpts);
}
/**
* A mock implementation of webstorage
*
* @constructor
*/
export class MockStorageApi {
private data: Record<string, any> = {};
public get length() {
return Object.keys(this.data).length;
}
public key(i: number): any {
return Object.keys(this.data)[i];
}
public setItem(k: string, v: any): void {
this.data[k] = v;
}
public getItem(k: string): any {
return this.data[k] || null;
}
public removeItem(k: string): void {
delete this.data[k];
}
}
/**
* If an event is being decrypted, wait for it to finish being decrypted.
*
* @param {MatrixEvent} event
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
*/
export async function awaitDecryption(event: MatrixEvent): Promise<MatrixEvent> {
// An event is not always decrypted ahead of time
// getClearContent is a good signal to know whether an event has been decrypted
// already
if (event.getClearContent() !== null) {
return event;
} else {
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
return new Promise((resolve) => {
event.once(MatrixEventEvent.Decrypted, (ev) => {
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev);
});
});
}
}
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => 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";
+127
View File
@@ -0,0 +1,127 @@
/*
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 { REFERENCE_RELATION } from "matrix-events-sdk";
import { M_BEACON_INFO } from "../../src/@types/beacon";
import { LocationAssetType, M_ASSET, M_LOCATION, M_TIMESTAMP } from "../../src/@types/location";
import { makeBeaconContent, makeBeaconInfoContent } from "../../src/content-helpers";
describe('Beacon content helpers', () => {
describe('makeBeaconInfoContent()', () => {
const mockDateNow = 123456789;
beforeEach(() => {
jest.spyOn(global.Date, 'now').mockReturnValue(mockDateNow);
});
afterAll(() => {
jest.spyOn(global.Date, 'now').mockRestore();
});
it('create fully defined event content', () => {
expect(makeBeaconInfoContent(
1234,
true,
'nice beacon_info',
LocationAssetType.Pin,
)).toEqual({
[M_BEACON_INFO.name]: {
description: 'nice beacon_info',
timeout: 1234,
live: true,
},
[M_TIMESTAMP.name]: mockDateNow,
[M_ASSET.name]: {
type: LocationAssetType.Pin,
},
});
});
it('defaults timestamp to current time', () => {
expect(makeBeaconInfoContent(
1234,
true,
'nice beacon_info',
LocationAssetType.Pin,
)).toEqual(expect.objectContaining({
[M_TIMESTAMP.name]: mockDateNow,
}));
});
it('uses timestamp when provided', () => {
expect(makeBeaconInfoContent(
1234,
true,
'nice beacon_info',
LocationAssetType.Pin,
99999,
)).toEqual(expect.objectContaining({
[M_TIMESTAMP.name]: 99999,
}));
});
it('defaults asset type to self when not set', () => {
expect(makeBeaconInfoContent(
1234,
true,
'nice beacon_info',
// no assetType passed
)).toEqual(expect.objectContaining({
[M_ASSET.name]: {
type: LocationAssetType.Self,
},
}));
});
});
describe('makeBeaconContent()', () => {
it('creates event content without description', () => {
expect(makeBeaconContent(
'geo:foo',
123,
'$1234',
// no description
)).toEqual({
[M_LOCATION.name]: {
description: undefined,
uri: 'geo:foo',
},
[M_TIMESTAMP.name]: 123,
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: '$1234',
},
});
});
it('creates event content with description', () => {
expect(makeBeaconContent(
'geo:foo',
123,
'$1234',
'test description',
)).toEqual({
[M_LOCATION.name]: {
description: 'test description',
uri: 'geo:foo',
},
[M_TIMESTAMP.name]: 123,
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: '$1234',
},
});
});
});
});
+28 -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,9 +11,9 @@ 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";
import { logger } from '../../src/logger';
const Olm = global.Olm;
@@ -398,4 +401,28 @@ describe("Crypto", function() {
expect(aliceClient.sendToDevice.mock.calls[2][2]).not.toBe(txnId);
});
});
describe('Secret storage', function() {
it("creates secret storage even if there is no keyInfo", async function() {
jest.spyOn(logger, 'log').mockImplementation(() => {});
jest.setTimeout(10000);
const client = (new TestClient("@a:example.com", "dev")).client;
await client.initCrypto();
client.crypto.getSecretStorageKey = async () => null;
client.crypto.isCrossSigningReady = async () => false;
client.crypto.baseApis.uploadDeviceSigningKeys = () => null;
client.crypto.baseApis.setAccountData = () => null;
client.crypto.baseApis.uploadKeySignatures = () => null;
client.crypto.baseApis.http.authedRequest = () => null;
const createSecretStorageKey = async () => {
return {
keyInfo: undefined, // Returning undefined here used to cause a crash
privateKey: Uint8Array.of(32, 33),
};
};
await client.crypto.bootstrapSecretStorage({
createSecretStorageKey,
});
});
});
});
+187 -80
View File
@@ -2,7 +2,7 @@ import '../../../olm-loader';
import * as algorithms from "../../../../src/crypto/algorithms";
import { MemoryCryptoStore } from "../../../../src/crypto/store/memory-crypto-store";
import { MockStorageApi } from "../../../MockStorageApi";
import * as testUtils from "../../../test-utils";
import * as testUtils from "../../../test-utils/test-utils";
import { OlmDevice } from "../../../../src/crypto/OlmDevice";
import { Crypto } from "../../../../src/crypto";
import { logger } from "../../../../src/logger";
@@ -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();
});
});
});
@@ -414,7 +462,7 @@ describe("MegolmDecryption", function() {
let run = false;
aliceClient.sendToDevice = async (msgtype, contentMap) => {
run = true;
expect(msgtype).toBe("org.matrix.room_key.withheld");
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
delete contentMap["@bob:example.com"].bobdevice1.session_id;
delete contentMap["@bob:example.com"].bobdevice2.session_id;
expect(contentMap).toStrictEqual({
@@ -524,7 +572,7 @@ describe("MegolmDecryption", function() {
const sendPromise = new Promise((resolve, reject) => {
aliceClient.sendToDevice = async (msgtype, contentMap) => {
expect(msgtype).toBe("org.matrix.room_key.withheld");
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
expect(contentMap).toStrictEqual({
'@bob:example.com': {
bobdevice: {
@@ -571,7 +619,7 @@ describe("MegolmDecryption", function() {
content: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
session_id: "session_id",
session_id: "session_id1",
sender_key: bobDevice.deviceCurve25519Key,
code: "m.blacklisted",
reason: "You have been blocked",
@@ -588,7 +636,34 @@ describe("MegolmDecryption", function() {
ciphertext: "blablabla",
device_id: "bobdevice",
sender_key: bobDevice.deviceCurve25519Key,
session_id: "session_id",
session_id: "session_id1",
},
}))).rejects.toThrow("The sender has blocked you.");
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
type: "m.room_key.withheld",
sender: "@bob:example.com",
content: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
session_id: "session_id2",
sender_key: bobDevice.deviceCurve25519Key,
code: "m.blacklisted",
reason: "You have been blocked",
},
}));
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
event_id: "$event",
room_id: roomId,
content: {
algorithm: "m.megolm.v1.aes-sha2",
ciphertext: "blablabla",
device_id: "bobdevice",
sender_key: bobDevice.deviceCurve25519Key,
session_id: "session_id2",
},
}))).rejects.toThrow("The sender has blocked you.");
});
@@ -617,7 +692,7 @@ describe("MegolmDecryption", function() {
content: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
session_id: "session_id",
session_id: "session_id1",
sender_key: bobDevice.deviceCurve25519Key,
code: "m.no_olm",
reason: "Unable to establish a secure channel.",
@@ -638,7 +713,39 @@ describe("MegolmDecryption", function() {
ciphertext: "blablabla",
device_id: "bobdevice",
sender_key: bobDevice.deviceCurve25519Key,
session_id: "session_id",
session_id: "session_id1",
},
origin_server_ts: now,
}))).rejects.toThrow("The sender was unable to establish a secure channel.");
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
type: "m.room_key.withheld",
sender: "@bob:example.com",
content: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
session_id: "session_id2",
sender_key: bobDevice.deviceCurve25519Key,
code: "m.no_olm",
reason: "Unable to establish a secure channel.",
},
}));
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
event_id: "$event",
room_id: roomId,
content: {
algorithm: "m.megolm.v1.aes-sha2",
ciphertext: "blablabla",
device_id: "bobdevice",
sender_key: bobDevice.deviceCurve25519Key,
session_id: "session_id2",
},
origin_server_ts: now,
}))).rejects.toThrow("The sender was unable to establish a secure channel.");
+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([
+4 -4
View File
@@ -24,7 +24,7 @@ import * as algorithms from "../../../src/crypto/algorithms";
import { WebStorageSessionStore } from "../../../src/store/session/webstorage";
import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store";
import { MockStorageApi } from "../../MockStorageApi";
import * as testUtils from "../../test-utils";
import * as testUtils from "../../test-utils/test-utils";
import { OlmDevice } from "../../../src/crypto/OlmDevice";
import { Crypto } from "../../../src/crypto";
import { resetCrossSigningKeys } from "./crypto-utils";
@@ -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);
});
+50 -25
View File
@@ -17,13 +17,36 @@ 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';
import { resetCrossSigningKeys } from "./crypto-utils";
import { MatrixError } from '../../../src/http-api';
import { logger } from '../../../src/logger';
const PUSH_RULES_RESPONSE = {
method: "GET",
path: "/pushrules/",
data: {},
};
const filterResponse = function(userId) {
const filterPath = "/user/" + encodeURIComponent(userId) + "/filter";
return {
method: "POST",
path: filterPath,
data: { filter_id: "f1lt3r" },
};
};
function setHttpResponses(httpBackend, responses) {
responses.forEach(response => {
httpBackend
.when(response.method, response.path)
.respond(200, response.data);
});
}
async function makeTestClient(userInfo, options, keys) {
if (!keys) keys = {};
@@ -39,13 +62,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 +83,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 +103,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 +154,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 +184,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 +198,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,7 +259,7 @@ describe("Cross Signing", function() {
// feed sync result that includes master key, ssk, device key
const responses = [
HttpResponse.PUSH_RULES_RESPONSE,
PUSH_RULES_RESPONSE,
{
method: "POST",
path: "/keys/upload",
@@ -246,7 +270,7 @@ describe("Cross Signing", function() {
},
},
},
HttpResponse.filterResponse("@alice:example.com"),
filterResponse("@alice:example.com"),
{
method: "GET",
path: "/sync",
@@ -310,9 +334,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 +356,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 +439,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,7 +515,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.PUSH_RULES_RESPONSE,
PUSH_RULES_RESPONSE,
{
method: "POST",
path: "/keys/upload",
@@ -501,7 +526,7 @@ describe("Cross Signing", function() {
},
},
},
HttpResponse.filterResponse("@alice:example.com"),
filterResponse("@alice:example.com"),
{
method: "GET",
path: "/sync",
@@ -560,10 +585,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 +603,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 +672,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 +810,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 +822,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) {
+180
View File
@@ -0,0 +1,180 @@
/*
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 { MatrixClient, MatrixEvent, MatrixEventEvent, MatrixScheduler, Room } from "../../src";
import { eventMapperFor } from "../../src/event-mapper";
import { IStore } from "../../src/store";
describe("eventMapperFor", function() {
let rooms: Room[] = [];
const userId = "@test:example.org";
let client: MatrixClient;
beforeEach(() => {
client = new MatrixClient({
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
request: function() {} as any, // NOP
store: {
getRoom(roomId: string): Room | null {
return rooms.find(r => r.roomId === roomId);
},
} as IStore,
scheduler: {
setProcessFunction: jest.fn(),
} as unknown as MatrixScheduler,
userId: userId,
});
rooms = [];
});
it("should de-duplicate MatrixEvent instances by means of findEventById on the room object", async () => {
const roomId = "!room:example.org";
const room = new Room(roomId, client, userId);
rooms.push(room);
const mapper = eventMapperFor(client, {
preventReEmit: true,
decrypt: false,
});
const eventId = "$event1:server";
const eventDefinition = {
type: "m.room.message",
room_id: roomId,
sender: userId,
content: {
body: "body",
},
unsigned: {},
event_id: eventId,
};
const event = mapper(eventDefinition);
expect(event).toBeInstanceOf(MatrixEvent);
room.addLiveEvents([event]);
expect(room.findEventById(eventId)).toBe(event);
const event2 = mapper(eventDefinition);
expect(event).toBe(event2);
});
it("should not de-duplicate state events due to directionality of sentinel members", async () => {
const roomId = "!room:example.org";
const room = new Room(roomId, client, userId);
rooms.push(room);
const mapper = eventMapperFor(client, {
preventReEmit: true,
decrypt: false,
});
const eventId = "$event1:server";
const eventDefinition = {
type: "m.room.name",
room_id: roomId,
sender: userId,
content: {
name: "Room name",
},
unsigned: {},
event_id: eventId,
state_key: "",
};
const event = mapper(eventDefinition);
expect(event).toBeInstanceOf(MatrixEvent);
room.oldState.setStateEvents([event]);
room.currentState.setStateEvents([event]);
room.addLiveEvents([event]);
expect(room.findEventById(eventId)).toBe(event);
const event2 = mapper(eventDefinition);
expect(event).not.toBe(event2);
});
it("should decrypt appropriately", async () => {
const roomId = "!room:example.org";
const room = new Room(roomId, client, userId);
rooms.push(room);
const eventId = "$event1:server";
const eventDefinition = {
type: "m.room.encrypted",
room_id: roomId,
sender: userId,
content: {
ciphertext: "",
},
unsigned: {},
event_id: eventId,
};
const decryptEventIfNeededSpy = jest.spyOn(client, "decryptEventIfNeeded");
decryptEventIfNeededSpy.mockResolvedValue(); // stub it out
const mapper = eventMapperFor(client, {
decrypt: true,
});
const event = mapper(eventDefinition);
expect(event).toBeInstanceOf(MatrixEvent);
expect(decryptEventIfNeededSpy).toHaveBeenCalledWith(event);
});
it("should configure re-emitter appropriately", async () => {
const roomId = "!room:example.org";
const room = new Room(roomId, client, userId);
rooms.push(room);
const eventId = "$event1:server";
const eventDefinition = {
type: "m.room.message",
room_id: roomId,
sender: userId,
content: {
body: "body",
},
unsigned: {},
event_id: eventId,
};
const evListener = jest.fn();
client.on(MatrixEventEvent.Replaced, evListener);
const noReEmitMapper = eventMapperFor(client, {
preventReEmit: true,
});
const event1 = noReEmitMapper(eventDefinition);
expect(event1).toBeInstanceOf(MatrixEvent);
event1.emit(MatrixEventEvent.Replaced, event1);
expect(evListener).not.toHaveBeenCalled();
const reEmitMapper = eventMapperFor(client, {
preventReEmit: false,
});
const event2 = reEmitMapper(eventDefinition);
expect(event2).toBeInstanceOf(MatrixEvent);
event2.emit(MatrixEventEvent.Replaced, event2);
expect(evListener.mock.calls[0][0]).toEqual(event2);
expect(event1).not.toBe(event2); // the event wasn't added to a room so de-duplication wouldn't occur
});
});
+1 -1
View File
@@ -1,4 +1,4 @@
import * as utils from "../test-utils";
import * as utils from "../test-utils/test-utils";
import { EventTimeline } from "../../src/models/event-timeline";
import { RoomState } from "../../src/models/room-state";
+1 -1
View File
@@ -1,6 +1,6 @@
/*
Copyright 2017 New Vector Ltd
Copyright 2019 The Matrix.org Foundaction C.I.C.
Copyright 2019 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.
-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);
});
});
});
+170
View File
@@ -0,0 +1,170 @@
import {
MatrixEvent,
RelationType,
} from "../../src";
import { FilterComponent } from "../../src/filter-component";
import { mkEvent } from '../test-utils/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,
}) as MatrixEvent;
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,
}) as MatrixEvent;
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({
related_by_senders: [currentUserId],
}, currentUserId);
const threadRootNotParticipated = mkEvent({
type: 'm.room.message',
content: {},
room: 'roomId',
user: '@someone-else:server.org',
event: true,
unsigned: {
"m.relations": {
"m.thread": {
count: 2,
current_user_participated: false,
},
},
},
}) as MatrixEvent;
expect(filter.check(threadRootNotParticipated)).toBe(false);
});
it("should keep events by relation participation", function() {
const currentUserId = '@me:server.org';
const filter = new FilterComponent({
related_by_senders: [currentUserId],
}, currentUserId);
const threadRootParticipated = mkEvent({
type: 'm.room.message',
content: {},
unsigned: {
"m.relations": {
"m.thread": {
count: 2,
current_user_participated: true,
},
},
},
user: '@someone-else:server.org',
room: 'roomId',
event: true,
}) as MatrixEvent;
expect(filter.check(threadRootParticipated)).toBe(true);
});
it("should filter out events by relation type", function() {
const filter = new FilterComponent({
related_by_rel_types: ["m.thread"],
});
const referenceRelationEvent = mkEvent({
type: 'm.room.message',
content: {},
room: 'roomId',
event: true,
unsigned: {
"m.relations": {
[RelationType.Reference]: {},
},
},
}) as MatrixEvent;
expect(filter.check(referenceRelationEvent)).toBe(false);
});
it("should keep events by relation type", function() {
const filter = new FilterComponent({
related_by_rel_types: ["m.thread"],
});
const threadRootEvent = mkEvent({
type: 'm.room.message',
content: {},
unsigned: {
"m.relations": {
"m.thread": {
count: 2,
current_user_participated: true,
},
},
},
room: 'roomId',
event: true,
}) as MatrixEvent;
const eventWithMultipleRelations = mkEvent({
"type": "m.room.message",
"content": {},
"unsigned": {
"m.relations": {
"testtesttest": {},
"m.annotation": {
"chunk": [
{
"type": "m.reaction",
"key": "🤫",
"count": 1,
},
],
},
"m.thread": {
count: 2,
current_user_participated: true,
},
},
},
"room": 'roomId',
"event": true,
}) as MatrixEvent;
const noMatchEvent = mkEvent({
"type": "m.room.message",
"content": {},
"unsigned": {
"m.relations": {
"testtesttest": {},
},
},
"room": 'roomId',
"event": true,
}) as MatrixEvent;
expect(filter.check(threadRootEvent)).toBe(true);
expect(filter.check(eventWithMultipleRelations)).toBe(true);
expect(filter.check(noMatchEvent)).toBe(false);
});
});
});
+111
View File
@@ -0,0 +1,111 @@
/*
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, parseLocationEvent } from "../../src/content-helpers";
import {
M_ASSET,
LocationAssetType,
M_LOCATION,
M_TIMESTAMP,
LocationEventWireContent,
} from "../../src/@types/location";
import { TEXT_NODE_TYPE } from "../../src/@types/extensible_events";
import { MsgType } from "../../src/@types/event";
describe("Location", function() {
const defaultContent = {
"body": "Location geo:-36.24484561954707,175.46884959563613;u=10 at 2022-03-09T11:01:52.443Z",
"msgtype": "m.location",
"geo_uri": "geo:-36.24484561954707,175.46884959563613;u=10",
[M_LOCATION.name]: { "uri": "geo:-36.24484561954707,175.46884959563613;u=10", "description": null },
[M_ASSET.name]: { "type": "m.self" },
[TEXT_NODE_TYPE.name]: "Location geo:-36.24484561954707,175.46884959563613;u=10 at 2022-03-09T11:01:52.443Z",
[M_TIMESTAMP.name]: 1646823712443,
} as any;
const backwardsCompatibleEventContent = { ...defaultContent };
// eslint-disable-next-line camelcase
const { body, msgtype, geo_uri, ...modernProperties } = defaultContent;
const modernEventContent = { ...modernProperties };
const legacyEventContent = {
// eslint-disable-next-line camelcase
body, msgtype, geo_uri,
} as LocationEventWireContent;
it("should create a valid location with defaults", function() {
const loc = makeLocationContent(undefined, "geo:foo", 134235435);
expect(loc.body).toEqual('User Location geo:foo at 1970-01-02T13:17:15.435Z');
expect(loc.msgtype).toEqual(MsgType.Location);
expect(loc.geo_uri).toEqual("geo:foo");
expect(M_LOCATION.findIn(loc)).toEqual({
uri: "geo:foo",
description: undefined,
});
expect(M_ASSET.findIn(loc)).toEqual({ type: LocationAssetType.Self });
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual('User Location geo:foo at 1970-01-02T13:17:15.435Z');
expect(M_TIMESTAMP.findIn(loc)).toEqual(134235435);
});
it("should create a valid location with explicit properties", function() {
const loc = makeLocationContent(
undefined, "geo:bar", 134235436, "desc", LocationAssetType.Pin);
expect(loc.body).toEqual('Location "desc" geo:bar at 1970-01-02T13:17:15.436Z');
expect(loc.msgtype).toEqual(MsgType.Location);
expect(loc.geo_uri).toEqual("geo:bar");
expect(M_LOCATION.findIn(loc)).toEqual({
uri: "geo:bar",
description: "desc",
});
expect(M_ASSET.findIn(loc)).toEqual({ type: LocationAssetType.Pin });
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual('Location "desc" geo:bar at 1970-01-02T13:17:15.436Z');
expect(M_TIMESTAMP.findIn(loc)).toEqual(134235436);
});
it('parses backwards compatible event correctly', () => {
const eventContent = parseLocationEvent(backwardsCompatibleEventContent);
expect(eventContent).toEqual(backwardsCompatibleEventContent);
});
it('parses modern correctly', () => {
const eventContent = parseLocationEvent(modernEventContent);
expect(eventContent).toEqual(backwardsCompatibleEventContent);
});
it('parses legacy event correctly', () => {
const eventContent = parseLocationEvent(legacyEventContent);
const {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
[M_TIMESTAMP.name]: timestamp,
...expectedResult
} = defaultContent;
expect(eventContent).toEqual({
...expectedResult,
[M_LOCATION.name]: {
...expectedResult[M_LOCATION.name],
description: undefined,
},
});
// don't infer timestamp from legacy event
expect(M_TIMESTAMP.findIn(eventContent)).toBeFalsy();
});
});
@@ -1,3 +1,19 @@
/*
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 { logger } from "../../src/logger";
import { MatrixClient } from "../../src/client";
import { Filter } from "../../src/filter";
@@ -11,8 +27,12 @@ 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/test-utils";
import { makeBeaconInfoContent } from "../../src/content-helpers";
import { M_BEACON_INFO } from "../../src/@types/beacon";
import { Room } from "../../src";
jest.useFakeTimers();
@@ -84,11 +104,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 +132,7 @@ describe("MatrixClient", function() {
}
if (next.error) {
// eslint-disable-next-line
return Promise.reject({
errcode: next.error.errcode,
httpStatus: next.error.httpStatus,
@@ -126,7 +143,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 +168,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,
@@ -237,6 +254,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 +288,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 +349,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 +381,16 @@ 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 = [
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 +411,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 +431,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,6 +465,7 @@ describe("MatrixClient", function() {
describe("retryImmediately", function() {
it("should return false if there is no request waiting", async function() {
httpLookups = [];
await client.startClient();
expect(client.retryImmediately()).toBe(false);
});
@@ -462,7 +507,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) {
@@ -542,33 +587,36 @@ describe("MatrixClient", function() {
client.startClient();
});
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,
});
// Disabled because now `startClient` makes a legit call to `/versions`
// And those tests are really unhappy about it... Not possible to figure
// out what a good resolution would look like
xit("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,
});
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 = [];
@@ -578,7 +626,7 @@ describe("MatrixClient", function() {
client.startClient();
});
it("should transition SYNCING -> ERROR after a failed /sync", function(done) {
xit("should transition SYNCING -> ERROR after a failed /sync", function(done) {
acceptKeepalives = false;
const expectedStates = [];
httpLookups.push({
@@ -598,34 +646,34 @@ 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) {
xit("should transition ERROR -> ERROR if keepalive keeps failing", function(done) {
acceptKeepalives = false;
const expectedStates = [];
httpLookups.push({
@@ -671,7 +719,7 @@ 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({
method: "GET",
path: "/sync",
@@ -702,4 +750,282 @@ 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 room = new Room("!room1:matrix.org", client, userId);
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": {
"m.thread": {
"latest_event": {},
"count": 33,
"current_user_participated": false,
},
},
},
"event_id": "$ev1",
"user_id": "@alice:matrix.org",
});
expect(rootEvent.isThreadRoot).toBe(true);
const [roomEvents, threadEvents] = client.partitionThreadedEvents(room, [rootEvent]);
expect(roomEvents).toHaveLength(1);
expect(threadEvents).toHaveLength(1);
// Restore method
client.supportsExperimentalThreads = supportsExperimentalThreads;
});
});
describe("beacons", () => {
const roomId = '!room:server.org';
const content = makeBeaconInfoContent(100, true);
beforeEach(() => {
client.http.authedRequest.mockClear().mockResolvedValue({});
});
it("creates new beacon info", async () => {
await client.unstable_createLiveBeacon(roomId, content, '123');
// event type combined
const expectedEventType = `${M_BEACON_INFO.name}.${userId}.123`;
const [callback, method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
expect(callback).toBeFalsy();
expect(method).toBe('PUT');
expect(path).toEqual(
`/rooms/${encodeURIComponent(roomId)}/state/` +
`${encodeURIComponent(expectedEventType)}/${encodeURIComponent(userId)}`,
);
expect(queryParams).toBeFalsy();
expect(requestContent).toEqual(content);
});
it("updates beacon info with specific event type", async () => {
const eventType = `${M_BEACON_INFO.name}.${userId}.456`;
await client.unstable_setLiveBeacon(roomId, eventType, content);
// event type combined
const [, , path, , requestContent] = client.http.authedRequest.mock.calls[0];
expect(path).toEqual(
`/rooms/${encodeURIComponent(roomId)}/state/` +
`${encodeURIComponent(eventType)}/${encodeURIComponent(userId)}`,
);
expect(requestContent).toEqual(content);
});
});
});
+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);
});
});
+281
View File
@@ -0,0 +1,281 @@
/*
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 { EventType } from "../../../src";
import { M_BEACON_INFO } from "../../../src/@types/beacon";
import {
isTimestampInDuration,
isBeaconInfoEventType,
Beacon,
BeaconEvent,
} from "../../../src/models/beacon";
import { makeBeaconInfoEvent } from "../../test-utils/beacon";
jest.useFakeTimers();
describe('Beacon', () => {
describe('isTimestampInDuration()', () => {
const startTs = new Date('2022-03-11T12:07:47.592Z').getTime();
const HOUR_MS = 3600000;
it('returns false when timestamp is before start time', () => {
// day before
const timestamp = new Date('2022-03-10T12:07:47.592Z').getTime();
expect(isTimestampInDuration(startTs, HOUR_MS, timestamp)).toBe(false);
});
it('returns false when timestamp is after start time + duration', () => {
// 1 second later
const timestamp = new Date('2022-03-10T12:07:48.592Z').getTime();
expect(isTimestampInDuration(startTs, HOUR_MS, timestamp)).toBe(false);
});
it('returns true when timestamp is exactly start time', () => {
expect(isTimestampInDuration(startTs, HOUR_MS, startTs)).toBe(true);
});
it('returns true when timestamp is exactly the end of the duration', () => {
expect(isTimestampInDuration(startTs, HOUR_MS, startTs + HOUR_MS)).toBe(true);
});
it('returns true when timestamp is within the duration', () => {
const twoHourDuration = HOUR_MS * 2;
const now = startTs + HOUR_MS;
expect(isTimestampInDuration(startTs, twoHourDuration, now)).toBe(true);
});
});
describe('isBeaconInfoEventType', () => {
it.each([
EventType.CallAnswer,
`prefix.${M_BEACON_INFO.name}`,
`prefix.${M_BEACON_INFO.altName}`,
])('returns false for %s', (type) => {
expect(isBeaconInfoEventType(type)).toBe(false);
});
it.each([
M_BEACON_INFO.name,
M_BEACON_INFO.altName,
`${M_BEACON_INFO.name}.@test:server.org.12345`,
`${M_BEACON_INFO.altName}.@test:server.org.12345`,
])('returns true for %s', (type) => {
expect(isBeaconInfoEventType(type)).toBe(true);
});
});
describe('Beacon', () => {
const userId = '@user:server.org';
const roomId = '$room:server.org';
// 14.03.2022 16:15
const now = 1647270879403;
const HOUR_MS = 3600000;
// beacon_info events
// created 'an hour ago'
// without timeout of 3 hours
let liveBeaconEvent;
let notLiveBeaconEvent;
const advanceDateAndTime = (ms: number) => {
// bc liveness check uses Date.now we have to advance this mock
jest.spyOn(global.Date, 'now').mockReturnValue(now + ms);
// then advance time for the interval by the same amount
jest.advanceTimersByTime(ms);
};
beforeEach(() => {
// go back in time to create the beacon
jest.spyOn(global.Date, 'now').mockReturnValue(now - HOUR_MS);
liveBeaconEvent = makeBeaconInfoEvent(
userId,
roomId,
{
timeout: HOUR_MS * 3,
isLive: true,
},
'$live123',
'$live123',
);
notLiveBeaconEvent = makeBeaconInfoEvent(
userId,
roomId,
{ timeout: HOUR_MS * 3, isLive: false },
'$dead123',
'$dead123',
);
// back to now
jest.spyOn(global.Date, 'now').mockReturnValue(now);
});
afterAll(() => {
jest.spyOn(global.Date, 'now').mockRestore();
});
it('creates beacon from event', () => {
const beacon = new Beacon(liveBeaconEvent);
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
expect(beacon.roomId).toEqual(roomId);
expect(beacon.isLive).toEqual(true);
expect(beacon.beaconInfoOwner).toEqual(userId);
expect(beacon.beaconInfoEventType).toEqual(liveBeaconEvent.getType());
expect(beacon.identifier).toEqual(liveBeaconEvent.getType());
expect(beacon.beaconInfo).toBeTruthy();
});
describe('isLive()', () => {
it('returns false when beacon is explicitly set to not live', () => {
const beacon = new Beacon(notLiveBeaconEvent);
expect(beacon.isLive).toEqual(false);
});
it('returns false when beacon is expired', () => {
// time travel to beacon creation + 3 hours
jest.spyOn(global.Date, 'now').mockReturnValue(now - 3 * HOUR_MS);
const beacon = new Beacon(liveBeaconEvent);
expect(beacon.isLive).toEqual(false);
});
it('returns false when beacon timestamp is in future', () => {
// time travel to before beacon events timestamp
// event was created now - 1 hour
jest.spyOn(global.Date, 'now').mockReturnValue(now - HOUR_MS - HOUR_MS);
const beacon = new Beacon(liveBeaconEvent);
expect(beacon.isLive).toEqual(false);
});
it('returns true when beacon was created in past and not yet expired', () => {
// liveBeaconEvent was created 1 hour ago
const beacon = new Beacon(liveBeaconEvent);
expect(beacon.isLive).toEqual(true);
});
});
describe('update()', () => {
it('does not update with different event', () => {
const beacon = new Beacon(liveBeaconEvent);
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
expect(() => beacon.update(notLiveBeaconEvent)).toThrow();
expect(beacon.isLive).toEqual(true);
});
it('updates event', () => {
const beacon = new Beacon(liveBeaconEvent);
const emitSpy = jest.spyOn(beacon, 'emit');
expect(beacon.isLive).toEqual(true);
const updatedBeaconEvent = makeBeaconInfoEvent(
userId, roomId, { timeout: HOUR_MS * 3, isLive: false }, '$live123', '$live123');
beacon.update(updatedBeaconEvent);
expect(beacon.isLive).toEqual(false);
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.Update, updatedBeaconEvent, beacon);
});
it('emits livenesschange event when beacon liveness changes', () => {
const beacon = new Beacon(liveBeaconEvent);
const emitSpy = jest.spyOn(beacon, 'emit');
expect(beacon.isLive).toEqual(true);
const updatedBeaconEvent = makeBeaconInfoEvent(
userId,
roomId,
{ timeout: HOUR_MS * 3, isLive: false },
beacon.beaconInfoId,
'$live123',
);
beacon.update(updatedBeaconEvent);
expect(beacon.isLive).toEqual(false);
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LivenessChange, false, beacon);
});
});
describe('monitorLiveness()', () => {
it('does not set a monitor interval when beacon is not live', () => {
// beacon was created an hour ago
// and has a 3hr duration
const beacon = new Beacon(notLiveBeaconEvent);
const emitSpy = jest.spyOn(beacon, 'emit');
beacon.monitorLiveness();
// @ts-ignore
expect(beacon.livenessWatchInterval).toBeFalsy();
advanceDateAndTime(HOUR_MS * 2 + 1);
// no emit
expect(emitSpy).not.toHaveBeenCalled();
});
it('checks liveness of beacon at expected expiry time', () => {
// live beacon was created an hour ago
// and has a 3hr duration
const beacon = new Beacon(liveBeaconEvent);
expect(beacon.isLive).toBeTruthy();
const emitSpy = jest.spyOn(beacon, 'emit');
beacon.monitorLiveness();
advanceDateAndTime(HOUR_MS * 2 + 1);
expect(emitSpy).toHaveBeenCalledTimes(1);
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LivenessChange, false, beacon);
});
it('clears monitor interval when re-monitoring liveness', () => {
// live beacon was created an hour ago
// and has a 3hr duration
const beacon = new Beacon(liveBeaconEvent);
expect(beacon.isLive).toBeTruthy();
beacon.monitorLiveness();
// @ts-ignore
const oldMonitor = beacon.livenessWatchInterval;
beacon.monitorLiveness();
// @ts-ignore
expect(beacon.livenessWatchInterval).not.toEqual(oldMonitor);
});
it('destroy kills liveness monitor and emits', () => {
// live beacon was created an hour ago
// and has a 3hr duration
const beacon = new Beacon(liveBeaconEvent);
expect(beacon.isLive).toBeTruthy();
const emitSpy = jest.spyOn(beacon, 'emit');
beacon.monitorLiveness();
// destroy the beacon
beacon.destroy();
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.Destroy, beacon.identifier);
// live forced to false
expect(beacon.isLive).toBe(false);
advanceDateAndTime(HOUR_MS * 2 + 1);
// no additional calls
expect(emitSpy).toHaveBeenCalledTimes(1);
});
});
});
});
+17 -1
View File
@@ -1,4 +1,4 @@
import * as utils from "../test-utils";
import * as utils from "../test-utils/test-utils";
import { PushProcessor } from "../../src/pushprocessor";
describe('NotificationService', function() {
@@ -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
@@ -1,4 +1,4 @@
import * as utils from "../test-utils";
import * as utils from "../test-utils/test-utils";
import { RoomMember } from "../../src/models/room-member";
describe("RoomMember", function() {
+93 -3
View File
@@ -1,5 +1,8 @@
import * as utils from "../test-utils";
import { RoomState } from "../../src/models/room-state";
import * as utils from "../test-utils/test-utils";
import { makeBeaconInfoEvent } from "../test-utils/beacon";
import { filterEmitCallsByEventType } from "../test-utils/emitter";
import { RoomState, RoomStateEvent } from "../../src/models/room-state";
import { BeaconEvent } from "../../src/models/beacon";
describe("RoomState", function() {
const roomId = "!foo:bar";
@@ -120,7 +123,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",
});
});
@@ -248,6 +251,93 @@ describe("RoomState", function() {
memberEvent, state,
);
});
describe('beacon events', () => {
it('adds new beacon info events to state and emits', () => {
const beaconEvent = makeBeaconInfoEvent(userA, roomId);
const emitSpy = jest.spyOn(state, 'emit');
state.setStateEvents([beaconEvent]);
expect(state.beacons.size).toEqual(1);
const beaconInstance = state.beacons.get(beaconEvent.getType());
expect(beaconInstance).toBeTruthy();
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.New, beaconEvent, beaconInstance);
});
it('does not add redacted beacon info events to state', () => {
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId);
const redactionEvent = { event: { type: 'm.room.redaction' } };
redactedBeaconEvent.makeRedacted(redactionEvent);
const emitSpy = jest.spyOn(state, 'emit');
state.setStateEvents([redactedBeaconEvent]);
// no beacon added
expect(state.beacons.size).toEqual(0);
expect(state.beacons.get(redactedBeaconEvent.getType)).toBeFalsy();
// no new beacon emit
expect(filterEmitCallsByEventType(BeaconEvent.New, emitSpy).length).toBeFalsy();
});
it('updates existing beacon info events in state', () => {
const beaconId = '$beacon1';
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId, beaconId);
const updatedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, beaconId, beaconId);
state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(beaconEvent.getType());
expect(beaconInstance.isLive).toEqual(true);
state.setStateEvents([updatedBeaconEvent]);
// same Beacon
expect(state.beacons.get(beaconEvent.getType())).toBe(beaconInstance);
// updated liveness
expect(state.beacons.get(beaconEvent.getType()).isLive).toEqual(false);
});
it('destroys and removes redacted beacon events', () => {
const beaconId = '$beacon1';
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId, beaconId);
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId, beaconId);
const redactionEvent = { event: { type: 'm.room.redaction' } };
redactedBeaconEvent.makeRedacted(redactionEvent);
state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(beaconEvent.getType());
const destroySpy = jest.spyOn(beaconInstance, 'destroy');
expect(beaconInstance.isLive).toEqual(true);
state.setStateEvents([redactedBeaconEvent]);
expect(destroySpy).toHaveBeenCalled();
expect(state.beacons.get(beaconEvent.getType())).toBe(undefined);
});
it('updates live beacon ids once after setting state events', () => {
const liveBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, '$beacon1', '$beacon1');
const deadBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, '$beacon2', '$beacon2');
const emitSpy = jest.spyOn(state, 'emit');
state.setStateEvents([liveBeaconEvent, deadBeaconEvent]);
// called once
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(1);
// live beacon is now not live
const updatedLiveBeaconEvent = makeBeaconInfoEvent(
userA, roomId, { isLive: false }, liveBeaconEvent.getId(), '$beacon1',
);
state.setStateEvents([updatedLiveBeaconEvent]);
expect(state.hasLiveBeacons).toBe(false);
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(2);
expect(emitSpy).toHaveBeenCalledWith(RoomStateEvent.BeaconLiveness, state, false);
});
});
});
describe("setOutOfBandMembers", function() {
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -4,7 +4,7 @@
import { defer } from '../../src/utils';
import { MatrixError } from "../../src/http-api";
import { MatrixScheduler } from "../../src/scheduler";
import * as utils from "../test-utils";
import * as utils from "../test-utils/test-utils";
jest.useFakeTimers();
+1 -1
View File
@@ -1,6 +1,6 @@
import { EventTimeline } from "../../src/models/event-timeline";
import { TimelineIndex, TimelineWindow } from "../../src/timeline-window";
import * as utils from "../test-utils";
import * as utils from "../test-utils/test-utils";
const ROOM_ID = "roomId";
const USER_ID = "userId";
+1 -1
View File
@@ -1,5 +1,5 @@
import { User } from "../../src/models/user";
import * as utils from "../test-utils";
import * as utils from "../test-utils/test-utils";
describe("User", function() {
const userId = "@alice:bar";
+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 */
+147
View File
@@ -0,0 +1,147 @@
/*
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 { EitherAnd, RELATES_TO_RELATIONSHIP, REFERENCE_RELATION } from "matrix-events-sdk";
import { UnstableValue } from "../NamespacedValue";
import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location";
/**
* Beacon info and beacon event types as described in MSC3489
* https://github.com/matrix-org/matrix-spec-proposals/pull/3489
*/
/**
* Beacon info events are state events.
* We have two requirements for these events:
* 1. they can only be written by their owner
* 2. a user can have an arbitrary number of beacon_info events
*
* 1. is achieved by setting the state_key to the owners mxid.
* Event keys in room state are a combination of `type` + `state_key`.
* To achieve an arbitrary number of only owner-writable state events
* we introduce a variable suffix to the event type
*
* Eg
* {
* "type": "m.beacon_info.@matthew:matrix.org.1",
* "state_key": "@matthew:matrix.org",
* "content": {
* "m.beacon_info": {
* "description": "The Matthew Tracker",
* "timeout": 86400000,
* },
* // more content as described below
* }
* },
* {
* "type": "m.beacon_info.@matthew:matrix.org.2",
* "state_key": "@matthew:matrix.org",
* "content": {
* "m.beacon_info": {
* "description": "Another different Matthew tracker",
* "timeout": 400000,
* },
* // more content as described below
* }
* }
*/
/**
* Variable event type for m.beacon_info
*/
export const M_BEACON_INFO_VARIABLE = new UnstableValue("m.beacon_info.*", "org.matrix.msc3489.beacon_info.*");
/**
* Non-variable type for m.beacon_info event content
*/
export const M_BEACON_INFO = new UnstableValue("m.beacon_info", "org.matrix.msc3489.beacon_info");
export const M_BEACON = new UnstableValue("m.beacon", "org.matrix.msc3489.beacon");
export type MBeaconInfoContent = {
description?: string;
// how long from the last event until we consider the beacon inactive in milliseconds
timeout: number;
// true when this is a live location beacon
// https://github.com/matrix-org/matrix-spec-proposals/pull/3672
live?: boolean;
};
export type MBeaconInfoEvent = EitherAnd<
{ [M_BEACON_INFO.name]: MBeaconInfoContent },
{ [M_BEACON_INFO.altName]: MBeaconInfoContent }
>;
/**
* m.beacon_info Event example from the spec
* https://github.com/matrix-org/matrix-spec-proposals/pull/3489
* {
"type": "m.beacon_info.@matthew:matrix.org.1",
"state_key": "@matthew:matrix.org",
"content": {
"m.beacon_info": {
"description": "The Matthew Tracker", // same as an `m.location` description
"timeout": 86400000, // how long from the last event until we consider the beacon inactive in milliseconds
},
"m.ts": 1436829458432, // creation timestamp of the beacon on the client
"m.asset": {
"type": "m.self" // the type of asset being tracked as per MSC3488
}
}
}
*/
/**
* m.beacon_info.* event content
*/
export type MBeaconInfoEventContent = &
MBeaconInfoEvent &
// creation timestamp of the beacon on the client
MTimestampEvent &
// the type of asset being tracked as per MSC3488
MAssetEvent;
/**
* m.beacon event example
* https://github.com/matrix-org/matrix-spec-proposals/pull/3489
*
* {
"type": "m.beacon",
"sender": "@matthew:matrix.org",
"content": {
"m.relates_to": { // from MSC2674: https://github.com/matrix-org/matrix-doc/pull/2674
"rel_type": "m.reference", // from MSC3267: https://github.com/matrix-org/matrix-doc/pull/3267
"event_id": "$beacon_info"
},
"m.location": {
"uri": "geo:51.5008,0.1247;u=35",
"description": "Arbitrary beacon information"
},
"m.ts": 1636829458432,
}
}
*/
/**
* Content of an m.beacon event
*/
export type MBeaconEventContent = &
MLocationEvent &
// timestamp when location was taken
MTimestampEvent &
// relates to a beacon_info event
RELATES_TO_RELATIONSHIP<typeof REFERENCE_RELATION>;
+15
View File
@@ -92,6 +92,8 @@ export enum EventType {
export enum RelationType {
Annotation = "m.annotation",
Replace = "m.replace",
Reference = "m.reference",
Thread = "m.thread",
}
export enum MsgType {
@@ -103,12 +105,15 @@ export enum MsgType {
Audio = "m.audio",
Location = "m.location",
Video = "m.video",
KeyVerificationRequest = "m.key.verification.request",
}
export const RoomCreateTypeField = "type";
export enum RoomType {
Space = "m.space",
UnstableCall = "org.matrix.msc3417.call",
ElementVideo = "io.element.video",
}
/**
@@ -168,6 +173,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;
}
}
+97
View File
@@ -0,0 +1,97 @@
/*
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 { EitherAnd } from "matrix-events-sdk";
import { UnstableValue } from "../NamespacedValue";
import { TEXT_NODE_TYPE } from "./extensible_events";
export enum LocationAssetType {
Self = "m.self",
Pin = "m.pin",
}
export const M_ASSET = new UnstableValue("m.asset", "org.matrix.msc3488.asset");
export type MAssetContent = { type: LocationAssetType };
/**
* The event definition for an m.asset event (in content)
*/
export type MAssetEvent = EitherAnd<{ [M_ASSET.name]: MAssetContent }, { [M_ASSET.altName]: MAssetContent }>;
export const M_TIMESTAMP = new UnstableValue("m.ts", "org.matrix.msc3488.ts");
/**
* The event definition for an m.ts event (in content)
*/
export type MTimestampEvent = EitherAnd<{ [M_TIMESTAMP.name]: number }, { [M_TIMESTAMP.altName]: number }>;
export const M_LOCATION = new UnstableValue(
"m.location", "org.matrix.msc3488.location");
export type MLocationContent = {
uri: string;
description?: string | null;
};
export type MLocationEvent = EitherAnd<
{ [M_LOCATION.name]: MLocationContent },
{ [M_LOCATION.altName]: MLocationContent }
>;
export type MTextEvent = EitherAnd<{ [TEXT_NODE_TYPE.name]: string }, { [TEXT_NODE_TYPE.altName]: string }>;
/* 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,
}
}
*/
type OptionalTimestampEvent = MTimestampEvent | undefined;
/**
* The content for an m.location event
*/
export type MLocationEventContent = &
MLocationEvent &
MAssetEvent &
MTextEvent &
OptionalTimestampEvent;
export type LegacyLocationEventContent = {
body: string;
msgtype: string;
geo_uri: string;
};
/**
* Possible content for location events as sent over the wire
*/
export type LocationEventWireContent = Partial<LegacyLocationEventContent & MLocationEventContent>;
export type ILocationContent = MLocationEventContent & LegacyLocationEventContent;
+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;
}
+16
View File
@@ -70,6 +70,22 @@ export class NamespacedValue<S extends string, U extends string> {
}
}
export class ServerControlledNamespacedValue<S extends string, U extends string>
extends NamespacedValue<S, U> {
private preferUnstable = false;
public setPreferUnstable(preferUnstable: boolean): void {
this.preferUnstable = preferUnstable;
}
public get name(): U | S {
if (this.stable && !this.preferUnstable) {
return this.stable;
}
return this.unstable;
}
}
/**
* Represents a namespaced value which prioritizes the unstable value over the stable
* value.
+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
+1384 -858
View File
File diff suppressed because it is too large Load Diff
+166 -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.
@@ -17,7 +16,22 @@ limitations under the License.
/** @module ContentHelpers */
import { REFERENCE_RELATION } from "matrix-events-sdk";
import { MBeaconEventContent, MBeaconInfoContent, MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
import { MsgType } from "./@types/event";
import { TEXT_NODE_TYPE } from "./@types/extensible_events";
import {
M_ASSET,
LocationAssetType,
M_LOCATION,
M_TIMESTAMP,
LocationEventWireContent,
MLocationEventContent,
MLocationContent,
MAssetContent,
LegacyLocationEventContent,
} from "./@types/location";
/**
* Generates the content for a HTML Message event
@@ -99,3 +113,153 @@ export function makeEmoteMessage(body: string) {
body: body,
};
}
/** Location content helpers */
export const getTextForLocationEvent = (
uri: string,
assetType: LocationAssetType,
timestamp: number,
description?: string,
): string => {
const date = `at ${new Date(timestamp).toISOString()}`;
const assetName = assetType === LocationAssetType.Self ? 'User' : undefined;
const quotedDescription = description ? `"${description}"` : undefined;
return [
assetName,
'Location',
quotedDescription,
uri,
date,
].filter(Boolean).join(' ');
};
/**
* Generates the content for a Location event
* @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"
* @param text optional. A text for the location
*/
export const makeLocationContent = (
// this is first but optional
// to avoid a breaking change
text: string | undefined,
uri: string,
timestamp?: number,
description?: string,
assetType?: LocationAssetType,
): LegacyLocationEventContent & MLocationEventContent => {
const defaultedText = text ??
getTextForLocationEvent(uri, assetType || LocationAssetType.Self, timestamp, description);
const timestampEvent = timestamp ? { [M_TIMESTAMP.name]: timestamp } : {};
return {
msgtype: MsgType.Location,
body: defaultedText,
geo_uri: uri,
[M_LOCATION.name]: {
description,
uri,
},
[M_ASSET.name]: {
type: assetType || LocationAssetType.Self,
},
[TEXT_NODE_TYPE.name]: defaultedText,
...timestampEvent,
} as LegacyLocationEventContent & MLocationEventContent;
};
/**
* Parse location event content and transform to
* a backwards compatible modern m.location event format
*/
export const parseLocationEvent = (wireEventContent: LocationEventWireContent): MLocationEventContent => {
const location = M_LOCATION.findIn<MLocationContent>(wireEventContent);
const asset = M_ASSET.findIn<MAssetContent>(wireEventContent);
const timestamp = M_TIMESTAMP.findIn<number>(wireEventContent);
const text = TEXT_NODE_TYPE.findIn<string>(wireEventContent);
const geoUri = location?.uri ?? wireEventContent?.geo_uri;
const description = location?.description;
const assetType = asset?.type ?? LocationAssetType.Self;
const fallbackText = text ?? wireEventContent.body;
return makeLocationContent(fallbackText, geoUri, timestamp, description, assetType);
};
/**
* Beacon event helpers
*/
export type MakeBeaconInfoContent = (
timeout: number,
isLive?: boolean,
description?: string,
assetType?: LocationAssetType,
timestamp?: number
) => MBeaconInfoEventContent;
export const makeBeaconInfoContent: MakeBeaconInfoContent = (
timeout,
isLive,
description,
assetType,
timestamp,
) => ({
[M_BEACON_INFO.name]: {
description,
timeout,
live: isLive,
},
[M_TIMESTAMP.name]: timestamp || Date.now(),
[M_ASSET.name]: {
type: assetType ?? LocationAssetType.Self,
},
});
export type BeaconInfoState = MBeaconInfoContent & {
assetType: LocationAssetType;
timestamp: number;
};
/**
* Flatten beacon info event content
*/
export const parseBeaconInfoContent = (content: MBeaconInfoEventContent): BeaconInfoState => {
const { description, timeout, live } = M_BEACON_INFO.findIn<MBeaconInfoContent>(content);
const { type: assetType } = M_ASSET.findIn<MAssetContent>(content);
const timestamp = M_TIMESTAMP.findIn<number>(content);
return {
description,
timeout,
live,
assetType,
timestamp,
};
};
export type MakeBeaconContent = (
uri: string,
timestamp: number,
beaconInfoId: string,
description?: string,
) => MBeaconEventContent;
export const makeBeaconContent: MakeBeaconContent = (
uri,
timestamp,
beaconInfoId,
description,
) => ({
[M_LOCATION.name]: {
description,
uri,
},
[M_TIMESTAMP.name]: timestamp,
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: beaconInfoId,
},
});
+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;
+80 -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;
@@ -684,8 +709,9 @@ class MegolmEncryption extends EncryptionAlgorithm {
}
await this.baseApis.sendToDevice("org.matrix.room_key.withheld", contentMap);
await this.baseApis.sendToDevice("m.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 +745,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 +754,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 +860,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 +878,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 +917,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 +947,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 +1113,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 +1312,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 +1406,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 +1425,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 +1767,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 +1801,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)) {
+261 -166
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")
@@ -867,7 +920,7 @@ export class Crypto extends EventEmitter {
// secrets using it, in theory. We could move them to the new key but a)
// that would mean we'd need to prompt for the old passphrase, and b)
// it's not clear that would be the right thing to do anyway.
const { keyInfo, privateKey } = await createSecretStorageKey();
const { keyInfo = {} as IAddSecretStorageKeyOpts, privateKey } = await createSecretStorageKey();
newKeyId = await createSSSS(keyInfo, privateKey);
} else if (!storageExists && keyBackupInfo) {
// we have an existing backup, but no SSSS
@@ -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: ` +
@@ -3035,7 +3115,8 @@ export class Crypto extends EventEmitter {
this.secretStorage.onRequestReceived(event);
} else if (event.getType() === "m.secret.send") {
this.secretStorage.onSecretReceived(event);
} else if (event.getType() === "org.matrix.room_key.withheld") {
} else if (event.getType() === "m.room_key.withheld"
|| event.getType() === "org.matrix.room_key.withheld") {
this.onRoomKeyWithheldEvent(event);
} else if (event.getContent().transaction_id) {
this.onKeyVerificationMessage(event);
@@ -3046,7 +3127,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 +3206,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 +3224,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 +3247,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 +3255,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 +3311,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 +3375,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 +3612,7 @@ export class Crypto extends EventEmitter {
return;
}
this.emit("crypto.roomKeyRequest", req);
this.emit(CryptoEvent.RoomKeyRequest, req);
}
/**
@@ -3536,7 +3631,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";
/**

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