Compare commits

...

256 Commits

Author SHA1 Message Date
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
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
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
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
RiotRobot 67434bc5d4 v12.3.0-rc.2 2021-08-12 15:11:35 +01:00
RiotRobot 167183775e Prepare changelog for v12.3.0-rc.2 2021-08-12 15:11:35 +01:00
David Baker 4ce6313126 Merge pull request #1840 from matrix-org/dbkr/allchange_npm
Use changelog generator from npm
2021-08-12 15:02:12 +01:00
David Baker 1298ed61b6 Merge pull request #1842 from matrix-org/dbkr/fix_types_build
Fix the types in shipped package
2021-08-12 14:33:01 +01: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
David Baker 2bc1f2fe21 Apparently target matters when not emitting 2021-08-12 11:54:19 +01:00
David Baker b392dbf6f8 Fix the types building
1829 added the spec files to 'src', presumably because they just
were't type checked otherwise, but this chaqnged the implicit rootDir
so the lib directory we shipped has types in src/ and spec/ which
meant lib/index.d.ts was at lib/src/index.d.ts.

Specify the rootDir on the build tsconfig to put it back in the
right place, and also try to make the base tsconfig never emit
to avoid it ever generating .d.ts files in silly places. Move all
the optoins related to emitting to -build since they aren't relevant
in the main one.

Is this a sensible way to do this? It doesn't feel terribly elegant.

Fixes https://github.com/vector-im/element-web/issues/18503
Regressed in https://github.com/matrix-org/matrix-js-sdk/pull/1829
2021-08-12 11:46:49 +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
David Baker 6170796ed8 Use changelog generator from npm 2021-08-11 21:50:13 +01: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
RiotRobot 429f32dacb v12.3.0-rc.1 2021-08-11 15:41:40 +01:00
David Baker ad036104e6 Changelog 2021-08-11 15:41:00 +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
Matthew Hodgson 946dcd0301 Merge pull request #1838 from matrix-org/matthew/fix-turn
fix TURN by fixing regression preventing multiple ICE candidates from sending.
2021-08-11 11:35:36 +01:00
Matthew Hodgson 97d718e850 tab->space 2021-08-11 11:31:40 +01:00
Matthew Hodgson 913b2d148c clarify blank candidate comment 2021-08-11 11:30:42 +01:00
Matthew Hodgson 916f6a6126 fix TURN by fixing regression preventing multiple candidates from sending
In https://github.com/matrix-org/matrix-js-sdk/commit/eafecd36bc860e79d9aa280a7e60455dbd7c6f67#diff-df33fb6c18a9b5896cd500875824d6c10980d42c92b21cedac6d8f570a8d52b7L1639
the candidateSendTries reset was accidentally lost, meaning that we never send more than one m.call.candidates, breaking trickle ICE.

Also, we leak blank candidates out in m.call.candidates on Safari and FF, so fix that too.
2021-08-11 04:42:32 +01:00
David Baker 7dbee19456 Merge pull request #1837 from matrix-org/dbkr/fix_blank_membership_events
Fix blank profile in join events
2021-08-10 22:13:16 +01:00
David Baker 2375b00b52 more null checking 2021-08-10 22:09:01 +01:00
David Baker 13cdca028f Fix blank profile in join events
This definitely needs explanation, but hopefully the comment is sufficient.

Type: defect
Fixes https://github.com/vector-im/element-web/issues/18321
Regressed in https://github.com/matrix-org/matrix-js-sdk/pull/1765
2021-08-10 21:59:40 +01:00
David Baker 264636c879 Merge pull request #1835 from matrix-org/travis/fsdk/get-perm
Add support for reading permissions from tree spaces
2021-08-10 11:08:51 +01: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
David Baker 94726aa1d5 Merge pull request #1832 from matrix-org/dbkr/preview_changelog
Add changelog preview action
2021-08-09 20:21:22 +01:00
Travis Ralston 8da2f91e22 Add support for reading permissions from tree spaces
This was originally thought to be able to be performed in another way, but unfortunately isn't as easy as originally predicted.
2021-08-09 08:43:46 -06:00
Dariusz Niemczyk b03555a50b Merge pull request #1833 from SimonBrandner/fix/screensharingc-crash
Fix error on turning off screensharing
2021-08-08 21:08:52 +02:00
Šimon Brandner b61312feca Fix screensharing crash
We have to first stop the track and only then delete the feed as stopping the tracks depends on the feed being defined

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-08 11:10:08 +02:00
David Baker ea9459d330 Add changelog preview action 2021-08-06 23:05:00 +01:00
Travis Ralston 3edfe70a7b Merge pull request #1831 from matrix-org/travis/fsdk/expose-get-file
Expose a getFileEvent() function off a MSC3089Branch
2021-08-06 09:15:24 -06:00
Travis Ralston 5482a19979 Expose a getFileEvent() function off a MSC3089Branch 2021-08-06 09:09:03 -06:00
Michael Telatynski e1678aaf11 Merge pull request #1830 from matrix-org/t3chguy/js-docfix
Fix type documentation on MatrixClient::setRoomMutePushRule
2021-08-06 14:48:05 +01:00
Michael Telatynski 2bdb570f3c Fix type documentation on MatrixClient::setRoomMutePushRule 2021-08-06 09:38:50 +01:00
Germain 3723b2da39 Merge pull request #1829 from matrix-org/gsouquet/jest-typescript 2021-08-05 11:00:40 +01:00
Germain Souquet b63af00579 Exclude tests when building browserify package 2021-08-05 11:56:26 +02:00
David Baker 4ed6fbe5fe Merge pull request #1820 from SimonBrandner/fix/hangup-reason/18219
Send `user_hangup` reason if the opponent supports it
2021-08-05 10:41:28 +01:00
Germain Souquet 026260502b Fix linting issues in TypeScript test files 2021-08-05 11:37:27 +02:00
Germain Souquet c45bc91389 Include all TypeScript @types listed in project dependency
Fixes vector-im/element-web#17743

