Compare commits
256 Commits
v12.2.0-rc.1
...
v12.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
| f8186add92 | |||
| c15f22e81d | |||
| 121e8a51c1 | |||
| 88e3ada701 | |||
| 92d822d494 | |||
| 62c93f54cc | |||
| 96e8f65af7 | |||
| 8fb036ba2d | |||
| 0a10fa12ef | |||
| e30dad7913 | |||
| c2a0c02898 | |||
| 7c1e89f86a | |||
| 29f8a4cba2 | |||
| 3216d7e5a7 | |||
| b68f649414 | |||
| 543f35d9a8 | |||
| 03d42a98cd | |||
| 4f48554cd2 | |||
| 85d9c0f403 | |||
| 66b4279dd8 | |||
| 1ee84fbeca | |||
| c0e15b206a | |||
| 290575ba65 | |||
| bd8690de57 | |||
| 0d09f87777 | |||
| 0f45b7e516 | |||
| 4c552cc350 | |||
| 6a87f54292 | |||
| bfb2c5aad0 | |||
| db898e7bcf | |||
| dbebbe3a19 | |||
| 10e8aff46a | |||
| 81e2ea6a61 | |||
| 2594c59250 | |||
| b56a4ee6ec | |||
| ab524ad3b4 | |||
| 83bf80feec | |||
| b9e5417218 | |||
| 082169a756 | |||
| cdc87d371c | |||
| f8ec3bc591 | |||
| 85f4651e9b | |||
| da53241c48 | |||
| fa265296e8 | |||
| ccf296ea92 | |||
| d65b48204b | |||
| 977720cee4 | |||
| 3c7cdb1da8 | |||
| dd80e744ff | |||
| 0b12e37459 | |||
| 51e815f0cb | |||
| f3856d569d | |||
| e6d1909f0b | |||
| 408976a199 | |||
| 4da49d926b | |||
| 75750ed760 | |||
| 2f74779bf1 | |||
| 7b038393b1 | |||
| 768c0e7f77 | |||
| c6009b1056 | |||
| 2a280afa88 | |||
| be980f4bc9 | |||
| 78118e9442 | |||
| a2c10a7913 | |||
| 7c4f7c9dad | |||
| 62058e6d48 | |||
| a2f514b544 | |||
| 67434bc5d4 | |||
| 167183775e | |||
| 4ce6313126 | |||
| 1298ed61b6 | |||
| 14b424ee94 | |||
| 2bc1f2fe21 | |||
| b392dbf6f8 | |||
| 696b3ef1ce | |||
| 4e2ee3b3a8 | |||
| 1f9fab9a0c | |||
| 6170796ed8 | |||
| d91d1cea34 | |||
| 69ba32683c | |||
| fc78e63ad1 | |||
| db212a555d | |||
| 429f32dacb | |||
| ad036104e6 | |||
| 4607f0177c | |||
| d7dbaeba46 | |||
| 3e94db1837 | |||
| 4fd77c2f05 | |||
| 9fe05e7d40 | |||
| 946dcd0301 | |||
| 97d718e850 | |||
| 913b2d148c | |||
| 916f6a6126 | |||
| 7dbee19456 | |||
| 2375b00b52 | |||
| 13cdca028f | |||
| 264636c879 | |||
| 9b50455049 | |||
| a531446396 | |||
| f876c35283 | |||
| 1bcec53c6b | |||
| 0ad5ef4e9b | |||
| 94726aa1d5 | |||
| 8da2f91e22 | |||
| b03555a50b | |||
| b61312feca | |||
| ea9459d330 | |||
| 3edfe70a7b | |||
| 5482a19979 | |||
| e1678aaf11 | |||
| 2bdb570f3c | |||
| 3723b2da39 | |||
| b63af00579 | |||
| 4ed6fbe5fe | |||
| 026260502b | |||
| c45bc91389 | |||
| 69358f8372 | |||
| 8800c6d2e0 | |||
| b63de6a902 | |||
| b1cec7d6a5 | |||
| 08821e499c | |||
| 3ca84cfc49 | |||
| be8ecce995 | |||
| 775c3465bb | |||
| 7484e3c032 | |||
| b93e79274f | |||
| fee6a96350 | |||
| ac2c09108f | |||
| 5b13785115 | |||
| dd3c53edac | |||
| 904071ebe6 | |||
| 7e0aeb425d | |||
| f4e985b5cc | |||
| 03134832e9 | |||
| dbb0f66094 | |||
| 4631cd1fe3 | |||
| bc07ad5909 | |||
| 8b0c4a0efb | |||
| eac77a6695 | |||
| aae8f1c706 | |||
| 1e281f6838 | |||
| aef89e4a26 | |||
| 8a1bc98d4e | |||
| 8b2d5959ec | |||
| 4891446dc3 | |||
| cace8f232f | |||
| 88b8b24629 | |||
| b15ba8c5e3 | |||
| 58c60d85e7 | |||
| 529fe93ab1 | |||
| 600438d02e | |||
| 21a357c433 | |||
| 9c05077c26 | |||
| d49d6936a5 | |||
| e6696f78f4 | |||
| f2da3ba6ae | |||
| db5de4c6e4 | |||
| ca042b3647 | |||
| 87fd3521ea | |||
| b3c66848e2 | |||
| 606aa29381 | |||
| 2d25150a21 | |||
| ca7a457094 | |||
| 23ab3e3ec0 | |||
| b11a8459d8 | |||
| 057eb0f2a5 | |||
| 8de6c5aad1 | |||
| 6fda2a0c57 | |||
| efeeba265f | |||
| 35ca7fdc48 | |||
| 959e3f68a4 | |||
| 19f90f56be | |||
| a69d9a9f20 | |||
| c5e113d0ca | |||
| 1df90f8fc7 | |||
| 8e6040ad6f | |||
| 21f107d734 | |||
| 019abc7ad8 | |||
| db4b6fe2af | |||
| c340e6f1ba | |||
| f227a307c1 | |||
| 011a2acfd2 | |||
| c2b5b14d26 | |||
| fcbbcc0398 | |||
| ceafb1c5d0 | |||
| 80d7c88b40 | |||
| bee840ce16 | |||
| 019e93e76c | |||
| 1607ad2111 | |||
| f1a0c46a29 | |||
| 0318c65847 | |||
| e775bcac3c | |||
| 641c852ee2 | |||
| ca37ae6794 | |||
| c1625e5c27 | |||
| 0e05f9fd73 | |||
| ef7595bb06 | |||
| 090c2c0891 | |||
| d8fd12103a | |||
| 5724462c2c | |||
| 0a0489750c | |||
| f46190509a | |||
| 75321220fd | |||
| 43198b0425 | |||
| 4aa2c03ff0 | |||
| db4c6af472 | |||
| 87689ca733 | |||
| a1a5d85979 | |||
| b1459a43ef | |||
| 8797381f44 | |||
| 92e89ffbcf | |||
| c2da4376e0 | |||
| 78ebf8f117 | |||
| 5de189bfa3 | |||
| 44fc820f99 | |||
| 5a8e5a9785 | |||
| 960c5da3b2 | |||
| 00a28e743d | |||
| 2596c25ccc | |||
| 582aafa552 | |||
| 61e7d4f807 | |||
| f2c215311f | |||
| a13cf0e1e0 | |||
| ff60bbac9d | |||
| 16f569136b | |||
| 27c172361f | |||
| 1e0d6b9d4a | |||
| b67cd94ee2 | |||
| c6764490c6 | |||
| 18580624e6 | |||
| 7b333a34b5 | |||
| d0707e183d | |||
| fa3b246de5 | |||
| df28d87d25 | |||
| fc68bb3ae0 | |||
| 82c530da95 | |||
| d250e7387c | |||
| cbc74815d8 | |||
| e9b802deb3 | |||
| 377ca0c678 | |||
| 972aef7a9d | |||
| a35559be65 | |||
| 0e6b43a769 | |||
| 8c8a68d3ae | |||
| 4d74b5cdad | |||
| b1ace49f9a | |||
| 640d13af99 | |||
| 50e0f6353a | |||
| 30f2263443 | |||
| 25eb6de220 | |||
| cebdc44689 | |||
| 23f5c2e03f | |||
| 4d4a6ede21 | |||
| 6a920fe623 | |||
| 631faa2046 | |||
| a4e853e1d4 |
@@ -0,0 +1 @@
|
||||
* @matrix-org/element-web
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ` +
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -61,7 +61,7 @@ interface IBlockedMap {
|
||||
};
|
||||
}
|
||||
|
||||
interface IOlmDevice<T = DeviceInfo> {
|
||||
export interface IOlmDevice<T = DeviceInfo> {
|
||||
userId: string;
|
||||
deviceInfo: T;
|
||||
}
|
||||
|
||||
+3
-4
@@ -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
@@ -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
|
||||
|
||||
+194
-105
@@ -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);
|
||||
}
|
||||
};
|
||||
});
|
||||
+196
-143
@@ -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';
|
||||
+98
-83
@@ -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));
|
||||
}
|
||||
+165
-120
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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}
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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[];
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"noEmit": false,
|
||||
"emitDecoratorMetadata": true,
|
||||
"outDir": "./lib",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"exclude": [
|
||||
"./spec/**/*.ts"
|
||||
]
|
||||
}
|
||||
+5
-9
@@ -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",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user