Compare commits

..

283 Commits

Author SHA1 Message Date
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
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
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
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
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
Š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
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
Dariusz Niemczyk 4bc8f968e0 Merge pull request #1932 from matrix-org/palid/threads-typescriptify
Fix PR comments
2021-09-17 08:11:36 +02: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
RiotRobot b68f649414 v12.3.1 2021-08-17 09:24:43 +01:00
RiotRobot 543f35d9a8 Prepare changelog for v12.3.1 2021-08-17 09:24:42 +01:00
David Baker 03d42a98cd Merge pull request #1862 from matrix-org/dbkr/release_script_handle_no_signing_id_rel_take_2
Fix release script to handle no signing ID
2021-08-17 09:19:30 +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
David Baker 85d9c0f403 Merge pull request #1860 from matrix-org/dbkr/1858_rel
Fix multiple VoIP regressions
2021-08-17 09:11:36 +01:00
David Baker 66b4279dd8 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-17 09:10:11 +01:00
Šimon Brandner 1ee84fbeca Fix regressions
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-17 09:06:53 +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
RiotRobot 10e8aff46a v12.3.0 2021-08-16 13:52:36 +01:00
RiotRobot 81e2ea6a61 Prepare changelog for v12.3.0 2021-08-16 13:52:35 +01:00
David Baker 2594c59250 Merge pull request #1855 from matrix-org/dbkr/update_allchange
Update changelog generator
2021-08-16 13:31:28 +01:00
David Baker b56a4ee6ec Fix dates 2021-08-16 13:24:11 +01:00
David Baker ab524ad3b4 Update changelog generator 2021-08-16 13:18:06 +01:00
David Baker 83bf80feec Merge pull request #1854 from SimonBrandner/release/fix/glare/18538
[Release] Fix glare related regressions
2021-08-16 12:40:08 +01:00
Šimon Brandner b9e5417218 Await handleCallEvent()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-16 13:30:22 +02:00
Šimon Brandner 082169a756 Fix glare
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-16 13:30:21 +02:00
Šimon Brandner cdc87d371c Reformat some code
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-16 13:30:21 +02:00
Šimon Brandner f8ec3bc591 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-16 13:30:21 +02:00
Šimon Brandner 85f4651e9b Simplifie some code
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-16 13:30:20 +02:00
Šimon Brandner da53241c48 Remove unnecessary logs
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-16 13:30:20 +02:00
Šimon Brandner fa265296e8 Add a comment
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-16 13:30:19 +02: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
75 changed files with 5766 additions and 4182 deletions
+92 -5
View File
@@ -1,18 +1,105 @@
Changes in [12.3.0-rc.2](https://github.com/vector-im/element-desktop/releases/tag/v12.3.0-rc.2) (2021-07-12)
=============================================================================================================
Changes in [14.0.0](https://github.com/vector-im/element-desktop/releases/tag/v14.0.0) (2021-10-11)
===================================================================================================
## 🚨 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 the types in shipped package ([\#1842](https://github.com/matrix-org/matrix-js-sdk/pull/1842)). Fixes vector-im/element-web#18503 and vector-im/element-web#18503.
* 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 [12.3.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v12.3.0-rc.1) (2021-07-11)
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)
===================================================================================================
## 🐛 Bug Fixes
* Fix multiple VoIP regressions ([\#1860](https://github.com/matrix-org/matrix-js-sdk/pull/1860)).
Changes in [12.3.0](https://github.com/vector-im/element-desktop/releases/tag/v12.3.0) (2021-08-16)
===================================================================================================
## ✨ Features
* Support for MSC3291: Muting in VoIP calls ([\#1812](https://github.com/matrix-org/matrix-js-sdk/pull/1812)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Support for screen-sharing using multi-stream VoIP (MSC3077) ([\#1685](https://github.com/matrix-org/matrix-js-sdk/pull/1685)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Handle DTMF support ([\#1813](https://github.com/matrix-org/matrix-js-sdk/pull/1813)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
## 🐛 Bug Fixes
* [Release] Fix glare related regressions ([\#1854](https://github.com/matrix-org/matrix-js-sdk/pull/1854)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Fix the types in shipped package ([\#1842](https://github.com/matrix-org/matrix-js-sdk/pull/1842)). Fixes vector-im/element-web#18503 and vector-im/element-web#18503.
* Fix error on turning off screensharing ([\#1833](https://github.com/matrix-org/matrix-js-sdk/pull/1833)). Fixes vector-im/element-web#18449. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Fix blank profile in join events ([\#1837](https://github.com/matrix-org/matrix-js-sdk/pull/1837)). Fixes vector-im/element-web#18321.
* fix TURN by fixing regression preventing multiple ICE candidates from sending. ([\#1838](https://github.com/matrix-org/matrix-js-sdk/pull/1838)).
@@ -20,7 +107,7 @@ Changes in [12.3.0-rc.1](https://github.com/vector-im/element-desktop/releases/t
* Apply hidden char check to rawDisplayName too ([\#1816](https://github.com/matrix-org/matrix-js-sdk/pull/1816)).
* Only clear bit 63 when we create the IV ([\#1819](https://github.com/matrix-org/matrix-js-sdk/pull/1819)).
Changes in [12.2.0](https://github.com/vector-im/element-desktop/releases/tag/v12.2.0) (2021-07-02)
Changes in [12.2.0](https://github.com/vector-im/element-desktop/releases/tag/v12.2.0) (2021-08-02)
===================================================================================================
## ✨ Features
+8 -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
+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>
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "12.3.0-rc.2",
"version": "14.0.0",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepublishOnly": "yarn build",
@@ -81,7 +81,7 @@
"@types/request": "^2.48.5",
"@typescript-eslint/eslint-plugin": "^4.17.0",
"@typescript-eslint/parser": "^4.17.0",
"allchange": "^0.0.1",
"allchange": "^1.0.2",
"babel-jest": "^26.6.3",
"babelify": "^10.0.0",
"better-docs": "^2.4.0-beta.9",
+4 -1
View File
@@ -191,7 +191,10 @@ git commit package.json $pkglock -m "$tag"
# figure out if we should be signing this release
signing_id=
if [ -f release_config.yaml ]; then
signing_id=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']"`
result=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']" 2> /dev/null || true`
if [ "$?" -eq 0 ]; then
signing_id=$result
fi
fi
+121 -73
View File
@@ -256,97 +256,145 @@ describe("MegolmDecryption", function() {
});
});
it("re-uses sessions for sequential messages", async function() {
mockCrypto.backupManager = {
backupGroupSession: () => {},
};
const mockStorage = new MockStorageApi();
const cryptoStore = new MemoryCryptoStore(mockStorage);
describe("session reuse and key reshares", () => {
let megolmEncryption;
let aliceDeviceInfo;
let mockRoom;
let olmDevice;
const olmDevice = new OlmDevice(cryptoStore);
olmDevice.verifySignature = jest.fn();
await olmDevice.init();
beforeEach(async () => {
mockCrypto.backupManager = {
backupGroupSession: () => {},
};
const mockStorage = new MockStorageApi();
const cryptoStore = new MemoryCryptoStore(mockStorage);
mockBaseApis.claimOneTimeKeys = jest.fn().mockReturnValue(Promise.resolve({
one_time_keys: {
'@alice:home.server': {
aliceDevice: {
'signed_curve25519:flooble': {
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
signatures: {
'@alice:home.server': {
'ed25519:aliceDevice': 'totally valid',
olmDevice = new OlmDevice(cryptoStore);
olmDevice.verifySignature = jest.fn();
await olmDevice.init();
mockBaseApis.claimOneTimeKeys = jest.fn().mockReturnValue(Promise.resolve({
one_time_keys: {
'@alice:home.server': {
aliceDevice: {
'signed_curve25519:flooble': {
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
signatures: {
'@alice:home.server': {
'ed25519:aliceDevice': 'totally valid',
},
},
},
},
},
},
},
}));
mockBaseApis.sendToDevice = jest.fn().mockResolvedValue(undefined);
}));
mockBaseApis.sendToDevice = jest.fn().mockResolvedValue(undefined);
mockCrypto.downloadKeys.mockReturnValue(Promise.resolve({
'@alice:home.server': {
aliceDevice: {
deviceId: 'aliceDevice',
isBlocked: jest.fn().mockReturnValue(false),
isUnverified: jest.fn().mockReturnValue(false),
getIdentityKey: jest.fn().mockReturnValue(
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE',
),
getFingerprint: jest.fn().mockReturnValue(''),
aliceDeviceInfo = {
deviceId: 'aliceDevice',
isBlocked: jest.fn().mockReturnValue(false),
isUnverified: jest.fn().mockReturnValue(false),
getIdentityKey: jest.fn().mockReturnValue(
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE',
),
getFingerprint: jest.fn().mockReturnValue(''),
};
mockCrypto.downloadKeys.mockReturnValue(Promise.resolve({
'@alice:home.server': {
aliceDevice: aliceDeviceInfo,
},
},
}));
}));
mockCrypto.checkDeviceTrust.mockReturnValue({
isVerified: () => false,
mockCrypto.checkDeviceTrust.mockReturnValue({
isVerified: () => false,
});
megolmEncryption = new MegolmEncryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: mockBaseApis,
roomId: ROOM_ID,
config: {
rotation_period_ms: 9999999999999,
},
});
mockRoom = {
getEncryptionTargetMembers: jest.fn().mockReturnValue(
[{ userId: "@alice:home.server" }],
),
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
};
});
const megolmEncryption = new MegolmEncryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: mockBaseApis,
roomId: ROOM_ID,
config: {
rotation_period_ms: 9999999999999,
},
});
const mockRoom = {
getEncryptionTargetMembers: jest.fn().mockReturnValue(
[{ userId: "@alice:home.server" }],
),
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
};
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some text",
});
expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
it("re-uses sessions for sequential messages", async function() {
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some text",
});
expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
// this should have claimed a key for alice as it's starting a new session
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
);
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
['@alice:home.server'], false,
);
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
);
// this should have claimed a key for alice as it's starting a new session
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
);
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
['@alice:home.server'], false,
);
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
);
mockBaseApis.claimOneTimeKeys.mockReset();
mockBaseApis.claimOneTimeKeys.mockReset();
const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some more text",
const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some more text",
});
// this should *not* have claimed a key as it should be using the same session
expect(mockBaseApis.claimOneTimeKeys).not.toHaveBeenCalled();
// likewise they should show the same session ID
expect(ct2.session_id).toEqual(ct1.session_id);
});
// this should *not* have claimed a key as it should be using the same session
expect(mockBaseApis.claimOneTimeKeys).not.toHaveBeenCalled();
it("re-shares keys to devices it's already sent to", async function() {
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some text",
});
// likewise they should show the same session ID
expect(ct2.session_id).toEqual(ct1.session_id);
mockBaseApis.sendToDevice.mockClear();
await megolmEncryption.reshareKeyWithDevice(
olmDevice.deviceCurve25519Key,
ct1.session_id,
'@alice:home.server',
aliceDeviceInfo,
);
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
});
it("does not re-share keys to devices whose keys have changed", async function() {
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some text",
});
aliceDeviceInfo.getIdentityKey = jest.fn().mockReturnValue(
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWI',
);
mockBaseApis.sendToDevice.mockClear();
await megolmEncryption.reshareKeyWithDevice(
olmDevice.deviceCurve25519Key,
ct1.session_id,
'@alice:home.server',
aliceDeviceInfo,
);
expect(mockBaseApis.sendToDevice).not.toHaveBeenCalled();
});
});
});
+2 -2
View File
@@ -246,7 +246,7 @@ describe("OlmDevice", function() {
// After a single tick through the first task, it should have
// claimed ownership of all devices to avoid deadlocking others.
expect(Object.keys(aliceOlmDevice._sessionsInProgress).length).toBe(2);
expect(Object.keys(aliceOlmDevice.sessionsInProgress).length).toBe(2);
const task2 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
aliceOlmDevice, baseApis, devicesByUserBA,
@@ -254,7 +254,7 @@ describe("OlmDevice", function() {
// The second task should not have changed the ownership count, as
// it's waiting on the first task.
expect(Object.keys(aliceOlmDevice._sessionsInProgress).length).toBe(2);
expect(Object.keys(aliceOlmDevice.sessionsInProgress).length).toBe(2);
// Track the tasks, but don't await them yet.
const promises = Promise.all([
+3 -3
View File
@@ -288,7 +288,7 @@ describe("MegolmBackup", function() {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice._pickleKey),
session: ibGroupSession.pickle(olmDevice.pickleKey),
},
txn);
});
@@ -369,7 +369,7 @@ describe("MegolmBackup", function() {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice._pickleKey),
session: ibGroupSession.pickle(olmDevice.pickleKey),
},
txn);
});
@@ -546,7 +546,7 @@ describe("MegolmBackup", function() {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice._pickleKey),
session: ibGroupSession.pickle(olmDevice.pickleKey),
},
txn);
});
@@ -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();
});
});
@@ -93,7 +93,7 @@ describe("self-verifications", () => {
undefined, // startEvent
request,
);
verification._resolve = () => undefined;
verification.resolve = () => undefined;
const result = await verification.done();
+26
View File
@@ -237,6 +237,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 +271,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 +332,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) {
+56
View File
@@ -127,6 +127,45 @@ describe("MSC3089Branch", () => {
expect(stateFn).toHaveBeenCalledTimes(1);
});
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 };
@@ -151,4 +190,21 @@ 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 eventsArr = [
{ getId: () => "$not-file", getContent: () => ({}) },
{ getId: () => fileEventId, getContent: () => ({ file: fileContent }) },
];
client.getEventTimeline = () => Promise.resolve({
getEvents: () => eventsArr,
}) as any as Promise<EventTimeline>; // partial
client.decryptEventIfNeeded = () => Promise.resolve();
const res = await branch.getFileEvent();
expect(res).toBeDefined();
expect(res).toBe(eventsArr[1]);
});
});
+31 -6
View File
@@ -227,40 +227,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 +291,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', () => {
+3 -2
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;
+5
View File
@@ -168,6 +168,11 @@ export const UNSTABLE_ELEMENT_FUNCTIONAL_USERS = new UnstableValue(
"io.element.functional_members",
"io.element.functional_members");
export const UNSTABLE_ELEMENT_REPLY_IN_THREAD = new UnstableValue(
"m.in_thread",
"io.element.in_thread",
);
export interface IEncryptedFile {
url: string;
mimetype?: string;
+8 -20
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,21 +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;
@@ -107,4 +88,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;
}
}
+11 -11
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 {
@@ -74,11 +82,3 @@ export enum HistoryVisibility {
Shared = "shared",
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[];
}
+20
View File
@@ -15,21 +15,26 @@ 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 */
/** @deprecated Use hierarchy instead where possible. */
export interface ISpaceSummaryRoom extends IPublicRoomsChunkRoom {
num_refs: number;
room_type: string;
}
/** @deprecated Use hierarchy instead where possible. */
export interface ISpaceSummaryEvent {
room_id: string;
event_id: string;
origin_server_ts: number;
type: string;
state_key: string;
sender: string;
content: {
order?: string;
suggested?: boolean;
@@ -37,4 +42,19 @@ export interface ISpaceSummaryEvent {
via?: string[];
};
}
export interface IHierarchyRelation extends IStrippedState {
room_id: string;
origin_server_ts: number;
content: {
order?: string;
suggested?: boolean;
via?: string[];
};
}
export interface IHierarchyRoom extends IPublicRoomsChunkRoom {
room_type?: RoomType | string;
children_state: IHierarchyRelation[];
}
/* eslint-enable camelcase */
+54 -132
View File
@@ -17,79 +17,19 @@ limitations under the License.
/** @module auto-discovery */
import { IClientWellKnown, IWellKnownConfig } from "./client";
import { logger } from './logger';
import { URL as NodeURL } from "url";
// 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 +42,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 +79,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 +90,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 +99,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 +114,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 +161,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 +171,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 +193,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 +208,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 +220,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 +238,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 +280,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 +316,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 +348,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 +368,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 +416,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 +427,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 +438,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 +446,7 @@ export class AutoDiscovery {
}
resolve({
raw: {},
action: "FAIL_PROMPT",
action: AutoDiscoveryAction.FAIL_PROMPT,
reason: reason,
error: e,
});
+137 -39
View File
@@ -30,10 +30,11 @@ import * as utils from './utils';
import { sleep } from './utils';
import { Group } from "./models/group";
import { Direction, EventTimeline } from "./models/event-timeline";
import { PushAction, PushProcessor } from "./pushprocessor";
import { AutoDiscovery } from "./autodiscovery";
import { IActionsObject, PushProcessor } from "./pushprocessor";
import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery";
import * as olmlib from "./crypto/olmlib";
import { decodeBase64, encodeBase64 } from "./crypto/olmlib";
import { IExportedDevice as IOlmDevice } from "./crypto/OlmDevice";
import { ReEmitter } from './ReEmitter';
import { IRoomEncryption, RoomList } from './crypto/RoomList';
import { logger } from './logger';
@@ -74,7 +75,6 @@ import {
IKeyBackupPrepareOpts,
IKeyBackupRestoreOpts,
IKeyBackupRestoreResult,
IKeyBackupSession,
} from "./crypto/keybackup";
import { IIdentityServerProvider } from "./@types/IIdentityServerProvider";
import type Request from "request";
@@ -92,7 +92,7 @@ import {
import { SyncState } from "./sync.api";
import { EventTimelineSet } from "./models/event-timeline-set";
import { VerificationRequest } from "./crypto/verification/request/VerificationRequest";
import { Base as Verification } from "./crypto/verification/Base";
import { VerificationBase as Verification } from "./crypto/verification/Base";
import * as ContentHelpers from "./content-helpers";
import { CrossSigningInfo, DeviceTrustLevel, ICacheCallbacks, UserTrustLevel } from "./crypto/CrossSigning";
import { Room } from "./models/room";
@@ -140,10 +140,11 @@ import {
SearchOrderBy,
} from "./@types/search";
import { ISynapseAdminDeactivateResponse, ISynapseAdminWhoisResponse } from "./@types/synapse";
import { ISpaceSummaryEvent, ISpaceSummaryRoom } from "./@types/spaces";
import { IHierarchyRoom, ISpaceSummaryEvent, ISpaceSummaryRoom } from "./@types/spaces";
import { IPusher, IPusherRequest, IPushRules, PushRuleAction, PushRuleKind, RuleId } from "./@types/PushRules";
import { IThreepid } from "./@types/threepids";
import { CryptoStore } from "./crypto/store/base";
import { MediaHandler } from "./webrtc/mediaHandler";
export type Store = IStore;
export type SessionStore = WebStorageSessionStore;
@@ -156,12 +157,6 @@ export const CRYPTO_ENABLED: boolean = isCryptoAvailable();
const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value
const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes
interface IOlmDevice {
pickledAccount: string;
sessions: Array<Record<string, IKeyBackupSession>>;
pickleKey: string;
}
interface IExportedDevice {
olmDevice: IOlmDevice;
userId: string;
@@ -380,6 +375,11 @@ export interface IStartClientOpts {
* This should be in the order of hours. Default: undefined.
*/
clientWellKnownPollPeriod?: number;
/**
* @experimental
*/
experimentalThreadSupport?: boolean;
}
export interface IStoredClientOpts extends IStartClientOpts {
@@ -476,14 +476,19 @@ interface IServerVersions {
unstable_features: Record<string, boolean>;
}
interface IClientWellKnown {
export interface IClientWellKnown {
[key: string]: any;
"m.homeserver": {
base_url: string;
};
"m.identity_server"?: {
base_url: string;
};
"m.homeserver"?: IWellKnownConfig;
"m.identity_server"?: IWellKnownConfig;
}
export interface IWellKnownConfig {
raw?: any; // todo typings
action?: AutoDiscoveryAction;
reason?: string;
error?: Error | string;
// eslint-disable-next-line
base_url?: string | null;
}
interface IKeyBackupPath {
@@ -521,7 +526,7 @@ interface IMessagesResponse {
state: IStateEvent[];
}
interface IRequestTokenResponse {
export interface IRequestTokenResponse {
sid: string;
submit_url?: string;
}
@@ -600,7 +605,7 @@ interface IUserDirectoryResponse {
limited: boolean;
}
interface IMyDevice {
export interface IMyDevice {
device_id: string;
display_name?: string;
last_seen_ip?: string;
@@ -673,7 +678,7 @@ export class MatrixClient extends EventEmitter {
public static readonly RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY';
public reEmitter = new ReEmitter(this);
public olmVersion: string = null; // populated after initCrypto
public olmVersion: [number, number, number] = null; // populated after initCrypto
public usingExternalCrypto = false;
public store: Store;
public deviceId?: string;
@@ -734,6 +739,7 @@ export class MatrixClient extends EventEmitter {
protected checkTurnServersIntervalID: number;
protected exportedOlmDeviceToImport: IOlmDevice;
protected txnCtr = 0;
protected mediaHandler = new MediaHandler();
constructor(opts: IMatrixClientCreateOpts) {
super();
@@ -1241,6 +1247,13 @@ export class MatrixClient extends EventEmitter {
return this.canSupportVoip;
}
/**
* @returns {MediaHandler}
*/
public getMediaHandler(): MediaHandler {
return this.mediaHandler;
}
/**
* Set whether VoIP calls are forced to use only TURN
* candidates. This is the same as the forceTURN option
@@ -2028,7 +2041,7 @@ export class MatrixClient 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 createRecoveryKeyFromPassphrase(password: string): Promise<IRecoveryKey> {
public createRecoveryKeyFromPassphrase(password?: string): Promise<IRecoveryKey> {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
@@ -2471,7 +2484,7 @@ export class MatrixClient extends EventEmitter {
*/
// TODO: Verify types
public async prepareKeyBackupVersion(
password: string,
password?: string,
opts: IKeyBackupPrepareOpts = { secureSecretStorage: false },
): Promise<Pick<IPreparedKeyBackupVersion, "algorithm" | "auth_data" | "recovery_key">> {
if (!this.crypto) {
@@ -2931,6 +2944,7 @@ export class MatrixClient extends EventEmitter {
* has been emitted.
* @param {string} groupId The group ID
* @return {Group} The Group or null if the group is not known or there is no data store.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroup(groupId: string): Group {
return this.store.getGroup(groupId);
@@ -2939,6 +2953,7 @@ export class MatrixClient extends EventEmitter {
/**
* Retrieve all known groups.
* @return {Group[]} A list of groups, or an empty list if there is no data store.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroups(): Group[] {
return this.store.getGroups();
@@ -4340,7 +4355,7 @@ export class MatrixClient extends EventEmitter {
* @param {MatrixEvent} event The event to get push actions for.
* @return {module:pushprocessor~PushAction} A dict of actions to perform.
*/
public getPushActionsForEvent(event: MatrixEvent): PushAction {
public getPushActionsForEvent(event: MatrixEvent): IActionsObject {
if (!event.getPushActions()) {
event.setPushActions(this.pushProcessor.actionsForEvent(event));
}
@@ -5076,7 +5091,7 @@ export class MatrixClient extends EventEmitter {
email: string,
clientSecret: string,
sendAttempt: number,
nextLink: string,
nextLink?: string,
): Promise<IRequestTokenResponse> {
return this.requestTokenFromEndpoint(
"/account/password/email/requestToken",
@@ -6106,17 +6121,17 @@ export class MatrixClient extends EventEmitter {
public register(
username: string,
password: string,
sessionId: string,
auth: any,
bindThreepids: any,
guestAccessToken: string,
inhibitLogin: boolean,
sessionId: string | null,
auth: { session?: string, type: string },
bindThreepids?: boolean | null | { email?: boolean, msisdn?: boolean },
guestAccessToken?: string,
inhibitLogin?: boolean,
callback?: Callback,
): Promise<any> { // TODO: Types (many)
// backwards compat
if (bindThreepids === true) {
bindThreepids = { email: true };
} else if (bindThreepids === null || bindThreepids === undefined) {
} else if (bindThreepids === null || bindThreepids === undefined || bindThreepids === false) {
bindThreepids = {};
}
if (typeof inhibitLogin === 'function') {
@@ -7450,7 +7465,7 @@ export class MatrixClient extends EventEmitter {
return this.http.authedRequest(undefined, "GET", path, qps, undefined);
}
public uploadDeviceSigningKeys(auth: any, keys: CrossSigningKeys): Promise<{}> { // TODO: types
public uploadDeviceSigningKeys(auth: any, keys?: CrossSigningKeys): Promise<{}> { // TODO: types
const data = Object.assign({}, keys);
if (auth) Object.assign(data, { auth });
return this.http.authedRequest(
@@ -7973,14 +7988,15 @@ export class MatrixClient extends EventEmitter {
}
/**
* Fetches or paginates a summary of a space as defined by MSC2946
* Fetches or paginates a summary of a space as defined by an initial version of MSC2946
* @param {string} roomId The ID of the space-room to use as the root of the summary.
* @param {number?} maxRoomsPerSpace The maximum number of rooms to return per subspace.
* @param {boolean?} suggestedOnly Whether to only return rooms with suggested=true.
* @param {boolean?} autoJoinOnly Whether to only return rooms with auto_join=true.
* @param {number?} limit The maximum number of rooms to return in total.
* @param {string?} batch The opaque token to paginate a previous summary request.
* @returns {Promise} the response, with next_batch, rooms, events fields.
* @returns {Promise} the response, with next_token, rooms fields.
* @deprecated in favour of `getRoomHierarchy` due to the MSC changing paths.
*/
public getSpaceSummary(
roomId: string,
@@ -7989,10 +8005,7 @@ export class MatrixClient extends EventEmitter {
autoJoinOnly?: boolean,
limit?: number,
batch?: string,
): Promise<{
rooms: ISpaceSummaryRoom[];
events: ISpaceSummaryEvent[];
}> {
): Promise<{rooms: ISpaceSummaryRoom[], events: ISpaceSummaryEvent[]}> {
const path = utils.encodeUri("/rooms/$roomId/spaces", {
$roomId: roomId,
});
@@ -8008,6 +8021,60 @@ export class MatrixClient extends EventEmitter {
});
}
/**
* Fetches or paginates a room hierarchy as defined by MSC2946.
* Falls back gracefully to sourcing its data from `getSpaceSummary` if this API is not yet supported by the server.
* @param {string} roomId The ID of the space-room to use as the root of the summary.
* @param {number?} limit The maximum number of rooms to return per page.
* @param {number?} maxDepth The maximum depth in the tree from the root room to return.
* @param {boolean?} suggestedOnly Whether to only return rooms with suggested=true.
* @param {string?} fromToken The opaque token to paginate a previous request.
* @returns {Promise} the response, with next_batch & rooms fields.
*/
public getRoomHierarchy(
roomId: string,
limit?: number,
maxDepth?: number,
suggestedOnly = false,
fromToken?: string,
): Promise<{
rooms: IHierarchyRoom[];
next_batch?: string; // eslint-disable-line camelcase
}> {
const path = utils.encodeUri("/rooms/$roomId/hierarchy", {
$roomId: roomId,
});
return this.http.authedRequest(undefined, "GET", path, {
suggested_only: suggestedOnly,
max_depth: maxDepth,
from: fromToken,
limit,
}, undefined, {
prefix: "/_matrix/client/unstable/org.matrix.msc2946",
}).catch(e => {
if (e.errcode === "M_UNRECOGNIZED") {
// fall back to the older space summary API as it exposes the same data just in a different shape.
return this.getSpaceSummary(roomId, undefined, suggestedOnly, undefined, limit)
.then(({ rooms, events }) => {
// Translate response from `/spaces` to that we expect in this API.
const roomMap = new Map(rooms.map(r => {
return [r.room_id, <IHierarchyRoom>{ ...r, children_state: [] }];
}));
events.forEach(e => {
roomMap.get(e.room_id)?.children_state.push(e);
});
return {
rooms: Array.from(roomMap.values()),
};
});
}
throw e;
});
}
/**
* Creates a new file tree space with the given name. The client will pick
* defaults for how it expects to be able to support the remaining API offered
@@ -8060,7 +8127,7 @@ export class MatrixClient extends EventEmitter {
*/
public unstableGetFileTreeSpace(roomId: string): MSC3089TreeSpace {
const room = this.getRoom(roomId);
if (!room) return null;
if (room?.getMyMembership() !== 'join') return null;
const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, "");
const purposeEvent = room.currentState.getStateEvents(
@@ -8085,6 +8152,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} groupId
* @return {Promise} Resolves: Group summary object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroupSummary(groupId: string): Promise<any> {
const path = utils.encodeUri("/groups/$groupId/summary", { $groupId: groupId });
@@ -8095,6 +8163,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} groupId
* @return {Promise} Resolves: Group profile object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroupProfile(groupId: string): Promise<any> {
const path = utils.encodeUri("/groups/$groupId/profile", { $groupId: groupId });
@@ -8110,6 +8179,7 @@ export class MatrixClient extends EventEmitter {
* @param {string=} profile.long_description A longer HTML description of the room
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public setGroupProfile(groupId: string, profile: any): Promise<any> {
const path = utils.encodeUri("/groups/$groupId/profile", { $groupId: groupId });
@@ -8126,6 +8196,7 @@ export class MatrixClient extends EventEmitter {
* required to join.
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public setGroupJoinPolicy(groupId: string, policy: any): Promise<any> {
const path = utils.encodeUri(
@@ -8143,6 +8214,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} groupId
* @return {Promise} Resolves: Group users list object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroupUsers(groupId: string): Promise<any> {
const path = utils.encodeUri("/groups/$groupId/users", { $groupId: groupId });
@@ -8153,6 +8225,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} groupId
* @return {Promise} Resolves: Group users list object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroupInvitedUsers(groupId: string): Promise<any> {
const path = utils.encodeUri("/groups/$groupId/invited_users", { $groupId: groupId });
@@ -8163,6 +8236,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} groupId
* @return {Promise} Resolves: Group rooms list object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroupRooms(groupId: string): Promise<any> {
const path = utils.encodeUri("/groups/$groupId/rooms", { $groupId: groupId });
@@ -8174,6 +8248,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} userId
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public inviteUserToGroup(groupId: string, userId: string): Promise<any> {
const path = utils.encodeUri(
@@ -8188,6 +8263,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} userId
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public removeUserFromGroup(groupId: string, userId: string): Promise<any> {
const path = utils.encodeUri(
@@ -8203,6 +8279,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} roleId Optional.
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public addUserToGroupSummary(groupId: string, userId: string, roleId: string): Promise<any> {
const path = utils.encodeUri(
@@ -8219,6 +8296,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} userId
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public removeUserFromGroupSummary(groupId: string, userId: string): Promise<any> {
const path = utils.encodeUri(
@@ -8234,6 +8312,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} categoryId Optional.
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public addRoomToGroupSummary(groupId: string, roomId: string, categoryId: string): Promise<any> {
const path = utils.encodeUri(
@@ -8250,6 +8329,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} roomId
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public removeRoomFromGroupSummary(groupId: string, roomId: string): Promise<any> {
const path = utils.encodeUri(
@@ -8265,6 +8345,7 @@ export class MatrixClient extends EventEmitter {
* @param {boolean} isPublic Whether the room-group association is visible to non-members
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public addRoomToGroup(groupId: string, roomId: string, isPublic: boolean): Promise<any> {
if (isPublic === undefined) {
@@ -8286,6 +8367,7 @@ export class MatrixClient extends EventEmitter {
* @param {boolean} isPublic Whether the room-group association is visible to non-members
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public updateGroupRoomVisibility(groupId: string, roomId: string, isPublic: boolean): Promise<any> {
// NB: The /config API is generic but there's not much point in exposing this yet as synapse
@@ -8306,6 +8388,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} roomId
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public removeRoomFromGroup(groupId: string, roomId: string): Promise<any> {
const path = utils.encodeUri(
@@ -8320,6 +8403,7 @@ export class MatrixClient extends EventEmitter {
* @param {Object} opts Additional options to send alongside the acceptance.
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public acceptGroupInvite(groupId: string, opts = null): Promise<any> {
const path = utils.encodeUri(
@@ -8333,6 +8417,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} groupId
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public joinGroup(groupId: string): Promise<any> {
const path = utils.encodeUri(
@@ -8346,6 +8431,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} groupId
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public leaveGroup(groupId: string): Promise<any> {
const path = utils.encodeUri(
@@ -8358,6 +8444,7 @@ export class MatrixClient extends EventEmitter {
/**
* @return {Promise} Resolves: The groups to which the user is joined
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getJoinedGroups(): Promise<any> {
const path = utils.encodeUri("/joined_groups", {});
@@ -8370,6 +8457,7 @@ export class MatrixClient extends EventEmitter {
* @param {Object} content.profile Group profile object
* @return {Promise} Resolves: Object with key group_id: id of the created group
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public createGroup(content: any): Promise<any> {
const path = utils.encodeUri("/create_group", {});
@@ -8390,6 +8478,7 @@ export class MatrixClient extends EventEmitter {
* }
* }
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getPublicisedGroups(userIds: string[]): Promise<any> {
const path = utils.encodeUri("/publicised_groups", {});
@@ -8403,6 +8492,7 @@ export class MatrixClient extends EventEmitter {
* @param {boolean} isPublic Whether the user's membership of this group is made public
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public setGroupPublicity(groupId: string, isPublic: boolean): Promise<any> {
const path = utils.encodeUri(
@@ -8413,6 +8503,13 @@ export class MatrixClient extends EventEmitter {
publicise: isPublic,
});
}
/**
* @experimental
*/
public supportsExperimentalThreads(): boolean {
return this.clientOpts?.experimentalThreadSupport || false;
}
}
/**
@@ -8567,6 +8664,7 @@ export class MatrixClient extends EventEmitter {
* is experimental and may change.</strong>
* @event module:client~MatrixClient#"Group"
* @param {Group} group The newly created, fully populated group.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
* @example
* matrixClient.on("Group", function(group){
* var groupId = group.groupId;
+15 -9
View File
@@ -713,7 +713,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 +726,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 +739,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 +748,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 +787,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 +807,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 +820,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) => {
+5 -4
View File
@@ -59,7 +59,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);
@@ -351,12 +351,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 +365,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 +374,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
+4 -3
View File
@@ -26,9 +26,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;
}
/**
+32 -14
View File
@@ -67,7 +67,7 @@ export interface IOlmDevice<T = DeviceInfo> {
}
/* eslint-disable camelcase */
interface IOutboundGroupSessionKey {
export interface IOutboundGroupSessionKey {
chain_index: number;
key: string;
}
@@ -101,6 +101,13 @@ interface IPayload extends Partial<IMessage> {
}
/* 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 +122,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 +157,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 {
@@ -572,6 +579,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
payload: IPayload,
): Promise<void> {
const contentMap = {};
const deviceInfoByDeviceId = new Map<string, DeviceInfo>();
const promises = [];
for (let i = 0; i < userDeviceMap.length; i++) {
@@ -584,6 +592,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 +645,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,
);
}
}
@@ -719,8 +731,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 +740,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) {
@@ -882,14 +902,12 @@ class MegolmEncryption extends EncryptionAlgorithm {
const deviceId = deviceInfo.deviceId;
session.markSharedWithDevice(
userId, deviceId, key.chain_index,
userId, deviceId, deviceInfo.getIdentityKey(), key.chain_index,
);
}
const filteredFailedDevices =
await this.olmDevice.filterOutNotifiedErrorDevices(
failedDevices,
);
await this.olmDevice.filterOutNotifiedErrorDevices(failedDevices);
logger.debug(
`Filtered down to ${filteredFailedDevices.length} error devices ` +
`in ${this.roomId}`,
@@ -1391,7 +1409,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();
+3 -3
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;
}
}
+2 -2
View File
@@ -58,7 +58,7 @@ export interface IEncryptedEventInfo {
}
export interface IRecoveryKey {
keyInfo: {
keyInfo?: {
pubkey: string;
passphrase?: {
algorithm: string;
@@ -67,7 +67,7 @@ export interface IRecoveryKey {
};
};
privateKey: Uint8Array;
encodedPrivateKey: string;
encodedPrivateKey?: string;
}
export interface ICreateSecretStorageOpts {
+4 -3
View File
@@ -20,6 +20,7 @@ import { decryptAES, encryptAES } from './aes';
import anotherjson from "another-json";
import { logger } from '../logger';
import { ISecretStorageKeyInfo } from "./api";
import { Crypto } from "./index";
// FIXME: these types should eventually go in a different file
type Signatures = Record<string, Record<string, string>>;
@@ -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);
+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)) {
+36 -27
View File
@@ -26,7 +26,7 @@ import { EventEmitter } from 'events';
import { ReEmitter } 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";
@@ -66,6 +66,7 @@ import { IRecoveryKey, IEncryptedEventInfo } from "./api";
import { IKeyBackupInfo } from "./keybackup";
import { ISyncStateData } from "../sync";
import { CryptoStore } from "./store/base";
import { IVerificationChannel } from "./verification/request/Channel";
const DeviceVerification = DeviceInfo.DeviceVerification;
@@ -84,12 +85,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 +99,7 @@ export function isCryptoAvailable(): boolean {
const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000;
interface IInitOpts {
exportedOlmDevice?: any; // TODO types
exportedOlmDevice?: IExportedDevice;
pickleKey?: string;
}
@@ -130,13 +131,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 */
@@ -192,7 +194,7 @@ export class Crypto extends EventEmitter {
/**
* @return {string} The version of Olm.
*/
static getOlmVersion(): string {
static getOlmVersion(): [number, number, number] {
return OlmDevice.getOlmVersion();
}
@@ -205,7 +207,7 @@ export class Crypto extends EventEmitter {
private readonly reEmitter: ReEmitter;
private readonly verificationMethods: any; // TODO types
private readonly supportedAlgorithms: string[];
public readonly supportedAlgorithms: string[];
private readonly outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager;
private readonly toDeviceVerificationRequests: ToDeviceRequests;
public readonly inRoomVerificationRequests: InRoomRequests;
@@ -291,7 +293,7 @@ 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,
@@ -504,7 +506,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"]> = {};
@@ -1097,7 +1099,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 +1115,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',
@@ -1912,7 +1914,7 @@ export class Crypto extends EventEmitter {
const fallbackJson: Record<string, IOneTimeKey> = {};
if (this.getNeedsNewFallback()) {
const fallbackKeys = await this.olmDevice.getFallbackKey() as Record<string, Record<string, string>>;
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;
@@ -2176,11 +2178,11 @@ 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);
@@ -2193,7 +2195,7 @@ export class Crypto extends EventEmitter {
);
}
public requestVerification(userId: string, devices: string[]): VerificationRequest {
public requestVerification(userId: string, devices: string[]): Promise<VerificationRequest> {
if (!devices) {
devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(userId));
}
@@ -2211,9 +2213,9 @@ export class Crypto extends EventEmitter {
private async requestVerificationWithChannel(
userId: string,
channel: any, // TODO types
channel: IVerificationChannel,
requestsMap: any, // TODO types
): VerificationRequest {
): Promise<VerificationRequest> {
let request = new VerificationRequest(channel, this.verificationMethods, this.baseApis);
// if transaction id is already known, add request
if (channel.transactionId) {
@@ -2259,14 +2261,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
@@ -2757,12 +2756,22 @@ export class Crypto extends EventEmitter {
delete content['m.relates_to'];
}
// 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",
+5 -7
View File
@@ -24,7 +24,7 @@ import anotherjson from "another-json";
import type { PkSigning } from "@matrix-org/olm";
import { Logger } from "loglevel";
import OlmDevice from "./OlmDevice";
import { OlmDevice } from "./OlmDevice";
import { DeviceInfo } from "./deviceinfo";
import { logger } from '../logger';
import * as utils from "../utils";
@@ -252,13 +252,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 +291,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
+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;
+1 -1
View File
@@ -34,7 +34,7 @@ import { IRoomKeyRequestBody } from "../index";
import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { InboundGroupSessionData } from "../../@types/partials";
import { InboundGroupSessionData } from "../OlmDevice";
import { IEncryptedPayload } from "../aes";
/**
@@ -20,7 +20,7 @@ import { IDeviceData, IProblem, ISession, ISessionInfo, IWithheld, Mode } from "
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { ICrossSigningKey } from "../../client";
import { InboundGroupSessionData } from "../../@types/partials";
import { InboundGroupSessionData } from "../OlmDevice";
import { IEncryptedPayload } from "../aes";
/**
+1 -1
View File
@@ -30,7 +30,7 @@ import { IRoomKeyRequestBody } from "../index";
import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { InboundGroupSessionData } from "../../@types/partials";
import { InboundGroupSessionData } from "../OlmDevice";
import { IEncryptedPayload } from "../aes";
/**
@@ -25,18 +25,33 @@ import { EventEmitter } from 'events';
import { logger } from '../../logger';
import { DeviceInfo } from '../deviceinfo';
import { newTimeoutError } from "./Error";
import { requestKeysDuringVerification } from "../CrossSigning";
import { KeysDuringVerification, requestKeysDuringVerification } from "../CrossSigning";
import { IVerificationChannel } from "./request/Channel";
import { MatrixClient } from "../../client";
import { VerificationRequest } from "./request/VerificationRequest";
const timeoutException = new Error("Verification timed out");
export class SwitchStartEventError extends Error {
constructor(startEvent) {
constructor(public readonly startEvent: MatrixEvent) {
super();
this.startEvent = startEvent;
}
}
export type KeyVerifier = (keyId: string, device: DeviceInfo, keyInfo: string) => void;
export class VerificationBase extends EventEmitter {
private cancelled = false;
private _done = false;
private promise: Promise<void> = null;
private transactionTimeoutTimer: number = null;
protected expectedEvent: string;
private resolve: () => void;
private reject: (e: Error | MatrixEvent) => void;
private resolveEvent: (e: MatrixEvent) => void;
private rejectEvent: (e: Error) => void;
private started: boolean;
/**
* Base class for verification methods.
*
@@ -64,22 +79,18 @@ export class VerificationBase extends EventEmitter {
* @param {object} [request] the key verification request object related to
* this verification, if any
*/
constructor(channel, baseApis, userId, deviceId, startEvent, request) {
constructor(
public readonly channel: IVerificationChannel,
public readonly baseApis: MatrixClient,
public readonly userId: string,
public readonly deviceId: string,
public startEvent: MatrixEvent,
public readonly request: VerificationRequest,
) {
super();
this._channel = channel;
this._baseApis = baseApis;
this.userId = userId;
this.deviceId = deviceId;
this.startEvent = startEvent;
this.request = request;
this.cancelled = false;
this._done = false;
this._promise = null;
this._transactionTimeoutTimer = null;
}
get initiatedByMe() {
public get initiatedByMe(): boolean {
// if there is no start event yet,
// we probably want to send it,
// which happens if we initiate
@@ -88,16 +99,20 @@ export class VerificationBase extends EventEmitter {
}
const sender = this.startEvent.getSender();
const content = this.startEvent.getContent();
return sender === this._baseApis.getUserId() &&
content.from_device === this._baseApis.getDeviceId();
return sender === this.baseApis.getUserId() &&
content.from_device === this.baseApis.getDeviceId();
}
_resetTimer() {
public get hasBeenCancelled(): boolean {
return this.cancelled;
}
private resetTimer(): void {
logger.info("Refreshing/starting the verification transaction timeout timer");
if (this._transactionTimeoutTimer !== null) {
clearTimeout(this._transactionTimeoutTimer);
if (this.transactionTimeoutTimer !== null) {
clearTimeout(this.transactionTimeoutTimer);
}
this._transactionTimeoutTimer = setTimeout(() => {
this.transactionTimeoutTimer = setTimeout(() => {
if (!this._done && !this.cancelled) {
logger.info("Triggering verification timeout");
this.cancel(timeoutException);
@@ -105,18 +120,18 @@ export class VerificationBase extends EventEmitter {
}, 10 * 60 * 1000); // 10 minutes
}
_endTimer() {
if (this._transactionTimeoutTimer !== null) {
clearTimeout(this._transactionTimeoutTimer);
this._transactionTimeoutTimer = null;
private endTimer(): void {
if (this.transactionTimeoutTimer !== null) {
clearTimeout(this.transactionTimeoutTimer);
this.transactionTimeoutTimer = null;
}
}
_send(type, uncompletedContent) {
return this._channel.send(type, uncompletedContent);
protected send(type: string, uncompletedContent: Record<string, any>): Promise<void> {
return this.channel.send(type, uncompletedContent);
}
_waitForEvent(type) {
protected waitForEvent(type: string): Promise<MatrixEvent> {
if (this._done) {
return Promise.reject(new Error("Verification is already done"));
}
@@ -125,24 +140,24 @@ export class VerificationBase extends EventEmitter {
return Promise.resolve(existingEvent);
}
this._expectedEvent = type;
this.expectedEvent = type;
return new Promise((resolve, reject) => {
this._resolveEvent = resolve;
this._rejectEvent = reject;
this.resolveEvent = resolve;
this.rejectEvent = reject;
});
}
canSwitchStartEvent() {
public canSwitchStartEvent(event: MatrixEvent): boolean {
return false;
}
switchStartEvent(event) {
public switchStartEvent(event: MatrixEvent): void {
if (this.canSwitchStartEvent(event)) {
logger.log("Verification Base: switching verification start event",
{ restartingFlow: !!this._rejectEvent });
if (this._rejectEvent) {
const reject = this._rejectEvent;
this._rejectEvent = undefined;
{ restartingFlow: !!this.rejectEvent });
if (this.rejectEvent) {
const reject = this.rejectEvent;
this.rejectEvent = undefined;
reject(new SwitchStartEventError(event));
} else {
this.startEvent = event;
@@ -150,21 +165,21 @@ export class VerificationBase extends EventEmitter {
}
}
handleEvent(e) {
public handleEvent(e: MatrixEvent): void {
if (this._done) {
return;
} else if (e.getType() === this._expectedEvent) {
} else if (e.getType() === this.expectedEvent) {
// if we receive an expected m.key.verification.done, then just
// ignore it, since we don't need to do anything about it
if (this._expectedEvent !== "m.key.verification.done") {
this._expectedEvent = undefined;
this._rejectEvent = undefined;
this._resetTimer();
this._resolveEvent(e);
if (this.expectedEvent !== "m.key.verification.done") {
this.expectedEvent = undefined;
this.rejectEvent = undefined;
this.resetTimer();
this.resolveEvent(e);
}
} else if (e.getType() === "m.key.verification.cancel") {
const reject = this._reject;
this._reject = undefined;
const reject = this.reject;
this.reject = undefined;
// there is only promise to reject if verify has been called
if (reject) {
const content = e.getContent();
@@ -172,36 +187,36 @@ export class VerificationBase extends EventEmitter {
reject(new Error(`Other side cancelled verification ` +
`because ${reason} (${code})`));
}
} else if (this._expectedEvent) {
} else if (this.expectedEvent) {
// only cancel if there is an event expected.
// if there is no event expected, it means verify() wasn't called
// and we're just replaying the timeline events when syncing
// after a refresh when the events haven't been stored in the cache yet.
const exception = new Error(
"Unexpected message: expecting " + this._expectedEvent
"Unexpected message: expecting " + this.expectedEvent
+ " but got " + e.getType(),
);
this._expectedEvent = undefined;
if (this._rejectEvent) {
const reject = this._rejectEvent;
this._rejectEvent = undefined;
this.expectedEvent = undefined;
if (this.rejectEvent) {
const reject = this.rejectEvent;
this.rejectEvent = undefined;
reject(exception);
}
this.cancel(exception);
}
}
done() {
this._endTimer(); // always kill the activity timer
public done(): Promise<KeysDuringVerification | void> {
this.endTimer(); // always kill the activity timer
if (!this._done) {
this.request.onVerifierFinished();
this._resolve();
return requestKeysDuringVerification(this._baseApis, this.userId, this.deviceId);
this.resolve();
return requestKeysDuringVerification(this.baseApis, this.userId, this.deviceId);
}
}
cancel(e) {
this._endTimer(); // always kill the activity timer
public cancel(e: Error | MatrixEvent): void {
this.endTimer(); // always kill the activity timer
if (!this._done) {
this.cancelled = true;
this.request.onVerifierCancelled();
@@ -210,7 +225,7 @@ export class VerificationBase extends EventEmitter {
// cancelled by the other user)
if (e === timeoutException) {
const timeoutEvent = newTimeoutError();
this._send(timeoutEvent.getType(), timeoutEvent.getContent());
this.send(timeoutEvent.getType(), timeoutEvent.getContent());
} else if (e instanceof MatrixEvent) {
const sender = e.getSender();
if (sender !== this.userId) {
@@ -219,29 +234,29 @@ export class VerificationBase extends EventEmitter {
content.code = content.code || "m.unknown";
content.reason = content.reason || content.body
|| "Unknown reason";
this._send("m.key.verification.cancel", content);
this.send("m.key.verification.cancel", content);
} else {
this._send("m.key.verification.cancel", {
this.send("m.key.verification.cancel", {
code: "m.unknown",
reason: content.body || "Unknown reason",
});
}
}
} else {
this._send("m.key.verification.cancel", {
this.send("m.key.verification.cancel", {
code: "m.unknown",
reason: e.toString(),
});
}
}
if (this._promise !== null) {
if (this.promise !== null) {
// when we cancel without a promise, we end up with a promise
// but no reject function. If cancel is called again, we'd error.
if (this._reject) this._reject(e);
if (this.reject) this.reject(e);
} else {
// FIXME: this causes an "Uncaught promise" console message
// if nothing ends up chaining this promise.
this._promise = Promise.reject(e);
this.promise = Promise.reject(e);
}
// Also emit a 'cancel' event that the app can listen for to detect cancellation
// before calling verify()
@@ -255,31 +270,32 @@ export class VerificationBase extends EventEmitter {
* @returns {Promise} Promise which resolves when the verification has
* completed.
*/
verify() {
if (this._promise) return this._promise;
public verify(): Promise<void> {
if (this.promise) return this.promise;
this._promise = new Promise((resolve, reject) => {
this._resolve = (...args) => {
this.promise = new Promise((resolve, reject) => {
this.resolve = (...args) => {
this._done = true;
this._endTimer();
this.endTimer();
resolve(...args);
};
this._reject = (...args) => {
this.reject = (e: Error) => {
this._done = true;
this._endTimer();
reject(...args);
this.endTimer();
reject(e);
};
});
if (this._doVerification && !this._started) {
this._started = true;
this._resetTimer(); // restart the timeout
Promise.resolve(this._doVerification())
.then(this.done.bind(this), this.cancel.bind(this));
if (this.doVerification && !this.started) {
this.started = true;
this.resetTimer(); // restart the timeout
Promise.resolve(this.doVerification()).then(this.done.bind(this), this.cancel.bind(this));
}
return this._promise;
return this.promise;
}
async _verifyKeys(userId, keys, verifier) {
protected doVerification?: () => Promise<void>;
protected async verifyKeys(userId: string, keys: Record<string, string>, verifier: KeyVerifier): Promise<void> {
// we try to verify all the keys that we're told about, but we might
// not know about all of them, so keep track of the keys that we know
// about, and ignore the rest
@@ -287,15 +303,14 @@ export class VerificationBase extends EventEmitter {
for (const [keyId, keyInfo] of Object.entries(keys)) {
const deviceId = keyId.split(':', 2)[1];
const device = this._baseApis.getStoredDevice(userId, deviceId);
const device = this.baseApis.getStoredDevice(userId, deviceId);
if (device) {
await verifier(keyId, device, keyInfo);
verifier(keyId, device, keyInfo);
verifiedDevices.push(deviceId);
} else {
const crossSigningInfo = this._baseApis.crypto.deviceList
.getStoredCrossSigningForUser(userId);
const crossSigningInfo = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(userId);
if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
await verifier(keyId, DeviceInfo.fromStorage({
verifier(keyId, DeviceInfo.fromStorage({
keys: {
[keyId]: deviceId,
},
@@ -323,7 +338,11 @@ export class VerificationBase extends EventEmitter {
// to upload each signature in a separate API call which is silly because the
// API supports as many signatures as you like.
for (const deviceId of verifiedDevices) {
await this._baseApis.setDeviceVerified(userId, deviceId);
await this.baseApis.setDeviceVerified(userId, deviceId);
}
}
public get events(): string[] | undefined {
return undefined;
}
}
@@ -1,5 +1,5 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -22,17 +22,17 @@ limitations under the License.
import { MatrixEvent } from "../../models/event";
export function newVerificationError(code, reason, extradata) {
const content = Object.assign({}, { code, reason }, extradata);
export function newVerificationError(code: string, reason: string, extraData: Record<string, any>): MatrixEvent {
const content = Object.assign({}, { code, reason }, extraData);
return new MatrixEvent({
type: "m.key.verification.cancel",
content,
});
}
export function errorFactory(code, reason) {
return function(extradata) {
return newVerificationError(code, reason, extradata);
export function errorFactory(code: string, reason: string): (extraData?: Record<string, any>) => MatrixEvent {
return function(extraData?: Record<string, any>) {
return newVerificationError(code, reason, extraData);
};
}
@@ -84,7 +84,7 @@ export const newInvalidMessageError = errorFactory(
"m.invalid_message", "Invalid message",
);
export function errorFromEvent(event) {
export function errorFromEvent(event: MatrixEvent): { code: string, reason: string } {
const content = event.getContent();
if (content) {
const { code, reason } = content;
@@ -21,23 +21,35 @@ limitations under the License.
*/
import { VerificationBase as Base } from "./Base";
import { IVerificationChannel } from "./request/Channel";
import { MatrixClient } from "../../client";
import { MatrixEvent } from "../../models/event";
import { VerificationRequest } from "./request/VerificationRequest";
/**
* @class crypto/verification/IllegalMethod/IllegalMethod
* @extends {module:crypto/verification/Base}
*/
export class IllegalMethod extends Base {
static factory(...args) {
return new IllegalMethod(...args);
public static factory(
channel: IVerificationChannel,
baseApis: MatrixClient,
userId: string,
deviceId: string,
startEvent: MatrixEvent,
request: VerificationRequest,
): IllegalMethod {
return new IllegalMethod(channel, baseApis, userId, deviceId, startEvent, request);
}
static get NAME() {
// eslint-disable-next-line @typescript-eslint/naming-convention
public static get NAME(): string {
// Typically the name will be something else, but to complete
// the contract we offer a default one here.
return "org.matrix.illegal_method";
}
async _doVerification() {
protected doVerification = async (): Promise<void> => {
throw new Error("Verification is not possible with this method");
}
};
}
@@ -1,6 +1,5 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -21,12 +20,13 @@ limitations under the License.
*/
import { VerificationBase as Base } from "./Base";
import {
newKeyMismatchError,
newUserCancelledError,
} from './Error';
import { encodeUnpaddedBase64, decodeBase64 } from "../olmlib";
import { newKeyMismatchError, newUserCancelledError } from './Error';
import { decodeBase64, encodeUnpaddedBase64 } from "../olmlib";
import { logger } from '../../logger';
import { VerificationRequest } from "./request/VerificationRequest";
import { MatrixClient } from "../../client";
import { IVerificationChannel } from "./request/Channel";
import { MatrixEvent } from "../../models/event";
export const SHOW_QR_CODE_METHOD = "m.qr_code.show.v1";
export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1";
@@ -36,15 +36,28 @@ export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1";
* @extends {module:crypto/verification/Base}
*/
export class ReciprocateQRCode extends Base {
static factory(...args) {
return new ReciprocateQRCode(...args);
public reciprocateQREvent: {
confirm(): void;
cancel(): void;
};
public static factory(
channel: IVerificationChannel,
baseApis: MatrixClient,
userId: string,
deviceId: string,
startEvent: MatrixEvent,
request: VerificationRequest,
): ReciprocateQRCode {
return new ReciprocateQRCode(channel, baseApis, userId, deviceId, startEvent, request);
}
static get NAME() {
// eslint-disable-next-line @typescript-eslint/naming-convention
public static get NAME(): string {
return "m.reciprocate.v1";
}
async _doVerification() {
protected doVerification = async (): Promise<void> => {
if (!this.startEvent) {
// TODO: Support scanning QR codes
throw new Error("It is not currently possible to start verification" +
@@ -58,7 +71,7 @@ export class ReciprocateQRCode extends Base {
}
// 2. ask if other user shows shield as well
await new Promise((resolve, reject) => {
await new Promise<void>((resolve, reject) => {
this.reciprocateQREvent = {
confirm: resolve,
cancel: () => reject(newUserCancelledError()),
@@ -67,21 +80,21 @@ export class ReciprocateQRCode extends Base {
});
// 3. determine key to sign / mark as trusted
const keys = {};
const keys: Record<string, string> = {};
switch (qrCodeData.mode) {
case MODE_VERIFY_OTHER_USER: {
case Mode.VerifyOtherUser: {
// add master key to keys to be signed, only if we're not doing self-verification
const masterKey = qrCodeData.otherUserMasterKey;
keys[`ed25519:${masterKey}`] = masterKey;
break;
}
case MODE_VERIFY_SELF_TRUSTED: {
case Mode.VerifySelfTrusted: {
const deviceId = this.request.targetDevice.deviceId;
keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey;
break;
}
case MODE_VERIFY_SELF_UNTRUSTED: {
case Mode.VerifySelfUntrusted: {
const masterKey = qrCodeData.myMasterKey;
keys[`ed25519:${masterKey}`] = masterKey;
break;
@@ -89,7 +102,7 @@ export class ReciprocateQRCode extends Base {
}
// 4. sign the key (or mark own MSK as verified in case of MODE_VERIFY_SELF_TRUSTED)
await this._verifyKeys(this.userId, keys, (keyId, device, keyInfo) => {
await this.verifyKeys(this.userId, keys, (keyId, device, keyInfo) => {
// make sure the device has the expected keys
const targetKey = keys[keyId];
if (!targetKey) throw newKeyMismatchError();
@@ -108,103 +121,88 @@ export class ReciprocateQRCode extends Base {
}
}
});
}
};
}
const CODE_VERSION = 0x02; // the version of binary QR codes we support
const BINARY_PREFIX = "MATRIX"; // ASCII, used to prefix the binary format
const MODE_VERIFY_OTHER_USER = 0x00; // Verifying someone who isn't us
const MODE_VERIFY_SELF_TRUSTED = 0x01; // We trust the master key
const MODE_VERIFY_SELF_UNTRUSTED = 0x02; // We do not trust the master key
enum Mode {
VerifyOtherUser = 0x00, // Verifying someone who isn't us
VerifySelfTrusted = 0x01, // We trust the master key
VerifySelfUntrusted = 0x02, // We do not trust the master key
}
interface IQrData {
prefix: string;
version: number;
mode: Mode;
transactionId: string;
firstKeyB64: string;
secondKeyB64: string;
secretB64: string;
}
export class QRCodeData {
constructor(
mode, sharedSecret, otherUserMasterKey,
otherDeviceKey, myMasterKey, buffer,
) {
this._sharedSecret = sharedSecret;
this._mode = mode;
this._otherUserMasterKey = otherUserMasterKey;
this._otherDeviceKey = otherDeviceKey;
this._myMasterKey = myMasterKey;
this._buffer = buffer;
}
public readonly mode: Mode,
private readonly sharedSecret: string,
// only set when mode is MODE_VERIFY_OTHER_USER, master key of other party at time of generating QR code
public readonly otherUserMasterKey: string | undefined,
// only set when mode is MODE_VERIFY_SELF_TRUSTED, device key of other party at time of generating QR code
public readonly otherDeviceKey: string | undefined,
// only set when mode is MODE_VERIFY_SELF_UNTRUSTED, own master key at time of generating QR code
public readonly myMasterKey: string | undefined,
private readonly buffer: Buffer,
) {}
static async create(request, client) {
const sharedSecret = QRCodeData._generateSharedSecret();
const mode = QRCodeData._determineMode(request, client);
public static async create(request: VerificationRequest, client: MatrixClient): Promise<QRCodeData> {
const sharedSecret = QRCodeData.generateSharedSecret();
const mode = QRCodeData.determineMode(request, client);
let otherUserMasterKey = null;
let otherDeviceKey = null;
let myMasterKey = null;
if (mode === MODE_VERIFY_OTHER_USER) {
if (mode === Mode.VerifyOtherUser) {
const otherUserCrossSigningInfo =
client.getStoredCrossSigningForUser(request.otherUserId);
otherUserMasterKey = otherUserCrossSigningInfo.getId("master");
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
otherDeviceKey = await QRCodeData._getOtherDeviceKey(request, client);
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) {
} else if (mode === Mode.VerifySelfTrusted) {
otherDeviceKey = await QRCodeData.getOtherDeviceKey(request, client);
} else if (mode === Mode.VerifySelfUntrusted) {
const myUserId = client.getUserId();
const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
myMasterKey = myCrossSigningInfo.getId("master");
}
const qrData = QRCodeData._generateQrData(
const qrData = QRCodeData.generateQrData(
request, client, mode,
sharedSecret,
otherUserMasterKey,
otherDeviceKey,
myMasterKey,
);
const buffer = QRCodeData._generateBuffer(qrData);
const buffer = QRCodeData.generateBuffer(qrData);
return new QRCodeData(mode, sharedSecret,
otherUserMasterKey, otherDeviceKey, myMasterKey, buffer);
}
get buffer() {
return this._buffer;
}
get mode() {
return this._mode;
}
/**
* only set when mode is MODE_VERIFY_SELF_TRUSTED
* @return {string} device key of other party at time of generating QR code
*/
get otherDeviceKey() {
return this._otherDeviceKey;
}
/**
* only set when mode is MODE_VERIFY_OTHER_USER
* @return {string} master key of other party at time of generating QR code
*/
get otherUserMasterKey() {
return this._otherUserMasterKey;
}
/**
* only set when mode is MODE_VERIFY_SELF_UNTRUSTED
* @return {string} own master key at time of generating QR code
*/
get myMasterKey() {
return this._myMasterKey;
}
/**
* The unpadded base64 encoded shared secret.
*/
get encodedSharedSecret() {
return this._sharedSecret;
public get encodedSharedSecret(): string {
return this.sharedSecret;
}
static _generateSharedSecret() {
public getBuffer(): Buffer {
return this.buffer;
}
private static generateSharedSecret(): string {
const secretBytes = new Uint8Array(11);
global.crypto.getRandomValues(secretBytes);
return encodeUnpaddedBase64(secretBytes);
}
static async _getOtherDeviceKey(request, client) {
private static async getOtherDeviceKey(request: VerificationRequest, client: MatrixClient): Promise<string> {
const myUserId = client.getUserId();
const otherDevice = request.targetDevice;
const otherDeviceId = otherDevice ? otherDevice.deviceId : null;
@@ -212,31 +210,35 @@ export class QRCodeData {
if (!device) {
throw new Error("could not find device " + otherDeviceId);
}
const key = device.getFingerprint();
return key;
return device.getFingerprint();
}
static _determineMode(request, client) {
private static determineMode(request: VerificationRequest, client: MatrixClient): Mode {
const myUserId = client.getUserId();
const otherUserId = request.otherUserId;
let mode = MODE_VERIFY_OTHER_USER;
let mode = Mode.VerifyOtherUser;
if (myUserId === otherUserId) {
// Mode changes depending on whether or not we trust the master cross signing key
const myTrust = client.checkUserTrust(myUserId);
if (myTrust.isCrossSigningVerified()) {
mode = MODE_VERIFY_SELF_TRUSTED;
mode = Mode.VerifySelfTrusted;
} else {
mode = MODE_VERIFY_SELF_UNTRUSTED;
mode = Mode.VerifySelfUntrusted;
}
}
return mode;
}
static _generateQrData(request, client, mode,
encodedSharedSecret, otherUserMasterKey,
otherDeviceKey, myMasterKey,
) {
private static generateQrData(
request: VerificationRequest,
client: MatrixClient,
mode: Mode,
encodedSharedSecret: string,
otherUserMasterKey: string,
otherDeviceKey: string,
myMasterKey: string,
): IQrData {
const myUserId = client.getUserId();
const transactionId = request.channel.transactionId;
const qrData = {
@@ -251,16 +253,16 @@ export class QRCodeData {
const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
if (mode === MODE_VERIFY_OTHER_USER) {
if (mode === Mode.VerifyOtherUser) {
// First key is our master cross signing key
qrData.firstKeyB64 = myCrossSigningInfo.getId("master");
// Second key is the other user's master cross signing key
qrData.secondKeyB64 = otherUserMasterKey;
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
} else if (mode === Mode.VerifySelfTrusted) {
// First key is our master cross signing key
qrData.firstKeyB64 = myCrossSigningInfo.getId("master");
qrData.secondKeyB64 = otherDeviceKey;
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) {
} else if (mode === Mode.VerifySelfUntrusted) {
// First key is our device's key
qrData.firstKeyB64 = client.getDeviceEd25519Key();
// Second key is what we think our master cross signing key is
@@ -269,7 +271,7 @@ export class QRCodeData {
return qrData;
}
static _generateBuffer(qrData) {
private static generateBuffer(qrData: IQrData): Buffer {
let buf = Buffer.alloc(0); // we'll concat our way through life
const appendByte = (b) => {
@@ -1,5 +1,5 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -29,6 +29,8 @@ import {
newUserCancelledError,
} from './Error';
import { logger } from '../../logger';
import { Utility, SAS as OlmSAS } from "@matrix-org/olm";
import { IContent, MatrixEvent } from "../../models/event";
const START_TYPE = "m.key.verification.start";
@@ -38,7 +40,7 @@ const EVENTS = [
"m.key.verification.mac",
];
let olmutil;
let olmutil: Utility;
const newMismatchedSASError = errorFactory(
"m.mismatched_sas", "Mismatched short authentication string",
@@ -48,7 +50,7 @@ const newMismatchedCommitmentError = errorFactory(
"m.mismatched_commitment", "Mismatched commitment",
);
function generateDecimalSas(sasBytes) {
function generateDecimalSas(sasBytes: number[]): [number, number, number] {
/**
* +--------+--------+--------+--------+--------+
* | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 |
@@ -64,7 +66,9 @@ function generateDecimalSas(sasBytes) {
];
}
const emojiMapping = [
type EmojiMapping = [emoji: string, name: string];
const emojiMapping: EmojiMapping[] = [
["🐶", "dog"], // 0
["🐱", "cat"], // 1
["🦁", "lion"], // 2
@@ -131,7 +135,7 @@ const emojiMapping = [
["📌", "pin"], // 63
];
function generateEmojiSas(sasBytes) {
function generateEmojiSas(sasBytes: number[]): EmojiMapping[] {
const emojis = [
// just like base64 encoding
sasBytes[0] >> 2,
@@ -151,8 +155,20 @@ const sasGenerators = {
emoji: generateEmojiSas,
};
function generateSas(sasBytes, methods) {
const sas = {};
export interface IGeneratedSas {
decimal?: [number, number, number];
emoji?: EmojiMapping[];
}
export interface ISasEvent {
sas: IGeneratedSas;
confirm(): Promise<void>;
cancel(): void;
mismatch(): void;
}
function generateSas(sasBytes: number[], methods: string[]): IGeneratedSas {
const sas: IGeneratedSas = {};
for (const method of methods) {
if (method in sasGenerators) {
sas[method] = sasGenerators[method](sasBytes);
@@ -166,7 +182,7 @@ const macMethods = {
"hmac-sha256": "calculate_mac_long_kdf",
};
function calculateMAC(olmSAS, method) {
function calculateMAC(olmSAS: OlmSAS, method: string) {
return function(...args) {
const macFunction = olmSAS[macMethods[method]];
const mac = macFunction.apply(olmSAS, args);
@@ -176,23 +192,23 @@ function calculateMAC(olmSAS, method) {
}
const calculateKeyAgreement = {
"curve25519-hkdf-sha256": function(sas, olmSAS, bytes) {
const ourInfo = `${sas._baseApis.getUserId()}|${sas._baseApis.deviceId}|`
"curve25519-hkdf-sha256": function(sas: SAS, olmSAS: OlmSAS, bytes: number): Uint8Array {
const ourInfo = `${sas.baseApis.getUserId()}|${sas.baseApis.deviceId}|`
+ `${sas.ourSASPubKey}|`;
const theirInfo = `${sas.userId}|${sas.deviceId}|${sas.theirSASPubKey}|`;
const sasInfo =
"MATRIX_KEY_VERIFICATION_SAS|"
+ (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo)
+ sas._channel.transactionId;
+ sas.channel.transactionId;
return olmSAS.generate_bytes(sasInfo, bytes);
},
"curve25519": function(sas, olmSAS, bytes) {
const ourInfo = `${sas._baseApis.getUserId()}${sas._baseApis.deviceId}`;
"curve25519": function(sas: SAS, olmSAS: OlmSAS, bytes: number): Uint8Array {
const ourInfo = `${sas.baseApis.getUserId()}${sas.baseApis.deviceId}`;
const theirInfo = `${sas.userId}${sas.deviceId}`;
const sasInfo =
"MATRIX_KEY_VERIFICATION_SAS"
+ (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo)
+ sas._channel.transactionId;
+ sas.channel.transactionId;
return olmSAS.generate_bytes(sasInfo, bytes);
},
};
@@ -211,7 +227,7 @@ const HASHES_SET = new Set(HASHES_LIST);
const MAC_SET = new Set(MAC_LIST);
const SAS_SET = new Set(SAS_LIST);
function intersection(anArray, aSet) {
function intersection<T>(anArray: T[], aSet: Set<T>): T[] {
return anArray instanceof Array ? anArray.filter(x => aSet.has(x)) : [];
}
@@ -220,28 +236,34 @@ function intersection(anArray, aSet) {
* @extends {module:crypto/verification/Base}
*/
export class SAS extends Base {
static get NAME() {
private waitingForAccept: boolean;
public ourSASPubKey: string;
public theirSASPubKey: string;
public sasEvent: ISasEvent;
// eslint-disable-next-line @typescript-eslint/naming-convention
public static get NAME(): string {
return "m.sas.v1";
}
get events() {
public get events(): string[] {
return EVENTS;
}
async _doVerification() {
protected doVerification = async (): Promise<void> => {
await global.Olm.init();
olmutil = olmutil || new global.Olm.Utility();
// make sure user's keys are downloaded
await this._baseApis.downloadKeys([this.userId]);
await this.baseApis.downloadKeys([this.userId]);
let retry = false;
do {
try {
if (this.initiatedByMe) {
return await this._doSendVerification();
return await this.doSendVerification();
} else {
return await this._doRespondVerification();
return await this.doRespondVerification();
}
} catch (err) {
if (err instanceof SwitchStartEventError) {
@@ -253,38 +275,37 @@ export class SAS extends Base {
}
}
} while (retry);
}
};
canSwitchStartEvent(event) {
public canSwitchStartEvent(event: MatrixEvent): boolean {
if (event.getType() !== START_TYPE) {
return false;
}
const content = event.getContent();
return content && content.method === SAS.NAME &&
this._waitingForAccept;
return content && content.method === SAS.NAME && this.waitingForAccept;
}
async _sendStart() {
const startContent = this._channel.completeContent(START_TYPE, {
private async sendStart(): Promise<Record<string, any>> {
const startContent = this.channel.completeContent(START_TYPE, {
method: SAS.NAME,
from_device: this._baseApis.deviceId,
from_device: this.baseApis.deviceId,
key_agreement_protocols: KEY_AGREEMENT_LIST,
hashes: HASHES_LIST,
message_authentication_codes: MAC_LIST,
// FIXME: allow app to specify what SAS methods can be used
short_authentication_string: SAS_LIST,
});
await this._channel.sendCompleted(START_TYPE, startContent);
await this.channel.sendCompleted(START_TYPE, startContent);
return startContent;
}
async _doSendVerification() {
this._waitingForAccept = true;
private async doSendVerification(): Promise<void> {
this.waitingForAccept = true;
let startContent;
if (this.startEvent) {
startContent = this._channel.completedContentFromEvent(this.startEvent);
startContent = this.channel.completedContentFromEvent(this.startEvent);
} else {
startContent = await this._sendStart();
startContent = await this.sendStart();
}
// we might have switched to a different start event,
@@ -297,9 +318,9 @@ export class SAS extends Base {
let e;
try {
e = await this._waitForEvent("m.key.verification.accept");
e = await this.waitForEvent("m.key.verification.accept");
} finally {
this._waitingForAccept = false;
this.waitingForAccept = false;
}
let content = e.getContent();
const sasMethods
@@ -319,11 +340,11 @@ export class SAS extends Base {
const olmSAS = new global.Olm.SAS();
try {
this.ourSASPubKey = olmSAS.get_pubkey();
await this._send("m.key.verification.key", {
await this.send("m.key.verification.key", {
key: this.ourSASPubKey,
});
e = await this._waitForEvent("m.key.verification.key");
e = await this.waitForEvent("m.key.verification.key");
// FIXME: make sure event is properly formed
content = e.getContent();
const commitmentStr = content.key + anotherjson.stringify(startContent);
@@ -335,12 +356,12 @@ export class SAS extends Base {
olmSAS.set_their_key(content.key);
const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
const verifySAS = new Promise((resolve, reject) => {
const verifySAS = new Promise<void>((resolve, reject) => {
this.sasEvent = {
sas: generateSas(sasBytes, sasMethods),
confirm: async () => {
try {
await this._sendMAC(olmSAS, macMethod);
await this.sendMAC(olmSAS, macMethod);
resolve();
} catch (err) {
reject(err);
@@ -353,54 +374,45 @@ export class SAS extends Base {
});
[e] = await Promise.all([
this._waitForEvent("m.key.verification.mac")
this.waitForEvent("m.key.verification.mac")
.then((e) => {
// we don't expect any more messages from the other
// party, and they may send a m.key.verification.done
// when they're done on their end
this._expectedEvent = "m.key.verification.done";
this.expectedEvent = "m.key.verification.done";
return e;
}),
verifySAS,
]);
content = e.getContent();
await this._checkMAC(olmSAS, content, macMethod);
await this.checkMAC(olmSAS, content, macMethod);
} finally {
olmSAS.free();
}
}
async _doRespondVerification() {
private async doRespondVerification(): Promise<void> {
// as m.related_to is not included in the encrypted content in e2e rooms,
// we need to make sure it is added
let content = this._channel.completedContentFromEvent(this.startEvent);
let content = this.channel.completedContentFromEvent(this.startEvent);
// Note: we intersect using our pre-made lists, rather than the sets,
// so that the result will be in our order of preference. Then
// fetching the first element from the array will give our preferred
// method out of the ones offered by the other party.
const keyAgreement
= intersection(
KEY_AGREEMENT_LIST, new Set(content.key_agreement_protocols),
)[0];
const hashMethod
= intersection(HASHES_LIST, new Set(content.hashes))[0];
const macMethod
= intersection(MAC_LIST, new Set(content.message_authentication_codes))[0];
const keyAgreement = intersection(KEY_AGREEMENT_LIST, new Set(content.key_agreement_protocols))[0];
const hashMethod = intersection(HASHES_LIST, new Set(content.hashes))[0];
const macMethod = intersection(MAC_LIST, new Set(content.message_authentication_codes))[0];
// FIXME: allow app to specify what SAS methods can be used
const sasMethods
= intersection(content.short_authentication_string, SAS_SET);
if (!(keyAgreement !== undefined
&& hashMethod !== undefined
&& macMethod !== undefined
&& sasMethods.length)) {
const sasMethods = intersection(content.short_authentication_string, SAS_SET);
if (!(keyAgreement !== undefined && hashMethod !== undefined && macMethod !== undefined && sasMethods.length)) {
throw newUnknownMethodError();
}
const olmSAS = new global.Olm.SAS();
try {
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
await this._send("m.key.verification.accept", {
await this.send("m.key.verification.accept", {
key_agreement_protocol: keyAgreement,
hash: hashMethod,
message_authentication_code: macMethod,
@@ -409,23 +421,23 @@ export class SAS extends Base {
commitment: olmutil.sha256(commitmentStr),
});
let e = await this._waitForEvent("m.key.verification.key");
let e = await this.waitForEvent("m.key.verification.key");
// FIXME: make sure event is properly formed
content = e.getContent();
this.theirSASPubKey = content.key;
olmSAS.set_their_key(content.key);
this.ourSASPubKey = olmSAS.get_pubkey();
await this._send("m.key.verification.key", {
await this.send("m.key.verification.key", {
key: this.ourSASPubKey,
});
const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
const verifySAS = new Promise((resolve, reject) => {
const verifySAS = new Promise<void>((resolve, reject) => {
this.sasEvent = {
sas: generateSas(sasBytes, sasMethods),
confirm: async () => {
try {
await this._sendMAC(olmSAS, macMethod);
await this.sendMAC(olmSAS, macMethod);
resolve();
} catch (err) {
reject(err);
@@ -438,39 +450,39 @@ export class SAS extends Base {
});
[e] = await Promise.all([
this._waitForEvent("m.key.verification.mac")
this.waitForEvent("m.key.verification.mac")
.then((e) => {
// we don't expect any more messages from the other
// party, and they may send a m.key.verification.done
// when they're done on their end
this._expectedEvent = "m.key.verification.done";
this.expectedEvent = "m.key.verification.done";
return e;
}),
verifySAS,
]);
content = e.getContent();
await this._checkMAC(olmSAS, content, macMethod);
await this.checkMAC(olmSAS, content, macMethod);
} finally {
olmSAS.free();
}
}
_sendMAC(olmSAS, method) {
private sendMAC(olmSAS: OlmSAS, method: string): Promise<void> {
const mac = {};
const keyList = [];
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this.baseApis.getUserId() + this.baseApis.deviceId
+ this.userId + this.deviceId
+ this._channel.transactionId;
+ this.channel.transactionId;
const deviceKeyId = `ed25519:${this._baseApis.deviceId}`;
const deviceKeyId = `ed25519:${this.baseApis.deviceId}`;
mac[deviceKeyId] = calculateMAC(olmSAS, method)(
this._baseApis.getDeviceEd25519Key(),
this.baseApis.getDeviceEd25519Key(),
baseInfo + deviceKeyId,
);
keyList.push(deviceKeyId);
const crossSigningId = this._baseApis.getCrossSigningId();
const crossSigningId = this.baseApis.getCrossSigningId();
if (crossSigningId) {
const crossSigningKeyId = `ed25519:${crossSigningId}`;
mac[crossSigningKeyId] = calculateMAC(olmSAS, method)(
@@ -484,14 +496,14 @@ export class SAS extends Base {
keyList.sort().join(","),
baseInfo + "KEY_IDS",
);
return this._send("m.key.verification.mac", { mac, keys });
return this.send("m.key.verification.mac", { mac, keys });
}
async _checkMAC(olmSAS, content, method) {
private async checkMAC(olmSAS: OlmSAS, content: IContent, method: string): Promise<void> {
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
+ this.userId + this.deviceId
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this._channel.transactionId;
+ this.baseApis.getUserId() + this.baseApis.deviceId
+ this.channel.transactionId;
if (content.keys !== calculateMAC(olmSAS, method)(
Object.keys(content.mac).sort().join(","),
@@ -500,7 +512,7 @@ export class SAS extends Base {
throw newKeyMismatchError();
}
await this._verifyKeys(this.userId, content.mac, (keyId, device, keyInfo) => {
await this.verifyKeys(this.userId, content.mac, (keyId, device, keyInfo) => {
if (keyInfo !== calculateMAC(olmSAS, method)(
device.keys[keyId],
baseInfo + keyId,
@@ -0,0 +1,33 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixEvent } from "../../../models/event";
import { VerificationRequest } from "./VerificationRequest";
export interface IVerificationChannel {
request?: VerificationRequest;
readonly userId: string;
readonly roomId?: string;
readonly deviceId?: string;
readonly transactionId: string;
readonly receiveStartFromOtherDevices?: boolean;
getTimestamp(event: MatrixEvent): number;
send(type: string, uncompletedContent: Record<string, any>): Promise<void>;
completeContent(type: string, content: Record<string, any>): Record<string, any>;
sendCompleted(type: string, content: Record<string, any>): Promise<void>;
completedContentFromEvent(event: MatrixEvent): Record<string, any>;
canCreateRequest(type: string): boolean;
}
@@ -22,8 +22,12 @@ import {
START_TYPE,
} from "./VerificationRequest";
import { logger } from '../../../logger';
import { IVerificationChannel } from "./Channel";
import { EventType } from "../../../@types/event";
import { MatrixClient } from "../../../client";
import { MatrixEvent } from "../../../models/event";
const MESSAGE_TYPE = "m.room.message";
const MESSAGE_TYPE = EventType.RoomMessage;
const M_REFERENCE = "m.reference";
const M_RELATES_TO = "m.relates_to";
@@ -31,36 +35,34 @@ const M_RELATES_TO = "m.relates_to";
* A key verification channel that sends verification events in the timeline of a room.
* Uses the event id of the initial m.key.verification.request event as a transaction id.
*/
export class InRoomChannel {
export class InRoomChannel implements IVerificationChannel {
private requestEventId = null;
/**
* @param {MatrixClient} client the matrix client, to send messages with and get current user & device from.
* @param {string} roomId id of the room where verification events should be posted in, should be a DM with the given user.
* @param {string} userId id of user that the verification request is directed at, should be present in the room.
*/
constructor(client, roomId, userId = null) {
this._client = client;
this._roomId = roomId;
this.userId = userId;
this._requestEventId = null;
constructor(
private readonly client: MatrixClient,
public readonly roomId: string,
public userId: string = null,
) {
}
get receiveStartFromOtherDevices() {
public get receiveStartFromOtherDevices(): boolean {
return true;
}
get roomId() {
return this._roomId;
}
/** The transaction id generated/used by this verification channel */
get transactionId() {
return this._requestEventId;
public get transactionId(): string {
return this.requestEventId;
}
static getOtherPartyUserId(event, client) {
public static getOtherPartyUserId(event: MatrixEvent, client: MatrixClient): string {
const type = InRoomChannel.getEventType(event);
if (type !== REQUEST_TYPE) {
return;
return;
}
const ownUserId = client.getUserId();
const sender = event.getSender();
@@ -78,25 +80,29 @@ export class InRoomChannel {
* @param {MatrixEvent} event the event to get the timestamp of
* @return {number} the timestamp when the event was sent
*/
getTimestamp(event) {
public getTimestamp(event: MatrixEvent): number {
return event.getTs();
}
/**
* Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel
* @param {string} type the event type to check
* @returns {bool} boolean flag
* @returns {boolean} boolean flag
*/
static canCreateRequest(type) {
public static canCreateRequest(type: string): boolean {
return type === REQUEST_TYPE;
}
public canCreateRequest(type: string): boolean {
return InRoomChannel.canCreateRequest(type);
}
/**
* Extract the transaction id used by a given key verification event, if any
* @param {MatrixEvent} event the event
* @returns {string} the transaction id
*/
static getTransactionId(event) {
public static getTransactionId(event: MatrixEvent): string {
if (InRoomChannel.getEventType(event) === REQUEST_TYPE) {
return event.getId();
} else {
@@ -114,9 +120,9 @@ export class InRoomChannel {
* `handleEvent` can do more checks and choose to ignore invalid events.
* @param {MatrixEvent} event the event to validate
* @param {MatrixClient} client the client to get the current user and device id from
* @returns {bool} whether the event is valid and should be passed to handleEvent
* @returns {boolean} whether the event is valid and should be passed to handleEvent
*/
static validateEvent(event, client) {
public static validateEvent(event: MatrixEvent, client: MatrixClient): boolean {
const txnId = InRoomChannel.getTransactionId(event);
if (typeof txnId !== "string" || txnId.length === 0) {
return false;
@@ -152,7 +158,7 @@ export class InRoomChannel {
* @param {MatrixEvent} event the event to get the type of
* @returns {string} the "symbolic" event type
*/
static getEventType(event) {
public static getEventType(event: MatrixEvent): string {
const type = event.getType();
if (type === MESSAGE_TYPE) {
const content = event.getContent();
@@ -174,10 +180,10 @@ export class InRoomChannel {
* Changes the state of the channel, request, and verifier in response to a key verification event.
* @param {MatrixEvent} event to handle
* @param {VerificationRequest} request the request to forward handling to
* @param {bool} isLiveEvent whether this is an even received through sync or not
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
* @param {boolean} isLiveEvent whether this is an even received through sync or not
* @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent.
*/
async handleEvent(event, request, isLiveEvent) {
public async handleEvent(event: MatrixEvent, request: VerificationRequest, isLiveEvent = false): Promise<void> {
// prevent processing the same event multiple times, as under
// some circumstances Room.timeline can get emitted twice for the same event
if (request.hasEventId(event.getId())) {
@@ -187,18 +193,18 @@ export class InRoomChannel {
// do validations that need state (roomId, userId),
// ignore if invalid
if (event.getRoomId() !== this._roomId) {
if (event.getRoomId() !== this.roomId) {
return;
}
// set userId if not set already
if (this.userId === null) {
const userId = InRoomChannel.getOtherPartyUserId(event, this._client);
const userId = InRoomChannel.getOtherPartyUserId(event, this.client);
if (userId) {
this.userId = userId;
}
}
// ignore events not sent by us or the other party
const ownUserId = this._client.getUserId();
const ownUserId = this.client.getUserId();
const sender = event.getSender();
if (this.userId !== null) {
if (sender !== ownUserId && sender !== this.userId) {
@@ -207,12 +213,12 @@ export class InRoomChannel {
return;
}
}
if (this._requestEventId === null) {
this._requestEventId = InRoomChannel.getTransactionId(event);
if (this.requestEventId === null) {
this.requestEventId = InRoomChannel.getTransactionId(event);
}
const isRemoteEcho = !!event.getUnsigned().transaction_id;
const isSentByUs = event.getSender() === this._client.getUserId();
const isSentByUs = event.getSender() === this.client.getUserId();
return await request.handleEvent(
type, event, isLiveEvent, isRemoteEcho, isSentByUs);
@@ -226,13 +232,14 @@ export class InRoomChannel {
* @param {MatrixEvent} event the received event
* @returns {Object} the content object with the relation added again
*/
completedContentFromEvent(event) {
public completedContentFromEvent(event: MatrixEvent): Record<string, any> {
// ensure m.related_to is included in e2ee rooms
// as the field is excluded from encryption
const content = Object.assign({}, event.getContent());
content[M_RELATES_TO] = event.getRelation();
return content;
}
/**
* Add all the fields to content needed for sending it over this channel.
* This is public so verification methods (SAS uses this) can get the exact
@@ -242,15 +249,15 @@ export class InRoomChannel {
* @param {object} content the (incomplete) content
* @returns {object} the complete content, as it will be sent.
*/
completeContent(type, content) {
public completeContent(type: string, content: Record<string, any>): Record<string, any> {
content = Object.assign({}, content);
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
content.from_device = this._client.getDeviceId();
content.from_device = this.client.getDeviceId();
}
if (type === REQUEST_TYPE) {
// type is mapped to m.room.message in the send method
content = {
body: this._client.getUserId() + " is requesting to verify " +
body: this.client.getUserId() + " is requesting to verify " +
"your key, but your client does not support in-chat key " +
"verification. You will need to use legacy key " +
"verification to verify keys.",
@@ -274,7 +281,7 @@ export class InRoomChannel {
* @param {object} uncompletedContent the (incomplete) content
* @returns {Promise} the promise of the request
*/
send(type, uncompletedContent) {
public send(type: string, uncompletedContent: Record<string, any>): Promise<void> {
const content = this.completeContent(type, uncompletedContent);
return this.sendCompleted(type, content);
}
@@ -285,74 +292,68 @@ export class InRoomChannel {
* @param {object} content
* @returns {Promise} the promise of the request
*/
async sendCompleted(type, content) {
public async sendCompleted(type: string, content: Record<string, any>): Promise<void> {
let sendType = type;
if (type === REQUEST_TYPE) {
sendType = MESSAGE_TYPE;
}
const response = await this._client.sendEvent(this._roomId, sendType, content);
const response = await this.client.sendEvent(this.roomId, sendType, content);
if (type === REQUEST_TYPE) {
this._requestEventId = response.event_id;
this.requestEventId = response.event_id;
}
}
}
export class InRoomRequests {
constructor() {
this._requestsByRoomId = new Map();
}
private requestsByRoomId = new Map<string, Map<string, VerificationRequest>>();
getRequest(event) {
public getRequest(event: MatrixEvent): VerificationRequest {
const roomId = event.getRoomId();
const txnId = InRoomChannel.getTransactionId(event);
return this._getRequestByTxnId(roomId, txnId);
return this.getRequestByTxnId(roomId, txnId);
}
getRequestByChannel(channel) {
return this._getRequestByTxnId(channel.roomId, channel.transactionId);
public getRequestByChannel(channel: InRoomChannel): VerificationRequest {
return this.getRequestByTxnId(channel.roomId, channel.transactionId);
}
_getRequestByTxnId(roomId, txnId) {
const requestsByTxnId = this._requestsByRoomId.get(roomId);
private getRequestByTxnId(roomId: string, txnId: string): VerificationRequest {
const requestsByTxnId = this.requestsByRoomId.get(roomId);
if (requestsByTxnId) {
return requestsByTxnId.get(txnId);
}
}
setRequest(event, request) {
this._setRequest(
event.getRoomId(),
InRoomChannel.getTransactionId(event),
request,
);
public setRequest(event: MatrixEvent, request: VerificationRequest): void {
this.doSetRequest(event.getRoomId(), InRoomChannel.getTransactionId(event), request);
}
setRequestByChannel(channel, request) {
this._setRequest(channel.roomId, channel.transactionId, request);
public setRequestByChannel(channel: InRoomChannel, request: VerificationRequest): void {
this.doSetRequest(channel.roomId, channel.transactionId, request);
}
_setRequest(roomId, txnId, request) {
let requestsByTxnId = this._requestsByRoomId.get(roomId);
private doSetRequest(roomId: string, txnId: string, request: VerificationRequest): void {
let requestsByTxnId = this.requestsByRoomId.get(roomId);
if (!requestsByTxnId) {
requestsByTxnId = new Map();
this._requestsByRoomId.set(roomId, requestsByTxnId);
this.requestsByRoomId.set(roomId, requestsByTxnId);
}
requestsByTxnId.set(txnId, request);
}
removeRequest(event) {
public removeRequest(event: MatrixEvent): void {
const roomId = event.getRoomId();
const requestsByTxnId = this._requestsByRoomId.get(roomId);
const requestsByTxnId = this.requestsByRoomId.get(roomId);
if (requestsByTxnId) {
requestsByTxnId.delete(InRoomChannel.getTransactionId(event));
if (requestsByTxnId.size === 0) {
this._requestsByRoomId.delete(roomId);
this.requestsByRoomId.delete(roomId);
}
}
}
findRequestInProgress(roomId) {
const requestsByTxnId = this._requestsByRoomId.get(roomId);
public findRequestInProgress(roomId: string): VerificationRequest {
const requestsByTxnId = this.requestsByRoomId.get(roomId);
if (requestsByTxnId) {
for (const request of requestsByTxnId.values()) {
if (request.pending) {
@@ -28,26 +28,31 @@ import {
} from "./VerificationRequest";
import { errorFromEvent, newUnexpectedMessageError } from "../Error";
import { MatrixEvent } from "../../../models/event";
import { IVerificationChannel } from "./Channel";
import { MatrixClient } from "../../../client";
type Request = VerificationRequest<ToDeviceChannel>;
/**
* A key verification channel that sends verification events over to_device messages.
* Generates its own transaction ids.
*/
export class ToDeviceChannel {
// userId and devices of user we're about to verify
constructor(client, userId, devices, transactionId = null, deviceId = null) {
this._client = client;
this.userId = userId;
this._devices = devices;
this.transactionId = transactionId;
this._deviceId = deviceId;
}
export class ToDeviceChannel implements IVerificationChannel {
public request?: VerificationRequest;
isToDevices(devices) {
if (devices.length === this._devices.length) {
// userId and devices of user we're about to verify
constructor(
private readonly client: MatrixClient,
public readonly userId: string,
private readonly devices: string[],
public transactionId: string = null,
public deviceId: string = null,
) {}
public isToDevices(devices: string[]): boolean {
if (devices.length === this.devices.length) {
for (const device of devices) {
const d = this._devices.find(d => d.deviceId === device.deviceId);
if (!d) {
if (!this.devices.includes(device)) {
return false;
}
}
@@ -57,11 +62,7 @@ export class ToDeviceChannel {
}
}
get deviceId() {
return this._deviceId;
}
static getEventType(event) {
public static getEventType(event: MatrixEvent): string {
return event.getType();
}
@@ -70,7 +71,7 @@ export class ToDeviceChannel {
* @param {MatrixEvent} event the event
* @returns {string} the transaction id
*/
static getTransactionId(event) {
public static getTransactionId(event: MatrixEvent): string {
const content = event.getContent();
return content && content.transaction_id;
}
@@ -78,12 +79,16 @@ export class ToDeviceChannel {
/**
* Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel
* @param {string} type the event type to check
* @returns {bool} boolean flag
* @returns {boolean} boolean flag
*/
static canCreateRequest(type) {
public static canCreateRequest(type: string): boolean {
return type === REQUEST_TYPE || type === START_TYPE;
}
public canCreateRequest(type: string): boolean {
return ToDeviceChannel.canCreateRequest(type);
}
/**
* Checks whether this event is a well-formed key verification event.
* This only does checks that don't rely on the current state of a potentially already channel
@@ -91,9 +96,9 @@ export class ToDeviceChannel {
* `handleEvent` can do more checks and choose to ignore invalid events.
* @param {MatrixEvent} event the event to validate
* @param {MatrixClient} client the client to get the current user and device id from
* @returns {bool} whether the event is valid and should be passed to handleEvent
* @returns {boolean} whether the event is valid and should be passed to handleEvent
*/
static validateEvent(event, client) {
public static validateEvent(event: MatrixEvent, client: MatrixClient): boolean {
if (event.isCancelled()) {
logger.warn("Ignoring flagged verification request from "
+ event.getSender());
@@ -134,7 +139,7 @@ export class ToDeviceChannel {
* @param {MatrixEvent} event the event to get the timestamp of
* @return {number} the timestamp when the event was sent
*/
getTimestamp(event) {
public getTimestamp(event: MatrixEvent): number {
const content = event.getContent();
return content && content.timestamp;
}
@@ -143,10 +148,10 @@ export class ToDeviceChannel {
* Changes the state of the channel, request, and verifier in response to a key verification event.
* @param {MatrixEvent} event to handle
* @param {VerificationRequest} request the request to forward handling to
* @param {bool} isLiveEvent whether this is an even received through sync or not
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
* @param {boolean} isLiveEvent whether this is an even received through sync or not
* @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent.
*/
async handleEvent(event, request, isLiveEvent) {
public async handleEvent(event: MatrixEvent, request: Request, isLiveEvent = false): Promise<void> {
const type = event.getType();
const content = event.getContent();
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
@@ -155,17 +160,16 @@ export class ToDeviceChannel {
}
const deviceId = content.from_device;
// adopt deviceId if not set before and valid
if (!this._deviceId && this._devices.includes(deviceId)) {
this._deviceId = deviceId;
if (!this.deviceId && this.devices.includes(deviceId)) {
this.deviceId = deviceId;
}
// if no device id or different from addopted one, cancel with sender
if (!this._deviceId || this._deviceId !== deviceId) {
// if no device id or different from adopted one, cancel with sender
if (!this.deviceId || this.deviceId !== deviceId) {
// also check that message came from the device we sent the request to earlier on
// and do send a cancel message to that device
// (but don't cancel the request for the device we should be talking to)
const cancelContent =
this.completeContent(errorFromEvent(newUnexpectedMessageError()));
return this._sendToDevices(CANCEL_TYPE, cancelContent, [deviceId]);
const cancelContent = this.completeContent(CANCEL_TYPE, errorFromEvent(newUnexpectedMessageError()));
return this.sendToDevices(CANCEL_TYPE, cancelContent, [deviceId]);
}
}
const wasStarted = request.phase === PHASE_STARTED ||
@@ -178,16 +182,16 @@ export class ToDeviceChannel {
const isAcceptingEvent = type === START_TYPE || type === READY_TYPE;
// the request has picked a ready or start event, tell the other devices about it
if (isAcceptingEvent && !wasStarted && isStarted && this._deviceId) {
const nonChosenDevices = this._devices.filter(
d => d !== this._deviceId && d !== this._client.getDeviceId(),
if (isAcceptingEvent && !wasStarted && isStarted && this.deviceId) {
const nonChosenDevices = this.devices.filter(
d => d !== this.deviceId && d !== this.client.getDeviceId(),
);
if (nonChosenDevices.length) {
const message = this.completeContent({
const message = this.completeContent(CANCEL_TYPE, {
code: "m.accepted",
reason: "Verification request accepted by another device",
});
await this._sendToDevices(CANCEL_TYPE, message, nonChosenDevices);
await this.sendToDevices(CANCEL_TYPE, message, nonChosenDevices);
}
}
}
@@ -197,7 +201,7 @@ export class ToDeviceChannel {
* @param {MatrixEvent} event the received event
* @returns {Object} the content object
*/
completedContentFromEvent(event) {
public completedContentFromEvent(event: MatrixEvent): Record<string, any> {
return event.getContent();
}
@@ -210,14 +214,14 @@ export class ToDeviceChannel {
* @param {object} content the (incomplete) content
* @returns {object} the complete content, as it will be sent.
*/
completeContent(type, content) {
public completeContent(type: string, content: Record<string, any>): Record<string, any> {
// make a copy
content = Object.assign({}, content);
if (this.transactionId) {
content.transaction_id = this.transactionId;
}
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
content.from_device = this._client.getDeviceId();
content.from_device = this.client.getDeviceId();
}
if (type === REQUEST_TYPE) {
content.timestamp = Date.now();
@@ -231,7 +235,7 @@ export class ToDeviceChannel {
* @param {object} uncompletedContent the (incomplete) content
* @returns {Promise} the promise of the request
*/
send(type, uncompletedContent = {}) {
public send(type: string, uncompletedContent: Record<string, any> = {}): Promise<void> {
// create transaction id when sending request
if ((type === REQUEST_TYPE || type === START_TYPE) && !this.transactionId) {
this.transactionId = ToDeviceChannel.makeTransactionId();
@@ -246,21 +250,21 @@ export class ToDeviceChannel {
* @param {object} content
* @returns {Promise} the promise of the request
*/
async sendCompleted(type, content) {
public async sendCompleted(type: string, content: Record<string, any>): Promise<void> {
let result;
if (type === REQUEST_TYPE) {
result = await this._sendToDevices(type, content, this._devices);
if (type === REQUEST_TYPE || (type === CANCEL_TYPE && !this.deviceId)) {
result = await this.sendToDevices(type, content, this.devices);
} else {
result = await this._sendToDevices(type, content, [this._deviceId]);
result = await this.sendToDevices(type, content, [this.deviceId]);
}
// the VerificationRequest state machine requires remote echos of the event
// the client sends itself, so we fake this for to_device messages
const remoteEchoEvent = new MatrixEvent({
sender: this._client.getUserId(),
sender: this.client.getUserId(),
content,
type,
});
await this._request.handleEvent(
await this.request.handleEvent(
type,
remoteEchoEvent,
/*isLiveEvent=*/true,
@@ -270,16 +274,14 @@ export class ToDeviceChannel {
return result;
}
_sendToDevices(type, content, devices) {
private async sendToDevices(type: string, content: Record<string, any>, devices: string[]): Promise<void> {
if (devices.length) {
const msgMap = {};
for (const deviceId of devices) {
msgMap[deviceId] = content;
}
return this._client.sendToDevice(type, { [this.userId]: msgMap });
} else {
return Promise.resolve();
await this.client.sendToDevice(type, { [this.userId]: msgMap });
}
}
@@ -287,68 +289,62 @@ export class ToDeviceChannel {
* Allow Crypto module to create and know the transaction id before the .start event gets sent.
* @returns {string} the transaction id
*/
static makeTransactionId() {
public static makeTransactionId(): string {
return randomString(32);
}
}
export class ToDeviceRequests {
constructor() {
this._requestsByUserId = new Map();
}
private requestsByUserId = new Map<string, Map<string, Request>>();
getRequest(event) {
public getRequest(event: MatrixEvent): Request {
return this.getRequestBySenderAndTxnId(
event.getSender(),
ToDeviceChannel.getTransactionId(event),
);
}
getRequestByChannel(channel) {
public getRequestByChannel(channel: ToDeviceChannel): Request {
return this.getRequestBySenderAndTxnId(channel.userId, channel.transactionId);
}
getRequestBySenderAndTxnId(sender, txnId) {
const requestsByTxnId = this._requestsByUserId.get(sender);
public getRequestBySenderAndTxnId(sender: string, txnId: string): Request {
const requestsByTxnId = this.requestsByUserId.get(sender);
if (requestsByTxnId) {
return requestsByTxnId.get(txnId);
}
}
setRequest(event, request) {
this.setRequestBySenderAndTxnId(
event.getSender(),
ToDeviceChannel.getTransactionId(event),
request,
);
public setRequest(event: MatrixEvent, request: Request): void {
this.setRequestBySenderAndTxnId(event.getSender(), ToDeviceChannel.getTransactionId(event), request);
}
setRequestByChannel(channel, request) {
public setRequestByChannel(channel: ToDeviceChannel, request: Request): void {
this.setRequestBySenderAndTxnId(channel.userId, channel.transactionId, request);
}
setRequestBySenderAndTxnId(sender, txnId, request) {
let requestsByTxnId = this._requestsByUserId.get(sender);
public setRequestBySenderAndTxnId(sender: string, txnId: string, request: Request): void {
let requestsByTxnId = this.requestsByUserId.get(sender);
if (!requestsByTxnId) {
requestsByTxnId = new Map();
this._requestsByUserId.set(sender, requestsByTxnId);
this.requestsByUserId.set(sender, requestsByTxnId);
}
requestsByTxnId.set(txnId, request);
}
removeRequest(event) {
public removeRequest(event: MatrixEvent): void {
const userId = event.getSender();
const requestsByTxnId = this._requestsByUserId.get(userId);
const requestsByTxnId = this.requestsByUserId.get(userId);
if (requestsByTxnId) {
requestsByTxnId.delete(ToDeviceChannel.getTransactionId(event));
if (requestsByTxnId.size === 0) {
this._requestsByUserId.delete(userId);
this.requestsByUserId.delete(userId);
}
}
}
findRequestInProgress(userId, devices) {
const requestsByTxnId = this._requestsByUserId.get(userId);
public findRequestInProgress(userId: string, devices: string[]): Request {
const requestsByTxnId = this.requestsByUserId.get(userId);
if (requestsByTxnId) {
for (const request of requestsByTxnId.values()) {
if (request.pending && request.channel.isToDevices(devices)) {
@@ -358,8 +354,8 @@ export class ToDeviceRequests {
}
}
getRequestsInProgress(userId) {
const requestsByTxnId = this._requestsByUserId.get(userId);
public getRequestsInProgress(userId: string): Request[] {
const requestsByTxnId = this.requestsByUserId.get(userId);
if (requestsByTxnId) {
return Array.from(requestsByTxnId.values()).filter(r => r.pending);
}
@@ -1,6 +1,5 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -24,6 +23,11 @@ import {
newUnknownMethodError,
} from "../Error";
import { QRCodeData, SCAN_QR_CODE_METHOD } from "../QRCode";
import { IVerificationChannel } from "./Channel";
import { MatrixClient } from "../../../client";
import { MatrixEvent } from "../../../models/event";
import { VerificationBase } from "../Base";
import { VerificationMethod } from "../../index";
// How long after the event's timestamp that the request times out
const TIMEOUT_FROM_EVENT_TS = 10 * 60 * 1000; // 10 minutes
@@ -44,12 +48,32 @@ export const CANCEL_TYPE = EVENT_PREFIX + "cancel";
export const DONE_TYPE = EVENT_PREFIX + "done";
export const READY_TYPE = EVENT_PREFIX + "ready";
export const PHASE_UNSENT = 1;
export const PHASE_REQUESTED = 2;
export const PHASE_READY = 3;
export const PHASE_STARTED = 4;
export const PHASE_CANCELLED = 5;
export const PHASE_DONE = 6;
export enum Phase {
Unsent = 1,
Requested,
Ready,
Started,
Cancelled,
Done,
}
// Legacy export fields
export const PHASE_UNSENT = Phase.Unsent;
export const PHASE_REQUESTED = Phase.Requested;
export const PHASE_READY = Phase.Ready;
export const PHASE_STARTED = Phase.Started;
export const PHASE_CANCELLED = Phase.Cancelled;
export const PHASE_DONE = Phase.Done;
interface ITargetDevice {
userId?: string;
deviceId?: string;
}
interface ITransition {
phase: Phase;
event?: MatrixEvent;
}
/**
* State machine for verification requests.
@@ -57,32 +81,38 @@ export const PHASE_DONE = 6;
* send and receive verification events are put in `InRoomChannel` or `ToDeviceChannel`.
* @event "change" whenever the state of the request object has changed.
*/
export class VerificationRequest extends EventEmitter {
constructor(channel, verificationMethods, client) {
super();
this.channel = channel;
this.channel._request = this;
this._verificationMethods = verificationMethods;
this._client = client;
this._commonMethods = [];
this._setPhase(PHASE_UNSENT, false);
this._eventsByUs = new Map();
this._eventsByThem = new Map();
this._observeOnly = false;
this._timeoutTimer = null;
this._accepting = false;
this._declining = false;
this._verifierHasFinished = false;
this._cancelled = false;
this._chosenMethod = null;
// we keep a copy of the QR Code data (including other user master key) around
// for QR reciprocate verification, to protect against
// cross-signing identity reset between the .ready and .start event
// and signing the wrong key after .start
this._qrCodeData = null;
export class VerificationRequest<C extends IVerificationChannel = IVerificationChannel> extends EventEmitter {
private eventsByUs = new Map<string, MatrixEvent>();
private eventsByThem = new Map<string, MatrixEvent>();
private _observeOnly = false;
private timeoutTimer: number = null;
private _accepting = false;
private _declining = false;
private verifierHasFinished = false;
private _cancelled = false;
private _chosenMethod: VerificationMethod = null;
// we keep a copy of the QR Code data (including other user master key) around
// for QR reciprocate verification, to protect against
// cross-signing identity reset between the .ready and .start event
// and signing the wrong key after .start
private _qrCodeData: QRCodeData = null;
// The timestamp when we received the request event from the other side
this._requestReceivedAt = null;
// The timestamp when we received the request event from the other side
private requestReceivedAt: number = null;
private commonMethods: VerificationMethod[] = [];
private _phase: Phase;
private _cancellingUserId: string;
private _verifier: VerificationBase;
constructor(
public readonly channel: C,
private readonly verificationMethods: Map<VerificationMethod, typeof VerificationBase>,
private readonly client: MatrixClient,
) {
super();
this.channel.request = this;
this.setPhase(PHASE_UNSENT, false);
}
/**
@@ -91,9 +121,9 @@ export class VerificationRequest extends EventEmitter {
* @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel.
* @param {MatrixEvent} event the event to validate. Don't call getType() on it but use the `type` parameter instead.
* @param {MatrixClient} client the client to get the current user and device id from
* @returns {bool} whether the event is valid and should be passed to handleEvent
* @returns {boolean} whether the event is valid and should be passed to handleEvent
*/
static validateEvent(type, event, client) {
public static validateEvent(type: string, event: MatrixEvent, client: MatrixClient): boolean {
const content = event.getContent();
if (!type || !type.startsWith(EVENT_PREFIX)) {
@@ -128,53 +158,53 @@ export class VerificationRequest extends EventEmitter {
return true;
}
get invalid() {
public get invalid(): boolean {
return this.phase === PHASE_UNSENT;
}
/** returns whether the phase is PHASE_REQUESTED */
get requested() {
public get requested(): boolean {
return this.phase === PHASE_REQUESTED;
}
/** returns whether the phase is PHASE_CANCELLED */
get cancelled() {
public get cancelled(): boolean {
return this.phase === PHASE_CANCELLED;
}
/** returns whether the phase is PHASE_READY */
get ready() {
public get ready(): boolean {
return this.phase === PHASE_READY;
}
/** returns whether the phase is PHASE_STARTED */
get started() {
public get started(): boolean {
return this.phase === PHASE_STARTED;
}
/** returns whether the phase is PHASE_DONE */
get done() {
public get done(): boolean {
return this.phase === PHASE_DONE;
}
/** once the phase is PHASE_STARTED (and !initiatedByMe) or PHASE_READY: common methods supported by both sides */
get methods() {
return this._commonMethods;
public get methods(): VerificationMethod[] {
return this.commonMethods;
}
/** the method picked in the .start event */
get chosenMethod() {
public get chosenMethod(): VerificationMethod {
return this._chosenMethod;
}
calculateEventTimeout(event) {
public calculateEventTimeout(event: MatrixEvent): number {
let effectiveExpiresAt = this.channel.getTimestamp(event)
+ TIMEOUT_FROM_EVENT_TS;
if (this._requestReceivedAt && !this.initiatedByMe &&
if (this.requestReceivedAt && !this.initiatedByMe &&
this.phase <= PHASE_REQUESTED
) {
const expiresAtByReceipt = this._requestReceivedAt
const expiresAtByReceipt = this.requestReceivedAt
+ TIMEOUT_FROM_EVENT_RECEIPT;
effectiveExpiresAt = Math.min(effectiveExpiresAt, expiresAtByReceipt);
}
@@ -183,8 +213,8 @@ export class VerificationRequest extends EventEmitter {
}
/** The current remaining amount of ms before the request should be automatically cancelled */
get timeout() {
const requestEvent = this._getEventByEither(REQUEST_TYPE);
public get timeout(): number {
const requestEvent = this.getEventByEither(REQUEST_TYPE);
if (requestEvent) {
return this.calculateEventTimeout(requestEvent);
}
@@ -195,41 +225,41 @@ export class VerificationRequest extends EventEmitter {
* The key verification request event.
* @returns {MatrixEvent} The request event, or falsey if not found.
*/
get requestEvent() {
return this._getEventByEither(REQUEST_TYPE);
public get requestEvent(): MatrixEvent {
return this.getEventByEither(REQUEST_TYPE);
}
/** current phase of the request. Some properties might only be defined in a current phase. */
get phase() {
public get phase(): Phase {
return this._phase;
}
/** The verifier to do the actual verification, once the method has been established. Only defined when the `phase` is PHASE_STARTED. */
get verifier() {
public get verifier(): VerificationBase {
return this._verifier;
}
get canAccept() {
public get canAccept(): boolean {
return this.phase < PHASE_READY && !this._accepting && !this._declining;
}
get accepting() {
public get accepting(): boolean {
return this._accepting;
}
get declining() {
public get declining(): boolean {
return this._declining;
}
/** whether this request has sent it's initial event and needs more events to complete */
get pending() {
public get pending(): boolean {
return !this.observeOnly &&
this._phase !== PHASE_DONE &&
this._phase !== PHASE_CANCELLED;
}
/** Only set after a .ready if the other party can scan a QR code */
get qrCodeData() {
public get qrCodeData(): QRCodeData {
return this._qrCodeData;
}
@@ -239,19 +269,19 @@ export class VerificationRequest extends EventEmitter {
* For methods that need to be supported by both ends, use the `methods` property.
* @param {string} method the method to check
* @param {boolean} force to check even if the phase is not ready or started yet, internal usage
* @return {bool} whether or not the other party said the supported the method */
otherPartySupportsMethod(method, force = false) {
* @return {boolean} whether or not the other party said the supported the method */
public otherPartySupportsMethod(method: string, force = false): boolean {
if (!force && !this.ready && !this.started) {
return false;
}
const theirMethodEvent = this._eventsByThem.get(REQUEST_TYPE) ||
this._eventsByThem.get(READY_TYPE);
const theirMethodEvent = this.eventsByThem.get(REQUEST_TYPE) ||
this.eventsByThem.get(READY_TYPE);
if (!theirMethodEvent) {
// if we started straight away with .start event,
// we are assuming that the other side will support the
// chosen method, so return true for that.
if (this.started && this.initiatedByMe) {
const myStartEvent = this._eventsByUs.get(START_TYPE);
const myStartEvent = this.eventsByUs.get(START_TYPE);
const content = myStartEvent && myStartEvent.getContent();
const myStartMethod = content && content.method;
return method == myStartMethod;
@@ -274,22 +304,22 @@ export class VerificationRequest extends EventEmitter {
* For InRoomChannel, this is who sent the .request event.
* For ToDeviceChannel, this is who sent the .start event
*/
get initiatedByMe() {
public get initiatedByMe(): boolean {
// event created by us but no remote echo has been received yet
const noEventsYet = (this._eventsByUs.size + this._eventsByThem.size) === 0;
const noEventsYet = (this.eventsByUs.size + this.eventsByThem.size) === 0;
if (this._phase === PHASE_UNSENT && noEventsYet) {
return true;
}
const hasMyRequest = this._eventsByUs.has(REQUEST_TYPE);
const hasTheirRequest = this._eventsByThem.has(REQUEST_TYPE);
const hasMyRequest = this.eventsByUs.has(REQUEST_TYPE);
const hasTheirRequest = this.eventsByThem.has(REQUEST_TYPE);
if (hasMyRequest && !hasTheirRequest) {
return true;
}
if (!hasMyRequest && hasTheirRequest) {
return false;
}
const hasMyStart = this._eventsByUs.has(START_TYPE);
const hasTheirStart = this._eventsByThem.has(START_TYPE);
const hasMyStart = this.eventsByUs.has(START_TYPE);
const hasTheirStart = this.eventsByThem.has(START_TYPE);
if (hasMyStart && !hasTheirStart) {
return true;
}
@@ -297,39 +327,39 @@ export class VerificationRequest extends EventEmitter {
}
/** The id of the user that initiated the request */
get requestingUserId() {
public get requestingUserId(): string {
if (this.initiatedByMe) {
return this._client.getUserId();
return this.client.getUserId();
} else {
return this.otherUserId;
}
}
/** The id of the user that (will) receive(d) the request */
get receivingUserId() {
public get receivingUserId(): string {
if (this.initiatedByMe) {
return this.otherUserId;
} else {
return this._client.getUserId();
return this.client.getUserId();
}
}
/** The user id of the other party in this request */
get otherUserId() {
public get otherUserId(): string {
return this.channel.userId;
}
get isSelfVerification() {
return this._client.getUserId() === this.otherUserId;
public get isSelfVerification(): boolean {
return this.client.getUserId() === this.otherUserId;
}
/**
* The id of the user that cancelled the request,
* only defined when phase is PHASE_CANCELLED
*/
get cancellingUserId() {
const myCancel = this._eventsByUs.get(CANCEL_TYPE);
const theirCancel = this._eventsByThem.get(CANCEL_TYPE);
public get cancellingUserId(): string {
const myCancel = this.eventsByUs.get(CANCEL_TYPE);
const theirCancel = this.eventsByThem.get(CANCEL_TYPE);
if (myCancel && (!theirCancel || myCancel.getId() < theirCancel.getId())) {
return myCancel.getSender();
@@ -343,12 +373,12 @@ export class VerificationRequest extends EventEmitter {
/**
* The cancellation code e.g m.user which is responsible for cancelling this verification
*/
get cancellationCode() {
const ev = this._getEventByEither(CANCEL_TYPE);
public get cancellationCode(): string {
const ev = this.getEventByEither(CANCEL_TYPE);
return ev ? ev.getContent().code : null;
}
get observeOnly() {
public get observeOnly(): boolean {
return this._observeOnly;
}
@@ -359,11 +389,11 @@ export class VerificationRequest extends EventEmitter {
* verification to when no specific device is specified.
* @returns {{userId: *, deviceId: *}} The device information
*/
get targetDevice() {
public get targetDevice(): ITargetDevice {
const theirFirstEvent =
this._eventsByThem.get(REQUEST_TYPE) ||
this._eventsByThem.get(READY_TYPE) ||
this._eventsByThem.get(START_TYPE);
this.eventsByThem.get(REQUEST_TYPE) ||
this.eventsByThem.get(READY_TYPE) ||
this.eventsByThem.get(START_TYPE);
const theirFirstContent = theirFirstEvent.getContent();
const fromDevice = theirFirstContent.from_device;
return {
@@ -379,21 +409,20 @@ export class VerificationRequest extends EventEmitter {
* @param {string?} targetDevice.deviceId the id of the device to direct this request to
* @returns {VerifierBase} the verifier of the given method
*/
beginKeyVerification(method, targetDevice = null) {
public beginKeyVerification(method: VerificationMethod, targetDevice: ITargetDevice = null): VerificationBase {
// need to allow also when unsent in case of to_device
if (!this.observeOnly && !this._verifier) {
const validStartPhase =
this.phase === PHASE_REQUESTED ||
this.phase === PHASE_READY ||
(this.phase === PHASE_UNSENT &&
this.channel.constructor.canCreateRequest(START_TYPE));
(this.phase === PHASE_UNSENT && this.channel.canCreateRequest(START_TYPE));
if (validStartPhase) {
// when called on a request that was initiated with .request event
// check the method is supported by both sides
if (this._commonMethods.length && !this._commonMethods.includes(method)) {
if (this.commonMethods.length && !this.commonMethods.includes(method)) {
throw newUnknownMethodError();
}
this._verifier = this._createVerifier(method, null, targetDevice);
this._verifier = this.createVerifier(method, null, targetDevice);
if (!this._verifier) {
throw newUnknownMethodError();
}
@@ -407,9 +436,9 @@ export class VerificationRequest extends EventEmitter {
* sends the initial .request event.
* @returns {Promise} resolves when the event has been sent.
*/
async sendRequest() {
public async sendRequest(): Promise<void> {
if (!this.observeOnly && this._phase === PHASE_UNSENT) {
const methods = [...this._verificationMethods.keys()];
const methods = [...this.verificationMethods.keys()];
await this.channel.send(REQUEST_TYPE, { methods });
}
}
@@ -420,14 +449,14 @@ export class VerificationRequest extends EventEmitter {
* @param {string?} error.code the error code to send the cancellation with
* @returns {Promise} resolves when the event has been sent.
*/
async cancel({ reason = "User declined", code = "m.user" } = {}) {
public async cancel({ reason = "User declined", code = "m.user" } = {}): Promise<void> {
if (!this.observeOnly && this._phase !== PHASE_CANCELLED) {
this._declining = true;
this.emit("change");
if (this._verifier) {
return this._verifier.cancel(errorFactory(code, reason)());
} else {
this._cancellingUserId = this._client.getUserId();
this._cancellingUserId = this.client.getUserId();
await this.channel.send(CANCEL_TYPE, { code, reason });
}
}
@@ -437,9 +466,9 @@ export class VerificationRequest extends EventEmitter {
* Accepts the request, sending a .ready event to the other party
* @returns {Promise} resolves when the event has been sent.
*/
async accept() {
public async accept(): Promise<void> {
if (!this.observeOnly && this.phase === PHASE_REQUESTED && !this.initiatedByMe) {
const methods = [...this._verificationMethods.keys()];
const methods = [...this.verificationMethods.keys()];
this._accepting = true;
this.emit("change");
await this.channel.send(READY_TYPE, { methods });
@@ -453,7 +482,7 @@ export class VerificationRequest extends EventEmitter {
* @returns {Promise} that resolves once the callback returns true
* @throws {Error} when the request is cancelled
*/
waitFor(fn) {
public waitFor(fn: (request: VerificationRequest) => boolean): Promise<VerificationRequest> {
return new Promise((resolve, reject) => {
const check = () => {
let handled = false;
@@ -475,46 +504,46 @@ export class VerificationRequest extends EventEmitter {
});
}
_setPhase(phase, notify = true) {
private setPhase(phase: Phase, notify = true): void {
this._phase = phase;
if (notify) {
this.emit("change");
}
}
_getEventByEither(type) {
return this._eventsByThem.get(type) || this._eventsByUs.get(type);
private getEventByEither(type: string): MatrixEvent {
return this.eventsByThem.get(type) || this.eventsByUs.get(type);
}
_getEventBy(type, byThem) {
private getEventBy(type: string, byThem = false): MatrixEvent {
if (byThem) {
return this._eventsByThem.get(type);
return this.eventsByThem.get(type);
} else {
return this._eventsByUs.get(type);
return this.eventsByUs.get(type);
}
}
_calculatePhaseTransitions() {
const transitions = [{ phase: PHASE_UNSENT }];
private calculatePhaseTransitions(): ITransition[] {
const transitions: ITransition[] = [{ phase: PHASE_UNSENT }];
const phase = () => transitions[transitions.length - 1].phase;
// always pass by .request first to be sure channel.userId has been set
const hasRequestByThem = this._eventsByThem.has(REQUEST_TYPE);
const requestEvent = this._getEventBy(REQUEST_TYPE, hasRequestByThem);
const hasRequestByThem = this.eventsByThem.has(REQUEST_TYPE);
const requestEvent = this.getEventBy(REQUEST_TYPE, hasRequestByThem);
if (requestEvent) {
transitions.push({ phase: PHASE_REQUESTED, event: requestEvent });
}
const readyEvent =
requestEvent && this._getEventBy(READY_TYPE, !hasRequestByThem);
requestEvent && this.getEventBy(READY_TYPE, !hasRequestByThem);
if (readyEvent && phase() === PHASE_REQUESTED) {
transitions.push({ phase: PHASE_READY, event: readyEvent });
}
let startEvent;
if (readyEvent || !requestEvent) {
const theirStartEvent = this._eventsByThem.get(START_TYPE);
const ourStartEvent = this._eventsByUs.get(START_TYPE);
const theirStartEvent = this.eventsByThem.get(START_TYPE);
const ourStartEvent = this.eventsByUs.get(START_TYPE);
// any party can send .start after a .ready or unsent
if (theirStartEvent && ourStartEvent) {
startEvent = theirStartEvent.getSender() < ourStartEvent.getSender() ?
@@ -523,24 +552,22 @@ export class VerificationRequest extends EventEmitter {
startEvent = theirStartEvent ? theirStartEvent : ourStartEvent;
}
} else {
startEvent = this._getEventBy(START_TYPE, !hasRequestByThem);
startEvent = this.getEventBy(START_TYPE, !hasRequestByThem);
}
if (startEvent) {
const fromRequestPhase = phase() === PHASE_REQUESTED &&
requestEvent.getSender() !== startEvent.getSender();
const fromUnsentPhase = phase() === PHASE_UNSENT &&
this.channel.constructor.canCreateRequest(START_TYPE);
const fromRequestPhase = phase() === PHASE_REQUESTED && requestEvent.getSender() !== startEvent.getSender();
const fromUnsentPhase = phase() === PHASE_UNSENT && this.channel.canCreateRequest(START_TYPE);
if (fromRequestPhase || phase() === PHASE_READY || fromUnsentPhase) {
transitions.push({ phase: PHASE_STARTED, event: startEvent });
}
}
const ourDoneEvent = this._eventsByUs.get(DONE_TYPE);
if (this._verifierHasFinished || (ourDoneEvent && phase() === PHASE_STARTED)) {
const ourDoneEvent = this.eventsByUs.get(DONE_TYPE);
if (this.verifierHasFinished || (ourDoneEvent && phase() === PHASE_STARTED)) {
transitions.push({ phase: PHASE_DONE });
}
const cancelEvent = this._getEventByEither(CANCEL_TYPE);
const cancelEvent = this.getEventByEither(CANCEL_TYPE);
if ((this._cancelled || cancelEvent) && phase() !== PHASE_DONE) {
transitions.push({ phase: PHASE_CANCELLED, event: cancelEvent });
return transitions;
@@ -549,14 +576,14 @@ export class VerificationRequest extends EventEmitter {
return transitions;
}
_transitionToPhase(transition) {
private transitionToPhase(transition: ITransition): void {
const { phase, event } = transition;
// get common methods
if (phase === PHASE_REQUESTED || phase === PHASE_READY) {
if (!this._wasSentByOwnDevice(event)) {
if (!this.wasSentByOwnDevice(event)) {
const content = event.getContent();
this._commonMethods =
content.methods.filter(m => this._verificationMethods.has(m));
this.commonMethods =
content.methods.filter(m => this.verificationMethods.has(m));
}
}
// detect if we're not a party in the request, and we should just observe
@@ -568,8 +595,8 @@ export class VerificationRequest extends EventEmitter {
) {
if (
this.channel.receiveStartFromOtherDevices &&
this._wasSentByOwnUser(event) &&
!this._wasSentByOwnDevice(event)
this.wasSentByOwnUser(event) &&
!this.wasSentByOwnDevice(event)
) {
this._observeOnly = true;
}
@@ -579,7 +606,7 @@ export class VerificationRequest extends EventEmitter {
if (phase === PHASE_STARTED) {
const { method } = event.getContent();
if (!this._verifier && !this.observeOnly) {
this._verifier = this._createVerifier(method, event);
this._verifier = this.createVerifier(method, event);
if (!this._verifier) {
this.cancel({
code: "m.unknown_method",
@@ -592,19 +619,19 @@ export class VerificationRequest extends EventEmitter {
}
}
_applyPhaseTransitions() {
const transitions = this._calculatePhaseTransitions();
private applyPhaseTransitions(): ITransition[] {
const transitions = this.calculatePhaseTransitions();
const existingIdx = transitions.findIndex(t => t.phase === this.phase);
// trim off phases we already went through, if any
const newTransitions = transitions.slice(existingIdx + 1);
// transition to all new phases
for (const transition of newTransitions) {
this._transitionToPhase(transition);
this.transitionToPhase(transition);
}
return newTransitions;
}
_isWinningStartRace(newEvent) {
private isWinningStartRace(newEvent: MatrixEvent): boolean {
if (newEvent.getType() !== START_TYPE) {
return false;
}
@@ -620,13 +647,13 @@ export class VerificationRequest extends EventEmitter {
const oldContent = oldEvent.getContent();
oldRaceIdentifier = oldContent && oldContent.from_device;
} else {
oldRaceIdentifier = this._client.getDeviceId();
oldRaceIdentifier = this.client.getDeviceId();
}
} else {
if (oldEvent) {
oldRaceIdentifier = oldEvent.getSender();
} else {
oldRaceIdentifier = this._client.getUserId();
oldRaceIdentifier = this.client.getUserId();
}
}
@@ -640,13 +667,13 @@ export class VerificationRequest extends EventEmitter {
return newRaceIdentifier < oldRaceIdentifier;
}
hasEventId(eventId) {
for (const event of this._eventsByUs.values()) {
public hasEventId(eventId: string): boolean {
for (const event of this.eventsByUs.values()) {
if (event.getId() === eventId) {
return true;
}
}
for (const event of this._eventsByThem.values()) {
for (const event of this.eventsByThem.values()) {
if (event.getId() === eventId) {
return true;
}
@@ -658,23 +685,29 @@ export class VerificationRequest extends EventEmitter {
* Changes the state of the request and verifier in response to a key verification event.
* @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel.
* @param {MatrixEvent} event the event to handle. Don't call getType() on it but use the `type` parameter instead.
* @param {bool} isLiveEvent whether this is an even received through sync or not
* @param {bool} isRemoteEcho whether this is the remote echo of an event sent by the same device
* @param {bool} isSentByUs whether this event is sent by a party that can accept and/or observe the request like one of our peers.
* @param {boolean} isLiveEvent whether this is an even received through sync or not
* @param {boolean} isRemoteEcho whether this is the remote echo of an event sent by the same device
* @param {boolean} isSentByUs whether this event is sent by a party that can accept and/or observe the request like one of our peers.
* For InRoomChannel this means any device for the syncing user. For ToDeviceChannel, just the syncing device.
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
* @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent.
*/
async handleEvent(type, event, isLiveEvent, isRemoteEcho, isSentByUs) {
public async handleEvent(
type: string,
event: MatrixEvent,
isLiveEvent: boolean,
isRemoteEcho: boolean,
isSentByUs: boolean,
): Promise<void> {
// if reached phase cancelled or done, ignore anything else that comes
if (this.done || this.cancelled) {
return;
}
const wasObserveOnly = this._observeOnly;
this._adjustObserveOnly(event, isLiveEvent);
this.adjustObserveOnly(event, isLiveEvent);
if (!this.observeOnly && !isRemoteEcho) {
if (await this._cancelOnError(type, event)) {
if (await this.cancelOnError(type, event)) {
return;
}
}
@@ -685,27 +718,26 @@ export class VerificationRequest extends EventEmitter {
// added here to prevent verification getting cancelled
// when the server duplicates an event (https://github.com/matrix-org/synapse/issues/3365)
const isDuplicateEvent = isSentByUs ?
this._eventsByUs.has(type) :
this._eventsByThem.has(type);
this.eventsByUs.has(type) :
this.eventsByThem.has(type);
if (isDuplicateEvent) {
return;
}
const oldPhase = this.phase;
this._addEvent(type, event, isSentByUs);
this.addEvent(type, event, isSentByUs);
// this will create if needed the verifier so needs to happen before calling it
const newTransitions = this._applyPhaseTransitions();
const newTransitions = this.applyPhaseTransitions();
try {
// only pass events from the other side to the verifier,
// no remote echos of our own events
if (this._verifier && !this.observeOnly) {
const newEventWinsRace = this._isWinningStartRace(event);
const newEventWinsRace = this.isWinningStartRace(event);
if (this._verifier.canSwitchStartEvent(event) && newEventWinsRace) {
this._verifier.switchStartEvent(event);
} else if (!isRemoteEcho) {
if (type === CANCEL_TYPE || (this._verifier.events
&& this._verifier.events.includes(type))) {
if (type === CANCEL_TYPE || this._verifier.events?.includes(type)) {
this._verifier.handleEvent(event);
}
}
@@ -722,16 +754,16 @@ export class VerificationRequest extends EventEmitter {
const shouldGenerateQrCode =
this.otherPartySupportsMethod(SCAN_QR_CODE_METHOD, true);
if (shouldGenerateQrCode) {
this._qrCodeData = await QRCodeData.create(this, this._client);
this._qrCodeData = await QRCodeData.create(this, this.client);
}
}
const lastTransition = newTransitions[newTransitions.length - 1];
const { phase } = lastTransition;
this._setupTimeout(phase);
this.setupTimeout(phase);
// set phase as last thing as this emits the "change" event
this._setPhase(phase);
this.setPhase(phase);
} else if (this._observeOnly !== wasObserveOnly) {
this.emit("change");
}
@@ -748,26 +780,26 @@ export class VerificationRequest extends EventEmitter {
}
}
_setupTimeout(phase) {
const shouldTimeout = !this._timeoutTimer && !this.observeOnly &&
private setupTimeout(phase: Phase): void {
const shouldTimeout = !this.timeoutTimer && !this.observeOnly &&
phase === PHASE_REQUESTED;
if (shouldTimeout) {
this._timeoutTimer = setTimeout(this._cancelOnTimeout, this.timeout);
this.timeoutTimer = setTimeout(this.cancelOnTimeout, this.timeout);
}
if (this._timeoutTimer) {
if (this.timeoutTimer) {
const shouldClear = phase === PHASE_STARTED ||
phase === PHASE_READY ||
phase === PHASE_DONE ||
phase === PHASE_CANCELLED;
if (shouldClear) {
clearTimeout(this._timeoutTimer);
this._timeoutTimer = null;
clearTimeout(this.timeoutTimer);
this.timeoutTimer = null;
}
}
}
_cancelOnTimeout = () => {
private cancelOnTimeout = () => {
try {
if (this.initiatedByMe) {
this.cancel({
@@ -785,10 +817,10 @@ export class VerificationRequest extends EventEmitter {
}
};
async _cancelOnError(type, event) {
private async cancelOnError(type: string, event: MatrixEvent): Promise<boolean> {
if (type === START_TYPE) {
const method = event.getContent().method;
if (!this._verificationMethods.has(method)) {
if (!this.verificationMethods.has(method)) {
await this.cancel(errorFromEvent(newUnknownMethodError()));
return true;
}
@@ -811,7 +843,7 @@ export class VerificationRequest extends EventEmitter {
return false;
}
_adjustObserveOnly(event, isLiveEvent) {
private adjustObserveOnly(event: MatrixEvent, isLiveEvent = false): void {
// don't send out events for historical requests
if (!isLiveEvent) {
this._observeOnly = true;
@@ -821,83 +853,80 @@ export class VerificationRequest extends EventEmitter {
}
}
_addEvent(type, event, isSentByUs) {
private addEvent(type: string, event: MatrixEvent, isSentByUs = false): void {
if (isSentByUs) {
this._eventsByUs.set(type, event);
this.eventsByUs.set(type, event);
} else {
this._eventsByThem.set(type, event);
this.eventsByThem.set(type, event);
}
// once we know the userId of the other party (from the .request event)
// see if any event by anyone else crept into this._eventsByThem
// see if any event by anyone else crept into this.eventsByThem
if (type === REQUEST_TYPE) {
for (const [type, event] of this._eventsByThem.entries()) {
for (const [type, event] of this.eventsByThem.entries()) {
if (event.getSender() !== this.otherUserId) {
this._eventsByThem.delete(type);
this.eventsByThem.delete(type);
}
}
// also remember when we received the request event
this._requestReceivedAt = Date.now();
this.requestReceivedAt = Date.now();
}
}
_createVerifier(method, startEvent = null, targetDevice = null) {
private createVerifier(
method: VerificationMethod,
startEvent: MatrixEvent = null,
targetDevice: ITargetDevice = null,
): VerificationBase {
if (!targetDevice) {
targetDevice = this.targetDevice;
}
const { userId, deviceId } = targetDevice;
const VerifierCtor = this._verificationMethods.get(method);
const VerifierCtor = this.verificationMethods.get(method);
if (!VerifierCtor) {
logger.warn("could not find verifier constructor for method", method);
return;
}
return new VerifierCtor(
this.channel,
this._client,
userId,
deviceId,
startEvent,
this,
);
return new VerifierCtor(this.channel, this.client, userId, deviceId, startEvent, this);
}
_wasSentByOwnUser(event) {
return event.getSender() === this._client.getUserId();
private wasSentByOwnUser(event: MatrixEvent): boolean {
return event.getSender() === this.client.getUserId();
}
// only for .request, .ready or .start
_wasSentByOwnDevice(event) {
if (!this._wasSentByOwnUser(event)) {
private wasSentByOwnDevice(event: MatrixEvent): boolean {
if (!this.wasSentByOwnUser(event)) {
return false;
}
const content = event.getContent();
if (!content || content.from_device !== this._client.getDeviceId()) {
if (!content || content.from_device !== this.client.getDeviceId()) {
return false;
}
return true;
}
onVerifierCancelled() {
public onVerifierCancelled(): void {
this._cancelled = true;
// move to cancelled phase
const newTransitions = this._applyPhaseTransitions();
const newTransitions = this.applyPhaseTransitions();
if (newTransitions.length) {
this._setPhase(newTransitions[newTransitions.length - 1].phase);
this.setPhase(newTransitions[newTransitions.length - 1].phase);
}
}
onVerifierFinished() {
public onVerifierFinished(): void {
this.channel.send("m.key.verification.done", {});
this._verifierHasFinished = true;
this.verifierHasFinished = true;
// move to .done phase
const newTransitions = this._applyPhaseTransitions();
const newTransitions = this.applyPhaseTransitions();
if (newTransitions.length) {
this._setPhase(newTransitions[newTransitions.length - 1].phase);
this.setPhase(newTransitions[newTransitions.length - 1].phase);
}
}
getEventFromOtherParty(type) {
return this._eventsByThem.get(type);
public getEventFromOtherParty(type: string): MatrixEvent {
return this.eventsByThem.get(type);
}
}
@@ -20,10 +20,90 @@ limitations under the License.
import * as utils from "./utils";
import { logger } from './logger';
import { MatrixClient } from "./client";
import { defer, IDeferred } from "./utils";
import { MatrixError } from "./http-api";
const EMAIL_STAGE_TYPE = "m.login.email.identity";
const MSISDN_STAGE_TYPE = "m.login.msisdn";
interface IFlow {
stages: AuthType[];
}
export interface IInputs {
emailAddress?: string;
phoneCountry?: string;
phoneNumber?: string;
}
export interface IStageStatus {
emailSid?: string;
errcode?: string;
error?: string;
}
export interface IAuthData {
session?: string;
completed?: string[];
flows?: IFlow[];
params?: Record<string, Record<string, any>>;
errcode?: string;
error?: MatrixError;
}
export enum AuthType {
Password = "m.login.password",
Recaptcha = "m.login.recaptcha",
Terms = "m.login.terms",
Email = "m.login.email.identity",
Msisdn = "m.login.msisdn",
Sso = "m.login.sso",
SsoUnstable = "org.matrix.login.sso",
Dummy = "m.login.dummy",
}
export interface IAuthDict {
// [key: string]: any;
type?: string;
// session?: string; // TODO
// TODO: Remove `user` once servers support proper UIA
// See https://github.com/vector-im/element-web/issues/10312
user?: string;
identifier?: any;
password?: string;
response?: string;
// TODO: Remove `threepid_creds` once servers support proper UIA
// See https://github.com/vector-im/element-web/issues/10312
// See https://github.com/matrix-org/matrix-doc/issues/2220
// eslint-disable-next-line camelcase
threepid_creds?: any;
threepidCreds?: any;
}
class NoAuthFlowFoundError extends Error {
public name = "NoAuthFlowFoundError";
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
constructor(m: string, public readonly required_stages: string[], public readonly flows: IFlow[]) {
super(m);
}
}
interface IOpts {
matrixClient: MatrixClient;
authData?: IAuthData;
inputs?: IInputs;
sessionId?: string;
clientSecret?: string;
emailSid?: string;
doRequest(auth: IAuthData, background: boolean): Promise<IAuthData>;
stateUpdated(nextStage: AuthType, status: IStageStatus): void;
requestEmailToken(email: string, secret: string, attempt: number, session: string): Promise<{ sid: string }>;
busyChanged?(busy: boolean): void;
startAuthStage?(nextStage: string): Promise<void>; // LEGACY
}
/**
* Abstracts the logic used to drive the interactive auth process.
*
@@ -50,12 +130,12 @@ const MSISDN_STAGE_TYPE = "m.login.msisdn";
* called with the new auth dict to submit the request. Also passes a
* second deprecated arg which is a flag set to true if this request
* is a background request. The busyChanged callback should be used
* instead of the backfround flag. Should return a promise which resolves
* instead of the background flag. Should return a promise which resolves
* to the successful response or rejects with a MatrixError.
*
* @param {function(bool): Promise} opts.busyChanged
* @param {function(boolean): Promise} opts.busyChanged
* called whenever the interactive auth logic becomes busy submitting
* information provided by the user or finsihes. After this has been
* information provided by the user or finishes. After this has been
* called with true the UI should indicate that a request is in progress
* until it is called again with false.
*
@@ -101,33 +181,41 @@ const MSISDN_STAGE_TYPE = "m.login.msisdn";
* attemptAuth promise.
*
*/
export function InteractiveAuth(opts) {
this._matrixClient = opts.matrixClient;
this._data = opts.authData || {};
this._requestCallback = opts.doRequest;
this._busyChangedCallback = opts.busyChanged;
// startAuthStage included for backwards compat
this._stateUpdatedCallback = opts.stateUpdated || opts.startAuthStage;
this._resolveFunc = null;
this._rejectFunc = null;
this._inputs = opts.inputs || {};
this._requestEmailTokenCallback = opts.requestEmailToken;
export class InteractiveAuth {
private readonly matrixClient: MatrixClient;
private readonly inputs: IInputs;
private readonly clientSecret: string;
private readonly requestCallback: IOpts["doRequest"];
private readonly busyChangedCallback?: IOpts["busyChanged"];
private readonly stateUpdatedCallback: IOpts["stateUpdated"];
private readonly requestEmailTokenCallback: IOpts["requestEmailToken"];
if (opts.sessionId) this._data.session = opts.sessionId;
this._clientSecret = opts.clientSecret || this._matrixClient.generateClientSecret();
this._emailSid = opts.emailSid;
if (this._emailSid === undefined) this._emailSid = null;
this._requestingEmailToken = false;
this._chosenFlow = null;
this._currentStage = null;
private data: IAuthData;
private emailSid?: string;
private requestingEmailToken = false;
private attemptAuthDeferred: IDeferred<IAuthData> = null;
private chosenFlow: IFlow = null;
private currentStage: string = null;
// if we are currently trying to submit an auth dict (which includes polling)
// the promise the will resolve/reject when it completes
this._submitPromise = null;
}
private submitPromise: Promise<void> = null;
constructor(opts: IOpts) {
this.matrixClient = opts.matrixClient;
this.data = opts.authData || {};
this.requestCallback = opts.doRequest;
this.busyChangedCallback = opts.busyChanged;
// startAuthStage included for backwards compat
this.stateUpdatedCallback = opts.stateUpdated || opts.startAuthStage;
this.requestEmailTokenCallback = opts.requestEmailToken;
this.inputs = opts.inputs || {};
if (opts.sessionId) this.data.session = opts.sessionId;
this.clientSecret = opts.clientSecret || this.matrixClient.generateClientSecret();
this.emailSid = opts.emailSid ?? null;
}
InteractiveAuth.prototype = {
/**
* begin the authentication process.
*
@@ -135,58 +223,57 @@ InteractiveAuth.prototype = {
* or rejects with the error on failure. Rejects with NoAuthFlowFoundError if
* no suitable authentication flow can be found
*/
attemptAuth: function() {
public attemptAuth(): Promise<IAuthData> {
// This promise will be quite long-lived and will resolve when the
// request is authenticated and completes successfully.
return new Promise((resolve, reject) => {
this._resolveFunc = resolve;
this._rejectFunc = reject;
this.attemptAuthDeferred = defer();
// pluck the promise out now, as doRequest may clear before we return
const promise = this.attemptAuthDeferred.promise;
const hasFlows = this._data && this._data.flows;
// if we have no flows, try a request to acquire the flows
if (!hasFlows) {
if (this._busyChangedCallback) this._busyChangedCallback(true);
// use the existing sessionid, if one is present.
let auth = null;
if (this._data.session) {
auth = {
session: this._data.session,
};
}
this._doRequest(auth).finally(() => {
if (this._busyChangedCallback) this._busyChangedCallback(false);
});
} else {
this._startNextAuthStage();
// if we have no flows, try a request to acquire the flows
if (!this.data?.flows) {
this.busyChangedCallback?.(true);
// use the existing sessionId, if one is present.
let auth = null;
if (this.data.session) {
auth = {
session: this.data.session,
};
}
});
},
this.doRequest(auth).finally(() => {
this.busyChangedCallback?.(false);
});
} else {
this.startNextAuthStage();
}
return promise;
}
/**
* Poll to check if the auth session or current stage has been
* completed out-of-band. If so, the attemptAuth promise will
* be resolved.
*/
poll: async function() {
if (!this._data.session) return;
public async poll(): Promise<void> {
if (!this.data.session) return;
// likewise don't poll if there is no auth session in progress
if (!this._resolveFunc) return;
if (!this.attemptAuthDeferred) return;
// if we currently have a request in flight, there's no point making
// another just to check what the status is
if (this._submitPromise) return;
if (this.submitPromise) return;
let authDict = {};
if (this._currentStage == EMAIL_STAGE_TYPE) {
let authDict: IAuthDict = {};
if (this.currentStage == EMAIL_STAGE_TYPE) {
// The email can be validated out-of-band, but we need to provide the
// creds so the HS can go & check it.
if (this._emailSid) {
const creds = {
sid: this._emailSid,
client_secret: this._clientSecret,
if (this.emailSid) {
const creds: Record<string, string> = {
sid: this.emailSid,
client_secret: this.clientSecret,
};
if (await this._matrixClient.doesServerRequireIdServerParam()) {
const idServerParsedUrl = new URL(this._matrixClient.getIdentityServerUrl());
if (await this.matrixClient.doesServerRequireIdServerParam()) {
const idServerParsedUrl = new URL(this.matrixClient.getIdentityServerUrl());
creds.id_server = idServerParsedUrl.host;
}
authDict = {
@@ -201,16 +288,16 @@ InteractiveAuth.prototype = {
}
this.submitAuthDict(authDict, true);
},
}
/**
* get the auth session ID
*
* @return {string} session id
*/
getSessionId: function() {
return this._data ? this._data.session : undefined;
},
public getSessionId(): string {
return this.data ? this.data.session : undefined;
}
/**
* get the client secret used for validation sessions
@@ -218,9 +305,9 @@ InteractiveAuth.prototype = {
*
* @return {string} client secret
*/
getClientSecret: function() {
return this._clientSecret;
},
public getClientSecret(): string {
return this.clientSecret;
}
/**
* get the server params for a given stage
@@ -228,17 +315,13 @@ InteractiveAuth.prototype = {
* @param {string} loginType login type for the stage
* @return {object?} any parameters from the server for this stage
*/
getStageParams: function(loginType) {
let params = {};
if (this._data && this._data.params) {
params = this._data.params;
}
return params[loginType];
},
public getStageParams(loginType: string): Record<string, any> {
return this.data.params?.[loginType];
}
getChosenFlow() {
return this._chosenFlow;
},
public getChosenFlow(): IFlow {
return this.chosenFlow;
}
/**
* submit a new auth dict and fire off the request. This will either
@@ -246,38 +329,38 @@ InteractiveAuth.prototype = {
* to be called for a new stage.
*
* @param {object} authData new auth dict to send to the server. Should
* include a `type` propterty denoting the login type, as well as any
* include a `type` property denoting the login type, as well as any
* other params for that stage.
* @param {bool} background If true, this request failing will not result
* @param {boolean} background If true, this request failing will not result
* in the attemptAuth promise being rejected. This can be set to true
* for requests that just poll to see if auth has been completed elsewhere.
*/
submitAuthDict: async function(authData, background) {
if (!this._resolveFunc) {
public async submitAuthDict(authData: IAuthDict, background = false): Promise<void> {
if (!this.attemptAuthDeferred) {
throw new Error("submitAuthDict() called before attemptAuth()");
}
if (!background && this._busyChangedCallback) {
this._busyChangedCallback(true);
if (!background) {
this.busyChangedCallback?.(true);
}
// if we're currently trying a request, wait for it to finish
// as otherwise we can get multiple 200 responses which can mean
// things like multiple logins for register requests.
// (but discard any expections as we only care when its done,
// (but discard any exceptions as we only care when its done,
// not whether it worked or not)
while (this._submitPromise) {
while (this.submitPromise) {
try {
await this._submitPromise;
await this.submitPromise;
} catch (e) {
}
}
// use the sessionid from the last request, if one is present.
let auth;
if (this._data.session) {
if (this.data.session) {
auth = {
session: this._data.session,
session: this.data.session,
};
utils.extend(auth, authData);
} else {
@@ -287,15 +370,15 @@ InteractiveAuth.prototype = {
try {
// NB. the 'background' flag is deprecated by the busyChanged
// callback and is here for backwards compat
this._submitPromise = this._doRequest(auth, background);
await this._submitPromise;
this.submitPromise = this.doRequest(auth, background);
await this.submitPromise;
} finally {
this._submitPromise = null;
if (!background && this._busyChangedCallback) {
this._busyChangedCallback(false);
this.submitPromise = null;
if (!background) {
this.busyChangedCallback?.(false);
}
}
},
}
/**
* Gets the sid for the email validation session
@@ -303,9 +386,9 @@ InteractiveAuth.prototype = {
*
* @returns {string} The sid of the email auth session
*/
getEmailSid: function() {
return this._emailSid;
},
public getEmailSid(): string {
return this.emailSid;
}
/**
* Sets the sid for the email validation session
@@ -315,9 +398,9 @@ InteractiveAuth.prototype = {
*
* @param {string} sid The sid for the email validation session
*/
setEmailSid: function(sid) {
this._emailSid = sid;
},
public setEmailSid(sid: string): void {
this.emailSid = sid;
}
/**
* Fire off a request, and either resolve the promise, or call
@@ -325,33 +408,29 @@ InteractiveAuth.prototype = {
*
* @private
* @param {object?} auth new auth dict, including session id
* @param {bool?} background If true, this request is a background poll, so it
* @param {boolean?} background If true, this request is a background poll, so it
* failing will not result in the attemptAuth promise being rejected.
* This can be set to true for requests that just poll to see if auth has
* been completed elsewhere.
*/
_doRequest: async function(auth, background) {
private async doRequest(auth: IAuthData, background = false): Promise<void> {
try {
const result = await this._requestCallback(auth, background);
this._resolveFunc(result);
this._resolveFunc = null;
this._rejectFunc = null;
const result = await this.requestCallback(auth, background);
this.attemptAuthDeferred.resolve(result);
this.attemptAuthDeferred = null;
} catch (error) {
// sometimes UI auth errors don't come with flows
const errorFlows = error.data ? error.data.flows : null;
const haveFlows = this._data.flows || Boolean(errorFlows);
const errorFlows = error.data?.flows ?? null;
const haveFlows = this.data.flows || Boolean(errorFlows);
if (error.httpStatus !== 401 || !error.data || !haveFlows) {
// doesn't look like an interactive-auth failure.
if (!background) {
this._rejectFunc(error);
this.attemptAuthDeferred?.reject(error);
} else {
// We ignore all failures here (even non-UI auth related ones)
// since we don't want to suddenly fail if the internet connection
// had a blip whilst we were polling
logger.log(
"Background poll request failed doing UI auth: ignoring",
error,
);
logger.log("Background poll request failed doing UI auth: ignoring", error);
}
}
// if the error didn't come with flows, completed flows or session ID,
@@ -360,37 +439,36 @@ InteractiveAuth.prototype = {
// has not yet been validated). This appears to be a Synapse bug, which
// we workaround here.
if (!error.data.flows && !error.data.completed && !error.data.session) {
error.data.flows = this._data.flows;
error.data.completed = this._data.completed;
error.data.session = this._data.session;
error.data.flows = this.data.flows;
error.data.completed = this.data.completed;
error.data.session = this.data.session;
}
this._data = error.data;
this.data = error.data;
try {
this._startNextAuthStage();
this.startNextAuthStage();
} catch (e) {
this._rejectFunc(e);
this._resolveFunc = null;
this._rejectFunc = null;
this.attemptAuthDeferred.reject(e);
this.attemptAuthDeferred = null;
}
if (
!this._emailSid &&
!this._requestingEmailToken &&
this._chosenFlow.stages.includes('m.login.email.identity')
!this.emailSid &&
!this.requestingEmailToken &&
this.chosenFlow.stages.includes(AuthType.Email)
) {
// If we've picked a flow with email auth, we send the email
// now because we want the request to fail as soon as possible
// if the email address is not valid (ie. already taken or not
// registered, depending on what the operation is).
this._requestingEmailToken = true;
this.requestingEmailToken = true;
try {
const requestTokenResult = await this._requestEmailTokenCallback(
this._inputs.emailAddress,
this._clientSecret,
const requestTokenResult = await this.requestEmailTokenCallback(
this.inputs.emailAddress,
this.clientSecret,
1, // TODO: Multiple send attempts?
this._data.session,
this.data.session,
);
this._emailSid = requestTokenResult.sid;
this.emailSid = requestTokenResult.sid;
// NB. promise is not resolved here - at some point, doRequest
// will be called again and if the user has jumped through all
// the hoops correctly, auth will be complete and the request
@@ -404,15 +482,14 @@ InteractiveAuth.prototype = {
// to do) or it could be a network failure. Either way, pass
// the failure up as the user can't complete auth if we can't
// send the email, for whatever reason.
this._rejectFunc(e);
this._resolveFunc = null;
this._rejectFunc = null;
this.attemptAuthDeferred.reject(e);
this.attemptAuthDeferred = null;
} finally {
this._requestingEmailToken = false;
this.requestingEmailToken = false;
}
}
}
},
}
/**
* Pick the next stage and call the callback
@@ -420,34 +497,34 @@ InteractiveAuth.prototype = {
* @private
* @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found
*/
_startNextAuthStage: function() {
const nextStage = this._chooseStage();
private startNextAuthStage(): void {
const nextStage = this.chooseStage();
if (!nextStage) {
throw new Error("No incomplete flows from the server");
}
this._currentStage = nextStage;
this.currentStage = nextStage;
if (nextStage === 'm.login.dummy') {
if (nextStage === AuthType.Dummy) {
this.submitAuthDict({
type: 'm.login.dummy',
});
return;
}
if (this._data && this._data.errcode || this._data.error) {
this._stateUpdatedCallback(nextStage, {
errcode: this._data.errcode || "",
error: this._data.error || "",
if (this.data && this.data.errcode || this.data.error) {
this.stateUpdatedCallback(nextStage, {
errcode: this.data.errcode || "",
error: this.data.error || "",
});
return;
}
const stageStatus = {};
const stageStatus: IStageStatus = {};
if (nextStage == EMAIL_STAGE_TYPE) {
stageStatus.emailSid = this._emailSid;
stageStatus.emailSid = this.emailSid;
}
this._stateUpdatedCallback(nextStage, stageStatus);
},
this.stateUpdatedCallback(nextStage, stageStatus);
}
/**
* Pick the next auth stage
@@ -456,15 +533,15 @@ InteractiveAuth.prototype = {
* @return {string?} login type
* @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found
*/
_chooseStage: function() {
if (this._chosenFlow === null) {
this._chosenFlow = this._chooseFlow();
private chooseStage(): AuthType {
if (this.chosenFlow === null) {
this.chosenFlow = this.chooseFlow();
}
logger.log("Active flow => %s", JSON.stringify(this._chosenFlow));
const nextStage = this._firstUncompletedStage(this._chosenFlow);
logger.log("Active flow => %s", JSON.stringify(this.chosenFlow));
const nextStage = this.firstUncompletedStage(this.chosenFlow);
logger.log("Next stage: %s", nextStage);
return nextStage;
},
}
/**
* Pick one of the flows from the returned list
@@ -472,7 +549,7 @@ InteractiveAuth.prototype = {
* be returned, otherwise, null will be returned.
*
* Only flows using all given inputs are chosen because it
* is likley to be surprising if the user provides a
* is likely to be surprising if the user provides a
* credential and it is not used. For example, for registration,
* this could result in the email not being used which would leave
* the account with no means to reset a password.
@@ -481,14 +558,14 @@ InteractiveAuth.prototype = {
* @return {object} flow
* @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found
*/
_chooseFlow: function() {
const flows = this._data.flows || [];
private chooseFlow(): IFlow {
const flows = this.data.flows || [];
// we've been given an email or we've already done an email part
const haveEmail = Boolean(this._inputs.emailAddress) || Boolean(this._emailSid);
const haveEmail = Boolean(this.inputs.emailAddress) || Boolean(this.emailSid);
const haveMsisdn = (
Boolean(this._inputs.phoneCountry) &&
Boolean(this._inputs.phoneNumber)
Boolean(this.inputs.phoneCountry) &&
Boolean(this.inputs.phoneNumber)
);
for (const flow of flows) {
@@ -506,16 +583,14 @@ InteractiveAuth.prototype = {
return flow;
}
}
const requiredStages: string[] = [];
if (haveEmail) requiredStages.push(EMAIL_STAGE_TYPE);
if (haveMsisdn) requiredStages.push(MSISDN_STAGE_TYPE);
// Throw an error with a fairly generic description, but with more
// information such that the app can give a better one if so desired.
const err = new Error("No appropriate authentication flow found");
err.name = 'NoAuthFlowFoundError';
err.required_stages = [];
if (haveEmail) err.required_stages.push(EMAIL_STAGE_TYPE);
if (haveMsisdn) err.required_stages.push(MSISDN_STAGE_TYPE);
err.available_flows = flows;
throw err;
},
throw new NoAuthFlowFoundError("No appropriate authentication flow found", requiredStages, flows);
}
/**
* Get the first uncompleted stage in the given flow
@@ -524,14 +599,13 @@ InteractiveAuth.prototype = {
* @param {object} flow
* @return {string} login type
*/
_firstUncompletedStage: function(flow) {
const completed = (this._data || {}).completed || [];
private firstUncompletedStage(flow: IFlow): AuthType {
const completed = this.data.completed || [];
for (let i = 0; i < flow.stages.length; ++i) {
const stageType = flow.stages[i];
if (completed.indexOf(stageType) === -1) {
return stageType;
}
}
},
};
}
}
+2 -2
View File
@@ -60,7 +60,7 @@ log.methodFactory = function(methodName, logLevel, loggerName) {
* Can be tailored down to specific use cases if needed.
*/
export const logger: PrefixedLogger = log.getLogger(DEFAULT_NAMESPACE);
logger.setLevel(log.levels.DEBUG);
logger.setLevel(log.levels.DEBUG, false);
export interface PrefixedLogger extends Logger {
withPrefix?: (prefix: string) => PrefixedLogger;
@@ -82,7 +82,7 @@ function getPrefixedLogger(prefix): PrefixedLogger {
// Only do this setup work the first time through, as loggers are saved by name.
extendLogger(prefixLogger);
prefixLogger.prefix = prefix;
prefixLogger.setLevel(log.levels.DEBUG);
prefixLogger.setLevel(log.levels.DEBUG, false);
}
return prefixLogger;
}
-2
View File
@@ -49,8 +49,6 @@ export * from "./content-repo";
export * as ContentHelpers from "./content-helpers";
export {
createNewMatrixCall,
setAudioInput as setMatrixCallAudioInput,
setVideoInput as setMatrixCallVideoInput,
} from "./webrtc/call";
// expose the underlying request object so different environments can use
+20
View File
@@ -77,6 +77,26 @@ export class MSC3089Branch {
}, this.id);
}
/**
* Gets whether or not a file is locked.
* @returns {boolean} True if locked, false otherwise.
*/
public isLocked(): boolean {
return this.indexEvent.getContent()['locked'] || false;
}
/**
* Sets a file as locked or unlocked.
* @param {boolean} locked True to lock the file, false otherwise.
* @returns {Promise<void>} Resolves when complete.
*/
public async setLocked(locked: boolean): Promise<void> {
await this.client.sendStateEvent(this.roomId, UNSTABLE_MSC3089_BRANCH.name, {
...this.indexEvent.getContent(),
locked: locked,
}, this.id);
}
/**
* Gets information about the file needed to download it.
* @returns {Promise<{info: IEncryptedFile, httpUrl: string}>} Information about the file.
+6 -2
View File
@@ -18,7 +18,7 @@ import { MatrixClient } from "../client";
import { EventType, IEncryptedFile, MsgType, UNSTABLE_MSC3089_BRANCH, UNSTABLE_MSC3089_LEAF } from "../@types/event";
import { Room } from "./room";
import { logger } from "../logger";
import { MatrixEvent } from "./event";
import { IContent, MatrixEvent } from "./event";
import {
averageBetweenStrings,
DEFAULT_ALPHABET,
@@ -451,11 +451,14 @@ export class MSC3089TreeSpace {
* @param {string} name The name of the file.
* @param {ArrayBuffer} encryptedContents The encrypted contents.
* @param {Partial<IEncryptedFile>} info The encrypted file information.
* @param {IContent} additionalContent Optional event content fields to include in the message.
* @returns {Promise<void>} Resolves when uploaded.
*/
public async createFile(
name: string,
encryptedContents: ArrayBuffer, info: Partial<IEncryptedFile>,
encryptedContents: ArrayBuffer,
info: Partial<IEncryptedFile>,
additionalContent?: IContent,
): Promise<void> {
const mxc = await this.client.uploadContent(new Blob([encryptedContents]), {
includeFilename: false,
@@ -464,6 +467,7 @@ export class MSC3089TreeSpace {
info.url = mxc;
const res = await this.client.sendMessage(this.roomId, {
...(additionalContent ?? {}),
msgtype: MsgType.File,
body: name,
url: mxc,
+73 -8
View File
@@ -24,10 +24,18 @@ import { EventEmitter } from 'events';
import { logger } from '../logger';
import { VerificationRequest } from "../crypto/verification/request/VerificationRequest";
import { EventType, MsgType, RelationType } from "../@types/event";
import {
EventType,
MsgType,
RelationType,
UNSTABLE_ELEMENT_REPLY_IN_THREAD,
} from "../@types/event";
import { Crypto } from "../crypto";
import { deepSortedObjectEntries } from "../utils";
import { RoomMember } from "./room-member";
import { Thread, ThreadEvent } from "./thread";
import { IActionsObject } from '../pushprocessor';
import { ReEmitter } from '../ReEmitter';
/**
* Enum for event statuses.
@@ -148,7 +156,7 @@ export interface IDecryptOptions {
}
export class MatrixEvent extends EventEmitter {
private pushActions: object = null;
private pushActions: IActionsObject = null;
private _replacingEvent: MatrixEvent = null;
private _localRedactionEvent: MatrixEvent = null;
private _isCancelled = false;
@@ -190,6 +198,12 @@ export class MatrixEvent extends EventEmitter {
*/
private txnId: string = null;
/**
* @experimental
* A reference to the thread this event belongs to
*/
private thread: Thread = null;
/* Set an approximate timestamp for the event relative the local clock.
* This will inherently be approximate because it doesn't take into account
* the time between the server putting the 'age' field on the event as it sent
@@ -211,6 +225,8 @@ export class MatrixEvent extends EventEmitter {
*/
public verificationRequest = null;
private readonly reEmitter: ReEmitter;
/**
* Construct a Matrix Event object
* @constructor
@@ -260,6 +276,7 @@ export class MatrixEvent extends EventEmitter {
this.txnId = event.txn_id || null;
this.localTimestamp = Date.now() - this.getAge();
this.reEmitter = new ReEmitter(this);
}
/**
@@ -380,6 +397,34 @@ export class MatrixEvent extends EventEmitter {
return this.event.content || {};
}
/**
* @experimental
* Get the event ID of the replied event
*/
public get replyEventId(): string {
const relations = this.getWireContent()["m.relates_to"];
return relations?.["m.in_reply_to"]?.["event_id"];
}
/**
* @experimental
* Determines whether a reply should be rendered in a thread
* or in the main room timeline
*/
public get replyInThread(): boolean {
const replyTo = this.getWireContent()
?.["m.relates_to"]
?.["m.in_reply_to"];
return (this.replyEventId
&& replyTo[UNSTABLE_ELEMENT_REPLY_IN_THREAD.name])
|| this.thread instanceof Thread;
}
public get parentEventId(): string {
return this.replyEventId
|| this.getWireContent()["m.relates_to"]?.event_id;
}
/**
* Get the previous event content JSON. This will only return something for
* state events which exist in the timeline.
@@ -947,7 +992,7 @@ export class MatrixEvent extends EventEmitter {
*
* @return {?Object} push actions
*/
public getPushActions(): object | null {
public getPushActions(): IActionsObject | null {
return this.pushActions;
}
@@ -956,7 +1001,7 @@ export class MatrixEvent extends EventEmitter {
*
* @param {Object} pushActions push actions
*/
public setPushActions(pushActions: object): void {
public setPushActions(pushActions: IActionsObject): void {
this.pushActions = pushActions;
}
@@ -1234,10 +1279,15 @@ export class MatrixEvent extends EventEmitter {
}
/**
* Summarise the event as JSON for debugging. If encrypted, include both the
* decrypted and encrypted view of the event. This is named `toJSON` for use
* with `JSON.stringify` which checks objects for functions named `toJSON`
* and will call them to customise the output if they are defined.
* Summarise the event as JSON. This is currently used by React SDK's view
* event source feature and Seshat's event indexing, so take care when
* adjusting the output here.
*
* If encrypted, include both the decrypted and encrypted view of the event.
*
* This is named `toJSON` for use with `JSON.stringify` which checks objects
* for functions named `toJSON` and will call them to customise the output
* if they are defined.
*
* @return {Object}
*/
@@ -1265,6 +1315,21 @@ export class MatrixEvent extends EventEmitter {
public getTxnId(): string | undefined {
return this.txnId;
}
/**
* @experimental
*/
public setThread(thread: Thread): void {
this.thread = thread;
this.reEmitter.reEmit(thread, [ThreadEvent.Ready, ThreadEvent.Update]);
}
/**
* @experimental
*/
public getThread(): Thread {
return this.thread;
}
}
/* REDACT_KEEP_KEYS gives the keys we keep when an event is redacted
+4
View File
@@ -17,6 +17,7 @@ limitations under the License.
/**
* @module models/group
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
import * as utils from "../utils";
@@ -34,6 +35,7 @@ import { EventEmitter } from "events";
* @prop {Object} inviter Infomation about the user who invited the logged in user
* to the group, if myMembership is 'invite'.
* @prop {string} inviter.userId The user ID of the inviter
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
export function Group(groupId) {
this.groupId = groupId;
@@ -76,6 +78,7 @@ Group.prototype.setInviter = function(inviter) {
* This means the 'name' and 'avatarUrl' properties.
* @event module:client~MatrixClient#"Group.profile"
* @param {Group} group The group whose profile was updated.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
* @example
* matrixClient.on("Group.profile", function(group){
* var name = group.name;
@@ -87,6 +90,7 @@ Group.prototype.setInviter = function(inviter) {
* the group is updated.
* @event module:client~MatrixClient#"Group.myMembership"
* @param {Group} group The group in which the user's membership changed
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
* @example
* matrixClient.on("Group.myMembership", function(group){
* var myMembership = group.myMembership;
+2 -2
View File
@@ -103,7 +103,7 @@ export class RoomMember extends EventEmitter {
* @fires module:client~MatrixClient#event:"RoomMember.name"
* @fires module:client~MatrixClient#event:"RoomMember.membership"
*/
public setMembershipEvent(event: MatrixEvent, roomState: RoomState): void {
public setMembershipEvent(event: MatrixEvent, roomState?: RoomState): void {
const displayName = event.getDirectionalContent().displayname;
if (event.getType() !== "m.room.member") {
@@ -322,7 +322,7 @@ export class RoomMember extends EventEmitter {
const MXID_PATTERN = /@.+:.+/;
const LTR_RTL_PATTERN = /[\u200E\u200F\u202A-\u202F]/;
function shouldDisambiguate(selfUserId: string, displayName: string, roomState: RoomState): boolean {
function shouldDisambiguate(selfUserId: string, displayName: string, roomState?: RoomState): boolean {
if (!displayName || displayName === selfUserId) return false;
// First check if the displayname is something we consider truthy
+24 -3
View File
@@ -26,6 +26,7 @@ import * as utils from "../utils";
import { EventType } from "../@types/event";
import { MatrixEvent } from "./event";
import { MatrixClient } from "../client";
import { GuestAccess, HistoryVisibility, IJoinRuleEventContent, JoinRule } from "../@types/partials";
// possible statuses for out-of-band member loading
enum OobStatus {
@@ -728,10 +729,30 @@ export class RoomState extends EventEmitter {
* Returns the join rule based on the m.room.join_rule state event, defaulting to `invite`.
* @returns {string} the join_rule applied to this room
*/
public getJoinRule(): string {
public getJoinRule(): JoinRule {
const joinRuleEvent = this.getStateEvents(EventType.RoomJoinRules, "");
const joinRuleContent = joinRuleEvent ? joinRuleEvent.getContent() : {};
return joinRuleContent["join_rule"] || "invite";
const joinRuleContent = joinRuleEvent?.getContent<IJoinRuleEventContent>() ?? {};
return joinRuleContent["join_rule"] || JoinRule.Invite;
}
/**
* Returns the history visibility based on the m.room.history_visibility state event, defaulting to `shared`.
* @returns {HistoryVisibility} the history_visibility applied to this room
*/
public getHistoryVisibility(): HistoryVisibility {
const historyVisibilityEvent = this.getStateEvents(EventType.RoomHistoryVisibility, "");
const historyVisibilityContent = historyVisibilityEvent?.getContent() ?? {};
return historyVisibilityContent["history_visibility"] || HistoryVisibility.Shared;
}
/**
* Returns the guest access based on the m.room.guest_access state event, defaulting to `shared`.
* @returns {GuestAccess} the guest_access applied to this room
*/
public getGuestAccess(): GuestAccess {
const guestAccessEvent = this.getStateEvents(EventType.RoomGuestAccess, "");
const guestAccessContent = guestAccessEvent?.getContent() ?? {};
return guestAccessContent["guest_access"] || GuestAccess.Forbidden;
}
private updateThirdPartyTokenCache(memberEvent: MatrixEvent): void {
+127 -4
View File
@@ -32,9 +32,10 @@ import { logger } from '../logger';
import { ReEmitter } from '../ReEmitter';
import { EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../@types/event";
import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
import { ResizeMethod } from "../@types/partials";
import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials";
import { Filter } from "../filter";
import { RoomState } from "./room-state";
import { Thread, ThreadEvent } from "./thread";
// These constants are used as sane defaults when the homeserver doesn't support
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
@@ -145,6 +146,11 @@ export class Room extends EventEmitter {
public oldState: RoomState;
public currentState: RoomState;
/**
* @experimental
*/
public threads = new Set<Thread>();
/**
* Construct a new Room.
*
@@ -857,13 +863,26 @@ export class Room extends EventEmitter {
}
/**
* Get an event which is stored in our unfiltered timeline set
* Get an event which is stored in our unfiltered timeline set or in a thread
*
* @param {string} eventId event ID to look for
* @return {?module:models/event.MatrixEvent} the given event, or undefined if unknown
*/
public findEventById(eventId: string): MatrixEvent | undefined {
return this.getUnfilteredTimelineSet().findEventById(eventId);
let event = this.getUnfilteredTimelineSet().findEventById(eventId);
if (event) {
return event;
} else {
const threads = this.getThreads();
for (let i = 0; i < threads.length; i++) {
const thread = threads[i];
event = thread.findEventById(eventId);
if (event) {
return event;
}
}
}
}
/**
@@ -1049,6 +1068,55 @@ export class Room extends EventEmitter {
);
}
/**
* @experimental
*/
public addThread(thread: Thread): Set<Thread> {
this.threads.add(thread);
if (!thread.ready) {
thread.once(ThreadEvent.Ready, this.dedupeThreads);
this.emit(ThreadEvent.Update, thread);
this.reEmitter.reEmit(thread, [ThreadEvent.Update, ThreadEvent.Ready]);
}
return this.threads;
}
/**
* @experimental
*/
public getThread(eventId: string): Thread {
return this.getThreads().find(thread => {
return thread.id === eventId;
});
}
/**
* @experimental
*/
public getThreads(): Thread[] {
return Array.from(this.threads.values());
}
/**
* Two threads starting from a different child event can end up
* with the same event root. This method ensures that the duplicates
* are removed
* @experimental
*/
private dedupeThreads = (readyThread): void => {
const deduped = Array.from(this.threads).reduce((dedupedThreads, thread) => {
if (dedupedThreads.has(thread.id)) {
dedupedThreads.get(thread.id).merge(thread);
} else {
dedupedThreads.set(thread.id, thread);
}
return dedupedThreads;
}, new Map<string, Thread>());
this.threads = new Set<Thread>(deduped.values());
};
/**
* Get a member from the current room state.
* @param {string} userId The user ID of the member.
@@ -1225,6 +1293,30 @@ export class Room extends EventEmitter {
}
}
/**
* Add an event to a thread's timeline. Will fire "Thread.update"
* @experimental
*/
public addThreadedEvent(event: MatrixEvent): void {
if (event.getUnsigned().transaction_id) {
const existingEvent = this.txnToEvent[event.getUnsigned().transaction_id];
if (existingEvent) {
// remote echo of an event we sent earlier
this.handleRemoteEcho(event, existingEvent);
}
}
let thread = this.findEventById(event.parentEventId)?.getThread();
if (thread) {
thread.addEvent(event);
} else {
thread = new Thread([event], this, this.client);
}
if (!this.threads.has(thread)) {
this.addThread(thread);
}
}
/**
* Add an event to the end of this room's live timelines. Will fire
* "Room.timeline".
@@ -1321,6 +1413,13 @@ export class Room extends EventEmitter {
* unique transaction id.
*/
public addPendingEvent(event: MatrixEvent, txnId: string): void {
// TODO: Enable "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
if (this.client?.supportsExperimentalThreads() && event.replyInThread) {
return;
}
if (event.status !== EventStatus.SENDING && event.status !== EventStatus.NOT_SENT) {
throw new Error("addPendingEvent called on an event with status " +
event.status);
@@ -1493,6 +1592,14 @@ export class Room extends EventEmitter {
oldEventId, oldStatus);
}
public findThreadByEventId(eventId: string): Thread {
for (const thread of this.threads) {
if (thread.has(eventId)) {
return thread;
}
}
}
/**
* Update the status / event id on a pending event, to reflect its transmission
* progress.
@@ -1973,10 +2080,26 @@ export class Room extends EventEmitter {
* Returns the join rule based on the m.room.join_rule state event, defaulting to `invite`.
* @returns {string} the join_rule applied to this room
*/
public getJoinRule(): string {
public getJoinRule(): JoinRule {
return this.currentState.getJoinRule();
}
/**
* Returns the history visibility based on the m.room.history_visibility state event, defaulting to `shared`.
* @returns {HistoryVisibility} the history_visibility applied to this room
*/
public getHistoryVisibility(): HistoryVisibility {
return this.currentState.getHistoryVisibility();
}
/**
* Returns the history visibility based on the m.room.history_visibility state event, defaulting to `shared`.
* @returns {HistoryVisibility} the history_visibility applied to this room
*/
public getGuestAccess(): GuestAccess {
return this.currentState.getGuestAccess();
}
/**
* Returns the type of the room from the `m.room.create` event content or undefined if none is set
* @returns {?string} the type of the room. Currently only RoomType.Space is known.
+1 -2
View File
@@ -31,7 +31,7 @@ export class SearchResult {
* @return {SearchResult}
*/
static fromJson(jsonObj: ISearchResult, eventMapper: EventMapper): SearchResult {
public static fromJson(jsonObj: ISearchResult, eventMapper: EventMapper): SearchResult {
const jsonContext = jsonObj.context || {} as IResultContext;
const eventsBefore = jsonContext.events_before || [];
const eventsAfter = jsonContext.events_after || [];
@@ -57,4 +57,3 @@ export class SearchResult {
*/
constructor(public readonly rank: number, public readonly context: EventContext) {}
}
+222
View File
@@ -0,0 +1,222 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { EventEmitter } from "events";
import { MatrixClient } from "../matrix";
import { MatrixEvent } from "./event";
import { EventTimelineSet } from './event-timeline-set';
import { Room } from './room';
export enum ThreadEvent {
Ready = "Thread.ready",
Update = "Thread.update"
}
/**
* @experimental
*/
export class Thread extends EventEmitter {
/**
* A reference to the event ID at the top of the thread
*/
private root: string;
/**
* A reference to all the events ID at the bottom of the threads
*/
public readonly tail = new Set<string>();
public readonly timelineSet: EventTimelineSet;
constructor(
events: MatrixEvent[] = [],
public readonly room: Room,
public readonly client: MatrixClient,
) {
super();
this.timelineSet = new EventTimelineSet(room, {
unstableClientRelationAggregation: true,
timelineSupport: true,
});
events.forEach(event => this.addEvent(event));
}
/**
* Add an event to the thread and updates
* the tail/root references if needed
* Will fire "Thread.update"
* @param event The event to add
*/
public async addEvent(event: MatrixEvent): Promise<void> {
if (this.timelineSet.findEventById(event.getId()) || event.status !== null) {
return;
}
if (this.tail.has(event.replyEventId)) {
this.tail.delete(event.replyEventId);
}
this.tail.add(event.getId());
if (!event.replyEventId || !this.timelineSet.findEventById(event.replyEventId)) {
this.root = event.getId();
}
event.setThread(this);
this.timelineSet.addLiveEvent(event);
if (this.ready) {
this.client.decryptEventIfNeeded(event, {});
}
this.emit(ThreadEvent.Update, this);
}
/**
* Completes the reply chain with all events
* missing from the current sync data
* Will fire "Thread.ready"
*/
public async fetchReplyChain(): Promise<void> {
if (!this.ready) {
let mxEvent = this.room.findEventById(this.rootEvent.replyEventId);
if (!mxEvent) {
mxEvent = await this.fetchEventById(
this.rootEvent.getRoomId(),
this.rootEvent.replyEventId,
);
}
this.addEvent(mxEvent);
if (mxEvent.replyEventId) {
await this.fetchReplyChain();
} else {
await this.decryptEvents();
this.emit(ThreadEvent.Ready, this);
}
}
}
private async decryptEvents(): Promise<void> {
await Promise.allSettled(
Array.from(this.timelineSet.getLiveTimeline().getEvents()).map(event => {
return this.client.decryptEventIfNeeded(event, {});
}),
);
}
/**
* Fetches an event over the network
*/
private async fetchEventById(roomId: string, eventId: string): Promise<MatrixEvent> {
const response = await this.client.http.authedRequest(
undefined,
"GET",
`/rooms/${roomId}/event/${eventId}`,
);
return new MatrixEvent(response);
}
/**
* Finds an event by ID in the current thread
*/
public findEventById(eventId: string) {
return this.timelineSet.findEventById(eventId);
}
/**
* Determines thread's ready status
*/
public get ready(): boolean {
return this.rootEvent.replyEventId === undefined;
}
/**
* The thread ID, which is the same as the root event ID
*/
public get id(): string {
return this.root;
}
/**
* The thread root event
*/
public get rootEvent(): MatrixEvent {
return this.findEventById(this.root);
}
public get roomId(): string {
return this.rootEvent.getRoomId();
}
/**
* The number of messages in the thread
*/
public get length(): number {
return this.events.length;
}
/**
* A set of mxid participating to the thread
*/
public get participants(): Set<string> {
const participants = new Set<string>();
this.events.forEach(event => {
participants.add(event.getSender());
});
return participants;
}
/**
* A getter for the last event added to the thread
*/
public get replyToEvent(): MatrixEvent {
const events = this.events;
return events[events.length -1];
}
public get events(): MatrixEvent[] {
return this.timelineSet.getLiveTimeline().getEvents();
}
public merge(thread: Thread): void {
thread.events.forEach(event => {
this.addEvent(event);
});
this.events.forEach(event => event.setThread(this));
}
public has(eventId: string): boolean {
return this.timelineSet.findEventById(eventId) instanceof MatrixEvent;
}
public on(event: ThreadEvent, listener: (...args: any[]) => void): this {
super.on(event, listener);
return this;
}
public once(event: ThreadEvent, listener: (...args: any[]) => void): this {
super.once(event, listener);
return this;
}
public off(event: ThreadEvent, listener: (...args: any[]) => void): this {
super.off(event, listener);
return this;
}
public addListener(event: ThreadEvent, listener: (...args: any[]) => void): this {
super.addListener(event, listener);
return this;
}
public removeListener(event: ThreadEvent, listener: (...args: any[]) => void): this {
super.removeListener(event, listener);
return this;
}
}
+189 -155
View File
@@ -1,6 +1,5 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Copyright 2015 - 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.
@@ -17,12 +16,36 @@ limitations under the License.
import { escapeRegExp, globToRegexp, isNullOrUndefined } from "./utils";
import { logger } from './logger';
import { MatrixClient } from "./client";
import { MatrixEvent } from "./models/event";
import {
ConditionKind,
IAnnotatedPushRule,
IContainsDisplayNameCondition,
IEventMatchCondition,
IPushRule,
IPushRules,
IRoomMemberCountCondition,
ISenderNotificationPermissionCondition,
PushRuleAction,
PushRuleActionName,
PushRuleCondition,
PushRuleKind,
PushRuleSet,
TweakName,
} from "./@types/PushRules";
/**
* @module pushprocessor
*/
const RULEKINDS_IN_ORDER = ['override', 'content', 'room', 'sender', 'underride'];
const RULEKINDS_IN_ORDER = [
PushRuleKind.Override,
PushRuleKind.ContentSpecific,
PushRuleKind.RoomSpecific,
PushRuleKind.SenderSpecific,
PushRuleKind.Underride,
];
// The default override rules to apply to the push rules that arrive from the server.
// We do this for two reasons:
@@ -31,7 +54,7 @@ const RULEKINDS_IN_ORDER = ['override', 'content', 'room', 'sender', 'underride'
// more details.
// 2. We often want to start using push rules ahead of the server supporting them,
// and so we can put them here.
const DEFAULT_OVERRIDE_RULES = [
const DEFAULT_OVERRIDE_RULES: IPushRule[] = [
{
// For homeservers which don't support MSC1930 yet
rule_id: ".m.rule.tombstone",
@@ -39,20 +62,20 @@ const DEFAULT_OVERRIDE_RULES = [
enabled: true,
conditions: [
{
kind: "event_match",
kind: ConditionKind.EventMatch,
key: "type",
pattern: "m.room.tombstone",
},
{
kind: "event_match",
kind: ConditionKind.EventMatch,
key: "state_key",
pattern: "",
},
],
actions: [
"notify",
PushRuleActionName.Notify,
{
set_tweak: "highlight",
set_tweak: TweakName.Highlight,
value: true,
},
],
@@ -64,31 +87,97 @@ const DEFAULT_OVERRIDE_RULES = [
enabled: true,
conditions: [
{
kind: "event_match",
kind: ConditionKind.EventMatch,
key: "type",
pattern: "m.reaction",
},
],
actions: [
"dont_notify",
PushRuleActionName.DontNotify,
],
},
];
/**
* Construct a Push Processor.
* @constructor
* @param {Object} client The Matrix client object to use
*/
export function PushProcessor(client) {
const cachedGlobToRegex = {
// $glob: RegExp,
};
export interface IActionsObject {
notify: boolean;
tweaks: Partial<Record<TweakName, any>>;
}
const matchingRuleFromKindSet = (ev, kindset) => {
for (let ruleKindIndex = 0;
ruleKindIndex < RULEKINDS_IN_ORDER.length;
++ruleKindIndex) {
export class PushProcessor {
/**
* Construct a Push Processor.
* @constructor
* @param {Object} client The Matrix client object to use
*/
constructor(private readonly client: MatrixClient) {}
/**
* Convert a list of actions into a object with the actions as keys and their values
* eg. [ 'notify', { set_tweak: 'sound', value: 'default' } ]
* becomes { notify: true, tweaks: { sound: 'default' } }
* @param {array} actionList The actions list
*
* @return {object} A object with key 'notify' (true or false) and an object of actions
*/
public static actionListToActionsObject(actionList: PushRuleAction[]): IActionsObject {
const actionObj: IActionsObject = { notify: false, tweaks: {} };
for (let i = 0; i < actionList.length; ++i) {
const action = actionList[i];
if (action === PushRuleActionName.Notify) {
actionObj.notify = true;
} else if (typeof action === 'object') {
if (action.value === undefined) {
action.value = true;
}
actionObj.tweaks[action.set_tweak] = action.value;
}
}
return actionObj;
}
/**
* Rewrites conditions on a client's push rules to match the defaults
* where applicable. Useful for upgrading push rules to more strict
* conditions when the server is falling behind on defaults.
* @param {object} incomingRules The client's existing push rules
* @returns {object} The rewritten rules
*/
public static rewriteDefaultRules(incomingRules: IPushRules): IPushRules {
let newRules: IPushRules = JSON.parse(JSON.stringify(incomingRules)); // deep clone
// These lines are mostly to make the tests happy. We shouldn't run into these
// properties missing in practice.
if (!newRules) newRules = {} as IPushRules;
if (!newRules.global) newRules.global = {} as PushRuleSet;
if (!newRules.global.override) newRules.global.override = [];
// Merge the client-level defaults with the ones from the server
const globalOverrides = newRules.global.override;
for (const override of DEFAULT_OVERRIDE_RULES) {
const existingRule = globalOverrides
.find((r) => r.rule_id === override.rule_id);
if (existingRule) {
// Copy over the actions, default, and conditions. Don't touch the user's
// preference.
existingRule.default = override.default;
existingRule.conditions = override.conditions;
existingRule.actions = override.actions;
} else {
// Add the rule
const ruleId = override.rule_id;
logger.warn(`Adding default global override for ${ruleId}`);
globalOverrides.push(override);
}
}
return newRules;
}
private static cachedGlobToRegex: Record<string, RegExp> = {}; // $glob: RegExp
private matchingRuleFromKindSet(ev: MatrixEvent, kindset: PushRuleSet): IAnnotatedPushRule {
for (let ruleKindIndex = 0; ruleKindIndex < RULEKINDS_IN_ORDER.length; ++ruleKindIndex) {
const kind = RULEKINDS_IN_ORDER[ruleKindIndex];
const ruleset = kindset[kind];
if (!ruleset) {
@@ -101,89 +190,96 @@ export function PushProcessor(client) {
continue;
}
const rawrule = templateRuleToRaw(kind, rule);
const rawrule = this.templateRuleToRaw(kind, rule);
if (!rawrule) {
continue;
}
if (this.ruleMatchesEvent(rawrule, ev)) {
rule.kind = kind;
return rule;
return {
...rule,
kind,
};
}
}
}
return null;
};
}
const templateRuleToRaw = function(kind, tprule) {
private templateRuleToRaw(kind: PushRuleKind, tprule: any): any {
const rawrule = {
'rule_id': tprule.rule_id,
'actions': tprule.actions,
'conditions': [],
};
switch (kind) {
case 'underride':
case 'override':
case PushRuleKind.Underride:
case PushRuleKind.Override:
rawrule.conditions = tprule.conditions;
break;
case 'room':
case PushRuleKind.RoomSpecific:
if (!tprule.rule_id) {
return null;
}
rawrule.conditions.push({
'kind': 'event_match',
'kind': ConditionKind.EventMatch,
'key': 'room_id',
'value': tprule.rule_id,
});
break;
case 'sender':
case PushRuleKind.SenderSpecific:
if (!tprule.rule_id) {
return null;
}
rawrule.conditions.push({
'kind': 'event_match',
'kind': ConditionKind.EventMatch,
'key': 'user_id',
'value': tprule.rule_id,
});
break;
case 'content':
case PushRuleKind.ContentSpecific:
if (!tprule.pattern) {
return null;
}
rawrule.conditions.push({
'kind': 'event_match',
'kind': ConditionKind.EventMatch,
'key': 'content.body',
'pattern': tprule.pattern,
});
break;
}
return rawrule;
};
}
const eventFulfillsCondition = function(cond, ev) {
const condition_functions = {
"event_match": eventFulfillsEventMatchCondition,
"contains_display_name": eventFulfillsDisplayNameCondition,
"room_member_count": eventFulfillsRoomMemberCountCondition,
"sender_notification_permission": eventFulfillsSenderNotifPermCondition,
};
if (condition_functions[cond.kind]) {
return condition_functions[cond.kind](cond, ev);
private eventFulfillsCondition(cond: PushRuleCondition, ev: MatrixEvent): boolean {
switch (cond.kind) {
case ConditionKind.EventMatch:
return this.eventFulfillsEventMatchCondition(cond, ev);
case ConditionKind.ContainsDisplayName:
return this.eventFulfillsDisplayNameCondition(cond, ev);
case ConditionKind.RoomMemberCount:
return this.eventFulfillsRoomMemberCountCondition(cond, ev);
case ConditionKind.SenderNotificationPermission:
return this.eventFulfillsSenderNotifPermCondition(cond, ev);
}
// unknown conditions: we previously matched all unknown conditions,
// but given that rules can be added to the base rules on a server,
// it's probably better to not match unknown conditions.
return false;
};
}
const eventFulfillsSenderNotifPermCondition = function(cond, ev) {
private eventFulfillsSenderNotifPermCondition(
cond: ISenderNotificationPermissionCondition,
ev: MatrixEvent,
): boolean {
const notifLevelKey = cond['key'];
if (!notifLevelKey) {
return false;
}
const room = client.getRoom(ev.getRoomId());
if (!room || !room.currentState) {
const room = this.client.getRoom(ev.getRoomId());
if (!room?.currentState) {
return false;
}
@@ -191,14 +287,14 @@ export function PushProcessor(client) {
// the point the event is in the DAG. Unfortunately the js-sdk does not store
// this.
return room.currentState.mayTriggerNotifOfType(notifLevelKey, ev.getSender());
};
}
const eventFulfillsRoomMemberCountCondition = function(cond, ev) {
private eventFulfillsRoomMemberCountCondition(cond: IRoomMemberCountCondition, ev: MatrixEvent): boolean {
if (!cond.is) {
return false;
}
const room = client.getRoom(ev.getRoomId());
const room = this.client.getRoom(ev.getRoomId());
if (!room || !room.currentState || !room.currentState.members) {
return false;
}
@@ -229,9 +325,9 @@ export function PushProcessor(client) {
default:
return false;
}
};
}
const eventFulfillsDisplayNameCondition = function(cond, ev) {
private eventFulfillsDisplayNameCondition(cond: IContainsDisplayNameCondition, ev: MatrixEvent): boolean {
let content = ev.getContent();
if (ev.isEncrypted() && ev.getClearContent()) {
content = ev.getClearContent();
@@ -240,26 +336,26 @@ export function PushProcessor(client) {
return false;
}
const room = client.getRoom(ev.getRoomId());
const room = this.client.getRoom(ev.getRoomId());
if (!room || !room.currentState || !room.currentState.members ||
!room.currentState.getMember(client.credentials.userId)) {
!room.currentState.getMember(this.client.credentials.userId)) {
return false;
}
const displayName = room.currentState.getMember(client.credentials.userId).name;
const displayName = room.currentState.getMember(this.client.credentials.userId).name;
// N.B. we can't use \b as it chokes on unicode. however \W seems to be okay
// as shorthand for [^0-9A-Za-z_].
const pat = new RegExp("(^|\\W)" + escapeRegExp(displayName) + "(\\W|$)", 'i');
return content.body.search(pat) > -1;
};
}
const eventFulfillsEventMatchCondition = function(cond, ev) {
private eventFulfillsEventMatchCondition(cond: IEventMatchCondition, ev: MatrixEvent): boolean {
if (!cond.key) {
return false;
}
const val = valueForDottedKey(cond.key, ev);
const val = this.valueForDottedKey(cond.key, ev);
if (typeof val !== 'string') {
return false;
}
@@ -275,26 +371,26 @@ export function PushProcessor(client) {
let regex;
if (cond.key == 'content.body') {
regex = createCachedRegex('(^|\\W)', cond.pattern, '(\\W|$)');
regex = this.createCachedRegex('(^|\\W)', cond.pattern, '(\\W|$)');
} else {
regex = createCachedRegex('^', cond.pattern, '$');
regex = this.createCachedRegex('^', cond.pattern, '$');
}
return !!val.match(regex);
};
}
const createCachedRegex = function(prefix, glob, suffix) {
if (cachedGlobToRegex[glob]) {
return cachedGlobToRegex[glob];
private createCachedRegex(prefix: string, glob: string, suffix: string): RegExp {
if (PushProcessor.cachedGlobToRegex[glob]) {
return PushProcessor.cachedGlobToRegex[glob];
}
cachedGlobToRegex[glob] = new RegExp(
PushProcessor.cachedGlobToRegex[glob] = new RegExp(
prefix + globToRegexp(glob) + suffix,
'i', // Case insensitive
);
return cachedGlobToRegex[glob];
};
return PushProcessor.cachedGlobToRegex[glob];
}
const valueForDottedKey = function(key, ev) {
private valueForDottedKey(key: string, ev: MatrixEvent): any {
const parts = key.split('.');
let val;
@@ -319,23 +415,23 @@ export function PushProcessor(client) {
val = val[thisPart];
}
return val;
};
}
const matchingRuleForEventWithRulesets = function(ev, rulesets) {
private matchingRuleForEventWithRulesets(ev: MatrixEvent, rulesets): IAnnotatedPushRule {
if (!rulesets) {
return null;
}
if (ev.getSender() === client.credentials.userId) {
if (ev.getSender() === this.client.credentials.userId) {
return null;
}
return matchingRuleFromKindSet(ev, rulesets.global);
};
return this.matchingRuleFromKindSet(ev, rulesets.global);
}
const pushActionsForEventAndRulesets = function(ev, rulesets) {
const rule = matchingRuleForEventWithRulesets(ev, rulesets);
private pushActionsForEventAndRulesets(ev: MatrixEvent, rulesets): IActionsObject {
const rule = this.matchingRuleForEventWithRulesets(ev, rulesets);
if (!rule) {
return {};
return {} as IActionsObject;
}
const actionObj = PushProcessor.actionListToActionsObject(rule.actions);
@@ -344,21 +440,22 @@ export function PushProcessor(client) {
if (actionObj.tweaks.highlight === undefined) {
// if it isn't specified, highlight if it's a content
// rule but otherwise not
actionObj.tweaks.highlight = (rule.kind == 'content');
actionObj.tweaks.highlight = (rule.kind == PushRuleKind.ContentSpecific);
}
return actionObj;
};
}
this.ruleMatchesEvent = function(rule, ev) {
public ruleMatchesEvent(rule: IPushRule, ev: MatrixEvent): boolean {
let ret = true;
for (let i = 0; i < rule.conditions.length; ++i) {
const cond = rule.conditions[i];
ret &= eventFulfillsCondition(cond, ev);
// @ts-ignore
ret &= this.eventFulfillsCondition(cond, ev);
}
//console.log("Rule "+rule.rule_id+(ret ? " matches" : " doesn't match"));
return ret;
};
}
/**
* Get the user's push actions for the given event
@@ -367,9 +464,9 @@ export function PushProcessor(client) {
*
* @return {PushAction}
*/
this.actionsForEvent = function(ev) {
return pushActionsForEventAndRulesets(ev, client.pushRules);
};
public actionsForEvent(ev: MatrixEvent): IActionsObject {
return this.pushActionsForEventAndRulesets(ev, this.client.pushRules);
}
/**
* Get one of the users push rules by its ID
@@ -377,85 +474,22 @@ export function PushProcessor(client) {
* @param {string} ruleId The ID of the rule to search for
* @return {object} The push rule, or null if no such rule was found
*/
this.getPushRuleById = function(ruleId) {
public getPushRuleById(ruleId: string): IPushRule {
for (const scope of ['global']) {
if (client.pushRules[scope] === undefined) continue;
if (this.client.pushRules[scope] === undefined) continue;
for (const kind of RULEKINDS_IN_ORDER) {
if (client.pushRules[scope][kind] === undefined) continue;
if (this.client.pushRules[scope][kind] === undefined) continue;
for (const rule of client.pushRules[scope][kind]) {
for (const rule of this.client.pushRules[scope][kind]) {
if (rule.rule_id === ruleId) return rule;
}
}
}
return null;
};
}
}
/**
* Convert a list of actions into a object with the actions as keys and their values
* eg. [ 'notify', { set_tweak: 'sound', value: 'default' } ]
* becomes { notify: true, tweaks: { sound: 'default' } }
* @param {array} actionlist The actions list
*
* @return {object} A object with key 'notify' (true or false) and an object of actions
*/
PushProcessor.actionListToActionsObject = function(actionlist) {
const actionobj = { 'notify': false, 'tweaks': {} };
for (let i = 0; i < actionlist.length; ++i) {
const action = actionlist[i];
if (action === 'notify') {
actionobj.notify = true;
} else if (typeof action === 'object') {
if (action.value === undefined) {
action.value = true;
}
actionobj.tweaks[action.set_tweak] = action.value;
}
}
return actionobj;
};
/**
* Rewrites conditions on a client's push rules to match the defaults
* where applicable. Useful for upgrading push rules to more strict
* conditions when the server is falling behind on defaults.
* @param {object} incomingRules The client's existing push rules
* @returns {object} The rewritten rules
*/
PushProcessor.rewriteDefaultRules = function(incomingRules) {
let newRules = JSON.parse(JSON.stringify(incomingRules)); // deep clone
// These lines are mostly to make the tests happy. We shouldn't run into these
// properties missing in practice.
if (!newRules) newRules = {};
if (!newRules.global) newRules.global = {};
if (!newRules.global.override) newRules.global.override = [];
// Merge the client-level defaults with the ones from the server
const globalOverrides = newRules.global.override;
for (const override of DEFAULT_OVERRIDE_RULES) {
const existingRule = globalOverrides
.find((r) => r.rule_id === override.rule_id);
if (existingRule) {
// Copy over the actions, default, and conditions. Don't touch the user's
// preference.
existingRule.default = override.default;
existingRule.conditions = override.conditions;
existingRule.actions = override.actions;
} else {
// Add the rule
const ruleId = override.rule_id;
logger.warn(`Adding default global override for ${ruleId}`);
globalOverrides.push(override);
}
}
return newRules;
};
/**
* @typedef {Object} PushAction
* @type {Object}
@@ -31,17 +31,22 @@ import { logger } from './logger';
const TIMER_CHECK_PERIOD_MS = 1000;
// counter, for making up ids to return from setTimeout
let _count = 0;
let count = 0;
// the key for our callback with the real global.setTimeout
let _realCallbackKey;
let realCallbackKey: NodeJS.Timeout | number;
// a sorted list of the callbacks to be run.
// each is an object with keys [runAt, func, params, key].
const _callbackList = [];
const callbackList: {
runAt: number;
func: (...params: any[]) => void;
params: any[];
key: number;
}[] = [];
// var debuglog = logger.log.bind(logger);
const debuglog = function() {};
const debuglog = function(...params: any[]) {};
/**
* Replace the function used by this module to get the current time.
@@ -52,10 +57,10 @@ const debuglog = function() {};
*
* @internal
*/
export function setNow(f) {
_now = f || Date.now;
export function setNow(f: () => number): void {
now = f || Date.now;
}
let _now = Date.now;
let now = Date.now;
/**
* reimplementation of window.setTimeout, which will call the callback if
@@ -67,17 +72,16 @@ let _now = Date.now;
* @return {Number} an identifier for this callback, which may be passed into
* clearTimeout later.
*/
export function setTimeout(func, delayMs) {
export function setTimeout(func: (...params: any[]) => void, delayMs: number, ...params: any[]): number {
delayMs = delayMs || 0;
if (delayMs < 0) {
delayMs = 0;
}
const params = Array.prototype.slice.call(arguments, 2);
const runAt = _now() + delayMs;
const key = _count++;
const runAt = now() + delayMs;
const key = count++;
debuglog("setTimeout: scheduling cb", key, "at", runAt,
"(delay", delayMs, ")");
"(delay", delayMs, ")");
const data = {
runAt: runAt,
func: func,
@@ -87,13 +91,13 @@ export function setTimeout(func, delayMs) {
// figure out where it goes in the list
const idx = binarySearch(
_callbackList, function(el) {
callbackList, function(el) {
return el.runAt - runAt;
},
);
_callbackList.splice(idx, 0, data);
_scheduleRealCallback();
callbackList.splice(idx, 0, data);
scheduleRealCallback();
return key;
}
@@ -103,68 +107,69 @@ export function setTimeout(func, delayMs) {
*
* @param {Number} key result from an earlier setTimeout call
*/
export function clearTimeout(key) {
if (_callbackList.length === 0) {
export function clearTimeout(key: number): void {
if (callbackList.length === 0) {
return;
}
// remove the element from the list
let i;
for (i = 0; i < _callbackList.length; i++) {
const cb = _callbackList[i];
for (i = 0; i < callbackList.length; i++) {
const cb = callbackList[i];
if (cb.key == key) {
_callbackList.splice(i, 1);
callbackList.splice(i, 1);
break;
}
}
// iff it was the first one in the list, reschedule our callback.
if (i === 0) {
_scheduleRealCallback();
scheduleRealCallback();
}
}
// use the real global.setTimeout to schedule a callback to _runCallbacks.
function _scheduleRealCallback() {
if (_realCallbackKey) {
global.clearTimeout(_realCallbackKey);
// use the real global.setTimeout to schedule a callback to runCallbacks.
function scheduleRealCallback(): void {
if (realCallbackKey) {
global.clearTimeout(realCallbackKey as NodeJS.Timeout);
}
const first = _callbackList[0];
const first = callbackList[0];
if (!first) {
debuglog("_scheduleRealCallback: no more callbacks, not rescheduling");
debuglog("scheduleRealCallback: no more callbacks, not rescheduling");
return;
}
const now = _now();
const delayMs = Math.min(first.runAt - now, TIMER_CHECK_PERIOD_MS);
const timestamp = now();
const delayMs = Math.min(first.runAt - timestamp, TIMER_CHECK_PERIOD_MS);
debuglog("_scheduleRealCallback: now:", now, "delay:", delayMs);
_realCallbackKey = global.setTimeout(_runCallbacks, delayMs);
debuglog("scheduleRealCallback: now:", timestamp, "delay:", delayMs);
realCallbackKey = global.setTimeout(runCallbacks, delayMs);
}
function _runCallbacks() {
function runCallbacks(): void {
let cb;
const now = _now();
debuglog("_runCallbacks: now:", now);
const timestamp = now();
debuglog("runCallbacks: now:", timestamp);
// get the list of things to call
const callbacksToRun = [];
// eslint-disable-next-line
while (true) {
const first = _callbackList[0];
if (!first || first.runAt > now) {
const first = callbackList[0];
if (!first || first.runAt > timestamp) {
break;
}
cb = _callbackList.shift();
debuglog("_runCallbacks: popping", cb.key);
cb = callbackList.shift();
debuglog("runCallbacks: popping", cb.key);
callbacksToRun.push(cb);
}
// reschedule the real callback before running our functions, to
// keep the codepaths the same whether or not our functions
// register their own setTimeouts.
_scheduleRealCallback();
scheduleRealCallback();
for (let i = 0; i < callbacksToRun.length; i++) {
cb = callbacksToRun[i];
@@ -172,7 +177,7 @@ function _runCallbacks() {
cb.func.apply(global, cb.params);
} catch (e) {
logger.error("Uncaught exception in callback function",
e.stack || e);
e.stack || e);
}
}
}
@@ -182,7 +187,7 @@ function _runCallbacks() {
* returns the index of the last element for which func returns
* greater than zero, or array.length if no such element exists.
*/
function binarySearch(array, func) {
function binarySearch<T>(array: T[], func: (v: T) => number): number {
// min is inclusive, max exclusive.
let min = 0;
let max = array.length;
+149
View File
@@ -0,0 +1,149 @@
/*
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.
*/
/**
* @module room-hierarchy
*/
import { Room } from "./models/room";
import { IHierarchyRoom, IHierarchyRelation } from "./@types/spaces";
import { MatrixClient } from "./client";
import { EventType } from "./@types/event";
export class RoomHierarchy {
// Map from room id to list of servers which are listed as a via somewhere in the loaded hierarchy
public readonly viaMap = new Map<string, Set<string>>();
// Map from room id to list of rooms which claim this room as their child
public readonly backRefs = new Map<string, string[]>();
// Map from room id to object
public readonly roomMap = new Map<string, IHierarchyRoom>();
private loadRequest: ReturnType<MatrixClient["getRoomHierarchy"]>;
private nextBatch?: string;
private _rooms?: IHierarchyRoom[];
private serverSupportError?: Error;
/**
* Construct a new RoomHierarchy
*
* A RoomHierarchy instance allows you to easily make use of the /hierarchy API and paginate it.
*
* @param {Room} root the root of this hierarchy
* @param {number} pageSize the maximum number of rooms to return per page, can be overridden per load request.
* @param {number} maxDepth the maximum depth to traverse the hierarchy to
* @param {boolean} suggestedOnly whether to only return rooms with suggested=true.
* @constructor
*/
constructor(
private readonly root: Room,
private readonly pageSize?: number,
private readonly maxDepth?: number,
private readonly suggestedOnly = false,
) {}
public get noSupport(): boolean {
return !!this.serverSupportError;
}
public get canLoadMore(): boolean {
return !!this.serverSupportError || !!this.nextBatch || !this._rooms;
}
public get rooms(): IHierarchyRoom[] {
return this._rooms;
}
public async load(pageSize = this.pageSize): Promise<IHierarchyRoom[]> {
if (this.loadRequest) return this.loadRequest.then(r => r.rooms);
this.loadRequest = this.root.client.getRoomHierarchy(
this.root.roomId,
pageSize,
this.maxDepth,
this.suggestedOnly,
this.nextBatch,
);
let rooms: IHierarchyRoom[];
try {
({ rooms, next_batch: this.nextBatch } = await this.loadRequest);
} catch (e) {
if (e.errcode === "M_UNRECOGNIZED") {
this.serverSupportError = e;
} else {
throw e;
}
return [];
} finally {
this.loadRequest = null;
}
if (this._rooms) {
this._rooms = this._rooms.concat(rooms);
} else {
this._rooms = rooms;
}
rooms.forEach(room => {
this.roomMap.set(room.room_id, room);
room.children_state.forEach(ev => {
if (ev.type !== EventType.SpaceChild) return;
const childRoomId = ev.state_key;
// track backrefs for quicker hierarchy navigation
if (!this.backRefs.has(childRoomId)) {
this.backRefs.set(childRoomId, []);
}
this.backRefs.get(childRoomId).push(ev.room_id);
// fill viaMap
if (Array.isArray(ev.content.via)) {
if (!this.viaMap.has(childRoomId)) {
this.viaMap.set(childRoomId, new Set());
}
const vias = this.viaMap.get(childRoomId);
ev.content.via.forEach(via => vias.add(via));
}
});
});
return rooms;
}
public getRelation(parentId: string, childId: string): IHierarchyRelation {
return this.roomMap.get(parentId)?.children_state.find(e => e.state_key === childId);
}
public isSuggested(parentId: string, childId: string): boolean {
return this.getRelation(parentId, childId)?.content.suggested;
}
// locally remove a relation as a form of local echo
public removeRelation(parentId: string, childId: string): void {
const backRefs = this.backRefs.get(childId);
if (backRefs?.length === 1) {
this.backRefs.delete(childId);
} else if (backRefs?.length) {
this.backRefs.set(childId, backRefs.filter(ref => ref !== parentId));
}
const room = this.roomMap.get(parentId);
if (room) {
room.children_state = room.children_state.filter(ev => ev.state_key !== childId);
}
}
}
-327
View File
@@ -1,327 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
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.
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.
*/
/**
* This is an internal module which manages queuing, scheduling and retrying
* of requests.
* @module scheduler
*/
import * as utils from "./utils";
import { logger } from './logger';
const DEBUG = false; // set true to enable console logging.
/**
* Construct a scheduler for Matrix. Requires
* {@link module:scheduler~MatrixScheduler#setProcessFunction} to be provided
* with a way of processing events.
* @constructor
* @param {module:scheduler~retryAlgorithm} retryAlgorithm Optional. The retry
* algorithm to apply when determining when to try to send an event again.
* Defaults to {@link module:scheduler~MatrixScheduler.RETRY_BACKOFF_RATELIMIT}.
* @param {module:scheduler~queueAlgorithm} queueAlgorithm Optional. The queuing
* algorithm to apply when determining which events should be sent before the
* given event. Defaults to {@link module:scheduler~MatrixScheduler.QUEUE_MESSAGES}.
*/
export function MatrixScheduler(retryAlgorithm, queueAlgorithm) {
this.retryAlgorithm = retryAlgorithm || MatrixScheduler.RETRY_BACKOFF_RATELIMIT;
this.queueAlgorithm = queueAlgorithm || MatrixScheduler.QUEUE_MESSAGES;
this._queues = {
// queueName: [{
// event: MatrixEvent, // event to send
// defer: Deferred, // defer to resolve/reject at the END of the retries
// attempts: Number // number of times we've called processFn
// }, ...]
};
this._activeQueues = [];
this._procFn = null;
}
/**
* Retrieve a queue based on an event. The event provided does not need to be in
* the queue.
* @param {MatrixEvent} event An event to get the queue for.
* @return {?Array<MatrixEvent>} A shallow copy of events in the queue or null.
* Modifying this array will not modify the list itself. Modifying events in
* this array <i>will</i> modify the underlying event in the queue.
* @see MatrixScheduler.removeEventFromQueue To remove an event from the queue.
*/
MatrixScheduler.prototype.getQueueForEvent = function(event) {
const name = this.queueAlgorithm(event);
if (!name || !this._queues[name]) {
return null;
}
return this._queues[name].map(function(obj) {
return obj.event;
});
};
/**
* Remove this event from the queue. The event is equal to another event if they
* have the same ID returned from event.getId().
* @param {MatrixEvent} event The event to remove.
* @return {boolean} True if this event was removed.
*/
MatrixScheduler.prototype.removeEventFromQueue = function(event) {
const name = this.queueAlgorithm(event);
if (!name || !this._queues[name]) {
return false;
}
let removed = false;
utils.removeElement(this._queues[name], function(element) {
if (element.event.getId() === event.getId()) {
// XXX we should probably reject the promise?
// https://github.com/matrix-org/matrix-js-sdk/issues/496
removed = true;
return true;
}
});
return removed;
};
/**
* Set the process function. Required for events in the queue to be processed.
* If set after events have been added to the queue, this will immediately start
* processing them.
* @param {module:scheduler~processFn} fn The function that can process events
* in the queue.
*/
MatrixScheduler.prototype.setProcessFunction = function(fn) {
this._procFn = fn;
_startProcessingQueues(this);
};
/**
* Queue an event if it is required and start processing queues.
* @param {MatrixEvent} event The event that may be queued.
* @return {?Promise} A promise if the event was queued, which will be
* resolved or rejected in due time, else null.
*/
MatrixScheduler.prototype.queueEvent = function(event) {
const queueName = this.queueAlgorithm(event);
if (!queueName) {
return null;
}
// add the event to the queue and make a deferred for it.
if (!this._queues[queueName]) {
this._queues[queueName] = [];
}
const defer = utils.defer();
this._queues[queueName].push({
event: event,
defer: defer,
attempts: 0,
});
debuglog(
"Queue algorithm dumped event %s into queue '%s'",
event.getId(), queueName,
);
_startProcessingQueues(this);
return defer.promise;
};
/**
* Retries events up to 4 times using exponential backoff. This produces wait
* times of 2, 4, 8, and 16 seconds (30s total) after which we give up. If the
* failure was due to a rate limited request, the time specified in the error is
* waited before being retried.
* @param {MatrixEvent} event
* @param {Number} attempts
* @param {MatrixError} err
* @return {Number}
* @see module:scheduler~retryAlgorithm
*/
MatrixScheduler.RETRY_BACKOFF_RATELIMIT = function(event, attempts, err) {
if (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401) {
// client error; no amount of retrying with save you now.
return -1;
}
// we ship with browser-request which returns { cors: rejected } when trying
// with no connection, so if we match that, give up since they have no conn.
if (err.cors === "rejected") {
return -1;
}
// if event that we are trying to send is too large in any way then retrying won't help
if (err.name === "M_TOO_LARGE") {
return -1;
}
if (err.name === "M_LIMIT_EXCEEDED") {
const waitTime = err.data.retry_after_ms;
if (waitTime > 0) {
return waitTime;
}
}
if (attempts > 4) {
return -1; // give up
}
return (1000 * Math.pow(2, attempts));
};
/**
* Queues <code>m.room.message</code> events and lets other events continue
* concurrently.
* @param {MatrixEvent} event
* @return {string}
* @see module:scheduler~queueAlgorithm
*/
MatrixScheduler.QUEUE_MESSAGES = function(event) {
// enqueue messages or events that associate with another event (redactions and relations)
if (event.getType() === "m.room.message" || event.hasAssocation()) {
// put these events in the 'message' queue.
return "message";
}
// allow all other events continue concurrently.
return null;
};
function _startProcessingQueues(scheduler) {
if (!scheduler._procFn) {
return;
}
// for each inactive queue with events in them
Object.keys(scheduler._queues)
.filter(function(queueName) {
return scheduler._activeQueues.indexOf(queueName) === -1 &&
scheduler._queues[queueName].length > 0;
})
.forEach(function(queueName) {
// mark the queue as active
scheduler._activeQueues.push(queueName);
// begin processing the head of the queue
debuglog("Spinning up queue: '%s'", queueName);
_processQueue(scheduler, queueName);
});
}
function _processQueue(scheduler, queueName) {
// get head of queue
const obj = _peekNextEvent(scheduler, queueName);
if (!obj) {
// queue is empty. Mark as inactive and stop recursing.
const index = scheduler._activeQueues.indexOf(queueName);
if (index >= 0) {
scheduler._activeQueues.splice(index, 1);
}
debuglog("Stopping queue '%s' as it is now empty", queueName);
return;
}
debuglog(
"Queue '%s' has %s pending events",
queueName, scheduler._queues[queueName].length,
);
// fire the process function and if it resolves, resolve the deferred. Else
// invoke the retry algorithm.
// First wait for a resolved promise, so the resolve handlers for
// the deferred of the previously sent event can run.
// This way enqueued relations/redactions to enqueued events can receive
// the remove id of their target before being sent.
Promise.resolve().then(() => {
return scheduler._procFn(obj.event);
}).then(function(res) {
// remove this from the queue
_removeNextEvent(scheduler, queueName);
debuglog("Queue '%s' sent event %s", queueName, obj.event.getId());
obj.defer.resolve(res);
// keep processing
_processQueue(scheduler, queueName);
}, function(err) {
obj.attempts += 1;
// ask the retry algorithm when/if we should try again
const waitTimeMs = scheduler.retryAlgorithm(obj.event, obj.attempts, err);
debuglog(
"retry(%s) err=%s event_id=%s waitTime=%s",
obj.attempts, err, obj.event.getId(), waitTimeMs,
);
if (waitTimeMs === -1) { // give up (you quitter!)
debuglog(
"Queue '%s' giving up on event %s", queueName, obj.event.getId(),
);
// remove this from the queue
_removeNextEvent(scheduler, queueName);
obj.defer.reject(err);
// process next event
_processQueue(scheduler, queueName);
} else {
setTimeout(function() {
_processQueue(scheduler, queueName);
}, waitTimeMs);
}
});
}
function _peekNextEvent(scheduler, queueName) {
const queue = scheduler._queues[queueName];
if (!Array.isArray(queue)) {
return null;
}
return queue[0];
}
function _removeNextEvent(scheduler, queueName) {
const queue = scheduler._queues[queueName];
if (!Array.isArray(queue)) {
return null;
}
return queue.shift();
}
function debuglog() {
if (DEBUG) {
logger.log(...arguments);
}
}
/**
* The retry algorithm to apply when retrying events. To stop retrying, return
* <code>-1</code>. If this event was part of a queue, it will be removed from
* the queue.
* @callback retryAlgorithm
* @param {MatrixEvent} event The event being retried.
* @param {Number} attempts The number of failed attempts. This will always be
* >= 1.
* @param {MatrixError} err The most recent error message received when trying
* to send this event.
* @return {Number} The number of milliseconds to wait before trying again. If
* this is 0, the request will be immediately retried. If this is
* <code>-1</code>, the event will be marked as
* {@link module:models/event.EventStatus.NOT_SENT} and will not be retried.
*/
/**
* The queuing algorithm to apply to events. This function must be idempotent as
* it may be called multiple times with the same event. All queues created are
* serviced in a FIFO manner. To send the event ASAP, return <code>null</code>
* which will not put this event in a queue. Events that fail to send that form
* part of a queue will be removed from the queue and the next event in the
* queue will be sent.
* @callback queueAlgorithm
* @param {MatrixEvent} event The event to be sent.
* @return {string} The name of the queue to put the event into. If a queue with
* this name does not exist, it will be created. If this is <code>null</code>,
* the event is not put into a queue and will be sent concurrently.
*/
/**
* The function to invoke to process (send) events in the queue.
* @callback processFn
* @param {MatrixEvent} event The event to send.
* @return {Promise} Resolved/rejected depending on the outcome of the request.
*/
+329
View File
@@ -0,0 +1,329 @@
/*
Copyright 2015 - 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.
*/
/**
* This is an internal module which manages queuing, scheduling and retrying
* of requests.
* @module scheduler
*/
import * as utils from "./utils";
import { logger } from './logger';
import { MatrixEvent } from "./models/event";
import { EventType } from "./@types/event";
import { IDeferred } from "./utils";
import { MatrixError } from "./http-api";
import { ISendEventResponse } from "./@types/requests";
const DEBUG = false; // set true to enable console logging.
interface IQueueEntry<T> {
event: MatrixEvent;
defer: IDeferred<T>;
attempts: number;
}
type ProcessFunction<T> = (event: MatrixEvent) => Promise<T>;
/**
* Construct a scheduler for Matrix. Requires
* {@link module:scheduler~MatrixScheduler#setProcessFunction} to be provided
* with a way of processing events.
* @constructor
* @param {module:scheduler~retryAlgorithm} retryAlgorithm Optional. The retry
* algorithm to apply when determining when to try to send an event again.
* Defaults to {@link module:scheduler~MatrixScheduler.RETRY_BACKOFF_RATELIMIT}.
* @param {module:scheduler~queueAlgorithm} queueAlgorithm Optional. The queuing
* algorithm to apply when determining which events should be sent before the
* given event. Defaults to {@link module:scheduler~MatrixScheduler.QUEUE_MESSAGES}.
*/
// eslint-disable-next-line camelcase
export class MatrixScheduler<T = ISendEventResponse> {
/**
* Retries events up to 4 times using exponential backoff. This produces wait
* times of 2, 4, 8, and 16 seconds (30s total) after which we give up. If the
* failure was due to a rate limited request, the time specified in the error is
* waited before being retried.
* @param {MatrixEvent} event
* @param {Number} attempts
* @param {MatrixError} err
* @return {Number}
* @see module:scheduler~retryAlgorithm
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
public static RETRY_BACKOFF_RATELIMIT(event: MatrixEvent, attempts: number, err: MatrixError): number {
if (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401) {
// client error; no amount of retrying with save you now.
return -1;
}
// we ship with browser-request which returns { cors: rejected } when trying
// with no connection, so if we match that, give up since they have no conn.
if (err.cors === "rejected") {
return -1;
}
// if event that we are trying to send is too large in any way then retrying won't help
if (err.name === "M_TOO_LARGE") {
return -1;
}
if (err.name === "M_LIMIT_EXCEEDED") {
const waitTime = err.data.retry_after_ms;
if (waitTime > 0) {
return waitTime;
}
}
if (attempts > 4) {
return -1; // give up
}
return (1000 * Math.pow(2, attempts));
}
/**
* Queues <code>m.room.message</code> events and lets other events continue
* concurrently.
* @param {MatrixEvent} event
* @return {string}
* @see module:scheduler~queueAlgorithm
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
public static QUEUE_MESSAGES(event: MatrixEvent) {
// enqueue messages or events that associate with another event (redactions and relations)
if (event.getType() === EventType.RoomMessage || event.hasAssocation()) {
// put these events in the 'message' queue.
return "message";
}
// allow all other events continue concurrently.
return null;
}
// queueName: [{
// event: MatrixEvent, // event to send
// defer: Deferred, // defer to resolve/reject at the END of the retries
// attempts: Number // number of times we've called processFn
// }, ...]
private readonly queues: Record<string, IQueueEntry<T>[]> = {};
private activeQueues: string[] = [];
private procFn: ProcessFunction<T> = null;
constructor(
public readonly retryAlgorithm = MatrixScheduler.RETRY_BACKOFF_RATELIMIT,
public readonly queueAlgorithm = MatrixScheduler.QUEUE_MESSAGES,
) {}
/**
* Retrieve a queue based on an event. The event provided does not need to be in
* the queue.
* @param {MatrixEvent} event An event to get the queue for.
* @return {?Array<MatrixEvent>} A shallow copy of events in the queue or null.
* Modifying this array will not modify the list itself. Modifying events in
* this array <i>will</i> modify the underlying event in the queue.
* @see MatrixScheduler.removeEventFromQueue To remove an event from the queue.
*/
public getQueueForEvent(event: MatrixEvent): MatrixEvent[] {
const name = this.queueAlgorithm(event);
if (!name || !this.queues[name]) {
return null;
}
return this.queues[name].map(function(obj) {
return obj.event;
});
}
/**
* Remove this event from the queue. The event is equal to another event if they
* have the same ID returned from event.getId().
* @param {MatrixEvent} event The event to remove.
* @return {boolean} True if this event was removed.
*/
public removeEventFromQueue(event: MatrixEvent): boolean {
const name = this.queueAlgorithm(event);
if (!name || !this.queues[name]) {
return false;
}
let removed = false;
utils.removeElement(this.queues[name], (element) => {
if (element.event.getId() === event.getId()) {
// XXX we should probably reject the promise?
// https://github.com/matrix-org/matrix-js-sdk/issues/496
removed = true;
return true;
}
});
return removed;
}
/**
* Set the process function. Required for events in the queue to be processed.
* If set after events have been added to the queue, this will immediately start
* processing them.
* @param {module:scheduler~processFn} fn The function that can process events
* in the queue.
*/
public setProcessFunction(fn: ProcessFunction<T>): void {
this.procFn = fn;
this.startProcessingQueues();
}
/**
* Queue an event if it is required and start processing queues.
* @param {MatrixEvent} event The event that may be queued.
* @return {?Promise} A promise if the event was queued, which will be
* resolved or rejected in due time, else null.
*/
public queueEvent(event: MatrixEvent): Promise<T> | null {
const queueName = this.queueAlgorithm(event);
if (!queueName) {
return null;
}
// add the event to the queue and make a deferred for it.
if (!this.queues[queueName]) {
this.queues[queueName] = [];
}
const defer = utils.defer<T>();
this.queues[queueName].push({
event: event,
defer: defer,
attempts: 0,
});
debuglog("Queue algorithm dumped event %s into queue '%s'", event.getId(), queueName);
this.startProcessingQueues();
return defer.promise;
}
private startProcessingQueues(): void {
if (!this.procFn) return;
// for each inactive queue with events in them
Object.keys(this.queues)
.filter((queueName) => {
return this.activeQueues.indexOf(queueName) === -1 &&
this.queues[queueName].length > 0;
})
.forEach((queueName) => {
// mark the queue as active
this.activeQueues.push(queueName);
// begin processing the head of the queue
debuglog("Spinning up queue: '%s'", queueName);
this.processQueue(queueName);
});
}
private processQueue = (queueName: string): void => {
// get head of queue
const obj = this.peekNextEvent(queueName);
if (!obj) {
// queue is empty. Mark as inactive and stop recursing.
const index = this.activeQueues.indexOf(queueName);
if (index >= 0) {
this.activeQueues.splice(index, 1);
}
debuglog("Stopping queue '%s' as it is now empty", queueName);
return;
}
debuglog("Queue '%s' has %s pending events", queueName, this.queues[queueName].length);
// fire the process function and if it resolves, resolve the deferred. Else
// invoke the retry algorithm.
// First wait for a resolved promise, so the resolve handlers for
// the deferred of the previously sent event can run.
// This way enqueued relations/redactions to enqueued events can receive
// the remove id of their target before being sent.
Promise.resolve().then(() => {
return this.procFn(obj.event);
}).then((res) => {
// remove this from the queue
this.removeNextEvent(queueName);
debuglog("Queue '%s' sent event %s", queueName, obj.event.getId());
obj.defer.resolve(res);
// keep processing
this.processQueue(queueName);
}, (err) => {
obj.attempts += 1;
// ask the retry algorithm when/if we should try again
const waitTimeMs = this.retryAlgorithm(obj.event, obj.attempts, err);
debuglog("retry(%s) err=%s event_id=%s waitTime=%s", obj.attempts, err, obj.event.getId(), waitTimeMs);
if (waitTimeMs === -1) { // give up (you quitter!)
debuglog("Queue '%s' giving up on event %s", queueName, obj.event.getId());
// remove this from the queue
this.removeNextEvent(queueName);
obj.defer.reject(err);
// process next event
this.processQueue(queueName);
} else {
setTimeout(this.processQueue, waitTimeMs, queueName);
}
});
};
private peekNextEvent(queueName: string): IQueueEntry<T> {
const queue = this.queues[queueName];
if (!Array.isArray(queue)) {
return null;
}
return queue[0];
}
private removeNextEvent(queueName: string): IQueueEntry<T> {
const queue = this.queues[queueName];
if (!Array.isArray(queue)) {
return null;
}
return queue.shift();
}
}
function debuglog(...args) {
if (DEBUG) {
logger.log(...args);
}
}
/**
* The retry algorithm to apply when retrying events. To stop retrying, return
* <code>-1</code>. If this event was part of a queue, it will be removed from
* the queue.
* @callback retryAlgorithm
* @param {MatrixEvent} event The event being retried.
* @param {Number} attempts The number of failed attempts. This will always be
* >= 1.
* @param {MatrixError} err The most recent error message received when trying
* to send this event.
* @return {Number} The number of milliseconds to wait before trying again. If
* this is 0, the request will be immediately retried. If this is
* <code>-1</code>, the event will be marked as
* {@link module:models/event.EventStatus.NOT_SENT} and will not be retried.
*/
/**
* The queuing algorithm to apply to events. This function must be idempotent as
* it may be called multiple times with the same event. All queues created are
* serviced in a FIFO manner. To send the event ASAP, return <code>null</code>
* which will not put this event in a queue. Events that fail to send that form
* part of a queue will be removed from the queue and the next event in the
* queue will be sent.
* @callback queueAlgorithm
* @param {MatrixEvent} event The event to be sent.
* @return {string} The name of the queue to put the event into. If a queue with
* this name does not exist, it will be created. If this is <code>null</code>,
* the event is not put into a queue and will be sent concurrently.
*/
/**
* The function to invoke to process (send) events in the queue.
* @callback processFn
* @param {MatrixEvent} event The event to send.
* @return {Promise} Resolved/rejected depending on the outcome of the request.
*/
+3
View File
@@ -56,6 +56,7 @@ export interface IStore {
/**
* No-op.
* @param {Group} group
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
storeGroup(group: Group);
@@ -63,12 +64,14 @@ export interface IStore {
* No-op.
* @param {string} groupId
* @return {null}
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
getGroup(groupId: string): Group | null;
/**
* No-op.
* @return {Array} An empty array.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
getGroups(): Group[];
+3
View File
@@ -93,6 +93,7 @@ export class MemoryStore implements IStore {
/**
* Store the given room.
* @param {Group} group The group to be stored
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public storeGroup(group: Group) {
this.groups[group.groupId] = group;
@@ -102,6 +103,7 @@ export class MemoryStore implements IStore {
* Retrieve a group by its group ID.
* @param {string} groupId The group ID.
* @return {Group} The group or null.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroup(groupId: string): Group | null {
return this.groups[groupId] || null;
@@ -110,6 +112,7 @@ export class MemoryStore implements IStore {
/**
* Retrieve all known groups.
* @return {Group[]} A list of groups, which may be empty.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroups(): Group[] {
return Object.values(this.groups);
+3
View File
@@ -61,6 +61,7 @@ export class StubStore implements IStore {
/**
* No-op.
* @param {Group} group
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public storeGroup(group: Group) {}
@@ -68,6 +69,7 @@ export class StubStore implements IStore {
* No-op.
* @param {string} groupId
* @return {null}
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroup(groupId: string): Group | null {
return null;
@@ -76,6 +78,7 @@ export class StubStore implements IStore {
/**
* No-op.
* @return {Array} An empty array.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroups(): Group[] {
return [];
+90 -13
View File
@@ -52,6 +52,8 @@ import {
import { MatrixEvent } from "./models/event";
import { MatrixError } from "./http-api";
import { ISavedSync } from "./store";
import { EventType } from "./@types/event";
import { IPushRules } from "./@types/PushRules";
const DEBUG = true;
@@ -146,6 +148,7 @@ export class SyncApi {
this.opts.resolveInvitesToProfiles = this.opts.resolveInvitesToProfiles || false;
this.opts.pollTimeout = this.opts.pollTimeout || (30 * 1000);
this.opts.pendingEventOrdering = this.opts.pendingEventOrdering || PendingEventOrdering.Chronological;
this.opts.experimentalThreadSupport = this.opts.experimentalThreadSupport === true;
if (!opts.canResetEntireTimeline) {
opts.canResetEntireTimeline = (roomId: string) => {
@@ -281,8 +284,9 @@ export class SyncApi {
return;
}
leaveObj.timeline = leaveObj.timeline || {};
const timelineEvents =
this.mapSyncEventsFormat(leaveObj.timeline, room);
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(events);
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
// set the back-pagination token. Do this *before* adding any
@@ -291,17 +295,51 @@ export class SyncApi {
EventTimeline.BACKWARDS);
this.processRoomEvents(room, stateEvents, timelineEvents);
this.processThreadEvents(room, threadedEvents);
room.recalculate();
client.store.storeRoom(room);
client.emit("Room", room);
this.processEventsForNotifs(room, timelineEvents);
this.processEventsForNotifs(room, events);
});
return rooms;
});
}
/**
* Split events between the ones that will end up in the main
* room timeline versus the one that need to be processed in a thread
* @experimental
*/
public partitionThreadedEvents(events: MatrixEvent[]): [MatrixEvent[], MatrixEvent[]] {
if (this.opts.experimentalThreadSupport) {
return events.reduce((memo, event: MatrixEvent) => {
const room = this.client.getRoom(event.getRoomId());
// An event should live in the thread timeline if
// - It's a reply in thread event
// - It's related to a reply in thread event
let shouldLiveInThreadTimeline = event.replyInThread;
if (!shouldLiveInThreadTimeline) {
const parentEventId = event.getWireContent()["m.relates_to"]?.event_id;
const parentEvent = room?.findEventById(parentEventId) || events.find((mxEv: MatrixEvent) => {
return mxEv.getId() === parentEventId;
});
shouldLiveInThreadTimeline = parentEvent?.replyInThread;
}
memo[shouldLiveInThreadTimeline ? 1 : 0].push(event);
return memo;
}, [[], []]);
} else {
// When `experimentalThreadSupport` is disabled
// treat all events as timelineEvents
return [
events,
[],
];
}
}
/**
* Peek into a room. This will result in the room in question being synced so it
* is accessible via getRooms(). Live updates for the room will be provided.
@@ -678,7 +716,11 @@ export class SyncApi {
*/
public stop(): void {
debuglog("SyncApi.stop");
if (global.window) {
// It is necessary to check for the existance of
// global.window AND global.window.removeEventListener.
// Some platforms (e.g. React Native) register global.window,
// but do not have global.window.removeEventListener.
if (global.window && global.window.removeEventListener) {
global.window.removeEventListener("online", this.onOnline, false);
}
this.running = false;
@@ -1065,8 +1107,8 @@ export class SyncApi {
// honour push rules that were previously cached. Base rules
// will be updated when we receive push rules via getPushRules
// (see sync) before syncing over the network.
if (accountDataEvent.getType() === 'm.push_rules') {
const rules = accountDataEvent.getContent();
if (accountDataEvent.getType() === EventType.PushRules) {
const rules = accountDataEvent.getContent<IPushRules>();
client.pushRules = PushProcessor.rewriteDefaultRules(rules);
}
const prevEvent = prevEventsMap[accountDataEvent.getId()];
@@ -1191,7 +1233,7 @@ export class SyncApi {
// this helps large account to speed up faster
// room::decryptCriticalEvent is in charge of decrypting all the events
// required for a client to function properly
const timelineEvents = this.mapSyncEventsFormat(joinObj.timeline, room, false);
const events = this.mapSyncEventsFormat(joinObj.timeline, room, false);
const ephemeralEvents = this.mapSyncEventsFormat(joinObj.ephemeral);
const accountDataEvents = this.mapSyncEventsFormat(joinObj.account_data);
@@ -1238,8 +1280,8 @@ export class SyncApi {
// which we'll try to paginate but not get any new events (which
// will stop us linking the empty timeline into the chain).
//
for (let i = timelineEvents.length - 1; i >= 0; i--) {
const eventId = timelineEvents[i].getId();
for (let i = events.length - 1; i >= 0; i--) {
const eventId = events[i].getId();
if (room.getTimelineForEvent(eventId)) {
debuglog("Already have event " + eventId + " in limited " +
"sync - not resetting");
@@ -1248,7 +1290,7 @@ export class SyncApi {
// we might still be missing some of the events before i;
// we don't want to be adding them to the end of the
// timeline because that would put them out of order.
timelineEvents.splice(0, i);
events.splice(0, i);
// XXX: there's a problem here if the skipped part of the
// timeline modifies the state set in stateEvents, because
@@ -1278,7 +1320,10 @@ export class SyncApi {
}
}
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(events);
this.processRoomEvents(room, stateEvents, timelineEvents, syncEventData.fromCache);
this.processThreadEvents(room, threadedEvents);
// set summary after processing events,
// because it will trigger a name calculation
@@ -1299,7 +1344,7 @@ export class SyncApi {
client.emit("Room", room);
}
this.processEventsForNotifs(room, timelineEvents);
this.processEventsForNotifs(room, events);
const processRoomEvent = async (e) => {
client.emit("event", e);
@@ -1320,6 +1365,7 @@ export class SyncApi {
await utils.promiseMapSeries(stateEvents, processRoomEvent);
await utils.promiseMapSeries(timelineEvents, processRoomEvent);
await utils.promiseMapSeries(threadedEvents, processRoomEvent);
ephemeralEvents.forEach(function(e) {
client.emit("event", e);
});
@@ -1339,10 +1385,13 @@ export class SyncApi {
leaveRooms.forEach((leaveObj) => {
const room = leaveObj.room;
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
const timelineEvents = this.mapSyncEventsFormat(leaveObj.timeline, room);
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
const accountDataEvents = this.mapSyncEventsFormat(leaveObj.account_data);
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(events);
this.processRoomEvents(room, stateEvents, timelineEvents);
this.processThreadEvents(room, threadedEvents);
room.addAccountData(accountDataEvents);
room.recalculate();
@@ -1351,7 +1400,7 @@ export class SyncApi {
client.emit("Room", room);
}
this.processEventsForNotifs(room, timelineEvents);
this.processEventsForNotifs(room, events);
stateEvents.forEach(function(e) {
client.emit("event", e);
@@ -1359,6 +1408,9 @@ export class SyncApi {
timelineEvents.forEach(function(e) {
client.emit("event", e);
});
threadedEvents.forEach(function(e) {
client.emit("event", e);
});
accountDataEvents.forEach(function(e) {
client.emit("event", e);
});
@@ -1678,6 +1730,31 @@ export class SyncApi {
room.addLiveEvents(timelineEventList || [], null, fromCache);
}
/**
* @experimental
*/
private processThreadEvents(room: Room, threadedEvents: MatrixEvent[]): void {
threadedEvents
.sort((a, b) => a.getTs() - b.getTs())
.forEach(event => {
room.addThreadedEvent(event);
});
}
// extractRelatedEvents(event: MatrixEvent, events: MatrixEvent[], relatedEvents: MatrixEvent[] = []): MatrixEvent[] {
// relatedEvents.push(event);
// const parentEventId = event.parentEventId;
// const parentEventIndex = events.findIndex(event => event.getId() === parentEventId);
// if (parentEventIndex > -1) {
// const [relatedEvent] = events.splice(parentEventIndex, 1);
// return this.extractRelatedEvents(relatedEvent, events, relatedEvents);
// } else {
// return relatedEvents;
// }
// }
/**
* Takes a list of timelineEvents and adds and adds to notifEvents
* as appropriate.
+2 -2
View File
@@ -412,7 +412,7 @@ export function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export function globToRegexp(glob: string, extended: any): string {
export function globToRegexp(glob: string, extended?: any): string {
extended = typeof(extended) === 'boolean' ? extended : true;
// From
// https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
@@ -457,7 +457,7 @@ export interface IDeferred<T> {
}
// Returns a Deferred
export function defer<T>(): IDeferred<T> {
export function defer<T = void>(): IDeferred<T> {
let resolve;
let reject;
+214 -285
View File
File diff suppressed because it is too large Load Diff
+22 -13
View File
@@ -19,6 +19,7 @@ import { logger } from '../logger';
import { createNewMatrixCall, MatrixCall, CallErrorCode, CallState, CallDirection } from './call';
import { EventType } from '../@types/event';
import { MatrixClient } from '../client';
import { MCallAnswer, MCallHangupReject } from "./callEventTypes";
// Don't ring unless we'd be ringing for at least 3 seconds: the user needs some
// time to press the 'accept' button
@@ -80,7 +81,7 @@ export class CallEventHandler {
continue;
}
try {
this.handleCallEvent(e);
await this.handleCallEvent(e);
} catch (e) {
logger.error("Caught exception handling call event", e);
}
@@ -100,7 +101,7 @@ export class CallEventHandler {
if (event.isBeingDecrypted() || event.isDecryptionFailure()) {
// add an event listener for once the event is decrypted.
event.once("Event.decrypted", () => {
event.once("Event.decrypted", async () => {
if (!this.eventIsACall(event)) return;
if (this.callEventBuffer.includes(event)) {
@@ -110,7 +111,7 @@ export class CallEventHandler {
// This one wasn't buffered so just run the event handler for it
// straight away
try {
this.handleCallEvent(event);
await this.handleCallEvent(event);
} catch (e) {
logger.error("Caught exception handling call event", e);
}
@@ -128,7 +129,7 @@ export class CallEventHandler {
return type.startsWith("m.call.") || type.startsWith("org.matrix.call.");
}
private handleCallEvent(event: MatrixEvent) {
private async handleCallEvent(event: MatrixEvent) {
const content = event.getContent();
const type = event.getType() as EventType;
const weSentTheEvent = event.getSender() === this.client.credentials.userId;
@@ -169,7 +170,7 @@ export class CallEventHandler {
}
call.callId = content.call_id;
call.initWithInvite(event);
await call.initWithInvite(event);
this.calls.set(call.callId, call);
// if we stashed candidate events for that call ID, play them back now
@@ -201,9 +202,11 @@ export class CallEventHandler {
// we've got an invite, pick the incoming call because we know
// we haven't sent our invite yet otherwise, pick whichever
// call has the lowest call ID (by string comparison)
if (existingCall.state === CallState.WaitLocalMedia ||
existingCall.state === CallState.CreateOffer ||
existingCall.callId > call.callId) {
if (
existingCall.state === CallState.WaitLocalMedia ||
existingCall.state === CallState.CreateOffer ||
existingCall.callId > call.callId
) {
logger.log(
"Glare detected: answering incoming call " + call.callId +
" and canceling outgoing call " + existingCall.callId,
@@ -220,6 +223,7 @@ export class CallEventHandler {
} else {
this.client.emit("Call.incoming", call);
}
return;
} else if (type === EventType.CallCandidates) {
if (weSentTheEvent) return;
@@ -232,6 +236,7 @@ export class CallEventHandler {
} else {
call.onRemoteIceCandidatesReceived(event);
}
return;
} else if ([EventType.CallHangup, EventType.CallReject].includes(type)) {
// Note that we also observe our own hangups here so we can see
// if we've already rejected a call that would otherwise be valid
@@ -248,17 +253,21 @@ export class CallEventHandler {
} else {
if (call.state !== CallState.Ended) {
if (type === EventType.CallHangup) {
call.onHangupReceived(content);
call.onHangupReceived(content as MCallHangupReject);
} else {
call.onRejectReceived(content);
call.onRejectReceived(content as MCallHangupReject);
}
this.calls.delete(content.call_id);
}
}
return;
}
// The following events need a call
if (!call) return;
// The following events need a call and a peer connection
if (!call || !call.hasPeerConnection) {
logger.warn("Discarding an event, we don't have a call/peerConn", type);
return;
}
// Ignore remote echo
if (event.getContent().party_id === call.ourPartyId) return;
@@ -266,7 +275,7 @@ export class CallEventHandler {
case EventType.CallAnswer:
if (weSentTheEvent) {
if (call.state === CallState.Ringing) {
call.onAnsweredElsewhere(content);
call.onAnsweredElsewhere(content as MCallAnswer);
}
} else {
call.onAnswerReceived(event);
+38 -14
View File
@@ -1,6 +1,8 @@
// allow non-camelcase as these are events type that go onto the wire
/* eslint-disable camelcase */
import { CallErrorCode } from "./call";
// TODO: Change to "sdp_stream_metadata" when MSC3077 is merged
export const SDPStreamMetadataKey = "org.matrix.msc3077.sdp_stream_metadata";
@@ -19,11 +21,6 @@ export interface SDPStreamMetadata {
[key: string]: SDPStreamMetadataObject;
}
interface CallOfferAnswer {
type: string;
sdp: string;
}
export interface CallCapabilities {
'm.call.transferee': boolean;
'm.call.dtmf': boolean;
@@ -35,29 +32,56 @@ export interface CallReplacesTarget {
avatar_url: string;
}
export interface MCallAnswer {
answer: CallOfferAnswer;
capabilities: CallCapabilities;
export interface MCallBase {
call_id: string;
version: string | number;
party_id?: string;
}
export interface MCallAnswer extends MCallBase {
answer: RTCSessionDescription;
capabilities?: CallCapabilities;
[SDPStreamMetadataKey]: SDPStreamMetadata;
}
export interface MCallOfferNegotiate {
offer: CallOfferAnswer;
description: CallOfferAnswer;
export interface MCallSelectAnswer extends MCallBase {
selected_party_id: string;
}
export interface MCallInviteNegotiate extends MCallBase {
offer: RTCSessionDescription;
description: RTCSessionDescription;
lifetime: number;
capabilities: CallCapabilities;
capabilities?: CallCapabilities;
[SDPStreamMetadataKey]: SDPStreamMetadata;
}
export interface MCallSDPStreamMetadataChanged {
export interface MCallSDPStreamMetadataChanged extends MCallBase {
[SDPStreamMetadataKey]: SDPStreamMetadata;
}
export interface MCallReplacesEvent {
export interface MCallReplacesEvent extends MCallBase {
replacement_id: string;
target_user: CallReplacesTarget;
create_call: string;
await_call: string;
target_room: string;
}
export interface MCAllAssertedIdentity extends MCallBase {
asserted_identity: {
id: string;
display_name: string;
avatar_url: string;
};
}
export interface MCallCandidates extends MCallBase {
candidates: RTCIceCandidate[];
}
export interface MCallHangupReject extends MCallBase {
reason?: CallErrorCode;
}
/* eslint-enable camelcase */
+103 -2
View File
@@ -19,12 +19,25 @@ import { SDPStreamMetadataPurpose } from "./callEventTypes";
import { MatrixClient } from "../client";
import { RoomMember } from "../models/room-member";
const POLLING_INTERVAL = 250; // ms
const SPEAKING_THRESHOLD = -60; // dB
export enum CallFeedEvent {
NewStream = "new_stream",
MuteStateChanged = "mute_state_changed"
MuteStateChanged = "mute_state_changed",
VolumeChanged = "volume_changed",
Speaking = "speaking",
}
export class CallFeed extends EventEmitter {
private measuringVolumeActivity = false;
private audioContext: AudioContext;
private analyser: AnalyserNode;
private frequencyBinCount: Float32Array;
private speakingThreshold = SPEAKING_THRESHOLD;
private speaking = false;
private volumeLooperTimeout: number;
constructor(
public stream: MediaStream,
public userId: string,
@@ -35,6 +48,30 @@ export class CallFeed extends EventEmitter {
private videoMuted: boolean,
) {
super();
if (this.hasAudioTrack) {
this.initVolumeMeasuring();
}
}
private get hasAudioTrack(): boolean {
return this.stream.getAudioTracks().length > 0;
}
private initVolumeMeasuring(): void {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (!this.hasAudioTrack || !AudioContext) return;
this.audioContext = new AudioContext();
this.analyser = this.audioContext.createAnalyser();
this.analyser.fftSize = 512;
this.analyser.smoothingTimeConstant = 0.1;
const mediaStreamAudioSourceNode = this.audioContext.createMediaStreamSource(this.stream);
mediaStreamAudioSourceNode.connect(this.analyser);
this.frequencyBinCount = new Float32Array(this.analyser.frequencyBinCount);
}
/**
@@ -78,18 +115,82 @@ export class CallFeed extends EventEmitter {
* This method should be only used by MatrixCall.
* @param newStream new stream with which to replace the current one
*/
public setNewStream(newStream: MediaStream) {
public setNewStream(newStream: MediaStream): void {
this.stream = newStream;
this.emit(CallFeedEvent.NewStream, this.stream);
if (this.hasAudioTrack) {
this.initVolumeMeasuring();
} else {
this.measureVolumeActivity(false);
}
}
/**
* Set feed's internal audio mute state
* @param muted is the feed's audio muted?
*/
public setAudioMuted(muted: boolean): void {
this.audioMuted = muted;
this.emit(CallFeedEvent.MuteStateChanged, this.audioMuted, this.videoMuted);
}
/**
* Set feed's internal video mute state
* @param muted is the feed's video muted?
*/
public setVideoMuted(muted: boolean): void {
this.videoMuted = muted;
this.emit(CallFeedEvent.MuteStateChanged, this.audioMuted, this.videoMuted);
}
/**
* Starts emitting volume_changed events where the emitter value is in decibels
* @param enabled emit volume changes
*/
public measureVolumeActivity(enabled: boolean): void {
if (enabled) {
if (!this.audioContext || !this.analyser || !this.frequencyBinCount || !this.hasAudioTrack) return;
this.measuringVolumeActivity = true;
this.volumeLooper();
} else {
this.measuringVolumeActivity = false;
this.emit(CallFeedEvent.VolumeChanged, -Infinity);
}
}
public setSpeakingThreshold(threshold: number) {
this.speakingThreshold = threshold;
}
private volumeLooper(): void {
if (!this.analyser) return;
this.volumeLooperTimeout = setTimeout(() => {
if (!this.measuringVolumeActivity) return;
this.analyser.getFloatFrequencyData(this.frequencyBinCount);
let maxVolume = -Infinity;
for (let i = 0; i < this.frequencyBinCount.length; i++) {
if (this.frequencyBinCount[i] > maxVolume) {
maxVolume = this.frequencyBinCount[i];
}
}
this.emit(CallFeedEvent.VolumeChanged, maxVolume);
const newSpeaking = maxVolume > this.speakingThreshold;
if (this.speaking !== newSpeaking) {
this.speaking = newSpeaking;
this.emit(CallFeedEvent.Speaking, this.speaking);
}
this.volumeLooper();
}, POLLING_INTERVAL);
}
public dispose(): void {
clearTimeout(this.volumeLooperTimeout);
}
}
+200
View File
@@ -0,0 +1,200 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
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 "../logger";
export class MediaHandler {
private audioInput: string;
private videoInput: string;
private userMediaStreams: MediaStream[] = [];
private screensharingStreams: MediaStream[] = [];
/**
* Set an audio input device to use for MatrixCalls
* @param {string} deviceId the identifier for the device
* undefined treated as unset
*/
public setAudioInput(deviceId: string): void {
this.audioInput = deviceId;
}
/**
* Set a video input device to use for MatrixCalls
* @param {string} deviceId the identifier for the device
* undefined treated as unset
*/
public setVideoInput(deviceId: string): void {
this.videoInput = deviceId;
}
/**
* @returns {MediaStream} based on passed parameters
*/
public async getUserMediaStream(audio: boolean, video: boolean): Promise<MediaStream> {
let stream: MediaStream;
// Find a stream with matching tracks
const matchingStream = this.userMediaStreams.find((stream) => {
if (audio !== (stream.getAudioTracks().length > 0)) return false;
if (video !== (stream.getVideoTracks().length > 0)) return false;
});
if (matchingStream) {
logger.log("Cloning user media stream", matchingStream.id);
stream = matchingStream.clone();
} else {
const constraints = this.getUserMediaContraints(audio, video);
logger.log("Getting user media with constraints", constraints);
stream = await navigator.mediaDevices.getUserMedia(constraints);
}
this.userMediaStreams.push(stream);
return stream;
}
/**
* Stops all tracks on the provided usermedia stream
*/
public stopUserMediaStream(mediaStream: MediaStream) {
logger.debug("Stopping usermedia stream", mediaStream.id);
for (const track of mediaStream.getTracks()) {
track.stop();
}
const index = this.userMediaStreams.indexOf(mediaStream);
if (index !== -1) {
logger.debug("Splicing usermedia stream out stream array", mediaStream.id);
this.userMediaStreams.splice(index, 1);
}
}
/**
* @returns {MediaStream} based on passed parameters
*/
public async getScreensharingStream(desktopCapturerSourceId: string): Promise<MediaStream | null> {
let stream: MediaStream;
if (this.screensharingStreams.length === 0) {
const screenshareConstraints = this.getScreenshareContraints(desktopCapturerSourceId);
if (!screenshareConstraints) return null;
if (desktopCapturerSourceId) {
// We are using Electron
logger.debug("Getting screensharing stream using getUserMedia()", desktopCapturerSourceId);
stream = await navigator.mediaDevices.getUserMedia(screenshareConstraints);
} else {
// We are not using Electron
logger.debug("Getting screensharing stream using getDisplayMedia()");
stream = await navigator.mediaDevices.getDisplayMedia(screenshareConstraints);
}
} else {
const matchingStream = this.screensharingStreams[this.screensharingStreams.length - 1];
logger.log("Cloning screensharing stream", matchingStream.id);
stream = matchingStream.clone();
}
this.screensharingStreams.push(stream);
return stream;
}
/**
* Stops all tracks on the provided screensharing stream
*/
public stopScreensharingStream(mediaStream: MediaStream) {
logger.debug("Stopping screensharing stream", mediaStream.id);
for (const track of mediaStream.getTracks()) {
track.stop();
}
const index = this.screensharingStreams.indexOf(mediaStream);
if (index !== -1) {
logger.debug("Splicing screensharing stream out stream array", mediaStream.id);
this.screensharingStreams.splice(index, 1);
}
}
/**
* Stops all local media tracks
*/
public stopAllStreams() {
for (const stream of this.userMediaStreams) {
for (const track of stream.getTracks()) {
track.stop();
}
}
for (const stream of this.screensharingStreams) {
for (const track of stream.getTracks()) {
track.stop();
}
}
this.userMediaStreams = [];
this.screensharingStreams = [];
}
private getUserMediaContraints(audio: boolean, video: boolean): MediaStreamConstraints {
const isWebkit = !!navigator.webkitGetUserMedia;
return {
audio: audio
? {
deviceId: this.audioInput ? { ideal: this.audioInput } : undefined,
}
: false,
video: video
? {
deviceId: this.videoInput ? { ideal: this.videoInput } : undefined,
/* We want 640x360. Chrome will give it only if we ask exactly,
FF refuses entirely if we ask exactly, so have to ask for ideal
instead
XXX: Is this still true?
*/
width: isWebkit ? { exact: 640 } : { ideal: 640 },
height: isWebkit ? { exact: 360 } : { ideal: 360 },
}
: false,
};
}
private getScreenshareContraints(desktopCapturerSourceId?: string): DesktopCapturerConstraints {
if (desktopCapturerSourceId) {
logger.debug("Using desktop capturer source", desktopCapturerSourceId);
return {
audio: false,
video: {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: desktopCapturerSourceId,
},
},
};
} else {
logger.debug("Not using desktop capturer source");
return {
audio: false,
video: true,
};
}
}
}
+719 -697
View File
File diff suppressed because it is too large Load Diff