The package @types/jest was not discovered because if types is specified, only packages listed will be included in the global scope.
2021-08-05 11:16:28 +02:00
David Baker 69358f8372 Merge pull request #1825 from matrix-org/dbkr/codeowners
Add CODEOWNERS
2021-08-04 13:21:37 +01:00
David Baker 8800c6d2e0 Add CODEOWNERS
This should make github auto review request from the element-web team
2021-08-04 13:16:15 +01:00
David Baker b63de6a902 Merge pull request #1823 from matrix-org/dbkr/argh_the_linting
Update eslint plugin & fix silly indenting
2021-08-04 10:03:20 +01:00
David Baker b1cec7d6a5 Merge pull request #1824 from matrix-org/dbkr/allchange
Switch to new changelog generator
2021-08-04 10:03:08 +01:00
David Baker 08821e499c Switch to new changelog generator 2021-08-03 17:51:42 +01:00
David Baker 3ca84cfc49 Update eslint plugin & fix silly indenting
As per matrix-org/eslint-plugin-matrix-org#15
this caused a bunch of silly indenting to creep in, so this fixes it
back to the previous style.
2021-08-03 17:15:25 +01:00
David Baker be8ecce995 Merge pull request #1821 from SimonBrandner/task/cleanup
Clean-up some VoIP code
2021-08-03 13:18:47 +01:00
Šimon Brandner 775c3465bb Rename so that VS Code spell checker shutsup
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-03 10:05:05 +02:00
Šimon Brandner 7484e3c032 Improve wording
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-03 09:48:23 +02:00
Šimon Brandner b93e79274f Move for better organization
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-03 09:45:27 +02:00
Šimon Brandner fee6a96350 Rename for consistency
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-03 09:44:44 +02:00
Šimon Brandner ac2c09108f Improve styling a bit
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-03 09:42:20 +02:00
Šimon Brandner 5b13785115 Make a few things into one liners
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-03 09:40:04 +02:00
Šimon Brandner dd3c53edac Extract weSentTheEvent into a variable
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-03 09:37:41 +02:00
Šimon Brandner 904071ebe6 Extract type into a variable
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-03 09:33:40 +02:00
Šimon Brandner 7e0aeb425d Move things we can into a switch statement
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-03 09:31:06 +02:00
Šimon Brandner f4e985b5cc Fix typos
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-02 21:38:05 +02:00
Šimon Brandner 03134832e9 Standardise screensharing casing
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-02 21:30:33 +02:00
Šimon Brandner dbb0f66094 Remove unused mute state stuff
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-02 21:29:09 +02:00
Šimon Brandner 4631cd1fe3 Make localUsermediaStream into a getter
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-02 21:28:49 +02:00
Šimon Brandner bc07ad5909 Make localScreenSharingStream into a getter
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-02 21:25:55 +02:00
Šimon Brandner 8b0c4a0efb Merge remote-tracking branch 'upstream/develop' into task/cleanup
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-02 21:17:14 +02:00
Šimon Brandner eac77a6695 Remove config as it wasn't used
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-02 21:15:36 +02:00
Šimon Brandner aae8f1c706 screenSharingStream -> localScreenSharingStream
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-02 21:14:59 +02:00
Šimon Brandner 1e281f6838 localAVStream -> localUsermediaStream
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-02 21:02:09 +02:00
David Baker aef89e4a26 Merge pull request #1812 from SimonBrandner/feature/muting
Support for MSC3291: Muting in VoIP calls
2021-08-02 17:20:42 +01:00
RiotRobot 8a1bc98d4e Resetting package fields for development 2021-08-02 13:03:59 +01:00
RiotRobot 8b2d5959ec Merge branch 'master' into develop 2021-08-02 13:03:59 +01:00
RiotRobot 4891446dc3 v12.2.0 2021-08-02 13:00:06 +01:00
David Baker cace8f232f Changelog for v12.2.0 2021-08-02 12:45:25 +01:00
Michael Telatynski 88b8b24629 Merge room hierarchy pages 2021-07-30 11:01:08 +01:00
Šimon Brandner b15ba8c5e3 Send hangup reason if the opponent supports it
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-30 08:54:39 +02:00
Michael Telatynski 58c60d85e7 Refactor Space Hierarchy stuff in preparation for pagination 2021-07-29 17:35:17 +01:00
Hubert Chathi 529fe93ab1 Merge pull request #1819 from uhoreg/fix_aes_iv
Only clear bit 63 when we create the IV
2021-07-29 12:07:54 -04:00
Hubert Chathi 600438d02e only clear bit 63 when we create the IV
If we got the IV from somewhere else, we should just use it as-is, rather than
trying to re-clear the bit.
2021-07-29 11:58:08 -04:00
Michael Telatynski 21a357c433 fix typo 2021-07-29 15:00:39 +01:00
Michael Telatynski 9c05077c26 Merge pull request #1816 from matrix-org/t3chguy/hidden-names 2021-07-29 12:55:40 +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
Šimon Brandner e6696f78f4 Allow recieving unprefixed version of m.call.sdp_stream_metadata_changed
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-29 13:00:48 +02:00
Michael Telatynski f2da3ba6ae Apply hidden char check to rawDisplayName too 2021-07-29 11:00:30 +01:00
Šimon Brandner db5de4c6e4 Merge remote-tracking branch 'upstream/develop' into feature/muting 2021-07-29 09:10:38 +02:00
Šimon Brandner ca042b3647 Fix handling when remoteSDPStreamMetadata is null
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-28 20:12:21 +02:00
Šimon Brandner 87fd3521ea Fix types
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-28 20:11:51 +02:00
Šimon Brandner b3c66848e2 Handle missing params
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-28 19:06:24 +02:00
Šimon Brandner 606aa29381 Use recursivelyAssign
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-28 18:37:48 +02:00
Šimon Brandner 2d25150a21 Write tests for recursivelyAssign()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-28 17:57:16 +02:00
Šimon Brandner ca7a457094 Add recursivelyAssign()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-28 17:56:51 +02:00
Šimon Brandner 23ab3e3ec0 Test mute metadata gets mapped onto feeds
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-28 15:29:28 +02:00
Šimon Brandner b11a8459d8 Add a method for setting remoteSDPStreamMetadata
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-28 15:16:24 +02:00
Šimon Brandner 057eb0f2a5 Make mute state props mandetory
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-28 15:00:21 +02:00
Šimon Brandner 8de6c5aad1 Fix typo which caused all feeds to be muted by default
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-28 14:59:57 +02:00
Šimon Brandner 6fda2a0c57 Handle purpose changes
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-28 14:15:00 +02:00
David Baker efeeba265f Merge pull request #1813 from SimonBrandner/fix/call-view/18221
Handle DTMF support
2021-07-28 11:27:09 +01:00
Šimon Brandner 35ca7fdc48 Fix call transfer support
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-28 08:15:50 +02:00
Šimon Brandner 959e3f68a4 Handle DTMF support
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-27 17:51:13 +02:00
Šimon Brandner 19f90f56be Merge remote-tracking branch 'upstream/develop' into feature/muting 2021-07-27 16:40:07 +02:00
David Baker a69d9a9f20 Merge pull request #1685 from SimonBrandner/fix/12652/screen-share
Support for screen-sharing using multi-stream VoIP (MSC3077)
2021-07-27 15:36:40 +01:00
Michael Telatynski c5e113d0ca Merge pull request #1808 from matrix-org/t3chguy/ts/13 2021-07-27 15:36:21 +01:00
Šimon Brandner 1df90f8fc7 Basic implementation of MSC3291
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-27 14:31:13 +02:00
Šimon Brandner 8e6040ad6f Give CallFeed a mute state
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-27 14:23:58 +02:00
Šimon Brandner 21f107d734 Add types for MSC3291
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-27 14:19:54 +02:00
Šimon Brandner 019abc7ad8 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share 2021-07-25 08:13:23 +02:00
Michael Telatynski db4b6fe2af Update to beta better-docs to fix the gendocs issue 2021-07-24 12:10:45 +01:00
Michael Telatynski c340e6f1ba delint and fix tests 2021-07-24 12:01:23 +01:00
Michael Telatynski f227a307c1 Fix some confused signatures/types 2021-07-24 11:40:30 +01:00
Michael Telatynski 011a2acfd2 Convert crypto/store/* to Typescript 2021-07-24 11:35:30 +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
Šimon Brandner fcbbcc0398 Always return true for voice calls
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-22 09:24:59 +02:00
Šimon Brandner ceafb1c5d0 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share 2021-07-21 15:58:46 +02:00
Šimon Brandner 80d7c88b40 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share 2021-07-20 15:47:36 +02:00
Šimon Brandner bee840ce16 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share 2021-07-20 13:30:01 +02:00
Šimon Brandner 019e93e76c Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share 2021-07-16 21:38:10 +02:00
Šimon Brandner 1607ad2111 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share 2021-07-16 20:11:39 +02:00
Michael Telatynski f1a0c46a29 Deprecate groups APIs 2021-07-16 17:45:07 +01:00
Šimon Brandner 0318c65847 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share 2021-07-08 13:22:34 +02:00
Michael Telatynski e775bcac3c Convert SearchResult, InteractiveAuth, PushProcessor and Scheduler to Typescript 2021-07-07 11:08:54 +01:00
Šimon Brandner 641c852ee2 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share 2021-07-07 10:43:23 +02:00
Šimon Brandner ca37ae6794 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-06-09 19:20:21 +02:00
Šimon Brandner c1625e5c27 More styling :(
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-06-04 11:44:31 +02:00
Šimon Brandner 0e05f9fd73 Styling
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-06-04 11:41:35 +02:00
Šimon Brandner ef7595bb06 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-06-04 11:38:13 +02:00
Šimon Brandner 090c2c0891 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share 2021-06-04 08:27:46 +02:00
Šimon Brandner d8fd12103a Merge branch 'develop' into fix/12652/screen-share
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-06-02 16:09:11 +02:00
Šimon Brandner 5724462c2c Delint
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-28 17:45:52 +02:00
Šimon Brandner 0a0489750c Add missing space
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-28 17:37:55 +02:00
Šimon Brandner f46190509a Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-28 17:37:17 +02:00
Šimon Brandner 75321220fd Simplifie code - don't be an idiot
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-27 18:49:02 +02:00
Šimon Brandner 43198b0425 Disable RTX only for screen-sharing transceivers
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-27 18:41:48 +02:00
Šimon Brandner 4aa2c03ff0 Merge branch 'develop' into fix/12652/screen-share 2021-05-27 18:23:22 +02:00
Šimon Brandner db4c6af472 Fix an odd mistake
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-20 15:54:08 +02:00
Šimon Brandner 87689ca733 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share 2021-05-20 15:50:15 +02:00
Šimon Brandner a1a5d85979 Stop tracks only if disabling screen-sharing
The other thing lead to usermedia tracks being stopped when on hold

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-20 15:49:56 +02:00
Šimon Brandner b1459a43ef Also put getRidOfRTXCodecs() before createAnswer()
Just to be sure

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-20 15:11:23 +02:00
Šimon Brandner 8797381f44 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share 2021-05-19 21:07:04 +02:00
Šimon Brandner 92e89ffbcf Add getRidOfRTXCodecs() method
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-19 21:06:35 +02:00
Šimon Brandner c2da4376e0 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share 2021-05-19 15:21:00 +02:00
Šimon Brandner 78ebf8f117 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share 2021-05-19 08:46:54 +02:00
Šimon Brandner 5de189bfa3 Improve logging in pushRemoteFeed()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-15 11:06:04 +02:00
Šimon Brandner 44fc820f99 Make feed pushing methods more verbose
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-13 18:12:48 +02:00
Šimon Brandner 5a8e5a9785 Log local SDPStreamMetadata
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-13 13:43:44 +02:00
Šimon Brandner 960c5da3b2 Merge branch 'develop' into fix/12652/screen-share 2021-05-13 13:39:51 +02:00
Šimon Brandner 00a28e743d Move track.stop() to deleteFeedByStream()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-10 19:18:39 +02:00
Šimon Brandner 2596c25ccc Add missing semicolon
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-10 19:17:07 +02:00
Šimon Brandner 582aafa552 Simpler naming
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-10 19:12:33 +02:00
Šimon Brandner 61e7d4f807 Remove some leftovers from placeScreensharingCall()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-10 18:22:06 +02:00
Šimon Brandner f2c215311f If we can't get constraints don't error
We do this because it could mean the user just hasn't selected a window/screen

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-10 13:01:06 +02:00
Šimon Brandner a13cf0e1e0 Remove placeScreenSharingCall()
This method is quite problematic and doesn't have any benefits

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-10 12:27:21 +02:00
Šimon Brandner ff60bbac9d Remove import that was a mistake
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-10 07:35:25 +02:00
Šimon Brandner 16f569136b Simplifie and avoid repetation
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-09 18:48:08 +02:00
Šimon Brandner 27c172361f Add a log line to pushLocalFeed()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 20:03:47 +02:00
Šimon Brandner 1e0d6b9d4a Jest: should fallback to replaceTrack() if the other side doesn't support SPDStreamMetadata
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 15:50:20 +02:00
Šimon Brandner b67cd94ee2 Jest: should map SDPStreamMetadata to feeds
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 15:39:11 +02:00
Šimon Brandner c6764490c6 Use for loop
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 14:17:10 +02:00
Šimon Brandner 18580624e6 Use opponentSupportsSDPStreamMetadata()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 14:16:43 +02:00
Šimon Brandner 7b333a34b5 Warn level
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 14:16:30 +02:00
Šimon Brandner d0707e183d Make shift-click work again
This is VERY ugly but it works

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 10:02:27 +02:00
Šimon Brandner fa3b246de5 Add addToPeerConnection param
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 10:00:59 +02:00
Šimon Brandner df28d87d25 Remove log line
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 09:29:41 +02:00
Šimon Brandner fc68bb3ae0 Add ()!!!
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 09:29:31 +02:00
Šimon Brandner 82c530da95 Add setScreensharingEnabledWithoutMetadataSupport as a fallback()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 09:08:06 +02:00
Šimon Brandner d250e7387c Merge screenshare track into usermedia stream
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 09:02:01 +02:00
Šimon Brandner cbc74815d8 Use getScreensharingStream()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 08:46:29 +02:00
Šimon Brandner e9b802deb3 Use getScreensharingStream() in setScreensharingEnabled()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 08:03:45 +02:00
Šimon Brandner 377ca0c678 Add getScreensharingStream()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 08:01:56 +02:00
Šimon Brandner 972aef7a9d Merge feed delete methods and add sender arrays
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 07:40:46 +02:00
Šimon Brandner a35559be65 Add a method to start screensharing
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-07 21:33:05 +02:00
Šimon Brandner 0e6b43a769 Hook up methods to delete feeds
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-07 21:02:56 +02:00
Šimon Brandner 8c8a68d3ae Add methods to delete feeds
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-07 21:01:56 +02:00
Šimon Brandner 4d74b5cdad Send SDPStreamMetadata in negotiation response
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-07 20:57:45 +02:00
Šimon Brandner b1ace49f9a Add methods useful for screensharing
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-07 20:53:19 +02:00
Šimon Brandner 640d13af99 Set remoteSDPStreamMetadata in onNegotiateReceived()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-07 13:20:36 +02:00
Šimon Brandner 50e0f6353a Move adding tracks into pushLocalFeed()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-07 13:15:09 +02:00
Šimon Brandner 30f2263443 Rework pushing of remote feeds for MSC3077
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-07 11:18:25 +02:00
Šimon Brandner 25eb6de220 Send SDPStreamMetadata in answer
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-07 11:17:08 +02:00
Šimon Brandner cebdc44689 Set remoteSDPStreamMetadata from answer
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-07 11:11:31 +02:00
Šimon Brandner 23f5c2e03f Add a separate method to push local feed
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-07 10:43:36 +02:00
Šimon Brandner 4d4a6ede21 Use somicolons instead
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-07 10:41:59 +02:00
Šimon Brandner 6a920fe623 Get sdpStreamMetadata from invite
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-07 07:56:27 +02:00
Šimon Brandner 631faa2046 Send SDPStreamMetadata
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-06 17:18:30 +02:00
Šimon Brandner a4e853e1d4 Add types for MSC3077
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-06 17:15:04 +02:00
60 changed files with 3307 additions and 1585 deletions
+1
View File
@@ -0,0 +1 @@
* @matrix-org/element-web
+12
View File
@@ -0,0 +1,12 @@
name: Preview Changelog
on:
pull_request_target:
types: [ opened, edited, labeled ]
jobs:
changelog:
runs-on: ubuntu-latest
steps:
- name: Preview Changelog
uses: matrix-org/allchange@main
with:
ghToken: ${{ secrets.GITHUB_TOKEN }}
+49 -43
View File
@@ -1,53 +1,59 @@
Changes in [12.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v12.2.0-rc.1) (2021-07-27)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v12.1.0...v12.2.0-rc.1)
Changes in [12.4.0](https://github.com/vector-im/element-desktop/releases/tag/v12.4.0) (2021-08-31)
===================================================================================================
* Update ESLint config
[\#1810](https://github.com/matrix-org/matrix-js-sdk/pull/1810)
* Clean up Event.clearEvent handling to fix a bug where malformed events with
falsey content wouldn't be considered decrypted
[\#1807](https://github.com/matrix-org/matrix-js-sdk/pull/1807)
* Update matrix-org-eslint-plugin and tighten max warning limit
[\#1806](https://github.com/matrix-org/matrix-js-sdk/pull/1806)
* Initial MSC3244 support and type cleanup
[\#1745](https://github.com/matrix-org/matrix-js-sdk/pull/1745)
* Functional members
[\#1771](https://github.com/matrix-org/matrix-js-sdk/pull/1771)
* yarn upgrade
[\#1802](https://github.com/matrix-org/matrix-js-sdk/pull/1802)
## 🦖 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)).
* Send `user_hangup` reason if the opponent supports it ([\#1820](https://github.com/matrix-org/matrix-js-sdk/pull/1820)). Fixes vector-im/element-web#18219. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* 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-08-02)
===================================================================================================
## ✨ Features
* Improve calculateRoomName performances by using Intl.Collator
[\#1801](https://github.com/matrix-org/matrix-js-sdk/pull/1801)
* Conform to new typescript eslint rules
[\#1798](https://github.com/matrix-org/matrix-js-sdk/pull/1798)
* Actually print IndexedDB command error if there is one
[\#1799](https://github.com/matrix-org/matrix-js-sdk/pull/1799)
* Update eslint-plugin-matrix-org
[\#1796](https://github.com/matrix-org/matrix-js-sdk/pull/1796)
* Add minimal types for "notification settings" UI
[\#1778](https://github.com/matrix-org/matrix-js-sdk/pull/1778)
* Switch from `url` to URL constructor
[\#1791](https://github.com/matrix-org/matrix-js-sdk/pull/1791)
* Switch callEventHandler from listening on `event` to `Room.timeline`
[\#1789](https://github.com/matrix-org/matrix-js-sdk/pull/1789)
* Contributing guidelines for new changelog generation
[\#1787](https://github.com/matrix-org/matrix-js-sdk/pull/1787)
* Fix link to CONTRIBUTING.md
[\#1786](https://github.com/matrix-org/matrix-js-sdk/pull/1786)
* Use an unstable prefix for MSC2885: Hidden read receipts
[\#1788](https://github.com/matrix-org/matrix-js-sdk/pull/1788)
* Standardise spelling and casing of homeserver, identity server, and
integration manager
[\#1782](https://github.com/matrix-org/matrix-js-sdk/pull/1782)
* Convert CONTRIBUTING to markdown
[\#1785](https://github.com/matrix-org/matrix-js-sdk/pull/1785)
* Use webpack worker-loader instead of homegrown hack
[\#1780](https://github.com/matrix-org/matrix-js-sdk/pull/1780)
* Convert IndexedDB store & worker to Typescript
[\#1779](https://github.com/matrix-org/matrix-js-sdk/pull/1779)
* Expose MatrixEvent's internal clearEvent as a function
[\#1784](https://github.com/matrix-org/matrix-js-sdk/pull/1784)
* Improve and consolidate typing
[\#1777](https://github.com/matrix-org/matrix-js-sdk/pull/1777)
## 🐛 Bug Fixes
* Clean up Event.clearEvent handling to fix a bug where malformed events with falsey content wouldn't be considered decrypted
[\#1807](https://github.com/matrix-org/matrix-js-sdk/pull/1807)
* Standardise spelling and casing of homeserver, identity server, and integration manager
[\#1782](https://github.com/matrix-org/matrix-js-sdk/pull/1782)
Changes in [12.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v12.1.0) (2021-07-19)
==================================================================================================
+6 -5
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "12.2.0-rc.1",
"version": "12.4.0",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepublishOnly": "yarn build",
@@ -9,9 +9,9 @@
"clean": "rimraf lib dist",
"build": "yarn build:dev && yarn build:compile-browser && yarn build:minify-browser",
"build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
"build:types": "tsc --emitDeclarationOnly",
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
"build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
"build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig-build.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
"gendoc": "jsdoc -c jsdoc.json -P package.json",
"lint": "yarn lint:types && yarn lint:js",
@@ -81,14 +81,15 @@
"@types/request": "^2.48.5",
"@typescript-eslint/eslint-plugin": "^4.17.0",
"@typescript-eslint/parser": "^4.17.0",
"allchange": "^1.0.0",
"babel-jest": "^26.6.3",
"babelify": "^10.0.0",
"better-docs": "^2.3.2",
"better-docs": "^2.4.0-beta.9",
"browserify": "^17.0.0",
"docdash": "^1.2.0",
"eslint": "7.18.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#main",
"eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#2306b3d4da4eba908b256014b979f1d3d43d2945",
"exorcist": "^1.0.1",
"fake-indexeddb": "^3.1.2",
"jest": "^26.6.3",
+5 -15
View File
@@ -102,11 +102,6 @@ yarn cache clean
# Ensure all dependencies are updated
yarn install --ignore-scripts --pure-lockfile
if [ -z "$skip_changelog" ]; then
# update_changelog doesn't have a --version flag
update_changelog -h > /dev/null || (echo "github-changelog-generator is required: please install it"; exit)
fi
# Login and publish continues to use `npm`, as it seems to have more clearly
# defined options and semantics than `yarn` for writing to the registry.
if [ -z "$skip_npm" ]; then
@@ -133,14 +128,6 @@ if [ $prerelease -eq 1 ]; then
echo Making a PRE-RELEASE
fi
if [ -z "$skip_changelog" ]; then
if ! command -v update_changelog >/dev/null 2>&1; then
echo "release.sh requires github-changelog-generator. Try:" >&2
echo " pip install git+https://github.com/matrix-org/github-changelog-generator.git" >&2
exit 1
fi
fi
# we might already be on the release branch, in which case, yay
# If we're on any branch starting with 'release', we don't create
# a separate release branch (this allows us to use the same
@@ -156,7 +143,7 @@ fi
if [ -z "$skip_changelog" ]; then
echo "Generating changelog"
update_changelog -f "$changelog_file" "$release"
yarn run allchange "$release"
read -p "Edit $changelog_file manually, or press enter to continue " REPLY
if [ -n "$(git ls-files --modified $changelog_file)" ]; then
@@ -204,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
+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) {
+3 -4
View File
@@ -16,7 +16,6 @@ limitations under the License.
import { MatrixClient } from "../../../src";
import { Room } from "../../../src/models/room";
import { MatrixEvent } from "../../../src/models/event";
import { UNSTABLE_MSC3089_BRANCH } from "../../../src/@types/event";
import { EventTimelineSet } from "../../../src/models/event-timeline-set";
import { EventTimeline } from "../../../src/models/event-timeline";
@@ -25,7 +24,7 @@ import { MSC3089Branch } from "../../../src/models/MSC3089Branch";
describe("MSC3089Branch", () => {
let client: MatrixClient;
// @ts-ignore - TS doesn't know that this is a type
let indexEvent: MatrixEvent;
let indexEvent: any;
let branch: MSC3089Branch;
const branchRoomId = "!room:example.org";
@@ -47,10 +46,10 @@ describe("MSC3089Branch", () => {
}
},
};
indexEvent = {
indexEvent = ({
getRoomId: () => branchRoomId,
getStateKey: () => fileEventId,
};
});
branch = new MSC3089Branch(client, indexEvent);
});
+5 -5
View File
@@ -29,7 +29,7 @@ import { MatrixError } from "../../../src/http-api";
describe("MSC3089TreeSpace", () => {
let client: MatrixClient;
let room: Room;
let room: any;
let tree: MSC3089TreeSpace;
const roomId = "!tree:localhost";
const targetUser = "@target:example.org";
@@ -170,7 +170,7 @@ describe("MSC3089TreeSpace", () => {
expect(userIds).toMatchObject([target]);
return Promise.resolve();
});
client.invite = () => Promise.resolve(); // we're not testing this here - see other tests
client.invite = () => Promise.resolve({}); // we're not testing this here - see other tests
client.sendSharedHistoryKeys = sendKeysFn;
// Mock the history check as best as possible
@@ -198,7 +198,7 @@ describe("MSC3089TreeSpace", () => {
expect(userIds).toMatchObject([target]);
return Promise.resolve();
});
client.invite = () => Promise.resolve(); // we're not testing this here - see other tests
client.invite = () => Promise.resolve({}); // we're not testing this here - see other tests
client.sendSharedHistoryKeys = sendKeysFn;
const historyVis = "joined"; // NOTE: Changed.
@@ -446,9 +446,9 @@ describe("MSC3089TreeSpace", () => {
// Danger: these are partial implementations for testing purposes only
// @ts-ignore - "MatrixEvent is a value but used as a type", which is true but not important
let childState: { [roomId: string]: MatrixEvent[] } = {};
let childState: { [roomId: string]: any[] } = {};
// @ts-ignore - "MatrixEvent is a value but used as a type", which is true but not important
let parentState: MatrixEvent[] = [];
let parentState: any[] = [];
let parentRoom: Room;
let childTrees: MSC3089TreeSpace[];
let rooms: { [roomId: string]: Room };
+5 -5
View File
@@ -16,11 +16,13 @@ limitations under the License.
import { EventTimelineSet } from "../../src/models/event-timeline-set";
import { MatrixEvent } from "../../src/models/event";
import { Room } from "../../src/models/room";
import { Relations } from "../../src/models/relations";
describe("Relations", function() {
it("should deduplicate annotations", function() {
const relations = new Relations("m.annotation", "m.reaction");
const room = new Room("room123", null, null);
const relations = new Relations("m.annotation", "m.reaction", room);
// Create an instance of an annotation
const eventData = {
@@ -95,10 +97,8 @@ describe("Relations", function() {
});
// Stub the room
const room = {
getPendingEvent() { return null; },
getUnfilteredTimelineSet() { return null; },
};
const room = new Room("room123", null, null);
// Add the target event first, then the relation event
{
+64
View File
@@ -493,4 +493,68 @@ describe("utils", function() {
expect(deepSortedObjectEntries(input)).toMatchObject(output);
});
});
describe("recursivelyAssign", () => {
it("doesn't override with null/undefined", () => {
const result = utils.recursivelyAssign(
{
string: "Hello world",
object: {},
float: 0.1,
}, {
string: null,
object: undefined,
},
true,
);
expect(result).toStrictEqual({
string: "Hello world",
object: {},
float: 0.1,
});
});
it("assigns recursively", () => {
const result = utils.recursivelyAssign(
{
number: 42,
object: {
message: "Hello world",
day: "Monday",
langs: {
compiled: ["c++"],
},
},
thing: "string",
}, {
number: 2,
object: {
message: "How are you",
day: "Friday",
langs: {
compiled: ["c++", "c"],
},
},
thing: {
aSubThing: "something",
},
},
);
expect(result).toStrictEqual({
number: 2,
object: {
message: "How are you",
day: "Friday",
langs: {
compiled: ["c++", "c"],
},
},
thing: {
aSubThing: "something",
},
});
});
});
});
+65
View File
@@ -16,6 +16,7 @@ limitations under the License.
import { TestClient } from '../../TestClient';
import { MatrixCall, CallErrorCode, CallEvent } from '../../../src/webrtc/call';
import { SDPStreamMetadataKey, SDPStreamMetadataPurpose } from '../../../src/webrtc/callEventTypes';
const DUMMY_SDP = (
"v=0\r\n" +
@@ -298,4 +299,68 @@ describe('Call', function() {
// Hangup to stop timers
call.hangup(CallErrorCode.UserHangup, true);
});
it("should map SDPStreamMetadata to feeds", async () => {
const callPromise = call.placeVoiceCall();
await client.httpBackend.flush();
await callPromise;
call.getOpponentMember = () => {
return { userId: "@bob:bar.uk" };
};
await call.onAnswerReceived({
getContent: () => {
return {
version: 1,
call_id: call.callId,
party_id: 'party_id',
answer: {
sdp: DUMMY_SDP,
},
[SDPStreamMetadataKey]: {
"stream_id": {
purpose: SDPStreamMetadataPurpose.Usermedia,
audio_muted: true,
video_muted: false,
},
},
};
},
});
call.pushRemoteFeed({ id: "stream_id", getAudioTracks: () => ["track1"], getVideoTracks: () => ["track1"] });
const feed = call.getFeeds().find((feed) => feed.stream.id === "stream_id");
expect(feed?.purpose).toBe(SDPStreamMetadataPurpose.Usermedia);
expect(feed?.isAudioMuted()).toBeTruthy();
expect(feed?.isVideoMuted()).not.toBeTruthy();
});
it("should fallback to replaceTrack() if the other side doesn't support SPDStreamMetadata", async () => {
const callPromise = call.placeVoiceCall();
await client.httpBackend.flush();
await callPromise;
call.getOpponentMember = () => {
return { userId: "@bob:bar.uk" };
};
await call.onAnswerReceived({
getContent: () => {
return {
version: 1,
call_id: call.callId,
party_id: 'party_id',
answer: {
sdp: DUMMY_SDP,
},
};
},
});
call.setScreensharingEnabledWithoutMetadataSupport = jest.fn();
call.setScreensharingEnabled(true);
expect(call.setScreensharingEnabledWithoutMetadataSupport).toHaveBeenCalled();
});
});
+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;
+2
View File
@@ -53,6 +53,8 @@ export enum EventType {
CallReject = "m.call.reject",
CallSelectAnswer = "m.call.select_answer",
CallNegotiate = "m.call.negotiate",
CallSDPStreamMetadataChanged = "m.call.sdp_stream_metadata_changed",
CallSDPStreamMetadataChangedPrefix = "org.matrix.call.sdp_stream_metadata_changed",
CallReplaces = "m.call.replaces",
CallAssertedIdentity = "m.call.asserted_identity",
CallAssertedIdentityPrefix = "org.matrix.call.asserted_identity",
+8
View File
@@ -74,3 +74,11 @@ 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 */
+94 -19
View File
@@ -30,7 +30,7 @@ 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 { IActionsObject, PushProcessor } from "./pushprocessor";
import { AutoDiscovery } from "./autodiscovery";
import * as olmlib from "./crypto/olmlib";
import { decodeBase64, encodeBase64 } from "./crypto/olmlib";
@@ -89,9 +89,6 @@ import {
IRecoveryKey,
ISecretStorageKeyInfo,
} from "./crypto/api";
import { MemoryCryptoStore } from "./crypto/store/memory-crypto-store";
import { LocalStorageCryptoStore } from "./crypto/store/localStorage-crypto-store";
import { IndexedDBCryptoStore } from "./crypto/store/indexeddb-crypto-store";
import { SyncState } from "./sync.api";
import { EventTimelineSet } from "./models/event-timeline-set";
import { VerificationRequest } from "./crypto/verification/request/VerificationRequest";
@@ -143,13 +140,13 @@ 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";
export type Store = IStore;
export type SessionStore = WebStorageSessionStore;
export type CryptoStore = MemoryCryptoStore | LocalStorageCryptoStore | IndexedDBCryptoStore;
export type Callback = (err: Error | any | null, data?: any) => void;
export type ResetTimelineCallback = (roomId: string) => boolean;
@@ -524,7 +521,7 @@ interface IMessagesResponse {
state: IStateEvent[];
}
interface IRequestTokenResponse {
export interface IRequestTokenResponse {
sid: string;
submit_url?: string;
}
@@ -603,7 +600,7 @@ interface IUserDirectoryResponse {
limited: boolean;
}
interface IMyDevice {
export interface IMyDevice {
device_id: string;
display_name?: string;
last_seen_ip?: string;
@@ -2934,6 +2931,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);
@@ -2942,6 +2940,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();
@@ -4343,7 +4342,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));
}
@@ -5079,7 +5078,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",
@@ -5187,11 +5186,11 @@ export class MatrixClient extends EventEmitter {
* The operation also updates MatrixClient.pushRules at the end.
* @param {string} scope "global" or device-specific.
* @param {string} roomId the id of the room.
* @param {string} mute the mute state.
* @param {boolean} mute the mute state.
* @return {Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public setRoomMutePushRule(scope: string, roomId: string, mute: string): Promise<void> | void {
public setRoomMutePushRule(scope: string, roomId: string, mute: boolean): Promise<void> | void {
let deferred;
let hasDontNotifyRule;
@@ -7976,14 +7975,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,
@@ -7992,10 +7992,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,
});
@@ -8011,6 +8008,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
@@ -8063,7 +8114,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(
@@ -8088,6 +8139,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 });
@@ -8098,6 +8150,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 });
@@ -8113,6 +8166,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 });
@@ -8129,6 +8183,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(
@@ -8146,6 +8201,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 });
@@ -8156,6 +8212,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 });
@@ -8166,6 +8223,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 });
@@ -8177,6 +8235,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(
@@ -8191,6 +8250,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(
@@ -8206,6 +8266,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(
@@ -8222,6 +8283,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(
@@ -8237,6 +8299,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(
@@ -8253,6 +8316,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(
@@ -8268,6 +8332,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) {
@@ -8289,6 +8354,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
@@ -8309,6 +8375,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(
@@ -8323,6 +8390,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(
@@ -8336,6 +8404,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(
@@ -8349,6 +8418,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(
@@ -8361,6 +8431,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", {});
@@ -8373,6 +8444,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", {});
@@ -8393,6 +8465,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", {});
@@ -8406,6 +8479,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(
@@ -8570,6 +8644,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;
+10 -3
View File
@@ -28,10 +28,11 @@ import { decryptAES, encryptAES } from './aes';
import { PkSigning } from "@matrix-org/olm";
import { DeviceInfo } from "./deviceinfo";
import { SecretStorage } from "./SecretStorage";
import { CryptoStore, ICrossSigningKey, ISignedKey, MatrixClient } from "../client";
import { ICrossSigningKey, ISignedKey, MatrixClient } from "../client";
import { OlmDevice } from "./OlmDevice";
import { ICryptoCallbacks } from "../matrix";
import { ISignatures } from "../@types/signed";
import { CryptoStore } from "./store/base";
const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;
@@ -47,6 +48,12 @@ export interface ICacheCallbacks {
storeCrossSigningKeyCache?(type: string, key: Uint8Array): Promise<void>;
}
export interface ICrossSigningInfo {
keys: Record<string, ICrossSigningKey>;
firstUse: boolean;
crossSigningVerifiedBefore: boolean;
}
export class CrossSigningInfo extends EventEmitter {
public keys: Record<string, ICrossSigningKey> = {};
public firstUse = true;
@@ -75,7 +82,7 @@ export class CrossSigningInfo extends EventEmitter {
super();
}
public static fromStorage(obj: object, userId: string): CrossSigningInfo {
public static fromStorage(obj: ICrossSigningInfo, userId: string): CrossSigningInfo {
const res = new CrossSigningInfo(userId);
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {
@@ -85,7 +92,7 @@ export class CrossSigningInfo extends EventEmitter {
return res;
}
public toStorage(): object {
public toStorage(): ICrossSigningInfo {
return {
keys: this.keys,
firstUse: this.firstUse,
+11 -22
View File
@@ -24,12 +24,13 @@ import { EventEmitter } from 'events';
import { logger } from '../logger';
import { DeviceInfo, IDevice } from './deviceinfo';
import { CrossSigningInfo } from './CrossSigning';
import { CrossSigningInfo, ICrossSigningInfo } from './CrossSigning';
import * as olmlib from './olmlib';
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { chunkPromises, defer, IDeferred, sleep } from '../utils';
import { MatrixClient, CryptoStore } from "../client";
import { MatrixClient } from "../client";
import { OlmDevice } from "./OlmDevice";
import { CryptoStore } from "./store/base";
/* State transition diagram for DeviceList.deviceTrackingStatus
*
@@ -52,7 +53,7 @@ import { OlmDevice } from "./OlmDevice";
*/
// constants for DeviceList.deviceTrackingStatus
enum TrackingStatus {
export enum TrackingStatus {
NotTracked,
PendingDownload,
DownloadInProgress,
@@ -65,32 +66,22 @@ export type DeviceInfoMap = Record<string, Record<string, DeviceInfo>>;
* @alias module:crypto/DeviceList
*/
export class DeviceList extends EventEmitter {
// userId -> {
// deviceId -> {
// [device info]
// }
// }
private devices: Record<string, Record<string, IDevice>> = {};
private devices: { [userId: string]: { [deviceId: string]: IDevice } } = {};
// userId -> {
// [key info]
// }
public crossSigningInfo: Record<string, object> = {};
public crossSigningInfo: { [userId: string]: ICrossSigningInfo } = {};
// map of identity keys to the user who owns it
private userByIdentityKey: Record<string, string> = {};
// which users we are tracking device status for.
// userId -> TRACKING_STATUS_*
private deviceTrackingStatus: Record<string, TrackingStatus> = {}; // loaded from storage in load()
private deviceTrackingStatus: { [userId: string]: TrackingStatus } = {}; // loaded from storage in load()
// The 'next_batch' sync token at the point the data was written,
// ie. a token representing the point immediately after the
// moment represented by the snapshot in the db.
private syncToken: string = null;
// userId -> promise
private keyDownloadsInProgressByUser: Record<string, Promise<void>> = {};
private keyDownloadsInProgressByUser: { [userId: string]: Promise<void> } = {};
// Set whenever changes are made other than setting the sync token
private dirty = false;
@@ -375,7 +366,7 @@ export class DeviceList extends EventEmitter {
return CrossSigningInfo.fromStorage(this.crossSigningInfo[userId], userId);
}
public storeCrossSigningForUser(userId: string, info: CrossSigningInfo): void {
public storeCrossSigningForUser(userId: string, info: ICrossSigningInfo): void {
this.crossSigningInfo[userId] = info;
this.dirty = true;
}
@@ -603,7 +594,7 @@ export class DeviceList extends EventEmitter {
}
}
public setRawStoredCrossSigningForUser(userId: string, info: object): void {
public setRawStoredCrossSigningForUser(userId: string, info: ICrossSigningInfo): void {
this.crossSigningInfo[userId] = info;
}
@@ -864,9 +855,7 @@ class DeviceListUpdateSerialiser {
crossSigning.setKeys(crossSigningResponse);
this.deviceList.setRawStoredCrossSigningForUser(
userId, crossSigning.toStorage(),
);
this.deviceList.setRawStoredCrossSigningForUser(userId, crossSigning.toStorage());
// NB. Unlike most events in the js-sdk, this one is internal to the
// js-sdk and is not re-emitted
+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 {
+1 -1
View File
@@ -949,7 +949,7 @@ OlmDevice.prototype.getOutboundGroupSessionKey = function(sessionId) {
* data stored in the session store about an inbound group session
*
* @typedef {Object} InboundGroupSessionData
* @property {string} room_Id
* @property {string} room_id
* @property {string} session pickled Olm.InboundGroupSession
* @property {Object<string, string>} keysClaimed
* @property {Array<string>} forwardingCurve25519KeyChain Devices involved in forwarding
+9 -13
View File
@@ -15,9 +15,9 @@ limitations under the License.
*/
import { logger } from '../logger';
import { CryptoStore, MatrixClient } from "../client";
import { MatrixClient } from "../client";
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "./index";
import { OutgoingRoomKeyRequest } from './store/base';
import { CryptoStore, OutgoingRoomKeyRequest } from './store/base';
import { EventType } from "../@types/event";
/**
@@ -137,9 +137,7 @@ export class OutgoingRoomKeyRequestManager {
recipients: IRoomKeyRequestRecipient[],
resend = false,
): Promise<void> {
const req = await this.cryptoStore.getOutgoingRoomKeyRequest(
requestBody,
);
const req = await this.cryptoStore.getOutgoingRoomKeyRequest(requestBody);
if (!req) {
await this.cryptoStore.getOrAddOutgoingRoomKeyRequest({
requestBody: requestBody,
@@ -237,10 +235,10 @@ export class OutgoingRoomKeyRequestManager {
* @returns {Promise} resolves when the request has been updated in our
* pending list.
*/
public cancelRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<void> {
public cancelRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<unknown> {
return this.cryptoStore.getOutgoingRoomKeyRequest(
requestBody,
).then((req) => {
).then((req): unknown => {
if (!req) {
// no request was made for this key
return;
@@ -263,9 +261,7 @@ export class OutgoingRoomKeyRequestManager {
'deleting unnecessary room key request for ' +
stringifyRequestBody(requestBody),
);
return this.cryptoStore.deleteOutgoingRoomKeyRequest(
req.requestId, RoomKeyRequestState.Unsent,
);
return this.cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId, RoomKeyRequestState.Unsent);
case RoomKeyRequestState.Sent: {
// send a cancellation.
@@ -325,7 +321,7 @@ export class OutgoingRoomKeyRequestManager {
* @return {Promise} resolves to a list of all the
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
*/
public getOutgoingSentRoomKeyRequest(userId: string, deviceId: string): OutgoingRoomKeyRequest[] {
public getOutgoingSentRoomKeyRequest(userId: string, deviceId: string): Promise<OutgoingRoomKeyRequest[]> {
return this.cryptoStore.getOutgoingRoomKeyRequestsByTarget(userId, deviceId, [RoomKeyRequestState.Sent]);
}
@@ -415,7 +411,7 @@ export class OutgoingRoomKeyRequestManager {
}
// given a RoomKeyRequest, send it and update the request record
private sendOutgoingRoomKeyRequest(req: OutgoingRoomKeyRequest): Promise<void> {
private sendOutgoingRoomKeyRequest(req: OutgoingRoomKeyRequest): Promise<unknown> {
logger.log(
`Requesting keys for ${stringifyRequestBody(req.requestBody)}` +
` from ${stringifyRecipientList(req.recipients)}` +
@@ -441,7 +437,7 @@ export class OutgoingRoomKeyRequestManager {
// Given a RoomKeyRequest, cancel it and delete the request record unless
// andResend is set, in which case transition to UNSENT.
private sendOutgoingRoomKeyRequestCancellation(req: OutgoingRoomKeyRequest, andResend = false): Promise<void> {
private sendOutgoingRoomKeyRequestCancellation(req: OutgoingRoomKeyRequest, andResend = false): Promise<unknown> {
logger.log(
`Sending cancellation for key request for ` +
`${stringifyRequestBody(req.requestBody)} to ` +
+1 -1
View File
@@ -20,8 +20,8 @@ limitations under the License.
* Manages the list of encrypted rooms
*/
import { CryptoStore } from './store/base';
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { CryptoStore } from "../client";
/* eslint-disable camelcase */
export interface IRoomEncryption {
+10 -10
View File
@@ -50,12 +50,12 @@ async function encryptNode(data: string, key: Uint8Array, name: string, ivStr?:
iv = decodeBase64(ivStr);
} else {
iv = crypto.randomBytes(16);
}
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
// (which would mean we wouldn't be able to decrypt on Android). The loss
// of a single bit of iv is a price we have to pay.
iv[8] &= 0x7f;
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
// (which would mean we wouldn't be able to decrypt on Android). The loss
// of a single bit of iv is a price we have to pay.
iv[8] &= 0x7f;
}
const [aesKey, hmacKey] = deriveKeysNode(key, name);
@@ -137,12 +137,12 @@ async function encryptBrowser(data: string, key: Uint8Array, name: string, ivStr
} else {
iv = new Uint8Array(16);
window.crypto.getRandomValues(iv);
}
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
// (which would mean we wouldn't be able to decrypt on Android). The loss
// of a single bit of iv is a price we have to pay.
iv[8] &= 0x7f;
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
// (which would mean we wouldn't be able to decrypt on Android). The loss
// of a single bit of iv is a price we have to pay.
iv[8] &= 0x7f;
}
const [aesKey, hmacKey] = await deriveKeysBrowser(key, name);
const encodedData = new TextEncoder().encode(data);
+1 -1
View File
@@ -61,7 +61,7 @@ interface IBlockedMap {
};
}
interface IOlmDevice<T = DeviceInfo> {
export interface IOlmDevice<T = DeviceInfo> {
userId: string;
deviceInfo: T;
}
+3 -4
View File
@@ -59,12 +59,13 @@ import { IStore } from "../store";
import { Room } from "../models/room";
import { RoomMember } from "../models/room-member";
import { MatrixEvent } from "../models/event";
import { MatrixClient, IKeysUploadResponse, SessionStore, CryptoStore, ISignedKey } from "../client";
import { MatrixClient, IKeysUploadResponse, SessionStore, ISignedKey } from "../client";
import type { EncryptionAlgorithm, DecryptionAlgorithm } from "./algorithms/base";
import type { IRoomEncryption, RoomList } from "./RoomList";
import { IRecoveryKey, IEncryptedEventInfo } from "./api";
import { IKeyBackupInfo } from "./keybackup";
import { ISyncStateData } from "../sync";
import { CryptoStore } from "./store/base";
const DeviceVerification = DeviceInfo.DeviceVerification;
@@ -1410,9 +1411,7 @@ export class Crypto extends EventEmitter {
crossSigning.updateCrossSigningVerifiedBefore(
this.checkUserTrust(userId).isCrossSigningVerified(),
);
this.deviceList.setRawStoredCrossSigningForUser(
userId, crossSigning.toStorage(),
);
this.deviceList.setRawStoredCrossSigningForUser(userId, crossSigning.toStorage());
}
this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId));
+163 -3
View File
@@ -1,5 +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 { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
import { RoomKeyRequestState } from "../OutgoingRoomKeyRequestManager";
import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { TrackingStatus } from "../DeviceList";
import { IRoomEncryption } from "../RoomList";
import { IDevice } from "../deviceinfo";
import { ICrossSigningInfo } from "../CrossSigning";
import { PrefixedLogger } from "../../logger";
import { InboundGroupSessionData } from "../../@types/partials";
import { IEncryptedPayload } from "../aes";
/**
* Internal module. Defintions for storage for the crypto module
* Internal module. Definitions for storage for the crypto module
*
* @module
*/
@@ -9,9 +37,141 @@
*
* @interface CryptoStore
*/
export interface CryptoStore {
startup(): Promise<CryptoStore>;
deleteAllData(): Promise<void>;
getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest>;
getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null>;
getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null>;
getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]>;
getOutgoingRoomKeyRequestsByTarget(
userId: string,
deviceId: string,
wantedStates: number[],
): Promise<OutgoingRoomKeyRequest[]>;
updateOutgoingRoomKeyRequest(
requestId: string,
expectedState: number,
updates: Partial<OutgoingRoomKeyRequest>,
): Promise<OutgoingRoomKeyRequest | null>;
deleteOutgoingRoomKeyRequest(requestId: string, expectedState: number): Promise<OutgoingRoomKeyRequest | null>;
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
import { RoomKeyRequestState } from "../OutgoingRoomKeyRequestManager";
// Olm Account
getAccount(txn: unknown, func: (accountPickle: string) => void);
storeAccount(txn: unknown, accountPickle: string): void;
getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey>) => void): void;
getSecretStorePrivateKey(txn: unknown, func: (key: IEncryptedPayload | null) => void, type: string): void;
storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void;
storeSecretStorePrivateKey(txn: unknown, type: string, key: IEncryptedPayload): void;
// Olm Sessions
countEndToEndSessions(txn: unknown, func: (count: number) => void): void;
getEndToEndSession(
deviceKey: string,
sessionId: string,
txn: unknown,
func: (session: ISessionInfo) => void,
): void;
getEndToEndSessions(
deviceKey: string,
txn: unknown,
func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
): void;
getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void;
storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void;
storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void>;
getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null>;
filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]>;
// Inbound Group Sessions
getEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
txn: unknown,
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
): void;
getAllEndToEndInboundGroupSessions(
txn: unknown,
func: (session: ISession | null) => void,
): void;
addEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: unknown,
): void;
storeEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: unknown,
): void;
storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key: string,
sessionId: string,
sessionData: IWithheld,
txn: unknown,
): void;
// Device Data
getEndToEndDeviceData(txn: unknown, func: (deviceData: IDeviceData | null) => void): void;
storeEndToEndDeviceData(deviceData: IDeviceData, txn: unknown): void;
storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: unknown): void;
getEndToEndRooms(txn: unknown, func: (rooms: Record<string, IRoomEncryption>) => void): void;
getSessionsNeedingBackup(limit: number): Promise<ISession[]>;
countSessionsNeedingBackup(txn?: unknown): Promise<number>;
unmarkSessionsNeedingBackup(sessions: ISession[], txn?: unknown): Promise<void>;
markSessionsNeedingBackup(sessions: ISession[], txn?: unknown): Promise<void>;
addSharedHistoryInboundGroupSession(roomId: string, senderKey: string, sessionId: string, txn?: unknown): void;
getSharedHistoryInboundGroupSessions(
roomId: string,
txn?: IDBTransaction,
): Promise<[senderKey: string, sessionId: string][]>;
// Session key backups
doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T, log?: PrefixedLogger): Promise<T>;
}
export type Mode = "readonly" | "readwrite";
export interface ISession {
senderKey: string;
sessionId: string;
sessionData?: InboundGroupSessionData;
}
export interface ISessionInfo {
deviceKey?: string;
sessionId?: string;
session?: string;
lastReceivedMessageTs?: number;
}
export interface IDeviceData {
devices: {
[ userId: string ]: {
[ deviceId: string ]: IDevice;
};
};
trackingStatus: {
[ userId: string ]: TrackingStatus;
};
crossSigningInfo?: Record<string, ICrossSigningInfo>;
syncToken?: string;
}
export interface IProblem {
type: string;
fixed: boolean;
time: number;
}
export interface IWithheld {
// eslint-disable-next-line camelcase
room_id: string;
code: string;
reason: string;
}
/**
* Represents an outgoing room key request
@@ -1,7 +1,5 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,8 +14,24 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { logger } from '../../logger';
import { logger, PrefixedLogger } from '../../logger';
import * as utils from "../../utils";
import {
CryptoStore,
IDeviceData,
IProblem,
ISession,
ISessionInfo,
IWithheld,
Mode,
OutgoingRoomKeyRequest,
} from "./base";
import { IRoomKeyRequestBody } from "../index";
import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { InboundGroupSessionData } from "../../@types/partials";
import { IEncryptedPayload } from "../aes";
export const VERSION = 10;
const PROFILE_TRANSACTIONS = false;
@@ -29,23 +43,31 @@ const PROFILE_TRANSACTIONS = false;
*
* @implements {module:crypto/store/base~CryptoStore}
*/
export class Backend {
export class Backend implements CryptoStore {
private nextTxnId = 0;
/**
* @param {IDBDatabase} db
*/
constructor(db) {
this._db = db;
this._nextTxnId = 0;
constructor(private db: IDBDatabase) {
// make sure we close the db on `onversionchange` - otherwise
// attempts to delete the database will block (and subsequent
// attempts to re-create it will also block).
db.onversionchange = (ev) => {
logger.log(`versionchange for indexeddb ${this._dbName}: closing`);
db.onversionchange = () => {
logger.log(`versionchange for indexeddb ${this.db.name}: closing`);
db.close();
};
}
public async startup(): Promise<CryptoStore> {
// No work to do, as the startup is done by the caller (e.g IndexedDBCryptoStore)
// by passing us a ready IDBDatabase instance
return this;
}
public async deleteAllData(): Promise<void> {
throw Error("This is not implemented, call IDBFactory::deleteDatabase(dbName) instead.");
}
/**
* Look for an existing outgoing room key request, and if none is found,
* add a new one
@@ -56,11 +78,11 @@ export class Backend {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the
* same instance as passed in, or the existing one.
*/
getOrAddOutgoingRoomKeyRequest(request) {
public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> {
const requestBody = request.requestBody;
return new Promise((resolve, reject) => {
const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite");
const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite");
txn.onerror = reject;
// first see if we already have an entry for this request.
@@ -99,9 +121,9 @@ export class Backend {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* not found
*/
getOutgoingRoomKeyRequest(requestBody) {
public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null> {
return new Promise((resolve, reject) => {
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
txn.onerror = reject;
this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
@@ -122,7 +144,12 @@ export class Backend {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* not found.
*/
_getOutgoingRoomKeyRequest(txn, requestBody, callback) {
// eslint-disable-next-line @typescript-eslint/naming-convention
private _getOutgoingRoomKeyRequest(
txn: IDBTransaction,
requestBody: IRoomKeyRequestBody,
callback: (req: OutgoingRoomKeyRequest | null) => void,
): void {
const store = txn.objectStore("outgoingRoomKeyRequests");
const idx = store.index("session");
@@ -131,8 +158,8 @@ export class Backend {
requestBody.session_id,
]);
cursorReq.onsuccess = (ev) => {
const cursor = ev.target.result;
cursorReq.onsuccess = () => {
const cursor = cursorReq.result;
if (!cursor) {
// no match found
callback(null);
@@ -162,7 +189,7 @@ export class Backend {
* there are no pending requests in those states. If there are multiple
* requests in those states, an arbitrary one is chosen.
*/
getOutgoingRoomKeyRequestByState(wantedStates) {
public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null> {
if (wantedStates.length === 0) {
return Promise.resolve(null);
}
@@ -195,7 +222,7 @@ export class Backend {
cursorReq.onsuccess = onsuccess;
}
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
const store = txn.objectStore("outgoingRoomKeyRequests");
const wantedState = wantedStates[stateIndex];
@@ -210,19 +237,23 @@ export class Backend {
* @param {Number} wantedState
* @return {Promise<Array<*>>} All elements in a given state
*/
getAllOutgoingRoomKeyRequestsByState(wantedState) {
public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]> {
return new Promise((resolve, reject) => {
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
const store = txn.objectStore("outgoingRoomKeyRequests");
const index = store.index("state");
const request = index.getAll(wantedState);
request.onsuccess = (ev) => resolve(ev.target.result);
request.onerror = (ev) => reject(ev.target.error);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
public getOutgoingRoomKeyRequestsByTarget(
userId: string,
deviceId: string,
wantedStates: number[],
): Promise<OutgoingRoomKeyRequest[]> {
let stateIndex = 0;
const results = [];
@@ -248,7 +279,7 @@ export class Backend {
}
}
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
const store = txn.objectStore("outgoingRoomKeyRequests");
const wantedState = wantedStates[stateIndex];
@@ -270,7 +301,11 @@ export class Backend {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
* updated request, or null if no matching row was found
*/
updateOutgoingRoomKeyRequest(requestId, expectedState, updates) {
public updateOutgoingRoomKeyRequest(
requestId: string,
expectedState: number,
updates: Partial<OutgoingRoomKeyRequest>,
): Promise<OutgoingRoomKeyRequest | null> {
let result = null;
function onsuccess(ev) {
@@ -291,9 +326,8 @@ export class Backend {
result = data;
}
const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite");
const cursorReq = txn.objectStore("outgoingRoomKeyRequests")
.openCursor(requestId);
const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite");
const cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId);
cursorReq.onsuccess = onsuccess;
return promiseifyTxn(txn).then(() => result);
}
@@ -307,12 +341,14 @@ export class Backend {
*
* @returns {Promise} resolves once the operation is completed
*/
deleteOutgoingRoomKeyRequest(requestId, expectedState) {
const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite");
const cursorReq = txn.objectStore("outgoingRoomKeyRequests")
.openCursor(requestId);
cursorReq.onsuccess = (ev) => {
const cursor = ev.target.result;
public deleteOutgoingRoomKeyRequest(
requestId: string,
expectedState: number,
): Promise<OutgoingRoomKeyRequest | null> {
const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite");
const cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId);
cursorReq.onsuccess = () => {
const cursor = cursorReq.result;
if (!cursor) {
return;
}
@@ -326,12 +362,12 @@ export class Backend {
}
cursor.delete();
};
return promiseifyTxn(txn);
return promiseifyTxn<OutgoingRoomKeyRequest | null>(txn);
}
// Olm Account
getAccount(txn, func) {
public getAccount(txn: IDBTransaction, func: (accountPickle: string) => void): void {
const objectStore = txn.objectStore("account");
const getReq = objectStore.get("-");
getReq.onsuccess = function() {
@@ -343,12 +379,12 @@ export class Backend {
};
}
storeAccount(txn, newData) {
public storeAccount(txn: IDBTransaction, accountPickle: string): void {
const objectStore = txn.objectStore("account");
objectStore.put(newData, "-");
objectStore.put(accountPickle, "-");
}
getCrossSigningKeys(txn, func) {
public getCrossSigningKeys(txn: IDBTransaction, func: (keys: Record<string, ICrossSigningKey>) => void): void {
const objectStore = txn.objectStore("account");
const getReq = objectStore.get("crossSigningKeys");
getReq.onsuccess = function() {
@@ -360,7 +396,11 @@ export class Backend {
};
}
getSecretStorePrivateKey(txn, func, type) {
public getSecretStorePrivateKey(
txn: IDBTransaction,
func: (key: IEncryptedPayload | null) => void,
type: string,
): void {
const objectStore = txn.objectStore("account");
const getReq = objectStore.get(`ssss_cache:${type}`);
getReq.onsuccess = function() {
@@ -372,19 +412,19 @@ export class Backend {
};
}
storeCrossSigningKeys(txn, keys) {
public storeCrossSigningKeys(txn: IDBTransaction, keys: Record<string, ICrossSigningKey>): void {
const objectStore = txn.objectStore("account");
objectStore.put(keys, "crossSigningKeys");
}
storeSecretStorePrivateKey(txn, type, key) {
public storeSecretStorePrivateKey(txn: IDBTransaction, type: string, key: IEncryptedPayload): void {
const objectStore = txn.objectStore("account");
objectStore.put(key, `ssss_cache:${type}`);
}
// Olm Sessions
countEndToEndSessions(txn, func) {
public countEndToEndSessions(txn: IDBTransaction, func: (count: number) => void): void {
const objectStore = txn.objectStore("sessions");
const countReq = objectStore.count();
countReq.onsuccess = function() {
@@ -396,7 +436,11 @@ export class Backend {
};
}
getEndToEndSessions(deviceKey, txn, func) {
public getEndToEndSessions(
deviceKey: string,
txn: IDBTransaction,
func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
): void {
const objectStore = txn.objectStore("sessions");
const idx = objectStore.index("deviceKey");
const getReq = idx.openCursor(deviceKey);
@@ -419,7 +463,12 @@ export class Backend {
};
}
getEndToEndSession(deviceKey, sessionId, txn, func) {
public getEndToEndSession(
deviceKey: string,
sessionId: string,
txn: IDBTransaction,
func: (sessions: { [ sessionId: string ]: ISessionInfo }) => void,
): void {
const objectStore = txn.objectStore("sessions");
const getReq = objectStore.get([deviceKey, sessionId]);
getReq.onsuccess = function() {
@@ -438,7 +487,7 @@ export class Backend {
};
}
getAllEndToEndSessions(txn, func) {
public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo) => void): void {
const objectStore = txn.objectStore("sessions");
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
@@ -456,7 +505,12 @@ export class Backend {
};
}
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
public storeEndToEndSession(
deviceKey: string,
sessionId: string,
sessionInfo: ISessionInfo,
txn: IDBTransaction,
): void {
const objectStore = txn.objectStore("sessions");
objectStore.put({
deviceKey,
@@ -466,8 +520,8 @@ export class Backend {
});
}
async storeEndToEndSessionProblem(deviceKey, type, fixed) {
const txn = this._db.transaction("session_problems", "readwrite");
public async storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
const txn = this.db.transaction("session_problems", "readwrite");
const objectStore = txn.objectStore("session_problems");
objectStore.put({
deviceKey,
@@ -478,13 +532,13 @@ export class Backend {
return promiseifyTxn(txn);
}
async getEndToEndSessionProblem(deviceKey, timestamp) {
public async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
let result;
const txn = this._db.transaction("session_problems", "readwrite");
const txn = this.db.transaction("session_problems", "readwrite");
const objectStore = txn.objectStore("session_problems");
const index = objectStore.index("deviceKey");
const req = index.getAll(deviceKey);
req.onsuccess = (event) => {
req.onsuccess = () => {
const problems = req.result;
if (!problems.length) {
result = null;
@@ -511,14 +565,14 @@ export class Backend {
}
// FIXME: we should probably prune this when devices get deleted
async filterOutNotifiedErrorDevices(devices) {
const txn = this._db.transaction("notified_error_devices", "readwrite");
public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
const txn = this.db.transaction("notified_error_devices", "readwrite");
const objectStore = txn.objectStore("notified_error_devices");
const ret = [];
const ret: IOlmDevice[] = [];
await Promise.all(devices.map((device) => {
return new Promise((resolve) => {
return new Promise<void>((resolve) => {
const { userId, deviceInfo } = device;
const getReq = objectStore.get([userId, deviceInfo.deviceId]);
getReq.onsuccess = function() {
@@ -536,9 +590,14 @@ export class Backend {
// Inbound group sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
let session = false;
let withheld = false;
public getEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
txn: IDBTransaction,
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
): void {
let session: InboundGroupSessionData | boolean = false;
let withheld: IWithheld | boolean = false;
const objectStore = txn.objectStore("inbound_group_sessions");
const getReq = objectStore.get([senderCurve25519Key, sessionId]);
getReq.onsuccess = function() {
@@ -549,7 +608,7 @@ export class Backend {
session = null;
}
if (withheld !== false) {
func(session, withheld);
func(session as InboundGroupSessionData, withheld as IWithheld);
}
} catch (e) {
abortWithException(txn, e);
@@ -566,7 +625,7 @@ export class Backend {
withheld = null;
}
if (session !== false) {
func(session, withheld);
func(session as InboundGroupSessionData, withheld as IWithheld);
}
} catch (e) {
abortWithException(txn, e);
@@ -574,7 +633,7 @@ export class Backend {
};
}
getAllEndToEndInboundGroupSessions(txn, func) {
public getAllEndToEndInboundGroupSessions(txn: IDBTransaction, func: (session: ISession | null) => void): void {
const objectStore = txn.objectStore("inbound_group_sessions");
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
@@ -600,7 +659,12 @@ export class Backend {
};
}
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
public addEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: IDBTransaction,
): void {
const objectStore = txn.objectStore("inbound_group_sessions");
const addReq = objectStore.add({
senderCurve25519Key, sessionId, session: sessionData,
@@ -623,23 +687,31 @@ export class Backend {
};
}
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
public storeEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: IDBTransaction,
): void {
const objectStore = txn.objectStore("inbound_group_sessions");
objectStore.put({
senderCurve25519Key, sessionId, session: sessionData,
});
}
storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
) {
public storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key: string,
sessionId: string,
sessionData: IWithheld,
txn: IDBTransaction,
): void {
const objectStore = txn.objectStore("inbound_group_sessions_withheld");
objectStore.put({
senderCurve25519Key, sessionId, session: sessionData,
});
}
getEndToEndDeviceData(txn, func) {
public getEndToEndDeviceData(txn: IDBTransaction, func: (deviceData: IDeviceData | null) => void): void {
const objectStore = txn.objectStore("device_data");
const getReq = objectStore.get("-");
getReq.onsuccess = function() {
@@ -651,24 +723,24 @@ export class Backend {
};
}
storeEndToEndDeviceData(deviceData, txn) {
public storeEndToEndDeviceData(deviceData: IDeviceData, txn: IDBTransaction): void {
const objectStore = txn.objectStore("device_data");
objectStore.put(deviceData, "-");
}
storeEndToEndRoom(roomId, roomInfo, txn) {
public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: IDBTransaction): void {
const objectStore = txn.objectStore("rooms");
objectStore.put(roomInfo, roomId);
}
getEndToEndRooms(txn, func) {
public getEndToEndRooms(txn: IDBTransaction, func: (rooms: Record<string, IRoomEncryption>) => void): void {
const rooms = {};
const objectStore = txn.objectStore("rooms");
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
const cursor = getReq.result;
if (cursor) {
rooms[cursor.key] = cursor.value;
rooms[cursor.key as string] = cursor.value;
cursor.continue();
} else {
try {
@@ -682,11 +754,11 @@ export class Backend {
// session backups
getSessionsNeedingBackup(limit) {
public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
return new Promise((resolve, reject) => {
const sessions = [];
const txn = this._db.transaction(
const txn = this.db.transaction(
["sessions_needing_backup", "inbound_group_sessions"],
"readonly",
);
@@ -716,9 +788,9 @@ export class Backend {
});
}
countSessionsNeedingBackup(txn) {
public countSessionsNeedingBackup(txn?: IDBTransaction): Promise<number> {
if (!txn) {
txn = this._db.transaction("sessions_needing_backup", "readonly");
txn = this.db.transaction("sessions_needing_backup", "readonly");
}
const objectStore = txn.objectStore("sessions_needing_backup");
return new Promise((resolve, reject) => {
@@ -728,12 +800,12 @@ export class Backend {
});
}
unmarkSessionsNeedingBackup(sessions, txn) {
public async unmarkSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
if (!txn) {
txn = this._db.transaction("sessions_needing_backup", "readwrite");
txn = this.db.transaction("sessions_needing_backup", "readwrite");
}
const objectStore = txn.objectStore("sessions_needing_backup");
return Promise.all(sessions.map((session) => {
await Promise.all(sessions.map((session) => {
return new Promise((resolve, reject) => {
const req = objectStore.delete([session.senderKey, session.sessionId]);
req.onsuccess = resolve;
@@ -742,12 +814,12 @@ export class Backend {
}));
}
markSessionsNeedingBackup(sessions, txn) {
public async markSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
if (!txn) {
txn = this._db.transaction("sessions_needing_backup", "readwrite");
txn = this.db.transaction("sessions_needing_backup", "readwrite");
}
const objectStore = txn.objectStore("sessions_needing_backup");
return Promise.all(sessions.map((session) => {
await Promise.all(sessions.map((session) => {
return new Promise((resolve, reject) => {
const req = objectStore.put({
senderCurve25519Key: session.senderKey,
@@ -759,9 +831,14 @@ export class Backend {
}));
}
addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn) {
public addSharedHistoryInboundGroupSession(
roomId: string,
senderKey: string,
sessionId: string,
txn?: IDBTransaction,
): void {
if (!txn) {
txn = this._db.transaction(
txn = this.db.transaction(
"shared_history_inbound_group_sessions", "readwrite",
);
}
@@ -774,9 +851,12 @@ export class Backend {
};
}
getSharedHistoryInboundGroupSessions(roomId, txn) {
public getSharedHistoryInboundGroupSessions(
roomId: string,
txn?: IDBTransaction,
): Promise<[senderKey: string, sessionId: string][]> {
if (!txn) {
txn = this._db.transaction(
txn = this.db.transaction(
"shared_history_inbound_group_sessions", "readonly",
);
}
@@ -791,16 +871,21 @@ export class Backend {
});
}
doTxn(mode, stores, func, log = logger) {
public doTxn<T>(
mode: Mode,
stores: Iterable<string>,
func: (txn: IDBTransaction) => T,
log: PrefixedLogger = logger,
): Promise<T> {
let startTime;
let description;
if (PROFILE_TRANSACTIONS) {
const txnId = this._nextTxnId++;
const txnId = this.nextTxnId++;
startTime = Date.now();
description = `${mode} crypto store transaction ${txnId} in ${stores}`;
log.debug(`Starting ${description}`);
}
const txn = this._db.transaction(stores, mode);
const txn = this.db.transaction(stores, mode);
const promise = promiseifyTxn(txn);
const result = func(txn);
if (PROFILE_TRANSACTIONS) {
@@ -818,7 +903,7 @@ export class Backend {
}
}
export function upgradeDatabase(db, oldVersion) {
export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
logger.log(
`Upgrading IndexedDBCryptoStore from version ${oldVersion}`
+ ` to ${VERSION}`,
@@ -874,7 +959,7 @@ export function upgradeDatabase(db, oldVersion) {
// Expand as needed.
}
function createDatabase(db) {
function createDatabase(db: IDBDatabase): void {
const outgoingRoomKeyRequestsStore =
db.createObjectStore("outgoingRoomKeyRequests", { keyPath: "requestId" });
@@ -887,15 +972,19 @@ function createDatabase(db) {
outgoingRoomKeyRequestsStore.createIndex("state", "state");
}
interface IWrappedIDBTransaction extends IDBTransaction {
_mx_abortexception: Error; // eslint-disable-line camelcase
}
/*
* Aborts a transaction with a given exception
* The transaction promise will be rejected with this exception.
*/
function abortWithException(txn, e) {
function abortWithException(txn: IDBTransaction, e: Error) {
// We cheekily stick our exception onto the transaction object here
// We could alternatively make the thing we pass back to the app
// an object containing the transaction and exception.
txn._mx_abortexception = e;
(txn as IWrappedIDBTransaction)._mx_abortexception = e;
try {
txn.abort();
} catch (e) {
@@ -904,28 +993,28 @@ function abortWithException(txn, e) {
}
}
function promiseifyTxn(txn) {
function promiseifyTxn<T>(txn: IDBTransaction): Promise<T> {
return new Promise((resolve, reject) => {
txn.oncomplete = () => {
if (txn._mx_abortexception !== undefined) {
reject(txn._mx_abortexception);
if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
reject((txn as IWrappedIDBTransaction)._mx_abortexception);
}
resolve();
resolve(null);
};
txn.onerror = (event) => {
if (txn._mx_abortexception !== undefined) {
reject(txn._mx_abortexception);
if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
reject((txn as IWrappedIDBTransaction)._mx_abortexception);
} else {
logger.log("Error performing indexeddb txn", event);
reject(event.target.error);
reject(txn.error);
}
};
txn.onabort = (event) => {
if (txn._mx_abortexception !== undefined) {
reject(txn._mx_abortexception);
if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
reject((txn as IWrappedIDBTransaction)._mx_abortexception);
} else {
logger.log("Error performing indexeddb txn", event);
reject(event.target.error);
reject(txn.error);
}
};
});
@@ -1,7 +1,5 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,12 +14,28 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { logger } from '../../logger';
import { logger, PrefixedLogger } from '../../logger';
import { LocalStorageCryptoStore } from './localStorage-crypto-store';
import { MemoryCryptoStore } from './memory-crypto-store';
import * as IndexedDBCryptoStoreBackend from './indexeddb-crypto-store-backend';
import { InvalidCryptoStoreError } from '../../errors';
import * as IndexedDBHelpers from "../../indexeddb-helpers";
import {
CryptoStore,
IDeviceData,
IProblem,
ISession,
ISessionInfo,
IWithheld,
Mode,
OutgoingRoomKeyRequest,
} from "./base";
import { IRoomKeyRequestBody } from "../index";
import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { InboundGroupSessionData } from "../../@types/partials";
import { IEncryptedPayload } from "../aes";
/**
* Internal module. indexeddb storage for e2e.
@@ -35,23 +49,30 @@ import * as IndexedDBHelpers from "../../indexeddb-helpers";
*
* @implements {module:crypto/store/base~CryptoStore}
*/
export class IndexedDBCryptoStore {
export class IndexedDBCryptoStore implements CryptoStore {
public static STORE_ACCOUNT = 'account';
public static STORE_SESSIONS = 'sessions';
public static STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
public static STORE_INBOUND_GROUP_SESSIONS_WITHHELD = 'inbound_group_sessions_withheld';
public static STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS = 'shared_history_inbound_group_sessions';
public static STORE_DEVICE_DATA = 'device_data';
public static STORE_ROOMS = 'rooms';
public static STORE_BACKUP = 'sessions_needing_backup';
public static exists(indexedDB: IDBFactory, dbName: string): Promise<boolean> {
return IndexedDBHelpers.exists(indexedDB, dbName);
}
private backendPromise: Promise<CryptoStore> = null;
private backend: CryptoStore = null;
/**
* Create a new IndexedDBCryptoStore
*
* @param {IDBFactory} indexedDB global indexedDB instance
* @param {string} dbName name of db to connect to
*/
constructor(indexedDB, dbName) {
this._indexedDB = indexedDB;
this._dbName = dbName;
this._backendPromise = null;
this._backend = null;
}
static exists(indexedDB, dbName) {
return IndexedDBHelpers.exists(indexedDB, dbName);
}
constructor(private readonly indexedDB: IDBFactory, private readonly dbName: string) {}
/**
* Ensure the database exists and is up-to-date, or fall back to
@@ -62,25 +83,23 @@ export class IndexedDBCryptoStore {
* @return {Promise} resolves to either an IndexedDBCryptoStoreBackend.Backend,
* or a MemoryCryptoStore
*/
startup() {
if (this._backendPromise) {
return this._backendPromise;
public startup(): Promise<CryptoStore> {
if (this.backendPromise) {
return this.backendPromise;
}
this._backendPromise = new Promise((resolve, reject) => {
if (!this._indexedDB) {
this.backendPromise = new Promise<CryptoStore>((resolve, reject) => {
if (!this.indexedDB) {
reject(new Error('no indexeddb support available'));
return;
}
logger.log(`connecting to indexeddb ${this._dbName}`);
logger.log(`connecting to indexeddb ${this.dbName}`);
const req = this._indexedDB.open(
this._dbName, IndexedDBCryptoStoreBackend.VERSION,
);
const req = this.indexedDB.open(this.dbName, IndexedDBCryptoStoreBackend.VERSION);
req.onupgradeneeded = (ev) => {
const db = ev.target.result;
const db = req.result;
const oldVersion = ev.oldVersion;
IndexedDBCryptoStoreBackend.upgradeDatabase(db, oldVersion);
};
@@ -93,13 +112,13 @@ export class IndexedDBCryptoStore {
req.onerror = (ev) => {
logger.log("Error connecting to indexeddb", ev);
reject(ev.target.error);
reject(req.error);
};
req.onsuccess = (r) => {
const db = r.target.result;
req.onsuccess = () => {
const db = req.result;
logger.log(`connected to indexeddb ${this._dbName}`);
logger.log(`connected to indexeddb ${this.dbName}`);
resolve(new IndexedDBCryptoStoreBackend.Backend(db));
};
}).then((backend) => {
@@ -114,9 +133,7 @@ export class IndexedDBCryptoStore {
],
(txn) => {
backend.getEndToEndInboundGroupSession('', '', txn, () => {});
}).then(() => {
return backend;
},
}).then(() => backend,
);
}).catch((e) => {
if (e.name === 'VersionError') {
@@ -126,7 +143,7 @@ export class IndexedDBCryptoStore {
throw new InvalidCryptoStoreError(InvalidCryptoStoreError.TOO_NEW);
}
logger.warn(
`unable to connect to indexeddb ${this._dbName}` +
`unable to connect to indexeddb ${this.dbName}` +
`: falling back to localStorage store: ${e}`,
);
@@ -139,10 +156,11 @@ export class IndexedDBCryptoStore {
return new MemoryCryptoStore();
}
}).then(backend => {
this._backend = backend;
this.backend = backend;
return backend as CryptoStore;
});
return this._backendPromise;
return this.backendPromise;
}
/**
@@ -150,15 +168,15 @@ export class IndexedDBCryptoStore {
*
* @returns {Promise} resolves when the store has been cleared.
*/
deleteAllData() {
return new Promise((resolve, reject) => {
if (!this._indexedDB) {
public deleteAllData(): Promise<void> {
return new Promise<void>((resolve, reject) => {
if (!this.indexedDB) {
reject(new Error('no indexeddb support available'));
return;
}
logger.log(`Removing indexeddb instance: ${this._dbName}`);
const req = this._indexedDB.deleteDatabase(this._dbName);
logger.log(`Removing indexeddb instance: ${this.dbName}`);
const req = this.indexedDB.deleteDatabase(this.dbName);
req.onblocked = () => {
logger.log(
@@ -168,11 +186,11 @@ export class IndexedDBCryptoStore {
req.onerror = (ev) => {
logger.log("Error deleting data from indexeddb", ev);
reject(ev.target.error);
reject(req.error);
};
req.onsuccess = () => {
logger.log(`Removed indexeddb instance: ${this._dbName}`);
logger.log(`Removed indexeddb instance: ${this.dbName}`);
resolve();
};
}).catch((e) => {
@@ -193,8 +211,8 @@ export class IndexedDBCryptoStore {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the
* same instance as passed in, or the existing one.
*/
getOrAddOutgoingRoomKeyRequest(request) {
return this._backend.getOrAddOutgoingRoomKeyRequest(request);
public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> {
return this.backend.getOrAddOutgoingRoomKeyRequest(request);
}
/**
@@ -207,8 +225,8 @@ export class IndexedDBCryptoStore {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* not found
*/
getOutgoingRoomKeyRequest(requestBody) {
return this._backend.getOutgoingRoomKeyRequest(requestBody);
public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null> {
return this.backend.getOutgoingRoomKeyRequest(requestBody);
}
/**
@@ -221,8 +239,8 @@ export class IndexedDBCryptoStore {
* there are no pending requests in those states. If there are multiple
* requests in those states, an arbitrary one is chosen.
*/
getOutgoingRoomKeyRequestByState(wantedStates) {
return this._backend.getOutgoingRoomKeyRequestByState(wantedStates);
public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null> {
return this.backend.getOutgoingRoomKeyRequestByState(wantedStates);
}
/**
@@ -232,8 +250,8 @@ export class IndexedDBCryptoStore {
* @param {Number} wantedState
* @return {Promise<Array<*>>} Returns an array of requests in the given state
*/
getAllOutgoingRoomKeyRequestsByState(wantedState) {
return this._backend.getAllOutgoingRoomKeyRequestsByState(wantedState);
public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]> {
return this.backend.getAllOutgoingRoomKeyRequestsByState(wantedState);
}
/**
@@ -246,8 +264,12 @@ export class IndexedDBCryptoStore {
* @return {Promise} resolves to a list of all the
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
*/
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
return this._backend.getOutgoingRoomKeyRequestsByTarget(
public getOutgoingRoomKeyRequestsByTarget(
userId: string,
deviceId: string,
wantedStates: number[],
): Promise<OutgoingRoomKeyRequest[]> {
return this.backend.getOutgoingRoomKeyRequestsByTarget(
userId, deviceId, wantedStates,
);
}
@@ -264,8 +286,12 @@ export class IndexedDBCryptoStore {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
* updated request, or null if no matching row was found
*/
updateOutgoingRoomKeyRequest(requestId, expectedState, updates) {
return this._backend.updateOutgoingRoomKeyRequest(
public updateOutgoingRoomKeyRequest(
requestId: string,
expectedState: number,
updates: Partial<OutgoingRoomKeyRequest>,
): Promise<OutgoingRoomKeyRequest | null> {
return this.backend.updateOutgoingRoomKeyRequest(
requestId, expectedState, updates,
);
}
@@ -279,8 +305,11 @@ export class IndexedDBCryptoStore {
*
* @returns {Promise} resolves once the operation is completed
*/
deleteOutgoingRoomKeyRequest(requestId, expectedState) {
return this._backend.deleteOutgoingRoomKeyRequest(requestId, expectedState);
public deleteOutgoingRoomKeyRequest(
requestId: string,
expectedState: number,
): Promise<OutgoingRoomKeyRequest | null> {
return this.backend.deleteOutgoingRoomKeyRequest(requestId, expectedState);
}
// Olm Account
@@ -292,8 +321,8 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn().
* @param {function(string)} func Called with the account pickle
*/
getAccount(txn, func) {
this._backend.getAccount(txn, func);
public getAccount(txn: IDBTransaction, func: (accountPickle: string) => void) {
this.backend.getAccount(txn, func);
}
/**
@@ -301,10 +330,10 @@ export class IndexedDBCryptoStore {
* This requires an active transaction. See doTxn().
*
* @param {*} txn An active transaction. See doTxn().
* @param {string} newData The new account pickle to store.
* @param {string} accountPickle The new account pickle to store.
*/
storeAccount(txn, newData) {
this._backend.storeAccount(txn, newData);
public storeAccount(txn: IDBTransaction, accountPickle: string): void {
this.backend.storeAccount(txn, accountPickle);
}
/**
@@ -315,8 +344,8 @@ export class IndexedDBCryptoStore {
* @param {function(string)} func Called with the account keys object:
* { key_type: base64 encoded seed } where key type = user_signing_key_seed or self_signing_key_seed
*/
getCrossSigningKeys(txn, func) {
this._backend.getCrossSigningKeys(txn, func);
public getCrossSigningKeys(txn: IDBTransaction, func: (keys: Record<string, ICrossSigningKey>) => void): void {
this.backend.getCrossSigningKeys(txn, func);
}
/**
@@ -324,8 +353,12 @@ export class IndexedDBCryptoStore {
* @param {function(string)} func Called with the private key
* @param {string} type A key type
*/
getSecretStorePrivateKey(txn, func, type) {
this._backend.getSecretStorePrivateKey(txn, func, type);
public getSecretStorePrivateKey(
txn: IDBTransaction,
func: (key: IEncryptedPayload | null) => void,
type: string,
): void {
this.backend.getSecretStorePrivateKey(txn, func, type);
}
/**
@@ -334,8 +367,8 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn().
* @param {string} keys keys object as getCrossSigningKeys()
*/
storeCrossSigningKeys(txn, keys) {
this._backend.storeCrossSigningKeys(txn, keys);
public storeCrossSigningKeys(txn: IDBTransaction, keys: Record<string, ICrossSigningKey>): void {
this.backend.storeCrossSigningKeys(txn, keys);
}
/**
@@ -345,8 +378,8 @@ export class IndexedDBCryptoStore {
* @param {string} type The type of cross-signing private key to store
* @param {string} key keys object as getCrossSigningKeys()
*/
storeSecretStorePrivateKey(txn, type, key) {
this._backend.storeSecretStorePrivateKey(txn, type, key);
public storeSecretStorePrivateKey(txn: IDBTransaction, type: string, key: IEncryptedPayload): void {
this.backend.storeSecretStorePrivateKey(txn, type, key);
}
// Olm sessions
@@ -356,8 +389,8 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn().
* @param {function(int)} func Called with the count of sessions
*/
countEndToEndSessions(txn, func) {
this._backend.countEndToEndSessions(txn, func);
public countEndToEndSessions(txn: IDBTransaction, func: (count: number) => void): void {
this.backend.countEndToEndSessions(txn, func);
}
/**
@@ -372,8 +405,13 @@ export class IndexedDBCryptoStore {
* timestamp in milliseconds at which the session last received
* a message.
*/
getEndToEndSession(deviceKey, sessionId, txn, func) {
this._backend.getEndToEndSession(deviceKey, sessionId, txn, func);
public getEndToEndSession(
deviceKey: string,
sessionId: string,
txn: IDBTransaction,
func: (sessions: { [ sessionId: string ]: ISessionInfo }) => void,
): void {
this.backend.getEndToEndSession(deviceKey, sessionId, txn, func);
}
/**
@@ -387,8 +425,12 @@ export class IndexedDBCryptoStore {
* timestamp in milliseconds at which the session last received
* a message.
*/
getEndToEndSessions(deviceKey, txn, func) {
this._backend.getEndToEndSessions(deviceKey, txn, func);
public getEndToEndSessions(
deviceKey: string,
txn: IDBTransaction,
func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
): void {
this.backend.getEndToEndSessions(deviceKey, txn, func);
}
/**
@@ -398,8 +440,8 @@ export class IndexedDBCryptoStore {
* an object with, deviceKey, lastReceivedMessageTs, sessionId
* and session keys.
*/
getAllEndToEndSessions(txn, func) {
this._backend.getAllEndToEndSessions(txn, func);
public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo) => void): void {
this.backend.getAllEndToEndSessions(txn, func);
}
/**
@@ -409,22 +451,25 @@ export class IndexedDBCryptoStore {
* @param {string} sessionInfo Session information object
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
this._backend.storeEndToEndSession(
deviceKey, sessionId, sessionInfo, txn,
);
public storeEndToEndSession(
deviceKey: string,
sessionId: string,
sessionInfo: ISessionInfo,
txn: IDBTransaction,
): void {
this.backend.storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn);
}
storeEndToEndSessionProblem(deviceKey, type, fixed) {
return this._backend.storeEndToEndSessionProblem(deviceKey, type, fixed);
public storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
return this.backend.storeEndToEndSessionProblem(deviceKey, type, fixed);
}
getEndToEndSessionProblem(deviceKey, timestamp) {
return this._backend.getEndToEndSessionProblem(deviceKey, timestamp);
public getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
return this.backend.getEndToEndSessionProblem(deviceKey, timestamp);
}
filterOutNotifiedErrorDevices(devices) {
return this._backend.filterOutNotifiedErrorDevices(devices);
public filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
return this.backend.filterOutNotifiedErrorDevices(devices);
}
// Inbound group sessions
@@ -438,10 +483,13 @@ export class IndexedDBCryptoStore {
* @param {function(object)} func Called with A map from sessionId
* to Base64 end-to-end session.
*/
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
this._backend.getEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, txn, func,
);
public getEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
txn: IDBTransaction,
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
): void {
this.backend.getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func);
}
/**
@@ -451,8 +499,11 @@ export class IndexedDBCryptoStore {
* in the store with an object having keys {senderKey, sessionId,
* sessionData}, then once with null to indicate the end of the list.
*/
getAllEndToEndInboundGroupSessions(txn, func) {
this._backend.getAllEndToEndInboundGroupSessions(txn, func);
public getAllEndToEndInboundGroupSessions(
txn: IDBTransaction,
func: (session: ISession | null) => void,
): void {
this.backend.getAllEndToEndInboundGroupSessions(txn, func);
}
/**
@@ -464,10 +515,13 @@ export class IndexedDBCryptoStore {
* @param {object} sessionData The session data structure
* @param {*} txn An active transaction. See doTxn().
*/
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
this._backend.addEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, sessionData, txn,
);
public addEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: IDBTransaction,
): void {
this.backend.addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
}
/**
@@ -479,18 +533,22 @@ export class IndexedDBCryptoStore {
* @param {object} sessionData The session data structure
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
this._backend.storeEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, sessionData, txn,
);
public storeEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: IDBTransaction,
): void {
this.backend.storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
}
storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
) {
this._backend.storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
);
public storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key: string,
sessionId: string,
sessionData: IWithheld,
txn: IDBTransaction,
): void {
this.backend.storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn);
}
// End-to-end device tracking
@@ -505,8 +563,8 @@ export class IndexedDBCryptoStore {
* @param {Object} deviceData
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndDeviceData(deviceData, txn) {
this._backend.storeEndToEndDeviceData(deviceData, txn);
public storeEndToEndDeviceData(deviceData: IDeviceData, txn: IDBTransaction): void {
this.backend.storeEndToEndDeviceData(deviceData, txn);
}
/**
@@ -516,8 +574,8 @@ export class IndexedDBCryptoStore {
* @param {function(Object)} func Function called with the
* device data
*/
getEndToEndDeviceData(txn, func) {
this._backend.getEndToEndDeviceData(txn, func);
public getEndToEndDeviceData(txn: IDBTransaction, func: (deviceData: IDeviceData | null) => void): void {
this.backend.getEndToEndDeviceData(txn, func);
}
// End to End Rooms
@@ -528,8 +586,8 @@ export class IndexedDBCryptoStore {
* @param {object} roomInfo The end-to-end info for the room.
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndRoom(roomId, roomInfo, txn) {
this._backend.storeEndToEndRoom(roomId, roomInfo, txn);
public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: IDBTransaction): void {
this.backend.storeEndToEndRoom(roomId, roomInfo, txn);
}
/**
@@ -537,20 +595,20 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn().
* @param {function(Object)} func Function called with the end to end encrypted rooms
*/
getEndToEndRooms(txn, func) {
this._backend.getEndToEndRooms(txn, func);
public getEndToEndRooms(txn: IDBTransaction, func: (rooms: Record<string, IRoomEncryption>) => void): void {
this.backend.getEndToEndRooms(txn, func);
}
// session backups
/**
* Get the inbound group sessions that need to be backed up.
* @param {integer} limit The maximum number of sessions to retrieve. 0
* @param {number} limit The maximum number of sessions to retrieve. 0
* for no limit.
* @returns {Promise} resolves to an array of inbound group sessions
*/
getSessionsNeedingBackup(limit) {
return this._backend.getSessionsNeedingBackup(limit);
public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
return this.backend.getSessionsNeedingBackup(limit);
}
/**
@@ -558,8 +616,8 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} resolves to the number of sessions
*/
countSessionsNeedingBackup(txn) {
return this._backend.countSessionsNeedingBackup(txn);
public countSessionsNeedingBackup(txn?: IDBTransaction): Promise<number> {
return this.backend.countSessionsNeedingBackup(txn);
}
/**
@@ -568,8 +626,8 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} resolves when the sessions are unmarked
*/
unmarkSessionsNeedingBackup(sessions, txn) {
return this._backend.unmarkSessionsNeedingBackup(sessions, txn);
public unmarkSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
return this.backend.unmarkSessionsNeedingBackup(sessions, txn);
}
/**
@@ -578,8 +636,8 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} resolves when the sessions are marked
*/
markSessionsNeedingBackup(sessions, txn) {
return this._backend.markSessionsNeedingBackup(sessions, txn);
public markSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
return this.backend.markSessionsNeedingBackup(sessions, txn);
}
/**
@@ -589,10 +647,13 @@ export class IndexedDBCryptoStore {
* @param {string} sessionId The ID of the session
* @param {*} txn An active transaction. See doTxn(). (optional)
*/
addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn) {
this._backend.addSharedHistoryInboundGroupSession(
roomId, senderKey, sessionId, txn,
);
public addSharedHistoryInboundGroupSession(
roomId: string,
senderKey: string,
sessionId: string,
txn?: IDBTransaction,
): void {
this.backend.addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn);
}
/**
@@ -601,8 +662,11 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} Resolves to an array of [senderKey, sessionId]
*/
getSharedHistoryInboundGroupSessions(roomId, txn) {
return this._backend.getSharedHistoryInboundGroupSessions(roomId, txn);
public getSharedHistoryInboundGroupSessions(
roomId: string,
txn?: IDBTransaction,
): Promise<[senderKey: string, sessionId: string][]> {
return this.backend.getSharedHistoryInboundGroupSessions(roomId, txn);
}
/**
@@ -627,18 +691,7 @@ export class IndexedDBCryptoStore {
* reject with that exception. On synchronous backends, the
* exception will propagate to the caller of the getFoo method.
*/
doTxn(mode, stores, func, log) {
return this._backend.doTxn(mode, stores, func, log);
doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: IDBTransaction) => T, log?: PrefixedLogger): Promise<T> {
return this.backend.doTxn(mode, stores, func, log);
}
}
IndexedDBCryptoStore.STORE_ACCOUNT = 'account';
IndexedDBCryptoStore.STORE_SESSIONS = 'sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD
= 'inbound_group_sessions_withheld';
IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS
= 'shared_history_inbound_group_sessions';
IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data';
IndexedDBCryptoStore.STORE_ROOMS = 'rooms';
IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup';
@@ -1,6 +1,5 @@
/*
Copyright 2017, 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2017 - 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,6 +16,12 @@ limitations under the License.
import { logger } from '../../logger';
import { MemoryCryptoStore } from './memory-crypto-store';
import { IDeviceData, IProblem, ISession, ISessionInfo, IWithheld, Mode } from "./base";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { ICrossSigningKey } from "../../client";
import { InboundGroupSessionData } from "../../@types/partials";
import { IEncryptedPayload } from "../aes";
/**
* Internal module. Partial localStorage backed storage for e2e.
@@ -38,23 +43,23 @@ const KEY_INBOUND_SESSION_WITHHELD_PREFIX = E2E_PREFIX + "inboundgroupsessions.w
const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/";
const KEY_SESSIONS_NEEDING_BACKUP = E2E_PREFIX + "sessionsneedingbackup";
function keyEndToEndSessions(deviceKey) {
function keyEndToEndSessions(deviceKey: string): string {
return E2E_PREFIX + "sessions/" + deviceKey;
}
function keyEndToEndSessionProblems(deviceKey) {
function keyEndToEndSessionProblems(deviceKey: string): string {
return E2E_PREFIX + "session.problems/" + deviceKey;
}
function keyEndToEndInboundGroupSession(senderKey, sessionId) {
function keyEndToEndInboundGroupSession(senderKey: string, sessionId: string): string {
return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId;
}
function keyEndToEndInboundGroupSessionWithheld(senderKey, sessionId) {
function keyEndToEndInboundGroupSessionWithheld(senderKey: string, sessionId: string): string {
return KEY_INBOUND_SESSION_WITHHELD_PREFIX + senderKey + "/" + sessionId;
}
function keyEndToEndRoomsPrefix(roomId) {
function keyEndToEndRoomsPrefix(roomId: string): string {
return KEY_ROOMS_PREFIX + roomId;
}
@@ -62,24 +67,23 @@ function keyEndToEndRoomsPrefix(roomId) {
* @implements {module:crypto/store/base~CryptoStore}
*/
export class LocalStorageCryptoStore extends MemoryCryptoStore {
constructor(webStore) {
super();
this.store = webStore;
}
static exists(webStore) {
const length = webStore.length;
public static exists(store: Storage): boolean {
const length = store.length;
for (let i = 0; i < length; i++) {
if (webStore.key(i).startsWith(E2E_PREFIX)) {
if (store.key(i).startsWith(E2E_PREFIX)) {
return true;
}
}
return false;
}
constructor(private readonly store: Storage) {
super();
}
// Olm Sessions
countEndToEndSessions(txn, func) {
public countEndToEndSessions(txn: unknown, func: (count: number) => void): void {
let count = 0;
for (let i = 0; i < this.store.length; ++i) {
if (this.store.key(i).startsWith(keyEndToEndSessions(''))) ++count;
@@ -87,9 +91,10 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
func(count);
}
_getEndToEndSessions(deviceKey, txn, func) {
// eslint-disable-next-line @typescript-eslint/naming-convention
private _getEndToEndSessions(deviceKey: string): Record<string, ISessionInfo> {
const sessions = getJsonItem(this.store, keyEndToEndSessions(deviceKey));
const fixedSessions = {};
const fixedSessions: Record<string, ISessionInfo> = {};
// fix up any old sessions to be objects rather than just the base64 pickle
for (const [sid, val] of Object.entries(sessions || {})) {
@@ -105,16 +110,25 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
return fixedSessions;
}
getEndToEndSession(deviceKey, sessionId, txn, func) {
public getEndToEndSession(
deviceKey: string,
sessionId: string,
txn: unknown,
func: (session: ISessionInfo) => void,
): void {
const sessions = this._getEndToEndSessions(deviceKey);
func(sessions[sessionId] || {});
}
getEndToEndSessions(deviceKey, txn, func) {
public getEndToEndSessions(
deviceKey: string,
txn: unknown,
func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
): void {
func(this._getEndToEndSessions(deviceKey) || {});
}
getAllEndToEndSessions(txn, func) {
public getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void {
for (let i = 0; i < this.store.length; ++i) {
if (this.store.key(i).startsWith(keyEndToEndSessions(''))) {
const deviceKey = this.store.key(i).split('/')[1];
@@ -125,17 +139,15 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
}
}
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
public storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void {
const sessions = this._getEndToEndSessions(deviceKey) || {};
sessions[sessionId] = sessionInfo;
setJsonItem(
this.store, keyEndToEndSessions(deviceKey), sessions,
);
setJsonItem(this.store, keyEndToEndSessions(deviceKey), sessions);
}
async storeEndToEndSessionProblem(deviceKey, type, fixed) {
public async storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
const key = keyEndToEndSessionProblems(deviceKey);
const problems = getJsonItem(this.store, key) || [];
const problems = getJsonItem<IProblem[]>(this.store, key) || [];
problems.push({ type, fixed, time: Date.now() });
problems.sort((a, b) => {
return a.time - b.time;
@@ -143,9 +155,9 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
setJsonItem(this.store, key, problems);
}
async getEndToEndSessionProblem(deviceKey, timestamp) {
async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
const key = keyEndToEndSessionProblems(deviceKey);
const problems = getJsonItem(this.store, key) || [];
const problems = getJsonItem<IProblem[]>(this.store, key) || [];
if (!problems.length) {
return null;
}
@@ -162,9 +174,8 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
}
}
async filterOutNotifiedErrorDevices(devices) {
const notifiedErrorDevices =
getJsonItem(this.store, KEY_NOTIFIED_ERROR_DEVICES) || {};
public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
const notifiedErrorDevices = getJsonItem<string[]>(this.store, KEY_NOTIFIED_ERROR_DEVICES) || {};
const ret = [];
for (const device of devices) {
@@ -187,7 +198,12 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
// Inbound Group Sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
public getEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
txn: unknown,
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
): void {
func(
getJsonItem(
this.store,
@@ -200,7 +216,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
);
}
getAllEndToEndInboundGroupSessions(txn, func) {
public getAllEndToEndInboundGroupSessions(txn: unknown, func: (session: ISession | null) => void): void {
for (let i = 0; i < this.store.length; ++i) {
const key = this.store.key(i);
if (key.startsWith(KEY_INBOUND_SESSION_PREFIX)) {
@@ -219,7 +235,12 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
func(null);
}
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
public addEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: unknown,
): void {
const existing = getJsonItem(
this.store,
keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId),
@@ -231,7 +252,12 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
}
}
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
public storeEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: unknown,
): void {
setJsonItem(
this.store,
keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId),
@@ -239,9 +265,12 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
);
}
storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
) {
public storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key: string,
sessionId: string,
sessionData: IWithheld,
txn: unknown,
): void {
setJsonItem(
this.store,
keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId),
@@ -249,25 +278,19 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
);
}
getEndToEndDeviceData(txn, func) {
func(getJsonItem(
this.store, KEY_DEVICE_DATA,
));
public getEndToEndDeviceData(txn: unknown, func: (deviceData: IDeviceData | null) => void): void {
func(getJsonItem(this.store, KEY_DEVICE_DATA));
}
storeEndToEndDeviceData(deviceData, txn) {
setJsonItem(
this.store, KEY_DEVICE_DATA, deviceData,
);
public storeEndToEndDeviceData(deviceData: IDeviceData, txn: unknown): void {
setJsonItem(this.store, KEY_DEVICE_DATA, deviceData);
}
storeEndToEndRoom(roomId, roomInfo, txn) {
setJsonItem(
this.store, keyEndToEndRoomsPrefix(roomId), roomInfo,
);
public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: unknown): void {
setJsonItem(this.store, keyEndToEndRoomsPrefix(roomId), roomInfo);
}
getEndToEndRooms(txn, func) {
public getEndToEndRooms(txn: unknown, func: (rooms: Record<string, IRoomEncryption>) => void): void {
const result = {};
const prefix = keyEndToEndRoomsPrefix('');
@@ -281,9 +304,8 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
func(result);
}
getSessionsNeedingBackup(limit) {
const sessionsNeedingBackup
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
const sessionsNeedingBackup = getJsonItem<string[]>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
const sessions = [];
for (const session in sessionsNeedingBackup) {
@@ -309,13 +331,12 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
return Promise.resolve(sessions);
}
countSessionsNeedingBackup() {
const sessionsNeedingBackup
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
public countSessionsNeedingBackup(): Promise<number> {
const sessionsNeedingBackup = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
return Promise.resolve(Object.keys(sessionsNeedingBackup).length);
}
unmarkSessionsNeedingBackup(sessions) {
public unmarkSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
const sessionsNeedingBackup
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
for (const session of sessions) {
@@ -327,7 +348,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
return Promise.resolve();
}
markSessionsNeedingBackup(sessions) {
public markSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
const sessionsNeedingBackup
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
for (const session of sessions) {
@@ -344,52 +365,46 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
*
* @returns {Promise} Promise which resolves when the store has been cleared.
*/
deleteAllData() {
public deleteAllData(): Promise<void> {
this.store.removeItem(KEY_END_TO_END_ACCOUNT);
return Promise.resolve();
}
// Olm account
getAccount(txn, func) {
const account = getJsonItem(this.store, KEY_END_TO_END_ACCOUNT);
func(account);
public getAccount(txn: unknown, func: (accountPickle: string) => void): void {
const accountPickle = getJsonItem<string>(this.store, KEY_END_TO_END_ACCOUNT);
func(accountPickle);
}
storeAccount(txn, newData) {
setJsonItem(
this.store, KEY_END_TO_END_ACCOUNT, newData,
);
public storeAccount(txn: unknown, accountPickle: string): void {
setJsonItem(this.store, KEY_END_TO_END_ACCOUNT, accountPickle);
}
getCrossSigningKeys(txn, func) {
const keys = getJsonItem(this.store, KEY_CROSS_SIGNING_KEYS);
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey>) => void): void {
const keys = getJsonItem<Record<string, ICrossSigningKey>>(this.store, KEY_CROSS_SIGNING_KEYS);
func(keys);
}
getSecretStorePrivateKey(txn, func, type) {
const key = getJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`);
public getSecretStorePrivateKey(txn: unknown, func: (key: IEncryptedPayload | null) => void, type: string): void {
const key = getJsonItem<IEncryptedPayload>(this.store, E2E_PREFIX + `ssss_cache.${type}`);
func(key);
}
storeCrossSigningKeys(txn, keys) {
setJsonItem(
this.store, KEY_CROSS_SIGNING_KEYS, keys,
);
public storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void {
setJsonItem(this.store, KEY_CROSS_SIGNING_KEYS, keys);
}
storeSecretStorePrivateKey(txn, type, key) {
setJsonItem(
this.store, E2E_PREFIX + `ssss_cache.${type}`, key,
);
public storeSecretStorePrivateKey(txn: unknown, type: string, key: IEncryptedPayload): void {
setJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`, key);
}
doTxn(mode, stores, func) {
doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T): Promise<T> {
return Promise.resolve(func(null));
}
}
function getJsonItem(store, key) {
function getJsonItem<T>(store: Storage, key: string): T | null {
try {
// if the key is absent, store.getItem() returns null, and
// JSON.parse(null) === null, so this returns null.
@@ -401,6 +416,6 @@ function getJsonItem(store, key) {
return null;
}
function setJsonItem(store, key, val) {
function setJsonItem<T>(store: Storage, key: string, val: T): void {
store.setItem(key, JSON.stringify(val));
}
@@ -1,7 +1,5 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2017 - 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.
@@ -18,6 +16,22 @@ limitations under the License.
import { logger } from '../../logger';
import * as utils from "../../utils";
import {
CryptoStore,
IDeviceData,
IProblem,
ISession,
ISessionInfo,
IWithheld,
Mode,
OutgoingRoomKeyRequest,
} from "./base";
import { IRoomKeyRequestBody } from "../index";
import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { InboundGroupSessionData } from "../../@types/partials";
import { IEncryptedPayload } from "../aes";
/**
* Internal module. in-memory storage for e2e.
@@ -28,32 +42,22 @@ import * as utils from "../../utils";
/**
* @implements {module:crypto/store/base~CryptoStore}
*/
export class MemoryCryptoStore {
constructor() {
this._outgoingRoomKeyRequests = [];
this._account = null;
this._crossSigningKeys = null;
this._privateKeys = {};
this._backupKeys = {};
export class MemoryCryptoStore implements CryptoStore {
private outgoingRoomKeyRequests: OutgoingRoomKeyRequest[] = [];
private account: string = null;
private crossSigningKeys: Record<string, ICrossSigningKey> = null;
private privateKeys: Record<string, IEncryptedPayload> = {};
// Map of {devicekey -> {sessionId -> session pickle}}
this._sessions = {};
// Map of {devicekey -> array of problems}
this._sessionProblems = {};
// Map of {userId -> deviceId -> true}
this._notifiedErrorDevices = {};
// Map of {senderCurve25519Key+'/'+sessionId -> session data object}
this._inboundGroupSessions = {};
this._inboundGroupSessionsWithheld = {};
// Opaque device data object
this._deviceData = null;
// roomId -> Opaque roomInfo object
this._rooms = {};
// Set of {senderCurve25519Key+'/'+sessionId}
this._sessionsNeedingBackup = {};
// roomId -> array of [senderKey, sessionId]
this._sharedHistoryInboundGroupSessions = {};
}
private sessions: { [deviceKey: string]: { [sessionId: string]: ISessionInfo } } = {};
private sessionProblems: { [deviceKey: string]: IProblem[] } = {};
private notifiedErrorDevices: { [userId: string]: { [deviceId: string]: boolean } } = {};
private inboundGroupSessions: { [sessionKey: string]: InboundGroupSessionData } = {};
private inboundGroupSessionsWithheld: Record<string, IWithheld> = {};
// Opaque device data object
private deviceData: IDeviceData = null;
private rooms: { [roomId: string]: IRoomEncryption } = {};
private sessionsNeedingBackup: { [sessionKey: string]: boolean } = {};
private sharedHistoryInboundGroupSessions: { [roomId: string]: [senderKey: string, sessionId: string][] } = {};
/**
* Ensure the database exists and is up-to-date.
@@ -62,7 +66,7 @@ export class MemoryCryptoStore {
*
* @return {Promise} resolves to the store.
*/
async startup() {
public async startup(): Promise<CryptoStore> {
// No startup work to do for the memory store.
return this;
}
@@ -72,7 +76,7 @@ export class MemoryCryptoStore {
*
* @returns {Promise} Promise which resolves when the store has been cleared.
*/
deleteAllData() {
public deleteAllData(): Promise<void> {
return Promise.resolve();
}
@@ -86,7 +90,7 @@ export class MemoryCryptoStore {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the
* same instance as passed in, or the existing one.
*/
getOrAddOutgoingRoomKeyRequest(request) {
public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> {
const requestBody = request.requestBody;
return utils.promiseTry(() => {
@@ -109,7 +113,7 @@ export class MemoryCryptoStore {
`enqueueing key request for ${requestBody.room_id} / ` +
requestBody.session_id,
);
this._outgoingRoomKeyRequests.push(request);
this.outgoingRoomKeyRequests.push(request);
return request;
});
}
@@ -124,7 +128,7 @@ export class MemoryCryptoStore {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* not found
*/
getOutgoingRoomKeyRequest(requestBody) {
public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null> {
return Promise.resolve(this._getOutgoingRoomKeyRequest(requestBody));
}
@@ -139,8 +143,9 @@ export class MemoryCryptoStore {
* @return {module:crypto/store/base~OutgoingRoomKeyRequest?}
* the matching request, or null if not found
*/
_getOutgoingRoomKeyRequest(requestBody) {
for (const existing of this._outgoingRoomKeyRequests) {
// eslint-disable-next-line @typescript-eslint/naming-convention
private _getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): OutgoingRoomKeyRequest | null {
for (const existing of this.outgoingRoomKeyRequests) {
if (utils.deepCompare(existing.requestBody, requestBody)) {
return existing;
}
@@ -157,8 +162,8 @@ export class MemoryCryptoStore {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* there are no pending requests in those states
*/
getOutgoingRoomKeyRequestByState(wantedStates) {
for (const req of this._outgoingRoomKeyRequests) {
public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null> {
for (const req of this.outgoingRoomKeyRequests) {
for (const state of wantedStates) {
if (req.state === state) {
return Promise.resolve(req);
@@ -173,18 +178,22 @@ export class MemoryCryptoStore {
* @param {Number} wantedState
* @return {Promise<Array<*>>} All OutgoingRoomKeyRequests in state
*/
getAllOutgoingRoomKeyRequestsByState(wantedState) {
public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]> {
return Promise.resolve(
this._outgoingRoomKeyRequests.filter(
this.outgoingRoomKeyRequests.filter(
(r) => r.state == wantedState,
),
);
}
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
public getOutgoingRoomKeyRequestsByTarget(
userId: string,
deviceId: string,
wantedStates: number[],
): Promise<OutgoingRoomKeyRequest[]> {
const results = [];
for (const req of this._outgoingRoomKeyRequests) {
for (const req of this.outgoingRoomKeyRequests) {
for (const state of wantedStates) {
if (req.state === state && req.recipients.includes({ userId, deviceId })) {
results.push(req);
@@ -206,13 +215,17 @@ export class MemoryCryptoStore {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
* updated request, or null if no matching row was found
*/
updateOutgoingRoomKeyRequest(requestId, expectedState, updates) {
for (const req of this._outgoingRoomKeyRequests) {
public updateOutgoingRoomKeyRequest(
requestId: string,
expectedState: number,
updates: Partial<OutgoingRoomKeyRequest>,
): Promise<OutgoingRoomKeyRequest | null> {
for (const req of this.outgoingRoomKeyRequests) {
if (req.requestId !== requestId) {
continue;
}
if (req.state != expectedState) {
if (req.state !== expectedState) {
logger.warn(
`Cannot update room key request from ${expectedState} ` +
`as it was already updated to ${req.state}`,
@@ -235,9 +248,12 @@ export class MemoryCryptoStore {
*
* @returns {Promise} resolves once the operation is completed
*/
deleteOutgoingRoomKeyRequest(requestId, expectedState) {
for (let i = 0; i < this._outgoingRoomKeyRequests.length; i++) {
const req = this._outgoingRoomKeyRequests[i];
public deleteOutgoingRoomKeyRequest(
requestId: string,
expectedState: number,
): Promise<OutgoingRoomKeyRequest | null> {
for (let i = 0; i < this.outgoingRoomKeyRequests.length; i++) {
const req = this.outgoingRoomKeyRequests[i];
if (req.requestId !== requestId) {
continue;
@@ -251,7 +267,7 @@ export class MemoryCryptoStore {
return Promise.resolve(null);
}
this._outgoingRoomKeyRequests.splice(i, 1);
this.outgoingRoomKeyRequests.splice(i, 1);
return Promise.resolve(req);
}
@@ -260,48 +276,57 @@ export class MemoryCryptoStore {
// Olm Account
getAccount(txn, func) {
func(this._account);
public getAccount(txn: unknown, func: (accountPickle: string) => void) {
func(this.account);
}
storeAccount(txn, newData) {
this._account = newData;
public storeAccount(txn: unknown, accountPickle: string): void {
this.account = accountPickle;
}
getCrossSigningKeys(txn, func) {
func(this._crossSigningKeys);
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey>) => void): void {
func(this.crossSigningKeys);
}
getSecretStorePrivateKey(txn, func, type) {
const result = this._privateKeys[type];
return func(result || null);
public getSecretStorePrivateKey(txn: unknown, func: (key: IEncryptedPayload | null) => void, type: string): void {
const result = this.privateKeys[type];
func(result || null);
}
storeCrossSigningKeys(txn, keys) {
this._crossSigningKeys = keys;
public storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void {
this.crossSigningKeys = keys;
}
storeSecretStorePrivateKey(txn, type, key) {
this._privateKeys[type] = key;
public storeSecretStorePrivateKey(txn: unknown, type: string, key: IEncryptedPayload): void {
this.privateKeys[type] = key;
}
// Olm Sessions
countEndToEndSessions(txn, func) {
return Object.keys(this._sessions).length;
public countEndToEndSessions(txn: unknown, func: (count: number) => void): void {
func(Object.keys(this.sessions).length);
}
getEndToEndSession(deviceKey, sessionId, txn, func) {
const deviceSessions = this._sessions[deviceKey] || {};
public getEndToEndSession(
deviceKey: string,
sessionId: string,
txn: unknown,
func: (session: ISessionInfo) => void,
): void {
const deviceSessions = this.sessions[deviceKey] || {};
func(deviceSessions[sessionId] || null);
}
getEndToEndSessions(deviceKey, txn, func) {
func(this._sessions[deviceKey] || {});
public getEndToEndSessions(
deviceKey: string,
txn: unknown,
func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
): void {
func(this.sessions[deviceKey] || {});
}
getAllEndToEndSessions(txn, func) {
Object.entries(this._sessions).forEach(([deviceKey, deviceSessions]) => {
public getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void {
Object.entries(this.sessions).forEach(([deviceKey, deviceSessions]) => {
Object.entries(deviceSessions).forEach(([sessionId, session]) => {
func({
...session,
@@ -312,26 +337,25 @@ export class MemoryCryptoStore {
});
}
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
let deviceSessions = this._sessions[deviceKey];
public storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void {
let deviceSessions = this.sessions[deviceKey];
if (deviceSessions === undefined) {
deviceSessions = {};
this._sessions[deviceKey] = deviceSessions;
this.sessions[deviceKey] = deviceSessions;
}
deviceSessions[sessionId] = sessionInfo;
}
async storeEndToEndSessionProblem(deviceKey, type, fixed) {
const problems = this._sessionProblems[deviceKey]
= this._sessionProblems[deviceKey] || [];
public async storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
const problems = this.sessionProblems[deviceKey] = this.sessionProblems[deviceKey] || [];
problems.push({ type, fixed, time: Date.now() });
problems.sort((a, b) => {
return a.time - b.time;
});
}
async getEndToEndSessionProblem(deviceKey, timestamp) {
const problems = this._sessionProblems[deviceKey] || [];
public async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
const problems = this.sessionProblems[deviceKey] || [];
if (!problems.length) {
return null;
}
@@ -348,9 +372,9 @@ export class MemoryCryptoStore {
}
}
async filterOutNotifiedErrorDevices(devices) {
const notifiedErrorDevices = this._notifiedErrorDevices;
const ret = [];
public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
const notifiedErrorDevices = this.notifiedErrorDevices;
const ret: IOlmDevice[] = [];
for (const device of devices) {
const { userId, deviceInfo } = device;
@@ -370,16 +394,24 @@ export class MemoryCryptoStore {
// Inbound Group Sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
public getEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
txn: unknown,
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
): void {
const k = senderCurve25519Key+'/'+sessionId;
func(
this._inboundGroupSessions[k] || null,
this._inboundGroupSessionsWithheld[k] || null,
this.inboundGroupSessions[k] || null,
this.inboundGroupSessionsWithheld[k] || null,
);
}
getAllEndToEndInboundGroupSessions(txn, func) {
for (const key of Object.keys(this._inboundGroupSessions)) {
public getAllEndToEndInboundGroupSessions(
txn: unknown,
func: (session: ISession | null) => void,
): void {
for (const key of Object.keys(this.inboundGroupSessions)) {
// we can't use split, as the components we are trying to split out
// might themselves contain '/' characters. We rely on the
// senderKey being a (32-byte) curve25519 key, base64-encoded
@@ -388,58 +420,71 @@ export class MemoryCryptoStore {
func({
senderKey: key.substr(0, 43),
sessionId: key.substr(44),
sessionData: this._inboundGroupSessions[key],
sessionData: this.inboundGroupSessions[key],
});
}
func(null);
}
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
public addEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: unknown,
): void {
const k = senderCurve25519Key+'/'+sessionId;
if (this._inboundGroupSessions[k] === undefined) {
this._inboundGroupSessions[k] = sessionData;
if (this.inboundGroupSessions[k] === undefined) {
this.inboundGroupSessions[k] = sessionData;
}
}
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
this._inboundGroupSessions[senderCurve25519Key+'/'+sessionId] = sessionData;
public storeEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: unknown,
): void {
this.inboundGroupSessions[senderCurve25519Key+'/'+sessionId] = sessionData;
}
storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
) {
public storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key: string,
sessionId: string,
sessionData: IWithheld,
txn: unknown,
): void {
const k = senderCurve25519Key+'/'+sessionId;
this._inboundGroupSessionsWithheld[k] = sessionData;
this.inboundGroupSessionsWithheld[k] = sessionData;
}
// Device Data
getEndToEndDeviceData(txn, func) {
func(this._deviceData);
public getEndToEndDeviceData(txn: unknown, func: (deviceData: IDeviceData | null) => void): void {
func(this.deviceData);
}
storeEndToEndDeviceData(deviceData, txn) {
this._deviceData = deviceData;
public storeEndToEndDeviceData(deviceData: IDeviceData, txn: unknown): void {
this.deviceData = deviceData;
}
// E2E rooms
storeEndToEndRoom(roomId, roomInfo, txn) {
this._rooms[roomId] = roomInfo;
public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: unknown): void {
this.rooms[roomId] = roomInfo;
}
getEndToEndRooms(txn, func) {
func(this._rooms);
public getEndToEndRooms(txn: unknown, func: (rooms: Record<string, IRoomEncryption>) => void): void {
func(this.rooms);
}
getSessionsNeedingBackup(limit) {
const sessions = [];
for (const session in this._sessionsNeedingBackup) {
if (this._inboundGroupSessions[session]) {
public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
const sessions: ISession[] = [];
for (const session in this.sessionsNeedingBackup) {
if (this.inboundGroupSessions[session]) {
sessions.push({
senderKey: session.substr(0, 43),
sessionId: session.substr(44),
sessionData: this._inboundGroupSessions[session],
sessionData: this.inboundGroupSessions[session],
});
if (limit && session.length >= limit) {
break;
@@ -449,39 +494,39 @@ export class MemoryCryptoStore {
return Promise.resolve(sessions);
}
countSessionsNeedingBackup() {
return Promise.resolve(Object.keys(this._sessionsNeedingBackup).length);
public countSessionsNeedingBackup(): Promise<number> {
return Promise.resolve(Object.keys(this.sessionsNeedingBackup).length);
}
unmarkSessionsNeedingBackup(sessions) {
public unmarkSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
for (const session of sessions) {
const sessionKey = session.senderKey + '/' + session.sessionId;
delete this._sessionsNeedingBackup[sessionKey];
delete this.sessionsNeedingBackup[sessionKey];
}
return Promise.resolve();
}
markSessionsNeedingBackup(sessions) {
public markSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
for (const session of sessions) {
const sessionKey = session.senderKey + '/' + session.sessionId;
this._sessionsNeedingBackup[sessionKey] = true;
this.sessionsNeedingBackup[sessionKey] = true;
}
return Promise.resolve();
}
addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId) {
const sessions = this._sharedHistoryInboundGroupSessions[roomId] || [];
public addSharedHistoryInboundGroupSession(roomId: string, senderKey: string, sessionId: string): void {
const sessions = this.sharedHistoryInboundGroupSessions[roomId] || [];
sessions.push([senderKey, sessionId]);
this._sharedHistoryInboundGroupSessions[roomId] = sessions;
this.sharedHistoryInboundGroupSessions[roomId] = sessions;
}
getSharedHistoryInboundGroupSessions(roomId) {
return Promise.resolve(this._sharedHistoryInboundGroupSessions[roomId] || []);
public getSharedHistoryInboundGroupSessions(roomId: string): Promise<[senderKey: string, sessionId: string][]> {
return Promise.resolve(this.sharedHistoryInboundGroupSessions[roomId] || []);
}
// Session key backups
doTxn(mode, stores, func) {
public doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn?: unknown) => T): Promise<T> {
return Promise.resolve(func(null));
}
}
@@ -22,8 +22,8 @@ limitations under the License.
* @param {string} dbName The database name to test for
* @returns {boolean} Whether the database exists
*/
export function exists(indexedDB, dbName) {
return new Promise((resolve, reject) => {
export function exists(indexedDB: IDBFactory, dbName: string): Promise<boolean> {
return new Promise<boolean>((resolve, reject) => {
let exists = true;
const req = indexedDB.open(dbName);
req.onupgradeneeded = () => {
@@ -31,7 +31,7 @@ export function exists(indexedDB, dbName) {
// should only fire if the DB did not exist before at any version.
exists = false;
};
req.onblocked = () => reject();
req.onblocked = () => reject(req.error);
req.onsuccess = () => {
const db = req.result;
db.close();
@@ -45,6 +45,6 @@ export function exists(indexedDB, dbName) {
}
resolve(exists);
};
req.onerror = ev => reject(ev.target.error);
req.onerror = ev => reject(req.error);
});
}
@@ -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;
}
}
},
};
}
}
+1 -1
View File
@@ -62,7 +62,7 @@ log.methodFactory = function(methodName, logLevel, loggerName) {
export const logger: PrefixedLogger = log.getLogger(DEFAULT_NAMESPACE);
logger.setLevel(log.levels.DEBUG);
interface PrefixedLogger extends Logger {
export interface PrefixedLogger extends Logger {
withPrefix?: (prefix: string) => PrefixedLogger;
prefix?: string;
}
+1 -1
View File
@@ -154,7 +154,7 @@ export function createClient(opts: ICreateClientOpts | string) {
}
opts.request = opts.request || requestInstance;
opts.store = opts.store || new MemoryStore({
localStorage: global.localStorage,
localStorage: global.localStorage,
});
opts.scheduler = opts.scheduler || new MatrixScheduler();
opts.cryptoStore = opts.cryptoStore || cryptoStoreFactory();
+14 -4
View File
@@ -82,6 +82,19 @@ export class MSC3089Branch {
* @returns {Promise<{info: IEncryptedFile, httpUrl: string}>} Information about the file.
*/
public async getFileInfo(): Promise<{ info: IEncryptedFile, httpUrl: string }> {
const event = await this.getFileEvent();
const file = event.getContent()['file'];
const httpUrl = this.client.mxcUrlToHttp(file['url']);
return { info: file, httpUrl: httpUrl };
}
/**
* Gets the event the file points to.
* @returns {Promise<MatrixEvent>} Resolves to the file's event.
*/
public async getFileEvent(): Promise<MatrixEvent> {
const room = this.client.getRoom(this.roomId);
if (!room) throw new Error("Unknown room");
@@ -94,9 +107,6 @@ export class MSC3089Branch {
// Sometimes the event context doesn't decrypt for us, so do that.
await this.client.decryptEventIfNeeded(event, { emit: false, isRetry: false });
const file = event.getContent()['file'];
const httpUrl = this.client.mxcUrlToHttp(file['url']);
return { info: file, httpUrl: httpUrl };
return event;
}
}
+28 -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,
@@ -193,6 +193,28 @@ export class MSC3089TreeSpace {
await this.client.sendStateEvent(this.roomId, EventType.RoomPowerLevels, pls, "");
}
/**
* Gets the current permissions of a user. Note that any users missing explicit permissions (or not
* in the space) will be considered Viewers. Appropriate membership checks need to be performed
* elsewhere.
* @param {string} userId The user ID to check permissions of.
* @returns {TreePermissions} The permissions for the user, defaulting to Viewer.
*/
public getPermissions(userId: string): TreePermissions {
const currentPls = this.room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
if (Array.isArray(currentPls)) throw new Error("Unexpected return type for power levels");
const pls = currentPls.getContent() || {};
const viewLevel = pls['users_default'] || 0;
const editLevel = pls['events_default'] || 50;
const adminLevel = pls['events']?.[EventType.RoomPowerLevels] || 100;
const userLevel = pls['users']?.[userId] || viewLevel;
if (userLevel >= adminLevel) return TreePermissions.Owner;
if (userLevel >= editLevel) return TreePermissions.Editor;
return TreePermissions.Viewer;
}
/**
* Creates a directory under this tree space, represented as another tree space.
* @param {string} name The name for the directory.
@@ -429,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,
@@ -442,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,
+7 -5
View File
@@ -50,13 +50,15 @@ export class EventTimeline {
* @param {boolean} toStartOfTimeline if true the event's forwardLooking flag is set false
*/
static setEventMetadata(event: MatrixEvent, stateContext: RoomState, toStartOfTimeline: boolean): void {
// We always check if the event doesn't already have the property. We do
// this to avoid overriding non-sentinel members by sentinel ones when
// adding the event to a filtered timeline
if (!event.sender) {
// When we try to generate a sentinel member before we have that member
// in the members object, we still generate a sentinel but it doesn't
// have a membership event, so test to see if events.member is set. We
// check this to avoid overriding non-sentinel members by sentinel ones
// when adding the event to a filtered timeline
if (!event.sender?.events?.member) {
event.sender = stateContext.getSentinelMember(event.getSender());
}
if (!event.target && event.getType() === EventType.RoomMember) {
if (!event.target?.events?.member && event.getType() === EventType.RoomMember) {
event.target = stateContext.getSentinelMember(event.getStateKey());
}
+13 -7
View File
@@ -28,6 +28,7 @@ import { EventType, MsgType, RelationType } from "../@types/event";
import { Crypto } from "../crypto";
import { deepSortedObjectEntries } from "../utils";
import { RoomMember } from "./room-member";
import { IActionsObject } from '../pushprocessor';
/**
* Enum for event statuses.
@@ -148,7 +149,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;
@@ -947,7 +948,7 @@ export class MatrixEvent extends EventEmitter {
*
* @return {?Object} push actions
*/
public getPushActions(): object | null {
public getPushActions(): IActionsObject | null {
return this.pushActions;
}
@@ -956,7 +957,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 +1235,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}
*/
+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;
+5 -1
View File
@@ -131,7 +131,11 @@ export class RoomMember extends EventEmitter {
this.disambiguate,
);
this.rawDisplayName = event.getDirectionalContent().displayname || this.userId;
this.rawDisplayName = event.getDirectionalContent().displayname;
if (!this.rawDisplayName || !utils.removeHiddenChars(this.rawDisplayName)) {
this.rawDisplayName = this.userId;
}
if (oldMembership !== this.membership) {
this.updateModifiedTime();
this.emit("RoomMember.membership", event, this, oldMembership);
+3 -3
View File
@@ -204,9 +204,9 @@ export class RoomState extends EventEmitter {
* @return {MatrixEvent[]|MatrixEvent} A list of events if state_key was
* <code>undefined</code>, else a single event (or null if no match found).
*/
public getStateEvents(eventType: string): MatrixEvent[];
public getStateEvents(eventType: string, stateKey: string): MatrixEvent;
public getStateEvents(eventType: string, stateKey?: string) {
public getStateEvents(eventType: EventType | string): MatrixEvent[];
public getStateEvents(eventType: EventType | string, stateKey: string): MatrixEvent;
public getStateEvents(eventType: EventType | string, stateKey?: string) {
if (!this.events.has(eventType)) {
// no match
return stateKey === undefined ? [] : null;
+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) {}
}
+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}
+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[];
+1 -1
View File
@@ -117,7 +117,7 @@ function reqAsCursorPromise(req: IDBRequest<IDBCursor | null>): Promise<IDBCurso
}
export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
public static exists(indexedDB: IDBFactory, dbName: string): boolean {
public static exists(indexedDB: IDBFactory, dbName: string): Promise<boolean> {
dbName = "matrix-js-sdk:" + (dbName || "default");
return IndexedDBHelpers.exists(indexedDB, dbName);
}
+1 -1
View File
@@ -47,7 +47,7 @@ interface IOpts extends IBaseOpts {
}
export class IndexedDBStore extends MemoryStore {
static exists(indexedDB: IDBFactory, dbName: string): boolean {
static exists(indexedDB: IDBFactory, dbName: string): Promise<boolean> {
return LocalIndexedDBStoreBackend.exists(indexedDB, dbName);
}
+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 [];
+4 -2
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;
@@ -1065,8 +1067,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()];
+26 -3
View File
@@ -402,16 +402,17 @@ export function normalize(str: string): string {
// various width spaces U+2000 - U+200D
// LTR and RTL marks U+200E and U+200F
// LTR/RTL and other directional formatting marks U+202A - U+202F
// Arabic Letter RTL mark U+061C
// Combining characters U+0300 - U+036F
// Zero width no-break space (BOM) U+FEFF
// eslint-disable-next-line no-misleading-character-class
const removeHiddenCharsRegex = /[\u2000-\u200F\u202A-\u202F\u0300-\u036f\uFEFF\s]/g;
const removeHiddenCharsRegex = /[\u2000-\u200F\u202A-\u202F\u0300-\u036F\uFEFF\u061C\s]/g;
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
@@ -456,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;
@@ -693,3 +694,25 @@ const collator = new Intl.Collator();
export function compare(a: string, b: string): number {
return collator.compare(a, b);
}
/**
* This function is similar to Object.assign() but it assigns recursively and
* allows you to ignore nullish values from the source
*
* @param {Object} target
* @param {Object} source
* @returns the target object
*/
export function recursivelyAssign(target: Object, source: Object, ignoreNullish = false): any {
for (const [sourceKey, sourceValue] of Object.entries(source)) {
if (target[sourceKey] instanceof Object && sourceValue) {
recursivelyAssign(target[sourceKey], sourceValue);
continue;
}
if ((sourceValue !== null && sourceValue !== undefined) || !ignoreNullish) {
target[sourceKey] = sourceValue;
continue;
}
}
return target;
}
+460 -185
View File
File diff suppressed because it is too large Load Diff
+65 -65
View File
@@ -80,7 +80,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 +100,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 +110,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,23 +128,21 @@ 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;
let call = content.call_id ? this.calls.get(content.call_id) : undefined;
//console.info("RECV %s content=%s", event.getType(), JSON.stringify(content));
//console.info("RECV %s content=%s", type, JSON.stringify(content));
if (event.getType() === EventType.CallInvite) {
if (event.getSender() === this.client.credentials.userId) {
return; // ignore invites you send
}
if (type === EventType.CallInvite) {
// ignore invites you send
if (weSentTheEvent) return;
// expired call
if (event.getLocalAge() > content.lifetime - RING_GRACE_PERIOD) return;
// stale/old invite event
if (call && call.state === CallState.Ended) return;
if (event.getLocalAge() > content.lifetime - RING_GRACE_PERIOD) {
return; // expired call
}
if (call && call.state === CallState.Ended) {
return; // stale/old invite event
}
if (call) {
logger.log(
`WARN: Already have a MatrixCall with id ${content.call_id} but got an ` +
@@ -154,9 +152,11 @@ export class CallEventHandler {
const timeUntilTurnCresExpire = this.client.getTurnServersExpiry() - Date.now();
logger.info("Current turn creds expire in " + timeUntilTurnCresExpire + " ms");
call = createNewMatrixCall(this.client, event.getRoomId(), {
forceTURN: this.client.forceTURN,
});
call = createNewMatrixCall(
this.client,
event.getRoomId(),
{ forceTURN: this.client.forceTURN },
);
if (!call) {
logger.log(
"Incoming call ID " + content.call_id + " but this client " +
@@ -169,7 +169,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 +201,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,21 +222,10 @@ export class CallEventHandler {
} else {
this.client.emit("Call.incoming", call);
}
} else if (event.getType() === EventType.CallAnswer) {
if (!call) {
return;
}
if (event.getSender() === this.client.credentials.userId) {
if (call.state === CallState.Ringing) {
call.onAnsweredElsewhere(content);
}
} else {
call.onAnswerReceived(event);
}
} else if (event.getType() === EventType.CallCandidates) {
if (event.getSender() === this.client.credentials.userId) {
return;
}
return;
} else if (type === EventType.CallCandidates) {
if (weSentTheEvent) return;
if (!call) {
// store the candidates; we may get a call eventually.
if (!this.candidateEventsByCall.has(content.call_id)) {
@@ -244,7 +235,8 @@ export class CallEventHandler {
} else {
call.onRemoteIceCandidatesReceived(event);
}
} else if ([EventType.CallHangup, EventType.CallReject].includes(event.getType() as EventType)) {
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
if (!call) {
@@ -259,7 +251,7 @@ export class CallEventHandler {
}
} else {
if (call.state !== CallState.Ended) {
if (event.getType() === EventType.CallHangup) {
if (type === EventType.CallHangup) {
call.onHangupReceived(content);
} else {
call.onRejectReceived(content);
@@ -267,36 +259,44 @@ export class CallEventHandler {
this.calls.delete(content.call_id);
}
}
} else if (event.getType() === EventType.CallSelectAnswer) {
if (!call) return;
return;
}
if (event.getContent().party_id === call.ourPartyId) {
// Ignore remote echo
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;
call.onSelectAnswerReceived(event);
} else if (event.getType() === EventType.CallNegotiate) {
if (!call) return;
switch (type) {
case EventType.CallAnswer:
if (weSentTheEvent) {
if (call.state === CallState.Ringing) {
call.onAnsweredElsewhere(content);
}
} else {
call.onAnswerReceived(event);
}
break;
case EventType.CallSelectAnswer:
call.onSelectAnswerReceived(event);
break;
if (event.getContent().party_id === call.ourPartyId) {
// Ignore remote echo
return;
}
case EventType.CallNegotiate:
call.onNegotiateReceived(event);
break;
call.onNegotiateReceived(event);
} else if (
event.getType() === EventType.CallAssertedIdentity ||
event.getType() === EventType.CallAssertedIdentityPrefix
) {
if (!call) return;
case EventType.CallAssertedIdentity:
case EventType.CallAssertedIdentityPrefix:
call.onAssertedIdentityReceived(event);
break;
if (event.getContent().party_id === call.ourPartyId) {
// Ignore remote echo (not that we send asserted identity, but still...)
return;
}
call.onAssertedIdentityReceived(event);
case EventType.CallSDPStreamMetadataChanged:
case EventType.CallSDPStreamMetadataChangedPrefix:
call.onSDPStreamMetadataChangedReceived(event);
break;
}
}
}
+26 -6
View File
@@ -1,11 +1,24 @@
// allow camelcase as these are events type that go onto the wire
// allow non-camelcase as these are events type that go onto the wire
/* eslint-disable camelcase */
// TODO: Change to "sdp_stream_metadata" when MSC3077 is merged
export const SDPStreamMetadataKey = "org.matrix.msc3077.sdp_stream_metadata";
export enum SDPStreamMetadataPurpose {
Usermedia = "m.usermedia",
Screenshare = "m.screenshare",
}
export interface SDPStreamMetadataObject {
purpose: SDPStreamMetadataPurpose;
audio_muted: boolean;
video_muted: boolean;
}
export interface SDPStreamMetadata {
[key: string]: SDPStreamMetadataObject;
}
interface CallOfferAnswer {
type: string;
sdp: string;
@@ -13,11 +26,19 @@ interface CallOfferAnswer {
export interface CallCapabilities {
'm.call.transferee': boolean;
'm.call.dtmf': boolean;
}
export interface CallReplacesTarget {
id: string;
display_name: string;
avatar_url: string;
}
export interface MCallAnswer {
answer: CallOfferAnswer;
capabilities: CallCapabilities;
[SDPStreamMetadataKey]: SDPStreamMetadata;
}
export interface MCallOfferNegotiate {
@@ -25,17 +46,16 @@ export interface MCallOfferNegotiate {
description: CallOfferAnswer;
lifetime: number;
capabilities: CallCapabilities;
[SDPStreamMetadataKey]: SDPStreamMetadata;
}
export interface MCallReplacesTarget {
id: string;
display_name: string;
avatar_url: string;
export interface MCallSDPStreamMetadataChanged {
[SDPStreamMetadataKey]: SDPStreamMetadata;
}
export interface MCallReplacesEvent {
replacement_id: string;
target_user: MCallReplacesTarget;
target_user: CallReplacesTarget;
create_call: string;
await_call: string;
target_room: string;
+15 -4
View File
@@ -21,6 +21,7 @@ import { RoomMember } from "../models/room-member";
export enum CallFeedEvent {
NewStream = "new_stream",
MuteStateChanged = "mute_state_changed"
}
export class CallFeed extends EventEmitter {
@@ -30,6 +31,8 @@ export class CallFeed extends EventEmitter {
public purpose: SDPStreamMetadataPurpose,
private client: MatrixClient,
private roomId: string,
private audioMuted: boolean,
private videoMuted: boolean,
) {
super();
}
@@ -51,15 +54,13 @@ export class CallFeed extends EventEmitter {
return this.userId === this.client.getUserId();
}
// TODO: The two following methods should be later replaced
// by something that will also check if the remote is muted
/**
* Returns true if audio is muted or if there are no audio
* tracks, otherwise returns false
* @returns {boolean} is audio muted?
*/
public isAudioMuted(): boolean {
return this.stream.getAudioTracks().length === 0;
return this.stream.getAudioTracks().length === 0 || this.audioMuted;
}
/**
@@ -69,7 +70,7 @@ export class CallFeed extends EventEmitter {
*/
public isVideoMuted(): boolean {
// We assume only one video track
return this.stream.getVideoTracks().length === 0;
return this.stream.getVideoTracks().length === 0 || this.videoMuted;
}
/**
@@ -81,4 +82,14 @@ export class CallFeed extends EventEmitter {
this.stream = newStream;
this.emit(CallFeedEvent.NewStream, this.stream);
}
public setAudioMuted(muted: boolean): void {
this.audioMuted = muted;
this.emit(CallFeedEvent.MuteStateChanged, this.audioMuted, this.videoMuted);
}
public setVideoMuted(muted: boolean): void {
this.videoMuted = muted;
this.emit(CallFeedEvent.MuteStateChanged, this.audioMuted, this.videoMuted);
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"sourceMap": true,
"noEmit": false,
"emitDecoratorMetadata": true,
"outDir": "./lib",
"rootDir": "src"
},
"exclude": [
"./spec/**/*.ts"
]
}
+5 -9
View File
@@ -1,20 +1,16 @@
{
"compilerOptions": {
"target": "es2016",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"module": "commonjs",
"moduleResolution": "node",
"target": "es2016",
"noImplicitAny": false,
"sourceMap": true,
"outDir": "./lib",
"declaration": true,
"types": [
"node"
]
"noEmit": true,
"declaration": true
},
"include": [
"./src/**/*.ts"
"./src/**/*.ts",
"./spec/**/*.ts",
]
}
+453 -14
View File
@@ -2,6 +2,28 @@
# yarn lockfile v1
"@actions/core@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.4.0.tgz#cf2e6ee317e314b03886adfeb20e448d50d6e524"
integrity sha512-CGx2ilGq5i7zSLgiiGUtBCxhRRxibJYU6Fim0Q1Wg2aQL2LTnF27zbqZOrxfvFQ55eSBW0L8uVStgtKMpa0Qlg==
"@actions/github@^5.0.0":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@actions/github/-/github-5.0.0.tgz#1754127976c50bd88b2e905f10d204d76d1472f8"
integrity sha512-QvE9eAAfEsS+yOOk0cylLBIO/d6WyWIOvsxxzdrPFaud39G6BOkUwScXZn1iBzQzHyu9SBkkLSWlohDWdsasAQ==
dependencies:
"@actions/http-client" "^1.0.11"
"@octokit/core" "^3.4.0"
"@octokit/plugin-paginate-rest" "^2.13.3"
"@octokit/plugin-rest-endpoint-methods" "^5.1.1"
"@actions/http-client@^1.0.11":
version "1.0.11"
resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-1.0.11.tgz#c58b12e9aa8b159ee39e7dd6cbd0e91d905633c0"
integrity sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==
dependencies:
tunnel "0.0.6"
"@babel/cli@^7.12.10":
version "7.14.8"
resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.14.8.tgz#fac73c0e2328a8af9fd3560c06b096bfa3730933"
@@ -1228,6 +1250,134 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@octokit/auth-token@^2.4.4":
version "2.4.5"
resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.5.tgz#568ccfb8cb46f36441fac094ce34f7a875b197f3"
integrity sha512-BpGYsPgJt05M7/L/5FoE1PiAbdxXFZkX/3kDYcsvd1v6UhlnE5e96dTDr0ezX/EFwciQxf3cNV0loipsURU+WA==
dependencies:
"@octokit/types" "^6.0.3"
"@octokit/core@^3.4.0", "@octokit/core@^3.5.0":
version "3.5.1"
resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.5.1.tgz#8601ceeb1ec0e1b1b8217b960a413ed8e947809b"
integrity sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw==
dependencies:
"@octokit/auth-token" "^2.4.4"
"@octokit/graphql" "^4.5.8"
"@octokit/request" "^5.6.0"
"@octokit/request-error" "^2.0.5"
"@octokit/types" "^6.0.3"
before-after-hook "^2.2.0"
universal-user-agent "^6.0.0"
"@octokit/endpoint@^6.0.1":
version "6.0.12"
resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.12.tgz#3b4d47a4b0e79b1027fb8d75d4221928b2d05658"
integrity sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==
dependencies:
"@octokit/types" "^6.0.3"
is-plain-object "^5.0.0"
universal-user-agent "^6.0.0"
"@octokit/graphql@^4.5.8":
version "4.6.4"
resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.6.4.tgz#0c3f5bed440822182e972317122acb65d311a5ed"
integrity sha512-SWTdXsVheRmlotWNjKzPOb6Js6tjSqA2a8z9+glDJng0Aqjzti8MEWOtuT8ZSu6wHnci7LZNuarE87+WJBG4vg==
dependencies:
"@octokit/request" "^5.6.0"
"@octokit/types" "^6.0.3"
universal-user-agent "^6.0.0"
"@octokit/openapi-types@^9.3.0":
version "9.3.0"
resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-9.3.0.tgz#160347858d727527901c6aae7f7d5c2414cc1f2e"
integrity sha512-oz60hhL+mDsiOWhEwrj5aWXTOMVtQgcvP+sRzX4C3cH7WOK9QSAoEtjWh0HdOf6V3qpdgAmUMxnQPluzDWR7Fw==
"@octokit/openapi-types@^9.5.0":
version "9.7.0"
resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-9.7.0.tgz#9897cdefd629cd88af67b8dbe2e5fb19c63426b2"
integrity sha512-TUJ16DJU8mekne6+KVcMV5g6g/rJlrnIKn7aALG9QrNpnEipFc1xjoarh0PKaAWf2Hf+HwthRKYt+9mCm5RsRg==
"@octokit/plugin-paginate-rest@^2.13.3":
version "2.15.1"
resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.15.1.tgz#264189dd3ce881c6c33758824aac05a4002e056a"
integrity sha512-47r52KkhQDkmvUKZqXzA1lKvcyJEfYh3TKAIe5+EzMeyDM3d+/s5v11i2gTk8/n6No6DPi3k5Ind6wtDbo/AEg==
dependencies:
"@octokit/types" "^6.24.0"
"@octokit/plugin-paginate-rest@^2.6.2":
version "2.15.0"
resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.15.0.tgz#9c956c3710b2bd786eb3814eaf5a2b17392c150d"
integrity sha512-/vjcb0w6ggVRtsb8OJBcRR9oEm+fpdo8RJk45khaWw/W0c8rlB2TLCLyZt/knmC17NkX7T9XdyQeEY7OHLSV1g==
dependencies:
"@octokit/types" "^6.23.0"
"@octokit/plugin-request-log@^1.0.2":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85"
integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==
"@octokit/plugin-rest-endpoint-methods@5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.6.0.tgz#c28833b88d0f07bf94093405d02d43d73c7de99b"
integrity sha512-2G7lIPwjG9XnTlNhe/TRnpI8yS9K2l68W4RP/ki3wqw2+sVeTK8hItPxkqEI30VeH0UwnzpuksMU/yHxiVVctw==
dependencies:
"@octokit/types" "^6.23.0"
deprecation "^2.3.1"
"@octokit/plugin-rest-endpoint-methods@^5.1.1":
version "5.8.0"
resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.8.0.tgz#33b342fe41f2603fdf8b958e6652103bb3ea3f3b"
integrity sha512-qeLZZLotNkoq+it6F+xahydkkbnvSK0iDjlXFo3jNTB+Ss0qIbYQb9V/soKLMkgGw8Q2sHjY5YEXiA47IVPp4A==
dependencies:
"@octokit/types" "^6.25.0"
deprecation "^2.3.1"
"@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.1.0.tgz#9e150357831bfc788d13a4fd4b1913d60c74d677"
integrity sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==
dependencies:
"@octokit/types" "^6.0.3"
deprecation "^2.0.0"
once "^1.4.0"
"@octokit/request@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.6.0.tgz#6084861b6e4fa21dc40c8e2a739ec5eff597e672"
integrity sha512-4cPp/N+NqmaGQwbh3vUsYqokQIzt7VjsgTYVXiwpUP2pxd5YiZB2XuTedbb0SPtv9XS7nzAKjAuQxmY8/aZkiA==
dependencies:
"@octokit/endpoint" "^6.0.1"
"@octokit/request-error" "^2.1.0"
"@octokit/types" "^6.16.1"
is-plain-object "^5.0.0"
node-fetch "^2.6.1"
universal-user-agent "^6.0.0"
"@octokit/rest@^18.6.7":
version "18.8.0"
resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.8.0.tgz#ba24f7ba554f015a7ae2b7cc2aecef5386ddfea5"
integrity sha512-lsuNRhgzGnLMn/NmQTNCit/6jplFWiTUlPXhqN0zCMLwf2/9pseHzsnTW+Cjlp4bLMEJJNPa5JOzSLbSCOahKw==
dependencies:
"@octokit/core" "^3.5.0"
"@octokit/plugin-paginate-rest" "^2.6.2"
"@octokit/plugin-request-log" "^1.0.2"
"@octokit/plugin-rest-endpoint-methods" "5.6.0"
"@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.23.0":
version "6.23.0"
resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.23.0.tgz#b39f242b20036e89fa8f34f7962b4e9b7ff8f65b"
integrity sha512-eG3clC31GSS7K3oBK6C6o7wyXPrkP+mu++eus8CSZdpRytJ5PNszYxudOQ0spWZQ3S9KAtoTG6v1WK5prJcJrA==
dependencies:
"@octokit/openapi-types" "^9.3.0"
"@octokit/types@^6.24.0", "@octokit/types@^6.25.0":
version "6.25.0"
resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.25.0.tgz#c8e37e69dbe7ce55ed98ee63f75054e7e808bf1a"
integrity sha512-bNvyQKfngvAd/08COlYIN54nRgxskmejgywodizQNyiKoXmWRAjKup2/LYwm+T9V0gsKH6tuld1gM0PzmOiB4Q==
dependencies:
"@octokit/openapi-types" "^9.5.0"
"@sinonjs/commons@^1.7.0":
version "1.8.3"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d"
@@ -1572,6 +1722,20 @@ align-text@^0.1.1, align-text@^0.1.3:
longest "^1.0.1"
repeat-string "^1.5.2"
allchange@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/allchange/-/allchange-1.0.0.tgz#f5177b7d97f8e97a2d059a1524db9a72d94dc6d2"
integrity sha512-O0VIaMIORxOaReyYEijDfKdpudJhbzzVYLdJR1aROyUgOLBEp9e5V/TDXQpjX23W90IFCSRZxsDb3exLRD05HA==
dependencies:
"@actions/core" "^1.4.0"
"@actions/github" "^5.0.0"
"@octokit/rest" "^18.6.7"
cli-color "^2.0.0"
js-yaml "^4.1.0"
loglevel "^1.7.1"
semver "^7.3.5"
yargs "^17.0.1"
another-json@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/another-json/-/another-json-0.2.0.tgz#b5f4019c973b6dd5c6506a2d93469cb6d32aeedc"
@@ -1589,6 +1753,11 @@ ansi-escapes@^4.2.1:
dependencies:
type-fest "^0.21.3"
ansi-regex@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
ansi-regex@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
@@ -1636,6 +1805,11 @@ argparse@^1.0.7:
dependencies:
sprintf-js "~1.0.2"
argparse@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
arr-diff@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
@@ -1910,16 +2084,24 @@ bcrypt-pbkdf@^1.0.0:
dependencies:
tweetnacl "^0.14.3"
better-docs@^2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/better-docs/-/better-docs-2.3.2.tgz#0de059301c49669a4350409d8c235868cf8bcbf7"
integrity sha512-VlbXQgEftaynJSaPa853XB5WqTlPoQQr2TnxIkKi6OsyJJxF42Ke+9SIES/hqTe58aaBnuoDGrIzOso8RdNx6Q==
before-after-hook@^2.2.0:
version "2.2.2"
resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e"
integrity sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==
better-docs@^2.4.0-beta.9:
version "2.4.0-beta.9"
resolved "https://registry.yarnpkg.com/better-docs/-/better-docs-2.4.0-beta.9.tgz#9ffa25b90b9a0fe4eb97528faedf53c04c416cfe"
integrity sha512-ehR/4gxVE8+hIdiDN2sP/YOYxbOptUpuXcV0qMcN2snyn+gWAtFBqXmrRUcFJRj+e6HCdD3I2fK8Jy50+cvhVg==
dependencies:
brace "^0.11.1"
marked "^1.1.1"
prism-react-renderer "^1.1.1"
react-ace "^6.5.0"
react-docgen "^5.3.0"
react-frame-component "^4.1.1"
typescript "^3.7.5"
react-live "^2.2.2"
react-simple-code-editor "^0.11.0"
underscore "^1.9.1"
vue-docgen-api "^3.22.0"
vue2-ace-editor "^0.0.13"
@@ -2159,6 +2341,18 @@ bser@2.1.1:
dependencies:
node-int64 "^0.4.0"
buble@0.19.6:
version "0.19.6"
resolved "https://registry.yarnpkg.com/buble/-/buble-0.19.6.tgz#915909b6bd5b11ee03b1c885ec914a8b974d34d3"
integrity sha512-9kViM6nJA1Q548Jrd06x0geh+BG2ru2+RMDkIHHgJY/8AcyCs34lTHwra9BX7YdPrZXd5aarkpr/SY8bmPgPdg==
dependencies:
chalk "^2.4.1"
magic-string "^0.25.1"
minimist "^1.2.0"
os-homedir "^1.0.1"
regexpu-core "^4.2.0"
vlq "^1.0.0"
buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
@@ -2280,7 +2474,7 @@ center-align@^0.1.1:
align-text "^0.1.3"
lazy-cache "^1.0.3"
chalk@^2.0.0:
chalk@^2.0.0, chalk@^2.4.1:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@@ -2359,6 +2553,18 @@ clean-css@^4.1.11:
dependencies:
source-map "~0.6.0"
cli-color@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/cli-color/-/cli-color-2.0.0.tgz#11ecfb58a79278cf6035a60c54e338f9d837897c"
integrity sha512-a0VZ8LeraW0jTuCkuAGMNufareGHhyZU9z8OGsW0gXd1hZGi1SRuNRXdbGkraBBKnhyUhyebFWnRbp+dIn0f0A==
dependencies:
ansi-regex "^2.1.1"
d "^1.0.1"
es5-ext "^0.10.51"
es6-iterator "^2.0.3"
memoizee "^0.4.14"
timers-ext "^0.1.7"
cliui@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1"
@@ -2479,6 +2685,16 @@ component-emitter@^1.2.1:
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
component-props@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/component-props/-/component-props-1.1.1.tgz#f9b7df9b9927b6e6d97c9bd272aa867670f34944"
integrity sha1-+bffm5kntubZfJvScqqGdnDzSUQ=
component-xor@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/component-xor/-/component-xor-0.0.4.tgz#c55d83ccc1b94cd5089a4e93fa7891c7263e59aa"
integrity sha1-xV2DzMG5TNUImk6T+niRxyY+Wao=
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -2544,7 +2760,7 @@ core-js-compat@^3.14.0, core-js-compat@^3.15.0:
browserslist "^4.16.6"
semver "7.0.0"
core-js@^2.4.0, core-js@^2.5.3, core-js@^2.6.5:
core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.3, core-js@^2.6.5:
version "2.6.12"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
@@ -2639,6 +2855,14 @@ cssstyle@^2.3.0:
dependencies:
cssom "~0.3.6"
d@1, d@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==
dependencies:
es5-ext "^0.10.50"
type "^1.0.1"
dash-ast@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/dash-ast/-/dash-ast-1.0.0.tgz#12029ba5fb2f8aa6f0a861795b23c1b4b6c27d37"
@@ -2743,6 +2967,11 @@ delayed-stream@~1.0.0:
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
deprecation@^2.0.0, deprecation@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919"
integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==
deps-sort@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/deps-sort/-/deps-sort-2.0.1.tgz#9dfdc876d2bcec3386b6829ac52162cda9fa208d"
@@ -2818,6 +3047,14 @@ doctypes@^1.1.0:
resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9"
integrity sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=
dom-iterator@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/dom-iterator/-/dom-iterator-1.0.0.tgz#9c09899846ec41c2d257adc4d6015e4759ef05ad"
integrity sha512-7dsMOQI07EMU98gQM8NSB3GsAiIeBYIPKpnxR3c9xOvdvBjChAcOM0iJ222I3p5xyiZO9e5oggkNaCusuTdYig==
dependencies:
component-props "1.1.1"
component-xor "0.0.4"
domain-browser@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
@@ -2951,6 +3188,42 @@ es-to-primitive@^1.2.1:
is-date-object "^1.0.1"
is-symbol "^1.0.2"
es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@^0.10.51, es5-ext@^0.10.53, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46:
version "0.10.53"
resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1"
integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==
dependencies:
es6-iterator "~2.0.3"
es6-symbol "~3.1.3"
next-tick "~1.0.0"
es6-iterator@^2.0.3, es6-iterator@~2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c=
dependencies:
d "1"
es5-ext "^0.10.35"
es6-symbol "^3.1.1"
es6-symbol@^3.1.1, es6-symbol@~3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18"
integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==
dependencies:
d "^1.0.1"
ext "^1.1.2"
es6-weak-map@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53"
integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==
dependencies:
d "1"
es5-ext "^0.10.46"
es6-iterator "^2.0.3"
es6-symbol "^3.1.1"
escalade@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@@ -2983,9 +3256,9 @@ eslint-config-google@^0.14.0:
resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.14.0.tgz#4f5f8759ba6e11b424294a219dbfa18c508bcc1a"
integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==
"eslint-plugin-matrix-org@github:matrix-org/eslint-plugin-matrix-org#main":
version "0.3.4"
resolved "https://codeload.github.com/matrix-org/eslint-plugin-matrix-org/tar.gz/45f6937539192e3820edcafc4d6d4d4187e85a6a"
"eslint-plugin-matrix-org@github:matrix-org/eslint-plugin-matrix-org#2306b3d4da4eba908b256014b979f1d3d43d2945":
version "0.3.5"
resolved "https://codeload.github.com/matrix-org/eslint-plugin-matrix-org/tar.gz/2306b3d4da4eba908b256014b979f1d3d43d2945"
eslint-rule-composer@^0.3.0:
version "0.3.0"
@@ -3119,6 +3392,14 @@ esutils@^2.0.2:
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
event-emitter@^0.3.5:
version "0.3.5"
resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
integrity sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=
dependencies:
d "1"
es5-ext "~0.10.14"
events@^3.0.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
@@ -3218,6 +3499,13 @@ expect@^26.6.2:
jest-message-util "^26.6.2"
jest-regex-util "^26.0.0"
ext@^1.1.2:
version "1.4.0"
resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244"
integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==
dependencies:
type "^2.0.0"
extend-shallow@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
@@ -4079,12 +4367,17 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4:
dependencies:
isobject "^3.0.1"
is-plain-object@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
is-potential-custom-element-name@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
is-promise@^2.0.0:
is-promise@^2.0.0, is-promise@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
@@ -4643,6 +4936,13 @@ js-yaml@^3.13.1:
argparse "^1.0.7"
esprima "^4.0.0"
js-yaml@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
dependencies:
argparse "^2.0.1"
js2xmlparser@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/js2xmlparser/-/js2xmlparser-4.0.1.tgz#670ef71bc5661f089cc90481b99a05a1227ae3bd"
@@ -4949,6 +5249,20 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
lru-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3"
integrity sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=
dependencies:
es5-ext "~0.10.2"
magic-string@^0.25.1:
version "0.25.7"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051"
integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==
dependencies:
sourcemap-codec "^1.4.4"
make-dir@^2.0.0, make-dir@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
@@ -4999,6 +5313,11 @@ markdown-it@^10.0.0:
mdurl "^1.0.1"
uc.micro "^1.0.5"
marked@^1.1.1:
version "1.2.9"
resolved "https://registry.yarnpkg.com/marked/-/marked-1.2.9.tgz#53786f8b05d4c01a2a5a76b7d1ec9943d29d72dc"
integrity sha512-H8lIX2SvyitGX+TRdtS06m1jHMijKN/XjfH6Ooii9fvxMlh8QdqBfBDkGUpMWH2kQNrtixjzYUa3SH8ROTgRRw==
marked@^2.0.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/marked/-/marked-2.1.3.tgz#bd017cef6431724fd4b27e0657f5ceb14bff3753"
@@ -5026,6 +5345,20 @@ mdurl@^1.0.1:
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=
memoizee@^0.4.14:
version "0.4.15"
resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.15.tgz#e6f3d2da863f318d02225391829a6c5956555b72"
integrity sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==
dependencies:
d "^1.0.1"
es5-ext "^0.10.53"
es6-weak-map "^2.0.3"
event-emitter "^0.3.5"
is-promise "^2.2.2"
lru-queue "^0.1.0"
next-tick "^1.1.0"
timers-ext "^0.1.7"
merge-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@@ -5211,6 +5544,16 @@ neo-async@^2.6.1:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
next-tick@1, next-tick@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb"
integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==
next-tick@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
nice-try@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
@@ -5223,6 +5566,11 @@ node-dir@^0.1.10:
dependencies:
minimatch "^3.0.2"
node-fetch@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
node-int64@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
@@ -5406,6 +5754,11 @@ os-browserify@~0.3.0:
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
os-homedir@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
p-each-series@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.2.0.tgz#105ab0357ce72b202a8a8b94933672657b5e2a9a"
@@ -5638,6 +5991,11 @@ pretty-format@^26.0.0, pretty-format@^26.6.2:
ansi-styles "^4.0.0"
react-is "^17.0.1"
prism-react-renderer@^1.0.1, prism-react-renderer@^1.1.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.2.1.tgz#392460acf63540960e5e3caa699d851264e99b89"
integrity sha512-w23ch4f75V1Tnz8DajsYKvY5lF7H1+WvzvLUcF0paFxkTHSp42RS0H5CttdN2Q8RR3DRGZ9v5xD/h3n8C8kGmg==
private@^0.1.8:
version "0.1.8"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
@@ -5673,7 +6031,7 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@^15.7.2:
prop-types@^15.5.8, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -5917,6 +6275,29 @@ react-is@^17.0.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
react-live@^2.2.2:
version "2.2.3"
resolved "https://registry.yarnpkg.com/react-live/-/react-live-2.2.3.tgz#260f99194213799f0005e473e7a4154c699d6a7c"
integrity sha512-tpKruvfytNETuzO3o1mrQUj180GVrq35IE8F5gH1NJVPt4szYCx83/dOSCOyjgRhhc3gQvl0pQ3k/CjOjwJkKQ==
dependencies:
buble "0.19.6"
core-js "^2.4.1"
dom-iterator "^1.0.0"
prism-react-renderer "^1.0.1"
prop-types "^15.5.8"
react-simple-code-editor "^0.10.0"
unescape "^1.0.1"
react-simple-code-editor@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/react-simple-code-editor/-/react-simple-code-editor-0.10.0.tgz#73e7ac550a928069715482aeb33ccba36efe2373"
integrity sha512-bL5W5mAxSW6+cLwqqVWY47Silqgy2DKDTR4hDBrLrUqC5BXc29YVx17l2IZk5v36VcDEq1Bszu2oHm1qBwKqBA==
react-simple-code-editor@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/react-simple-code-editor/-/react-simple-code-editor-0.11.0.tgz#bb57c7c29b570f2ab229872599eac184f5bc673c"
integrity sha512-xGfX7wAzspl113ocfKQAR8lWPhavGWHL3xSzNLeseDRHysT+jzRBi/ExdUqevSMos+7ZtdfeuBOXtgk9HTwsrw==
read-only-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/read-only-stream/-/read-only-stream-2.0.0.tgz#2724fd6a8113d73764ac288d4386270c1dbf17f0"
@@ -6054,7 +6435,7 @@ regexpp@^3.1.0:
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
regexpu-core@^4.7.1:
regexpu-core@^4.2.0, regexpu-core@^4.7.1:
version "4.7.1"
resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6"
integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==
@@ -6474,6 +6855,11 @@ source-map@^0.7.3, source-map@~0.7.2:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
sourcemap-codec@^1.4.4:
version "1.4.8"
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
spdx-correct@^3.0.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
@@ -6788,6 +7174,14 @@ timers-browserify@^1.0.1:
dependencies:
process "~0.11.0"
timers-ext@^0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.7.tgz#6f57ad8578e07a3fb9f91d9387d65647555e25c6"
integrity sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==
dependencies:
es5-ext "~0.10.46"
next-tick "1"
tmatch@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/tmatch/-/tmatch-2.0.1.tgz#0c56246f33f30da1b8d3d72895abaf16660f38cf"
@@ -6925,6 +7319,11 @@ tunnel-agent@^0.6.0:
dependencies:
safe-buffer "^5.0.1"
tunnel@0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
@@ -6964,6 +7363,16 @@ type-fest@^0.8.1:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
type@^1.0.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0"
integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
type@^2.0.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/type/-/type-2.5.0.tgz#0a2e78c2e77907b252abe5f298c1b01c63f0db3d"
integrity sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==
typedarray-to-buffer@^3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
@@ -6976,7 +7385,7 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typescript@^3.2.2, typescript@^3.7.5:
typescript@^3.2.2:
version "3.9.10"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8"
integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==
@@ -7056,6 +7465,13 @@ underscore@^1.9.1, underscore@~1.13.1:
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.1.tgz#0c1c6bd2df54b6b69f2314066d65b6cde6fcf9d1"
integrity sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==
unescape@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/unescape/-/unescape-1.0.1.tgz#956e430f61cad8a4d57d82c518f5e6cc5d0dda96"
integrity sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==
dependencies:
extend-shallow "^2.0.1"
unhomoglyph@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/unhomoglyph/-/unhomoglyph-1.0.6.tgz#ea41f926d0fcf598e3b8bb2980c2ddac66b081d3"
@@ -7094,6 +7510,11 @@ union-value@^1.0.0:
is-extendable "^0.1.1"
set-value "^2.0.1"
universal-user-agent@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee"
integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==
universalify@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
@@ -7211,6 +7632,11 @@ verror@1.10.0:
core-util-is "1.0.2"
extsprintf "^1.2.0"
vlq@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/vlq/-/vlq-1.0.1.tgz#c003f6e7c0b4c1edd623fd6ee50bbc0d6a1de468"
integrity sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==
vm-browserify@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
@@ -7523,6 +7949,19 @@ yargs@^16.2.0:
y18n "^5.0.5"
yargs-parser "^20.2.2"
yargs@^17.0.1:
version "17.0.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.0.1.tgz#6a1ced4ed5ee0b388010ba9fd67af83b9362e0bb"
integrity sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ==
dependencies:
cliui "^7.0.2"
escalade "^3.1.1"
get-caller-file "^2.0.5"
require-directory "^2.1.1"
string-width "^4.2.0"
y18n "^5.0.5"
yargs-parser "^20.2.2"
yargs@~3.10.0:
version "3.10.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"