Compare commits
602 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f1db4dc668 | |||
| 98c1710ac1 | |||
| 3141a7d7c1 | |||
| 1304e811d0 | |||
| 2ce1e7e6ef | |||
| 70efed1a58 | |||
| 9e4f109e80 | |||
| 5d54bf558c | |||
| fc5f0e8047 | |||
| 6bc584ba8b | |||
| ac4e504b8d | |||
| 0abee2a0bf | |||
| 4e4afdb795 | |||
| 9b429c1902 | |||
| b782dee2ef | |||
| 54e815085f | |||
| 0739da4ef4 | |||
| c300a6bdd6 | |||
| 901d53eb38 | |||
| a54471f737 | |||
| 1fae9cb3ef | |||
| 124bfc9328 | |||
| 53aa34fba5 | |||
| 58756a1973 | |||
| 7d9800b817 | |||
| e408590c21 | |||
| 735ccca18b | |||
| 946f47e037 | |||
| 4de1699c49 | |||
| 3d9221f054 | |||
| 1d1d59c757 | |||
| af43687354 | |||
| 2ec5acb55d | |||
| 2128f67dc3 | |||
| 55dda8420c | |||
| d595717e60 | |||
| 4603d4e578 | |||
| 12e525b664 | |||
| 1ac4cc4b11 | |||
| 7b218905fb | |||
| 080426dfdd | |||
| a89c1990d6 | |||
| 4e72290d53 | |||
| 7a7318b636 | |||
| 74d24f38f7 | |||
| 78dcae9143 | |||
| 0db640a679 | |||
| a3ddfd519b | |||
| e86d8861b9 | |||
| 2910e62bb6 | |||
| 2bb14698a5 | |||
| 2fd08d72dc | |||
| 222f62164c | |||
| bf8ee39a3f | |||
| cc1d1d76d0 | |||
| 30fdd96168 | |||
| cfad8d3614 | |||
| d9c3b880fc | |||
| 25c115739c | |||
| d97c514b8d | |||
| 6b822ccd61 | |||
| 47c5c4645e | |||
| ea0eaff212 | |||
| 41bf8c2d5f | |||
| d473a2e095 | |||
| d398ffd6df | |||
| ffab55452a | |||
| 9120b1dfd9 | |||
| 5d4e3183aa | |||
| 07171a95c4 | |||
| 12afcd3850 | |||
| 2d23330b74 | |||
| 7faff66006 | |||
| b07457726b | |||
| 6bf8142ff6 | |||
| cf0ccaf93d | |||
| 34a3955b60 | |||
| 82122872bf | |||
| 9c242a9ce6 | |||
| 51f3fac87b | |||
| 66b98844a2 | |||
| d03db00e4c | |||
| cf8d2bf6ef | |||
| 913a0b51a6 | |||
| ede0f696ee | |||
| d05214a169 | |||
| 1e93d0b19f | |||
| 5e125e646c | |||
| 1643201363 | |||
| 8e99e084a8 | |||
| 67d362fdd9 | |||
| e66430ada6 | |||
| 52ca60142a | |||
| 7d2a5afa6d | |||
| 02ba233644 | |||
| 50493a3330 | |||
| 8434f29c54 | |||
| a34426a7f6 | |||
| 033693283d | |||
| fab7b7f26a | |||
| 230e3b4ace | |||
| a50a627300 | |||
| 80930f6690 | |||
| 652b3a9208 | |||
| 0a8b825702 | |||
| f4e2a38f4b | |||
| b3474531c9 | |||
| f0597e0124 | |||
| 2fdc91bd04 | |||
| 16ca09eed8 | |||
| 129fb7f10f | |||
| 016e24472a | |||
| 4acb98c496 | |||
| 96d1b30012 | |||
| 6fc586598a | |||
| 2d9c938765 | |||
| 83d1a6c046 | |||
| 87393c7db8 | |||
| bd47667e63 | |||
| cfd865bf8b | |||
| 9b54df7b2b | |||
| 5da562fa6f | |||
| 83d952eae8 | |||
| f30be87879 | |||
| ab19480cc5 | |||
| 99451698a4 | |||
| 48dbed8001 | |||
| bae883a891 | |||
| 2aae2362e3 | |||
| f780e1dbc3 | |||
| fdc11b5e53 | |||
| 1a0bdb0f98 | |||
| e870bebd3d | |||
| dd0ca91aa9 | |||
| 36906e2ddb | |||
| 3eaed30446 | |||
| 72643026a3 | |||
| d6980cb8fa | |||
| dcd24fd516 | |||
| 75cc873d77 | |||
| dd23a1a401 | |||
| 6ac84a2465 | |||
| e4d3124e72 | |||
| df11a9e832 | |||
| e4703989fe | |||
| feb83ba161 | |||
| 22a98b3e4f | |||
| 934a0e8943 | |||
| 963c7690b6 | |||
| aeec4aa4a8 | |||
| 169b6b5572 | |||
| 6244d77d44 | |||
| f4839a3b4f | |||
| 4b21b67a45 | |||
| 799a83a115 | |||
| f3ebb2a314 | |||
| c0fbed914e | |||
| dd8c157bb9 | |||
| 8bd2d640f2 | |||
| 80aaa6c32b | |||
| 4e9fe8f53f | |||
| f926a2f777 | |||
| f8097221e6 | |||
| 95e7a76ba9 | |||
| 72bc13eb39 | |||
| 2206b80e65 | |||
| 39a8399977 | |||
| b7247fcedc | |||
| e4494707fa | |||
| 45b7cab203 | |||
| 4dfc3b9dbb | |||
| 15d05cd92f | |||
| ba1b1cc6fa | |||
| b716cde9b5 | |||
| 46a0645865 | |||
| 2b30f45f60 | |||
| 87b920698f | |||
| 4f4811e1d9 | |||
| db9936e07c | |||
| 7dcee2eb40 | |||
| 6b34ea6fe5 | |||
| b33b01df0f | |||
| 1c895432ed | |||
| ddd6a05198 | |||
| 0ccde7807f | |||
| 9f97992196 | |||
| 67b8da719f | |||
| 04fad564f7 | |||
| aee698c285 | |||
| 55b489b17a | |||
| c5d188c30a | |||
| 6fff3d6db5 | |||
| 8043d83921 | |||
| b2d83c1f80 | |||
| 1b91cd7037 | |||
| 0baf926c84 | |||
| 763e25e031 | |||
| 861023b3f6 | |||
| 1f5a83994b | |||
| af523522de | |||
| c3a266b3e7 | |||
| f41d815aa6 | |||
| ad8a93dde8 | |||
| b07d44a6c0 | |||
| 1dc899ba6e | |||
| 0b1e3edaff | |||
| 9fb4fed044 | |||
| 730b0b121d | |||
| 9d9d9e2cfa | |||
| 43bc09f392 | |||
| 195498e9db | |||
| 50332c4999 | |||
| 9e46646b16 | |||
| a74a03e414 | |||
| fe8b9eca8e | |||
| a7cc1ae630 | |||
| 14e008bbad | |||
| 6a12c265cf | |||
| c35cb57a79 | |||
| 51c776f51e | |||
| 7ce0eb26d4 | |||
| f0091fa811 | |||
| 25c88bc986 | |||
| ab06f57965 | |||
| c46209a159 | |||
| 0c47e27f9f | |||
| 0385f265e8 | |||
| 59b3960a42 | |||
| d5ab4ba2ef | |||
| f59b81f46b | |||
| 4c39ce8c96 | |||
| 94ff0ea4cd | |||
| 526758a07f | |||
| 9686fba81e | |||
| bbc547fd7f | |||
| 7d5dbeaf2d | |||
| 8c19cf45e2 | |||
| c9277de8ee | |||
| b5baca51a6 | |||
| fc800821f9 | |||
| efbc46937f | |||
| 9ceeb2d220 | |||
| 7b89862ed0 | |||
| 6804e4290a | |||
| 0a3d820fda | |||
| e2a9c95ea8 | |||
| 3aefc9f02e | |||
| 0042cb5292 | |||
| b288c97372 | |||
| a0de1740a4 | |||
| 8e2e3018d8 | |||
| 60e93024e7 | |||
| d89aa365f9 | |||
| 4241263f2c | |||
| b009f9c21b | |||
| 04ccec6de3 | |||
| e7b41fadd0 | |||
| 2dc1acef2e | |||
| b977b629f4 | |||
| 6bbd493e20 | |||
| 38ad435af5 | |||
| 8f245af317 | |||
| 9b2bfbe7d0 | |||
| f69ea85e55 | |||
| 7559a10526 | |||
| 68dbe959bb | |||
| 74a875aa23 | |||
| 4b679c5056 | |||
| cb8a260b9c | |||
| 7d7975b8bb | |||
| f12513da96 | |||
| 785a3e3381 | |||
| f5fcc20840 | |||
| 17e20c17b3 | |||
| 39fd350ffe | |||
| b4d100baff | |||
| 29bc83f4a7 | |||
| 13c8774d45 | |||
| c9212e770d | |||
| 08aa7803e3 | |||
| 770858c255 | |||
| c73bb7d408 | |||
| 15cd675354 | |||
| e8137d3f88 | |||
| c63abe9988 | |||
| a7a08fd760 | |||
| df51f95fbd | |||
| a14cf1a339 | |||
| 30758600f0 | |||
| 823242a93c | |||
| 482e3c4cb7 | |||
| 1dbd7158ad | |||
| 46b3f0babd | |||
| b4bc554d7a | |||
| 1485969493 | |||
| 92f7ddcf3e | |||
| 90d480b4cf | |||
| 2515d07c8f | |||
| 7acb9416c2 | |||
| 095e3a8a88 | |||
| 92623a246a | |||
| 0c4212ca90 | |||
| 9a06a05a75 | |||
| f05a0615f7 | |||
| 6b93687ff0 | |||
| 8d910d5e26 | |||
| 9264050f41 | |||
| eab0c54663 | |||
| 82a254b7bd | |||
| 0f6a59ed98 | |||
| afb4682987 | |||
| a18326519b | |||
| b033540f95 | |||
| ff529d733d | |||
| 5951958c05 | |||
| 886f2fb95d | |||
| f6ce0c581e | |||
| 5ce68b6c12 | |||
| c748aebfc5 | |||
| f8ec445ff0 | |||
| 5d4f347eaa | |||
| 1359c0574e | |||
| b1d239b292 | |||
| 70031415e8 | |||
| 3c97d17770 | |||
| 5a73a9758a | |||
| 6848e96244 | |||
| b53566e074 | |||
| 012b914a97 | |||
| 3c6eed0135 | |||
| f2a2e7f5dd | |||
| 15b6a9a257 | |||
| b83977e72f | |||
| 8f5082cd2f | |||
| 50c877cb60 | |||
| e3e7fd5900 | |||
| 69823f9cae | |||
| e5dcdf109a | |||
| 5c6a643436 | |||
| 1ca1a69eb8 | |||
| 76ea1ba192 | |||
| 8cd8ec134e | |||
| 6c649e2bfe | |||
| 9f42647a75 | |||
| 9747e5e0d5 | |||
| a4cacbc73a | |||
| f24840d09c | |||
| acb7991cc5 | |||
| 6addc7dd3d | |||
| 2bd60f4160 | |||
| e71362a6df | |||
| 673c09cd77 | |||
| 4606422e57 | |||
| 4bc8f968e0 | |||
| 35ee58dfe1 | |||
| 3705426b6d | |||
| 5813aea27f | |||
| 5ba836a31b | |||
| 4f387d15b9 | |||
| 8ab0246ca2 | |||
| ed68bd4108 | |||
| 3d39e8ec26 | |||
| 53bdab7cc8 | |||
| fafa55ba1d | |||
| 3a818c1419 | |||
| 422853298c | |||
| f3a2bd6b40 | |||
| 6834137f50 | |||
| c4d7fef0cd | |||
| 556b5ff628 | |||
| 2daa1ec4ce | |||
| 348ea72b70 | |||
| 4e14d810c5 | |||
| f84905b003 | |||
| 9bb87145f6 | |||
| c7675570aa | |||
| be942fd66a | |||
| 8544aca362 | |||
| c3308e1de9 | |||
| 047d7125a1 | |||
| cf4d3117c1 | |||
| 88c2844e48 | |||
| 6ca4694fe6 | |||
| 062287e270 | |||
| 50e0d76f10 | |||
| df039d3d8c | |||
| 799606b73c | |||
| c048f5d357 | |||
| f712a8cecb | |||
| e1d8d6bf4e | |||
| 14ed8a110c | |||
| cb6700f21b | |||
| d4f35bf07a | |||
| b4eb29138b | |||
| 8ca3a071f9 | |||
| 5abd211436 | |||
| 99f428e091 | |||
| a734fdf9b0 | |||
| 6f91b2bc80 | |||
| 32d7272939 | |||
| c8d85452bf | |||
| 557f800919 | |||
| 2119e88c78 | |||
| 24e7a7e6bf | |||
| 4e5847f356 | |||
| 72d580edfd | |||
| e878b7683b | |||
| fac700bf4d | |||
| 894c24880d | |||
| d742249fd1 | |||
| 9c100fea48 | |||
| 7daab62850 | |||
| fed0ced89d | |||
| df9cfaed1d | |||
| edd614dd0c | |||
| 375b228bb2 | |||
| 7143ef8a32 | |||
| 1bd7de5a18 | |||
| a1f73eee86 | |||
| d9f8710758 | |||
| 3a9d5439a2 | |||
| 5d1be6e8be | |||
| 775e85a2e8 | |||
| dc8f403c44 | |||
| cf9fa991d1 | |||
| d26dc49755 | |||
| be04559a66 | |||
| 0722dafe58 | |||
| ab906faa17 | |||
| 9d63a12469 | |||
| f40d0d24c7 | |||
| 2cdea36abf | |||
| 8d4f4c8832 | |||
| 3b4dcbb01d | |||
| b52f3b1c57 | |||
| 2cdf5629e5 | |||
| 97b4f0e369 | |||
| a508c8d236 | |||
| 7911e6371a | |||
| bbace01e9a | |||
| 87469fea63 | |||
| 48a9db0315 | |||
| 33c9471112 | |||
| 04fdcbb672 | |||
| 4d2286c5be | |||
| 620e45e4dc | |||
| fb97e74344 | |||
| 3bbb7df72d | |||
| 58b08821bf | |||
| 19bdfd5b84 | |||
| b6c857e2a2 | |||
| 0991626e85 | |||
| 7b10d4fac0 | |||
| 980dacaa87 | |||
| 3790e8abb4 | |||
| 14ba851daf | |||
| 95d80772d7 | |||
| 341e936082 | |||
| e87399c4a1 | |||
| a7678ea1e0 | |||
| 0388bd3b48 | |||
| f27327164b | |||
| a0f85075c1 | |||
| e97d18a03b | |||
| f07cdb43b1 | |||
| da8c8f166d | |||
| 0830a30498 | |||
| 68a2762b75 | |||
| 666e471369 | |||
| d89f4a74f9 | |||
| 191535d01a | |||
| 241203f804 | |||
| 26cc7a0f64 | |||
| 324f9e58ea | |||
| e71519c638 | |||
| 9b6cee0cab | |||
| 2a5f6a7b3d | |||
| cae03817cb | |||
| 37736184a0 | |||
| 9431a52abe | |||
| 13c664ad34 | |||
| d7640d9e15 | |||
| 3c36be9839 | |||
| 9dce7bbb97 | |||
| fa44300abc | |||
| 2783d162b7 | |||
| 39566dbd4f | |||
| f8186add92 | |||
| c15f22e81d | |||
| 052ac8f95b | |||
| c37f8ba4c7 | |||
| 7e2458d5b5 | |||
| fe989177bb | |||
| 412645f025 | |||
| e527c46f34 | |||
| da471b57ad | |||
| a4ff97c8df | |||
| 411fc47f28 | |||
| b97e3113d1 | |||
| f107d63fab | |||
| a65ac5a82b | |||
| c7c1db00e8 | |||
| 824f93daa3 | |||
| 9fec2f8c58 | |||
| 0b46389176 | |||
| db03f16b8e | |||
| f83fcb1a18 | |||
| ba0f1f86b9 | |||
| 81d349d993 | |||
| d65d2b94b9 | |||
| acd52bc070 | |||
| a64339763f | |||
| 4f21b95bbf | |||
| 03e4dcf1ed | |||
| 3c56d1372d | |||
| 27ecdef09b | |||
| 121e8a51c1 | |||
| 88e3ada701 | |||
| 92d822d494 | |||
| 62c93f54cc | |||
| 500c2ed4f2 | |||
| 425362edfe | |||
| 9c554f9b3d | |||
| b7a5f81f95 | |||
| 0746a64c83 | |||
| 22cd475d22 | |||
| 6904f6b36f | |||
| e25a649e1a | |||
| 0eb8cbee4d | |||
| 96e8f65af7 | |||
| 8fb036ba2d | |||
| 0a10fa12ef | |||
| e30dad7913 | |||
| c2a0c02898 | |||
| 7c1e89f86a | |||
| 29f8a4cba2 | |||
| b884accc99 | |||
| d56540320e | |||
| 526fe7e9a4 | |||
| aa696c4c15 | |||
| 3216d7e5a7 | |||
| 4f48554cd2 | |||
| c0e15b206a | |||
| 290575ba65 | |||
| bd8690de57 | |||
| 0d09f87777 | |||
| 0f45b7e516 | |||
| 4c552cc350 | |||
| 6a87f54292 | |||
| bfb2c5aad0 | |||
| db898e7bcf | |||
| dbebbe3a19 | |||
| ccf296ea92 | |||
| d65b48204b | |||
| 977720cee4 | |||
| 3c7cdb1da8 | |||
| dd80e744ff | |||
| 0b12e37459 | |||
| 51e815f0cb | |||
| f3856d569d | |||
| e6d1909f0b | |||
| 408976a199 | |||
| 4da49d926b | |||
| 75750ed760 | |||
| 2f74779bf1 | |||
| 7b038393b1 | |||
| 768c0e7f77 | |||
| c6009b1056 | |||
| 2a280afa88 | |||
| be980f4bc9 | |||
| 78118e9442 | |||
| a2c10a7913 | |||
| 7c4f7c9dad | |||
| 62058e6d48 | |||
| a2f514b544 | |||
| 14b424ee94 | |||
| 696b3ef1ce | |||
| 4e2ee3b3a8 | |||
| 1f9fab9a0c | |||
| d91d1cea34 | |||
| 69ba32683c | |||
| fc78e63ad1 | |||
| db212a555d | |||
| 4607f0177c | |||
| d7dbaeba46 | |||
| 3e94db1837 | |||
| 4fd77c2f05 | |||
| 9fe05e7d40 | |||
| e5a67500e4 | |||
| 9b50455049 | |||
| a531446396 | |||
| f876c35283 | |||
| 1bcec53c6b | |||
| 0ad5ef4e9b | |||
| 18113900fe | |||
| 88b8b24629 | |||
| 58c60d85e7 | |||
| 21a357c433 | |||
| d49d6936a5 | |||
| c2b5b14d26 | |||
| f1a0c46a29 | |||
| e775bcac3c |
@@ -31,6 +31,9 @@ module.exports = {
|
||||
"no-async-promise-executor": "off",
|
||||
// We use a `logger` intermediary module
|
||||
"no-console": "error",
|
||||
|
||||
// restrict EventEmitters to force callers to use TypedEventEmitter
|
||||
"no-restricted-imports": ["error", "events"],
|
||||
},
|
||||
overrides: [{
|
||||
files: [
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
comment:
|
||||
layout: "diff, files"
|
||||
behavior: default
|
||||
require_changes: false
|
||||
require_base: no
|
||||
require_head: no
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Test coverage
|
||||
on:
|
||||
pull_request: {}
|
||||
push:
|
||||
branches: [develop, main, master]
|
||||
jobs:
|
||||
test-coverage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: "yarn install && yarn build && yarn coverage"
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
verbose: true
|
||||
+392
@@ -1,3 +1,395 @@
|
||||
Changes in [16.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.0) (2022-03-15)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Improve typing around event emitter handlers ([\#2180](https://github.com/matrix-org/matrix-js-sdk/pull/2180)).
|
||||
|
||||
## ✨ Features
|
||||
* Fix defer not supporting resolving with a Promise<T> ([\#2216](https://github.com/matrix-org/matrix-js-sdk/pull/2216)).
|
||||
* add LocationAssetType enum ([\#2214](https://github.com/matrix-org/matrix-js-sdk/pull/2214)).
|
||||
* Support for mid-call devices changes ([\#2154](https://github.com/matrix-org/matrix-js-sdk/pull/2154)). Contributed by @SimonBrandner.
|
||||
* Add new room state emit RoomStateEvent.Update for lower-frequency hits ([\#2192](https://github.com/matrix-org/matrix-js-sdk/pull/2192)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix wrong event_id being sent for m.in_reply_to of threads ([\#2213](https://github.com/matrix-org/matrix-js-sdk/pull/2213)).
|
||||
* Fix wrongly asserting that PushRule::conditions is non-null ([\#2217](https://github.com/matrix-org/matrix-js-sdk/pull/2217)).
|
||||
* Make createThread more resilient when missing rootEvent ([\#2207](https://github.com/matrix-org/matrix-js-sdk/pull/2207)). Fixes vector-im/element-web#21130.
|
||||
* Fix bug with the /hierarchy API sending invalid requests ([\#2201](https://github.com/matrix-org/matrix-js-sdk/pull/2201)). Fixes vector-im/element-web#21170.
|
||||
* fix relation sender filter ([\#2196](https://github.com/matrix-org/matrix-js-sdk/pull/2196)). Fixes vector-im/element-web#20877.
|
||||
* Fix bug with one-way audio after a transfer ([\#2193](https://github.com/matrix-org/matrix-js-sdk/pull/2193)).
|
||||
|
||||
Changes in [16.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.0-rc.1) (2022-03-08)
|
||||
============================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Improve typing around event emitter handlers ([\#2180](https://github.com/matrix-org/matrix-js-sdk/pull/2180)).
|
||||
|
||||
## ✨ Features
|
||||
* Fix defer not supporting resolving with a Promise<T> ([\#2216](https://github.com/matrix-org/matrix-js-sdk/pull/2216)).
|
||||
* add LocationAssetType enum ([\#2214](https://github.com/matrix-org/matrix-js-sdk/pull/2214)).
|
||||
* Support for mid-call devices changes ([\#2154](https://github.com/matrix-org/matrix-js-sdk/pull/2154)). Contributed by @SimonBrandner.
|
||||
* Add new room state emit RoomStateEvent.Update for lower-frequency hits ([\#2192](https://github.com/matrix-org/matrix-js-sdk/pull/2192)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix wrong event_id being sent for m.in_reply_to of threads ([\#2213](https://github.com/matrix-org/matrix-js-sdk/pull/2213)).
|
||||
* Fix wrongly asserting that PushRule::conditions is non-null ([\#2217](https://github.com/matrix-org/matrix-js-sdk/pull/2217)).
|
||||
* Make createThread more resilient when missing rootEvent ([\#2207](https://github.com/matrix-org/matrix-js-sdk/pull/2207)). Fixes vector-im/element-web#21130.
|
||||
* Fix bug with the /hierarchy API sending invalid requests ([\#2201](https://github.com/matrix-org/matrix-js-sdk/pull/2201)). Fixes vector-im/element-web#21170.
|
||||
* fix relation sender filter ([\#2196](https://github.com/matrix-org/matrix-js-sdk/pull/2196)). Fixes vector-im/element-web#20877.
|
||||
* Fix bug with one-way audio after a transfer ([\#2193](https://github.com/matrix-org/matrix-js-sdk/pull/2193)).
|
||||
|
||||
Changes in [15.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.6.0) (2022-02-28)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Return send event response from MSC3089Branch.createNewVersion() ([\#2186](https://github.com/matrix-org/matrix-js-sdk/pull/2186)).
|
||||
* Add functions to support refresh tokens ([\#2178](https://github.com/matrix-org/matrix-js-sdk/pull/2178)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* [Release] Fix bug with the /hierarchy API sending invalid requests ([\#2202](https://github.com/matrix-org/matrix-js-sdk/pull/2202)).
|
||||
* Fix bug where calls could break if rejected from somewhere else ([\#2189](https://github.com/matrix-org/matrix-js-sdk/pull/2189)).
|
||||
* Fix camera stuck on after call transfer ([\#2188](https://github.com/matrix-org/matrix-js-sdk/pull/2188)).
|
||||
* Fix synthetic read receipt handling ([\#2174](https://github.com/matrix-org/matrix-js-sdk/pull/2174)). Fixes vector-im/element-web#21016.
|
||||
* Revert "Sign backup with cross-signing key when we reset it." ([\#2175](https://github.com/matrix-org/matrix-js-sdk/pull/2175)).
|
||||
* Sign backup with cross-signing key when we reset it. ([\#2170](https://github.com/matrix-org/matrix-js-sdk/pull/2170)).
|
||||
* Fix error in uploadContent() when file is empty under Node.js ([\#2155](https://github.com/matrix-org/matrix-js-sdk/pull/2155)).
|
||||
* Check the backup info against the stored private key when determining trust. ([\#2167](https://github.com/matrix-org/matrix-js-sdk/pull/2167)).
|
||||
* Back up keys before logging out ([\#2158](https://github.com/matrix-org/matrix-js-sdk/pull/2158)). Fixes vector-im/element-web#13151.
|
||||
|
||||
Changes in [15.6.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.6.0-rc.1) (2022-02-22)
|
||||
============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Return send event response from MSC3089Branch.createNewVersion() ([\#2186](https://github.com/matrix-org/matrix-js-sdk/pull/2186)).
|
||||
* Add functions to support refresh tokens ([\#2178](https://github.com/matrix-org/matrix-js-sdk/pull/2178)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix bug where calls could break if rejected from somewhere else ([\#2189](https://github.com/matrix-org/matrix-js-sdk/pull/2189)).
|
||||
* Fix camera stuck on after call transfer ([\#2188](https://github.com/matrix-org/matrix-js-sdk/pull/2188)).
|
||||
* Fix synthetic read receipt handling ([\#2174](https://github.com/matrix-org/matrix-js-sdk/pull/2174)). Fixes vector-im/element-web#21016.
|
||||
* Revert "Sign backup with cross-signing key when we reset it." ([\#2175](https://github.com/matrix-org/matrix-js-sdk/pull/2175)).
|
||||
* Sign backup with cross-signing key when we reset it. ([\#2170](https://github.com/matrix-org/matrix-js-sdk/pull/2170)).
|
||||
* Fix error in uploadContent() when file is empty under Node.js ([\#2155](https://github.com/matrix-org/matrix-js-sdk/pull/2155)).
|
||||
* Check the backup info against the stored private key when determining trust. ([\#2167](https://github.com/matrix-org/matrix-js-sdk/pull/2167)).
|
||||
* Back up keys before logging out ([\#2158](https://github.com/matrix-org/matrix-js-sdk/pull/2158)). Fixes vector-im/element-web#13151.
|
||||
|
||||
Changes in [15.5.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.2) (2022-02-17)
|
||||
==================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix synthetic read receipt handling
|
||||
|
||||
Changes in [15.5.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.1) (2022-02-14)
|
||||
==================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix issue with rooms not getting marked as unread ([\#2163](https://github.com/matrix-org/matrix-js-sdk/pull/2163)). Fixes vector-im/element-web#20971.
|
||||
* Don't store streams that are only used once ([\#2157](https://github.com/matrix-org/matrix-js-sdk/pull/2157)). Fixes vector-im/element-web#20932. Contributed by @SimonBrandner.
|
||||
* Fix edge cases around RR calculations ([\#2160](https://github.com/matrix-org/matrix-js-sdk/pull/2160)). Fixes vector-im/element-web#20922.
|
||||
* Account for encryption in `maySendMessage()` ([\#2159](https://github.com/matrix-org/matrix-js-sdk/pull/2159)). Contributed by @SimonBrandner.
|
||||
* Send references to thread root to threads, even out of order ([\#2156](https://github.com/matrix-org/matrix-js-sdk/pull/2156)).
|
||||
* Fix initial sync fail when event fetching unsuccessful ([\#2150](https://github.com/matrix-org/matrix-js-sdk/pull/2150)). Fixes vector-im/element-web#20862.
|
||||
* Don't decrypt redacted messages ([\#2143](https://github.com/matrix-org/matrix-js-sdk/pull/2143)). Contributed by @SimonBrandner.
|
||||
|
||||
Changes in [15.5.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.1-rc.1) (2022-02-08)
|
||||
============================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix issue with rooms not getting marked as unread ([\#2163](https://github.com/matrix-org/matrix-js-sdk/pull/2163)). Fixes vector-im/element-web#20971.
|
||||
* Don't store streams that are only used once ([\#2157](https://github.com/matrix-org/matrix-js-sdk/pull/2157)). Fixes vector-im/element-web#20932. Contributed by @SimonBrandner.
|
||||
* Fix edge cases around RR calculations ([\#2160](https://github.com/matrix-org/matrix-js-sdk/pull/2160)). Fixes vector-im/element-web#20922.
|
||||
* Account for encryption in `maySendMessage()` ([\#2159](https://github.com/matrix-org/matrix-js-sdk/pull/2159)). Contributed by @SimonBrandner.
|
||||
* Send references to thread root to threads, even out of order ([\#2156](https://github.com/matrix-org/matrix-js-sdk/pull/2156)).
|
||||
* Fix initial sync fail when event fetching unsuccessful ([\#2150](https://github.com/matrix-org/matrix-js-sdk/pull/2150)). Fixes vector-im/element-web#20862.
|
||||
* Don't decrypt redacted messages ([\#2143](https://github.com/matrix-org/matrix-js-sdk/pull/2143)). Contributed by @SimonBrandner.
|
||||
|
||||
Changes in [15.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.0) (2022-01-31)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Support m.asset in m.location event content ([\#2109](https://github.com/matrix-org/matrix-js-sdk/pull/2109)).
|
||||
* Send extensible events structure and support on-demand parsing ([\#2091](https://github.com/matrix-org/matrix-js-sdk/pull/2091)).
|
||||
* Support cancelling events whilst they are in status = ENCRYPTING ([\#2095](https://github.com/matrix-org/matrix-js-sdk/pull/2095)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix http-api butchering idServer requests ([\#2134](https://github.com/matrix-org/matrix-js-sdk/pull/2134)). Fixes vector-im/element-web#20680.
|
||||
* Don't remove streams that still have tracks ([\#2104](https://github.com/matrix-org/matrix-js-sdk/pull/2104)).
|
||||
|
||||
Changes in [15.5.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.0-rc.1) (2022-01-26)
|
||||
============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Support m.asset in m.location event content ([\#2109](https://github.com/matrix-org/matrix-js-sdk/pull/2109)).
|
||||
* Send extensible events structure and support on-demand parsing ([\#2091](https://github.com/matrix-org/matrix-js-sdk/pull/2091)).
|
||||
* Support cancelling events whilst they are in status = ENCRYPTING ([\#2095](https://github.com/matrix-org/matrix-js-sdk/pull/2095)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix http-api butchering idServer requests ([\#2134](https://github.com/matrix-org/matrix-js-sdk/pull/2134)). Fixes vector-im/element-web#20680.
|
||||
* Don't remove streams that still have tracks ([\#2104](https://github.com/matrix-org/matrix-js-sdk/pull/2104)).
|
||||
|
||||
Changes in [15.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.4.0) (2022-01-17)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Don't consider alt_aliases when calculating room name ([\#2094](https://github.com/matrix-org/matrix-js-sdk/pull/2094)). Fixes vector-im/element-web#13887.
|
||||
* Load room history if necessary when searching for MSC3089 getFileEvent() ([\#2066](https://github.com/matrix-org/matrix-js-sdk/pull/2066)).
|
||||
* Add support for MSC3030 `/timestamp_to_event` ([\#2072](https://github.com/matrix-org/matrix-js-sdk/pull/2072)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Stop encrypting redactions as it isn't spec compliant ([\#2098](https://github.com/matrix-org/matrix-js-sdk/pull/2098)). Fixes vector-im/element-web#20460.
|
||||
* Fix more function typings relating to key backup ([\#2086](https://github.com/matrix-org/matrix-js-sdk/pull/2086)).
|
||||
* Fix timeline search in MSC3089 getFileEvent() ([\#2085](https://github.com/matrix-org/matrix-js-sdk/pull/2085)).
|
||||
* Set a `deviceId` for VoIP example and use `const`/`let` ([\#2090](https://github.com/matrix-org/matrix-js-sdk/pull/2090)). Fixes #2083. Contributed by @SimonBrandner.
|
||||
* Fix incorrect TS return type for secret storage and key backup functions ([\#2082](https://github.com/matrix-org/matrix-js-sdk/pull/2082)).
|
||||
|
||||
Changes in [15.4.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.4.0-rc.1) (2022-01-11)
|
||||
============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Don't consider alt_aliases when calculating room name ([\#2094](https://github.com/matrix-org/matrix-js-sdk/pull/2094)). Fixes vector-im/element-web#13887.
|
||||
* Load room history if necessary when searching for MSC3089 getFileEvent() ([\#2066](https://github.com/matrix-org/matrix-js-sdk/pull/2066)).
|
||||
* Add support for MSC3030 `/timestamp_to_event` ([\#2072](https://github.com/matrix-org/matrix-js-sdk/pull/2072)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Stop encrypting redactions as it isn't spec compliant ([\#2098](https://github.com/matrix-org/matrix-js-sdk/pull/2098)). Fixes vector-im/element-web#20460.
|
||||
* Fix more function typings relating to key backup ([\#2086](https://github.com/matrix-org/matrix-js-sdk/pull/2086)).
|
||||
* Fix timeline search in MSC3089 getFileEvent() ([\#2085](https://github.com/matrix-org/matrix-js-sdk/pull/2085)).
|
||||
* Set a `deviceId` for VoIP example and use `const`/`let` ([\#2090](https://github.com/matrix-org/matrix-js-sdk/pull/2090)). Fixes #2083. Contributed by @SimonBrandner.
|
||||
* Fix incorrect TS return type for secret storage and key backup functions ([\#2082](https://github.com/matrix-org/matrix-js-sdk/pull/2082)).
|
||||
|
||||
Changes in [15.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.3.0) (2021-12-20)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Improve fallback key behaviour ([\#2037](https://github.com/matrix-org/matrix-js-sdk/pull/2037)).
|
||||
* Add new room event filter fields ([\#2051](https://github.com/matrix-org/matrix-js-sdk/pull/2051)).
|
||||
* Add method to fetch /account/whoami ([\#2046](https://github.com/matrix-org/matrix-js-sdk/pull/2046)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Filter out falsey opts in /relations API hits ([\#2059](https://github.com/matrix-org/matrix-js-sdk/pull/2059)). Fixes vector-im/element-web#20137.
|
||||
* Fix paginateEventTimeline resolve to boolean ([\#2054](https://github.com/matrix-org/matrix-js-sdk/pull/2054)).
|
||||
* Fix incorrect MSC3089 typings and add null checks ([\#2049](https://github.com/matrix-org/matrix-js-sdk/pull/2049)).
|
||||
|
||||
Changes in [15.3.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.3.0-rc.1) (2021-12-14)
|
||||
============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Improve fallback key behaviour ([\#2037](https://github.com/matrix-org/matrix-js-sdk/pull/2037)).
|
||||
* Add new room event filter fields ([\#2051](https://github.com/matrix-org/matrix-js-sdk/pull/2051)).
|
||||
* Add method to fetch /account/whoami ([\#2046](https://github.com/matrix-org/matrix-js-sdk/pull/2046)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Filter out falsey opts in /relations API hits ([\#2059](https://github.com/matrix-org/matrix-js-sdk/pull/2059)). Fixes vector-im/element-web#20137.
|
||||
* Fix paginateEventTimeline resolve to boolean ([\#2054](https://github.com/matrix-org/matrix-js-sdk/pull/2054)).
|
||||
* Fix incorrect MSC3089 typings and add null checks ([\#2049](https://github.com/matrix-org/matrix-js-sdk/pull/2049)).
|
||||
|
||||
Changes in [15.2.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.2.1) (2021-12-13)
|
||||
==================================================================================================
|
||||
|
||||
* Security release with updated version of Olm to fix https://matrix.org/blog/2021/12/03/pre-disclosure-upcoming-security-release-of-libolm-and-matrix-js-sdk
|
||||
|
||||
Changes in [15.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.2.0) (2021-12-06)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Remove support for `ArrayBuffer` in unstable MSC3089 `createFile()` and `createNewVersion()` and instead use same content types as handled by `MatrixClient.uploadContent()`. This enables support for Node.js. ([\#2014](https://github.com/matrix-org/matrix-js-sdk/pull/2014)).
|
||||
* Support for password-based backup on Node.js ([\#2021](https://github.com/matrix-org/matrix-js-sdk/pull/2021)).
|
||||
* Add optional force parameter when ensuring Olm sessions ([\#2027](https://github.com/matrix-org/matrix-js-sdk/pull/2027)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix call upgrades ([\#2024](https://github.com/matrix-org/matrix-js-sdk/pull/2024)). Contributed by @SimonBrandner.
|
||||
|
||||
Changes in [15.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.2.0-rc.1) (2021-11-30)
|
||||
============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Remove support for `ArrayBuffer` in unstable MSC3089 `createFile()` and `createNewVersion()` and instead use same content types as handled by `MatrixClient.uploadContent()`. This enables support for Node.js. ([\#2014](https://github.com/matrix-org/matrix-js-sdk/pull/2014)).
|
||||
* Support for password-based backup on Node.js ([\#2021](https://github.com/matrix-org/matrix-js-sdk/pull/2021)).
|
||||
* Add optional force parameter when ensuring Olm sessions ([\#2027](https://github.com/matrix-org/matrix-js-sdk/pull/2027)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix call upgrades ([\#2024](https://github.com/matrix-org/matrix-js-sdk/pull/2024)). Contributed by @SimonBrandner.
|
||||
|
||||
Changes in [15.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.1.1) (2021-11-22)
|
||||
==================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix edit history being broken after editing an unencrypted event with an encrypted event ([\#2013](https://github.com/matrix-org/matrix-js-sdk/pull/2013)). Fixes vector-im/element-web#19651 and vector-im/element-web#19651. Contributed by @aaronraimist.
|
||||
* Make events pagination responses parse threads ([\#2011](https://github.com/matrix-org/matrix-js-sdk/pull/2011)). Fixes vector-im/element-web#19587 and vector-im/element-web#19587.
|
||||
|
||||
Changes in [15.1.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.1.1-rc.1) (2021-11-17)
|
||||
============================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix edit history being broken after editing an unencrypted event with an encrypted event ([\#2013](https://github.com/matrix-org/matrix-js-sdk/pull/2013)). Fixes vector-im/element-web#19651 and vector-im/element-web#19651. Contributed by @aaronraimist.
|
||||
* Make events pagination responses parse threads ([\#2011](https://github.com/matrix-org/matrix-js-sdk/pull/2011)). Fixes vector-im/element-web#19587 and vector-im/element-web#19587.
|
||||
|
||||
Changes in [15.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.1.0) (2021-11-08)
|
||||
==================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Mark old verification methods as deprecated ([\#1994](https://github.com/matrix-org/matrix-js-sdk/pull/1994)).
|
||||
|
||||
## ✨ Features
|
||||
* Try to set a sender on search result events if possible ([\#2004](https://github.com/matrix-org/matrix-js-sdk/pull/2004)).
|
||||
* Port some changes from group calls branch to develop ([\#2001](https://github.com/matrix-org/matrix-js-sdk/pull/2001)). Contributed by @SimonBrandner.
|
||||
* Fetch room membership from server rather than relying on stored data ([\#1998](https://github.com/matrix-org/matrix-js-sdk/pull/1998)).
|
||||
* Add method to fetch the MSC3266 Room Summary of a Room ([\#1988](https://github.com/matrix-org/matrix-js-sdk/pull/1988)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Don't show `Unable to access microphone` when cancelling screensharing dialog ([\#2005](https://github.com/matrix-org/matrix-js-sdk/pull/2005)). Fixes vector-im/element-web#19533 and vector-im/element-web#19533. Contributed by @SimonBrandner.
|
||||
* Strip direction override characters from display names ([\#1992](https://github.com/matrix-org/matrix-js-sdk/pull/1992)). Fixes vector-im/element-web#1712 and vector-im/element-web#1712.
|
||||
|
||||
Changes in [15.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.1.0-rc.1) (2021-11-02)
|
||||
============================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Mark old verification methods as deprecated ([\#1994](https://github.com/matrix-org/matrix-js-sdk/pull/1994)).
|
||||
|
||||
## ✨ Features
|
||||
* Try to set a sender on search result events if possible ([\#2004](https://github.com/matrix-org/matrix-js-sdk/pull/2004)).
|
||||
* Port some changes from group calls branch to develop ([\#2001](https://github.com/matrix-org/matrix-js-sdk/pull/2001)). Contributed by @SimonBrandner.
|
||||
* Fetch room membership from server rather than relying on stored data ([\#1998](https://github.com/matrix-org/matrix-js-sdk/pull/1998)).
|
||||
* Add method to fetch the MSC3266 Room Summary of a Room ([\#1988](https://github.com/matrix-org/matrix-js-sdk/pull/1988)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Don't show `Unable to access microphone` when cancelling screensharing dialog ([\#2005](https://github.com/matrix-org/matrix-js-sdk/pull/2005)). Fixes vector-im/element-web#19533 and vector-im/element-web#19533. Contributed by @SimonBrandner.
|
||||
* Strip direction override characters from display names ([\#1992](https://github.com/matrix-org/matrix-js-sdk/pull/1992)). Fixes vector-im/element-web#1712 and vector-im/element-web#1712.
|
||||
|
||||
Changes in [15.0.0](https://github.com/vector-im/element-desktop/releases/tag/v15.0.0) (2021-10-25)
|
||||
===================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Use `ICallFeedOpts` in the `CallFeed` constructor. To construct a new `CallFeed` object you have to pass `ICallFeedOpts` e.g. `const callFeed = new CallFeed({client ([\#1964](https://github.com/matrix-org/matrix-js-sdk/pull/1964)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
|
||||
## ✨ Features
|
||||
* Make threads use 'm.thread' relation ([\#1980](https://github.com/matrix-org/matrix-js-sdk/pull/1980)).
|
||||
* Try to answer a call without video if we can't access the camera ([\#1972](https://github.com/matrix-org/matrix-js-sdk/pull/1972)). Fixes vector-im/element-web#17975 and vector-im/element-web#17975. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Make `opts` in `importRoomKeys()` optional ([\#1974](https://github.com/matrix-org/matrix-js-sdk/pull/1974)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Enable TypeScript declaration maps ([\#1966](https://github.com/matrix-org/matrix-js-sdk/pull/1966)). Contributed by [Alexendoo](https://github.com/Alexendoo).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix `requestVerificationDM` with chronological `pendingEventOrdering` ([\#1943](https://github.com/matrix-org/matrix-js-sdk/pull/1943)). Contributed by [freaktechnik](https://github.com/freaktechnik).
|
||||
|
||||
Changes in [15.0.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v15.0.0-rc.1) (2021-10-19)
|
||||
=============================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Use `ICallFeedOpts` in the `CallFeed` constructor. To construct a new `CallFeed` object you have to pass `ICallFeedOpts` e.g. `const callFeed = new CallFeed({client ([\#1964](https://github.com/matrix-org/matrix-js-sdk/pull/1964)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
|
||||
## ✨ Features
|
||||
* Make threads use 'm.thread' relation ([\#1980](https://github.com/matrix-org/matrix-js-sdk/pull/1980)).
|
||||
* Try to answer a call without video if we can't access the camera ([\#1972](https://github.com/matrix-org/matrix-js-sdk/pull/1972)). Fixes vector-im/element-web#17975 and vector-im/element-web#17975. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Make `opts` in `importRoomKeys()` optional ([\#1974](https://github.com/matrix-org/matrix-js-sdk/pull/1974)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Enable TypeScript declaration maps ([\#1966](https://github.com/matrix-org/matrix-js-sdk/pull/1966)). Contributed by [Alexendoo](https://github.com/Alexendoo).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix `requestVerificationDM` with chronological `pendingEventOrdering` ([\#1943](https://github.com/matrix-org/matrix-js-sdk/pull/1943)). Contributed by [freaktechnik](https://github.com/freaktechnik).
|
||||
|
||||
Changes in [14.0.1](https://github.com/vector-im/element-desktop/releases/tag/v14.0.1) (2021-10-12)
|
||||
===================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Support for call upgrades. `setLocalVideoMuted()` and `setMicrophoneMuted()` are now `async` and return the new mute state ([\#1827](https://github.com/matrix-org/matrix-js-sdk/pull/1827)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
|
||||
## ✨ Features
|
||||
* Implement file versioning for tree spaces ([\#1952](https://github.com/matrix-org/matrix-js-sdk/pull/1952)).
|
||||
* Allow answering calls without audio/video ([\#1950](https://github.com/matrix-org/matrix-js-sdk/pull/1950)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Add `bound` to `IThreepid` ([\#1941](https://github.com/matrix-org/matrix-js-sdk/pull/1941)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Add `trusted_locally` to `TrustInfo` ([\#1942](https://github.com/matrix-org/matrix-js-sdk/pull/1942)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix incorrect return value type in getJoinedRooms() ([\#1959](https://github.com/matrix-org/matrix-js-sdk/pull/1959)). Contributed by [psrpinto](https://github.com/psrpinto).
|
||||
* Make sure to set `callLengthInterval` only once ([\#1958](https://github.com/matrix-org/matrix-js-sdk/pull/1958)). Fixes vector-im/element-web#19221 and vector-im/element-web#19221. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Fix event partitioning from non threading ready clients ([\#1948](https://github.com/matrix-org/matrix-js-sdk/pull/1948)).
|
||||
* Ensure unencrypted fields get exposed by getEffectiveEvent() ([\#1938](https://github.com/matrix-org/matrix-js-sdk/pull/1938)). Fixes vector-im/element-web#19062 and vector-im/element-web#19062.
|
||||
|
||||
|
||||
Changes in [14.0.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v14.0.0-rc.1) (2021-10-04)
|
||||
=============================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Support for call upgrades. `setLocalVideoMuted()` and `setMicrophoneMuted()` are now `async` and return the new mute state ([\#1827](https://github.com/matrix-org/matrix-js-sdk/pull/1827)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
|
||||
## ✨ Features
|
||||
* Implement file versioning for tree spaces ([\#1952](https://github.com/matrix-org/matrix-js-sdk/pull/1952)).
|
||||
* Allow answering calls without audio/video ([\#1950](https://github.com/matrix-org/matrix-js-sdk/pull/1950)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Add `bound` to `IThreepid` ([\#1941](https://github.com/matrix-org/matrix-js-sdk/pull/1941)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Add `trusted_locally` to `TrustInfo` ([\#1942](https://github.com/matrix-org/matrix-js-sdk/pull/1942)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix incorrect return value type in getJoinedRooms() ([\#1959](https://github.com/matrix-org/matrix-js-sdk/pull/1959)). Contributed by [psrpinto](https://github.com/psrpinto).
|
||||
* Make sure to set `callLengthInterval` only once ([\#1958](https://github.com/matrix-org/matrix-js-sdk/pull/1958)). Fixes vector-im/element-web#19221 and vector-im/element-web#19221. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Fix event partitioning from non threading ready clients ([\#1948](https://github.com/matrix-org/matrix-js-sdk/pull/1948)).
|
||||
* Ensure unencrypted fields get exposed by getEffectiveEvent() ([\#1938](https://github.com/matrix-org/matrix-js-sdk/pull/1938)). Fixes vector-im/element-web#19062 and vector-im/element-web#19062.
|
||||
|
||||
Changes in [13.0.0](https://github.com/vector-im/element-desktop/releases/tag/v13.0.0) (2021-09-27)
|
||||
===================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Add `getHistoryVisibility()` and `getGuestAccess()` ([\#1940](https://github.com/matrix-org/matrix-js-sdk/pull/1940)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Add `getBuffer()` to `QRCodeData` ([\#1927](https://github.com/matrix-org/matrix-js-sdk/pull/1927)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Added `createDataChannel()` and `CallEvent.DataChannel` to `MatrixCall` for creating and listening for WebRTC datachannels. ([\#1929](https://github.com/matrix-org/matrix-js-sdk/pull/1929)). Contributed by [robertlong](https://github.com/robertlong).
|
||||
* Add file locking to MSC3089 branches ([\#1909](https://github.com/matrix-org/matrix-js-sdk/pull/1909)).
|
||||
* Add `hasBeenCancelled` to `VerificationBase` ([\#1915](https://github.com/matrix-org/matrix-js-sdk/pull/1915)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Add `ISasEvent` ([\#1908](https://github.com/matrix-org/matrix-js-sdk/pull/1908)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Count notifications in encrypted rooms client-side ([\#1872](https://github.com/matrix-org/matrix-js-sdk/pull/1872)). Fixes vector-im/element-web#15393 and vector-im/element-web#15393. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Exclude opt-in Element performance metrics from encryption ([\#1897](https://github.com/matrix-org/matrix-js-sdk/pull/1897)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix race on automatic backup restore ([\#1936](https://github.com/matrix-org/matrix-js-sdk/pull/1936)). Fixes vector-im/element-web#17781 and vector-im/element-web#17781.
|
||||
|
||||
Changes in [13.0.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v13.0.0-rc.1) (2021-09-21)
|
||||
=============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Add `getHistoryVisibility()` and `getGuestAccess()` ([\#1940](https://github.com/matrix-org/matrix-js-sdk/pull/1940)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Add `getBuffer()` to `QRCodeData` ([\#1927](https://github.com/matrix-org/matrix-js-sdk/pull/1927)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Added `createDataChannel()` and `CallEvent.DataChannel` to `MatrixCall` for creating and listening for WebRTC datachannels. ([\#1929](https://github.com/matrix-org/matrix-js-sdk/pull/1929)). Contributed by [robertlong](https://github.com/robertlong).
|
||||
* Add file locking to MSC3089 branches ([\#1909](https://github.com/matrix-org/matrix-js-sdk/pull/1909)).
|
||||
* Add `hasBeenCancelled` to `VerificationBase` ([\#1915](https://github.com/matrix-org/matrix-js-sdk/pull/1915)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Add `ISasEvent` ([\#1908](https://github.com/matrix-org/matrix-js-sdk/pull/1908)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Count notifications in encrypted rooms client-side ([\#1872](https://github.com/matrix-org/matrix-js-sdk/pull/1872)). Fixes vector-im/element-web#15393 and vector-im/element-web#15393. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Exclude opt-in Element performance metrics from encryption ([\#1897](https://github.com/matrix-org/matrix-js-sdk/pull/1897)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix race on automatic backup restore ([\#1936](https://github.com/matrix-org/matrix-js-sdk/pull/1936)). Fixes vector-im/element-web#17781 and vector-im/element-web#17781.
|
||||
|
||||
Changes in [12.5.0](https://github.com/vector-im/element-desktop/releases/tag/v12.5.0) (2021-09-14)
|
||||
===================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* [Release] Exclude opt-in Element performance metrics from encryption ([\#1901](https://github.com/matrix-org/matrix-js-sdk/pull/1901)).
|
||||
* Give `MatrixCall` the capability to emit `LengthChanged` events ([\#1873](https://github.com/matrix-org/matrix-js-sdk/pull/1873)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Improve browser example ([\#1875](https://github.com/matrix-org/matrix-js-sdk/pull/1875)). Contributed by [psrpinto](https://github.com/psrpinto).
|
||||
* Give `CallFeed` the capability to emit on volume changes ([\#1865](https://github.com/matrix-org/matrix-js-sdk/pull/1865)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix verification request cancellation ([\#1871](https://github.com/matrix-org/matrix-js-sdk/pull/1871)).
|
||||
|
||||
Changes in [12.4.1](https://github.com/vector-im/element-desktop/releases/tag/v12.4.1) (2021-09-13)
|
||||
===================================================================================================
|
||||
|
||||
## 🔒 SECURITY FIXES
|
||||
* Fix a security issue with message key sharing. See https://matrix.org/blog/2021/09/13/vulnerability-disclosure-key-sharing
|
||||
for details.
|
||||
|
||||
Changes in [12.4.0](https://github.com/vector-im/element-desktop/releases/tag/v12.4.0) (2021-08-31)
|
||||
===================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Deprecate groups APIs. Groups are no longer supported, only Synapse has support. They are being replaced by Spaces which build off of Rooms and are far more flexible. ([\#1792](https://github.com/matrix-org/matrix-js-sdk/pull/1792)).
|
||||
|
||||
## ✨ Features
|
||||
* Add method for including extra fields when uploading to a tree space ([\#1850](https://github.com/matrix-org/matrix-js-sdk/pull/1850)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix broken voice calls, no ringing and broken call notifications ([\#1858](https://github.com/matrix-org/matrix-js-sdk/pull/1858)). Fixes vector-im/element-web#18578 vector-im/element-web#18538 and vector-im/element-web#18578. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Revert "Fix glare related regressions" ([\#1857](https://github.com/matrix-org/matrix-js-sdk/pull/1857)).
|
||||
* Fix glare related regressions ([\#1851](https://github.com/matrix-org/matrix-js-sdk/pull/1851)). Fixes vector-im/element-web#18538 and vector-im/element-web#18538. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
|
||||
* Fix temporary call messages being handled without call ([\#1834](https://github.com/matrix-org/matrix-js-sdk/pull/1834)). Contributed by [Palid](https://github.com/Palid).
|
||||
* Fix conditional on returning file tree spaces ([\#1841](https://github.com/matrix-org/matrix-js-sdk/pull/1841)).
|
||||
|
||||
Changes in [12.3.1](https://github.com/vector-im/element-desktop/releases/tag/v12.3.1) (2021-08-17)
|
||||
===================================================================================================
|
||||
|
||||
|
||||
+23
-2
@@ -22,8 +22,14 @@ ask you to make any refinements needed or merge it and make them ourselves.
|
||||
Things that should go into your PR description:
|
||||
* A changelog entry in the `Notes` section (see below)
|
||||
* References to any bugs fixed by the change (in GitHub's `Fixes` notation)
|
||||
* Notes for the reviewer that might help them to understand why the change is
|
||||
necessary or how they might better review it.
|
||||
* Describe the why and what is changing in the PR description so it's easy for
|
||||
onlookers and reviewers to onboard and context switch.
|
||||
* Include both **before** and **after** screenshots to easily compare and discuss
|
||||
what's changing.
|
||||
* Include a step-by-step testing strategy so that a reviewer can check out the
|
||||
code locally and easily get to the point of testing your change.
|
||||
* Add comments to the diff for the reviewer that might help them to understand
|
||||
why the change is necessary or how they might better understand and review it.
|
||||
|
||||
Things that should *not* go into your PR description:
|
||||
* Any information on how the code works or why you chose to do it the way
|
||||
@@ -92,6 +98,21 @@ We use continuous integration, and all pull requests get automatically tested:
|
||||
if your change breaks the build, then the PR will show that there are failed
|
||||
checks, so please check back after a few minutes.
|
||||
|
||||
Tests
|
||||
-----
|
||||
If your PR is a feature (ie. if it's being labelled with the 'T-Enhancement'
|
||||
label) then we require that the PR also includes tests. These need to test that
|
||||
your feature works as expected and ideally test edge cases too. For the js-sdk
|
||||
itself, your tests should generally be unit tests. matrix-react-sdk also uses
|
||||
these guidelines, so for that your tests can be unit tests using
|
||||
react-test-utils, snapshot tests or screenshot tests.
|
||||
|
||||
We don't require tests for bug fixes (T-Defect) but strongly encourage regression
|
||||
tests for the bug itself wherever possible.
|
||||
|
||||
In the future we may formalise this more with a minimum test coverage
|
||||
percentage for the diff.
|
||||
|
||||
Code style
|
||||
----------
|
||||
The js-sdk aims to target TypeScript/ES6. All new files should be written in
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
console.log("Loading browser sdk");
|
||||
|
||||
var client = matrixcs.createClient("http://matrix.org");
|
||||
var client = matrixcs.createClient("https://matrix.org");
|
||||
client.publicRooms(function (err, data) {
|
||||
if (err) {
|
||||
console.error("err %s", JSON.stringify(err));
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Test</title>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel="icon" href="data:,">
|
||||
<script src="lib/matrix.js"></script>
|
||||
<script src="browserTest.js"></script>
|
||||
</head>
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
console.log("Loading browser sdk");
|
||||
var BASE_URL = "https://matrix.org";
|
||||
var TOKEN = "accesstokengoeshere";
|
||||
var USER_ID = "@username:localhost";
|
||||
var ROOM_ID = "!room:id";
|
||||
const BASE_URL = "https://matrix.org";
|
||||
const TOKEN = "accesstokengoeshere";
|
||||
const USER_ID = "@username:localhost";
|
||||
const ROOM_ID = "!room:id";
|
||||
const DEVICE_ID = "some_device_id";
|
||||
|
||||
|
||||
var client = matrixcs.createClient({
|
||||
const client = matrixcs.createClient({
|
||||
baseUrl: BASE_URL,
|
||||
accessToken: TOKEN,
|
||||
userId: USER_ID
|
||||
userId: USER_ID,
|
||||
deviceId: DEVICE_ID
|
||||
});
|
||||
var call;
|
||||
let call;
|
||||
|
||||
function disableButtons(place, answer, hangup) {
|
||||
document.getElementById("hangup").disabled = hangup;
|
||||
@@ -19,7 +20,7 @@ function disableButtons(place, answer, hangup) {
|
||||
}
|
||||
|
||||
function addListeners(call) {
|
||||
var lastError = "";
|
||||
let lastError = "";
|
||||
call.on("hangup", function() {
|
||||
disableButtons(false, true, true);
|
||||
document.getElementById("result").innerHTML = (
|
||||
|
||||
+14
-10
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "12.3.1",
|
||||
"version": "16.0.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
@@ -15,7 +15,7 @@
|
||||
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
|
||||
"gendoc": "jsdoc -c jsdoc.json -P package.json",
|
||||
"lint": "yarn lint:types && yarn lint:js",
|
||||
"lint:js": "eslint --max-warnings 7 src spec",
|
||||
"lint:js": "eslint --max-warnings 0 src spec",
|
||||
"lint:js-fix": "eslint --fix src spec",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"test": "jest",
|
||||
@@ -56,6 +56,7 @@
|
||||
"bs58": "^4.0.1",
|
||||
"content-type": "^1.0.4",
|
||||
"loglevel": "^1.7.1",
|
||||
"matrix-events-sdk": "^0.0.1-beta.7",
|
||||
"p-retry": "^4.5.0",
|
||||
"qs": "^6.9.6",
|
||||
"request": "^2.88.2",
|
||||
@@ -74,22 +75,24 @@
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/register": "^7.12.10",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/node": "12",
|
||||
"@types/request": "^2.48.5",
|
||||
"@typescript-eslint/eslint-plugin": "^4.17.0",
|
||||
"@typescript-eslint/parser": "^4.17.0",
|
||||
"allchange": "^1.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.6.0",
|
||||
"@typescript-eslint/parser": "^5.6.0",
|
||||
"allchange": "^1.0.6",
|
||||
"babel-jest": "^26.6.3",
|
||||
"babelify": "^10.0.0",
|
||||
"better-docs": "^2.4.0-beta.9",
|
||||
"browserify": "^17.0.0",
|
||||
"docdash": "^1.2.0",
|
||||
"eslint": "7.18.0",
|
||||
"eslint": "8.9.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#2306b3d4da4eba908b256014b979f1d3d43d2945",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-matrix-org": "^0.4.0",
|
||||
"exorcist": "^1.0.1",
|
||||
"fake-indexeddb": "^3.1.2",
|
||||
"jest": "^26.6.3",
|
||||
@@ -99,7 +102,7 @@
|
||||
"rimraf": "^3.0.2",
|
||||
"terser": "^5.5.1",
|
||||
"tsify": "^5.0.2",
|
||||
"typescript": "^4.1.3"
|
||||
"typescript": "^4.5.3"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node",
|
||||
@@ -110,7 +113,8 @@
|
||||
"<rootDir>/src/**/*.{js,ts}"
|
||||
],
|
||||
"coverageReporters": [
|
||||
"text"
|
||||
"text",
|
||||
"json"
|
||||
]
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
|
||||
@@ -255,6 +255,12 @@ if [ -n "$signing_id" ]; then
|
||||
# the easiest way to check the validity of the tarball from git is to unzip
|
||||
# it and compare it with our own idea of what the tar should look like.
|
||||
|
||||
# This uses git archive which seems to be what github uses. Specifically,
|
||||
# the header fields are set in the same way: same file mode, uid & gid
|
||||
# both zero and mtime set to the timestamp of the commit that the tag
|
||||
# references. Also note that this puts the commit into the tar headers
|
||||
# and can be extracted with gunzip -c foo.tar.gz | git get-tar-commit-id
|
||||
|
||||
# the name of the sig file we want to create
|
||||
source_sigfile="${tag}-src.tar.gz.asc"
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ limitations under the License.
|
||||
import './olm-loader';
|
||||
|
||||
import MockHttpBackend from 'matrix-mock-request';
|
||||
|
||||
import { LocalStorageCryptoStore } from '../src/crypto/store/localStorage-crypto-store';
|
||||
import { logger } from '../src/logger';
|
||||
import { WebStorageSessionStore } from "../src/store/session/webstorage";
|
||||
@@ -85,6 +86,7 @@ TestClient.prototype.toString = function() {
|
||||
*/
|
||||
TestClient.prototype.start = function() {
|
||||
logger.log(this + ': starting');
|
||||
this.httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
|
||||
this.httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
this.expectDeviceKeyUpload();
|
||||
|
||||
@@ -17,54 +17,34 @@ limitations under the License.
|
||||
// load XmlHttpRequest mock
|
||||
import "./setupTests";
|
||||
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
|
||||
import { MockStorageApi } from "../MockStorageApi";
|
||||
import { WebStorageSessionStore } from "../../src/store/session/webstorage";
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
import { LocalStorageCryptoStore } from "../../src/crypto/store/localStorage-crypto-store";
|
||||
import * as utils from "../test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
const USER_ID = "@user:test.server";
|
||||
const DEVICE_ID = "device_id";
|
||||
const ACCESS_TOKEN = "access_token";
|
||||
const ROOM_ID = "!room_id:server.test";
|
||||
|
||||
/* global matrixcs */
|
||||
|
||||
describe("Browserify Test", function() {
|
||||
let client;
|
||||
let httpBackend;
|
||||
|
||||
async function createTestClient() {
|
||||
const sessionStoreBackend = new MockStorageApi();
|
||||
const sessionStore = new WebStorageSessionStore(sessionStoreBackend);
|
||||
const httpBackend = new MockHttpBackend();
|
||||
beforeEach(() => {
|
||||
const testClient = new TestClient(USER_ID, DEVICE_ID, ACCESS_TOKEN);
|
||||
|
||||
const options = {
|
||||
baseUrl: "http://" + USER_ID + ".test.server",
|
||||
userId: USER_ID,
|
||||
accessToken: ACCESS_TOKEN,
|
||||
deviceId: DEVICE_ID,
|
||||
sessionStore: sessionStore,
|
||||
request: httpBackend.requestFn,
|
||||
cryptoStore: new LocalStorageCryptoStore(sessionStoreBackend),
|
||||
};
|
||||
|
||||
const client = matrixcs.createClient(options);
|
||||
client = testClient.client;
|
||||
httpBackend = testClient.httpBackend;
|
||||
|
||||
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
|
||||
return { client, httpBackend };
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
({ client, httpBackend } = await createTestClient());
|
||||
await client.startClient();
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
client.stopClient();
|
||||
await httpBackend.stop();
|
||||
httpBackend.stop();
|
||||
});
|
||||
|
||||
it("Sync", async function() {
|
||||
@@ -91,10 +71,8 @@ describe("Browserify Test", function() {
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
await Promise.race([
|
||||
Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
]),
|
||||
return await Promise.race([
|
||||
httpBackend.flushAllExpected(),
|
||||
new Promise((_, reject) => {
|
||||
client.once("sync.unexpectedError", reject);
|
||||
}),
|
||||
|
||||
@@ -348,7 +348,7 @@ function recvMessage(httpBackend, client, sender, message) {
|
||||
return testUtils.awaitDecryption(event);
|
||||
}).then((event) => {
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent()).toEqual({
|
||||
expect(event.getContent()).toMatchObject({
|
||||
msgtype: "m.text",
|
||||
body: "Hello, World",
|
||||
});
|
||||
@@ -722,6 +722,7 @@ describe("MatrixClient crypto", function() {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
logger.log(aliTestClient + ': starting');
|
||||
httpBackend.when("GET", "/capabilities").respond(200, {});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
aliTestClient.expectDeviceKeyUpload();
|
||||
|
||||
@@ -13,6 +13,7 @@ describe("MatrixClient events", function() {
|
||||
httpBackend = testClient.httpBackend;
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
|
||||
@@ -71,6 +71,7 @@ const EVENTS = [
|
||||
|
||||
// start the client, and wait for it to initialise
|
||||
function startClient(httpBackend, client) {
|
||||
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA);
|
||||
@@ -502,7 +503,7 @@ describe("MatrixClient event timelines", function() {
|
||||
const params = req.queryParams;
|
||||
expect(params.dir).toEqual("b");
|
||||
expect(params.from).toEqual("start_token0");
|
||||
expect(params.limit).toEqual(30);
|
||||
expect(params.limit).toEqual("30");
|
||||
}).respond(200, function() {
|
||||
return {
|
||||
chunk: [EVENTS[1], EVENTS[2]],
|
||||
@@ -553,7 +554,7 @@ describe("MatrixClient event timelines", function() {
|
||||
const params = req.queryParams;
|
||||
expect(params.dir).toEqual("f");
|
||||
expect(params.from).toEqual("end_token0");
|
||||
expect(params.limit).toEqual(20);
|
||||
expect(params.limit).toEqual("20");
|
||||
}).respond(200, function() {
|
||||
return {
|
||||
chunk: [EVENTS[1], EVENTS[2]],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as utils from "../test-utils";
|
||||
import { CRYPTO_ENABLED } from "../../src/client";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { Filter, MemoryStore, Room } from "../../src/matrix";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
@@ -392,6 +393,414 @@ describe("MatrixClient", function() {
|
||||
return prom;
|
||||
});
|
||||
});
|
||||
|
||||
describe("partitionThreadedEvents", function() {
|
||||
it("returns empty arrays when given an empty arrays", function() {
|
||||
const events = [];
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
expect(timeline).toEqual([]);
|
||||
expect(threaded).toEqual([]);
|
||||
});
|
||||
|
||||
it("copies pre-thread in-timeline vote events onto both timelines", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
|
||||
const eventMessageInThread = buildEventMessageInThread();
|
||||
const eventPollResponseReference = buildEventPollResponseReference();
|
||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||
|
||||
const events = [
|
||||
eventMessageInThread,
|
||||
eventPollResponseReference,
|
||||
eventPollStartThreadRoot,
|
||||
];
|
||||
// Vote has no threadId yet
|
||||
expect(eventPollResponseReference.threadId).toBeFalsy();
|
||||
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
|
||||
expect(timeline).toEqual([
|
||||
// The message that was sent in a thread is missing
|
||||
eventPollResponseReference,
|
||||
eventPollStartThreadRoot,
|
||||
]);
|
||||
|
||||
// The vote event has been copied into the thread
|
||||
const eventRefWithThreadId = withThreadId(
|
||||
eventPollResponseReference, eventPollStartThreadRoot.getId());
|
||||
expect(eventRefWithThreadId.threadId).toBeTruthy();
|
||||
|
||||
expect(threaded).toEqual([
|
||||
eventMessageInThread,
|
||||
eventRefWithThreadId,
|
||||
// Thread does not see thread root
|
||||
]);
|
||||
});
|
||||
|
||||
it("copies pre-thread in-timeline reactions onto both timelines", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
|
||||
const eventMessageInThread = buildEventMessageInThread();
|
||||
const eventReaction = buildEventReaction();
|
||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||
|
||||
const events = [
|
||||
eventMessageInThread,
|
||||
eventReaction,
|
||||
eventPollStartThreadRoot,
|
||||
];
|
||||
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
|
||||
expect(timeline).toEqual([
|
||||
eventReaction,
|
||||
eventPollStartThreadRoot,
|
||||
]);
|
||||
|
||||
expect(threaded).toEqual([
|
||||
eventMessageInThread,
|
||||
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
|
||||
]);
|
||||
});
|
||||
|
||||
it("copies post-thread in-timeline vote events onto both timelines", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
|
||||
const eventPollResponseReference = buildEventPollResponseReference();
|
||||
const eventMessageInThread = buildEventMessageInThread();
|
||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||
|
||||
const events = [
|
||||
eventPollResponseReference,
|
||||
eventMessageInThread,
|
||||
eventPollStartThreadRoot,
|
||||
];
|
||||
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
|
||||
expect(timeline).toEqual([
|
||||
eventPollResponseReference,
|
||||
eventPollStartThreadRoot,
|
||||
]);
|
||||
|
||||
expect(threaded).toEqual([
|
||||
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
|
||||
eventMessageInThread,
|
||||
]);
|
||||
});
|
||||
|
||||
it("copies post-thread in-timeline reactions onto both timelines", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
|
||||
const eventReaction = buildEventReaction();
|
||||
const eventMessageInThread = buildEventMessageInThread();
|
||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||
|
||||
const events = [
|
||||
eventReaction,
|
||||
eventMessageInThread,
|
||||
eventPollStartThreadRoot,
|
||||
];
|
||||
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
|
||||
expect(timeline).toEqual([
|
||||
eventReaction,
|
||||
eventPollStartThreadRoot,
|
||||
]);
|
||||
|
||||
expect(threaded).toEqual([
|
||||
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
|
||||
eventMessageInThread,
|
||||
]);
|
||||
});
|
||||
|
||||
it("sends room state events to the main timeline only", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
// This is based on recording the events in a real room:
|
||||
|
||||
const eventMessageInThread = buildEventMessageInThread();
|
||||
const eventPollResponseReference = buildEventPollResponseReference();
|
||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||
const eventRoomName = buildEventRoomName();
|
||||
const eventEncryption = buildEventEncryption();
|
||||
const eventGuestAccess = buildEventGuestAccess();
|
||||
const eventHistoryVisibility = buildEventHistoryVisibility();
|
||||
const eventJoinRules = buildEventJoinRules();
|
||||
const eventPowerLevels = buildEventPowerLevels();
|
||||
const eventMember = buildEventMember();
|
||||
const eventCreate = buildEventCreate();
|
||||
|
||||
const events = [
|
||||
eventMessageInThread,
|
||||
eventPollResponseReference,
|
||||
eventPollStartThreadRoot,
|
||||
eventRoomName,
|
||||
eventEncryption,
|
||||
eventGuestAccess,
|
||||
eventHistoryVisibility,
|
||||
eventJoinRules,
|
||||
eventPowerLevels,
|
||||
eventMember,
|
||||
eventCreate,
|
||||
];
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
|
||||
expect(timeline).toEqual([
|
||||
// The message that was sent in a thread is missing
|
||||
eventPollResponseReference,
|
||||
eventPollStartThreadRoot,
|
||||
eventRoomName,
|
||||
eventEncryption,
|
||||
eventGuestAccess,
|
||||
eventHistoryVisibility,
|
||||
eventJoinRules,
|
||||
eventPowerLevels,
|
||||
eventMember,
|
||||
eventCreate,
|
||||
]);
|
||||
|
||||
// Thread should contain only stuff that happened in the thread -
|
||||
// no thread root, and no room state events
|
||||
expect(threaded).toEqual([
|
||||
eventMessageInThread,
|
||||
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function withThreadId(event, newThreadId) {
|
||||
const ret = event.toSnapshot();
|
||||
ret.setThreadId(newThreadId);
|
||||
return ret;
|
||||
}
|
||||
|
||||
const buildEventMessageInThread = () => new MatrixEvent({
|
||||
"age": 80098509,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
"device_id": "XISFUZSKHH",
|
||||
"m.relates_to": {
|
||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
"m.in_reply_to": {
|
||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
},
|
||||
"rel_type": "io.element.thread",
|
||||
},
|
||||
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
|
||||
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
|
||||
},
|
||||
"event_id": "$W4chKIGYowtBblVLkRimeIg8TcdjETnxhDPGfi6NpDg",
|
||||
"origin_server_ts": 1643815466378,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.room.encrypted",
|
||||
"unsigned": { "age": 80098509 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventPollResponseReference = () => new MatrixEvent({
|
||||
"age": 80098509,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
"device_id": "XISFUZSKHH",
|
||||
"m.relates_to": {
|
||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
"rel_type": "m.reference",
|
||||
},
|
||||
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
|
||||
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
|
||||
},
|
||||
"event_id": "$91JvpezvsF0cKgav3g8W-uEVS4WkDHgxbJZvL3uMR1g",
|
||||
"origin_server_ts": 1643815458650,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.room.encrypted",
|
||||
"unsigned": { "age": 80106237 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventReaction = () => new MatrixEvent({
|
||||
"content": {
|
||||
"m.relates_to": {
|
||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
"key": "🤗",
|
||||
"rel_type": "m.annotation",
|
||||
},
|
||||
},
|
||||
"origin_server_ts": 1643977249238,
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.reaction",
|
||||
"unsigned": {
|
||||
"age": 22598,
|
||||
"transaction_id": "m1643977249073.16",
|
||||
},
|
||||
"event_id": "$86B2b-x3LgE4DlV4y24b7UHnt72LIA3rzjvMysTtAfA",
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventPollStartThreadRoot = () => new MatrixEvent({
|
||||
"age": 80108647,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
"device_id": "XISFUZSKHH",
|
||||
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
|
||||
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
|
||||
},
|
||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
"origin_server_ts": 1643815456240,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.room.encrypted",
|
||||
"unsigned": { "age": 80108647 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventRoomName = () => new MatrixEvent({
|
||||
"age": 80123249,
|
||||
"content": {
|
||||
"name": "1 poll, 1 vote, 1 thread",
|
||||
},
|
||||
"event_id": "$QAdyNJtKnl1j7or2yMycbOCvb6bCgvHs5lg3ZMd5xWk",
|
||||
"origin_server_ts": 1643815441638,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.name",
|
||||
"unsigned": { "age": 80123249 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventEncryption = () => new MatrixEvent({
|
||||
"age": 80123383,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
},
|
||||
"event_id": "$1hGykogKQkXbHw8bVuyE3BjHnFBEJBcUWnakd0ck2K0",
|
||||
"origin_server_ts": 1643815441504,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.encryption",
|
||||
"unsigned": { "age": 80123383 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventGuestAccess = () => new MatrixEvent({
|
||||
"age": 80123473,
|
||||
"content": {
|
||||
"guest_access": "can_join",
|
||||
},
|
||||
"event_id": "$4_2n-H6K9-0nPbnjjtIue2SU44tGJsnuTmi6UuSrh-U",
|
||||
"origin_server_ts": 1643815441414,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.guest_access",
|
||||
"unsigned": { "age": 80123473 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventHistoryVisibility = () => new MatrixEvent({
|
||||
"age": 80123556,
|
||||
"content": {
|
||||
"history_visibility": "shared",
|
||||
},
|
||||
"event_id": "$W6kp44CTnvciOiHSPyhp8dh4n2v1_9kclUPddeaQj0E",
|
||||
"origin_server_ts": 1643815441331,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.history_visibility",
|
||||
"unsigned": { "age": 80123556 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventJoinRules = () => new MatrixEvent({
|
||||
"age": 80123696,
|
||||
"content": {
|
||||
"join_rule": "invite",
|
||||
},
|
||||
"event_id": "$6JDDeDp7fEc0F6YnTWMruNcKWFltR3e9wk7wWDDJrAU",
|
||||
"origin_server_ts": 1643815441191,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.join_rules",
|
||||
"unsigned": { "age": 80123696 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventPowerLevels = () => new MatrixEvent({
|
||||
"age": 80124105,
|
||||
"content": {
|
||||
"ban": 50,
|
||||
"events": {
|
||||
"m.room.avatar": 50,
|
||||
"m.room.canonical_alias": 50,
|
||||
"m.room.encryption": 100,
|
||||
"m.room.history_visibility": 100,
|
||||
"m.room.name": 50,
|
||||
"m.room.power_levels": 100,
|
||||
"m.room.server_acl": 100,
|
||||
"m.room.tombstone": 100,
|
||||
},
|
||||
"events_default": 0,
|
||||
"historical": 100,
|
||||
"invite": 0,
|
||||
"kick": 50,
|
||||
"redact": 50,
|
||||
"state_default": 50,
|
||||
"users": {
|
||||
"@andybalaam-test1:matrix.org": 100,
|
||||
},
|
||||
"users_default": 0,
|
||||
},
|
||||
"event_id": "$XZY2YgQhXskpc7gmJJG3S0VmS9_QjjCUVeeFTfgfC2E",
|
||||
"origin_server_ts": 1643815440782,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.power_levels",
|
||||
"unsigned": { "age": 80124105 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventMember = () => new MatrixEvent({
|
||||
"age": 80125279,
|
||||
"content": {
|
||||
"avatar_url": "mxc://matrix.org/aNtbVcFfwotudypZcHsIcPOc",
|
||||
"displayname": "andybalaam-test1",
|
||||
"membership": "join",
|
||||
},
|
||||
"event_id": "$Ex5eVmMs_ti784mo8bgddynbwLvy6231lCycJr7Cl9M",
|
||||
"origin_server_ts": 1643815439608,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": { "age": 80125279 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventCreate = () => new MatrixEvent({
|
||||
"age": 80126105,
|
||||
"content": {
|
||||
"creator": "@andybalaam-test1:matrix.org",
|
||||
"room_version": "6",
|
||||
},
|
||||
"event_id": "$e7j2Gt37k5NPwB6lz2N3V9lO5pUdNK8Ai7i2FPEK-oI",
|
||||
"origin_server_ts": 1643815438782,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.create",
|
||||
"unsigned": { "age": 80126105 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
function assertObjectContains(obj, expected) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as utils from "../test-utils";
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
import * as utils from "../test-utils";
|
||||
import { MatrixClient } from "../../src/matrix";
|
||||
import { MatrixScheduler } from "../../src/scheduler";
|
||||
import { MemoryStore } from "../../src/store/memory";
|
||||
@@ -104,10 +105,12 @@ describe("MatrixClient opts", function() {
|
||||
expectedEventTypes.indexOf(event.getType()), 1,
|
||||
);
|
||||
});
|
||||
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "foo" });
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
await client.startClient();
|
||||
client.startClient();
|
||||
await httpBackend.flush("/capabilities", 1);
|
||||
await httpBackend.flush("/pushrules", 1);
|
||||
await httpBackend.flush("/filter", 1);
|
||||
await Promise.all([
|
||||
|
||||
+17
-11
@@ -1,16 +1,16 @@
|
||||
import { EventStatus } from "../../src/matrix";
|
||||
import { EventStatus, RoomEvent } from "../../src/matrix";
|
||||
import { MatrixScheduler } from "../../src/scheduler";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient retrying", function() {
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
let client: TestClient = null;
|
||||
let httpBackend: TestClient["httpBackend"] = null;
|
||||
let scheduler;
|
||||
const userId = "@alice:localhost";
|
||||
const accessToken = "aseukfgwef";
|
||||
const roomId = "!room:here";
|
||||
let room;
|
||||
let room: Room;
|
||||
|
||||
beforeEach(function() {
|
||||
scheduler = new MatrixScheduler();
|
||||
@@ -23,7 +23,7 @@ describe("MatrixClient retrying", function() {
|
||||
);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
room = new Room(roomId);
|
||||
room = new Room(roomId, client, userId);
|
||||
client.store.storeRoom(room);
|
||||
});
|
||||
|
||||
@@ -50,17 +50,23 @@ describe("MatrixClient retrying", function() {
|
||||
|
||||
it("should mark events as EventStatus.CANCELLED when cancelled", function() {
|
||||
// send a couple of events; the second will be queued
|
||||
const p1 = client.sendMessage(roomId, "m1").then(function(ev) {
|
||||
const p1 = client.sendMessage(roomId, {
|
||||
"msgtype": "m.text",
|
||||
"body": "m1",
|
||||
}).then(function() {
|
||||
// we expect the first message to fail
|
||||
throw new Error('Message 1 unexpectedly sent successfully');
|
||||
}, (e) => {
|
||||
}, () => {
|
||||
// this is expected
|
||||
});
|
||||
|
||||
// XXX: it turns out that the promise returned by this message
|
||||
// never gets resolved.
|
||||
// https://github.com/matrix-org/matrix-js-sdk/issues/496
|
||||
client.sendMessage(roomId, "m2");
|
||||
client.sendMessage(roomId, {
|
||||
"msgtype": "m.text",
|
||||
"body": "m2",
|
||||
});
|
||||
|
||||
// both events should be in the timeline at this point
|
||||
const tl = room.getLiveTimeline().getEvents();
|
||||
@@ -72,7 +78,7 @@ describe("MatrixClient retrying", function() {
|
||||
expect(ev2.status).toEqual(EventStatus.SENDING);
|
||||
|
||||
// the first message should get sent, and the second should get queued
|
||||
httpBackend.when("PUT", "/send/m.room.message/").check(function(rq) {
|
||||
httpBackend.when("PUT", "/send/m.room.message/").check(function() {
|
||||
// ev2 should now have been queued
|
||||
expect(ev2.status).toEqual(EventStatus.QUEUED);
|
||||
|
||||
@@ -88,8 +94,8 @@ describe("MatrixClient retrying", function() {
|
||||
}).respond(400); // fail the first message
|
||||
|
||||
// wait for the localecho of ev1 to be updated
|
||||
const p3 = new Promise((resolve, reject) => {
|
||||
room.on("Room.localEchoUpdated", (ev0) => {
|
||||
const p3 = new Promise<void>((resolve, reject) => {
|
||||
room.on(RoomEvent.LocalEchoUpdated, (ev0) => {
|
||||
if (ev0 === ev1) {
|
||||
resolve();
|
||||
}
|
||||
@@ -96,7 +96,7 @@ describe("MatrixClient room timelines", function() {
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
beforeEach(async function() {
|
||||
// these tests should work with or without timelineSupport
|
||||
const testClient = new TestClient(
|
||||
userId,
|
||||
@@ -109,6 +109,7 @@ describe("MatrixClient room timelines", function() {
|
||||
client = testClient.client;
|
||||
|
||||
setNextSyncData();
|
||||
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
@@ -116,9 +117,10 @@ describe("MatrixClient room timelines", function() {
|
||||
return NEXT_SYNC_DATA;
|
||||
});
|
||||
client.startClient();
|
||||
return httpBackend.flush("/pushrules").then(function() {
|
||||
return httpBackend.flush("/filter");
|
||||
});
|
||||
|
||||
await httpBackend.flush("/capabilities");
|
||||
await httpBackend.flush("/pushrules");
|
||||
await httpBackend.flush("/filter");
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
|
||||
@@ -19,6 +19,7 @@ describe("MatrixClient syncing", function() {
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import anotherjson from "another-json";
|
||||
|
||||
import * as testUtils from "../test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { logger } from "../../src/logger";
|
||||
|
||||
+16
-14
@@ -85,6 +85,7 @@ export function mkEvent(opts) {
|
||||
room_id: opts.room,
|
||||
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
||||
content: opts.content,
|
||||
unsigned: opts.unsigned,
|
||||
event_id: "$" + Math.random() + "-" + Math.random(),
|
||||
};
|
||||
if (opts.skey !== undefined) {
|
||||
@@ -341,8 +342,15 @@ HttpResponse.SYNC_RESPONSE = {
|
||||
data: HttpResponse.SYNC_DATA,
|
||||
};
|
||||
|
||||
HttpResponse.CAPABILITIES_RESPONSE = {
|
||||
method: "GET",
|
||||
path: "/capabilities",
|
||||
data: { capabilities: {} },
|
||||
};
|
||||
|
||||
HttpResponse.defaultResponses = function(userId) {
|
||||
return [
|
||||
HttpResponse.CAPABILITIES_RESPONSE,
|
||||
HttpResponse.PUSH_RULES_RESPONSE,
|
||||
HttpResponse.filterResponse(userId),
|
||||
HttpResponse.SYNC_RESPONSE,
|
||||
@@ -350,19 +358,13 @@ HttpResponse.defaultResponses = function(userId) {
|
||||
};
|
||||
|
||||
export function setHttpResponses(
|
||||
client, responses, acceptKeepalives, ignoreUnhandledSyncs,
|
||||
httpBackend, responses,
|
||||
) {
|
||||
const httpResponseObj = new HttpResponse(
|
||||
responses, acceptKeepalives, ignoreUnhandledSyncs,
|
||||
);
|
||||
|
||||
const httpReq = httpResponseObj.request.bind(httpResponseObj);
|
||||
client.http = [
|
||||
"authedRequest", "authedRequestWithPrefix", "getContentUri",
|
||||
"request", "requestWithPrefix", "uploadContent",
|
||||
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
|
||||
client.http.authedRequest.mockImplementation(httpReq);
|
||||
client.http.authedRequestWithPrefix.mockImplementation(httpReq);
|
||||
client.http.requestWithPrefix.mockImplementation(httpReq);
|
||||
client.http.request.mockImplementation(httpReq);
|
||||
responses.forEach(response => {
|
||||
httpBackend
|
||||
.when(response.method, response.path)
|
||||
.respond(200, response.data);
|
||||
});
|
||||
}
|
||||
|
||||
export const emitPromise = (e, k) => new Promise(r => e.once(k, r));
|
||||
|
||||
@@ -14,7 +14,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { ReEmitter } from "../../src/ReEmitter";
|
||||
|
||||
const EVENTNAME = "UnknownEntry";
|
||||
|
||||
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import * as sdk from "../../src";
|
||||
import { AutoDiscovery } from "../../src/autodiscovery";
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import '../olm-loader';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { Crypto } from "../../src/crypto";
|
||||
import { WebStorageSessionStore } from "../../src/store/session/webstorage";
|
||||
import { MemoryCryptoStore } from "../../src/crypto/store/memory-crypto-store";
|
||||
@@ -8,7 +11,6 @@ import { MatrixEvent } from "../../src/models/event";
|
||||
import { Room } from "../../src/models/room";
|
||||
import * as olmlib from "../../src/crypto/olmlib";
|
||||
import { sleep } from "../../src/utils";
|
||||
import { EventEmitter } from "events";
|
||||
import { CRYPTO_ENABLED } from "../../src/client";
|
||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
|
||||
|
||||
@@ -256,97 +256,145 @@ describe("MegolmDecryption", function() {
|
||||
});
|
||||
});
|
||||
|
||||
it("re-uses sessions for sequential messages", async function() {
|
||||
mockCrypto.backupManager = {
|
||||
backupGroupSession: () => {},
|
||||
};
|
||||
const mockStorage = new MockStorageApi();
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
describe("session reuse and key reshares", () => {
|
||||
let megolmEncryption;
|
||||
let aliceDeviceInfo;
|
||||
let mockRoom;
|
||||
let olmDevice;
|
||||
|
||||
const olmDevice = new OlmDevice(cryptoStore);
|
||||
olmDevice.verifySignature = jest.fn();
|
||||
await olmDevice.init();
|
||||
beforeEach(async () => {
|
||||
mockCrypto.backupManager = {
|
||||
backupGroupSession: () => {},
|
||||
};
|
||||
const mockStorage = new MockStorageApi();
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
|
||||
mockBaseApis.claimOneTimeKeys = jest.fn().mockReturnValue(Promise.resolve({
|
||||
one_time_keys: {
|
||||
'@alice:home.server': {
|
||||
aliceDevice: {
|
||||
'signed_curve25519:flooble': {
|
||||
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
|
||||
signatures: {
|
||||
'@alice:home.server': {
|
||||
'ed25519:aliceDevice': 'totally valid',
|
||||
olmDevice = new OlmDevice(cryptoStore);
|
||||
olmDevice.verifySignature = jest.fn();
|
||||
await olmDevice.init();
|
||||
|
||||
mockBaseApis.claimOneTimeKeys = jest.fn().mockReturnValue(Promise.resolve({
|
||||
one_time_keys: {
|
||||
'@alice:home.server': {
|
||||
aliceDevice: {
|
||||
'signed_curve25519:flooble': {
|
||||
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
|
||||
signatures: {
|
||||
'@alice:home.server': {
|
||||
'ed25519:aliceDevice': 'totally valid',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
mockBaseApis.sendToDevice = jest.fn().mockResolvedValue(undefined);
|
||||
}));
|
||||
mockBaseApis.sendToDevice = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
mockCrypto.downloadKeys.mockReturnValue(Promise.resolve({
|
||||
'@alice:home.server': {
|
||||
aliceDevice: {
|
||||
deviceId: 'aliceDevice',
|
||||
isBlocked: jest.fn().mockReturnValue(false),
|
||||
isUnverified: jest.fn().mockReturnValue(false),
|
||||
getIdentityKey: jest.fn().mockReturnValue(
|
||||
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE',
|
||||
),
|
||||
getFingerprint: jest.fn().mockReturnValue(''),
|
||||
aliceDeviceInfo = {
|
||||
deviceId: 'aliceDevice',
|
||||
isBlocked: jest.fn().mockReturnValue(false),
|
||||
isUnverified: jest.fn().mockReturnValue(false),
|
||||
getIdentityKey: jest.fn().mockReturnValue(
|
||||
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE',
|
||||
),
|
||||
getFingerprint: jest.fn().mockReturnValue(''),
|
||||
};
|
||||
|
||||
mockCrypto.downloadKeys.mockReturnValue(Promise.resolve({
|
||||
'@alice:home.server': {
|
||||
aliceDevice: aliceDeviceInfo,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}));
|
||||
|
||||
mockCrypto.checkDeviceTrust.mockReturnValue({
|
||||
isVerified: () => false,
|
||||
mockCrypto.checkDeviceTrust.mockReturnValue({
|
||||
isVerified: () => false,
|
||||
});
|
||||
|
||||
megolmEncryption = new MegolmEncryption({
|
||||
userId: '@user:id',
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: mockBaseApis,
|
||||
roomId: ROOM_ID,
|
||||
config: {
|
||||
rotation_period_ms: 9999999999999,
|
||||
},
|
||||
});
|
||||
mockRoom = {
|
||||
getEncryptionTargetMembers: jest.fn().mockReturnValue(
|
||||
[{ userId: "@alice:home.server" }],
|
||||
),
|
||||
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
|
||||
};
|
||||
});
|
||||
|
||||
const megolmEncryption = new MegolmEncryption({
|
||||
userId: '@user:id',
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: mockBaseApis,
|
||||
roomId: ROOM_ID,
|
||||
config: {
|
||||
rotation_period_ms: 9999999999999,
|
||||
},
|
||||
});
|
||||
const mockRoom = {
|
||||
getEncryptionTargetMembers: jest.fn().mockReturnValue(
|
||||
[{ userId: "@alice:home.server" }],
|
||||
),
|
||||
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
|
||||
};
|
||||
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some text",
|
||||
});
|
||||
expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
|
||||
it("re-uses sessions for sequential messages", async function() {
|
||||
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some text",
|
||||
});
|
||||
expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
|
||||
|
||||
// this should have claimed a key for alice as it's starting a new session
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
|
||||
);
|
||||
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
|
||||
['@alice:home.server'], false,
|
||||
);
|
||||
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
|
||||
);
|
||||
// this should have claimed a key for alice as it's starting a new session
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
|
||||
);
|
||||
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
|
||||
['@alice:home.server'], false,
|
||||
);
|
||||
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
|
||||
);
|
||||
|
||||
mockBaseApis.claimOneTimeKeys.mockReset();
|
||||
mockBaseApis.claimOneTimeKeys.mockReset();
|
||||
|
||||
const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some more text",
|
||||
const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some more text",
|
||||
});
|
||||
|
||||
// this should *not* have claimed a key as it should be using the same session
|
||||
expect(mockBaseApis.claimOneTimeKeys).not.toHaveBeenCalled();
|
||||
|
||||
// likewise they should show the same session ID
|
||||
expect(ct2.session_id).toEqual(ct1.session_id);
|
||||
});
|
||||
|
||||
// this should *not* have claimed a key as it should be using the same session
|
||||
expect(mockBaseApis.claimOneTimeKeys).not.toHaveBeenCalled();
|
||||
it("re-shares keys to devices it's already sent to", async function() {
|
||||
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some text",
|
||||
});
|
||||
|
||||
// likewise they should show the same session ID
|
||||
expect(ct2.session_id).toEqual(ct1.session_id);
|
||||
mockBaseApis.sendToDevice.mockClear();
|
||||
await megolmEncryption.reshareKeyWithDevice(
|
||||
olmDevice.deviceCurve25519Key,
|
||||
ct1.session_id,
|
||||
'@alice:home.server',
|
||||
aliceDeviceInfo,
|
||||
);
|
||||
|
||||
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not re-share keys to devices whose keys have changed", async function() {
|
||||
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some text",
|
||||
});
|
||||
|
||||
aliceDeviceInfo.getIdentityKey = jest.fn().mockReturnValue(
|
||||
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWI',
|
||||
);
|
||||
|
||||
mockBaseApis.sendToDevice.mockClear();
|
||||
await megolmEncryption.reshareKeyWithDevice(
|
||||
olmDevice.deviceCurve25519Key,
|
||||
ct1.session_id,
|
||||
'@alice:home.server',
|
||||
aliceDeviceInfo,
|
||||
);
|
||||
|
||||
expect(mockBaseApis.sendToDevice).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -246,7 +246,7 @@ describe("OlmDevice", function() {
|
||||
|
||||
// After a single tick through the first task, it should have
|
||||
// claimed ownership of all devices to avoid deadlocking others.
|
||||
expect(Object.keys(aliceOlmDevice._sessionsInProgress).length).toBe(2);
|
||||
expect(Object.keys(aliceOlmDevice.sessionsInProgress).length).toBe(2);
|
||||
|
||||
const task2 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
|
||||
aliceOlmDevice, baseApis, devicesByUserBA,
|
||||
@@ -254,7 +254,7 @@ describe("OlmDevice", function() {
|
||||
|
||||
// The second task should not have changed the ownership count, as
|
||||
// it's waiting on the first task.
|
||||
expect(Object.keys(aliceOlmDevice._sessionsInProgress).length).toBe(2);
|
||||
expect(Object.keys(aliceOlmDevice.sessionsInProgress).length).toBe(2);
|
||||
|
||||
// Track the tasks, but don't await them yet.
|
||||
const promises = Promise.all([
|
||||
|
||||
@@ -288,7 +288,7 @@ describe("MegolmBackup", function() {
|
||||
ed25519: "SENDER_ED25519",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
session: ibGroupSession.pickle(olmDevice._pickleKey),
|
||||
session: ibGroupSession.pickle(olmDevice.pickleKey),
|
||||
},
|
||||
txn);
|
||||
});
|
||||
@@ -369,7 +369,7 @@ describe("MegolmBackup", function() {
|
||||
ed25519: "SENDER_ED25519",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
session: ibGroupSession.pickle(olmDevice._pickleKey),
|
||||
session: ibGroupSession.pickle(olmDevice.pickleKey),
|
||||
},
|
||||
txn);
|
||||
});
|
||||
@@ -546,7 +546,7 @@ describe("MegolmBackup", function() {
|
||||
ed25519: "SENDER_ED25519",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
session: ibGroupSession.pickle(olmDevice._pickleKey),
|
||||
session: ibGroupSession.pickle(olmDevice.pickleKey),
|
||||
},
|
||||
txn);
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ limitations under the License.
|
||||
|
||||
import '../../olm-loader';
|
||||
import anotherjson from 'another-json';
|
||||
|
||||
import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import { TestClient } from '../../TestClient';
|
||||
import { HttpResponse, setHttpResponses } from '../../test-utils';
|
||||
@@ -39,13 +40,14 @@ async function makeTestClient(userInfo, options, keys) {
|
||||
options.cryptoCallbacks = Object.assign(
|
||||
{}, { getCrossSigningKey, saveCrossSigningKeys }, options.cryptoCallbacks || {},
|
||||
);
|
||||
const client = (new TestClient(
|
||||
const testClient = new TestClient(
|
||||
userInfo.userId, userInfo.deviceId, undefined, undefined, options,
|
||||
)).client;
|
||||
);
|
||||
const client = testClient.client;
|
||||
|
||||
await client.initCrypto();
|
||||
|
||||
return client;
|
||||
return { client, httpBackend: testClient.httpBackend };
|
||||
}
|
||||
|
||||
describe("Cross Signing", function() {
|
||||
@@ -59,7 +61,7 @@ describe("Cross Signing", function() {
|
||||
});
|
||||
|
||||
it("should sign the master key with the device key", async function() {
|
||||
const alice = await makeTestClient(
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = jest.fn(async (auth, keys) => {
|
||||
@@ -79,7 +81,7 @@ describe("Cross Signing", function() {
|
||||
});
|
||||
|
||||
it("should abort bootstrap if device signing auth fails", async function() {
|
||||
const alice = await makeTestClient(
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async (auth, keys) => {
|
||||
@@ -130,7 +132,7 @@ describe("Cross Signing", function() {
|
||||
});
|
||||
|
||||
it("should upload a signature when a user is verified", async function() {
|
||||
const alice = await makeTestClient(
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
@@ -160,7 +162,7 @@ describe("Cross Signing", function() {
|
||||
await promise;
|
||||
});
|
||||
|
||||
it("should get cross-signing keys from sync", async function() {
|
||||
it.skip("should get cross-signing keys from sync", async function() {
|
||||
const masterKey = new Uint8Array([
|
||||
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82,
|
||||
0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef,
|
||||
@@ -174,7 +176,7 @@ describe("Cross Signing", function() {
|
||||
0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f,
|
||||
]);
|
||||
|
||||
const alice = await makeTestClient(
|
||||
const { client: alice, httpBackend } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
@@ -235,6 +237,7 @@ describe("Cross Signing", function() {
|
||||
|
||||
// feed sync result that includes master key, ssk, device key
|
||||
const responses = [
|
||||
HttpResponse.CAPABILITIES_RESPONSE,
|
||||
HttpResponse.PUSH_RULES_RESPONSE,
|
||||
{
|
||||
method: "POST",
|
||||
@@ -310,9 +313,10 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
},
|
||||
];
|
||||
setHttpResponses(alice, responses, true, true);
|
||||
setHttpResponses(httpBackend, responses);
|
||||
|
||||
await alice.startClient();
|
||||
alice.startClient();
|
||||
httpBackend.flushAllExpected();
|
||||
|
||||
// once ssk is confirmed, device key should be trusted
|
||||
await keyChangePromise;
|
||||
@@ -331,7 +335,7 @@ describe("Cross Signing", function() {
|
||||
});
|
||||
|
||||
it("should use trust chain to determine device verification", async function() {
|
||||
const alice = await makeTestClient(
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
@@ -414,9 +418,9 @@ describe("Cross Signing", function() {
|
||||
expect(bobDeviceTrust2.isTofu()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should trust signatures received from other devices", async function() {
|
||||
it.skip("should trust signatures received from other devices", async function() {
|
||||
const aliceKeys = {};
|
||||
const alice = await makeTestClient(
|
||||
const { client: alice, httpBackend } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
null,
|
||||
aliceKeys,
|
||||
@@ -490,6 +494,7 @@ describe("Cross Signing", function() {
|
||||
// - master key signed by her usk (pretend that it was signed by another
|
||||
// of Alice's devices)
|
||||
const responses = [
|
||||
HttpResponse.CAPABILITIES_RESPONSE,
|
||||
HttpResponse.PUSH_RULES_RESPONSE,
|
||||
{
|
||||
method: "POST",
|
||||
@@ -560,10 +565,10 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
},
|
||||
];
|
||||
setHttpResponses(alice, responses);
|
||||
|
||||
await alice.startClient();
|
||||
setHttpResponses(httpBackend, responses);
|
||||
|
||||
alice.startClient();
|
||||
httpBackend.flushAllExpected();
|
||||
await keyChangePromise;
|
||||
|
||||
// Bob's device key should be trusted
|
||||
@@ -578,7 +583,7 @@ describe("Cross Signing", function() {
|
||||
});
|
||||
|
||||
it("should dis-trust an unsigned device", async function() {
|
||||
const alice = await makeTestClient(
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
@@ -647,7 +652,7 @@ describe("Cross Signing", function() {
|
||||
});
|
||||
|
||||
it("should dis-trust a user when their ssk changes", async function() {
|
||||
const alice = await makeTestClient(
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
@@ -785,7 +790,7 @@ describe("Cross Signing", function() {
|
||||
it("should offer to upgrade device verifications to cross-signing", async function() {
|
||||
let upgradeResolveFunc;
|
||||
|
||||
const alice = await makeTestClient(
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
@@ -797,7 +802,7 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
},
|
||||
);
|
||||
const bob = await makeTestClient(
|
||||
const { client: bob } = await makeTestClient(
|
||||
{ userId: "@bob:example.com", deviceId: "Dynabook" },
|
||||
);
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function resetCrossSigningKeys(client, {
|
||||
crypto.crossSigningInfo.keys = oldKeys;
|
||||
throw e;
|
||||
}
|
||||
crypto.baseApis.emit("crossSigning.keysChanged", {});
|
||||
crypto.emit("crossSigning.keysChanged", {});
|
||||
await crypto.afterCrossSigningLocalKeyChange();
|
||||
}
|
||||
|
||||
|
||||
@@ -18,11 +18,11 @@ import {
|
||||
IndexedDBCryptoStore,
|
||||
} from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store';
|
||||
import { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager';
|
||||
|
||||
import 'fake-indexeddb/auto';
|
||||
import 'jest-localstorage-mock';
|
||||
|
||||
import { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager';
|
||||
|
||||
const requests = [
|
||||
{
|
||||
requestId: "A",
|
||||
|
||||
@@ -23,7 +23,6 @@ import { makeTestClients } from './verification/util';
|
||||
import { encryptAES } from "../../../src/crypto/aes";
|
||||
import { resetCrossSigningKeys, createSecretStorageKey } from "./crypto-utils";
|
||||
import { logger } from '../../../src/logger';
|
||||
|
||||
import * as utils from "../../../src/utils";
|
||||
|
||||
try {
|
||||
|
||||
@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
|
||||
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
||||
|
||||
describe("InRoomChannel tests", function() {
|
||||
const ALICE = "@alice:hs.tld";
|
||||
|
||||
@@ -69,7 +69,7 @@ describe("verification request integration tests with crypto layer", function()
|
||||
bobVerifier.verify();
|
||||
|
||||
// XXX: Private function access (but it's a test, so we're okay)
|
||||
bobVerifier._endTimer();
|
||||
bobVerifier.endTimer();
|
||||
});
|
||||
const aliceRequest = await alice.client.requestVerification("@bob:example.com");
|
||||
await aliceRequest.waitFor(r => r.started);
|
||||
@@ -77,6 +77,6 @@ describe("verification request integration tests with crypto layer", function()
|
||||
expect(aliceVerifier).toBeInstanceOf(SAS);
|
||||
|
||||
// XXX: Private function access (but it's a test, so we're okay)
|
||||
aliceVerifier._endTimer();
|
||||
aliceVerifier.endTimer();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { VerificationBase } from '../../../../src/crypto/verification/Base';
|
||||
import { CrossSigningInfo } from '../../../../src/crypto/CrossSigning';
|
||||
import { encodeBase64 } from "../../../../src/crypto/olmlib";
|
||||
import { setupWebcrypto, teardownWebcrypto } from './util';
|
||||
import { VerificationBase } from '../../../../src/crypto/verification/Base';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -93,7 +93,7 @@ describe("self-verifications", () => {
|
||||
undefined, // startEvent
|
||||
request,
|
||||
);
|
||||
verification._resolve = () => undefined;
|
||||
verification.resolve = () => undefined;
|
||||
|
||||
const result = await verification.done();
|
||||
|
||||
|
||||
@@ -15,9 +15,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import nodeCrypto from "crypto";
|
||||
|
||||
import { TestClient } from '../../../TestClient';
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import nodeCrypto from "crypto";
|
||||
import { logger } from '../../../../src/logger';
|
||||
|
||||
export async function makeTestClients(userInfos, options) {
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { FilterComponent } from "../../src/filter-component";
|
||||
import { mkEvent } from '../test-utils';
|
||||
|
||||
describe("Filter Component", function() {
|
||||
describe("types", function() {
|
||||
it("should filter out events with other types", function() {
|
||||
const filter = new FilterComponent({ types: ['m.room.message'] });
|
||||
const event = mkEvent({
|
||||
type: 'm.room.member',
|
||||
content: { },
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
});
|
||||
|
||||
const checkResult = filter.check(event);
|
||||
|
||||
expect(checkResult).toBe(false);
|
||||
});
|
||||
|
||||
it("should validate events with the same type", function() {
|
||||
const filter = new FilterComponent({ types: ['m.room.message'] });
|
||||
const event = mkEvent({
|
||||
type: 'm.room.message',
|
||||
content: { },
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
});
|
||||
|
||||
const checkResult = filter.check(event);
|
||||
|
||||
expect(checkResult).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
import {
|
||||
RelationType,
|
||||
UNSTABLE_FILTER_RELATED_BY_REL_TYPES,
|
||||
UNSTABLE_FILTER_RELATED_BY_SENDERS,
|
||||
} from "../../src";
|
||||
import { FilterComponent } from "../../src/filter-component";
|
||||
import { mkEvent } from '../test-utils';
|
||||
|
||||
describe("Filter Component", function() {
|
||||
describe("types", function() {
|
||||
it("should filter out events with other types", function() {
|
||||
const filter = new FilterComponent({ types: ['m.room.message'] });
|
||||
const event = mkEvent({
|
||||
type: 'm.room.member',
|
||||
content: { },
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
});
|
||||
|
||||
const checkResult = filter.check(event);
|
||||
|
||||
expect(checkResult).toBe(false);
|
||||
});
|
||||
|
||||
it("should validate events with the same type", function() {
|
||||
const filter = new FilterComponent({ types: ['m.room.message'] });
|
||||
const event = mkEvent({
|
||||
type: 'm.room.message',
|
||||
content: { },
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
});
|
||||
|
||||
const checkResult = filter.check(event);
|
||||
|
||||
expect(checkResult).toBe(true);
|
||||
});
|
||||
|
||||
it("should filter out events by relation participation", function() {
|
||||
const currentUserId = '@me:server.org';
|
||||
const filter = new FilterComponent({
|
||||
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]: [currentUserId],
|
||||
}, currentUserId);
|
||||
|
||||
const threadRootNotParticipated = mkEvent({
|
||||
type: 'm.room.message',
|
||||
content: {},
|
||||
room: 'roomId',
|
||||
user: '@someone-else:server.org',
|
||||
event: true,
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
[RelationType.Thread]: {
|
||||
count: 2,
|
||||
current_user_participated: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(filter.check(threadRootNotParticipated)).toBe(false);
|
||||
});
|
||||
|
||||
it("should keep events by relation participation", function() {
|
||||
const currentUserId = '@me:server.org';
|
||||
const filter = new FilterComponent({
|
||||
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]: [currentUserId],
|
||||
}, currentUserId);
|
||||
|
||||
const threadRootParticipated = mkEvent({
|
||||
type: 'm.room.message',
|
||||
content: {},
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
[RelationType.Thread]: {
|
||||
count: 2,
|
||||
current_user_participated: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
user: '@someone-else:server.org',
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
});
|
||||
|
||||
expect(filter.check(threadRootParticipated)).toBe(true);
|
||||
});
|
||||
|
||||
it("should filter out events by relation type", function() {
|
||||
const filter = new FilterComponent({
|
||||
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]: [RelationType.Thread],
|
||||
});
|
||||
|
||||
const referenceRelationEvent = mkEvent({
|
||||
type: 'm.room.message',
|
||||
content: {},
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
[RelationType.Reference]: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(filter.check(referenceRelationEvent)).toBe(false);
|
||||
});
|
||||
|
||||
it("should keep events by relation type", function() {
|
||||
const filter = new FilterComponent({
|
||||
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]: [RelationType.Thread],
|
||||
});
|
||||
|
||||
const threadRootEvent = mkEvent({
|
||||
type: 'm.room.message',
|
||||
content: {},
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
[RelationType.Thread]: {
|
||||
count: 2,
|
||||
current_user_participated: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
});
|
||||
|
||||
expect(filter.check(threadRootEvent)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { makeLocationContent } from "../../src/content-helpers";
|
||||
import {
|
||||
ASSET_NODE_TYPE,
|
||||
LocationAssetType,
|
||||
LOCATION_EVENT_TYPE,
|
||||
TIMESTAMP_NODE_TYPE,
|
||||
} from "../../src/@types/location";
|
||||
import { TEXT_NODE_TYPE } from "../../src/@types/extensible_events";
|
||||
|
||||
describe("Location", function() {
|
||||
it("should create a valid location with defaults", function() {
|
||||
const loc = makeLocationContent("txt", "geo:foo", 134235435);
|
||||
expect(loc.body).toEqual("txt");
|
||||
expect(loc.msgtype).toEqual("m.location");
|
||||
expect(loc.geo_uri).toEqual("geo:foo");
|
||||
expect(LOCATION_EVENT_TYPE.findIn(loc)).toEqual({
|
||||
uri: "geo:foo",
|
||||
description: undefined,
|
||||
});
|
||||
expect(ASSET_NODE_TYPE.findIn(loc)).toEqual({ type: LocationAssetType.Self });
|
||||
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual("txt");
|
||||
expect(TIMESTAMP_NODE_TYPE.findIn(loc)).toEqual(134235435);
|
||||
});
|
||||
|
||||
it("should create a valid location with explicit properties", function() {
|
||||
const loc = makeLocationContent(
|
||||
"txxt", "geo:bar", 134235436, "desc", LocationAssetType.Pin);
|
||||
|
||||
expect(loc.body).toEqual("txxt");
|
||||
expect(loc.msgtype).toEqual("m.location");
|
||||
expect(loc.geo_uri).toEqual("geo:bar");
|
||||
expect(LOCATION_EVENT_TYPE.findIn(loc)).toEqual({
|
||||
uri: "geo:bar",
|
||||
description: "desc",
|
||||
});
|
||||
expect(ASSET_NODE_TYPE.findIn(loc)).toEqual({ type: LocationAssetType.Pin });
|
||||
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual("txxt");
|
||||
expect(TIMESTAMP_NODE_TYPE.findIn(loc)).toEqual(134235436);
|
||||
});
|
||||
});
|
||||
@@ -11,8 +11,9 @@ import {
|
||||
UNSTABLE_MSC3089_TREE_SUBTYPE,
|
||||
} from "../../src/@types/event";
|
||||
import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { EventStatus, MatrixEvent } from "../../src/models/event";
|
||||
import { Preset } from "../../src/@types/partials";
|
||||
import * as testUtils from "../test-utils";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -52,6 +53,12 @@ describe("MatrixClient", function() {
|
||||
data: SYNC_DATA,
|
||||
};
|
||||
|
||||
const CAPABILITIES_RESPONSE = {
|
||||
method: "GET",
|
||||
path: "/capabilities",
|
||||
data: { capabilities: {} },
|
||||
};
|
||||
|
||||
let httpLookups = [
|
||||
// items are objects which look like:
|
||||
// {
|
||||
@@ -84,11 +91,7 @@ describe("MatrixClient", function() {
|
||||
return pendingLookup.promise;
|
||||
}
|
||||
// >1 pending thing, and they are different, whine.
|
||||
expect(false).toBe(
|
||||
true, ">1 pending request. You should probably handle them. " +
|
||||
"PENDING: " + JSON.stringify(pendingLookup) + " JUST GOT: " +
|
||||
method + " " + path,
|
||||
);
|
||||
expect(false).toBe(true);
|
||||
}
|
||||
pendingLookup = {
|
||||
promise: new Promise(() => {}),
|
||||
@@ -116,6 +119,7 @@ describe("MatrixClient", function() {
|
||||
}
|
||||
|
||||
if (next.error) {
|
||||
// eslint-disable-next-line
|
||||
return Promise.reject({
|
||||
errcode: next.error.errcode,
|
||||
httpStatus: next.error.httpStatus,
|
||||
@@ -126,7 +130,7 @@ describe("MatrixClient", function() {
|
||||
}
|
||||
return Promise.resolve(next.data);
|
||||
}
|
||||
expect(true).toBe(false, "Expected different request. " + logLine);
|
||||
expect(true).toBe(false);
|
||||
return new Promise(() => {});
|
||||
}
|
||||
|
||||
@@ -151,7 +155,7 @@ describe("MatrixClient", function() {
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: identityServerUrl,
|
||||
accessToken: "my.access.token",
|
||||
request: function() {}, // NOP
|
||||
request: function() {} as any, // NOP
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: userId,
|
||||
@@ -167,6 +171,7 @@ describe("MatrixClient", function() {
|
||||
acceptKeepalives = true;
|
||||
pendingLookup = null;
|
||||
httpLookups = [];
|
||||
httpLookups.push(CAPABILITIES_RESPONSE);
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
@@ -237,6 +242,7 @@ describe("MatrixClient", function() {
|
||||
it("should get (unstable) file trees with valid state", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "join",
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
@@ -270,9 +276,33 @@ describe("MatrixClient", function() {
|
||||
expect(tree.room).toBe(mockRoom);
|
||||
});
|
||||
|
||||
it("should not get (unstable) file trees if not joined", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "leave", // "not join"
|
||||
};
|
||||
client.getRoom = (getRoomId) => {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
return mockRoom;
|
||||
};
|
||||
const tree = client.unstableGetFileTreeSpace(roomId);
|
||||
expect(tree).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not get (unstable) file trees for unknown rooms", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
client.getRoom = (getRoomId) => {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
return null; // imply unknown
|
||||
};
|
||||
const tree = client.unstableGetFileTreeSpace(roomId);
|
||||
expect(tree).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not get (unstable) file trees with invalid create contents", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "join",
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
@@ -307,6 +337,7 @@ describe("MatrixClient", function() {
|
||||
it("should not get (unstable) file trees with invalid purpose/subtype contents", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "join",
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
@@ -338,15 +369,17 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
it("should not POST /filter if a matching filter already exists", async function() {
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
httpLookups = [
|
||||
CAPABILITIES_RESPONSE,
|
||||
PUSH_RULES_RESPONSE,
|
||||
SYNC_RESPONSE,
|
||||
];
|
||||
const filterId = "ehfewf";
|
||||
store.getFilterIdByName.mockReturnValue(filterId);
|
||||
const filter = new Filter(0, filterId);
|
||||
const filter = new Filter("0", filterId);
|
||||
filter.setDefinition({ "room": { "timeline": { "limit": 8 } } });
|
||||
store.getFilter.mockReturnValue(filter);
|
||||
const syncPromise = new Promise((resolve, reject) => {
|
||||
const syncPromise = new Promise<void>((resolve, reject) => {
|
||||
client.on("sync", function syncListener(state) {
|
||||
if (state === "SYNCING") {
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
@@ -367,7 +400,7 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
it("should return the same sync state as emitted sync events", async function() {
|
||||
const syncingPromise = new Promise((resolve) => {
|
||||
const syncingPromise = new Promise<void>((resolve) => {
|
||||
client.on("sync", function syncListener(state) {
|
||||
expect(state).toEqual(client.getSyncState());
|
||||
if (state === "SYNCING") {
|
||||
@@ -387,7 +420,7 @@ describe("MatrixClient", function() {
|
||||
it("should use an existing filter if id is present in localStorage", function() {
|
||||
});
|
||||
it("should handle localStorage filterId missing from the server", function(done) {
|
||||
function getFilterName(userId, suffix) {
|
||||
function getFilterName(userId, suffix?: string) {
|
||||
// scope this on the user ID because people may login on many accounts
|
||||
// and they all need to be stored!
|
||||
return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : "");
|
||||
@@ -421,12 +454,15 @@ describe("MatrixClient", function() {
|
||||
|
||||
describe("retryImmediately", function() {
|
||||
it("should return false if there is no request waiting", async function() {
|
||||
httpLookups = [];
|
||||
httpLookups.push(CAPABILITIES_RESPONSE);
|
||||
await client.startClient();
|
||||
expect(client.retryImmediately()).toBe(false);
|
||||
});
|
||||
|
||||
it("should work on /filter", function(done) {
|
||||
httpLookups = [];
|
||||
httpLookups.push(CAPABILITIES_RESPONSE);
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
@@ -462,7 +498,7 @@ describe("MatrixClient", function() {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(1);
|
||||
expect(client.retryImmediately()).toBe(
|
||||
true, "retryImmediately returned false",
|
||||
true,
|
||||
);
|
||||
jest.advanceTimersByTime(1);
|
||||
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
|
||||
@@ -477,6 +513,7 @@ describe("MatrixClient", function() {
|
||||
|
||||
it("should work on /pushrules", function(done) {
|
||||
httpLookups = [];
|
||||
httpLookups.push(CAPABILITIES_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/pushrules/", error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
});
|
||||
@@ -533,6 +570,7 @@ describe("MatrixClient", function() {
|
||||
it("should transition null -> ERROR after a failed /filter", function(done) {
|
||||
const expectedStates = [];
|
||||
httpLookups = [];
|
||||
httpLookups.push(CAPABILITIES_RESPONSE);
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
@@ -543,32 +581,33 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
it("should transition ERROR -> CATCHUP after /sync if prev failed",
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
acceptKeepalives = false;
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH,
|
||||
error: { errcode: "KEEPALIVE_FAIL" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH, data: {},
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", data: SYNC_DATA,
|
||||
});
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
acceptKeepalives = false;
|
||||
httpLookups = [];
|
||||
httpLookups.push(CAPABILITIES_RESPONSE);
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH,
|
||||
error: { errcode: "KEEPALIVE_FAIL" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH, data: {},
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", data: SYNC_DATA,
|
||||
});
|
||||
|
||||
expectedStates.push(["RECONNECTING", null]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["CATCHUP", "ERROR"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
expectedStates.push(["RECONNECTING", null]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["CATCHUP", "ERROR"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition PREPARED -> SYNCING after /sync", function(done) {
|
||||
const expectedStates = [];
|
||||
@@ -598,32 +637,32 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
xit("should transition ERROR -> SYNCING after /sync if prev failed",
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
|
||||
});
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
|
||||
});
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["ERROR", "SYNCING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["ERROR", "SYNCING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition SYNCING -> SYNCING on subsequent /sync successes",
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
function(done) {
|
||||
const expectedStates = [];
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["SYNCING", "SYNCING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["SYNCING", "SYNCING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition ERROR -> ERROR if keepalive keeps failing", function(done) {
|
||||
acceptKeepalives = false;
|
||||
@@ -671,7 +710,8 @@ describe("MatrixClient", function() {
|
||||
|
||||
describe("guest rooms", function() {
|
||||
it("should only do /sync calls (without filter/pushrules)", function(done) {
|
||||
httpLookups = []; // no /pushrules or /filter
|
||||
httpLookups = []; // no /pushrules or /filterw
|
||||
httpLookups.push(CAPABILITIES_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "GET",
|
||||
path: "/sync",
|
||||
@@ -702,4 +742,242 @@ describe("MatrixClient", function() {
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendEvent", () => {
|
||||
const roomId = "!room:example.org";
|
||||
const body = "This is the body";
|
||||
const content = { body };
|
||||
|
||||
it("overload without threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: content,
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, EventType.RoomMessage, content, txnId);
|
||||
});
|
||||
|
||||
it("overload with null threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: content,
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, null, EventType.RoomMessage, content, txnId);
|
||||
});
|
||||
|
||||
it("overload with threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: content,
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, "$threadId:server", EventType.RoomMessage, content, txnId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("redactEvent", () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "join",
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
if (eventType === EventType.RoomEncryption) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({ content: {} });
|
||||
} else {
|
||||
throw new Error("Unexpected event type or state key");
|
||||
}
|
||||
},
|
||||
},
|
||||
threads: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
addPendingEvent: jest.fn(),
|
||||
updatePendingEvent: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
client.getRoom = (getRoomId) => {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
return mockRoom;
|
||||
};
|
||||
});
|
||||
|
||||
it("overload without threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
}];
|
||||
|
||||
await client.redactEvent(roomId, eventId, txnId);
|
||||
});
|
||||
|
||||
it("overload with null threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
}];
|
||||
|
||||
await client.redactEvent(roomId, null, eventId, txnId);
|
||||
});
|
||||
|
||||
it("overload with threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
}];
|
||||
|
||||
await client.redactEvent(roomId, "$threadId:server", eventId, txnId);
|
||||
});
|
||||
|
||||
it("does not get wrongly encrypted", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
const reason = "This is the redaction reason";
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${txnId}`,
|
||||
expectBody: { reason }, // NOT ENCRYPTED
|
||||
data: { event_id: eventId },
|
||||
}];
|
||||
|
||||
await client.redactEvent(roomId, eventId, txnId, { reason });
|
||||
});
|
||||
});
|
||||
|
||||
describe("cancelPendingEvent", () => {
|
||||
const roomId = "!room:server";
|
||||
const txnId = "m12345";
|
||||
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "join",
|
||||
updatePendingEvent: (event, status) => event.setStatus(status),
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[RoomCreateTypeField]: RoomType.Space,
|
||||
},
|
||||
});
|
||||
} else if (eventType === EventType.RoomEncryption) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({ content: {} });
|
||||
} else {
|
||||
throw new Error("Unexpected event type or state key");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let event;
|
||||
beforeEach(async () => {
|
||||
event = new MatrixEvent({
|
||||
event_id: "~" + roomId + ":" + txnId,
|
||||
user_id: client.credentials.userId,
|
||||
sender: client.credentials.userId,
|
||||
room_id: roomId,
|
||||
origin_server_ts: new Date().getTime(),
|
||||
});
|
||||
event.setTxnId(txnId);
|
||||
|
||||
client.getRoom = (getRoomId) => {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
return mockRoom;
|
||||
};
|
||||
client.crypto = { // mock crypto
|
||||
encryptEvent: (event, room) => new Promise(() => {}),
|
||||
};
|
||||
});
|
||||
|
||||
function assertCancelled() {
|
||||
expect(event.status).toBe(EventStatus.CANCELLED);
|
||||
expect(client.scheduler.removeEventFromQueue(event)).toBeFalsy();
|
||||
expect(httpLookups.filter(h => h.path.includes("/send/")).length).toBe(0);
|
||||
}
|
||||
|
||||
it("should cancel an event which is queued", () => {
|
||||
event.setStatus(EventStatus.QUEUED);
|
||||
client.scheduler.queueEvent(event);
|
||||
client.cancelPendingEvent(event);
|
||||
assertCancelled();
|
||||
});
|
||||
|
||||
it("should cancel an event which is encrypting", async () => {
|
||||
client.encryptAndSendEvent(null, event);
|
||||
await testUtils.emitPromise(event, "Event.status");
|
||||
client.cancelPendingEvent(event);
|
||||
assertCancelled();
|
||||
});
|
||||
|
||||
it("should cancel an event which is not sent", () => {
|
||||
event.setStatus(EventStatus.NOT_SENT);
|
||||
client.cancelPendingEvent(event);
|
||||
assertCancelled();
|
||||
});
|
||||
|
||||
it("should error when given any other event status", () => {
|
||||
event.setStatus(EventStatus.SENDING);
|
||||
expect(() => client.cancelPendingEvent(event)).toThrow("cannot cancel an event with status sending");
|
||||
expect(event.status).toBe(EventStatus.SENDING);
|
||||
});
|
||||
});
|
||||
|
||||
describe("threads", () => {
|
||||
it("partitions root events to room timeline and thread timeline", () => {
|
||||
const supportsExperimentalThreads = client.supportsExperimentalThreads;
|
||||
client.supportsExperimentalThreads = () => true;
|
||||
|
||||
const rootEvent = new MatrixEvent({
|
||||
"content": {},
|
||||
"origin_server_ts": 1,
|
||||
"room_id": "!room1:matrix.org",
|
||||
"sender": "@alice:matrix.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"m.relations": {
|
||||
"io.element.thread": {
|
||||
"latest_event": {},
|
||||
"count": 33,
|
||||
"current_user_participated": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
"event_id": "$ev1",
|
||||
"user_id": "@alice:matrix.org",
|
||||
});
|
||||
|
||||
expect(rootEvent.isThreadRoot).toBe(true);
|
||||
|
||||
const [room, threads] = client.partitionThreadedEvents([rootEvent]);
|
||||
expect(room).toHaveLength(1);
|
||||
expect(threads).toHaveLength(1);
|
||||
|
||||
// Restore method
|
||||
client.supportsExperimentalThreads = supportsExperimentalThreads;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -14,21 +14,25 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "../../../src";
|
||||
import { IContent, MatrixClient, MatrixEvent } from "../../../src";
|
||||
import { Room } from "../../../src/models/room";
|
||||
import { UNSTABLE_MSC3089_BRANCH } from "../../../src/@types/event";
|
||||
import { IEncryptedFile, RelationType, UNSTABLE_MSC3089_BRANCH } from "../../../src/@types/event";
|
||||
import { EventTimelineSet } from "../../../src/models/event-timeline-set";
|
||||
import { EventTimeline } from "../../../src/models/event-timeline";
|
||||
import { MSC3089Branch } from "../../../src/models/MSC3089Branch";
|
||||
import { MSC3089TreeSpace } from "../../../src/models/MSC3089TreeSpace";
|
||||
|
||||
describe("MSC3089Branch", () => {
|
||||
let client: MatrixClient;
|
||||
// @ts-ignore - TS doesn't know that this is a type
|
||||
let indexEvent: any;
|
||||
let directory: MSC3089TreeSpace;
|
||||
let branch: MSC3089Branch;
|
||||
let branch2: MSC3089Branch;
|
||||
|
||||
const branchRoomId = "!room:example.org";
|
||||
const fileEventId = "$file";
|
||||
const fileEventId2 = "$second_file";
|
||||
|
||||
const staticTimelineSets = {} as EventTimelineSet;
|
||||
const staticRoom = {
|
||||
@@ -50,7 +54,12 @@ describe("MSC3089Branch", () => {
|
||||
getRoomId: () => branchRoomId,
|
||||
getStateKey: () => fileEventId,
|
||||
});
|
||||
branch = new MSC3089Branch(client, indexEvent);
|
||||
directory = new MSC3089TreeSpace(client, branchRoomId);
|
||||
branch = new MSC3089Branch(client, indexEvent, directory);
|
||||
branch2 = new MSC3089Branch(client, {
|
||||
getRoomId: () => branchRoomId,
|
||||
getStateKey: () => fileEventId2,
|
||||
} as MatrixEvent, directory);
|
||||
});
|
||||
|
||||
it('should know the file event ID', () => {
|
||||
@@ -69,13 +78,15 @@ describe("MSC3089Branch", () => {
|
||||
});
|
||||
|
||||
it('should be able to delete the file', async () => {
|
||||
const eventIdOrder = [fileEventId, fileEventId2];
|
||||
|
||||
const stateFn = jest.fn()
|
||||
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
|
||||
expect(roomId).toEqual(branchRoomId);
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
|
||||
expect(content).toMatchObject({});
|
||||
expect(content['active']).toBeUndefined();
|
||||
expect(stateKey).toEqual(fileEventId);
|
||||
expect(stateKey).toEqual(eventIdOrder[stateFn.mock.calls.length - 1]);
|
||||
|
||||
return Promise.resolve(); // return value not used
|
||||
});
|
||||
@@ -83,16 +94,19 @@ describe("MSC3089Branch", () => {
|
||||
|
||||
const redactFn = jest.fn().mockImplementation((roomId: string, eventId: string) => {
|
||||
expect(roomId).toEqual(branchRoomId);
|
||||
expect(eventId).toEqual(fileEventId);
|
||||
expect(eventId).toEqual(eventIdOrder[stateFn.mock.calls.length - 1]);
|
||||
|
||||
return Promise.resolve(); // return value not used
|
||||
});
|
||||
client.redactEvent = redactFn;
|
||||
|
||||
branch.getVersionHistory = () => Promise.resolve([branch, branch2]);
|
||||
branch2.getVersionHistory = () => Promise.resolve([branch2]);
|
||||
|
||||
await branch.delete();
|
||||
|
||||
expect(stateFn).toHaveBeenCalledTimes(1);
|
||||
expect(redactFn).toHaveBeenCalledTimes(1);
|
||||
expect(stateFn).toHaveBeenCalledTimes(2);
|
||||
expect(redactFn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should know its name', async () => {
|
||||
@@ -127,16 +141,71 @@ describe("MSC3089Branch", () => {
|
||||
expect(stateFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should be v1 by default', () => {
|
||||
indexEvent.getContent = () => ({ active: true });
|
||||
|
||||
const res = branch.version;
|
||||
|
||||
expect(res).toEqual(1);
|
||||
});
|
||||
|
||||
it('should be vN when set', () => {
|
||||
indexEvent.getContent = () => ({ active: true, version: 3 });
|
||||
|
||||
const res = branch.version;
|
||||
|
||||
expect(res).toEqual(3);
|
||||
});
|
||||
|
||||
it('should be unlocked by default', async () => {
|
||||
indexEvent.getContent = () => ({ active: true });
|
||||
|
||||
const res = branch.isLocked();
|
||||
|
||||
expect(res).toEqual(false);
|
||||
});
|
||||
|
||||
it('should use lock status from index event', async () => {
|
||||
indexEvent.getContent = () => ({ active: true, locked: true });
|
||||
|
||||
const res = branch.isLocked();
|
||||
|
||||
expect(res).toEqual(true);
|
||||
});
|
||||
|
||||
it('should be able to change its locked status', async () => {
|
||||
const locked = true;
|
||||
indexEvent.getContent = () => ({ active: true, retained: true });
|
||||
const stateFn = jest.fn()
|
||||
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
|
||||
expect(roomId).toEqual(branchRoomId);
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
|
||||
expect(content).toMatchObject({
|
||||
retained: true, // canary for copying state
|
||||
active: true,
|
||||
locked: locked,
|
||||
});
|
||||
expect(stateKey).toEqual(fileEventId);
|
||||
|
||||
return Promise.resolve(); // return value not used
|
||||
});
|
||||
client.sendStateEvent = stateFn;
|
||||
|
||||
await branch.setLocked(locked);
|
||||
|
||||
expect(stateFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should be able to return event information', async () => {
|
||||
const mxcLatter = "example.org/file";
|
||||
const fileContent = { isFile: "not quite", url: "mxc://" + mxcLatter };
|
||||
const eventsArr = [
|
||||
{ getId: () => "$not-file", getContent: () => ({}) },
|
||||
{ getId: () => fileEventId, getContent: () => ({ file: fileContent }) },
|
||||
];
|
||||
client.getEventTimeline = () => Promise.resolve({
|
||||
getEvents: () => eventsArr,
|
||||
}) as any as Promise<EventTimeline>; // partial
|
||||
const fileEvent = { getId: () => fileEventId, getOriginalContent: () => ({ file: fileContent }) };
|
||||
staticRoom.getUnfilteredTimelineSet = () => ({
|
||||
findEventById: (eventId) => {
|
||||
expect(eventId).toEqual(fileEventId);
|
||||
return fileEvent;
|
||||
},
|
||||
}) as EventTimelineSet;
|
||||
client.mxcUrlToHttp = (mxc: string) => {
|
||||
expect(mxc).toEqual("mxc://" + mxcLatter);
|
||||
return `https://example.org/_matrix/media/v1/download/${mxcLatter}`;
|
||||
@@ -151,4 +220,112 @@ describe("MSC3089Branch", () => {
|
||||
httpUrl: expect.stringMatching(`.+${mxcLatter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`),
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to return the event object', async () => {
|
||||
const mxcLatter = "example.org/file";
|
||||
const fileContent = { isFile: "not quite", url: "mxc://" + mxcLatter };
|
||||
const fileEvent = { getId: () => fileEventId, getOriginalContent: () => ({ file: fileContent }) };
|
||||
staticRoom.getUnfilteredTimelineSet = () => ({
|
||||
findEventById: (eventId) => {
|
||||
expect(eventId).toEqual(fileEventId);
|
||||
return fileEvent;
|
||||
},
|
||||
}) as EventTimelineSet;
|
||||
client.mxcUrlToHttp = (mxc: string) => {
|
||||
expect(mxc).toEqual("mxc://" + mxcLatter);
|
||||
return `https://example.org/_matrix/media/v1/download/${mxcLatter}`;
|
||||
};
|
||||
client.decryptEventIfNeeded = () => Promise.resolve();
|
||||
|
||||
const res = await branch.getFileEvent();
|
||||
expect(res).toBeDefined();
|
||||
expect(res).toBe(fileEvent);
|
||||
});
|
||||
|
||||
it('should create new versions of itself', async () => {
|
||||
const canaryName = "canary";
|
||||
const canaryContents = "contents go here";
|
||||
const canaryFile = {} as IEncryptedFile;
|
||||
const canaryAddl = { canary: true };
|
||||
indexEvent.getContent = () => ({ active: true, retained: true });
|
||||
const stateKeyOrder = [fileEventId2, fileEventId];
|
||||
const stateFn = jest.fn()
|
||||
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
|
||||
expect(roomId).toEqual(branchRoomId);
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
|
||||
expect(stateKey).toEqual(stateKeyOrder[stateFn.mock.calls.length - 1]);
|
||||
if (stateKey === fileEventId) {
|
||||
expect(content).toMatchObject({
|
||||
retained: true, // canary for copying state
|
||||
active: false,
|
||||
});
|
||||
} else if (stateKey === fileEventId2) {
|
||||
expect(content).toMatchObject({
|
||||
active: true,
|
||||
version: 2,
|
||||
name: canaryName,
|
||||
});
|
||||
} else {
|
||||
throw new Error("Unexpected state key: " + stateKey);
|
||||
}
|
||||
|
||||
return Promise.resolve(); // return value not used
|
||||
});
|
||||
client.sendStateEvent = stateFn;
|
||||
|
||||
const createFn = jest.fn().mockImplementation((
|
||||
name: string,
|
||||
contents: ArrayBuffer,
|
||||
info: Partial<IEncryptedFile>,
|
||||
addl: IContent,
|
||||
) => {
|
||||
expect(name).toEqual(canaryName);
|
||||
expect(contents).toBe(canaryContents);
|
||||
expect(info).toBe(canaryFile);
|
||||
expect(addl).toMatchObject({
|
||||
...canaryAddl,
|
||||
"m.new_content": true,
|
||||
"m.relates_to": {
|
||||
"rel_type": RelationType.Replace,
|
||||
"event_id": fileEventId,
|
||||
},
|
||||
});
|
||||
|
||||
return Promise.resolve({ event_id: fileEventId2 });
|
||||
});
|
||||
directory.createFile = createFn;
|
||||
|
||||
await branch.createNewVersion(canaryName, canaryContents, canaryFile, canaryAddl);
|
||||
|
||||
expect(stateFn).toHaveBeenCalledTimes(2);
|
||||
expect(createFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should fetch file history', async () => {
|
||||
branch2.getFileEvent = () => Promise.resolve({
|
||||
replacingEventId: () => undefined,
|
||||
getId: () => fileEventId2,
|
||||
} as MatrixEvent);
|
||||
branch.getFileEvent = () => Promise.resolve({
|
||||
replacingEventId: () => fileEventId2,
|
||||
getId: () => fileEventId,
|
||||
} as MatrixEvent);
|
||||
|
||||
const events = [await branch.getFileEvent(), await branch2.getFileEvent(), {
|
||||
replacingEventId: (): string => null,
|
||||
getId: () => "$unknown",
|
||||
}];
|
||||
staticRoom.getLiveTimeline = () => ({ getEvents: () => events }) as EventTimeline;
|
||||
|
||||
directory.getFile = (evId: string) => {
|
||||
expect(evId).toEqual(fileEventId);
|
||||
return branch;
|
||||
};
|
||||
|
||||
const results = await branch2.getVersionHistory();
|
||||
expect(results).toMatchObject([
|
||||
branch2,
|
||||
branch,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
TreePermissions,
|
||||
} from "../../../src/models/MSC3089TreeSpace";
|
||||
import { DEFAULT_ALPHABET } from "../../../src/utils";
|
||||
import { MockBlob } from "../../MockBlob";
|
||||
import { MatrixError } from "../../../src/http-api";
|
||||
|
||||
describe("MSC3089TreeSpace", () => {
|
||||
@@ -227,40 +226,61 @@ describe("MSC3089TreeSpace", () => {
|
||||
[targetUser]: expectedPl,
|
||||
},
|
||||
});
|
||||
|
||||
// Store new power levels so the `getPermissions()` test passes
|
||||
makePowerLevels(content);
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
client.sendStateEvent = fn;
|
||||
await tree.setPermissions(targetUser, role);
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
|
||||
const finalPermissions = tree.getPermissions(targetUser);
|
||||
expect(finalPermissions).toEqual(role);
|
||||
}
|
||||
|
||||
it('should support setting Viewer permissions', () => {
|
||||
return evaluatePowerLevels({
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
users_default: 1024,
|
||||
events_default: 1025,
|
||||
events: {
|
||||
[EventType.RoomPowerLevels]: 1026,
|
||||
},
|
||||
}, TreePermissions.Viewer, 1024);
|
||||
});
|
||||
|
||||
it('should support setting Editor permissions', () => {
|
||||
return evaluatePowerLevels({
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
events_default: 1024,
|
||||
}, TreePermissions.Editor, 1024);
|
||||
users_default: 1024,
|
||||
events_default: 1025,
|
||||
events: {
|
||||
[EventType.RoomPowerLevels]: 1026,
|
||||
},
|
||||
}, TreePermissions.Editor, 1025);
|
||||
});
|
||||
|
||||
it('should support setting Owner permissions', () => {
|
||||
return evaluatePowerLevels({
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
users_default: 1024,
|
||||
events_default: 1025,
|
||||
events: {
|
||||
[EventType.RoomPowerLevels]: 1024,
|
||||
[EventType.RoomPowerLevels]: 1026,
|
||||
},
|
||||
}, TreePermissions.Owner, 1024);
|
||||
}, TreePermissions.Owner, 1026);
|
||||
});
|
||||
|
||||
it('should support demoting permissions', () => {
|
||||
return evaluatePowerLevels({
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
users_default: 1024,
|
||||
events_default: 1025,
|
||||
events: {
|
||||
[EventType.RoomPowerLevels]: 1026,
|
||||
},
|
||||
users: {
|
||||
[targetUser]: 2222,
|
||||
},
|
||||
@@ -270,11 +290,15 @@ describe("MSC3089TreeSpace", () => {
|
||||
it('should support promoting permissions', () => {
|
||||
return evaluatePowerLevels({
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
events_default: 1024,
|
||||
users_default: 1024,
|
||||
events_default: 1025,
|
||||
events: {
|
||||
[EventType.RoomPowerLevels]: 1026,
|
||||
},
|
||||
users: {
|
||||
[targetUser]: 5,
|
||||
},
|
||||
}, TreePermissions.Editor, 1024);
|
||||
}, TreePermissions.Editor, 1025);
|
||||
});
|
||||
|
||||
it('should support defaults: Viewer', () => {
|
||||
@@ -862,12 +886,8 @@ describe("MSC3089TreeSpace", () => {
|
||||
const fileName = "My File.txt";
|
||||
const fileContents = "This is a test file";
|
||||
|
||||
// Mock out Blob for the test environment
|
||||
(<any>global).Blob = MockBlob;
|
||||
|
||||
const uploadFn = jest.fn().mockImplementation((contents: Blob, opts: any) => {
|
||||
expect(contents).toBeInstanceOf(Blob);
|
||||
expect(contents.size).toEqual(fileContents.length);
|
||||
const uploadFn = jest.fn().mockImplementation((contents: Buffer, opts: any) => {
|
||||
expect(contents.length).toEqual(fileContents.length);
|
||||
expect(opts).toMatchObject({
|
||||
includeFilename: false,
|
||||
onlyContentUri: true, // because the tests rely on this - we shouldn't really be testing for this.
|
||||
@@ -883,6 +903,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
body: fileName,
|
||||
url: mxc,
|
||||
file: fileInfo,
|
||||
metadata: true, // additional content from test
|
||||
[UNSTABLE_MSC3089_LEAF.unstable]: {}, // test to ensure we're definitely using unstable
|
||||
});
|
||||
|
||||
@@ -900,14 +921,78 @@ describe("MSC3089TreeSpace", () => {
|
||||
name: fileName,
|
||||
});
|
||||
|
||||
return Promise.resolve(); // return value not used.
|
||||
return Promise.resolve({ event_id: "wrong" }); // return value shouldn't be used
|
||||
});
|
||||
client.sendStateEvent = sendStateFn;
|
||||
|
||||
const buf = Uint8Array.from(Array.from(fileContents).map((_, i) => fileContents.charCodeAt(i)));
|
||||
const buf = Buffer.from(fileContents);
|
||||
|
||||
// We clone the file info just to make sure it doesn't get mutated for the test.
|
||||
await tree.createFile(fileName, buf, Object.assign({}, fileInfo));
|
||||
const result = await tree.createFile(fileName, buf, Object.assign({}, fileInfo), { metadata: true });
|
||||
expect(result).toMatchObject({ event_id: fileEventId });
|
||||
|
||||
expect(uploadFn).toHaveBeenCalledTimes(1);
|
||||
expect(sendMsgFn).toHaveBeenCalledTimes(1);
|
||||
expect(sendStateFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should upload file versions', async () => {
|
||||
const mxc = "mxc://example.org/file";
|
||||
const fileInfo = {
|
||||
mimetype: "text/plain",
|
||||
// other fields as required by encryption, but ignored here
|
||||
};
|
||||
const fileEventId = "$file";
|
||||
const fileName = "My File.txt";
|
||||
const fileContents = "This is a test file";
|
||||
|
||||
const uploadFn = jest.fn().mockImplementation((contents: Buffer, opts: any) => {
|
||||
expect(contents.length).toEqual(fileContents.length);
|
||||
expect(opts).toMatchObject({
|
||||
includeFilename: false,
|
||||
onlyContentUri: true, // because the tests rely on this - we shouldn't really be testing for this.
|
||||
});
|
||||
return Promise.resolve(mxc);
|
||||
});
|
||||
client.uploadContent = uploadFn;
|
||||
|
||||
const sendMsgFn = jest.fn().mockImplementation((roomId: string, contents: any) => {
|
||||
expect(roomId).toEqual(tree.roomId);
|
||||
const content = {
|
||||
msgtype: MsgType.File,
|
||||
body: fileName,
|
||||
url: mxc,
|
||||
file: fileInfo,
|
||||
};
|
||||
expect(contents).toMatchObject({
|
||||
...content,
|
||||
"m.new_content": content,
|
||||
[UNSTABLE_MSC3089_LEAF.unstable]: {}, // test to ensure we're definitely using unstable
|
||||
});
|
||||
|
||||
return Promise.resolve({ event_id: fileEventId }); // eslint-disable-line camelcase
|
||||
});
|
||||
client.sendMessage = sendMsgFn;
|
||||
|
||||
const sendStateFn = jest.fn()
|
||||
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
|
||||
expect(roomId).toEqual(tree.roomId);
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
|
||||
expect(stateKey).toEqual(fileEventId);
|
||||
expect(content).toMatchObject({
|
||||
active: true,
|
||||
name: fileName,
|
||||
});
|
||||
|
||||
return Promise.resolve({ event_id: "wrong" }); // return value shouldn't be used
|
||||
});
|
||||
client.sendStateEvent = sendStateFn;
|
||||
|
||||
const buf = Buffer.from(fileContents);
|
||||
|
||||
// We clone the file info just to make sure it doesn't get mutated for the test.
|
||||
const result = await tree.createFile(fileName, buf, Object.assign({}, fileInfo), { "m.new_content": true });
|
||||
expect(result).toMatchObject({ event_id: fileEventId });
|
||||
|
||||
expect(uploadFn).toHaveBeenCalledTimes(1);
|
||||
expect(sendMsgFn).toHaveBeenCalledTimes(1);
|
||||
@@ -933,7 +1018,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
it('should return falsy for unknown files', () => {
|
||||
const fileEventId = "$file";
|
||||
room.currentState = {
|
||||
getStateEvents: (eventType: string, stateKey?: string) => {
|
||||
getStateEvents: (eventType: string, stateKey?: string): MatrixEvent[] | MatrixEvent | null => {
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
|
||||
expect(stateKey).toEqual(fileEventId);
|
||||
return null;
|
||||
@@ -960,4 +1045,22 @@ describe("MSC3089TreeSpace", () => {
|
||||
expect(files.length).toEqual(1);
|
||||
expect(files[0].indexEvent).toBe(firstFile);
|
||||
});
|
||||
|
||||
it('should list all files', () => {
|
||||
const firstFile = { getContent: () => ({ active: true }) };
|
||||
const secondFile = { getContent: () => ({ active: false }) }; // deliberately inactive
|
||||
room.currentState = {
|
||||
getStateEvents: (eventType: string, stateKey?: string) => {
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
|
||||
expect(stateKey).toBeUndefined();
|
||||
return [firstFile, secondFile];
|
||||
},
|
||||
};
|
||||
|
||||
const files = tree.listAllFiles();
|
||||
expect(files).toBeDefined();
|
||||
expect(files.length).toEqual(2);
|
||||
expect(files[0].indexEvent).toBe(firstFile);
|
||||
expect(files[1].indexEvent).toBe(secondFile);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -302,4 +302,20 @@ describe('NotificationService', function() {
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(false);
|
||||
});
|
||||
|
||||
it("a rule with no conditions matches every event.", function() {
|
||||
expect(pushProcessor.ruleMatchesEvent({
|
||||
rule_id: "rule1",
|
||||
actions: [],
|
||||
conditions: [],
|
||||
default: false,
|
||||
enabled: true,
|
||||
}, testEvent)).toBe(true);
|
||||
expect(pushProcessor.ruleMatchesEvent({
|
||||
rule_id: "rule1",
|
||||
actions: [],
|
||||
default: false,
|
||||
enabled: true,
|
||||
}, testEvent)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventTimelineSet } from "../../src/models/event-timeline-set";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { Relations } from "../../src/models/relations";
|
||||
|
||||
@@ -103,7 +103,7 @@ describe("Relations", function() {
|
||||
// Add the target event first, then the relation event
|
||||
{
|
||||
const relationsCreated = new Promise(resolve => {
|
||||
targetEvent.once("Event.relationsCreated", resolve);
|
||||
targetEvent.once(MatrixEventEvent.RelationsCreated, resolve);
|
||||
});
|
||||
|
||||
const timelineSet = new EventTimelineSet(room, {
|
||||
@@ -118,7 +118,7 @@ describe("Relations", function() {
|
||||
// Add the relation event first, then the target event
|
||||
{
|
||||
const relationsCreated = new Promise(resolve => {
|
||||
targetEvent.once("Event.relationsCreated", resolve);
|
||||
targetEvent.once(MatrixEventEvent.RelationsCreated, resolve);
|
||||
});
|
||||
|
||||
const timelineSet = new EventTimelineSet(room, {
|
||||
|
||||
@@ -120,7 +120,7 @@ describe("RoomState", function() {
|
||||
it("should return a single MatrixEvent if a state_key was specified",
|
||||
function() {
|
||||
const event = state.getStateEvents("m.room.member", userA);
|
||||
expect(event.getContent()).toEqual({
|
||||
expect(event.getContent()).toMatchObject({
|
||||
membership: "join",
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+15
-66
@@ -26,6 +26,15 @@ describe("utils", function() {
|
||||
"foo=bar&baz=beer%40",
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle boolean and numeric values", function() {
|
||||
const params = {
|
||||
string: "foobar",
|
||||
number: 12345,
|
||||
boolean: false,
|
||||
};
|
||||
expect(utils.encodeParams(params)).toEqual("string=foobar&number=12345&boolean=false");
|
||||
});
|
||||
});
|
||||
|
||||
describe("encodeUri", function() {
|
||||
@@ -111,10 +120,10 @@ describe("utils", function() {
|
||||
|
||||
describe("deepCompare", function() {
|
||||
const assert = {
|
||||
isTrue: function(x) {
|
||||
isTrue: function(x: any) {
|
||||
expect(x).toBe(true);
|
||||
},
|
||||
isFalse: function(x) {
|
||||
isFalse: function(x: any) {
|
||||
expect(x).toBe(false);
|
||||
},
|
||||
};
|
||||
@@ -176,10 +185,10 @@ describe("utils", function() {
|
||||
// no two different function is equal really, they capture their
|
||||
// context variables so even if they have same toString(), they
|
||||
// won't have same functionality
|
||||
const func = function(x) {
|
||||
const func = function() {
|
||||
return true;
|
||||
};
|
||||
const func2 = function(x) {
|
||||
const func2 = function() {
|
||||
return true;
|
||||
};
|
||||
assert.isTrue(utils.deepCompare(func, func));
|
||||
@@ -189,66 +198,6 @@ describe("utils", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("extend", function() {
|
||||
const SOURCE = { "prop2": 1, "string2": "x", "newprop": "new" };
|
||||
|
||||
it("should extend", function() {
|
||||
const target = {
|
||||
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
|
||||
};
|
||||
const merged = {
|
||||
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
|
||||
"newprop": "new",
|
||||
};
|
||||
const sourceOrig = JSON.stringify(SOURCE);
|
||||
|
||||
utils.extend(target, SOURCE);
|
||||
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
|
||||
|
||||
// check the originial wasn't modified
|
||||
expect(JSON.stringify(SOURCE)).toEqual(sourceOrig);
|
||||
});
|
||||
|
||||
it("should ignore null", function() {
|
||||
const target = {
|
||||
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
|
||||
};
|
||||
const merged = {
|
||||
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
|
||||
"newprop": "new",
|
||||
};
|
||||
const sourceOrig = JSON.stringify(SOURCE);
|
||||
|
||||
utils.extend(target, null, SOURCE);
|
||||
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
|
||||
|
||||
// check the originial wasn't modified
|
||||
expect(JSON.stringify(SOURCE)).toEqual(sourceOrig);
|
||||
});
|
||||
|
||||
it("should handle properties created with defineProperties", function() {
|
||||
const source = Object.defineProperties({}, {
|
||||
"enumerableProp": {
|
||||
get: function() {
|
||||
return true;
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
"nonenumerableProp": {
|
||||
get: function() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: Fix type
|
||||
const target: any = {};
|
||||
utils.extend(target, source);
|
||||
expect(target.enumerableProp).toBe(true);
|
||||
expect(target.nonenumerableProp).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("chunkPromises", function() {
|
||||
it("should execute promises in chunks", async function() {
|
||||
let promiseCount = 0;
|
||||
@@ -273,7 +222,7 @@ describe("utils", function() {
|
||||
it('should retry', async () => {
|
||||
let count = 0;
|
||||
const val = {};
|
||||
const fn = (attempt) => {
|
||||
const fn = (attempt: any) => {
|
||||
count++;
|
||||
|
||||
// If this expectation fails then it can appear as a Jest Timeout due to
|
||||
@@ -480,7 +429,7 @@ describe("utils", function() {
|
||||
},
|
||||
[72]: "test",
|
||||
};
|
||||
const output = [
|
||||
const output: any = [
|
||||
["72", "test"],
|
||||
["a", 42],
|
||||
["b", [
|
||||
|
||||
+154
-12
@@ -17,6 +17,7 @@ limitations under the License.
|
||||
import { TestClient } from '../../TestClient';
|
||||
import { MatrixCall, CallErrorCode, CallEvent } from '../../../src/webrtc/call';
|
||||
import { SDPStreamMetadataKey, SDPStreamMetadataPurpose } from '../../../src/webrtc/callEventTypes';
|
||||
import { RoomMember } from "../../../src";
|
||||
|
||||
const DUMMY_SDP = (
|
||||
"v=0\r\n" +
|
||||
@@ -81,6 +82,51 @@ class MockRTCPeerConnection {
|
||||
}
|
||||
close() {}
|
||||
getStats() { return []; }
|
||||
addTrack(track: MockMediaStreamTrack) {return new MockRTCRtpSender(track);}
|
||||
}
|
||||
|
||||
class MockRTCRtpSender {
|
||||
constructor(public track: MockMediaStreamTrack) {}
|
||||
|
||||
replaceTrack(track: MockMediaStreamTrack) {this.track = track;}
|
||||
}
|
||||
|
||||
class MockMediaStreamTrack {
|
||||
constructor(public readonly id: string, public readonly kind: "audio" | "video", public enabled = true) {}
|
||||
|
||||
stop() {}
|
||||
}
|
||||
|
||||
class MockMediaStream {
|
||||
constructor(
|
||||
public id: string,
|
||||
private tracks: MockMediaStreamTrack[] = [],
|
||||
) {}
|
||||
|
||||
getTracks() { return this.tracks; }
|
||||
getAudioTracks() { return this.tracks.filter((track) => track.kind === "audio"); }
|
||||
getVideoTracks() { return this.tracks.filter((track) => track.kind === "video"); }
|
||||
addEventListener() {}
|
||||
removeEventListener() { }
|
||||
addTrack(track: MockMediaStreamTrack) {this.tracks.push(track);}
|
||||
removeTrack(track: MockMediaStreamTrack) {this.tracks.splice(this.tracks.indexOf(track), 1);}
|
||||
}
|
||||
|
||||
class MockMediaDeviceInfo {
|
||||
constructor(
|
||||
public kind: "audio" | "video",
|
||||
) {}
|
||||
}
|
||||
|
||||
class MockMediaHandler {
|
||||
getUserMediaStream(audio: boolean, video: boolean) {
|
||||
const tracks = [];
|
||||
if (audio) tracks.push(new MockMediaStreamTrack("audio_track", "audio"));
|
||||
if (video) tracks.push(new MockMediaStreamTrack("video_track", "video"));
|
||||
|
||||
return new MockMediaStream("mock_stream_from_media_handler", tracks);
|
||||
}
|
||||
stopUserMediaStream() {}
|
||||
}
|
||||
|
||||
describe('Call', function() {
|
||||
@@ -98,13 +144,9 @@ describe('Call', function() {
|
||||
global.navigator = {
|
||||
mediaDevices: {
|
||||
// @ts-ignore Mock
|
||||
getUserMedia: () => {
|
||||
return {
|
||||
getTracks: () => [],
|
||||
getAudioTracks: () => [],
|
||||
getVideoTracks: () => [],
|
||||
};
|
||||
},
|
||||
getUserMedia: () => new MockMediaStream("local_stream"),
|
||||
// @ts-ignore Mock
|
||||
enumerateDevices: async () => [new MockMediaDeviceInfo("audio"), new MockMediaDeviceInfo("video")],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -115,7 +157,7 @@ describe('Call', function() {
|
||||
RTCSessionDescription: {},
|
||||
// @ts-ignore Mock
|
||||
RTCIceCandidate: {},
|
||||
getUserMedia: {},
|
||||
getUserMedia: () => new MockMediaStream("local_stream"),
|
||||
};
|
||||
// @ts-ignore Mock
|
||||
global.document = {};
|
||||
@@ -124,6 +166,8 @@ describe('Call', function() {
|
||||
// We just stub out sendEvent: we're not interested in testing the client's
|
||||
// event sending code here
|
||||
client.client.sendEvent = () => {};
|
||||
client.client.mediaHandler = new MockMediaHandler;
|
||||
client.client.getMediaHandler = () => client.client.mediaHandler;
|
||||
client.httpBackend.when("GET", "/voip/turnServer").respond(200, {});
|
||||
call = new MatrixCall({
|
||||
client: client.client,
|
||||
@@ -319,7 +363,7 @@ describe('Call', function() {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
[SDPStreamMetadataKey]: {
|
||||
"stream_id": {
|
||||
"remote_stream": {
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
audio_muted: true,
|
||||
video_muted: false,
|
||||
@@ -329,8 +373,16 @@ describe('Call', function() {
|
||||
},
|
||||
});
|
||||
|
||||
call.pushRemoteFeed({ id: "stream_id", getAudioTracks: () => ["track1"], getVideoTracks: () => ["track1"] });
|
||||
const feed = call.getFeeds().find((feed) => feed.stream.id === "stream_id");
|
||||
call.pushRemoteFeed(
|
||||
new MockMediaStream(
|
||||
"remote_stream",
|
||||
[
|
||||
new MockMediaStreamTrack("remote_audio_track", "audio"),
|
||||
new MockMediaStreamTrack("remote_video_track", "video"),
|
||||
],
|
||||
),
|
||||
);
|
||||
const feed = call.getFeeds().find((feed) => feed.stream.id === "remote_stream");
|
||||
expect(feed?.purpose).toBe(SDPStreamMetadataPurpose.Usermedia);
|
||||
expect(feed?.isAudioMuted()).toBeTruthy();
|
||||
expect(feed?.isVideoMuted()).not.toBeTruthy();
|
||||
@@ -342,7 +394,7 @@ describe('Call', function() {
|
||||
await callPromise;
|
||||
|
||||
call.getOpponentMember = () => {
|
||||
return { userId: "@bob:bar.uk" };
|
||||
return { userId: "@bob:bar.uk" } as RoomMember;
|
||||
};
|
||||
|
||||
await call.onAnswerReceived({
|
||||
@@ -363,4 +415,94 @@ describe('Call', function() {
|
||||
call.setScreensharingEnabled(true);
|
||||
expect(call.setScreensharingEnabledWithoutMetadataSupport).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should fallback to answering with no video", async () => {
|
||||
await client.httpBackend.flush();
|
||||
|
||||
call.shouldAnswerWithMediaType = (wantedValue: boolean) => wantedValue;
|
||||
client.client.mediaHandler.getUserMediaStream = jest.fn().mockRejectedValue("reject");
|
||||
|
||||
await call.answer(true, true);
|
||||
|
||||
expect(client.client.mediaHandler.getUserMediaStream).toHaveBeenNthCalledWith(1, true, true);
|
||||
expect(client.client.mediaHandler.getUserMediaStream).toHaveBeenNthCalledWith(2, true, false);
|
||||
});
|
||||
|
||||
it("should handle mid-call device changes", async () => {
|
||||
client.client.mediaHandler.getUserMediaStream = jest.fn().mockReturnValue(
|
||||
new MockMediaStream(
|
||||
"stream", [
|
||||
new MockMediaStreamTrack("audio_track", "audio"),
|
||||
new MockMediaStreamTrack("video_track", "video"),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
const callPromise = call.placeVideoCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'party_id',
|
||||
answer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
await call.updateLocalUsermediaStream(
|
||||
new MockMediaStream(
|
||||
"replacement_stream",
|
||||
[
|
||||
new MockMediaStreamTrack("new_audio_track", "audio"),
|
||||
new MockMediaStreamTrack("video_track", "video"),
|
||||
],
|
||||
),
|
||||
);
|
||||
expect(call.localUsermediaStream.id).toBe("stream");
|
||||
expect(call.localUsermediaStream.getAudioTracks()[0].id).toBe("new_audio_track");
|
||||
expect(call.localUsermediaStream.getVideoTracks()[0].id).toBe("video_track");
|
||||
expect(call.usermediaSenders.find((sender) => {
|
||||
return sender?.track?.kind === "audio";
|
||||
}).track.id).toBe("new_audio_track");
|
||||
expect(call.usermediaSenders.find((sender) => {
|
||||
return sender?.track?.kind === "video";
|
||||
}).track.id).toBe("video_track");
|
||||
});
|
||||
|
||||
it("should handle upgrade to video call", async () => {
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'party_id',
|
||||
answer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
[SDPStreamMetadataKey]: {},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
await call.upgradeCall(false, true);
|
||||
|
||||
expect(call.localUsermediaStream.getAudioTracks()[0].id).toBe("audio_track");
|
||||
expect(call.localUsermediaStream.getVideoTracks()[0].id).toBe("video_track");
|
||||
expect(call.usermediaSenders.find((sender) => {
|
||||
return sender?.track?.kind === "audio";
|
||||
}).track.id).toBe("audio_track");
|
||||
expect(call.usermediaSenders.find((sender) => {
|
||||
return sender?.track?.kind === "video";
|
||||
}).track.id).toBe("video_track");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -90,8 +90,9 @@ export interface ISenderNotificationPermissionCondition
|
||||
key: string;
|
||||
}
|
||||
|
||||
export type PushRuleCondition = IPushRuleCondition<string>
|
||||
| IEventMatchCondition
|
||||
// XXX: custom conditions are possible but always fail, and break the typescript discriminated union so ignore them here
|
||||
// IPushRuleCondition<Exclude<string, ConditionKind>> unfortunately does not resolve this at the time of writing.
|
||||
export type PushRuleCondition = IEventMatchCondition
|
||||
| IContainsDisplayNameCondition
|
||||
| IRoomMemberCountCondition
|
||||
| ISenderNotificationPermissionCondition;
|
||||
@@ -146,12 +147,12 @@ export interface IPusher {
|
||||
app_display_name: string;
|
||||
app_id: string;
|
||||
data: {
|
||||
format?: string; // TODO: Types
|
||||
format?: string;
|
||||
url?: string; // TODO: Required if kind==http
|
||||
brand?: string; // TODO: For email notifications only?
|
||||
brand?: string; // TODO: For email notifications only? Unspecced field
|
||||
};
|
||||
device_display_name: string;
|
||||
kind: string; // TODO: Types
|
||||
kind: "http" | string;
|
||||
lang: string;
|
||||
profile_tag?: string;
|
||||
pushkey: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,14 +14,16 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export class MockBlob {
|
||||
private contents: number[] = [];
|
||||
// disable lint because these are wire responses
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
public constructor(private parts: ArrayLike<number>[]) {
|
||||
parts.forEach(p => Array.from(p).forEach(e => this.contents.push(e)));
|
||||
}
|
||||
|
||||
public get size(): number {
|
||||
return this.contents.length;
|
||||
}
|
||||
/**
|
||||
* Represents a response to the CSAPI `/refresh` endpoint.
|
||||
*/
|
||||
export interface IRefreshTokenResponse {
|
||||
access_token: string;
|
||||
expires_in_ms: number;
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
/* eslint-enable camelcase */
|
||||
@@ -92,6 +92,15 @@ export enum EventType {
|
||||
export enum RelationType {
|
||||
Annotation = "m.annotation",
|
||||
Replace = "m.replace",
|
||||
Reference = "m.reference",
|
||||
/**
|
||||
* Note, "io.element.thread" is hardcoded
|
||||
* Should be replaced with "m.thread" once MSC3440 lands
|
||||
* Can not use `UnstableValue` as TypeScript does not
|
||||
* allow computed values in enums
|
||||
* https://github.com/microsoft/TypeScript/issues/27976
|
||||
*/
|
||||
Thread = "io.element.thread",
|
||||
}
|
||||
|
||||
export enum MsgType {
|
||||
@@ -103,6 +112,7 @@ export enum MsgType {
|
||||
Audio = "m.audio",
|
||||
Location = "m.location",
|
||||
Video = "m.video",
|
||||
KeyVerificationRequest = "m.key.verification.request",
|
||||
}
|
||||
|
||||
export const RoomCreateTypeField = "type";
|
||||
@@ -168,6 +178,16 @@ export const UNSTABLE_ELEMENT_FUNCTIONAL_USERS = new UnstableValue(
|
||||
"io.element.functional_members",
|
||||
"io.element.functional_members");
|
||||
|
||||
/**
|
||||
* A type of message that affects visibility of a message,
|
||||
* as per https://github.com/matrix-org/matrix-doc/pull/3531
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export const EVENT_VISIBILITY_CHANGE_TYPE = new UnstableValue(
|
||||
"m.visibility",
|
||||
"org.matrix.msc3531.visibility");
|
||||
|
||||
export interface IEncryptedFile {
|
||||
url: string;
|
||||
mimetype?: string;
|
||||
|
||||
@@ -14,13 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// TODO: Merge this with sync.js once converted
|
||||
// Types for MSC1767: Extensible events in Matrix
|
||||
|
||||
export enum SyncState {
|
||||
Error = "ERROR",
|
||||
Prepared = "PREPARED",
|
||||
Stopped = "STOPPED",
|
||||
Syncing = "SYNCING",
|
||||
Catchup = "CATCHUP",
|
||||
Reconnecting = "RECONNECTING",
|
||||
}
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
|
||||
export const TEXT_NODE_TYPE = new UnstableValue("m.text", "org.matrix.msc1767.text");
|
||||
Vendored
+8
-26
@@ -33,11 +33,7 @@ declare global {
|
||||
}
|
||||
|
||||
interface Window {
|
||||
electron?: Electron;
|
||||
}
|
||||
|
||||
interface Electron {
|
||||
getDesktopCapturerSources(options: GetSourcesOptions): Promise<Array<DesktopCapturerSource>>;
|
||||
webkitAudioContext: typeof AudioContext;
|
||||
}
|
||||
|
||||
interface Crypto {
|
||||
@@ -66,27 +62,6 @@ declare global {
|
||||
};
|
||||
}
|
||||
|
||||
interface DesktopCapturerSource {
|
||||
id: string;
|
||||
name: string;
|
||||
thumbnailURL: string;
|
||||
}
|
||||
|
||||
interface GetSourcesOptions {
|
||||
types: Array<string>;
|
||||
thumbnailSize?: {
|
||||
height: number;
|
||||
width: number;
|
||||
};
|
||||
fetchWindowIcons?: boolean;
|
||||
}
|
||||
|
||||
interface HTMLAudioElement {
|
||||
// sinkId & setSinkId are experimental and typescript doesn't know about them
|
||||
sinkId: string;
|
||||
setSinkId(outputId: string);
|
||||
}
|
||||
|
||||
interface DummyInterfaceWeShouldntBeUsingThis {}
|
||||
|
||||
interface Navigator {
|
||||
@@ -107,4 +82,11 @@ declare global {
|
||||
interface PromiseConstructor {
|
||||
allSettled<T>(promises: Promise<T>[]): Promise<Array<ISettledFulfilled<T> | ISettledRejected>>;
|
||||
}
|
||||
|
||||
interface RTCRtpTransceiver {
|
||||
// This has been removed from TS
|
||||
// (https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1029),
|
||||
// but we still need this for MatrixCall::getRidOfRTXCodecs()
|
||||
setCodecPreferences(codecs: RTCRtpCodecCapability[]): void;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Types for MSC3488 - m.location: Extending events with location data
|
||||
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
import { IContent } from "../models/event";
|
||||
import { TEXT_NODE_TYPE } from "./extensible_events";
|
||||
|
||||
export const LOCATION_EVENT_TYPE = new UnstableValue(
|
||||
"m.location", "org.matrix.msc3488.location");
|
||||
|
||||
export const ASSET_NODE_TYPE = new UnstableValue("m.asset", "org.matrix.msc3488.asset");
|
||||
|
||||
export const TIMESTAMP_NODE_TYPE = new UnstableValue("m.ts", "org.matrix.msc3488.ts");
|
||||
|
||||
export enum LocationAssetType {
|
||||
Self = "m.self",
|
||||
Pin = "m.pin",
|
||||
}
|
||||
|
||||
/* From the spec at:
|
||||
* https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md
|
||||
{
|
||||
"type": "m.room.message",
|
||||
"content": {
|
||||
"body": "Matthew was at geo:51.5008,0.1247;u=35 as of Sat Nov 13 18:50:58 2021",
|
||||
"msgtype": "m.location",
|
||||
"geo_uri": "geo:51.5008,0.1247;u=35",
|
||||
"m.location": {
|
||||
"uri": "geo:51.5008,0.1247;u=35",
|
||||
"description": "Matthew's whereabouts",
|
||||
},
|
||||
"m.asset": {
|
||||
"type": "m.self"
|
||||
},
|
||||
"m.text": "Matthew was at geo:51.5008,0.1247;u=35 as of Sat Nov 13 18:50:58 2021",
|
||||
"m.ts": 1636829458432,
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface ILocationContent extends IContent {
|
||||
body: string;
|
||||
msgtype: string;
|
||||
geo_uri: string;
|
||||
[LOCATION_EVENT_TYPE.name]: {
|
||||
uri: string;
|
||||
description?: string;
|
||||
};
|
||||
[ASSET_NODE_TYPE.name]: {
|
||||
type: LocationAssetType;
|
||||
};
|
||||
[TEXT_NODE_TYPE.name]: string;
|
||||
[TIMESTAMP_NODE_TYPE.name]: number;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
+18
-9
@@ -55,12 +55,20 @@ export enum JoinRule {
|
||||
* @deprecated Reserved keyword. Should not be used. Not yet implemented.
|
||||
*/
|
||||
Private = "private",
|
||||
Knock = "knock", // MSC2403 - only valid inside experimental room versions at this time.
|
||||
Restricted = "restricted", // MSC3083 - only valid inside experimental room versions at this time.
|
||||
Knock = "knock",
|
||||
Restricted = "restricted",
|
||||
}
|
||||
|
||||
export enum RestrictedAllowType {
|
||||
RoomMembership = "m.room_membership", // MSC3083 - only valid inside experimental room versions at this time.
|
||||
RoomMembership = "m.room_membership",
|
||||
}
|
||||
|
||||
export interface IJoinRuleEventContent {
|
||||
join_rule: JoinRule; // eslint-disable-line camelcase
|
||||
allow?: {
|
||||
type: RestrictedAllowType;
|
||||
room_id: string; // eslint-disable-line camelcase
|
||||
}[];
|
||||
}
|
||||
|
||||
export enum GuestAccess {
|
||||
@@ -75,10 +83,11 @@ export enum HistoryVisibility {
|
||||
WorldReadable = "world_readable",
|
||||
}
|
||||
|
||||
// XXX move to OlmDevice when converted
|
||||
export interface InboundGroupSessionData {
|
||||
room_id: string; // eslint-disable-line camelcase
|
||||
session: string;
|
||||
keysClaimed: Record<string, string>;
|
||||
forwardingCurve25519KeyChain: string[];
|
||||
export interface IUsageLimit {
|
||||
// "hs_disabled" is NOT a specced string, but is used in Synapse
|
||||
// This is tracked over at https://github.com/matrix-org/synapse/issues/9237
|
||||
// eslint-disable-next-line camelcase
|
||||
limit_type: "monthly_active_user" | "hs_disabled" | string;
|
||||
// eslint-disable-next-line camelcase
|
||||
admin_contact?: string;
|
||||
}
|
||||
|
||||
+17
-1
@@ -15,10 +15,11 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { Callback } from "../client";
|
||||
import { IContent } from "../models/event";
|
||||
import { IContent, IEvent } from "../models/event";
|
||||
import { Preset, Visibility } from "./partials";
|
||||
import { SearchKey } from "./search";
|
||||
import { IRoomEventFilter } from "../filter";
|
||||
import { Direction } from "../models/event-timeline";
|
||||
|
||||
// allow camelcase as these are things that go onto the wire
|
||||
/* eslint-disable camelcase */
|
||||
@@ -139,4 +140,19 @@ export interface IBindThreePidBody {
|
||||
id_access_token: string;
|
||||
sid: string;
|
||||
}
|
||||
|
||||
export interface IRelationsRequestOpts {
|
||||
from?: string;
|
||||
to?: string;
|
||||
limit?: number;
|
||||
direction?: Direction;
|
||||
}
|
||||
|
||||
export interface IRelationsResponse {
|
||||
original_event: IEvent;
|
||||
chunk: IEvent[];
|
||||
next_batch?: string;
|
||||
prev_batch?: string;
|
||||
}
|
||||
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
+8
-11
@@ -15,26 +15,23 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { IPublicRoomsChunkRoom } from "../client";
|
||||
import { RoomType } from "./event";
|
||||
import { IStrippedState } from "../sync-accumulator";
|
||||
|
||||
// Types relating to Rooms of type `m.space` and related APIs
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface ISpaceSummaryRoom extends IPublicRoomsChunkRoom {
|
||||
num_refs: number;
|
||||
room_type: string;
|
||||
}
|
||||
|
||||
export interface ISpaceSummaryEvent {
|
||||
room_id: string;
|
||||
event_id: string;
|
||||
export interface IHierarchyRelation extends IStrippedState {
|
||||
origin_server_ts: number;
|
||||
type: string;
|
||||
state_key: string;
|
||||
content: {
|
||||
order?: string;
|
||||
suggested?: boolean;
|
||||
auto_join?: boolean;
|
||||
via?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface IHierarchyRoom extends IPublicRoomsChunkRoom {
|
||||
room_type?: RoomType | string;
|
||||
children_state: IHierarchyRelation[];
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
@@ -25,4 +25,5 @@ export interface IThreepid {
|
||||
address: string;
|
||||
validated_at: number; // eslint-disable-line camelcase
|
||||
added_at: number; // eslint-disable-line camelcase
|
||||
bound?: boolean;
|
||||
}
|
||||
|
||||
+22
-7
@@ -16,21 +16,20 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { ListenerMap, TypedEventEmitter } from "./models/typed-event-emitter";
|
||||
|
||||
export class ReEmitter {
|
||||
private target: EventEmitter;
|
||||
constructor(private readonly target: EventEmitter) {}
|
||||
|
||||
constructor(target: EventEmitter) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
reEmit(source: EventEmitter, eventNames: string[]) {
|
||||
public reEmit(source: EventEmitter, eventNames: string[]): void {
|
||||
for (const eventName of eventNames) {
|
||||
// We include the source as the last argument for event handlers which may need it,
|
||||
// such as read receipt listeners on the client class which won't have the context
|
||||
// of the room.
|
||||
const forSource = (...args) => {
|
||||
const forSource = (...args: any[]) => {
|
||||
// EventEmitter special cases 'error' to make the emit function throw if no
|
||||
// handler is attached, which sort of makes sense for making sure that something
|
||||
// handles an error, but for re-emitting, there could be a listener on the original
|
||||
@@ -48,3 +47,19 @@ export class ReEmitter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class TypedReEmitter<
|
||||
Events extends string,
|
||||
Arguments extends ListenerMap<Events>,
|
||||
> extends ReEmitter {
|
||||
constructor(target: TypedEventEmitter<Events, Arguments>) {
|
||||
super(target);
|
||||
}
|
||||
|
||||
public reEmit<ReEmittedEvents extends string, T extends Events & ReEmittedEvents>(
|
||||
source: TypedEventEmitter<ReEmittedEvents, any>,
|
||||
eventNames: T[],
|
||||
): void {
|
||||
super.reEmit(source, eventNames);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,79 +17,20 @@ limitations under the License.
|
||||
|
||||
/** @module auto-discovery */
|
||||
|
||||
import { logger } from './logger';
|
||||
import { URL as NodeURL } from "url";
|
||||
|
||||
import { IClientWellKnown, IWellKnownConfig } from "./client";
|
||||
import { logger } from './logger';
|
||||
|
||||
// Dev note: Auto discovery is part of the spec.
|
||||
// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
|
||||
|
||||
/**
|
||||
* Description for what an automatically discovered client configuration
|
||||
* would look like. Although this is a class, it is recommended that it
|
||||
* be treated as an interface definition rather than as a class.
|
||||
*
|
||||
* Additional properties than those defined here may be present, and
|
||||
* should follow the Java package naming convention.
|
||||
*/
|
||||
class DiscoveredClientConfig { // eslint-disable-line no-unused-vars
|
||||
// Dev note: this is basically a copy/paste of the .well-known response
|
||||
// object as defined in the spec. It does have additional information,
|
||||
// however. Overall, this exists to serve as a place for documentation
|
||||
// and not functionality.
|
||||
// See https://matrix.org/docs/spec/client_server/r0.4.0.html#get-well-known-matrix-client
|
||||
|
||||
constructor() {
|
||||
/**
|
||||
* The homeserver configuration the client should use. This will
|
||||
* always be present on the object.
|
||||
* @type {{state: string, base_url: string}} The configuration.
|
||||
*/
|
||||
this["m.homeserver"] = {
|
||||
/**
|
||||
* The lookup result state. If this is anything other than
|
||||
* AutoDiscovery.SUCCESS then base_url may be falsey. Additionally,
|
||||
* if this is not AutoDiscovery.SUCCESS then the client should
|
||||
* assume the other properties in the client config (such as
|
||||
* the identity server configuration) are not valid.
|
||||
*/
|
||||
state: AutoDiscovery.PROMPT,
|
||||
|
||||
/**
|
||||
* If the state is AutoDiscovery.FAIL_ERROR or .FAIL_PROMPT
|
||||
* then this will contain a human-readable (English) message
|
||||
* for what went wrong. If the state is none of those previously
|
||||
* mentioned, this will be falsey.
|
||||
*/
|
||||
error: "Something went wrong",
|
||||
|
||||
/**
|
||||
* The base URL clients should use to talk to the homeserver,
|
||||
* particularly for the login process. May be falsey if the
|
||||
* state is not AutoDiscovery.SUCCESS.
|
||||
*/
|
||||
base_url: "https://matrix.org",
|
||||
};
|
||||
|
||||
/**
|
||||
* The identity server configuration the client should use. This
|
||||
* will always be present on teh object.
|
||||
* @type {{state: string, base_url: string}} The configuration.
|
||||
*/
|
||||
this["m.identity_server"] = {
|
||||
/**
|
||||
* The lookup result state. If this is anything other than
|
||||
* AutoDiscovery.SUCCESS then base_url may be falsey.
|
||||
*/
|
||||
state: AutoDiscovery.PROMPT,
|
||||
|
||||
/**
|
||||
* The base URL clients should use for interacting with the
|
||||
* identity server. May be falsey if the state is not
|
||||
* AutoDiscovery.SUCCESS.
|
||||
*/
|
||||
base_url: "https://vector.im",
|
||||
};
|
||||
}
|
||||
export enum AutoDiscoveryAction {
|
||||
SUCCESS = "SUCCESS",
|
||||
IGNORE = "IGNORE",
|
||||
PROMPT = "PROMPT",
|
||||
FAIL_PROMPT = "FAIL_PROMPT",
|
||||
FAIL_ERROR = "FAIL_ERROR",
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,55 +43,36 @@ export class AutoDiscovery {
|
||||
// translate the meaning of the states in the spec, but also
|
||||
// support our own if needed.
|
||||
|
||||
static get ERROR_INVALID() {
|
||||
return "Invalid homeserver discovery response";
|
||||
}
|
||||
public static readonly ERROR_INVALID = "Invalid homeserver discovery response";
|
||||
|
||||
static get ERROR_GENERIC_FAILURE() {
|
||||
return "Failed to get autodiscovery configuration from server";
|
||||
}
|
||||
public static readonly ERROR_GENERIC_FAILURE = "Failed to get autodiscovery configuration from server";
|
||||
|
||||
static get ERROR_INVALID_HS_BASE_URL() {
|
||||
return "Invalid base_url for m.homeserver";
|
||||
}
|
||||
public static readonly ERROR_INVALID_HS_BASE_URL = "Invalid base_url for m.homeserver";
|
||||
|
||||
static get ERROR_INVALID_HOMESERVER() {
|
||||
return "Homeserver URL does not appear to be a valid Matrix homeserver";
|
||||
}
|
||||
public static readonly ERROR_INVALID_HOMESERVER = "Homeserver URL does not appear to be a valid Matrix homeserver";
|
||||
|
||||
static get ERROR_INVALID_IS_BASE_URL() {
|
||||
return "Invalid base_url for m.identity_server";
|
||||
}
|
||||
public static readonly ERROR_INVALID_IS_BASE_URL = "Invalid base_url for m.identity_server";
|
||||
|
||||
static get ERROR_INVALID_IDENTITY_SERVER() {
|
||||
return "Identity server URL does not appear to be a valid identity server";
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
public static readonly ERROR_INVALID_IDENTITY_SERVER = "Identity server URL does not appear to be a valid identity server";
|
||||
|
||||
static get ERROR_INVALID_IS() {
|
||||
return "Invalid identity server discovery response";
|
||||
}
|
||||
public static readonly ERROR_INVALID_IS = "Invalid identity server discovery response";
|
||||
|
||||
static get ERROR_MISSING_WELLKNOWN() {
|
||||
return "No .well-known JSON file found";
|
||||
}
|
||||
public static readonly ERROR_MISSING_WELLKNOWN = "No .well-known JSON file found";
|
||||
|
||||
static get ERROR_INVALID_JSON() {
|
||||
return "Invalid JSON";
|
||||
}
|
||||
public static readonly ERROR_INVALID_JSON = "Invalid JSON";
|
||||
|
||||
static get ALL_ERRORS() {
|
||||
return [
|
||||
AutoDiscovery.ERROR_INVALID,
|
||||
AutoDiscovery.ERROR_GENERIC_FAILURE,
|
||||
AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
|
||||
AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
|
||||
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
|
||||
AutoDiscovery.ERROR_INVALID_IS,
|
||||
AutoDiscovery.ERROR_MISSING_WELLKNOWN,
|
||||
AutoDiscovery.ERROR_INVALID_JSON,
|
||||
];
|
||||
}
|
||||
public static readonly ALL_ERRORS = [
|
||||
AutoDiscovery.ERROR_INVALID,
|
||||
AutoDiscovery.ERROR_GENERIC_FAILURE,
|
||||
AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
|
||||
AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
|
||||
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
|
||||
AutoDiscovery.ERROR_INVALID_IS,
|
||||
AutoDiscovery.ERROR_MISSING_WELLKNOWN,
|
||||
AutoDiscovery.ERROR_INVALID_JSON,
|
||||
];
|
||||
|
||||
/**
|
||||
* The auto discovery failed. The client is expected to communicate
|
||||
@@ -158,7 +80,7 @@ export class AutoDiscovery {
|
||||
* @return {string}
|
||||
* @constructor
|
||||
*/
|
||||
static get FAIL_ERROR() { return "FAIL_ERROR"; }
|
||||
public static readonly FAIL_ERROR = AutoDiscoveryAction.FAIL_ERROR;
|
||||
|
||||
/**
|
||||
* The auto discovery failed, however the client may still recover
|
||||
@@ -169,7 +91,7 @@ export class AutoDiscovery {
|
||||
* @return {string}
|
||||
* @constructor
|
||||
*/
|
||||
static get FAIL_PROMPT() { return "FAIL_PROMPT"; }
|
||||
public static readonly FAIL_PROMPT = AutoDiscoveryAction.FAIL_PROMPT;
|
||||
|
||||
/**
|
||||
* The auto discovery didn't fail but did not find anything of
|
||||
@@ -178,14 +100,14 @@ export class AutoDiscovery {
|
||||
* @return {string}
|
||||
* @constructor
|
||||
*/
|
||||
static get PROMPT() { return "PROMPT"; }
|
||||
public static readonly PROMPT = AutoDiscoveryAction.PROMPT;
|
||||
|
||||
/**
|
||||
* The auto discovery was successful.
|
||||
* @return {string}
|
||||
* @constructor
|
||||
*/
|
||||
static get SUCCESS() { return "SUCCESS"; }
|
||||
public static readonly SUCCESS = AutoDiscoveryAction.SUCCESS;
|
||||
|
||||
/**
|
||||
* Validates and verifies client configuration information for purposes
|
||||
@@ -193,13 +115,13 @@ export class AutoDiscovery {
|
||||
* and identity server URL the client would want. Additional details
|
||||
* may also be included, and will be transparently brought into the
|
||||
* response object unaltered.
|
||||
* @param {string} wellknown The configuration object itself, as returned
|
||||
* @param {object} wellknown The configuration object itself, as returned
|
||||
* by the .well-known auto-discovery endpoint.
|
||||
* @return {Promise<DiscoveredClientConfig>} Resolves to the verified
|
||||
* configuration, which may include error states. Rejects on unexpected
|
||||
* failure, not when verification fails.
|
||||
*/
|
||||
static async fromDiscoveryConfig(wellknown) {
|
||||
public static async fromDiscoveryConfig(wellknown: any): Promise<IClientWellKnown> {
|
||||
// Step 1 is to get the config, which is provided to us here.
|
||||
|
||||
// We default to an error state to make the first few checks easier to
|
||||
@@ -240,7 +162,7 @@ export class AutoDiscovery {
|
||||
|
||||
// Step 2: Make sure the homeserver URL is valid *looking*. We'll make
|
||||
// sure it points to a homeserver in Step 3.
|
||||
const hsUrl = this._sanitizeWellKnownUrl(
|
||||
const hsUrl = this.sanitizeWellKnownUrl(
|
||||
wellknown["m.homeserver"]["base_url"],
|
||||
);
|
||||
if (!hsUrl) {
|
||||
@@ -250,7 +172,7 @@ export class AutoDiscovery {
|
||||
}
|
||||
|
||||
// Step 3: Make sure the homeserver URL points to a homeserver.
|
||||
const hsVersions = await this._fetchWellKnownObject(
|
||||
const hsVersions = await this.fetchWellKnownObject(
|
||||
`${hsUrl}/_matrix/client/versions`,
|
||||
);
|
||||
if (!hsVersions || !hsVersions.raw["versions"]) {
|
||||
@@ -272,7 +194,7 @@ export class AutoDiscovery {
|
||||
};
|
||||
|
||||
// Step 5: Try to pull out the identity server configuration
|
||||
let isUrl = "";
|
||||
let isUrl: string | boolean = "";
|
||||
if (wellknown["m.identity_server"]) {
|
||||
// We prepare a failing identity server response to save lines later
|
||||
// in this branch.
|
||||
@@ -287,7 +209,7 @@ export class AutoDiscovery {
|
||||
|
||||
// Step 5a: Make sure the URL is valid *looking*. We'll make sure it
|
||||
// points to an identity server in Step 5b.
|
||||
isUrl = this._sanitizeWellKnownUrl(
|
||||
isUrl = this.sanitizeWellKnownUrl(
|
||||
wellknown["m.identity_server"]["base_url"],
|
||||
);
|
||||
if (!isUrl) {
|
||||
@@ -299,10 +221,10 @@ export class AutoDiscovery {
|
||||
|
||||
// Step 5b: Verify there is an identity server listening on the provided
|
||||
// URL.
|
||||
const isResponse = await this._fetchWellKnownObject(
|
||||
const isResponse = await this.fetchWellKnownObject(
|
||||
`${isUrl}/_matrix/identity/api/v1`,
|
||||
);
|
||||
if (!isResponse || !isResponse.raw || isResponse.action !== "SUCCESS") {
|
||||
if (!isResponse || !isResponse.raw || isResponse.action !== AutoDiscoveryAction.SUCCESS) {
|
||||
logger.error("Invalid /api/v1 response");
|
||||
failingClientConfig["m.identity_server"].error =
|
||||
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER;
|
||||
@@ -317,7 +239,7 @@ export class AutoDiscovery {
|
||||
|
||||
// Step 6: Now that the identity server is valid, or never existed,
|
||||
// populate the IS section.
|
||||
if (isUrl && isUrl.length > 0) {
|
||||
if (isUrl && isUrl.toString().length > 0) {
|
||||
clientConfig["m.identity_server"] = {
|
||||
state: AutoDiscovery.SUCCESS,
|
||||
error: null,
|
||||
@@ -359,7 +281,7 @@ export class AutoDiscovery {
|
||||
* configuration, which may include error states. Rejects on unexpected
|
||||
* failure, not when discovery fails.
|
||||
*/
|
||||
static async findClientConfig(domain) {
|
||||
public static async findClientConfig(domain: string): Promise<IClientWellKnown> {
|
||||
if (!domain || typeof(domain) !== "string" || domain.length === 0) {
|
||||
throw new Error("'domain' must be a string of non-zero length");
|
||||
}
|
||||
@@ -395,13 +317,13 @@ export class AutoDiscovery {
|
||||
|
||||
// Step 1: Actually request the .well-known JSON file and make sure it
|
||||
// at least has a homeserver definition.
|
||||
const wellknown = await this._fetchWellKnownObject(
|
||||
const wellknown = await this.fetchWellKnownObject(
|
||||
`https://${domain}/.well-known/matrix/client`,
|
||||
);
|
||||
if (!wellknown || wellknown.action !== "SUCCESS") {
|
||||
if (!wellknown || wellknown.action !== AutoDiscoveryAction.SUCCESS) {
|
||||
logger.error("No response or error when parsing .well-known");
|
||||
if (wellknown.reason) logger.error(wellknown.reason);
|
||||
if (wellknown.action === "IGNORE") {
|
||||
if (wellknown.action === AutoDiscoveryAction.IGNORE) {
|
||||
clientConfig["m.homeserver"] = {
|
||||
state: AutoDiscovery.PROMPT,
|
||||
error: null,
|
||||
@@ -427,12 +349,12 @@ export class AutoDiscovery {
|
||||
* @returns {Promise<object>} Resolves to the domain's client config. Can
|
||||
* be an empty object.
|
||||
*/
|
||||
static async getRawClientConfig(domain) {
|
||||
public static async getRawClientConfig(domain: string): Promise<IClientWellKnown> {
|
||||
if (!domain || typeof(domain) !== "string" || domain.length === 0) {
|
||||
throw new Error("'domain' must be a string of non-zero length");
|
||||
}
|
||||
|
||||
const response = await this._fetchWellKnownObject(
|
||||
const response = await this.fetchWellKnownObject(
|
||||
`https://${domain}/.well-known/matrix/client`,
|
||||
);
|
||||
if (!response) return {};
|
||||
@@ -447,7 +369,7 @@ export class AutoDiscovery {
|
||||
* @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid.
|
||||
* @private
|
||||
*/
|
||||
static _sanitizeWellKnownUrl(url) {
|
||||
private static sanitizeWellKnownUrl(url: string): string | boolean {
|
||||
if (!url) return false;
|
||||
|
||||
try {
|
||||
@@ -495,8 +417,9 @@ export class AutoDiscovery {
|
||||
* @return {Promise<object>} Resolves to the returned state.
|
||||
* @private
|
||||
*/
|
||||
static async _fetchWellKnownObject(url) {
|
||||
private static async fetchWellKnownObject(url: string): Promise<IWellKnownConfig> {
|
||||
return new Promise(function(resolve, reject) {
|
||||
// eslint-disable-next-line
|
||||
const request = require("./matrix").getRequest();
|
||||
if (!request) throw new Error("No request library available");
|
||||
request(
|
||||
@@ -505,10 +428,10 @@ export class AutoDiscovery {
|
||||
if (err || response &&
|
||||
(response.statusCode < 200 || response.statusCode >= 300)
|
||||
) {
|
||||
let action = "FAIL_PROMPT";
|
||||
let action = AutoDiscoveryAction.FAIL_PROMPT;
|
||||
let reason = (err ? err.message : null) || "General failure";
|
||||
if (response && response.statusCode === 404) {
|
||||
action = "IGNORE";
|
||||
action = AutoDiscoveryAction.IGNORE;
|
||||
reason = AutoDiscovery.ERROR_MISSING_WELLKNOWN;
|
||||
}
|
||||
resolve({ raw: {}, action: action, reason: reason, error: err });
|
||||
@@ -516,7 +439,7 @@ export class AutoDiscovery {
|
||||
}
|
||||
|
||||
try {
|
||||
resolve({ raw: JSON.parse(body), action: "SUCCESS" });
|
||||
resolve({ raw: JSON.parse(body), action: AutoDiscoveryAction.SUCCESS });
|
||||
} catch (e) {
|
||||
let reason = AutoDiscovery.ERROR_INVALID;
|
||||
if (e.name === "SyntaxError") {
|
||||
@@ -524,7 +447,7 @@ export class AutoDiscovery {
|
||||
}
|
||||
resolve({
|
||||
raw: {},
|
||||
action: "FAIL_PROMPT",
|
||||
action: AutoDiscoveryAction.FAIL_PROMPT,
|
||||
reason: reason,
|
||||
error: e,
|
||||
});
|
||||
@@ -14,10 +14,15 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as matrixcs from "./matrix";
|
||||
import request from "browser-request";
|
||||
import queryString from "qs";
|
||||
|
||||
import * as matrixcs from "./matrix";
|
||||
|
||||
if (matrixcs.getRequest()) {
|
||||
throw new Error("Multiple matrix-js-sdk entrypoints detected!");
|
||||
}
|
||||
|
||||
matrixcs.request(function(opts, fn) {
|
||||
// We manually fix the query string for browser-request because
|
||||
// it doesn't correctly handle cases like ?via=one&via=two. Instead
|
||||
|
||||
+1376
-448
File diff suppressed because it is too large
Load Diff
+42
-2
@@ -1,6 +1,5 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2018 - 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -18,6 +17,14 @@ limitations under the License.
|
||||
/** @module ContentHelpers */
|
||||
|
||||
import { MsgType } from "./@types/event";
|
||||
import { TEXT_NODE_TYPE } from "./@types/extensible_events";
|
||||
import {
|
||||
ASSET_NODE_TYPE,
|
||||
ILocationContent,
|
||||
LocationAssetType,
|
||||
LOCATION_EVENT_TYPE,
|
||||
TIMESTAMP_NODE_TYPE,
|
||||
} from "./@types/location";
|
||||
|
||||
/**
|
||||
* Generates the content for a HTML Message event
|
||||
@@ -99,3 +106,36 @@ export function makeEmoteMessage(body: string) {
|
||||
body: body,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the content for a Location event
|
||||
* @param text a text for of our location
|
||||
* @param uri a geo:// uri for the location
|
||||
* @param ts the timestamp when the location was correct (milliseconds since
|
||||
* the UNIX epoch)
|
||||
* @param description the (optional) label for this location on the map
|
||||
* @param asset_type the (optional) asset type of this location e.g. "m.self"
|
||||
*/
|
||||
export function makeLocationContent(
|
||||
text: string,
|
||||
uri: string,
|
||||
ts: number,
|
||||
description?: string,
|
||||
assetType?: LocationAssetType,
|
||||
): ILocationContent {
|
||||
return {
|
||||
"body": text,
|
||||
"msgtype": MsgType.Location,
|
||||
"geo_uri": uri,
|
||||
[LOCATION_EVENT_TYPE.name]: {
|
||||
uri,
|
||||
description,
|
||||
},
|
||||
[ASSET_NODE_TYPE.name]: {
|
||||
type: assetType ?? LocationAssetType.Self,
|
||||
},
|
||||
[TEXT_NODE_TYPE.name]: text,
|
||||
[TIMESTAMP_NODE_TYPE.name]: ts,
|
||||
// TODO: MSC1767 fallbacks m.image thumbnail
|
||||
};
|
||||
}
|
||||
|
||||
+3
-3
@@ -53,13 +53,13 @@ export function getHttpUriForMxc(
|
||||
}
|
||||
let serverAndMediaId = mxc.slice(6); // strips mxc://
|
||||
let prefix = "/_matrix/media/r0/download/";
|
||||
const params = {};
|
||||
const params: Record<string, string> = {};
|
||||
|
||||
if (width) {
|
||||
params["width"] = Math.round(width);
|
||||
params["width"] = Math.round(width).toString();
|
||||
}
|
||||
if (height) {
|
||||
params["height"] = Math.round(height);
|
||||
params["height"] = Math.round(height).toString();
|
||||
}
|
||||
if (resizeMethod) {
|
||||
params["method"] = resizeMethod;
|
||||
|
||||
+21
-17
@@ -19,13 +19,12 @@ limitations under the License.
|
||||
* @module crypto/CrossSigning
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { PkSigning } from "@matrix-org/olm";
|
||||
|
||||
import { decodeBase64, encodeBase64, pkSign, pkVerify } from './olmlib';
|
||||
import { logger } from '../logger';
|
||||
import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store';
|
||||
import { decryptAES, encryptAES } from './aes';
|
||||
import { PkSigning } from "@matrix-org/olm";
|
||||
import { DeviceInfo } from "./deviceinfo";
|
||||
import { SecretStorage } from "./SecretStorage";
|
||||
import { ICrossSigningKey, ISignedKey, MatrixClient } from "../client";
|
||||
@@ -33,6 +32,7 @@ import { OlmDevice } from "./OlmDevice";
|
||||
import { ICryptoCallbacks } from "../matrix";
|
||||
import { ISignatures } from "../@types/signed";
|
||||
import { CryptoStore } from "./store/base";
|
||||
import { ISecretStorageKeyInfo } from "./api";
|
||||
|
||||
const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;
|
||||
|
||||
@@ -54,7 +54,7 @@ export interface ICrossSigningInfo {
|
||||
crossSigningVerifiedBefore: boolean;
|
||||
}
|
||||
|
||||
export class CrossSigningInfo extends EventEmitter {
|
||||
export class CrossSigningInfo {
|
||||
public keys: Record<string, ICrossSigningKey> = {};
|
||||
public firstUse = true;
|
||||
// This tracks whether we've ever verified this user with any identity.
|
||||
@@ -78,9 +78,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
public readonly userId: string,
|
||||
private callbacks: ICryptoCallbacks = {},
|
||||
private cacheCallbacks: ICacheCallbacks = {},
|
||||
) {
|
||||
super();
|
||||
}
|
||||
) {}
|
||||
|
||||
public static fromStorage(obj: ICrossSigningInfo, userId: string): CrossSigningInfo {
|
||||
const res = new CrossSigningInfo(userId);
|
||||
@@ -175,7 +173,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
// check what SSSS keys have encrypted the master key (if any)
|
||||
const stored = await secretStorage.isStored("m.cross_signing.master", false) || {};
|
||||
// then check which of those SSSS keys have also encrypted the SSK and USK
|
||||
function intersect(s) {
|
||||
function intersect(s: Record<string, ISecretStorageKeyInfo>) {
|
||||
for (const k of Object.keys(stored)) {
|
||||
if (!s[k]) {
|
||||
delete stored[k];
|
||||
@@ -304,7 +302,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
}
|
||||
|
||||
const privateKeys: Record<string, Uint8Array> = {};
|
||||
const keys: Record<string, any> = {}; // TODO types
|
||||
const keys: Record<string, ICrossSigningKey> = {};
|
||||
let masterSigning;
|
||||
let masterPub;
|
||||
|
||||
@@ -713,7 +711,7 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
|
||||
});
|
||||
|
||||
if (key && key.ciphertext) {
|
||||
const pickleKey = Buffer.from(olmDevice._pickleKey);
|
||||
const pickleKey = Buffer.from(olmDevice.pickleKey);
|
||||
const decrypted = await decryptAES(key, pickleKey, type);
|
||||
return decodeBase64(decrypted);
|
||||
} else {
|
||||
@@ -726,7 +724,7 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
|
||||
`storeCrossSigningKeyCache expects Uint8Array, got ${key}`,
|
||||
);
|
||||
}
|
||||
const pickleKey = Buffer.from(olmDevice._pickleKey);
|
||||
const pickleKey = Buffer.from(olmDevice.pickleKey);
|
||||
const encryptedKey = await encryptAES(encodeBase64(key), pickleKey, type);
|
||||
return store.doTxn(
|
||||
'readwrite',
|
||||
@@ -739,6 +737,8 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
|
||||
};
|
||||
}
|
||||
|
||||
export type KeysDuringVerification = [[string, PkSigning], [string, PkSigning], [string, PkSigning], void];
|
||||
|
||||
/**
|
||||
* Request cross-signing keys from another device during verification.
|
||||
*
|
||||
@@ -746,15 +746,19 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
|
||||
* @param {string} userId The user ID being verified
|
||||
* @param {string} deviceId The device ID being verified
|
||||
*/
|
||||
export async function requestKeysDuringVerification(baseApis: MatrixClient, userId: string, deviceId: string) {
|
||||
export function requestKeysDuringVerification(
|
||||
baseApis: MatrixClient,
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
): Promise<KeysDuringVerification | void> {
|
||||
// If this is a self-verification, ask the other party for keys
|
||||
if (baseApis.getUserId() !== userId) {
|
||||
return;
|
||||
}
|
||||
logger.log("Cross-signing: Self-verification done; requesting keys");
|
||||
// This happens asynchronously, and we're not concerned about waiting for
|
||||
// it. We return here in order to test.
|
||||
return new Promise((resolve, reject) => {
|
||||
// it. We return here in order to test.
|
||||
return new Promise<KeysDuringVerification | void>((resolve, reject) => {
|
||||
const client = baseApis;
|
||||
const original = client.crypto.crossSigningInfo;
|
||||
|
||||
@@ -781,7 +785,7 @@ export async function requestKeysDuringVerification(baseApis: MatrixClient, user
|
||||
// https://github.com/vector-im/element-web/issues/12604
|
||||
// then change here to reject on the timeout
|
||||
// Requests can be ignored, so don't wait around forever
|
||||
const timeout = new Promise((resolve, reject) => {
|
||||
const timeout = new Promise<void>((resolve) => {
|
||||
setTimeout(
|
||||
resolve,
|
||||
KEY_REQUEST_TIMEOUT_MS,
|
||||
@@ -801,7 +805,7 @@ export async function requestKeysDuringVerification(baseApis: MatrixClient, user
|
||||
logger.info("Got key backup key, decoding...");
|
||||
const decodedKey = decodeBase64(base64Key);
|
||||
logger.info("Decoded backup key, storing...");
|
||||
client.crypto.storeSessionBackupPrivateKey(
|
||||
await client.crypto.storeSessionBackupPrivateKey(
|
||||
Uint8Array.from(decodedKey),
|
||||
);
|
||||
logger.info("Backup key stored. Starting backup restore...");
|
||||
@@ -814,13 +818,13 @@ export async function requestKeysDuringVerification(baseApis: MatrixClient, user
|
||||
})();
|
||||
|
||||
// We call getCrossSigningKey() for its side-effects
|
||||
return Promise.race([
|
||||
return Promise.race<KeysDuringVerification | void>([
|
||||
Promise.all([
|
||||
crossSigning.getCrossSigningKey("master"),
|
||||
crossSigning.getCrossSigningKey("self_signing"),
|
||||
crossSigning.getCrossSigningKey("user_signing"),
|
||||
backupKeyPromise,
|
||||
]),
|
||||
]) as Promise<KeysDuringVerification>,
|
||||
timeout,
|
||||
]).then(resolve, reject);
|
||||
}).catch((e) => {
|
||||
|
||||
+29
-19
@@ -20,17 +20,17 @@ limitations under the License.
|
||||
* Manages the list of other users' devices
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { logger } from '../logger';
|
||||
import { DeviceInfo, IDevice } from './deviceinfo';
|
||||
import { CrossSigningInfo, ICrossSigningInfo } from './CrossSigning';
|
||||
import * as olmlib from './olmlib';
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { chunkPromises, defer, IDeferred, sleep } from '../utils';
|
||||
import { MatrixClient } from "../client";
|
||||
import { IDownloadKeyResult, MatrixClient } from "../client";
|
||||
import { OlmDevice } from "./OlmDevice";
|
||||
import { CryptoStore } from "./store/base";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
import { CryptoEvent, CryptoEventHandlerMap } from "./index";
|
||||
|
||||
/* State transition diagram for DeviceList.deviceTrackingStatus
|
||||
*
|
||||
@@ -62,10 +62,12 @@ export enum TrackingStatus {
|
||||
|
||||
export type DeviceInfoMap = Record<string, Record<string, DeviceInfo>>;
|
||||
|
||||
type EmittedEvents = CryptoEvent.WillUpdateDevices | CryptoEvent.DevicesUpdated | CryptoEvent.UserCrossSigningUpdated;
|
||||
|
||||
/**
|
||||
* @alias module:crypto/DeviceList
|
||||
*/
|
||||
export class DeviceList extends EventEmitter {
|
||||
export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHandlerMap> {
|
||||
private devices: { [userId: string]: { [deviceId: string]: IDevice } } = {};
|
||||
|
||||
public crossSigningInfo: { [userId: string]: ICrossSigningInfo } = {};
|
||||
@@ -265,8 +267,8 @@ export class DeviceList extends EventEmitter {
|
||||
* module:crypto/deviceinfo|DeviceInfo}.
|
||||
*/
|
||||
public downloadKeys(userIds: string[], forceDownload: boolean): Promise<DeviceInfoMap> {
|
||||
const usersToDownload = [];
|
||||
const promises = [];
|
||||
const usersToDownload: string[] = [];
|
||||
const promises: Promise<unknown>[] = [];
|
||||
|
||||
userIds.forEach((u) => {
|
||||
const trackingStatus = this.deviceTrackingStatus[u];
|
||||
@@ -633,8 +635,8 @@ export class DeviceList extends EventEmitter {
|
||||
}
|
||||
});
|
||||
|
||||
const finished = (success) => {
|
||||
this.emit("crypto.willUpdateDevices", users, !this.hasFetched);
|
||||
const finished = (success: boolean): void => {
|
||||
this.emit(CryptoEvent.WillUpdateDevices, users, !this.hasFetched);
|
||||
users.forEach((u) => {
|
||||
this.dirty = true;
|
||||
|
||||
@@ -659,7 +661,7 @@ export class DeviceList extends EventEmitter {
|
||||
}
|
||||
});
|
||||
this.saveIfDirty();
|
||||
this.emit("crypto.devicesUpdated", users, !this.hasFetched);
|
||||
this.emit(CryptoEvent.DevicesUpdated, users, !this.hasFetched);
|
||||
this.hasFetched = true;
|
||||
};
|
||||
|
||||
@@ -756,17 +758,21 @@ class DeviceListUpdateSerialiser {
|
||||
opts.token = this.syncToken;
|
||||
}
|
||||
|
||||
const factories = [];
|
||||
const factories: Array<() => Promise<IDownloadKeyResult>> = [];
|
||||
for (let i = 0; i < downloadUsers.length; i += this.deviceList.keyDownloadChunkSize) {
|
||||
const userSlice = downloadUsers.slice(i, i + this.deviceList.keyDownloadChunkSize);
|
||||
factories.push(() => this.baseApis.downloadKeysForUsers(userSlice, opts));
|
||||
}
|
||||
|
||||
chunkPromises(factories, 3).then(async (responses: any[]) => {
|
||||
const dk = Object.assign({}, ...(responses.map(res => res.device_keys || {})));
|
||||
const masterKeys = Object.assign({}, ...(responses.map(res => res.master_keys || {})));
|
||||
const ssks = Object.assign({}, ...(responses.map(res => res.self_signing_keys || {})));
|
||||
const usks = Object.assign({}, ...(responses.map(res => res.user_signing_keys || {})));
|
||||
chunkPromises(factories, 3).then(async (responses: IDownloadKeyResult[]) => {
|
||||
const dk: IDownloadKeyResult["device_keys"]
|
||||
= Object.assign({}, ...(responses.map(res => res.device_keys || {})));
|
||||
const masterKeys: IDownloadKeyResult["master_keys"]
|
||||
= Object.assign({}, ...(responses.map(res => res.master_keys || {})));
|
||||
const ssks: IDownloadKeyResult["self_signing_keys"]
|
||||
= Object.assign({}, ...(responses.map(res => res.self_signing_keys || {})));
|
||||
const usks: IDownloadKeyResult["user_signing_keys"]
|
||||
= Object.assign({}, ...(responses.map(res => res.user_signing_keys || {})));
|
||||
|
||||
// yield to other things that want to execute in between users, to
|
||||
// avoid wedging the CPU
|
||||
@@ -811,8 +817,12 @@ class DeviceListUpdateSerialiser {
|
||||
|
||||
private async processQueryResponseForUser(
|
||||
userId: string,
|
||||
dkResponse: object,
|
||||
crossSigningResponse: any, // TODO types
|
||||
dkResponse: IDownloadKeyResult["device_keys"]["user_id"],
|
||||
crossSigningResponse: {
|
||||
master: IDownloadKeyResult["master_keys"]["user_id"];
|
||||
self_signing: IDownloadKeyResult["master_keys"]["user_id"]; // eslint-disable-line camelcase
|
||||
user_signing: IDownloadKeyResult["user_signing_keys"]["user_id"]; // eslint-disable-line camelcase
|
||||
},
|
||||
): Promise<void> {
|
||||
logger.log('got device keys for ' + userId + ':', dkResponse);
|
||||
logger.log('got cross-signing keys for ' + userId + ':', crossSigningResponse);
|
||||
@@ -859,7 +869,7 @@ class DeviceListUpdateSerialiser {
|
||||
|
||||
// NB. Unlike most events in the js-sdk, this one is internal to the
|
||||
// js-sdk and is not re-emitted
|
||||
this.deviceList.emit('userCrossSigningUpdated', userId);
|
||||
this.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -869,7 +879,7 @@ async function updateStoredDeviceKeysForUser(
|
||||
olmDevice: OlmDevice,
|
||||
userId: string,
|
||||
userStore: Record<string, DeviceInfo>,
|
||||
userResult: object,
|
||||
userResult: IDownloadKeyResult["device_keys"]["user_id"],
|
||||
localUserId: string,
|
||||
localDeviceId: string,
|
||||
): Promise<boolean> {
|
||||
|
||||
@@ -16,13 +16,14 @@ limitations under the License.
|
||||
|
||||
import { logger } from "../logger";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { EventEmitter } from "events";
|
||||
import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning";
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { PREFIX_UNSTABLE } from "../http-api";
|
||||
import { Method, PREFIX_UNSTABLE } from "../http-api";
|
||||
import { Crypto, IBootstrapCrossSigningOpts } from "./index";
|
||||
import {
|
||||
ClientEvent,
|
||||
CrossSigningKeys,
|
||||
ClientEventHandlerMap,
|
||||
ICrossSigningKey,
|
||||
ICryptoCallbacks,
|
||||
ISignedKey,
|
||||
@@ -30,6 +31,8 @@ import {
|
||||
} from "../matrix";
|
||||
import { ISecretStorageKeyInfo } from "./api";
|
||||
import { IKeyBackupInfo } from "./keybackup";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
import { IAccountDataClient } from "./SecretStorage";
|
||||
|
||||
interface ICrossSigningKeys {
|
||||
authUpload: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"];
|
||||
@@ -59,7 +62,7 @@ export class EncryptionSetupBuilder {
|
||||
* @param {Object.<String, MatrixEvent>} accountData pre-existing account data, will only be read, not written.
|
||||
* @param {CryptoCallbacks} delegateCryptoCallbacks crypto callbacks to delegate to if the key isn't in cache yet
|
||||
*/
|
||||
constructor(accountData: Record<string, MatrixEvent>, delegateCryptoCallbacks: ICryptoCallbacks) {
|
||||
constructor(accountData: Record<string, MatrixEvent>, delegateCryptoCallbacks?: ICryptoCallbacks) {
|
||||
this.accountDataClientAdapter = new AccountDataClientAdapter(accountData);
|
||||
this.crossSigningCallbacks = new CrossSigningCallbacks();
|
||||
this.ssssCryptoCallbacks = new SSSSCryptoCallbacks(delegateCryptoCallbacks);
|
||||
@@ -238,7 +241,7 @@ export class EncryptionSetupOperation {
|
||||
// Sign the backup with the cross signing key so the key backup can
|
||||
// be trusted via cross-signing.
|
||||
await baseApis.http.authedRequest(
|
||||
undefined, "PUT", "/room_keys/version/" + this.keyBackupInfo.version,
|
||||
undefined, Method.Put, "/room_keys/version/" + this.keyBackupInfo.version,
|
||||
undefined, {
|
||||
algorithm: this.keyBackupInfo.algorithm,
|
||||
auth_data: this.keyBackupInfo.auth_data,
|
||||
@@ -248,7 +251,7 @@ export class EncryptionSetupOperation {
|
||||
} else {
|
||||
// add new key backup
|
||||
await baseApis.http.authedRequest(
|
||||
undefined, "POST", "/room_keys/version",
|
||||
undefined, Method.Post, "/room_keys/version",
|
||||
undefined, this.keyBackupInfo,
|
||||
{ prefix: PREFIX_UNSTABLE },
|
||||
);
|
||||
@@ -261,7 +264,10 @@ export class EncryptionSetupOperation {
|
||||
* Catches account data set by SecretStorage during bootstrapping by
|
||||
* implementing the methods related to account data in MatrixClient
|
||||
*/
|
||||
class AccountDataClientAdapter extends EventEmitter {
|
||||
class AccountDataClientAdapter
|
||||
extends TypedEventEmitter<ClientEvent.AccountData, ClientEventHandlerMap>
|
||||
implements IAccountDataClient {
|
||||
//
|
||||
public readonly values = new Map<string, MatrixEvent>();
|
||||
|
||||
/**
|
||||
@@ -308,7 +314,7 @@ class AccountDataClientAdapter extends EventEmitter {
|
||||
// and it seems to rely on this.
|
||||
return Promise.resolve().then(() => {
|
||||
const event = new MatrixEvent({ type, content });
|
||||
this.emit("accountData", event, lastEvent);
|
||||
this.emit(ClientEvent.AccountData, event, lastEvent);
|
||||
return {};
|
||||
});
|
||||
}
|
||||
@@ -351,12 +357,12 @@ class CrossSigningCallbacks implements ICryptoCallbacks, ICacheCallbacks {
|
||||
class SSSSCryptoCallbacks {
|
||||
private readonly privateKeys = new Map<string, Uint8Array>();
|
||||
|
||||
constructor(private readonly delegateCryptoCallbacks: ICryptoCallbacks) {}
|
||||
constructor(private readonly delegateCryptoCallbacks?: ICryptoCallbacks) {}
|
||||
|
||||
public async getSecretStorageKey(
|
||||
{ keys }: { keys: Record<string, ISecretStorageKeyInfo> },
|
||||
name: string,
|
||||
): Promise<[string, Uint8Array]> {
|
||||
): Promise<[string, Uint8Array]|null> {
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
const privateKey = this.privateKeys.get(keyId);
|
||||
if (privateKey) {
|
||||
@@ -365,7 +371,7 @@ class SSSSCryptoCallbacks {
|
||||
}
|
||||
// if we don't have the key cached yet, ask
|
||||
// for it to the general crypto callbacks and cache it
|
||||
if (this.delegateCryptoCallbacks) {
|
||||
if (this?.delegateCryptoCallbacks?.getSecretStorageKey) {
|
||||
const result = await this.delegateCryptoCallbacks.
|
||||
getSecretStorageKey({ keys }, name);
|
||||
if (result) {
|
||||
@@ -374,6 +380,7 @@ class SSSSCryptoCallbacks {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public addPrivateKey(keyId: string, keyInfo: ISecretStorageKeyInfo, privKey: Uint8Array): void {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+37
-27
@@ -16,12 +16,13 @@ limitations under the License.
|
||||
|
||||
import { logger } from '../logger';
|
||||
import * as olmlib from './olmlib';
|
||||
import { encodeBase64 } from './olmlib';
|
||||
import { randomString } from '../randomstring';
|
||||
import { encryptAES, decryptAES, IEncryptedPayload, calculateKeyCheck } from './aes';
|
||||
import { encodeBase64 } from "./olmlib";
|
||||
import { ICryptoCallbacks, MatrixClient, MatrixEvent } from '../matrix';
|
||||
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from './aes';
|
||||
import { ClientEvent, ICryptoCallbacks, MatrixEvent } from '../matrix';
|
||||
import { ClientEventHandlerMap, MatrixClient } from "../client";
|
||||
import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from './api';
|
||||
import { EventEmitter } from 'stream';
|
||||
import { TypedEventEmitter } from '../models/typed-event-emitter';
|
||||
|
||||
export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2";
|
||||
|
||||
@@ -35,9 +36,9 @@ export interface ISecretRequest {
|
||||
cancel: (reason: string) => void;
|
||||
}
|
||||
|
||||
export interface IAccountDataClient extends EventEmitter {
|
||||
export interface IAccountDataClient extends TypedEventEmitter<ClientEvent.AccountData, ClientEventHandlerMap> {
|
||||
// Subset of MatrixClient (which also uses any for the event content)
|
||||
getAccountDataFromServer: (eventType: string) => Promise<Record<string, any>>;
|
||||
getAccountDataFromServer: <T extends {[k: string]: any}>(eventType: string) => Promise<T>;
|
||||
getAccountData: (eventType: string) => MatrixEvent;
|
||||
setAccountData: (eventType: string, content: any) => Promise<{}>;
|
||||
}
|
||||
@@ -54,6 +55,13 @@ interface IDecryptors {
|
||||
decrypt: (ciphertext: IEncryptedPayload) => Promise<string>;
|
||||
}
|
||||
|
||||
interface ISecretInfo {
|
||||
encrypted: {
|
||||
// eslint-disable-next-line camelcase
|
||||
key_id: IEncryptedPayload;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements Secure Secret Storage and Sharing (MSC1946)
|
||||
* @module crypto/SecretStorage
|
||||
@@ -75,8 +83,8 @@ export class SecretStorage {
|
||||
private readonly baseApis?: MatrixClient,
|
||||
) {}
|
||||
|
||||
public async getDefaultKeyId(): Promise<string> {
|
||||
const defaultKey = await this.accountDataAdapter.getAccountDataFromServer(
|
||||
public async getDefaultKeyId(): Promise<string | null> {
|
||||
const defaultKey = await this.accountDataAdapter.getAccountDataFromServer<{ key: string }>(
|
||||
'm.secret_storage.default_key',
|
||||
);
|
||||
if (!defaultKey) return null;
|
||||
@@ -90,17 +98,17 @@ export class SecretStorage {
|
||||
ev.getType() === 'm.secret_storage.default_key' &&
|
||||
ev.getContent().key === keyId
|
||||
) {
|
||||
this.accountDataAdapter.removeListener('accountData', listener);
|
||||
this.accountDataAdapter.removeListener(ClientEvent.AccountData, listener);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
this.accountDataAdapter.on('accountData', listener);
|
||||
this.accountDataAdapter.on(ClientEvent.AccountData, listener);
|
||||
|
||||
this.accountDataAdapter.setAccountData(
|
||||
'm.secret_storage.default_key',
|
||||
{ key: keyId },
|
||||
).catch(e => {
|
||||
this.accountDataAdapter.removeListener('accountData', listener);
|
||||
this.accountDataAdapter.removeListener(ClientEvent.AccountData, listener);
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
@@ -149,7 +157,7 @@ export class SecretStorage {
|
||||
do {
|
||||
keyId = randomString(32);
|
||||
} while (
|
||||
await this.accountDataAdapter.getAccountDataFromServer(
|
||||
await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
|
||||
`m.secret_storage.key.${keyId}`,
|
||||
)
|
||||
);
|
||||
@@ -182,9 +190,9 @@ export class SecretStorage {
|
||||
return null;
|
||||
}
|
||||
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
|
||||
"m.secret_storage.key." + keyId,
|
||||
) as ISecretStorageKeyInfo;
|
||||
);
|
||||
return keyInfo ? [keyId, keyInfo] : null;
|
||||
}
|
||||
|
||||
@@ -230,7 +238,7 @@ export class SecretStorage {
|
||||
* or null/undefined to use the default key.
|
||||
*/
|
||||
public async store(name: string, secret: string, keys?: string[]): Promise<void> {
|
||||
const encrypted = {};
|
||||
const encrypted: Record<string, IEncryptedPayload> = {};
|
||||
|
||||
if (!keys) {
|
||||
const defaultKeyId = await this.getDefaultKeyId();
|
||||
@@ -246,9 +254,9 @@ export class SecretStorage {
|
||||
|
||||
for (const keyId of keys) {
|
||||
// get key information from key storage
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
|
||||
"m.secret_storage.key." + keyId,
|
||||
) as ISecretStorageKeyInfo;
|
||||
);
|
||||
if (!keyInfo) {
|
||||
throw new Error("Unknown key: " + keyId);
|
||||
}
|
||||
@@ -277,7 +285,7 @@ export class SecretStorage {
|
||||
* @return {string} the contents of the secret
|
||||
*/
|
||||
public async get(name: string): Promise<string> {
|
||||
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name);
|
||||
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name);
|
||||
if (!secretInfo) {
|
||||
return;
|
||||
}
|
||||
@@ -286,11 +294,13 @@ export class SecretStorage {
|
||||
}
|
||||
|
||||
// get possible keys to decrypt
|
||||
const keys = {};
|
||||
const keys: Record<string, ISecretStorageKeyInfo> = {};
|
||||
for (const keyId of Object.keys(secretInfo.encrypted)) {
|
||||
// get key information from key storage
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
|
||||
"m.secret_storage.key." + keyId,
|
||||
const keyInfo = (
|
||||
await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
|
||||
"m.secret_storage.key." + keyId,
|
||||
)
|
||||
);
|
||||
const encInfo = secretInfo.encrypted[keyId];
|
||||
// only use keys we understand the encryption algorithm of
|
||||
@@ -306,7 +316,7 @@ export class SecretStorage {
|
||||
`the keys it is encrypted with are for a supported algorithm`);
|
||||
}
|
||||
|
||||
let keyId;
|
||||
let keyId: string;
|
||||
let decryption;
|
||||
try {
|
||||
// fetch private key from app
|
||||
@@ -335,9 +345,9 @@ export class SecretStorage {
|
||||
* with, or null if it is not present or not encrypted with a trusted
|
||||
* key
|
||||
*/
|
||||
public async isStored(name: string, checkKey: boolean): Promise<Record<string, ISecretStorageKeyInfo>> {
|
||||
public async isStored(name: string, checkKey: boolean): Promise<Record<string, ISecretStorageKeyInfo> | null> {
|
||||
// check if secret exists
|
||||
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name);
|
||||
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name);
|
||||
if (!secretInfo) return null;
|
||||
if (!secretInfo.encrypted) {
|
||||
return null;
|
||||
@@ -350,7 +360,7 @@ export class SecretStorage {
|
||||
// filter secret encryption keys with supported algorithm
|
||||
for (const keyId of Object.keys(secretInfo.encrypted)) {
|
||||
// get key information from key storage
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
|
||||
"m.secret_storage.key." + keyId,
|
||||
);
|
||||
if (!keyInfo) continue;
|
||||
@@ -375,8 +385,8 @@ export class SecretStorage {
|
||||
public request(name: string, devices: string[]): ISecretRequest {
|
||||
const requestId = this.baseApis.makeTxnId();
|
||||
|
||||
let resolve: (string) => void;
|
||||
let reject: (Error) => void;
|
||||
let resolve: (s: string) => void;
|
||||
let reject: (e: Error) => void;
|
||||
const promise = new Promise<string>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
|
||||
+4
-4
@@ -15,7 +15,6 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import type { BinaryLike } from "crypto";
|
||||
|
||||
import { getCrypto } from '../utils';
|
||||
import { decodeBase64, encodeBase64 } from './olmlib';
|
||||
|
||||
@@ -26,9 +25,10 @@ const subtleCrypto = (typeof window !== "undefined" && window.crypto) ?
|
||||
const zeroSalt = new Uint8Array(8);
|
||||
|
||||
export interface IEncryptedPayload {
|
||||
iv: string;
|
||||
ciphertext: string;
|
||||
mac: string;
|
||||
[key: string]: any; // extensible
|
||||
iv?: string;
|
||||
ciphertext?: string;
|
||||
mac?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -46,7 +46,7 @@ type DecryptionClassParams = Omit<IParams, "deviceId" | "config">;
|
||||
*/
|
||||
export const DECRYPTION_CLASSES: Record<string, new (params: DecryptionClassParams) => DecryptionAlgorithm> = {};
|
||||
|
||||
interface IParams {
|
||||
export interface IParams {
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
crypto: Crypto;
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
DecryptionAlgorithm,
|
||||
DecryptionError,
|
||||
EncryptionAlgorithm,
|
||||
IParams,
|
||||
registerAlgorithm,
|
||||
UnknownDeviceError,
|
||||
} from "./base";
|
||||
@@ -67,7 +68,7 @@ export interface IOlmDevice<T = DeviceInfo> {
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
interface IOutboundGroupSessionKey {
|
||||
export interface IOutboundGroupSessionKey {
|
||||
chain_index: number;
|
||||
key: string;
|
||||
}
|
||||
@@ -99,8 +100,21 @@ interface IPayload extends Partial<IMessage> {
|
||||
algorithm?: string;
|
||||
sender_key?: string;
|
||||
}
|
||||
|
||||
interface IEncryptedContent {
|
||||
algorithm: string;
|
||||
sender_key: string;
|
||||
ciphertext: Record<string, string>;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
interface SharedWithData {
|
||||
// The identity key of the device we shared with
|
||||
deviceKey: string;
|
||||
// The message index of the ratchet we shared with that device
|
||||
messageIndex: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @constructor
|
||||
@@ -115,12 +129,12 @@ interface IPayload extends Partial<IMessage> {
|
||||
*
|
||||
* @property {object} sharedWithDevices
|
||||
* devices with which we have shared the session key
|
||||
* userId -> {deviceId -> msgindex}
|
||||
* userId -> {deviceId -> SharedWithData}
|
||||
*/
|
||||
class OutboundSessionInfo {
|
||||
public useCount = 0;
|
||||
public creationTime: number;
|
||||
public sharedWithDevices: Record<string, Record<string, number>> = {};
|
||||
public sharedWithDevices: Record<string, Record<string, SharedWithData>> = {};
|
||||
public blockedDevicesNotified: Record<string, Record<string, boolean>> = {};
|
||||
|
||||
constructor(public readonly sessionId: string, public readonly sharedHistory = false) {
|
||||
@@ -150,11 +164,11 @@ class OutboundSessionInfo {
|
||||
return false;
|
||||
}
|
||||
|
||||
public markSharedWithDevice(userId: string, deviceId: string, chainIndex: number): void {
|
||||
public markSharedWithDevice(userId: string, deviceId: string, deviceKey: string, chainIndex: number): void {
|
||||
if (!this.sharedWithDevices[userId]) {
|
||||
this.sharedWithDevices[userId] = {};
|
||||
}
|
||||
this.sharedWithDevices[userId][deviceId] = chainIndex;
|
||||
this.sharedWithDevices[userId][deviceId] = { deviceKey, messageIndex: chainIndex };
|
||||
}
|
||||
|
||||
public markNotifiedBlockedDevice(userId: string, deviceId: string): void {
|
||||
@@ -231,7 +245,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
startTime: number;
|
||||
};
|
||||
|
||||
constructor(params) {
|
||||
constructor(params: IParams) {
|
||||
super(params);
|
||||
|
||||
this.sessionRotationPeriodMsgs = params.config?.rotation_period_msgs ?? 100;
|
||||
@@ -256,7 +270,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
blocked: IBlockedMap,
|
||||
singleOlmCreationPhase = false,
|
||||
): Promise<OutboundSessionInfo> {
|
||||
let session;
|
||||
let session: OutboundSessionInfo;
|
||||
|
||||
// takes the previous OutboundSessionInfo, and considers whether to create
|
||||
// a new one. Also shares the key with any (new) devices in the room.
|
||||
@@ -295,7 +309,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
}
|
||||
|
||||
// now check if we need to share with any devices
|
||||
const shareMap = {};
|
||||
const shareMap: Record<string, DeviceInfo[]> = {};
|
||||
|
||||
for (const [userId, userDevices] of Object.entries(devicesInRoom)) {
|
||||
for (const [deviceId, deviceInfo] of Object.entries(userDevices)) {
|
||||
@@ -334,13 +348,16 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
// share keys with devices that we already have a session for
|
||||
logger.debug(`Sharing keys with existing Olm sessions in ${this.roomId}`);
|
||||
logger.debug(`Sharing keys with existing Olm sessions in ${this.roomId}`, olmSessions);
|
||||
await this.shareKeyWithOlmSessions(session, key, payload, olmSessions);
|
||||
logger.debug(`Shared keys with existing Olm sessions in ${this.roomId}`);
|
||||
})(),
|
||||
(async () => {
|
||||
logger.debug(`Sharing keys (start phase 1) with new Olm sessions in ${this.roomId}`);
|
||||
const errorDevices = [];
|
||||
logger.debug(
|
||||
`Sharing keys (start phase 1) with new Olm sessions in ${this.roomId}`,
|
||||
devicesWithoutSession,
|
||||
);
|
||||
const errorDevices: IOlmDevice[] = [];
|
||||
|
||||
// meanwhile, establish olm sessions for devices that we don't
|
||||
// already have a session for, and share keys with them. If
|
||||
@@ -348,7 +365,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
// shorter timeout when fetching one-time keys for the first
|
||||
// phase.
|
||||
const start = Date.now();
|
||||
const failedServers = [];
|
||||
const failedServers: string[] = [];
|
||||
await this.shareKeyWithDevices(
|
||||
session, key, payload, devicesWithoutSession, errorDevices,
|
||||
singleOlmCreationPhase ? 10000 : 2000, failedServers,
|
||||
@@ -364,7 +381,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
// do this in the background and don't block anything else while we
|
||||
// do this. We only need to retry users from servers that didn't
|
||||
// respond the first time.
|
||||
const retryDevices = {};
|
||||
const retryDevices: Record<string, DeviceInfo[]> = {};
|
||||
const failedServerMap = new Set;
|
||||
for (const server of failedServers) {
|
||||
failedServerMap.add(server);
|
||||
@@ -396,8 +413,11 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
logger.debug(`Shared keys (all phases done) with new Olm sessions in ${this.roomId}`);
|
||||
})(),
|
||||
(async () => {
|
||||
logger.debug(`Notifying blocked devices in ${this.roomId}`);
|
||||
// also, notify blocked devices that they're blocked
|
||||
logger.debug(`There are ${Object.entries(blocked).length} blocked devices in ${this.roomId}`,
|
||||
Object.entries(blocked));
|
||||
|
||||
// also, notify newly blocked devices that they're blocked
|
||||
logger.debug(`Notifying newly blocked devices in ${this.roomId}`);
|
||||
const blockedMap: Record<string, Record<string, { device: IBlockedDevice }>> = {};
|
||||
let blockedCount = 0;
|
||||
for (const [userId, userBlockedDevices] of Object.entries(blocked)) {
|
||||
@@ -414,7 +434,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
}
|
||||
|
||||
await this.notifyBlockedDevices(session, blockedMap);
|
||||
logger.debug(`Notified ${blockedCount} blocked devices in ${this.roomId}`);
|
||||
logger.debug(`Notified ${blockedCount} newly blocked devices in ${this.roomId}`, blockedMap);
|
||||
})(),
|
||||
]);
|
||||
};
|
||||
@@ -571,11 +591,12 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
userDeviceMap: IOlmDevice[],
|
||||
payload: IPayload,
|
||||
): Promise<void> {
|
||||
const contentMap = {};
|
||||
const contentMap: Record<string, Record<string, IEncryptedContent>> = {};
|
||||
const deviceInfoByDeviceId = new Map<string, DeviceInfo>();
|
||||
|
||||
const promises = [];
|
||||
const promises: Promise<unknown>[] = [];
|
||||
for (let i = 0; i < userDeviceMap.length; i++) {
|
||||
const encryptedContent = {
|
||||
const encryptedContent: IEncryptedContent = {
|
||||
algorithm: olmlib.OLM_ALGORITHM,
|
||||
sender_key: this.olmDevice.deviceCurve25519Key,
|
||||
ciphertext: {},
|
||||
@@ -584,6 +605,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
const userId = val.userId;
|
||||
const deviceInfo = val.deviceInfo;
|
||||
const deviceId = deviceInfo.deviceId;
|
||||
deviceInfoByDeviceId.set(deviceId, deviceInfo);
|
||||
|
||||
if (!contentMap[userId]) {
|
||||
contentMap[userId] = {};
|
||||
@@ -636,7 +658,10 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
for (const userId of Object.keys(contentMap)) {
|
||||
for (const deviceId of Object.keys(contentMap[userId])) {
|
||||
session.markSharedWithDevice(
|
||||
userId, deviceId, chainIndex,
|
||||
userId,
|
||||
deviceId,
|
||||
deviceInfoByDeviceId.get(deviceId).getIdentityKey(),
|
||||
chainIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -661,7 +686,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
userDeviceMap: IOlmDevice<IBlockedDevice>[],
|
||||
payload: IPayload,
|
||||
): Promise<void> {
|
||||
const contentMap = {};
|
||||
const contentMap: Record<string, Record<string, IPayload>> = {};
|
||||
|
||||
for (const val of userDeviceMap) {
|
||||
const userId = val.userId;
|
||||
@@ -685,7 +710,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
|
||||
await this.baseApis.sendToDevice("org.matrix.room_key.withheld", contentMap);
|
||||
|
||||
// store that we successfully uploaded the keys of the current slice
|
||||
// record the fact that we notified these blocked devices
|
||||
for (const userId of Object.keys(contentMap)) {
|
||||
for (const deviceId of Object.keys(contentMap[userId])) {
|
||||
session.markNotifiedBlockedDevice(userId, deviceId);
|
||||
@@ -719,8 +744,8 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
logger.debug(`megolm session ${sessionId} never shared with user ${userId}`);
|
||||
return;
|
||||
}
|
||||
const sentChainIndex = obSessionInfo.sharedWithDevices[userId][device.deviceId];
|
||||
if (sentChainIndex === undefined) {
|
||||
const sessionSharedData = obSessionInfo.sharedWithDevices[userId][device.deviceId];
|
||||
if (sessionSharedData === undefined) {
|
||||
logger.debug(
|
||||
"megolm session ID " + sessionId + " never shared with device " +
|
||||
userId + ":" + device.deviceId,
|
||||
@@ -728,10 +753,18 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessionSharedData.deviceKey !== device.getIdentityKey()) {
|
||||
logger.warn(
|
||||
`Session has been shared with device ${device.deviceId} but with identity ` +
|
||||
`key ${sessionSharedData.deviceKey}. Key is now ${device.getIdentityKey()}!`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// get the key from the inbound session: the outbound one will already
|
||||
// have been ratcheted to the next chain index.
|
||||
const key = await this.olmDevice.getInboundGroupSessionKey(
|
||||
this.roomId, senderKey, sessionId, sentChainIndex,
|
||||
this.roomId, senderKey, sessionId, sessionSharedData.messageIndex,
|
||||
);
|
||||
|
||||
if (!key) {
|
||||
@@ -826,9 +859,9 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
|
||||
this.getDevicesWithoutSessions(devicemap, devicesByUser, errorDevices);
|
||||
|
||||
logger.debug(`Sharing keys with Olm sessions in ${this.roomId}`);
|
||||
logger.debug(`Sharing keys with newly created Olm sessions in ${this.roomId}`);
|
||||
await this.shareKeyWithOlmSessions(session, key, payload, devicemap);
|
||||
logger.debug(`Shared keys with Olm sessions in ${this.roomId}`);
|
||||
logger.debug(`Shared keys with newly created Olm sessions in ${this.roomId}`);
|
||||
}
|
||||
|
||||
private async shareKeyWithOlmSessions(
|
||||
@@ -844,7 +877,8 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
`megolm keys for ${session.sessionId} ` +
|
||||
`in ${this.roomId} (slice ${i + 1}/${userDeviceMaps.length})`;
|
||||
try {
|
||||
logger.debug(`Sharing ${taskDetail}`);
|
||||
logger.debug(`Sharing ${taskDetail}`,
|
||||
userDeviceMaps[i].map((d) => `${d.userId}/${d.deviceInfo.deviceId}`));
|
||||
await this.encryptAndSendKeysToDevices(
|
||||
session, key.chain_index, userDeviceMaps[i], payload,
|
||||
);
|
||||
@@ -882,20 +916,20 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
const deviceId = deviceInfo.deviceId;
|
||||
|
||||
session.markSharedWithDevice(
|
||||
userId, deviceId, key.chain_index,
|
||||
userId, deviceId, deviceInfo.getIdentityKey(), key.chain_index,
|
||||
);
|
||||
}
|
||||
|
||||
const filteredFailedDevices =
|
||||
const unnotifiedFailedDevices =
|
||||
await this.olmDevice.filterOutNotifiedErrorDevices(
|
||||
failedDevices,
|
||||
);
|
||||
logger.debug(
|
||||
`Filtered down to ${filteredFailedDevices.length} error devices ` +
|
||||
`in ${this.roomId}`,
|
||||
`Need to notify ${unnotifiedFailedDevices.length} failed devices ` +
|
||||
`which haven't been notified before in ${this.roomId}`,
|
||||
);
|
||||
const blockedMap: Record<string, Record<string, { device: IBlockedDevice }>> = {};
|
||||
for (const { userId, deviceInfo } of filteredFailedDevices) {
|
||||
for (const { userId, deviceInfo } of unnotifiedFailedDevices) {
|
||||
blockedMap[userId] = blockedMap[userId] || {};
|
||||
// we use a similar format to what
|
||||
// olmlib.ensureOlmSessionsForDevices returns, so that
|
||||
@@ -912,7 +946,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
// send the notifications
|
||||
await this.notifyBlockedDevices(session, blockedMap);
|
||||
logger.debug(
|
||||
`Notified ${filteredFailedDevices.length} devices we failed to ` +
|
||||
`Notified ${unnotifiedFailedDevices.length} devices we failed to ` +
|
||||
`create Olm sessions in ${this.roomId}`,
|
||||
);
|
||||
}
|
||||
@@ -1078,10 +1112,10 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
* devices we should shared the session with.
|
||||
*/
|
||||
private checkForUnknownDevices(devicesInRoom: DeviceInfoMap): void {
|
||||
const unknownDevices = {};
|
||||
const unknownDevices: Record<string, Record<string, DeviceInfo>> = {};
|
||||
|
||||
Object.keys(devicesInRoom).forEach((userId)=>{
|
||||
Object.keys(devicesInRoom[userId]).forEach((deviceId)=>{
|
||||
Object.keys(devicesInRoom).forEach((userId) => {
|
||||
Object.keys(devicesInRoom[userId]).forEach((deviceId) => {
|
||||
const device = devicesInRoom[userId][deviceId];
|
||||
if (device.isUnverified() && !device.isKnown()) {
|
||||
if (!unknownDevices[userId]) {
|
||||
@@ -1277,8 +1311,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
content.sender_key, event.getTs() - 120000,
|
||||
);
|
||||
if (problem) {
|
||||
let problemDescription = PROBLEM_DESCRIPTIONS[problem.type]
|
||||
|| PROBLEM_DESCRIPTIONS.unknown;
|
||||
let problemDescription = PROBLEM_DESCRIPTIONS[problem.type as "no_olm"] || PROBLEM_DESCRIPTIONS.unknown;
|
||||
if (problem.fixed) {
|
||||
problemDescription +=
|
||||
" Trying to create a new secure channel and re-requesting the keys.";
|
||||
@@ -1372,14 +1405,14 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
const senderKey = content.sender_key;
|
||||
const sessionId = content.session_id;
|
||||
const senderPendingEvents = this.pendingEvents[senderKey];
|
||||
const pendingEvents = senderPendingEvents && senderPendingEvents.get(sessionId);
|
||||
const pendingEvents = senderPendingEvents?.get(sessionId);
|
||||
if (!pendingEvents) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingEvents.delete(event);
|
||||
if (pendingEvents.size === 0) {
|
||||
senderPendingEvents.delete(senderKey);
|
||||
senderPendingEvents.delete(sessionId);
|
||||
}
|
||||
if (senderPendingEvents.size === 0) {
|
||||
delete this.pendingEvents[senderKey];
|
||||
@@ -1391,7 +1424,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} event key event
|
||||
*/
|
||||
public onRoomKeyEvent(event: MatrixEvent): void {
|
||||
public onRoomKeyEvent(event: MatrixEvent): Promise<void> {
|
||||
const content = event.getContent();
|
||||
const sessionId = content.session_id;
|
||||
let senderKey = event.getSenderKey();
|
||||
@@ -1733,7 +1766,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
}));
|
||||
|
||||
// If decrypted successfully, they'll have been removed from pendingEvents
|
||||
return !((this.pendingEvents[senderKey] || {})[sessionId]);
|
||||
return !this.pendingEvents[senderKey]?.has(sessionId);
|
||||
}
|
||||
|
||||
public async retryDecryptionFromSender(senderKey: string): Promise<boolean> {
|
||||
@@ -1767,12 +1800,12 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
for (const [senderKey, sessionId] of sharedHistorySessions) {
|
||||
const payload = await this.buildKeyForwardingMessage(this.roomId, senderKey, sessionId);
|
||||
|
||||
const promises = [];
|
||||
const contentMap = {};
|
||||
const promises: Promise<unknown>[] = [];
|
||||
const contentMap: Record<string, Record<string, IEncryptedContent>> = {};
|
||||
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
||||
contentMap[userId] = {};
|
||||
for (const deviceInfo of devices) {
|
||||
const encryptedContent = {
|
||||
const encryptedContent: IEncryptedContent = {
|
||||
algorithm: olmlib.OLM_ALGORITHM,
|
||||
sender_key: this.olmDevice.deviceCurve25519Key,
|
||||
ciphertext: {},
|
||||
|
||||
@@ -36,7 +36,7 @@ import { IEventDecryptionResult } from "../index";
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
|
||||
interface IMessage {
|
||||
type: number | string;
|
||||
type: number;
|
||||
body: string;
|
||||
}
|
||||
|
||||
@@ -269,11 +269,11 @@ class OlmDecryption extends DecryptionAlgorithm {
|
||||
// not a prekey message: we can safely just try & decrypt it
|
||||
return this.reallyDecryptMessage(theirDeviceIdentityKey, message);
|
||||
} else {
|
||||
const myPromise = this.olmDevice._olmPrekeyPromise.then(() => {
|
||||
const myPromise = this.olmDevice.olmPrekeyPromise.then(() => {
|
||||
return this.reallyDecryptMessage(theirDeviceIdentityKey, message);
|
||||
});
|
||||
// we want the error, but don't propagate it to the next decryption
|
||||
this.olmDevice._olmPrekeyPromise = myPromise.catch(() => {});
|
||||
this.olmDevice.olmPrekeyPromise = myPromise.catch(() => {});
|
||||
return await myPromise;
|
||||
}
|
||||
}
|
||||
@@ -282,7 +282,7 @@ class OlmDecryption extends DecryptionAlgorithm {
|
||||
const sessionIds = await this.olmDevice.getSessionIdsForDevice(theirDeviceIdentityKey);
|
||||
|
||||
// try each session in turn.
|
||||
const decryptionErrors = {};
|
||||
const decryptionErrors: Record<string, string> = {};
|
||||
for (let i = 0; i < sessionIds.length; i++) {
|
||||
const sessionId = sessionIds[i];
|
||||
try {
|
||||
|
||||
+7
-13
@@ -58,16 +58,9 @@ export interface IEncryptedEventInfo {
|
||||
}
|
||||
|
||||
export interface IRecoveryKey {
|
||||
keyInfo: {
|
||||
pubkey: string;
|
||||
passphrase?: {
|
||||
algorithm: string;
|
||||
iterations: number;
|
||||
salt: string;
|
||||
};
|
||||
};
|
||||
keyInfo?: IAddSecretStorageKeyOpts;
|
||||
privateKey: Uint8Array;
|
||||
encodedPrivateKey: string;
|
||||
encodedPrivateKey?: string;
|
||||
}
|
||||
|
||||
export interface ICreateSecretStorageOpts {
|
||||
@@ -125,12 +118,13 @@ export interface IPassphraseInfo {
|
||||
algorithm: "m.pbkdf2";
|
||||
iterations: number;
|
||||
salt: string;
|
||||
bits: number;
|
||||
bits?: number;
|
||||
}
|
||||
|
||||
export interface IAddSecretStorageKeyOpts {
|
||||
name: string;
|
||||
passphrase: IPassphraseInfo;
|
||||
pubkey: string;
|
||||
passphrase?: IPassphraseInfo;
|
||||
name?: string;
|
||||
key: Uint8Array;
|
||||
}
|
||||
|
||||
@@ -142,7 +136,7 @@ export interface IImportOpts {
|
||||
}
|
||||
|
||||
export interface IImportRoomKeysOpts {
|
||||
progressCallback: (stage: IImportOpts) => void;
|
||||
progressCallback?: (stage: IImportOpts) => void;
|
||||
untrusted?: boolean;
|
||||
source?: string; // TODO: Enum
|
||||
}
|
||||
|
||||
+37
-25
@@ -26,13 +26,13 @@ import { MEGOLM_ALGORITHM, verifySignature } from "./olmlib";
|
||||
import { DeviceInfo } from "./deviceinfo";
|
||||
import { DeviceTrustLevel } from './CrossSigning';
|
||||
import { keyFromPassphrase } from './key_passphrase';
|
||||
import { sleep } from "../utils";
|
||||
import { getCrypto, sleep } from "../utils";
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { encodeRecoveryKey } from './recoverykey';
|
||||
import { encryptAES, decryptAES, calculateKeyCheck } from './aes';
|
||||
import { getCrypto } from '../utils';
|
||||
import { ICurve25519AuthData, IAes256AuthData, IKeyBackupInfo, IKeyBackupSession } from "./keybackup";
|
||||
import { calculateKeyCheck, decryptAES, encryptAES } from './aes';
|
||||
import { IAes256AuthData, ICurve25519AuthData, IKeyBackupInfo, IKeyBackupSession } from "./keybackup";
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
import { CryptoEvent, IMegolmSessionData } from "./index";
|
||||
|
||||
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
|
||||
|
||||
@@ -49,6 +49,8 @@ type SigInfo = {
|
||||
export type TrustInfo = {
|
||||
usable: boolean; // is the backup trusted, true iff there is a sig that is valid & from a trusted device
|
||||
sigs: SigInfo[];
|
||||
// eslint-disable-next-line camelcase
|
||||
trusted_locally?: boolean;
|
||||
};
|
||||
|
||||
export interface IKeyBackupCheck {
|
||||
@@ -85,7 +87,7 @@ interface BackupAlgorithmClass {
|
||||
interface BackupAlgorithm {
|
||||
untrusted: boolean;
|
||||
encryptSession(data: Record<string, any>): Promise<any>;
|
||||
decryptSessions(ciphertexts: Record<string, IKeyBackupSession>): Promise<Record<string, any>[]>;
|
||||
decryptSessions(ciphertexts: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]>;
|
||||
authData: AuthData;
|
||||
keyMatches(key: ArrayLike<number>): Promise<boolean>;
|
||||
free(): void;
|
||||
@@ -152,7 +154,7 @@ export class BackupManager {
|
||||
|
||||
this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey);
|
||||
|
||||
this.baseApis.emit('crypto.keyBackupStatus', true);
|
||||
this.baseApis.emit(CryptoEvent.KeyBackupStatus, true);
|
||||
|
||||
// There may be keys left over from a partially completed backup, so
|
||||
// schedule a send to check.
|
||||
@@ -170,7 +172,7 @@ export class BackupManager {
|
||||
|
||||
this.backupInfo = undefined;
|
||||
|
||||
this.baseApis.emit('crypto.keyBackupStatus', false);
|
||||
this.baseApis.emit(CryptoEvent.KeyBackupStatus, false);
|
||||
}
|
||||
|
||||
public getKeyBackupEnabled(): boolean | null {
|
||||
@@ -183,7 +185,6 @@ export class BackupManager {
|
||||
public async prepareKeyBackupVersion(
|
||||
key?: string | Uint8Array | null,
|
||||
algorithm?: string | undefined,
|
||||
// eslint-disable-next-line camelcase
|
||||
): Promise<IPreparedKeyBackupVersion> {
|
||||
const Algorithm = algorithm ? algorithmsByName[algorithm] : DefaultAlgorithm;
|
||||
if (!Algorithm) {
|
||||
@@ -298,7 +299,7 @@ export class BackupManager {
|
||||
const ret = {
|
||||
usable: false,
|
||||
trusted_locally: false,
|
||||
sigs: [],
|
||||
sigs: [] as SigInfo[],
|
||||
};
|
||||
|
||||
if (
|
||||
@@ -311,14 +312,27 @@ export class BackupManager {
|
||||
return ret;
|
||||
}
|
||||
|
||||
const trustedPubkey = this.baseApis.crypto.sessionStore.getLocalTrustedBackupPubKey();
|
||||
const privKey = await this.baseApis.crypto.getSessionBackupPrivateKey();
|
||||
if (privKey) {
|
||||
let algorithm;
|
||||
try {
|
||||
algorithm = await BackupManager.makeAlgorithm(backupInfo, async () => privKey);
|
||||
|
||||
if ("public_key" in backupInfo.auth_data && backupInfo.auth_data.public_key === trustedPubkey) {
|
||||
logger.info("Backup public key " + trustedPubkey + " is trusted locally");
|
||||
ret.trusted_locally = true;
|
||||
if (await algorithm.keyMatches(privKey)) {
|
||||
logger.info("Backup is trusted locally");
|
||||
ret.trusted_locally = true;
|
||||
}
|
||||
} catch {
|
||||
// do nothing -- if we have an error, then we don't mark it as
|
||||
// locally trusted
|
||||
} finally {
|
||||
if (algorithm) {
|
||||
algorithm.free();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mySigs = backupInfo.auth_data.signatures[this.baseApis.getUserId()] || [];
|
||||
const mySigs = backupInfo.auth_data.signatures[this.baseApis.getUserId()] || {};
|
||||
|
||||
for (const keyId of Object.keys(mySigs)) {
|
||||
const keyIdParts = keyId.split(':');
|
||||
@@ -443,7 +457,7 @@ export class BackupManager {
|
||||
await this.checkKeyBackup();
|
||||
// Backup version has changed or this backup version
|
||||
// has been deleted
|
||||
this.baseApis.crypto.emit("crypto.keyBackupFailed", err.data.errcode);
|
||||
this.baseApis.crypto.emit(CryptoEvent.KeyBackupFailed, err.data.errcode);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -465,14 +479,14 @@ export class BackupManager {
|
||||
* @param {integer} limit Maximum number of keys to back up
|
||||
* @returns {integer} Number of sessions backed up
|
||||
*/
|
||||
private async backupPendingKeys(limit: number): Promise<number> {
|
||||
public async backupPendingKeys(limit: number): Promise<number> {
|
||||
const sessions = await this.baseApis.crypto.cryptoStore.getSessionsNeedingBackup(limit);
|
||||
if (!sessions.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
|
||||
this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
this.baseApis.crypto.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
|
||||
|
||||
const rooms: IKeyBackup["rooms"] = {};
|
||||
for (const session of sessions) {
|
||||
@@ -509,7 +523,7 @@ export class BackupManager {
|
||||
|
||||
await this.baseApis.crypto.cryptoStore.unmarkSessionsNeedingBackup(sessions);
|
||||
remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
|
||||
this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
this.baseApis.crypto.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
|
||||
|
||||
return sessions.length;
|
||||
}
|
||||
@@ -565,7 +579,7 @@ export class BackupManager {
|
||||
);
|
||||
|
||||
const remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
|
||||
this.baseApis.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
this.baseApis.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
|
||||
return remaining;
|
||||
}
|
||||
|
||||
@@ -643,9 +657,7 @@ export class Curve25519 implements BackupAlgorithm {
|
||||
return this.publicKey.encrypt(JSON.stringify(plainText));
|
||||
}
|
||||
|
||||
public async decryptSessions(
|
||||
sessions: Record<string, IKeyBackupSession>,
|
||||
): Promise<Record<string, any>[]> {
|
||||
public async decryptSessions(sessions: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]> {
|
||||
const privKey = await this.getKey();
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
try {
|
||||
@@ -656,7 +668,7 @@ export class Curve25519 implements BackupAlgorithm {
|
||||
throw { errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY };
|
||||
}
|
||||
|
||||
const keys = [];
|
||||
const keys: IMegolmSessionData[] = [];
|
||||
|
||||
for (const [sessionId, sessionData] of Object.entries(sessions)) {
|
||||
try {
|
||||
@@ -775,8 +787,8 @@ export class Aes256 implements BackupAlgorithm {
|
||||
return await encryptAES(JSON.stringify(plainText), this.key, data.session_id);
|
||||
}
|
||||
|
||||
async decryptSessions(sessions: Record<string, IKeyBackupSession>): Promise<Record<string, any>[]> {
|
||||
const keys = [];
|
||||
async decryptSessions(sessions: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]> {
|
||||
const keys: IMegolmSessionData[] = [];
|
||||
|
||||
for (const [sessionId, sessionData] of Object.entries(sessions)) {
|
||||
try {
|
||||
|
||||
+15
-13
@@ -14,15 +14,16 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import anotherjson from "another-json";
|
||||
|
||||
import { decodeBase64, encodeBase64 } from './olmlib';
|
||||
import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store';
|
||||
import { decryptAES, encryptAES } from './aes';
|
||||
import anotherjson from "another-json";
|
||||
import { logger } from '../logger';
|
||||
import { ISecretStorageKeyInfo } from "./api";
|
||||
|
||||
// FIXME: these types should eventually go in a different file
|
||||
type Signatures = Record<string, Record<string, string>>;
|
||||
import { Crypto } from "./index";
|
||||
import { Method } from "../http-api";
|
||||
import { ISignatures } from "../@types/signed";
|
||||
|
||||
export interface IDehydratedDevice {
|
||||
device_id: string; // eslint-disable-line camelcase
|
||||
@@ -41,13 +42,13 @@ export interface IDeviceKeys {
|
||||
device_id: string; // eslint-disable-line camelcase
|
||||
user_id: string; // eslint-disable-line camelcase
|
||||
keys: Record<string, string>;
|
||||
signatures?: Signatures;
|
||||
signatures?: ISignatures;
|
||||
}
|
||||
|
||||
export interface IOneTimeKey {
|
||||
key: string;
|
||||
fallback?: boolean;
|
||||
signatures?: Signatures;
|
||||
signatures?: ISignatures;
|
||||
}
|
||||
|
||||
export const DEHYDRATION_ALGORITHM = "org.matrix.msc2697.v1.olm.libolm_pickle";
|
||||
@@ -60,7 +61,7 @@ export class DehydrationManager {
|
||||
private key: Uint8Array;
|
||||
private keyInfo: {[props: string]: any};
|
||||
private deviceDisplayName: string;
|
||||
constructor(private crypto) {
|
||||
constructor(private readonly crypto: Crypto) {
|
||||
this.getDehydrationKeyFromCache();
|
||||
}
|
||||
async getDehydrationKeyFromCache(): Promise<void> {
|
||||
@@ -73,7 +74,7 @@ export class DehydrationManager {
|
||||
async (result) => {
|
||||
if (result) {
|
||||
const { key, keyInfo, deviceDisplayName, time } = result;
|
||||
const pickleKey = Buffer.from(this.crypto.olmDevice._pickleKey);
|
||||
const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey);
|
||||
const decrypted = await decryptAES(key, pickleKey, DEHYDRATION_ALGORITHM);
|
||||
this.key = decodeBase64(decrypted);
|
||||
this.keyInfo = keyInfo;
|
||||
@@ -158,7 +159,7 @@ export class DehydrationManager {
|
||||
this.timeoutId = undefined;
|
||||
}
|
||||
try {
|
||||
const pickleKey = Buffer.from(this.crypto.olmDevice._pickleKey);
|
||||
const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey);
|
||||
|
||||
// update the crypto store with the timestamp
|
||||
const key = await encryptAES(encodeBase64(this.key), pickleKey, DEHYDRATION_ALGORITHM);
|
||||
@@ -205,9 +206,10 @@ export class DehydrationManager {
|
||||
}
|
||||
|
||||
logger.log("Uploading account to server");
|
||||
const dehydrateResult = await this.crypto.baseApis.http.authedRequest(
|
||||
// eslint-disable-next-line camelcase
|
||||
const dehydrateResult = await this.crypto.baseApis.http.authedRequest<{ device_id: string }>(
|
||||
undefined,
|
||||
"PUT",
|
||||
Method.Put,
|
||||
"/dehydrated_device",
|
||||
undefined,
|
||||
{
|
||||
@@ -242,7 +244,7 @@ export class DehydrationManager {
|
||||
}
|
||||
|
||||
logger.log("Preparing one-time keys");
|
||||
const oneTimeKeys = {};
|
||||
const oneTimeKeys: Record<string, IOneTimeKey> = {};
|
||||
for (const [keyId, key] of Object.entries(otks.curve25519)) {
|
||||
const k: IOneTimeKey = { key };
|
||||
const signature = account.sign(anotherjson.stringify(k));
|
||||
@@ -270,7 +272,7 @@ export class DehydrationManager {
|
||||
logger.log("Uploading keys to server");
|
||||
await this.crypto.baseApis.http.authedRequest(
|
||||
undefined,
|
||||
"POST",
|
||||
Method.Post,
|
||||
"/keys/upload/" + encodeURI(deviceId),
|
||||
undefined,
|
||||
{
|
||||
|
||||
@@ -68,7 +68,7 @@ export class DeviceInfo {
|
||||
*
|
||||
* @return {module:crypto~DeviceInfo} new DeviceInfo
|
||||
*/
|
||||
public static fromStorage(obj: IDevice, deviceId: string): DeviceInfo {
|
||||
public static fromStorage(obj: Partial<IDevice>, deviceId: string): DeviceInfo {
|
||||
const res = new DeviceInfo(deviceId);
|
||||
for (const prop in obj) {
|
||||
if (obj.hasOwnProperty(prop)) {
|
||||
|
||||
+258
-164
@@ -22,27 +22,36 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import anotherjson from "another-json";
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { ReEmitter } from '../ReEmitter';
|
||||
import { TypedReEmitter } from '../ReEmitter';
|
||||
import { logger } from '../logger';
|
||||
import { OlmDevice } from "./OlmDevice";
|
||||
import { IExportedDevice, OlmDevice } from "./OlmDevice";
|
||||
import * as olmlib from "./olmlib";
|
||||
import { DeviceInfoMap, DeviceList } from "./DeviceList";
|
||||
import { DeviceInfo, IDevice } from "./deviceinfo";
|
||||
import type { DecryptionAlgorithm, EncryptionAlgorithm } from "./algorithms";
|
||||
import * as algorithms from "./algorithms";
|
||||
import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from './CrossSigning';
|
||||
import { EncryptionSetupBuilder } from "./EncryptionSetup";
|
||||
import {
|
||||
IAccountDataClient,
|
||||
ISecretRequest,
|
||||
SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
SecretStorage,
|
||||
SecretStorageKeyTuple,
|
||||
ISecretRequest,
|
||||
SecretStorageKeyObject,
|
||||
SecretStorageKeyTuple,
|
||||
} from './SecretStorage';
|
||||
import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from "./api";
|
||||
import {
|
||||
IAddSecretStorageKeyOpts,
|
||||
ICreateSecretStorageOpts,
|
||||
IEncryptedEventInfo,
|
||||
IImportRoomKeysOpts,
|
||||
IRecoveryKey,
|
||||
ISecretStorageKeyInfo,
|
||||
} from "./api";
|
||||
import { OutgoingRoomKeyRequestManager } from './OutgoingRoomKeyRequestManager';
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { VerificationBase } from "./verification/Base";
|
||||
import { ReciprocateQRCode, SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD } from './verification/QRCode';
|
||||
import { SAS as SASVerification } from './verification/SAS';
|
||||
import { keyFromPassphrase } from './key_passphrase';
|
||||
@@ -52,20 +61,28 @@ import { InRoomChannel, InRoomRequests } from "./verification/request/InRoomChan
|
||||
import { ToDeviceChannel, ToDeviceRequests } from "./verification/request/ToDeviceChannel";
|
||||
import { IllegalMethod } from "./verification/IllegalMethod";
|
||||
import { KeySignatureUploadError } from "../errors";
|
||||
import { decryptAES, encryptAES, calculateKeyCheck } from './aes';
|
||||
import { calculateKeyCheck, decryptAES, encryptAES } from './aes';
|
||||
import { DehydrationManager, IDeviceKeys, IOneTimeKey } from './dehydration';
|
||||
import { BackupManager } from "./backup";
|
||||
import { IStore } from "../store";
|
||||
import { Room } from "../models/room";
|
||||
import { RoomMember } from "../models/room-member";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { MatrixClient, IKeysUploadResponse, SessionStore, ISignedKey } from "../client";
|
||||
import type { EncryptionAlgorithm, DecryptionAlgorithm } from "./algorithms/base";
|
||||
import { Room, RoomEvent } from "../models/room";
|
||||
import { RoomMember, RoomMemberEvent } from "../models/room-member";
|
||||
import { EventStatus, IClearEvent, IEvent, MatrixEvent, MatrixEventEvent } from "../models/event";
|
||||
import {
|
||||
ClientEvent,
|
||||
ICrossSigningKey,
|
||||
IKeysUploadResponse,
|
||||
ISignedKey,
|
||||
IUploadKeySignaturesResponse,
|
||||
MatrixClient,
|
||||
SessionStore,
|
||||
} from "../client";
|
||||
import type { IRoomEncryption, RoomList } from "./RoomList";
|
||||
import { IRecoveryKey, IEncryptedEventInfo } from "./api";
|
||||
import { IKeyBackupInfo } from "./keybackup";
|
||||
import { ISyncStateData } from "../sync";
|
||||
import { CryptoStore } from "./store/base";
|
||||
import { IVerificationChannel } from "./verification/request/Channel";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
|
||||
@@ -84,12 +101,12 @@ const defaultVerificationMethods = {
|
||||
* verification method names
|
||||
*/
|
||||
// legacy export identifier
|
||||
export enum verificationMethods {
|
||||
RECIPROCATE_QR_CODE = ReciprocateQRCode.NAME,
|
||||
SAS = SASVerification.NAME,
|
||||
}
|
||||
export const verificationMethods = {
|
||||
RECIPROCATE_QR_CODE: ReciprocateQRCode.NAME,
|
||||
SAS: SASVerification.NAME,
|
||||
};
|
||||
|
||||
export type VerificationMethod = verificationMethods;
|
||||
export type VerificationMethod = keyof typeof verificationMethods | string;
|
||||
|
||||
export function isCryptoAvailable(): boolean {
|
||||
return Boolean(global.Olm);
|
||||
@@ -98,7 +115,7 @@ export function isCryptoAvailable(): boolean {
|
||||
const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000;
|
||||
|
||||
interface IInitOpts {
|
||||
exportedOlmDevice?: any; // TODO types
|
||||
exportedOlmDevice?: IExportedDevice;
|
||||
pickleKey?: string;
|
||||
}
|
||||
|
||||
@@ -107,17 +124,6 @@ export interface IBootstrapCrossSigningOpts {
|
||||
authUploadDeviceSigningKeys?(makeRequest: (authData: any) => {}): Promise<void>;
|
||||
}
|
||||
|
||||
interface IBootstrapSecretStorageOpts {
|
||||
keyBackupInfo?: any; // TODO types
|
||||
setupNewKeyBackup?: boolean;
|
||||
setupNewSecretStorage?: boolean;
|
||||
createSecretStorageKey?(): Promise<{
|
||||
keyInfo?: any; // TODO types
|
||||
privateKey?: Uint8Array;
|
||||
}>;
|
||||
getKeyBackupPassphrase?(): Promise<Uint8Array | null>;
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
interface IRoomKey {
|
||||
room_id: string;
|
||||
@@ -130,13 +136,14 @@ export interface IRoomKeyRequestBody extends IRoomKey {
|
||||
}
|
||||
|
||||
export interface IMegolmSessionData {
|
||||
[key: string]: any; // extensible
|
||||
sender_key: string;
|
||||
forwarding_curve25519_key_chain: string[];
|
||||
sender_claimed_keys: Record<string, string>;
|
||||
room_id: string;
|
||||
session_id: string;
|
||||
session_key: string;
|
||||
algorithm: string;
|
||||
algorithm?: string;
|
||||
untrusted?: boolean;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
@@ -181,18 +188,63 @@ interface ISignableObject {
|
||||
}
|
||||
|
||||
export interface IEventDecryptionResult {
|
||||
clearEvent: object;
|
||||
clearEvent: IClearEvent;
|
||||
forwardingCurve25519KeyChain?: string[];
|
||||
senderCurve25519Key?: string;
|
||||
claimedEd25519Key?: string;
|
||||
forwardingCurve25519KeyChain?: string[];
|
||||
untrusted?: boolean;
|
||||
}
|
||||
|
||||
export class Crypto extends EventEmitter {
|
||||
export interface IRequestsMap {
|
||||
getRequest(event: MatrixEvent): VerificationRequest;
|
||||
getRequestByChannel(channel: IVerificationChannel): VerificationRequest;
|
||||
setRequest(event: MatrixEvent, request: VerificationRequest): void;
|
||||
setRequestByChannel(channel: IVerificationChannel, request: VerificationRequest): void;
|
||||
}
|
||||
|
||||
export enum CryptoEvent {
|
||||
DeviceVerificationChanged = "deviceVerificationChanged",
|
||||
UserTrustStatusChanged = "userTrustStatusChanged",
|
||||
UserCrossSigningUpdated = "userCrossSigningUpdated",
|
||||
RoomKeyRequest = "crypto.roomKeyRequest",
|
||||
RoomKeyRequestCancellation = "crypto.roomKeyRequestCancellation",
|
||||
KeyBackupStatus = "crypto.keyBackupStatus",
|
||||
KeyBackupFailed = "crypto.keyBackupFailed",
|
||||
KeyBackupSessionsRemaining = "crypto.keyBackupSessionsRemaining",
|
||||
KeySignatureUploadFailure = "crypto.keySignatureUploadFailure",
|
||||
VerificationRequest = "crypto.verification.request",
|
||||
Warning = "crypto.warning",
|
||||
WillUpdateDevices = "crypto.willUpdateDevices",
|
||||
DevicesUpdated = "crypto.devicesUpdated",
|
||||
KeysChanged = "crossSigning.keysChanged",
|
||||
}
|
||||
|
||||
export type CryptoEventHandlerMap = {
|
||||
[CryptoEvent.DeviceVerificationChanged]: (userId: string, deviceId: string, device: DeviceInfo) => void;
|
||||
[CryptoEvent.UserTrustStatusChanged]: (userId: string, trustLevel: UserTrustLevel) => void;
|
||||
[CryptoEvent.RoomKeyRequest]: (request: IncomingRoomKeyRequest) => void;
|
||||
[CryptoEvent.RoomKeyRequestCancellation]: (request: IncomingRoomKeyRequestCancellation) => void;
|
||||
[CryptoEvent.KeyBackupStatus]: (enabled: boolean) => void;
|
||||
[CryptoEvent.KeyBackupFailed]: (errcode: string) => void;
|
||||
[CryptoEvent.KeyBackupSessionsRemaining]: (remaining: number) => void;
|
||||
[CryptoEvent.KeySignatureUploadFailure]: (
|
||||
failures: IUploadKeySignaturesResponse["failures"],
|
||||
source: "checkOwnCrossSigningTrust" | "afterCrossSigningLocalKeyChange" | "setDeviceVerification",
|
||||
upload: (opts: { shouldEmit: boolean }) => Promise<void>
|
||||
) => void;
|
||||
[CryptoEvent.VerificationRequest]: (request: VerificationRequest<any>) => void;
|
||||
[CryptoEvent.Warning]: (type: string) => void;
|
||||
[CryptoEvent.KeysChanged]: (data: {}) => void;
|
||||
[CryptoEvent.WillUpdateDevices]: (users: string[], initialFetch: boolean) => void;
|
||||
[CryptoEvent.DevicesUpdated]: (users: string[], initialFetch: boolean) => void;
|
||||
[CryptoEvent.UserCrossSigningUpdated]: (userId: string) => void;
|
||||
};
|
||||
|
||||
export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap> {
|
||||
/**
|
||||
* @return {string} The version of Olm.
|
||||
*/
|
||||
static getOlmVersion(): string {
|
||||
static getOlmVersion(): [number, number, number] {
|
||||
return OlmDevice.getOlmVersion();
|
||||
}
|
||||
|
||||
@@ -203,9 +255,9 @@ export class Crypto extends EventEmitter {
|
||||
public readonly dehydrationManager: DehydrationManager;
|
||||
public readonly secretStorage: SecretStorage;
|
||||
|
||||
private readonly reEmitter: ReEmitter;
|
||||
private readonly verificationMethods: any; // TODO types
|
||||
private readonly supportedAlgorithms: string[];
|
||||
private readonly reEmitter: TypedReEmitter<CryptoEvent, CryptoEventHandlerMap>;
|
||||
private readonly verificationMethods: Map<VerificationMethod, typeof VerificationBase>;
|
||||
public readonly supportedAlgorithms: string[];
|
||||
private readonly outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager;
|
||||
private readonly toDeviceVerificationRequests: ToDeviceRequests;
|
||||
public readonly inRoomVerificationRequests: InRoomRequests;
|
||||
@@ -257,6 +309,7 @@ export class Crypto extends EventEmitter {
|
||||
|
||||
private oneTimeKeyCount: number;
|
||||
private needsNewFallback: boolean;
|
||||
private fallbackCleanup?: number; // setTimeout ID
|
||||
|
||||
/**
|
||||
* Cryptography bits
|
||||
@@ -291,15 +344,15 @@ export class Crypto extends EventEmitter {
|
||||
constructor(
|
||||
public readonly baseApis: MatrixClient,
|
||||
public readonly sessionStore: SessionStore,
|
||||
private readonly userId: string,
|
||||
public readonly userId: string,
|
||||
private readonly deviceId: string,
|
||||
private readonly clientStore: IStore,
|
||||
public readonly cryptoStore: CryptoStore,
|
||||
private readonly roomList: RoomList,
|
||||
verificationMethods: any[], // TODO types
|
||||
verificationMethods: Array<keyof typeof defaultVerificationMethods | typeof VerificationBase>,
|
||||
) {
|
||||
super();
|
||||
this.reEmitter = new ReEmitter(this);
|
||||
this.reEmitter = new TypedReEmitter(this);
|
||||
|
||||
if (verificationMethods) {
|
||||
this.verificationMethods = new Map();
|
||||
@@ -308,20 +361,21 @@ export class Crypto extends EventEmitter {
|
||||
if (defaultVerificationMethods[method]) {
|
||||
this.verificationMethods.set(
|
||||
method,
|
||||
defaultVerificationMethods[method],
|
||||
<typeof VerificationBase>defaultVerificationMethods[method],
|
||||
);
|
||||
}
|
||||
} else if (method.NAME) {
|
||||
} else if (method["NAME"]) {
|
||||
this.verificationMethods.set(
|
||||
method.NAME,
|
||||
method,
|
||||
method["NAME"],
|
||||
method as typeof VerificationBase,
|
||||
);
|
||||
} else {
|
||||
logger.warn(`Excluding unknown verification method ${method}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.verificationMethods = defaultVerificationMethods;
|
||||
this.verificationMethods =
|
||||
new Map(Object.entries(defaultVerificationMethods)) as Map<VerificationMethod, typeof VerificationBase>;
|
||||
}
|
||||
|
||||
this.backupManager = new BackupManager(baseApis, async () => {
|
||||
@@ -359,8 +413,8 @@ export class Crypto extends EventEmitter {
|
||||
|
||||
// XXX: This isn't removed at any point, but then none of the event listeners
|
||||
// this class sets seem to be removed at any point... :/
|
||||
this.deviceList.on('userCrossSigningUpdated', this.onDeviceListUserCrossSigningUpdated);
|
||||
this.reEmitter.reEmit(this.deviceList, ["crypto.devicesUpdated", "crypto.willUpdateDevices"]);
|
||||
this.deviceList.on(CryptoEvent.UserCrossSigningUpdated, this.onDeviceListUserCrossSigningUpdated);
|
||||
this.reEmitter.reEmit(this.deviceList, [CryptoEvent.DevicesUpdated, CryptoEvent.WillUpdateDevices]);
|
||||
|
||||
this.supportedAlgorithms = Object.keys(algorithms.DECRYPTION_CLASSES);
|
||||
|
||||
@@ -376,7 +430,7 @@ export class Crypto extends EventEmitter {
|
||||
|
||||
this.crossSigningInfo = new CrossSigningInfo(userId, cryptoCallbacks, cacheCallbacks);
|
||||
// Yes, we pass the client twice here: see SecretStorage
|
||||
this.secretStorage = new SecretStorage(baseApis, cryptoCallbacks, baseApis);
|
||||
this.secretStorage = new SecretStorage(baseApis as IAccountDataClient, cryptoCallbacks, baseApis);
|
||||
this.dehydrationManager = new DehydrationManager(this);
|
||||
|
||||
// Assuming no app-supplied callback, default to getting from SSSS.
|
||||
@@ -488,7 +542,7 @@ export class Crypto extends EventEmitter {
|
||||
deviceTrust.isCrossSigningVerified()
|
||||
) {
|
||||
const deviceObj = this.deviceList.getStoredDevice(userId, deviceId);
|
||||
this.emit("deviceVerificationChanged", userId, deviceId, deviceObj);
|
||||
this.emit(CryptoEvent.DeviceVerificationChanged, userId, deviceId, deviceObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -504,7 +558,7 @@ export class Crypto extends EventEmitter {
|
||||
* recovery key which should be disposed of after displaying to the user,
|
||||
* and raw private key to avoid round tripping if needed.
|
||||
*/
|
||||
public async createRecoveryKeyFromPassphrase(password: string): Promise<IRecoveryKey> {
|
||||
public async createRecoveryKeyFromPassphrase(password?: string): Promise<IRecoveryKey> {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
try {
|
||||
const keyInfo: Partial<IRecoveryKey["keyInfo"]> = {};
|
||||
@@ -760,12 +814,12 @@ export class Crypto extends EventEmitter {
|
||||
*/
|
||||
// TODO this does not resolve with what it says it does
|
||||
public async bootstrapSecretStorage({
|
||||
createSecretStorageKey = async () => ({ }),
|
||||
createSecretStorageKey = async () => ({} as IRecoveryKey),
|
||||
keyBackupInfo,
|
||||
setupNewKeyBackup,
|
||||
setupNewSecretStorage,
|
||||
getKeyBackupPassphrase,
|
||||
}: IBootstrapSecretStorageOpts = {}) {
|
||||
}: ICreateSecretStorageOpts = {}) {
|
||||
logger.log("Bootstrapping Secure Secret Storage");
|
||||
const delegateCryptoCallbacks = this.baseApis.cryptoCallbacks;
|
||||
const builder = new EncryptionSetupBuilder(
|
||||
@@ -781,8 +835,7 @@ export class Crypto extends EventEmitter {
|
||||
let newKeyId = null;
|
||||
|
||||
// create a new SSSS key and set it as default
|
||||
const createSSSS = async (opts, privateKey: Uint8Array) => {
|
||||
opts = opts || {};
|
||||
const createSSSS = async (opts: IAddSecretStorageKeyOpts, privateKey: Uint8Array) => {
|
||||
if (privateKey) {
|
||||
opts.key = privateKey;
|
||||
}
|
||||
@@ -798,7 +851,7 @@ export class Crypto extends EventEmitter {
|
||||
return keyId;
|
||||
};
|
||||
|
||||
const ensureCanCheckPassphrase = async (keyId, keyInfo) => {
|
||||
const ensureCanCheckPassphrase = async (keyId: string, keyInfo: ISecretStorageKeyInfo) => {
|
||||
if (!keyInfo.mac) {
|
||||
const key = await this.baseApis.cryptoCallbacks.getSecretStorageKey(
|
||||
{ keys: { [keyId]: keyInfo } }, "",
|
||||
@@ -817,7 +870,7 @@ export class Crypto extends EventEmitter {
|
||||
}
|
||||
};
|
||||
|
||||
const signKeyBackupWithCrossSigning = async (keyBackupAuthData) => {
|
||||
const signKeyBackupWithCrossSigning = async (keyBackupAuthData: IKeyBackupInfo["auth_data"]) => {
|
||||
if (
|
||||
this.crossSigningInfo.getId() &&
|
||||
await this.crossSigningInfo.isStoredInKeyCache("master")
|
||||
@@ -878,7 +931,7 @@ export class Crypto extends EventEmitter {
|
||||
const backupKey = await this.getSessionBackupPrivateKey() || await getKeyBackupPassphrase();
|
||||
|
||||
// create a new SSSS key and use the backup key as the new SSSS key
|
||||
const opts: any = {}; // TODO types
|
||||
const opts = {} as IAddSecretStorageKeyOpts;
|
||||
|
||||
if (
|
||||
keyBackupInfo.auth_data.private_key_salt &&
|
||||
@@ -896,9 +949,7 @@ export class Crypto extends EventEmitter {
|
||||
newKeyId = await createSSSS(opts, backupKey);
|
||||
|
||||
// store the backup key in secret storage
|
||||
await secretStorage.store(
|
||||
"m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId],
|
||||
);
|
||||
await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId]);
|
||||
|
||||
// The backup is trusted because the user provided the private key.
|
||||
// Sign the backup with the cross-signing key so the key backup can
|
||||
@@ -1028,7 +1079,7 @@ export class Crypto extends EventEmitter {
|
||||
public isSecretStored(
|
||||
name: string,
|
||||
checkKey?: boolean,
|
||||
): Promise<Record<string, ISecretStorageKeyInfo>> {
|
||||
): Promise<Record<string, ISecretStorageKeyInfo> | null> {
|
||||
return this.secretStorage.isStored(name, checkKey);
|
||||
}
|
||||
|
||||
@@ -1039,7 +1090,7 @@ export class Crypto extends EventEmitter {
|
||||
return this.secretStorage.request(name, devices);
|
||||
}
|
||||
|
||||
public getDefaultSecretStorageKeyId(): Promise<string> {
|
||||
public getDefaultSecretStorageKeyId(): Promise<string | null> {
|
||||
return this.secretStorage.getDefaultKeyId();
|
||||
}
|
||||
|
||||
@@ -1097,7 +1148,7 @@ export class Crypto extends EventEmitter {
|
||||
await this.storeSessionBackupPrivateKey(key);
|
||||
}
|
||||
if (key && key.ciphertext) {
|
||||
const pickleKey = Buffer.from(this.olmDevice._pickleKey);
|
||||
const pickleKey = Buffer.from(this.olmDevice.pickleKey);
|
||||
const decrypted = await decryptAES(key, pickleKey, "m.megolm_backup.v1");
|
||||
key = olmlib.decodeBase64(decrypted);
|
||||
}
|
||||
@@ -1113,7 +1164,7 @@ export class Crypto extends EventEmitter {
|
||||
if (!(key instanceof Uint8Array)) {
|
||||
throw new Error(`storeSessionBackupPrivateKey expects Uint8Array, got ${key}`);
|
||||
}
|
||||
const pickleKey = Buffer.from(this.olmDevice._pickleKey);
|
||||
const pickleKey = Buffer.from(this.olmDevice.pickleKey);
|
||||
const encryptedKey = await encryptAES(olmlib.encodeBase64(key), pickleKey, "m.megolm_backup.v1");
|
||||
return this.cryptoStore.doTxn(
|
||||
'readwrite',
|
||||
@@ -1159,7 +1210,7 @@ export class Crypto extends EventEmitter {
|
||||
const signedDevice = await this.crossSigningInfo.signDevice(this.userId, device);
|
||||
logger.info(`Starting background key sig upload for ${this.deviceId}`);
|
||||
|
||||
const upload = ({ shouldEmit }) => {
|
||||
const upload = ({ shouldEmit = false }) => {
|
||||
return this.baseApis.uploadKeySignatures({
|
||||
[this.userId]: {
|
||||
[this.deviceId]: signedDevice,
|
||||
@@ -1169,7 +1220,7 @@ export class Crypto extends EventEmitter {
|
||||
if (Object.keys(failures || []).length > 0) {
|
||||
if (shouldEmit) {
|
||||
this.baseApis.emit(
|
||||
"crypto.keySignatureUploadFailure",
|
||||
CryptoEvent.KeySignatureUploadFailure,
|
||||
failures,
|
||||
"afterCrossSigningLocalKeyChange",
|
||||
upload, // continuation
|
||||
@@ -1195,7 +1246,7 @@ export class Crypto extends EventEmitter {
|
||||
|
||||
// Check all users for signatures if upgrade callback present
|
||||
// FIXME: do this in batches
|
||||
const users = {};
|
||||
const users: Record<string, IDeviceVerificationUpgrade> = {};
|
||||
for (const [userId, crossSigningInfo]
|
||||
of Object.entries(this.deviceList.crossSigningInfo)) {
|
||||
const upgradeInfo = await this.checkForDeviceVerificationUpgrade(
|
||||
@@ -1272,7 +1323,7 @@ export class Crypto extends EventEmitter {
|
||||
*/
|
||||
private async checkForValidDeviceSignature(
|
||||
userId: string,
|
||||
key: any, // TODO types
|
||||
key: ICrossSigningKey,
|
||||
devices: Record<string, IDevice>,
|
||||
): Promise<string[]> {
|
||||
const deviceIds: string[] = [];
|
||||
@@ -1395,11 +1446,10 @@ export class Crypto extends EventEmitter {
|
||||
// that reset the keys
|
||||
this.storeTrustedSelfKeys(null);
|
||||
// emit cross-signing has been disabled
|
||||
this.emit("crossSigning.keysChanged", {});
|
||||
this.emit(CryptoEvent.KeysChanged, {});
|
||||
// as the trust for our own user has changed,
|
||||
// also emit an event for this
|
||||
this.emit("userTrustStatusChanged",
|
||||
this.userId, this.checkUserTrust(userId));
|
||||
this.emit(CryptoEvent.UserTrustStatusChanged, this.userId, this.checkUserTrust(userId));
|
||||
}
|
||||
} else {
|
||||
await this.checkDeviceVerifications(userId);
|
||||
@@ -1414,7 +1464,7 @@ export class Crypto extends EventEmitter {
|
||||
this.deviceList.setRawStoredCrossSigningForUser(userId, crossSigning.toStorage());
|
||||
}
|
||||
|
||||
this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId));
|
||||
this.emit(CryptoEvent.UserTrustStatusChanged, userId, this.checkUserTrust(userId));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1493,7 +1543,7 @@ export class Crypto extends EventEmitter {
|
||||
!crossSigningPrivateKeys.has("user_signing")
|
||||
);
|
||||
|
||||
const keySignatures = {};
|
||||
const keySignatures: Record<string, ISignedKey> = {};
|
||||
|
||||
if (selfSigningChanged) {
|
||||
logger.info("Got new self-signing key", newCrossSigning.getId("self_signing"));
|
||||
@@ -1548,7 +1598,7 @@ export class Crypto extends EventEmitter {
|
||||
// We may have existing signatures from deleted devices, which will cause
|
||||
// the entire upload to fail.
|
||||
keySignatures[this.crossSigningInfo.getId()] = Object.assign(
|
||||
{},
|
||||
{} as ISignedKey,
|
||||
masterKey,
|
||||
{
|
||||
signatures: {
|
||||
@@ -1562,7 +1612,7 @@ export class Crypto extends EventEmitter {
|
||||
|
||||
const keysToUpload = Object.keys(keySignatures);
|
||||
if (keysToUpload.length) {
|
||||
const upload = ({ shouldEmit }) => {
|
||||
const upload = ({ shouldEmit = false }) => {
|
||||
logger.info(`Starting background key sig upload for ${keysToUpload}`);
|
||||
return this.baseApis.uploadKeySignatures({ [this.userId]: keySignatures })
|
||||
.then((response) => {
|
||||
@@ -1571,7 +1621,7 @@ export class Crypto extends EventEmitter {
|
||||
if (Object.keys(failures || []).length > 0) {
|
||||
if (shouldEmit) {
|
||||
this.baseApis.emit(
|
||||
"crypto.keySignatureUploadFailure",
|
||||
CryptoEvent.KeySignatureUploadFailure,
|
||||
failures,
|
||||
"checkOwnCrossSigningTrust",
|
||||
upload,
|
||||
@@ -1589,10 +1639,10 @@ export class Crypto extends EventEmitter {
|
||||
upload({ shouldEmit: true });
|
||||
}
|
||||
|
||||
this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId));
|
||||
this.emit(CryptoEvent.UserTrustStatusChanged, userId, this.checkUserTrust(userId));
|
||||
|
||||
if (masterChanged) {
|
||||
this.baseApis.emit("crossSigning.keysChanged", {});
|
||||
this.emit(CryptoEvent.KeysChanged, {});
|
||||
await this.afterCrossSigningLocalKeyChange();
|
||||
}
|
||||
|
||||
@@ -1607,7 +1657,7 @@ export class Crypto extends EventEmitter {
|
||||
*
|
||||
* @param {object} keys The new trusted set of keys
|
||||
*/
|
||||
private async storeTrustedSelfKeys(keys: any): Promise<void> { // TODO types
|
||||
private async storeTrustedSelfKeys(keys: Record<string, ICrossSigningKey>): Promise<void> {
|
||||
if (keys) {
|
||||
this.crossSigningInfo.setKeys(keys);
|
||||
} else {
|
||||
@@ -1679,18 +1729,14 @@ export class Crypto extends EventEmitter {
|
||||
* @param {external:EventEmitter} eventEmitter event source where we can register
|
||||
* for event notifications
|
||||
*/
|
||||
public registerEventHandlers(eventEmitter: EventEmitter): void {
|
||||
eventEmitter.on("RoomMember.membership", (event: MatrixEvent, member: RoomMember, oldMembership?: string) => {
|
||||
try {
|
||||
this.onRoomMembership(event, member, oldMembership);
|
||||
} catch (e) {
|
||||
logger.error("Error handling membership change:", e);
|
||||
}
|
||||
});
|
||||
|
||||
eventEmitter.on("toDeviceEvent", this.onToDeviceEvent);
|
||||
eventEmitter.on("Room.timeline", this.onTimelineEvent);
|
||||
eventEmitter.on("Event.decrypted", this.onTimelineEvent);
|
||||
public registerEventHandlers(eventEmitter: TypedEventEmitter<
|
||||
RoomMemberEvent.Membership | ClientEvent.ToDeviceEvent | RoomEvent.Timeline | MatrixEventEvent.Decrypted,
|
||||
any
|
||||
>): void {
|
||||
eventEmitter.on(RoomMemberEvent.Membership, this.onMembership);
|
||||
eventEmitter.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
|
||||
eventEmitter.on(RoomEvent.Timeline, this.onTimelineEvent);
|
||||
eventEmitter.on(MatrixEventEvent.Decrypted, this.onTimelineEvent);
|
||||
}
|
||||
|
||||
/** Start background processes related to crypto */
|
||||
@@ -1862,8 +1908,23 @@ export class Crypto extends EventEmitter {
|
||||
}
|
||||
|
||||
if (this.getNeedsNewFallback()) {
|
||||
logger.info("generating fallback key");
|
||||
await this.olmDevice.generateFallbackKey();
|
||||
const fallbackKeys = await this.olmDevice.getFallbackKey();
|
||||
// if fallbackKeys is non-empty, we've already generated a
|
||||
// fallback key, but it hasn't been published yet, so we
|
||||
// can use that instead of generating a new one
|
||||
if (!fallbackKeys.curve25519 ||
|
||||
Object.keys(fallbackKeys.curve25519).length == 0) {
|
||||
logger.info("generating fallback key");
|
||||
if (this.fallbackCleanup) {
|
||||
// cancel any pending fallback cleanup because generating
|
||||
// a new fallback key will already drop the old fallback
|
||||
// that would have been dropped, and we don't want to kill
|
||||
// the current key
|
||||
clearTimeout(this.fallbackCleanup);
|
||||
delete this.fallbackCleanup;
|
||||
}
|
||||
await this.olmDevice.generateFallbackKey();
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("calling uploadOneTimeKeys");
|
||||
@@ -1910,9 +1971,10 @@ export class Crypto extends EventEmitter {
|
||||
private async uploadOneTimeKeys() {
|
||||
const promises = [];
|
||||
|
||||
const fallbackJson: Record<string, IOneTimeKey> = {};
|
||||
let fallbackJson: Record<string, IOneTimeKey>;
|
||||
if (this.getNeedsNewFallback()) {
|
||||
const fallbackKeys = await this.olmDevice.getFallbackKey() as Record<string, Record<string, string>>;
|
||||
fallbackJson = {};
|
||||
const fallbackKeys = await this.olmDevice.getFallbackKey();
|
||||
for (const [keyId, key] of Object.entries(fallbackKeys.curve25519)) {
|
||||
const k = { key, fallback: true };
|
||||
fallbackJson["signed_curve25519:" + keyId] = k;
|
||||
@@ -1922,7 +1984,7 @@ export class Crypto extends EventEmitter {
|
||||
}
|
||||
|
||||
const oneTimeKeys = await this.olmDevice.getOneTimeKeys();
|
||||
const oneTimeJson = {};
|
||||
const oneTimeJson: Record<string, { key: string }> = {};
|
||||
|
||||
for (const keyId in oneTimeKeys.curve25519) {
|
||||
if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) {
|
||||
@@ -1936,10 +1998,23 @@ export class Crypto extends EventEmitter {
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const res = await this.baseApis.uploadKeysRequest({
|
||||
const requestBody: Record<string, any> = {
|
||||
"one_time_keys": oneTimeJson,
|
||||
"org.matrix.msc2732.fallback_keys": fallbackJson,
|
||||
});
|
||||
};
|
||||
|
||||
if (fallbackJson) {
|
||||
requestBody["org.matrix.msc2732.fallback_keys"] = fallbackJson;
|
||||
requestBody["fallback_keys"] = fallbackJson;
|
||||
}
|
||||
|
||||
const res = await this.baseApis.uploadKeysRequest(requestBody);
|
||||
|
||||
if (fallbackJson) {
|
||||
this.fallbackCleanup = setTimeout(() => {
|
||||
delete this.fallbackCleanup;
|
||||
this.olmDevice.forgetOldFallbackKey();
|
||||
}, 60*60*1000);
|
||||
}
|
||||
|
||||
await this.olmDevice.markKeysAsPublished();
|
||||
return res;
|
||||
@@ -2045,9 +2120,7 @@ export class Crypto extends EventEmitter {
|
||||
if (!this.crossSigningInfo.getId() && userId === this.crossSigningInfo.userId) {
|
||||
this.storeTrustedSelfKeys(xsk.keys);
|
||||
// This will cause our own user trust to change, so emit the event
|
||||
this.emit(
|
||||
"userTrustStatusChanged", this.userId, this.checkUserTrust(userId),
|
||||
);
|
||||
this.emit(CryptoEvent.UserTrustStatusChanged, this.userId, this.checkUserTrust(userId));
|
||||
}
|
||||
|
||||
// Now sign the master key with our user signing key (unless it's ourself)
|
||||
@@ -2058,7 +2131,7 @@ export class Crypto extends EventEmitter {
|
||||
);
|
||||
const device = await this.crossSigningInfo.signUser(xsk);
|
||||
if (device) {
|
||||
const upload = async ({ shouldEmit }) => {
|
||||
const upload = async ({ shouldEmit = false }) => {
|
||||
logger.info("Uploading signature for " + userId + "...");
|
||||
const response = await this.baseApis.uploadKeySignatures({
|
||||
[userId]: {
|
||||
@@ -2069,7 +2142,7 @@ export class Crypto extends EventEmitter {
|
||||
if (Object.keys(failures || []).length > 0) {
|
||||
if (shouldEmit) {
|
||||
this.baseApis.emit(
|
||||
"crypto.keySignatureUploadFailure",
|
||||
CryptoEvent.KeySignatureUploadFailure,
|
||||
failures,
|
||||
"setDeviceVerification",
|
||||
upload,
|
||||
@@ -2131,7 +2204,7 @@ export class Crypto extends EventEmitter {
|
||||
logger.info("Own device " + deviceId + " marked verified: signing");
|
||||
|
||||
// Signing only needed if other device not already signed
|
||||
let device;
|
||||
let device: ISignedKey;
|
||||
const deviceTrust = this.checkDeviceTrust(userId, deviceId);
|
||||
if (deviceTrust.isCrossSigningVerified()) {
|
||||
logger.log(`Own device ${deviceId} already cross-signing verified`);
|
||||
@@ -2142,7 +2215,7 @@ export class Crypto extends EventEmitter {
|
||||
}
|
||||
|
||||
if (device) {
|
||||
const upload = async ({ shouldEmit }) => {
|
||||
const upload = async ({ shouldEmit = false }) => {
|
||||
logger.info("Uploading signature for " + deviceId);
|
||||
const response = await this.baseApis.uploadKeySignatures({
|
||||
[userId]: {
|
||||
@@ -2153,7 +2226,7 @@ export class Crypto extends EventEmitter {
|
||||
if (Object.keys(failures || []).length > 0) {
|
||||
if (shouldEmit) {
|
||||
this.baseApis.emit(
|
||||
"crypto.keySignatureUploadFailure",
|
||||
CryptoEvent.KeySignatureUploadFailure,
|
||||
failures,
|
||||
"setDeviceVerification",
|
||||
upload, // continuation
|
||||
@@ -2168,7 +2241,7 @@ export class Crypto extends EventEmitter {
|
||||
}
|
||||
|
||||
const deviceObj = DeviceInfo.fromStorage(dev, deviceId);
|
||||
this.emit("deviceVerificationChanged", userId, deviceId, deviceObj);
|
||||
this.emit(CryptoEvent.DeviceVerificationChanged, userId, deviceId, deviceObj);
|
||||
return deviceObj;
|
||||
}
|
||||
|
||||
@@ -2176,24 +2249,20 @@ export class Crypto extends EventEmitter {
|
||||
return this.inRoomVerificationRequests.findRequestInProgress(roomId);
|
||||
}
|
||||
|
||||
public getVerificationRequestsToDeviceInProgress(userId: string): VerificationRequest {
|
||||
public getVerificationRequestsToDeviceInProgress(userId: string): VerificationRequest[] {
|
||||
return this.toDeviceVerificationRequests.getRequestsInProgress(userId);
|
||||
}
|
||||
|
||||
public requestVerificationDM(userId: string, roomId: string): VerificationRequest {
|
||||
public requestVerificationDM(userId: string, roomId: string): Promise<VerificationRequest> {
|
||||
const existingRequest = this.inRoomVerificationRequests.findRequestInProgress(roomId);
|
||||
if (existingRequest) {
|
||||
return Promise.resolve(existingRequest);
|
||||
}
|
||||
const channel = new InRoomChannel(this.baseApis, roomId, userId);
|
||||
return this.requestVerificationWithChannel(
|
||||
userId,
|
||||
channel,
|
||||
this.inRoomVerificationRequests,
|
||||
);
|
||||
return this.requestVerificationWithChannel(userId, channel, this.inRoomVerificationRequests);
|
||||
}
|
||||
|
||||
public requestVerification(userId: string, devices: string[]): VerificationRequest {
|
||||
public requestVerification(userId: string, devices: string[]): Promise<VerificationRequest> {
|
||||
if (!devices) {
|
||||
devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(userId));
|
||||
}
|
||||
@@ -2202,18 +2271,14 @@ export class Crypto extends EventEmitter {
|
||||
return Promise.resolve(existingRequest);
|
||||
}
|
||||
const channel = new ToDeviceChannel(this.baseApis, userId, devices, ToDeviceChannel.makeTransactionId());
|
||||
return this.requestVerificationWithChannel(
|
||||
userId,
|
||||
channel,
|
||||
this.toDeviceVerificationRequests,
|
||||
);
|
||||
return this.requestVerificationWithChannel(userId, channel, this.toDeviceVerificationRequests);
|
||||
}
|
||||
|
||||
private async requestVerificationWithChannel(
|
||||
userId: string,
|
||||
channel: any, // TODO types
|
||||
requestsMap: any, // TODO types
|
||||
): VerificationRequest {
|
||||
channel: IVerificationChannel,
|
||||
requestsMap: IRequestsMap,
|
||||
): Promise<VerificationRequest> {
|
||||
let request = new VerificationRequest(channel, this.verificationMethods, this.baseApis);
|
||||
// if transaction id is already known, add request
|
||||
if (channel.transactionId) {
|
||||
@@ -2259,14 +2324,11 @@ export class Crypto extends EventEmitter {
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
method: VerificationMethod,
|
||||
): VerificationRequest {
|
||||
): Promise<VerificationRequest> {
|
||||
const transactionId = ToDeviceChannel.makeTransactionId();
|
||||
const channel = new ToDeviceChannel(
|
||||
this.baseApis, userId, [deviceId], transactionId, deviceId);
|
||||
const request = new VerificationRequest(
|
||||
channel, this.verificationMethods, this.baseApis);
|
||||
this.toDeviceVerificationRequests.setRequestBySenderAndTxnId(
|
||||
userId, transactionId, request);
|
||||
const channel = new ToDeviceChannel(this.baseApis, userId, [deviceId], transactionId, deviceId);
|
||||
const request = new VerificationRequest(channel, this.verificationMethods, this.baseApis);
|
||||
this.toDeviceVerificationRequests.setRequestBySenderAndTxnId(userId, transactionId, request);
|
||||
const verifier = request.beginKeyVerification(method, { userId, deviceId });
|
||||
// either reject by an error from verify() while sending .start
|
||||
// or resolve when the request receives the
|
||||
@@ -2593,13 +2655,17 @@ export class Crypto extends EventEmitter {
|
||||
* the given users.
|
||||
*
|
||||
* @param {string[]} users list of user ids
|
||||
* @param {boolean} force If true, force a new Olm session to be created. Default false.
|
||||
*
|
||||
* @return {Promise} resolves once the sessions are complete, to
|
||||
* an Object mapping from userId to deviceId to
|
||||
* {@link module:crypto~OlmSessionResult}
|
||||
*/
|
||||
ensureOlmSessionsForUsers(users: string[]): Promise<Record<string, Record<string, olmlib.IOlmSessionResult>>> {
|
||||
const devicesByUser = {};
|
||||
public ensureOlmSessionsForUsers(
|
||||
users: string[],
|
||||
force?: boolean,
|
||||
): Promise<Record<string, Record<string, olmlib.IOlmSessionResult>>> {
|
||||
const devicesByUser: Record<string, DeviceInfo[]> = {};
|
||||
|
||||
for (let i = 0; i < users.length; ++i) {
|
||||
const userId = users[i];
|
||||
@@ -2623,7 +2689,7 @@ export class Crypto extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
return olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser);
|
||||
return olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser, force);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2632,7 +2698,7 @@ export class Crypto extends EventEmitter {
|
||||
* @return {module:crypto/OlmDevice.MegolmSessionData[]} a list of session export objects
|
||||
*/
|
||||
public async exportRoomKeys(): Promise<IMegolmSessionData[]> {
|
||||
const exportedSessions = [];
|
||||
const exportedSessions: IMegolmSessionData[] = [];
|
||||
await this.cryptoStore.doTxn(
|
||||
'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
|
||||
this.cryptoStore.getAllEndToEndInboundGroupSessions(txn, (s) => {
|
||||
@@ -2659,7 +2725,7 @@ export class Crypto extends EventEmitter {
|
||||
* @param {Function} opts.progressCallback called with an object which has a stage param
|
||||
* @return {Promise} a promise which resolves once the keys have been imported
|
||||
*/
|
||||
public importRoomKeys(keys: IMegolmSessionData[], opts: any = {}): Promise<any> { // TODO types
|
||||
public importRoomKeys(keys: IMegolmSessionData[], opts: IImportRoomKeysOpts = {}): Promise<void> {
|
||||
let successes = 0;
|
||||
let failures = 0;
|
||||
const total = keys.length;
|
||||
@@ -2686,7 +2752,7 @@ export class Crypto extends EventEmitter {
|
||||
successes++;
|
||||
if (opts.progressCallback) { updateProgress(); }
|
||||
});
|
||||
}));
|
||||
})).then();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2710,7 +2776,6 @@ export class Crypto extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307
|
||||
/**
|
||||
* Encrypt an event according to the configuration of the room.
|
||||
*
|
||||
@@ -2721,8 +2786,6 @@ export class Crypto extends EventEmitter {
|
||||
* @return {Promise?} Promise which resolves when the event has been
|
||||
* encrypted, or null if nothing was needed
|
||||
*/
|
||||
/* eslint-enable valid-jsdoc */
|
||||
// TODO this return type lies
|
||||
public async encryptEvent(event: MatrixEvent, room: Room): Promise<void> {
|
||||
if (!room) {
|
||||
throw new Error("Cannot send encrypted messages in unknown rooms");
|
||||
@@ -2757,12 +2820,21 @@ export class Crypto extends EventEmitter {
|
||||
delete content['m.relates_to'];
|
||||
}
|
||||
|
||||
const encryptedContent = await alg.encryptMessage(
|
||||
room, event.getType(), content);
|
||||
// Treat element's performance metrics the same as `m.relates_to` (when present)
|
||||
const elementPerfMetrics = content['io.element.performance_metrics'];
|
||||
if (elementPerfMetrics) {
|
||||
content = Object.assign({}, content);
|
||||
delete content['io.element.performance_metrics'];
|
||||
}
|
||||
|
||||
const encryptedContent = await alg.encryptMessage(room, event.getType(), content);
|
||||
|
||||
if (mRelatesTo) {
|
||||
encryptedContent['m.relates_to'] = mRelatesTo;
|
||||
}
|
||||
if (elementPerfMetrics) {
|
||||
encryptedContent['io.element.performance_metrics'] = elementPerfMetrics;
|
||||
}
|
||||
|
||||
event.makeEncrypted(
|
||||
"m.room.encrypted",
|
||||
@@ -2792,7 +2864,7 @@ export class Crypto extends EventEmitter {
|
||||
type: "m.room.message",
|
||||
content: {},
|
||||
unsigned: {
|
||||
redacted_because: decryptedEvent.clearEvent,
|
||||
redacted_because: decryptedEvent.clearEvent as IEvent,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -3021,6 +3093,14 @@ export class Crypto extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
private onMembership = (event: MatrixEvent, member: RoomMember, oldMembership?: string) => {
|
||||
try {
|
||||
this.onRoomMembership(event, member, oldMembership);
|
||||
} catch (e) {
|
||||
logger.error("Error handling membership change:", e);
|
||||
}
|
||||
};
|
||||
|
||||
private onToDeviceEvent = (event: MatrixEvent): void => {
|
||||
try {
|
||||
logger.log(`received to_device ${event.getType()} from: ` +
|
||||
@@ -3046,7 +3126,7 @@ export class Crypto extends EventEmitter {
|
||||
event.attemptDecryption(this);
|
||||
}
|
||||
// once the event has been decrypted, try again
|
||||
event.once('Event.decrypted', (ev) => {
|
||||
event.once(MatrixEventEvent.Decrypted, (ev) => {
|
||||
this.onToDeviceEvent(ev);
|
||||
});
|
||||
}
|
||||
@@ -3125,7 +3205,7 @@ export class Crypto extends EventEmitter {
|
||||
if (!ToDeviceChannel.validateEvent(event, this.baseApis)) {
|
||||
return;
|
||||
}
|
||||
const createRequest = event => {
|
||||
const createRequest = (event: MatrixEvent) => {
|
||||
if (!ToDeviceChannel.canCreateRequest(ToDeviceChannel.getEventType(event))) {
|
||||
return;
|
||||
}
|
||||
@@ -3143,11 +3223,7 @@ export class Crypto extends EventEmitter {
|
||||
return new VerificationRequest(
|
||||
channel, this.verificationMethods, this.baseApis);
|
||||
};
|
||||
this.handleVerificationEvent(
|
||||
event,
|
||||
this.toDeviceVerificationRequests,
|
||||
createRequest,
|
||||
);
|
||||
this.handleVerificationEvent(event, this.toDeviceVerificationRequests, createRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3170,7 +3246,7 @@ export class Crypto extends EventEmitter {
|
||||
if (!InRoomChannel.validateEvent(event, this.baseApis)) {
|
||||
return;
|
||||
}
|
||||
const createRequest = event => {
|
||||
const createRequest = (event: MatrixEvent) => {
|
||||
const channel = new InRoomChannel(
|
||||
this.baseApis,
|
||||
event.getRoomId(),
|
||||
@@ -3178,20 +3254,38 @@ export class Crypto extends EventEmitter {
|
||||
return new VerificationRequest(
|
||||
channel, this.verificationMethods, this.baseApis);
|
||||
};
|
||||
this.handleVerificationEvent(
|
||||
event,
|
||||
this.inRoomVerificationRequests,
|
||||
createRequest,
|
||||
liveEvent,
|
||||
);
|
||||
this.handleVerificationEvent(event, this.inRoomVerificationRequests, createRequest, liveEvent);
|
||||
};
|
||||
|
||||
private async handleVerificationEvent(
|
||||
event: MatrixEvent,
|
||||
requestsMap: any, // TODO types
|
||||
createRequest: any, // TODO types
|
||||
requestsMap: IRequestsMap,
|
||||
createRequest: (event: MatrixEvent) => VerificationRequest,
|
||||
isLiveEvent = true,
|
||||
): Promise<void> {
|
||||
// Wait for event to get its final ID with pendingEventOrdering: "chronological", since DM channels depend on it.
|
||||
if (event.isSending() && event.status != EventStatus.SENT) {
|
||||
let eventIdListener;
|
||||
let statusListener;
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
eventIdListener = resolve;
|
||||
statusListener = () => {
|
||||
if (event.status == EventStatus.CANCELLED) {
|
||||
reject(new Error("Event status set to CANCELLED."));
|
||||
}
|
||||
};
|
||||
event.once(MatrixEventEvent.LocalEventIdReplaced, eventIdListener);
|
||||
event.on(MatrixEventEvent.Status, statusListener);
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error("error while waiting for the verification event to be sent: " + err.message);
|
||||
return;
|
||||
} finally {
|
||||
event.removeListener(MatrixEventEvent.LocalEventIdReplaced, eventIdListener);
|
||||
event.removeListener(MatrixEventEvent.Status, statusListener);
|
||||
}
|
||||
}
|
||||
let request = requestsMap.getRequest(event);
|
||||
let isNewRequest = false;
|
||||
if (!request) {
|
||||
@@ -3216,7 +3310,7 @@ export class Crypto extends EventEmitter {
|
||||
!request.invalid && // check it has enough events to pass the UNSENT stage
|
||||
!request.observeOnly;
|
||||
if (shouldEmit) {
|
||||
this.baseApis.emit("crypto.verification.request", request);
|
||||
this.baseApis.emit(CryptoEvent.VerificationRequest, request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3280,7 +3374,7 @@ export class Crypto extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const devicesByUser = {};
|
||||
const devicesByUser: Record<string, DeviceInfo[]> = {};
|
||||
devicesByUser[sender] = [device];
|
||||
await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser, true);
|
||||
|
||||
@@ -3517,7 +3611,7 @@ export class Crypto extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
this.emit("crypto.roomKeyRequest", req);
|
||||
this.emit(CryptoEvent.RoomKeyRequest, req);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3536,7 +3630,7 @@ export class Crypto extends EventEmitter {
|
||||
// we should probably only notify the app of cancellations we told it
|
||||
// about, but we don't currently have a record of that, so we just pass
|
||||
// everything through.
|
||||
this.emit("crypto.roomKeyRequestCancellation", cancellation);
|
||||
this.emit(CryptoEvent.RoomKeyRequestCancellation, cancellation);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,6 +15,10 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { randomString } from '../randomstring';
|
||||
import { getCrypto } from '../utils';
|
||||
|
||||
const subtleCrypto = (typeof window !== "undefined" && window.crypto) ?
|
||||
(window.crypto.subtle || window.crypto.webkitSubtle) : null;
|
||||
|
||||
const DEFAULT_ITERATIONS = 500000;
|
||||
|
||||
@@ -70,11 +74,21 @@ export async function deriveKey(
|
||||
salt: string,
|
||||
iterations: number,
|
||||
numBits = DEFAULT_BITSIZE,
|
||||
): Promise<Uint8Array> {
|
||||
return subtleCrypto
|
||||
? deriveKeyBrowser(password, salt, iterations, numBits)
|
||||
: deriveKeyNode(password, salt, iterations, numBits);
|
||||
}
|
||||
|
||||
async function deriveKeyBrowser(
|
||||
password: string,
|
||||
salt: string,
|
||||
iterations: number,
|
||||
numBits: number,
|
||||
): Promise<Uint8Array> {
|
||||
const subtleCrypto = global.crypto.subtle;
|
||||
const TextEncoder = global.TextEncoder;
|
||||
if (!subtleCrypto || !TextEncoder) {
|
||||
// TODO: Implement this for node
|
||||
throw new Error("Password-based backup is not avaiable on this platform");
|
||||
}
|
||||
|
||||
@@ -99,3 +113,17 @@ export async function deriveKey(
|
||||
|
||||
return new Uint8Array(keybits);
|
||||
}
|
||||
|
||||
async function deriveKeyNode(
|
||||
password: string,
|
||||
salt: string,
|
||||
iterations: number,
|
||||
numBits: number,
|
||||
): Promise<Uint8Array> {
|
||||
const crypto = getCrypto();
|
||||
if (!crypto) {
|
||||
throw new Error("No usable crypto implementation");
|
||||
}
|
||||
|
||||
return crypto.pbkdf2Sync(password, Buffer.from(salt, 'binary'), iterations, numBits, 'sha512');
|
||||
}
|
||||
|
||||
@@ -67,5 +67,5 @@ export interface IKeyBackupRestoreResult {
|
||||
|
||||
export interface IKeyBackupRestoreOpts {
|
||||
cacheCompleteCallback?: () => void;
|
||||
progressCallback?: ({ stage: string }) => void;
|
||||
progressCallback?: (progress: { stage: string }) => void;
|
||||
}
|
||||
|
||||
+25
-24
@@ -21,15 +21,15 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import anotherjson from "another-json";
|
||||
import type { PkSigning } from "@matrix-org/olm";
|
||||
import { Logger } from "loglevel";
|
||||
|
||||
import OlmDevice from "./OlmDevice";
|
||||
import type { PkSigning } from "@matrix-org/olm";
|
||||
import { OlmDevice } from "./OlmDevice";
|
||||
import { DeviceInfo } from "./deviceinfo";
|
||||
import { logger } from '../logger';
|
||||
import * as utils from "../utils";
|
||||
import { IOneTimeKey } from "./dehydration";
|
||||
import { MatrixClient } from "../client";
|
||||
import { IClaimOTKsResult, MatrixClient } from "../client";
|
||||
import { ISignatures } from "../@types/signed";
|
||||
|
||||
enum Algorithm {
|
||||
Olm = "m.olm.v1.curve25519-aes-sha2",
|
||||
@@ -126,13 +126,18 @@ export async function encryptMessageForDevice(
|
||||
// involved in the session. If we're looking to reduce data transfer in the
|
||||
// future, we could elide them for subsequent messages.
|
||||
|
||||
utils.extend(payload, payloadFields);
|
||||
Object.assign(payload, payloadFields);
|
||||
|
||||
resultsObject[deviceKey] = await olmDevice.encryptMessage(
|
||||
deviceKey, sessionId, JSON.stringify(payload),
|
||||
);
|
||||
}
|
||||
|
||||
interface IExistingOlmSession {
|
||||
device: DeviceInfo;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the existing olm sessions for the given devices, and the devices that
|
||||
* don't have olm sessions.
|
||||
@@ -153,11 +158,11 @@ export async function getExistingOlmSessions(
|
||||
olmDevice: OlmDevice,
|
||||
baseApis: MatrixClient,
|
||||
devicesByUser: Record<string, DeviceInfo[]>,
|
||||
) {
|
||||
const devicesWithoutSession = {};
|
||||
const sessions = {};
|
||||
): Promise<[Record<string, DeviceInfo[]>, Record<string, Record<string, IExistingOlmSession>>]> {
|
||||
const devicesWithoutSession: {[userId: string]: DeviceInfo[]} = {};
|
||||
const sessions: {[userId: string]: {[deviceId: string]: IExistingOlmSession}} = {};
|
||||
|
||||
const promises = [];
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
||||
for (const deviceInfo of devices) {
|
||||
@@ -231,10 +236,10 @@ export async function ensureOlmSessionsForDevices(
|
||||
force = false;
|
||||
}
|
||||
|
||||
const devicesWithoutSession = [
|
||||
const devicesWithoutSession: [string, string][] = [
|
||||
// [userId, deviceId], ...
|
||||
];
|
||||
const result = {};
|
||||
const result: {[userId: string]: {[deviceId: string]: IExistingOlmSession}} = {};
|
||||
const resolveSession: Record<string, (sessionId?: string) => void> = {};
|
||||
|
||||
// Mark all sessions this task intends to update as in progress. It is
|
||||
@@ -252,13 +257,13 @@ export async function ensureOlmSessionsForDevices(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!olmDevice._sessionsInProgress[key]) {
|
||||
if (!olmDevice.sessionsInProgress[key]) {
|
||||
// pre-emptively mark the session as in-progress to avoid race
|
||||
// conditions. If we find that we already have a session, then
|
||||
// we'll resolve
|
||||
olmDevice._sessionsInProgress[key] = new Promise(resolve => {
|
||||
olmDevice.sessionsInProgress[key] = new Promise(resolve => {
|
||||
resolveSession[key] = (v: any) => {
|
||||
delete olmDevice._sessionsInProgress[key];
|
||||
delete olmDevice.sessionsInProgress[key];
|
||||
resolve(v);
|
||||
};
|
||||
});
|
||||
@@ -291,9 +296,7 @@ export async function ensureOlmSessionsForDevices(
|
||||
}
|
||||
|
||||
const forWhom = `for ${key} (${userId}:${deviceId})`;
|
||||
const sessionId = await olmDevice.getSessionIdForDevice(
|
||||
key, resolveSession[key], log,
|
||||
);
|
||||
const sessionId = await olmDevice.getSessionIdForDevice(key, !!resolveSession[key], log);
|
||||
if (sessionId !== null && resolveSession[key]) {
|
||||
// we found a session, but we had marked the session as
|
||||
// in-progress, so resolve it now, which will unmark it and
|
||||
@@ -324,9 +327,7 @@ export async function ensureOlmSessionsForDevices(
|
||||
let taskDetail = `one-time keys for ${devicesWithoutSession.length} devices`;
|
||||
try {
|
||||
log.debug(`Claiming ${taskDetail}`);
|
||||
res = await baseApis.claimOneTimeKeys(
|
||||
devicesWithoutSession, oneTimeKeyAlgorithm, otkTimeout,
|
||||
);
|
||||
res = await baseApis.claimOneTimeKeys(devicesWithoutSession, oneTimeKeyAlgorithm, otkTimeout);
|
||||
log.debug(`Claimed ${taskDetail}`);
|
||||
} catch (e) {
|
||||
for (const resolver of Object.values(resolveSession)) {
|
||||
@@ -340,8 +341,8 @@ export async function ensureOlmSessionsForDevices(
|
||||
failedServers.push(...Object.keys(res.failures));
|
||||
}
|
||||
|
||||
const otkResult = res.one_time_keys || {};
|
||||
const promises = [];
|
||||
const otkResult = res.one_time_keys || {} as IClaimOTKsResult["one_time_keys"];
|
||||
const promises: Promise<void>[] = [];
|
||||
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
||||
const userRes = otkResult[userId] || {};
|
||||
for (let j = 0; j < devices.length; j++) {
|
||||
@@ -362,7 +363,7 @@ export async function ensureOlmSessionsForDevices(
|
||||
}
|
||||
|
||||
const deviceRes = userRes[deviceId] || {};
|
||||
let oneTimeKey = null;
|
||||
let oneTimeKey: IOneTimeKey = null;
|
||||
for (const keyId in deviceRes) {
|
||||
if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) {
|
||||
oneTimeKey = deviceRes[keyId];
|
||||
@@ -444,7 +445,7 @@ async function _verifyKeyAndStartSession(
|
||||
|
||||
export interface IObject {
|
||||
unsigned?: object;
|
||||
signatures?: object;
|
||||
signatures?: ISignatures;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,7 +23,7 @@ import { IRoomEncryption } from "../RoomList";
|
||||
import { IDevice } from "../deviceinfo";
|
||||
import { ICrossSigningInfo } from "../CrossSigning";
|
||||
import { PrefixedLogger } from "../../logger";
|
||||
import { InboundGroupSessionData } from "../../@types/partials";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
import { IEncryptedPayload } from "../aes";
|
||||
|
||||
/**
|
||||
@@ -125,7 +125,7 @@ export interface CryptoStore {
|
||||
addSharedHistoryInboundGroupSession(roomId: string, senderKey: string, sessionId: string, txn?: unknown): void;
|
||||
getSharedHistoryInboundGroupSessions(
|
||||
roomId: string,
|
||||
txn?: IDBTransaction,
|
||||
txn?: unknown,
|
||||
): Promise<[senderKey: string, sessionId: string][]>;
|
||||
|
||||
// Session key backups
|
||||
|
||||
@@ -30,7 +30,7 @@ import { IRoomKeyRequestBody } from "../index";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
import { InboundGroupSessionData } from "../../@types/partials";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
import { IEncryptedPayload } from "../aes";
|
||||
|
||||
export const VERSION = 10;
|
||||
@@ -200,10 +200,10 @@ export class Backend implements CryptoStore {
|
||||
|
||||
// index into the wantedStates array
|
||||
let stateIndex = 0;
|
||||
let result;
|
||||
let result: OutgoingRoomKeyRequest;
|
||||
|
||||
function onsuccess(ev) {
|
||||
const cursor = ev.target.result;
|
||||
function onsuccess(this: IDBRequest<IDBCursorWithValue>) {
|
||||
const cursor = this.result;
|
||||
if (cursor) {
|
||||
// got a match
|
||||
result = cursor.value;
|
||||
@@ -218,7 +218,7 @@ export class Backend implements CryptoStore {
|
||||
}
|
||||
|
||||
const wantedState = wantedStates[stateIndex];
|
||||
const cursorReq = ev.target.source.openCursor(wantedState);
|
||||
const cursorReq = (this.source as IDBIndex).openCursor(wantedState);
|
||||
cursorReq.onsuccess = onsuccess;
|
||||
}
|
||||
|
||||
@@ -255,10 +255,10 @@ export class Backend implements CryptoStore {
|
||||
wantedStates: number[],
|
||||
): Promise<OutgoingRoomKeyRequest[]> {
|
||||
let stateIndex = 0;
|
||||
const results = [];
|
||||
const results: OutgoingRoomKeyRequest[] = [];
|
||||
|
||||
function onsuccess(ev) {
|
||||
const cursor = ev.target.result;
|
||||
function onsuccess(this: IDBRequest<IDBCursorWithValue>) {
|
||||
const cursor = this.result;
|
||||
if (cursor) {
|
||||
const keyReq = cursor.value;
|
||||
if (keyReq.recipients.includes({ userId, deviceId })) {
|
||||
@@ -274,7 +274,7 @@ export class Backend implements CryptoStore {
|
||||
}
|
||||
|
||||
const wantedState = wantedStates[stateIndex];
|
||||
const cursorReq = ev.target.source.openCursor(wantedState);
|
||||
const cursorReq = (this.source as IDBIndex).openCursor(wantedState);
|
||||
cursorReq.onsuccess = onsuccess;
|
||||
}
|
||||
}
|
||||
@@ -306,10 +306,10 @@ export class Backend implements CryptoStore {
|
||||
expectedState: number,
|
||||
updates: Partial<OutgoingRoomKeyRequest>,
|
||||
): Promise<OutgoingRoomKeyRequest | null> {
|
||||
let result = null;
|
||||
let result: OutgoingRoomKeyRequest = null;
|
||||
|
||||
function onsuccess(ev) {
|
||||
const cursor = ev.target.result;
|
||||
function onsuccess(this: IDBRequest<IDBCursorWithValue>) {
|
||||
const cursor = this.result;
|
||||
if (!cursor) {
|
||||
return;
|
||||
}
|
||||
@@ -444,7 +444,7 @@ export class Backend implements CryptoStore {
|
||||
const objectStore = txn.objectStore("sessions");
|
||||
const idx = objectStore.index("deviceKey");
|
||||
const getReq = idx.openCursor(deviceKey);
|
||||
const results = {};
|
||||
const results: Parameters<Parameters<Backend["getEndToEndSessions"]>[2]>[0] = {};
|
||||
getReq.onsuccess = function() {
|
||||
const cursor = getReq.result;
|
||||
if (cursor) {
|
||||
@@ -734,7 +734,7 @@ export class Backend implements CryptoStore {
|
||||
}
|
||||
|
||||
public getEndToEndRooms(txn: IDBTransaction, func: (rooms: Record<string, IRoomEncryption>) => void): void {
|
||||
const rooms = {};
|
||||
const rooms: Parameters<Parameters<Backend["getEndToEndRooms"]>[1]>[0] = {};
|
||||
const objectStore = txn.objectStore("rooms");
|
||||
const getReq = objectStore.openCursor();
|
||||
getReq.onsuccess = function() {
|
||||
@@ -756,7 +756,7 @@ export class Backend implements CryptoStore {
|
||||
|
||||
public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sessions = [];
|
||||
const sessions: ISession[] = [];
|
||||
|
||||
const txn = this.db.transaction(
|
||||
["sessions_needing_backup", "inbound_group_sessions"],
|
||||
@@ -877,8 +877,8 @@ export class Backend implements CryptoStore {
|
||||
func: (txn: IDBTransaction) => T,
|
||||
log: PrefixedLogger = logger,
|
||||
): Promise<T> {
|
||||
let startTime;
|
||||
let description;
|
||||
let startTime: number;
|
||||
let description: string;
|
||||
if (PROFILE_TRANSACTIONS) {
|
||||
const txnId = this.nextTxnId++;
|
||||
startTime = Date.now();
|
||||
|
||||
@@ -34,7 +34,7 @@ import { IRoomKeyRequestBody } from "../index";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
import { InboundGroupSessionData } from "../../@types/partials";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
import { IEncryptedPayload } from "../aes";
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,7 +20,7 @@ import { IDeviceData, IProblem, ISession, ISessionInfo, IWithheld, Mode } from "
|
||||
import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
import { InboundGroupSessionData } from "../../@types/partials";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
import { IEncryptedPayload } from "../aes";
|
||||
|
||||
/**
|
||||
@@ -175,8 +175,10 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
}
|
||||
|
||||
public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
|
||||
const notifiedErrorDevices = getJsonItem<string[]>(this.store, KEY_NOTIFIED_ERROR_DEVICES) || {};
|
||||
const ret = [];
|
||||
const notifiedErrorDevices = getJsonItem<MemoryCryptoStore["notifiedErrorDevices"]>(
|
||||
this.store, KEY_NOTIFIED_ERROR_DEVICES,
|
||||
) || {};
|
||||
const ret: IOlmDevice[] = [];
|
||||
|
||||
for (const device of devices) {
|
||||
const { userId, deviceInfo } = device;
|
||||
@@ -291,7 +293,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
}
|
||||
|
||||
public getEndToEndRooms(txn: unknown, func: (rooms: Record<string, IRoomEncryption>) => void): void {
|
||||
const result = {};
|
||||
const result: Record<string, IRoomEncryption> = {};
|
||||
const prefix = keyEndToEndRoomsPrefix('');
|
||||
|
||||
for (let i = 0; i < this.store.length; ++i) {
|
||||
@@ -306,7 +308,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
|
||||
public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
|
||||
const sessionsNeedingBackup = getJsonItem<string[]>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
|
||||
const sessions = [];
|
||||
const sessions: ISession[] = [];
|
||||
|
||||
for (const session in sessionsNeedingBackup) {
|
||||
if (Object.prototype.hasOwnProperty.call(sessionsNeedingBackup, session)) {
|
||||
|
||||
@@ -30,7 +30,7 @@ import { IRoomKeyRequestBody } from "../index";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
import { InboundGroupSessionData } from "../../@types/partials";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
import { IEncryptedPayload } from "../aes";
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,22 +21,48 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixEvent } from '../../models/event';
|
||||
import { EventEmitter } from 'events';
|
||||
import { logger } from '../../logger';
|
||||
import { DeviceInfo } from '../deviceinfo';
|
||||
import { newTimeoutError } from "./Error";
|
||||
import { requestKeysDuringVerification } from "../CrossSigning";
|
||||
import { KeysDuringVerification, requestKeysDuringVerification } from "../CrossSigning";
|
||||
import { IVerificationChannel } from "./request/Channel";
|
||||
import { MatrixClient } from "../../client";
|
||||
import { VerificationRequest } from "./request/VerificationRequest";
|
||||
import { ListenerMap, TypedEventEmitter } from "../../models/typed-event-emitter";
|
||||
|
||||
const timeoutException = new Error("Verification timed out");
|
||||
|
||||
export class SwitchStartEventError extends Error {
|
||||
constructor(startEvent) {
|
||||
constructor(public readonly startEvent: MatrixEvent) {
|
||||
super();
|
||||
this.startEvent = startEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export class VerificationBase extends EventEmitter {
|
||||
export type KeyVerifier = (keyId: string, device: DeviceInfo, keyInfo: string) => void;
|
||||
|
||||
export enum VerificationEvent {
|
||||
Cancel = "cancel",
|
||||
}
|
||||
|
||||
export type VerificationEventHandlerMap = {
|
||||
[VerificationEvent.Cancel]: (e: Error | MatrixEvent) => void;
|
||||
};
|
||||
|
||||
export class VerificationBase<
|
||||
Events extends string,
|
||||
Arguments extends ListenerMap<Events | VerificationEvent>,
|
||||
> extends TypedEventEmitter<Events | VerificationEvent, Arguments, VerificationEventHandlerMap> {
|
||||
private cancelled = false;
|
||||
private _done = false;
|
||||
private promise: Promise<void> = null;
|
||||
private transactionTimeoutTimer: number = null;
|
||||
protected expectedEvent: string;
|
||||
private resolve: () => void;
|
||||
private reject: (e: Error | MatrixEvent) => void;
|
||||
private resolveEvent: (e: MatrixEvent) => void;
|
||||
private rejectEvent: (e: Error) => void;
|
||||
private started: boolean;
|
||||
|
||||
/**
|
||||
* Base class for verification methods.
|
||||
*
|
||||
@@ -64,22 +90,18 @@ export class VerificationBase extends EventEmitter {
|
||||
* @param {object} [request] the key verification request object related to
|
||||
* this verification, if any
|
||||
*/
|
||||
constructor(channel, baseApis, userId, deviceId, startEvent, request) {
|
||||
constructor(
|
||||
public readonly channel: IVerificationChannel,
|
||||
public readonly baseApis: MatrixClient,
|
||||
public readonly userId: string,
|
||||
public readonly deviceId: string,
|
||||
public startEvent: MatrixEvent,
|
||||
public readonly request: VerificationRequest,
|
||||
) {
|
||||
super();
|
||||
this._channel = channel;
|
||||
this._baseApis = baseApis;
|
||||
this.userId = userId;
|
||||
this.deviceId = deviceId;
|
||||
this.startEvent = startEvent;
|
||||
this.request = request;
|
||||
|
||||
this.cancelled = false;
|
||||
this._done = false;
|
||||
this._promise = null;
|
||||
this._transactionTimeoutTimer = null;
|
||||
}
|
||||
|
||||
get initiatedByMe() {
|
||||
public get initiatedByMe(): boolean {
|
||||
// if there is no start event yet,
|
||||
// we probably want to send it,
|
||||
// which happens if we initiate
|
||||
@@ -88,16 +110,20 @@ export class VerificationBase extends EventEmitter {
|
||||
}
|
||||
const sender = this.startEvent.getSender();
|
||||
const content = this.startEvent.getContent();
|
||||
return sender === this._baseApis.getUserId() &&
|
||||
content.from_device === this._baseApis.getDeviceId();
|
||||
return sender === this.baseApis.getUserId() &&
|
||||
content.from_device === this.baseApis.getDeviceId();
|
||||
}
|
||||
|
||||
_resetTimer() {
|
||||
public get hasBeenCancelled(): boolean {
|
||||
return this.cancelled;
|
||||
}
|
||||
|
||||
private resetTimer(): void {
|
||||
logger.info("Refreshing/starting the verification transaction timeout timer");
|
||||
if (this._transactionTimeoutTimer !== null) {
|
||||
clearTimeout(this._transactionTimeoutTimer);
|
||||
if (this.transactionTimeoutTimer !== null) {
|
||||
clearTimeout(this.transactionTimeoutTimer);
|
||||
}
|
||||
this._transactionTimeoutTimer = setTimeout(() => {
|
||||
this.transactionTimeoutTimer = setTimeout(() => {
|
||||
if (!this._done && !this.cancelled) {
|
||||
logger.info("Triggering verification timeout");
|
||||
this.cancel(timeoutException);
|
||||
@@ -105,18 +131,18 @@ export class VerificationBase extends EventEmitter {
|
||||
}, 10 * 60 * 1000); // 10 minutes
|
||||
}
|
||||
|
||||
_endTimer() {
|
||||
if (this._transactionTimeoutTimer !== null) {
|
||||
clearTimeout(this._transactionTimeoutTimer);
|
||||
this._transactionTimeoutTimer = null;
|
||||
private endTimer(): void {
|
||||
if (this.transactionTimeoutTimer !== null) {
|
||||
clearTimeout(this.transactionTimeoutTimer);
|
||||
this.transactionTimeoutTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
_send(type, uncompletedContent) {
|
||||
return this._channel.send(type, uncompletedContent);
|
||||
protected send(type: string, uncompletedContent: Record<string, any>): Promise<void> {
|
||||
return this.channel.send(type, uncompletedContent);
|
||||
}
|
||||
|
||||
_waitForEvent(type) {
|
||||
protected waitForEvent(type: string): Promise<MatrixEvent> {
|
||||
if (this._done) {
|
||||
return Promise.reject(new Error("Verification is already done"));
|
||||
}
|
||||
@@ -125,24 +151,24 @@ export class VerificationBase extends EventEmitter {
|
||||
return Promise.resolve(existingEvent);
|
||||
}
|
||||
|
||||
this._expectedEvent = type;
|
||||
this.expectedEvent = type;
|
||||
return new Promise((resolve, reject) => {
|
||||
this._resolveEvent = resolve;
|
||||
this._rejectEvent = reject;
|
||||
this.resolveEvent = resolve;
|
||||
this.rejectEvent = reject;
|
||||
});
|
||||
}
|
||||
|
||||
canSwitchStartEvent() {
|
||||
public canSwitchStartEvent(event: MatrixEvent): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
switchStartEvent(event) {
|
||||
public switchStartEvent(event: MatrixEvent): void {
|
||||
if (this.canSwitchStartEvent(event)) {
|
||||
logger.log("Verification Base: switching verification start event",
|
||||
{ restartingFlow: !!this._rejectEvent });
|
||||
if (this._rejectEvent) {
|
||||
const reject = this._rejectEvent;
|
||||
this._rejectEvent = undefined;
|
||||
{ restartingFlow: !!this.rejectEvent });
|
||||
if (this.rejectEvent) {
|
||||
const reject = this.rejectEvent;
|
||||
this.rejectEvent = undefined;
|
||||
reject(new SwitchStartEventError(event));
|
||||
} else {
|
||||
this.startEvent = event;
|
||||
@@ -150,21 +176,21 @@ export class VerificationBase extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
handleEvent(e) {
|
||||
public handleEvent(e: MatrixEvent): void {
|
||||
if (this._done) {
|
||||
return;
|
||||
} else if (e.getType() === this._expectedEvent) {
|
||||
} else if (e.getType() === this.expectedEvent) {
|
||||
// if we receive an expected m.key.verification.done, then just
|
||||
// ignore it, since we don't need to do anything about it
|
||||
if (this._expectedEvent !== "m.key.verification.done") {
|
||||
this._expectedEvent = undefined;
|
||||
this._rejectEvent = undefined;
|
||||
this._resetTimer();
|
||||
this._resolveEvent(e);
|
||||
if (this.expectedEvent !== "m.key.verification.done") {
|
||||
this.expectedEvent = undefined;
|
||||
this.rejectEvent = undefined;
|
||||
this.resetTimer();
|
||||
this.resolveEvent(e);
|
||||
}
|
||||
} else if (e.getType() === "m.key.verification.cancel") {
|
||||
const reject = this._reject;
|
||||
this._reject = undefined;
|
||||
const reject = this.reject;
|
||||
this.reject = undefined;
|
||||
// there is only promise to reject if verify has been called
|
||||
if (reject) {
|
||||
const content = e.getContent();
|
||||
@@ -172,36 +198,36 @@ export class VerificationBase extends EventEmitter {
|
||||
reject(new Error(`Other side cancelled verification ` +
|
||||
`because ${reason} (${code})`));
|
||||
}
|
||||
} else if (this._expectedEvent) {
|
||||
} else if (this.expectedEvent) {
|
||||
// only cancel if there is an event expected.
|
||||
// if there is no event expected, it means verify() wasn't called
|
||||
// and we're just replaying the timeline events when syncing
|
||||
// after a refresh when the events haven't been stored in the cache yet.
|
||||
const exception = new Error(
|
||||
"Unexpected message: expecting " + this._expectedEvent
|
||||
"Unexpected message: expecting " + this.expectedEvent
|
||||
+ " but got " + e.getType(),
|
||||
);
|
||||
this._expectedEvent = undefined;
|
||||
if (this._rejectEvent) {
|
||||
const reject = this._rejectEvent;
|
||||
this._rejectEvent = undefined;
|
||||
this.expectedEvent = undefined;
|
||||
if (this.rejectEvent) {
|
||||
const reject = this.rejectEvent;
|
||||
this.rejectEvent = undefined;
|
||||
reject(exception);
|
||||
}
|
||||
this.cancel(exception);
|
||||
}
|
||||
}
|
||||
|
||||
done() {
|
||||
this._endTimer(); // always kill the activity timer
|
||||
public done(): Promise<KeysDuringVerification | void> {
|
||||
this.endTimer(); // always kill the activity timer
|
||||
if (!this._done) {
|
||||
this.request.onVerifierFinished();
|
||||
this._resolve();
|
||||
return requestKeysDuringVerification(this._baseApis, this.userId, this.deviceId);
|
||||
this.resolve();
|
||||
return requestKeysDuringVerification(this.baseApis, this.userId, this.deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
cancel(e) {
|
||||
this._endTimer(); // always kill the activity timer
|
||||
public cancel(e: Error | MatrixEvent): void {
|
||||
this.endTimer(); // always kill the activity timer
|
||||
if (!this._done) {
|
||||
this.cancelled = true;
|
||||
this.request.onVerifierCancelled();
|
||||
@@ -210,7 +236,7 @@ export class VerificationBase extends EventEmitter {
|
||||
// cancelled by the other user)
|
||||
if (e === timeoutException) {
|
||||
const timeoutEvent = newTimeoutError();
|
||||
this._send(timeoutEvent.getType(), timeoutEvent.getContent());
|
||||
this.send(timeoutEvent.getType(), timeoutEvent.getContent());
|
||||
} else if (e instanceof MatrixEvent) {
|
||||
const sender = e.getSender();
|
||||
if (sender !== this.userId) {
|
||||
@@ -219,33 +245,33 @@ export class VerificationBase extends EventEmitter {
|
||||
content.code = content.code || "m.unknown";
|
||||
content.reason = content.reason || content.body
|
||||
|| "Unknown reason";
|
||||
this._send("m.key.verification.cancel", content);
|
||||
this.send("m.key.verification.cancel", content);
|
||||
} else {
|
||||
this._send("m.key.verification.cancel", {
|
||||
this.send("m.key.verification.cancel", {
|
||||
code: "m.unknown",
|
||||
reason: content.body || "Unknown reason",
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._send("m.key.verification.cancel", {
|
||||
this.send("m.key.verification.cancel", {
|
||||
code: "m.unknown",
|
||||
reason: e.toString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (this._promise !== null) {
|
||||
if (this.promise !== null) {
|
||||
// when we cancel without a promise, we end up with a promise
|
||||
// but no reject function. If cancel is called again, we'd error.
|
||||
if (this._reject) this._reject(e);
|
||||
if (this.reject) this.reject(e);
|
||||
} else {
|
||||
// FIXME: this causes an "Uncaught promise" console message
|
||||
// if nothing ends up chaining this promise.
|
||||
this._promise = Promise.reject(e);
|
||||
this.promise = Promise.reject(e);
|
||||
}
|
||||
// Also emit a 'cancel' event that the app can listen for to detect cancellation
|
||||
// before calling verify()
|
||||
this.emit('cancel', e);
|
||||
this.emit(VerificationEvent.Cancel, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,31 +281,32 @@ export class VerificationBase extends EventEmitter {
|
||||
* @returns {Promise} Promise which resolves when the verification has
|
||||
* completed.
|
||||
*/
|
||||
verify() {
|
||||
if (this._promise) return this._promise;
|
||||
public verify(): Promise<void> {
|
||||
if (this.promise) return this.promise;
|
||||
|
||||
this._promise = new Promise((resolve, reject) => {
|
||||
this._resolve = (...args) => {
|
||||
this.promise = new Promise((resolve, reject) => {
|
||||
this.resolve = (...args) => {
|
||||
this._done = true;
|
||||
this._endTimer();
|
||||
this.endTimer();
|
||||
resolve(...args);
|
||||
};
|
||||
this._reject = (...args) => {
|
||||
this.reject = (e: Error) => {
|
||||
this._done = true;
|
||||
this._endTimer();
|
||||
reject(...args);
|
||||
this.endTimer();
|
||||
reject(e);
|
||||
};
|
||||
});
|
||||
if (this._doVerification && !this._started) {
|
||||
this._started = true;
|
||||
this._resetTimer(); // restart the timeout
|
||||
Promise.resolve(this._doVerification())
|
||||
.then(this.done.bind(this), this.cancel.bind(this));
|
||||
if (this.doVerification && !this.started) {
|
||||
this.started = true;
|
||||
this.resetTimer(); // restart the timeout
|
||||
Promise.resolve(this.doVerification()).then(this.done.bind(this), this.cancel.bind(this));
|
||||
}
|
||||
return this._promise;
|
||||
return this.promise;
|
||||
}
|
||||
|
||||
async _verifyKeys(userId, keys, verifier) {
|
||||
protected doVerification?: () => Promise<void>;
|
||||
|
||||
protected async verifyKeys(userId: string, keys: Record<string, string>, verifier: KeyVerifier): Promise<void> {
|
||||
// we try to verify all the keys that we're told about, but we might
|
||||
// not know about all of them, so keep track of the keys that we know
|
||||
// about, and ignore the rest
|
||||
@@ -287,15 +314,14 @@ export class VerificationBase extends EventEmitter {
|
||||
|
||||
for (const [keyId, keyInfo] of Object.entries(keys)) {
|
||||
const deviceId = keyId.split(':', 2)[1];
|
||||
const device = this._baseApis.getStoredDevice(userId, deviceId);
|
||||
const device = this.baseApis.getStoredDevice(userId, deviceId);
|
||||
if (device) {
|
||||
await verifier(keyId, device, keyInfo);
|
||||
verifier(keyId, device, keyInfo);
|
||||
verifiedDevices.push(deviceId);
|
||||
} else {
|
||||
const crossSigningInfo = this._baseApis.crypto.deviceList
|
||||
.getStoredCrossSigningForUser(userId);
|
||||
const crossSigningInfo = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(userId);
|
||||
if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
|
||||
await verifier(keyId, DeviceInfo.fromStorage({
|
||||
verifier(keyId, DeviceInfo.fromStorage({
|
||||
keys: {
|
||||
[keyId]: deviceId,
|
||||
},
|
||||
@@ -323,7 +349,11 @@ export class VerificationBase extends EventEmitter {
|
||||
// to upload each signature in a separate API call which is silly because the
|
||||
// API supports as many signatures as you like.
|
||||
for (const deviceId of verifiedDevices) {
|
||||
await this._baseApis.setDeviceVerified(userId, deviceId);
|
||||
await this.baseApis.setDeviceVerified(userId, deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
public get events(): string[] | undefined {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -22,17 +22,17 @@ limitations under the License.
|
||||
|
||||
import { MatrixEvent } from "../../models/event";
|
||||
|
||||
export function newVerificationError(code, reason, extradata) {
|
||||
const content = Object.assign({}, { code, reason }, extradata);
|
||||
export function newVerificationError(code: string, reason: string, extraData: Record<string, any>): MatrixEvent {
|
||||
const content = Object.assign({}, { code, reason }, extraData);
|
||||
return new MatrixEvent({
|
||||
type: "m.key.verification.cancel",
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
export function errorFactory(code, reason) {
|
||||
return function(extradata) {
|
||||
return newVerificationError(code, reason, extradata);
|
||||
export function errorFactory(code: string, reason: string): (extraData?: Record<string, any>) => MatrixEvent {
|
||||
return function(extraData?: Record<string, any>) {
|
||||
return newVerificationError(code, reason, extraData);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export const newInvalidMessageError = errorFactory(
|
||||
"m.invalid_message", "Invalid message",
|
||||
);
|
||||
|
||||
export function errorFromEvent(event) {
|
||||
export function errorFromEvent(event: MatrixEvent): { code: string, reason: string } {
|
||||
const content = event.getContent();
|
||||
if (content) {
|
||||
const { code, reason } = content;
|
||||
+19
-7
@@ -20,24 +20,36 @@ limitations under the License.
|
||||
* @module crypto/verification/IllegalMethod
|
||||
*/
|
||||
|
||||
import { VerificationBase as Base } from "./Base";
|
||||
import { VerificationBase as Base, VerificationEvent, VerificationEventHandlerMap } from "./Base";
|
||||
import { IVerificationChannel } from "./request/Channel";
|
||||
import { MatrixClient } from "../../client";
|
||||
import { MatrixEvent } from "../../models/event";
|
||||
import { VerificationRequest } from "./request/VerificationRequest";
|
||||
|
||||
/**
|
||||
* @class crypto/verification/IllegalMethod/IllegalMethod
|
||||
* @extends {module:crypto/verification/Base}
|
||||
*/
|
||||
export class IllegalMethod extends Base {
|
||||
static factory(...args) {
|
||||
return new IllegalMethod(...args);
|
||||
export class IllegalMethod extends Base<VerificationEvent, VerificationEventHandlerMap> {
|
||||
public static factory(
|
||||
channel: IVerificationChannel,
|
||||
baseApis: MatrixClient,
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
startEvent: MatrixEvent,
|
||||
request: VerificationRequest,
|
||||
): IllegalMethod {
|
||||
return new IllegalMethod(channel, baseApis, userId, deviceId, startEvent, request);
|
||||
}
|
||||
|
||||
static get NAME() {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
public static get NAME(): string {
|
||||
// Typically the name will be something else, but to complete
|
||||
// the contract we offer a default one here.
|
||||
return "org.matrix.illegal_method";
|
||||
}
|
||||
|
||||
async _doVerification() {
|
||||
protected doVerification = async (): Promise<void> => {
|
||||
throw new Error("Verification is not possible with this method");
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -20,31 +19,55 @@ limitations under the License.
|
||||
* @module crypto/verification/QRCode
|
||||
*/
|
||||
|
||||
import { VerificationBase as Base } from "./Base";
|
||||
import {
|
||||
newKeyMismatchError,
|
||||
newUserCancelledError,
|
||||
} from './Error';
|
||||
import { encodeUnpaddedBase64, decodeBase64 } from "../olmlib";
|
||||
import { VerificationBase as Base, VerificationEventHandlerMap } from "./Base";
|
||||
import { newKeyMismatchError, newUserCancelledError } from './Error';
|
||||
import { decodeBase64, encodeUnpaddedBase64 } from "../olmlib";
|
||||
import { logger } from '../../logger';
|
||||
import { VerificationRequest } from "./request/VerificationRequest";
|
||||
import { MatrixClient } from "../../client";
|
||||
import { IVerificationChannel } from "./request/Channel";
|
||||
import { MatrixEvent } from "../../models/event";
|
||||
|
||||
export const SHOW_QR_CODE_METHOD = "m.qr_code.show.v1";
|
||||
export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1";
|
||||
|
||||
interface IReciprocateQr {
|
||||
confirm(): void;
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
export enum QrCodeEvent {
|
||||
ShowReciprocateQr = "show_reciprocate_qr",
|
||||
}
|
||||
|
||||
type EventHandlerMap = {
|
||||
[QrCodeEvent.ShowReciprocateQr]: (qr: IReciprocateQr) => void;
|
||||
} & VerificationEventHandlerMap;
|
||||
|
||||
/**
|
||||
* @class crypto/verification/QRCode/ReciprocateQRCode
|
||||
* @extends {module:crypto/verification/Base}
|
||||
*/
|
||||
export class ReciprocateQRCode extends Base {
|
||||
static factory(...args) {
|
||||
return new ReciprocateQRCode(...args);
|
||||
export class ReciprocateQRCode extends Base<QrCodeEvent, EventHandlerMap> {
|
||||
public reciprocateQREvent: IReciprocateQr;
|
||||
|
||||
public static factory(
|
||||
channel: IVerificationChannel,
|
||||
baseApis: MatrixClient,
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
startEvent: MatrixEvent,
|
||||
request: VerificationRequest,
|
||||
): ReciprocateQRCode {
|
||||
return new ReciprocateQRCode(channel, baseApis, userId, deviceId, startEvent, request);
|
||||
}
|
||||
|
||||
static get NAME() {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
public static get NAME(): string {
|
||||
return "m.reciprocate.v1";
|
||||
}
|
||||
|
||||
async _doVerification() {
|
||||
protected doVerification = async (): Promise<void> => {
|
||||
if (!this.startEvent) {
|
||||
// TODO: Support scanning QR codes
|
||||
throw new Error("It is not currently possible to start verification" +
|
||||
@@ -58,30 +81,30 @@ export class ReciprocateQRCode extends Base {
|
||||
}
|
||||
|
||||
// 2. ask if other user shows shield as well
|
||||
await new Promise((resolve, reject) => {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
this.reciprocateQREvent = {
|
||||
confirm: resolve,
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
};
|
||||
this.emit("show_reciprocate_qr", this.reciprocateQREvent);
|
||||
this.emit(QrCodeEvent.ShowReciprocateQr, this.reciprocateQREvent);
|
||||
});
|
||||
|
||||
// 3. determine key to sign / mark as trusted
|
||||
const keys = {};
|
||||
const keys: Record<string, string> = {};
|
||||
|
||||
switch (qrCodeData.mode) {
|
||||
case MODE_VERIFY_OTHER_USER: {
|
||||
case Mode.VerifyOtherUser: {
|
||||
// add master key to keys to be signed, only if we're not doing self-verification
|
||||
const masterKey = qrCodeData.otherUserMasterKey;
|
||||
keys[`ed25519:${masterKey}`] = masterKey;
|
||||
break;
|
||||
}
|
||||
case MODE_VERIFY_SELF_TRUSTED: {
|
||||
case Mode.VerifySelfTrusted: {
|
||||
const deviceId = this.request.targetDevice.deviceId;
|
||||
keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey;
|
||||
break;
|
||||
}
|
||||
case MODE_VERIFY_SELF_UNTRUSTED: {
|
||||
case Mode.VerifySelfUntrusted: {
|
||||
const masterKey = qrCodeData.myMasterKey;
|
||||
keys[`ed25519:${masterKey}`] = masterKey;
|
||||
break;
|
||||
@@ -89,7 +112,7 @@ export class ReciprocateQRCode extends Base {
|
||||
}
|
||||
|
||||
// 4. sign the key (or mark own MSK as verified in case of MODE_VERIFY_SELF_TRUSTED)
|
||||
await this._verifyKeys(this.userId, keys, (keyId, device, keyInfo) => {
|
||||
await this.verifyKeys(this.userId, keys, (keyId, device, keyInfo) => {
|
||||
// make sure the device has the expected keys
|
||||
const targetKey = keys[keyId];
|
||||
if (!targetKey) throw newKeyMismatchError();
|
||||
@@ -108,103 +131,88 @@ export class ReciprocateQRCode extends Base {
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const CODE_VERSION = 0x02; // the version of binary QR codes we support
|
||||
const BINARY_PREFIX = "MATRIX"; // ASCII, used to prefix the binary format
|
||||
const MODE_VERIFY_OTHER_USER = 0x00; // Verifying someone who isn't us
|
||||
const MODE_VERIFY_SELF_TRUSTED = 0x01; // We trust the master key
|
||||
const MODE_VERIFY_SELF_UNTRUSTED = 0x02; // We do not trust the master key
|
||||
|
||||
enum Mode {
|
||||
VerifyOtherUser = 0x00, // Verifying someone who isn't us
|
||||
VerifySelfTrusted = 0x01, // We trust the master key
|
||||
VerifySelfUntrusted = 0x02, // We do not trust the master key
|
||||
}
|
||||
|
||||
interface IQrData {
|
||||
prefix: string;
|
||||
version: number;
|
||||
mode: Mode;
|
||||
transactionId: string;
|
||||
firstKeyB64: string;
|
||||
secondKeyB64: string;
|
||||
secretB64: string;
|
||||
}
|
||||
|
||||
export class QRCodeData {
|
||||
constructor(
|
||||
mode, sharedSecret, otherUserMasterKey,
|
||||
otherDeviceKey, myMasterKey, buffer,
|
||||
) {
|
||||
this._sharedSecret = sharedSecret;
|
||||
this._mode = mode;
|
||||
this._otherUserMasterKey = otherUserMasterKey;
|
||||
this._otherDeviceKey = otherDeviceKey;
|
||||
this._myMasterKey = myMasterKey;
|
||||
this._buffer = buffer;
|
||||
}
|
||||
public readonly mode: Mode,
|
||||
private readonly sharedSecret: string,
|
||||
// only set when mode is MODE_VERIFY_OTHER_USER, master key of other party at time of generating QR code
|
||||
public readonly otherUserMasterKey: string | undefined,
|
||||
// only set when mode is MODE_VERIFY_SELF_TRUSTED, device key of other party at time of generating QR code
|
||||
public readonly otherDeviceKey: string | undefined,
|
||||
// only set when mode is MODE_VERIFY_SELF_UNTRUSTED, own master key at time of generating QR code
|
||||
public readonly myMasterKey: string | undefined,
|
||||
private readonly buffer: Buffer,
|
||||
) {}
|
||||
|
||||
static async create(request, client) {
|
||||
const sharedSecret = QRCodeData._generateSharedSecret();
|
||||
const mode = QRCodeData._determineMode(request, client);
|
||||
public static async create(request: VerificationRequest, client: MatrixClient): Promise<QRCodeData> {
|
||||
const sharedSecret = QRCodeData.generateSharedSecret();
|
||||
const mode = QRCodeData.determineMode(request, client);
|
||||
let otherUserMasterKey = null;
|
||||
let otherDeviceKey = null;
|
||||
let myMasterKey = null;
|
||||
if (mode === MODE_VERIFY_OTHER_USER) {
|
||||
if (mode === Mode.VerifyOtherUser) {
|
||||
const otherUserCrossSigningInfo =
|
||||
client.getStoredCrossSigningForUser(request.otherUserId);
|
||||
otherUserMasterKey = otherUserCrossSigningInfo.getId("master");
|
||||
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
|
||||
otherDeviceKey = await QRCodeData._getOtherDeviceKey(request, client);
|
||||
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) {
|
||||
} else if (mode === Mode.VerifySelfTrusted) {
|
||||
otherDeviceKey = await QRCodeData.getOtherDeviceKey(request, client);
|
||||
} else if (mode === Mode.VerifySelfUntrusted) {
|
||||
const myUserId = client.getUserId();
|
||||
const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
|
||||
myMasterKey = myCrossSigningInfo.getId("master");
|
||||
}
|
||||
const qrData = QRCodeData._generateQrData(
|
||||
const qrData = QRCodeData.generateQrData(
|
||||
request, client, mode,
|
||||
sharedSecret,
|
||||
otherUserMasterKey,
|
||||
otherDeviceKey,
|
||||
myMasterKey,
|
||||
);
|
||||
const buffer = QRCodeData._generateBuffer(qrData);
|
||||
const buffer = QRCodeData.generateBuffer(qrData);
|
||||
return new QRCodeData(mode, sharedSecret,
|
||||
otherUserMasterKey, otherDeviceKey, myMasterKey, buffer);
|
||||
}
|
||||
|
||||
get buffer() {
|
||||
return this._buffer;
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this._mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* only set when mode is MODE_VERIFY_SELF_TRUSTED
|
||||
* @return {string} device key of other party at time of generating QR code
|
||||
*/
|
||||
get otherDeviceKey() {
|
||||
return this._otherDeviceKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* only set when mode is MODE_VERIFY_OTHER_USER
|
||||
* @return {string} master key of other party at time of generating QR code
|
||||
*/
|
||||
get otherUserMasterKey() {
|
||||
return this._otherUserMasterKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* only set when mode is MODE_VERIFY_SELF_UNTRUSTED
|
||||
* @return {string} own master key at time of generating QR code
|
||||
*/
|
||||
get myMasterKey() {
|
||||
return this._myMasterKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* The unpadded base64 encoded shared secret.
|
||||
*/
|
||||
get encodedSharedSecret() {
|
||||
return this._sharedSecret;
|
||||
public get encodedSharedSecret(): string {
|
||||
return this.sharedSecret;
|
||||
}
|
||||
|
||||
static _generateSharedSecret() {
|
||||
public getBuffer(): Buffer {
|
||||
return this.buffer;
|
||||
}
|
||||
|
||||
private static generateSharedSecret(): string {
|
||||
const secretBytes = new Uint8Array(11);
|
||||
global.crypto.getRandomValues(secretBytes);
|
||||
return encodeUnpaddedBase64(secretBytes);
|
||||
}
|
||||
|
||||
static async _getOtherDeviceKey(request, client) {
|
||||
private static async getOtherDeviceKey(request: VerificationRequest, client: MatrixClient): Promise<string> {
|
||||
const myUserId = client.getUserId();
|
||||
const otherDevice = request.targetDevice;
|
||||
const otherDeviceId = otherDevice ? otherDevice.deviceId : null;
|
||||
@@ -212,31 +220,35 @@ export class QRCodeData {
|
||||
if (!device) {
|
||||
throw new Error("could not find device " + otherDeviceId);
|
||||
}
|
||||
const key = device.getFingerprint();
|
||||
return key;
|
||||
return device.getFingerprint();
|
||||
}
|
||||
|
||||
static _determineMode(request, client) {
|
||||
private static determineMode(request: VerificationRequest, client: MatrixClient): Mode {
|
||||
const myUserId = client.getUserId();
|
||||
const otherUserId = request.otherUserId;
|
||||
|
||||
let mode = MODE_VERIFY_OTHER_USER;
|
||||
let mode = Mode.VerifyOtherUser;
|
||||
if (myUserId === otherUserId) {
|
||||
// Mode changes depending on whether or not we trust the master cross signing key
|
||||
const myTrust = client.checkUserTrust(myUserId);
|
||||
if (myTrust.isCrossSigningVerified()) {
|
||||
mode = MODE_VERIFY_SELF_TRUSTED;
|
||||
mode = Mode.VerifySelfTrusted;
|
||||
} else {
|
||||
mode = MODE_VERIFY_SELF_UNTRUSTED;
|
||||
mode = Mode.VerifySelfUntrusted;
|
||||
}
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
|
||||
static _generateQrData(request, client, mode,
|
||||
encodedSharedSecret, otherUserMasterKey,
|
||||
otherDeviceKey, myMasterKey,
|
||||
) {
|
||||
private static generateQrData(
|
||||
request: VerificationRequest,
|
||||
client: MatrixClient,
|
||||
mode: Mode,
|
||||
encodedSharedSecret: string,
|
||||
otherUserMasterKey: string,
|
||||
otherDeviceKey: string,
|
||||
myMasterKey: string,
|
||||
): IQrData {
|
||||
const myUserId = client.getUserId();
|
||||
const transactionId = request.channel.transactionId;
|
||||
const qrData = {
|
||||
@@ -251,16 +263,16 @@ export class QRCodeData {
|
||||
|
||||
const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
|
||||
|
||||
if (mode === MODE_VERIFY_OTHER_USER) {
|
||||
if (mode === Mode.VerifyOtherUser) {
|
||||
// First key is our master cross signing key
|
||||
qrData.firstKeyB64 = myCrossSigningInfo.getId("master");
|
||||
// Second key is the other user's master cross signing key
|
||||
qrData.secondKeyB64 = otherUserMasterKey;
|
||||
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
|
||||
} else if (mode === Mode.VerifySelfTrusted) {
|
||||
// First key is our master cross signing key
|
||||
qrData.firstKeyB64 = myCrossSigningInfo.getId("master");
|
||||
qrData.secondKeyB64 = otherDeviceKey;
|
||||
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) {
|
||||
} else if (mode === Mode.VerifySelfUntrusted) {
|
||||
// First key is our device's key
|
||||
qrData.firstKeyB64 = client.getDeviceEd25519Key();
|
||||
// Second key is what we think our master cross signing key is
|
||||
@@ -269,7 +281,7 @@ export class QRCodeData {
|
||||
return qrData;
|
||||
}
|
||||
|
||||
static _generateBuffer(qrData) {
|
||||
private static generateBuffer(qrData: IQrData): Buffer {
|
||||
let buf = Buffer.alloc(0); // we'll concat our way through life
|
||||
|
||||
const appendByte = (b) => {
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -19,8 +19,10 @@ limitations under the License.
|
||||
* @module crypto/verification/SAS
|
||||
*/
|
||||
|
||||
import { VerificationBase as Base, SwitchStartEventError } from "./Base";
|
||||
import anotherjson from 'another-json';
|
||||
import { Utility, SAS as OlmSAS } from "@matrix-org/olm";
|
||||
|
||||
import { VerificationBase as Base, SwitchStartEventError, VerificationEventHandlerMap } from "./Base";
|
||||
import {
|
||||
errorFactory,
|
||||
newInvalidMessageError,
|
||||
@@ -29,6 +31,7 @@ import {
|
||||
newUserCancelledError,
|
||||
} from './Error';
|
||||
import { logger } from '../../logger';
|
||||
import { IContent, MatrixEvent } from "../../models/event";
|
||||
|
||||
const START_TYPE = "m.key.verification.start";
|
||||
|
||||
@@ -38,7 +41,7 @@ const EVENTS = [
|
||||
"m.key.verification.mac",
|
||||
];
|
||||
|
||||
let olmutil;
|
||||
let olmutil: Utility;
|
||||
|
||||
const newMismatchedSASError = errorFactory(
|
||||
"m.mismatched_sas", "Mismatched short authentication string",
|
||||
@@ -48,7 +51,7 @@ const newMismatchedCommitmentError = errorFactory(
|
||||
"m.mismatched_commitment", "Mismatched commitment",
|
||||
);
|
||||
|
||||
function generateDecimalSas(sasBytes) {
|
||||
function generateDecimalSas(sasBytes: number[]): [number, number, number] {
|
||||
/**
|
||||
* +--------+--------+--------+--------+--------+
|
||||
* | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 |
|
||||
@@ -64,7 +67,9 @@ function generateDecimalSas(sasBytes) {
|
||||
];
|
||||
}
|
||||
|
||||
const emojiMapping = [
|
||||
type EmojiMapping = [emoji: string, name: string];
|
||||
|
||||
const emojiMapping: EmojiMapping[] = [
|
||||
["🐶", "dog"], // 0
|
||||
["🐱", "cat"], // 1
|
||||
["🦁", "lion"], // 2
|
||||
@@ -131,7 +136,7 @@ const emojiMapping = [
|
||||
["📌", "pin"], // 63
|
||||
];
|
||||
|
||||
function generateEmojiSas(sasBytes) {
|
||||
function generateEmojiSas(sasBytes: number[]): EmojiMapping[] {
|
||||
const emojis = [
|
||||
// just like base64 encoding
|
||||
sasBytes[0] >> 2,
|
||||
@@ -151,8 +156,20 @@ const sasGenerators = {
|
||||
emoji: generateEmojiSas,
|
||||
};
|
||||
|
||||
function generateSas(sasBytes, methods) {
|
||||
const sas = {};
|
||||
export interface IGeneratedSas {
|
||||
decimal?: [number, number, number];
|
||||
emoji?: EmojiMapping[];
|
||||
}
|
||||
|
||||
export interface ISasEvent {
|
||||
sas: IGeneratedSas;
|
||||
confirm(): Promise<void>;
|
||||
cancel(): void;
|
||||
mismatch(): void;
|
||||
}
|
||||
|
||||
function generateSas(sasBytes: number[], methods: string[]): IGeneratedSas {
|
||||
const sas: IGeneratedSas = {};
|
||||
for (const method of methods) {
|
||||
if (method in sasGenerators) {
|
||||
sas[method] = sasGenerators[method](sasBytes);
|
||||
@@ -166,7 +183,7 @@ const macMethods = {
|
||||
"hmac-sha256": "calculate_mac_long_kdf",
|
||||
};
|
||||
|
||||
function calculateMAC(olmSAS, method) {
|
||||
function calculateMAC(olmSAS: OlmSAS, method: string) {
|
||||
return function(...args) {
|
||||
const macFunction = olmSAS[macMethods[method]];
|
||||
const mac = macFunction.apply(olmSAS, args);
|
||||
@@ -176,23 +193,23 @@ function calculateMAC(olmSAS, method) {
|
||||
}
|
||||
|
||||
const calculateKeyAgreement = {
|
||||
"curve25519-hkdf-sha256": function(sas, olmSAS, bytes) {
|
||||
const ourInfo = `${sas._baseApis.getUserId()}|${sas._baseApis.deviceId}|`
|
||||
"curve25519-hkdf-sha256": function(sas: SAS, olmSAS: OlmSAS, bytes: number): Uint8Array {
|
||||
const ourInfo = `${sas.baseApis.getUserId()}|${sas.baseApis.deviceId}|`
|
||||
+ `${sas.ourSASPubKey}|`;
|
||||
const theirInfo = `${sas.userId}|${sas.deviceId}|${sas.theirSASPubKey}|`;
|
||||
const sasInfo =
|
||||
"MATRIX_KEY_VERIFICATION_SAS|"
|
||||
+ (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo)
|
||||
+ sas._channel.transactionId;
|
||||
+ sas.channel.transactionId;
|
||||
return olmSAS.generate_bytes(sasInfo, bytes);
|
||||
},
|
||||
"curve25519": function(sas, olmSAS, bytes) {
|
||||
const ourInfo = `${sas._baseApis.getUserId()}${sas._baseApis.deviceId}`;
|
||||
"curve25519": function(sas: SAS, olmSAS: OlmSAS, bytes: number): Uint8Array {
|
||||
const ourInfo = `${sas.baseApis.getUserId()}${sas.baseApis.deviceId}`;
|
||||
const theirInfo = `${sas.userId}${sas.deviceId}`;
|
||||
const sasInfo =
|
||||
"MATRIX_KEY_VERIFICATION_SAS"
|
||||
+ (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo)
|
||||
+ sas._channel.transactionId;
|
||||
+ sas.channel.transactionId;
|
||||
return olmSAS.generate_bytes(sasInfo, bytes);
|
||||
},
|
||||
};
|
||||
@@ -211,37 +228,51 @@ const HASHES_SET = new Set(HASHES_LIST);
|
||||
const MAC_SET = new Set(MAC_LIST);
|
||||
const SAS_SET = new Set(SAS_LIST);
|
||||
|
||||
function intersection(anArray, aSet) {
|
||||
function intersection<T>(anArray: T[], aSet: Set<T>): T[] {
|
||||
return anArray instanceof Array ? anArray.filter(x => aSet.has(x)) : [];
|
||||
}
|
||||
|
||||
export enum SasEvent {
|
||||
ShowSas = "show_sas",
|
||||
}
|
||||
|
||||
type EventHandlerMap = {
|
||||
[SasEvent.ShowSas]: (sas: ISasEvent) => void;
|
||||
} & VerificationEventHandlerMap;
|
||||
|
||||
/**
|
||||
* @alias module:crypto/verification/SAS
|
||||
* @extends {module:crypto/verification/Base}
|
||||
*/
|
||||
export class SAS extends Base {
|
||||
static get NAME() {
|
||||
export class SAS extends Base<SasEvent, EventHandlerMap> {
|
||||
private waitingForAccept: boolean;
|
||||
public ourSASPubKey: string;
|
||||
public theirSASPubKey: string;
|
||||
public sasEvent: ISasEvent;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
public static get NAME(): string {
|
||||
return "m.sas.v1";
|
||||
}
|
||||
|
||||
get events() {
|
||||
public get events(): string[] {
|
||||
return EVENTS;
|
||||
}
|
||||
|
||||
async _doVerification() {
|
||||
protected doVerification = async (): Promise<void> => {
|
||||
await global.Olm.init();
|
||||
olmutil = olmutil || new global.Olm.Utility();
|
||||
|
||||
// make sure user's keys are downloaded
|
||||
await this._baseApis.downloadKeys([this.userId]);
|
||||
await this.baseApis.downloadKeys([this.userId]);
|
||||
|
||||
let retry = false;
|
||||
do {
|
||||
try {
|
||||
if (this.initiatedByMe) {
|
||||
return await this._doSendVerification();
|
||||
return await this.doSendVerification();
|
||||
} else {
|
||||
return await this._doRespondVerification();
|
||||
return await this.doRespondVerification();
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof SwitchStartEventError) {
|
||||
@@ -253,38 +284,37 @@ export class SAS extends Base {
|
||||
}
|
||||
}
|
||||
} while (retry);
|
||||
}
|
||||
};
|
||||
|
||||
canSwitchStartEvent(event) {
|
||||
public canSwitchStartEvent(event: MatrixEvent): boolean {
|
||||
if (event.getType() !== START_TYPE) {
|
||||
return false;
|
||||
}
|
||||
const content = event.getContent();
|
||||
return content && content.method === SAS.NAME &&
|
||||
this._waitingForAccept;
|
||||
return content && content.method === SAS.NAME && this.waitingForAccept;
|
||||
}
|
||||
|
||||
async _sendStart() {
|
||||
const startContent = this._channel.completeContent(START_TYPE, {
|
||||
private async sendStart(): Promise<Record<string, any>> {
|
||||
const startContent = this.channel.completeContent(START_TYPE, {
|
||||
method: SAS.NAME,
|
||||
from_device: this._baseApis.deviceId,
|
||||
from_device: this.baseApis.deviceId,
|
||||
key_agreement_protocols: KEY_AGREEMENT_LIST,
|
||||
hashes: HASHES_LIST,
|
||||
message_authentication_codes: MAC_LIST,
|
||||
// FIXME: allow app to specify what SAS methods can be used
|
||||
short_authentication_string: SAS_LIST,
|
||||
});
|
||||
await this._channel.sendCompleted(START_TYPE, startContent);
|
||||
await this.channel.sendCompleted(START_TYPE, startContent);
|
||||
return startContent;
|
||||
}
|
||||
|
||||
async _doSendVerification() {
|
||||
this._waitingForAccept = true;
|
||||
private async doSendVerification(): Promise<void> {
|
||||
this.waitingForAccept = true;
|
||||
let startContent;
|
||||
if (this.startEvent) {
|
||||
startContent = this._channel.completedContentFromEvent(this.startEvent);
|
||||
startContent = this.channel.completedContentFromEvent(this.startEvent);
|
||||
} else {
|
||||
startContent = await this._sendStart();
|
||||
startContent = await this.sendStart();
|
||||
}
|
||||
|
||||
// we might have switched to a different start event,
|
||||
@@ -297,9 +327,9 @@ export class SAS extends Base {
|
||||
|
||||
let e;
|
||||
try {
|
||||
e = await this._waitForEvent("m.key.verification.accept");
|
||||
e = await this.waitForEvent("m.key.verification.accept");
|
||||
} finally {
|
||||
this._waitingForAccept = false;
|
||||
this.waitingForAccept = false;
|
||||
}
|
||||
let content = e.getContent();
|
||||
const sasMethods
|
||||
@@ -319,11 +349,11 @@ export class SAS extends Base {
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
try {
|
||||
this.ourSASPubKey = olmSAS.get_pubkey();
|
||||
await this._send("m.key.verification.key", {
|
||||
await this.send("m.key.verification.key", {
|
||||
key: this.ourSASPubKey,
|
||||
});
|
||||
|
||||
e = await this._waitForEvent("m.key.verification.key");
|
||||
e = await this.waitForEvent("m.key.verification.key");
|
||||
// FIXME: make sure event is properly formed
|
||||
content = e.getContent();
|
||||
const commitmentStr = content.key + anotherjson.stringify(startContent);
|
||||
@@ -335,12 +365,12 @@ export class SAS extends Base {
|
||||
olmSAS.set_their_key(content.key);
|
||||
|
||||
const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
|
||||
const verifySAS = new Promise((resolve, reject) => {
|
||||
const verifySAS = new Promise<void>((resolve, reject) => {
|
||||
this.sasEvent = {
|
||||
sas: generateSas(sasBytes, sasMethods),
|
||||
confirm: async () => {
|
||||
try {
|
||||
await this._sendMAC(olmSAS, macMethod);
|
||||
await this.sendMAC(olmSAS, macMethod);
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
@@ -349,58 +379,49 @@ export class SAS extends Base {
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
mismatch: () => reject(newMismatchedSASError()),
|
||||
};
|
||||
this.emit("show_sas", this.sasEvent);
|
||||
this.emit(SasEvent.ShowSas, this.sasEvent);
|
||||
});
|
||||
|
||||
[e] = await Promise.all([
|
||||
this._waitForEvent("m.key.verification.mac")
|
||||
this.waitForEvent("m.key.verification.mac")
|
||||
.then((e) => {
|
||||
// we don't expect any more messages from the other
|
||||
// party, and they may send a m.key.verification.done
|
||||
// when they're done on their end
|
||||
this._expectedEvent = "m.key.verification.done";
|
||||
this.expectedEvent = "m.key.verification.done";
|
||||
return e;
|
||||
}),
|
||||
verifySAS,
|
||||
]);
|
||||
content = e.getContent();
|
||||
await this._checkMAC(olmSAS, content, macMethod);
|
||||
await this.checkMAC(olmSAS, content, macMethod);
|
||||
} finally {
|
||||
olmSAS.free();
|
||||
}
|
||||
}
|
||||
|
||||
async _doRespondVerification() {
|
||||
private async doRespondVerification(): Promise<void> {
|
||||
// as m.related_to is not included in the encrypted content in e2e rooms,
|
||||
// we need to make sure it is added
|
||||
let content = this._channel.completedContentFromEvent(this.startEvent);
|
||||
let content = this.channel.completedContentFromEvent(this.startEvent);
|
||||
|
||||
// Note: we intersect using our pre-made lists, rather than the sets,
|
||||
// so that the result will be in our order of preference. Then
|
||||
// fetching the first element from the array will give our preferred
|
||||
// method out of the ones offered by the other party.
|
||||
const keyAgreement
|
||||
= intersection(
|
||||
KEY_AGREEMENT_LIST, new Set(content.key_agreement_protocols),
|
||||
)[0];
|
||||
const hashMethod
|
||||
= intersection(HASHES_LIST, new Set(content.hashes))[0];
|
||||
const macMethod
|
||||
= intersection(MAC_LIST, new Set(content.message_authentication_codes))[0];
|
||||
const keyAgreement = intersection(KEY_AGREEMENT_LIST, new Set(content.key_agreement_protocols))[0];
|
||||
const hashMethod = intersection(HASHES_LIST, new Set(content.hashes))[0];
|
||||
const macMethod = intersection(MAC_LIST, new Set(content.message_authentication_codes))[0];
|
||||
// FIXME: allow app to specify what SAS methods can be used
|
||||
const sasMethods
|
||||
= intersection(content.short_authentication_string, SAS_SET);
|
||||
if (!(keyAgreement !== undefined
|
||||
&& hashMethod !== undefined
|
||||
&& macMethod !== undefined
|
||||
&& sasMethods.length)) {
|
||||
const sasMethods = intersection(content.short_authentication_string, SAS_SET);
|
||||
if (!(keyAgreement !== undefined && hashMethod !== undefined && macMethod !== undefined && sasMethods.length)) {
|
||||
throw newUnknownMethodError();
|
||||
}
|
||||
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
try {
|
||||
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
|
||||
await this._send("m.key.verification.accept", {
|
||||
await this.send("m.key.verification.accept", {
|
||||
key_agreement_protocol: keyAgreement,
|
||||
hash: hashMethod,
|
||||
message_authentication_code: macMethod,
|
||||
@@ -409,23 +430,23 @@ export class SAS extends Base {
|
||||
commitment: olmutil.sha256(commitmentStr),
|
||||
});
|
||||
|
||||
let e = await this._waitForEvent("m.key.verification.key");
|
||||
let e = await this.waitForEvent("m.key.verification.key");
|
||||
// FIXME: make sure event is properly formed
|
||||
content = e.getContent();
|
||||
this.theirSASPubKey = content.key;
|
||||
olmSAS.set_their_key(content.key);
|
||||
this.ourSASPubKey = olmSAS.get_pubkey();
|
||||
await this._send("m.key.verification.key", {
|
||||
await this.send("m.key.verification.key", {
|
||||
key: this.ourSASPubKey,
|
||||
});
|
||||
|
||||
const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
|
||||
const verifySAS = new Promise((resolve, reject) => {
|
||||
const verifySAS = new Promise<void>((resolve, reject) => {
|
||||
this.sasEvent = {
|
||||
sas: generateSas(sasBytes, sasMethods),
|
||||
confirm: async () => {
|
||||
try {
|
||||
await this._sendMAC(olmSAS, macMethod);
|
||||
await this.sendMAC(olmSAS, macMethod);
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
@@ -434,43 +455,43 @@ export class SAS extends Base {
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
mismatch: () => reject(newMismatchedSASError()),
|
||||
};
|
||||
this.emit("show_sas", this.sasEvent);
|
||||
this.emit(SasEvent.ShowSas, this.sasEvent);
|
||||
});
|
||||
|
||||
[e] = await Promise.all([
|
||||
this._waitForEvent("m.key.verification.mac")
|
||||
this.waitForEvent("m.key.verification.mac")
|
||||
.then((e) => {
|
||||
// we don't expect any more messages from the other
|
||||
// party, and they may send a m.key.verification.done
|
||||
// when they're done on their end
|
||||
this._expectedEvent = "m.key.verification.done";
|
||||
this.expectedEvent = "m.key.verification.done";
|
||||
return e;
|
||||
}),
|
||||
verifySAS,
|
||||
]);
|
||||
content = e.getContent();
|
||||
await this._checkMAC(olmSAS, content, macMethod);
|
||||
await this.checkMAC(olmSAS, content, macMethod);
|
||||
} finally {
|
||||
olmSAS.free();
|
||||
}
|
||||
}
|
||||
|
||||
_sendMAC(olmSAS, method) {
|
||||
private sendMAC(olmSAS: OlmSAS, method: string): Promise<void> {
|
||||
const mac = {};
|
||||
const keyList = [];
|
||||
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
|
||||
+ this._baseApis.getUserId() + this._baseApis.deviceId
|
||||
+ this.baseApis.getUserId() + this.baseApis.deviceId
|
||||
+ this.userId + this.deviceId
|
||||
+ this._channel.transactionId;
|
||||
+ this.channel.transactionId;
|
||||
|
||||
const deviceKeyId = `ed25519:${this._baseApis.deviceId}`;
|
||||
const deviceKeyId = `ed25519:${this.baseApis.deviceId}`;
|
||||
mac[deviceKeyId] = calculateMAC(olmSAS, method)(
|
||||
this._baseApis.getDeviceEd25519Key(),
|
||||
this.baseApis.getDeviceEd25519Key(),
|
||||
baseInfo + deviceKeyId,
|
||||
);
|
||||
keyList.push(deviceKeyId);
|
||||
|
||||
const crossSigningId = this._baseApis.getCrossSigningId();
|
||||
const crossSigningId = this.baseApis.getCrossSigningId();
|
||||
if (crossSigningId) {
|
||||
const crossSigningKeyId = `ed25519:${crossSigningId}`;
|
||||
mac[crossSigningKeyId] = calculateMAC(olmSAS, method)(
|
||||
@@ -484,14 +505,14 @@ export class SAS extends Base {
|
||||
keyList.sort().join(","),
|
||||
baseInfo + "KEY_IDS",
|
||||
);
|
||||
return this._send("m.key.verification.mac", { mac, keys });
|
||||
return this.send("m.key.verification.mac", { mac, keys });
|
||||
}
|
||||
|
||||
async _checkMAC(olmSAS, content, method) {
|
||||
private async checkMAC(olmSAS: OlmSAS, content: IContent, method: string): Promise<void> {
|
||||
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
|
||||
+ this.userId + this.deviceId
|
||||
+ this._baseApis.getUserId() + this._baseApis.deviceId
|
||||
+ this._channel.transactionId;
|
||||
+ this.baseApis.getUserId() + this.baseApis.deviceId
|
||||
+ this.channel.transactionId;
|
||||
|
||||
if (content.keys !== calculateMAC(olmSAS, method)(
|
||||
Object.keys(content.mac).sort().join(","),
|
||||
@@ -500,7 +521,7 @@ export class SAS extends Base {
|
||||
throw newKeyMismatchError();
|
||||
}
|
||||
|
||||
await this._verifyKeys(this.userId, content.mac, (keyId, device, keyInfo) => {
|
||||
await this.verifyKeys(this.userId, content.mac, (keyId, device, keyInfo) => {
|
||||
if (keyInfo !== calculateMAC(olmSAS, method)(
|
||||
device.keys[keyId],
|
||||
baseInfo + keyId,
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixEvent } from "../../../models/event";
|
||||
import { VerificationRequest } from "./VerificationRequest";
|
||||
|
||||
export interface IVerificationChannel {
|
||||
request?: VerificationRequest;
|
||||
readonly userId: string;
|
||||
readonly roomId?: string;
|
||||
readonly deviceId?: string;
|
||||
readonly transactionId: string;
|
||||
readonly receiveStartFromOtherDevices?: boolean;
|
||||
getTimestamp(event: MatrixEvent): number;
|
||||
send(type: string, uncompletedContent: Record<string, any>): Promise<void>;
|
||||
completeContent(type: string, content: Record<string, any>): Record<string, any>;
|
||||
sendCompleted(type: string, content: Record<string, any>): Promise<void>;
|
||||
completedContentFromEvent(event: MatrixEvent): Record<string, any>;
|
||||
canCreateRequest(type: string): boolean;
|
||||
handleEvent(event: MatrixEvent, request: VerificationRequest, isLiveEvent: boolean): Promise<void>;
|
||||
}
|
||||
+68
-66
@@ -22,8 +22,13 @@ import {
|
||||
START_TYPE,
|
||||
} from "./VerificationRequest";
|
||||
import { logger } from '../../../logger';
|
||||
import { IVerificationChannel } from "./Channel";
|
||||
import { EventType } from "../../../@types/event";
|
||||
import { MatrixClient } from "../../../client";
|
||||
import { MatrixEvent } from "../../../models/event";
|
||||
import { IRequestsMap } from "../..";
|
||||
|
||||
const MESSAGE_TYPE = "m.room.message";
|
||||
const MESSAGE_TYPE = EventType.RoomMessage;
|
||||
const M_REFERENCE = "m.reference";
|
||||
const M_RELATES_TO = "m.relates_to";
|
||||
|
||||
@@ -31,36 +36,34 @@ const M_RELATES_TO = "m.relates_to";
|
||||
* A key verification channel that sends verification events in the timeline of a room.
|
||||
* Uses the event id of the initial m.key.verification.request event as a transaction id.
|
||||
*/
|
||||
export class InRoomChannel {
|
||||
export class InRoomChannel implements IVerificationChannel {
|
||||
private requestEventId: string = null;
|
||||
|
||||
/**
|
||||
* @param {MatrixClient} client the matrix client, to send messages with and get current user & device from.
|
||||
* @param {string} roomId id of the room where verification events should be posted in, should be a DM with the given user.
|
||||
* @param {string} userId id of user that the verification request is directed at, should be present in the room.
|
||||
*/
|
||||
constructor(client, roomId, userId = null) {
|
||||
this._client = client;
|
||||
this._roomId = roomId;
|
||||
this.userId = userId;
|
||||
this._requestEventId = null;
|
||||
constructor(
|
||||
private readonly client: MatrixClient,
|
||||
public readonly roomId: string,
|
||||
public userId: string = null,
|
||||
) {
|
||||
}
|
||||
|
||||
get receiveStartFromOtherDevices() {
|
||||
public get receiveStartFromOtherDevices(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
get roomId() {
|
||||
return this._roomId;
|
||||
}
|
||||
|
||||
/** The transaction id generated/used by this verification channel */
|
||||
get transactionId() {
|
||||
return this._requestEventId;
|
||||
public get transactionId(): string {
|
||||
return this.requestEventId;
|
||||
}
|
||||
|
||||
static getOtherPartyUserId(event, client) {
|
||||
public static getOtherPartyUserId(event: MatrixEvent, client: MatrixClient): string {
|
||||
const type = InRoomChannel.getEventType(event);
|
||||
if (type !== REQUEST_TYPE) {
|
||||
return;
|
||||
return;
|
||||
}
|
||||
const ownUserId = client.getUserId();
|
||||
const sender = event.getSender();
|
||||
@@ -78,25 +81,29 @@ export class InRoomChannel {
|
||||
* @param {MatrixEvent} event the event to get the timestamp of
|
||||
* @return {number} the timestamp when the event was sent
|
||||
*/
|
||||
getTimestamp(event) {
|
||||
public getTimestamp(event: MatrixEvent): number {
|
||||
return event.getTs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel
|
||||
* @param {string} type the event type to check
|
||||
* @returns {bool} boolean flag
|
||||
* @returns {boolean} boolean flag
|
||||
*/
|
||||
static canCreateRequest(type) {
|
||||
public static canCreateRequest(type: string): boolean {
|
||||
return type === REQUEST_TYPE;
|
||||
}
|
||||
|
||||
public canCreateRequest(type: string): boolean {
|
||||
return InRoomChannel.canCreateRequest(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the transaction id used by a given key verification event, if any
|
||||
* @param {MatrixEvent} event the event
|
||||
* @returns {string} the transaction id
|
||||
*/
|
||||
static getTransactionId(event) {
|
||||
public static getTransactionId(event: MatrixEvent): string {
|
||||
if (InRoomChannel.getEventType(event) === REQUEST_TYPE) {
|
||||
return event.getId();
|
||||
} else {
|
||||
@@ -114,9 +121,9 @@ export class InRoomChannel {
|
||||
* `handleEvent` can do more checks and choose to ignore invalid events.
|
||||
* @param {MatrixEvent} event the event to validate
|
||||
* @param {MatrixClient} client the client to get the current user and device id from
|
||||
* @returns {bool} whether the event is valid and should be passed to handleEvent
|
||||
* @returns {boolean} whether the event is valid and should be passed to handleEvent
|
||||
*/
|
||||
static validateEvent(event, client) {
|
||||
public static validateEvent(event: MatrixEvent, client: MatrixClient): boolean {
|
||||
const txnId = InRoomChannel.getTransactionId(event);
|
||||
if (typeof txnId !== "string" || txnId.length === 0) {
|
||||
return false;
|
||||
@@ -152,7 +159,7 @@ export class InRoomChannel {
|
||||
* @param {MatrixEvent} event the event to get the type of
|
||||
* @returns {string} the "symbolic" event type
|
||||
*/
|
||||
static getEventType(event) {
|
||||
public static getEventType(event: MatrixEvent): string {
|
||||
const type = event.getType();
|
||||
if (type === MESSAGE_TYPE) {
|
||||
const content = event.getContent();
|
||||
@@ -174,10 +181,10 @@ export class InRoomChannel {
|
||||
* Changes the state of the channel, request, and verifier in response to a key verification event.
|
||||
* @param {MatrixEvent} event to handle
|
||||
* @param {VerificationRequest} request the request to forward handling to
|
||||
* @param {bool} isLiveEvent whether this is an even received through sync or not
|
||||
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
|
||||
* @param {boolean} isLiveEvent whether this is an even received through sync or not
|
||||
* @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent.
|
||||
*/
|
||||
async handleEvent(event, request, isLiveEvent) {
|
||||
public async handleEvent(event: MatrixEvent, request: VerificationRequest, isLiveEvent = false): Promise<void> {
|
||||
// prevent processing the same event multiple times, as under
|
||||
// some circumstances Room.timeline can get emitted twice for the same event
|
||||
if (request.hasEventId(event.getId())) {
|
||||
@@ -187,18 +194,18 @@ export class InRoomChannel {
|
||||
// do validations that need state (roomId, userId),
|
||||
// ignore if invalid
|
||||
|
||||
if (event.getRoomId() !== this._roomId) {
|
||||
if (event.getRoomId() !== this.roomId) {
|
||||
return;
|
||||
}
|
||||
// set userId if not set already
|
||||
if (this.userId === null) {
|
||||
const userId = InRoomChannel.getOtherPartyUserId(event, this._client);
|
||||
const userId = InRoomChannel.getOtherPartyUserId(event, this.client);
|
||||
if (userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
}
|
||||
// ignore events not sent by us or the other party
|
||||
const ownUserId = this._client.getUserId();
|
||||
const ownUserId = this.client.getUserId();
|
||||
const sender = event.getSender();
|
||||
if (this.userId !== null) {
|
||||
if (sender !== ownUserId && sender !== this.userId) {
|
||||
@@ -207,12 +214,12 @@ export class InRoomChannel {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (this._requestEventId === null) {
|
||||
this._requestEventId = InRoomChannel.getTransactionId(event);
|
||||
if (this.requestEventId === null) {
|
||||
this.requestEventId = InRoomChannel.getTransactionId(event);
|
||||
}
|
||||
|
||||
const isRemoteEcho = !!event.getUnsigned().transaction_id;
|
||||
const isSentByUs = event.getSender() === this._client.getUserId();
|
||||
const isSentByUs = event.getSender() === this.client.getUserId();
|
||||
|
||||
return await request.handleEvent(
|
||||
type, event, isLiveEvent, isRemoteEcho, isSentByUs);
|
||||
@@ -226,13 +233,14 @@ export class InRoomChannel {
|
||||
* @param {MatrixEvent} event the received event
|
||||
* @returns {Object} the content object with the relation added again
|
||||
*/
|
||||
completedContentFromEvent(event) {
|
||||
public completedContentFromEvent(event: MatrixEvent): Record<string, any> {
|
||||
// ensure m.related_to is included in e2ee rooms
|
||||
// as the field is excluded from encryption
|
||||
const content = Object.assign({}, event.getContent());
|
||||
content[M_RELATES_TO] = event.getRelation();
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add all the fields to content needed for sending it over this channel.
|
||||
* This is public so verification methods (SAS uses this) can get the exact
|
||||
@@ -242,15 +250,15 @@ export class InRoomChannel {
|
||||
* @param {object} content the (incomplete) content
|
||||
* @returns {object} the complete content, as it will be sent.
|
||||
*/
|
||||
completeContent(type, content) {
|
||||
public completeContent(type: string, content: Record<string, any>): Record<string, any> {
|
||||
content = Object.assign({}, content);
|
||||
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
|
||||
content.from_device = this._client.getDeviceId();
|
||||
content.from_device = this.client.getDeviceId();
|
||||
}
|
||||
if (type === REQUEST_TYPE) {
|
||||
// type is mapped to m.room.message in the send method
|
||||
content = {
|
||||
body: this._client.getUserId() + " is requesting to verify " +
|
||||
body: this.client.getUserId() + " is requesting to verify " +
|
||||
"your key, but your client does not support in-chat key " +
|
||||
"verification. You will need to use legacy key " +
|
||||
"verification to verify keys.",
|
||||
@@ -274,7 +282,7 @@ export class InRoomChannel {
|
||||
* @param {object} uncompletedContent the (incomplete) content
|
||||
* @returns {Promise} the promise of the request
|
||||
*/
|
||||
send(type, uncompletedContent) {
|
||||
public send(type: string, uncompletedContent: Record<string, any>): Promise<void> {
|
||||
const content = this.completeContent(type, uncompletedContent);
|
||||
return this.sendCompleted(type, content);
|
||||
}
|
||||
@@ -285,74 +293,68 @@ export class InRoomChannel {
|
||||
* @param {object} content
|
||||
* @returns {Promise} the promise of the request
|
||||
*/
|
||||
async sendCompleted(type, content) {
|
||||
public async sendCompleted(type: string, content: Record<string, any>): Promise<void> {
|
||||
let sendType = type;
|
||||
if (type === REQUEST_TYPE) {
|
||||
sendType = MESSAGE_TYPE;
|
||||
}
|
||||
const response = await this._client.sendEvent(this._roomId, sendType, content);
|
||||
const response = await this.client.sendEvent(this.roomId, sendType, content);
|
||||
if (type === REQUEST_TYPE) {
|
||||
this._requestEventId = response.event_id;
|
||||
this.requestEventId = response.event_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class InRoomRequests {
|
||||
constructor() {
|
||||
this._requestsByRoomId = new Map();
|
||||
}
|
||||
export class InRoomRequests implements IRequestsMap {
|
||||
private requestsByRoomId = new Map<string, Map<string, VerificationRequest>>();
|
||||
|
||||
getRequest(event) {
|
||||
public getRequest(event: MatrixEvent): VerificationRequest {
|
||||
const roomId = event.getRoomId();
|
||||
const txnId = InRoomChannel.getTransactionId(event);
|
||||
return this._getRequestByTxnId(roomId, txnId);
|
||||
return this.getRequestByTxnId(roomId, txnId);
|
||||
}
|
||||
|
||||
getRequestByChannel(channel) {
|
||||
return this._getRequestByTxnId(channel.roomId, channel.transactionId);
|
||||
public getRequestByChannel(channel: InRoomChannel): VerificationRequest {
|
||||
return this.getRequestByTxnId(channel.roomId, channel.transactionId);
|
||||
}
|
||||
|
||||
_getRequestByTxnId(roomId, txnId) {
|
||||
const requestsByTxnId = this._requestsByRoomId.get(roomId);
|
||||
private getRequestByTxnId(roomId: string, txnId: string): VerificationRequest {
|
||||
const requestsByTxnId = this.requestsByRoomId.get(roomId);
|
||||
if (requestsByTxnId) {
|
||||
return requestsByTxnId.get(txnId);
|
||||
}
|
||||
}
|
||||
|
||||
setRequest(event, request) {
|
||||
this._setRequest(
|
||||
event.getRoomId(),
|
||||
InRoomChannel.getTransactionId(event),
|
||||
request,
|
||||
);
|
||||
public setRequest(event: MatrixEvent, request: VerificationRequest): void {
|
||||
this.doSetRequest(event.getRoomId(), InRoomChannel.getTransactionId(event), request);
|
||||
}
|
||||
|
||||
setRequestByChannel(channel, request) {
|
||||
this._setRequest(channel.roomId, channel.transactionId, request);
|
||||
public setRequestByChannel(channel: IVerificationChannel, request: VerificationRequest): void {
|
||||
this.doSetRequest(channel.roomId, channel.transactionId, request);
|
||||
}
|
||||
|
||||
_setRequest(roomId, txnId, request) {
|
||||
let requestsByTxnId = this._requestsByRoomId.get(roomId);
|
||||
private doSetRequest(roomId: string, txnId: string, request: VerificationRequest): void {
|
||||
let requestsByTxnId = this.requestsByRoomId.get(roomId);
|
||||
if (!requestsByTxnId) {
|
||||
requestsByTxnId = new Map();
|
||||
this._requestsByRoomId.set(roomId, requestsByTxnId);
|
||||
this.requestsByRoomId.set(roomId, requestsByTxnId);
|
||||
}
|
||||
requestsByTxnId.set(txnId, request);
|
||||
}
|
||||
|
||||
removeRequest(event) {
|
||||
public removeRequest(event: MatrixEvent): void {
|
||||
const roomId = event.getRoomId();
|
||||
const requestsByTxnId = this._requestsByRoomId.get(roomId);
|
||||
const requestsByTxnId = this.requestsByRoomId.get(roomId);
|
||||
if (requestsByTxnId) {
|
||||
requestsByTxnId.delete(InRoomChannel.getTransactionId(event));
|
||||
if (requestsByTxnId.size === 0) {
|
||||
this._requestsByRoomId.delete(roomId);
|
||||
this.requestsByRoomId.delete(roomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findRequestInProgress(roomId) {
|
||||
const requestsByTxnId = this._requestsByRoomId.get(roomId);
|
||||
public findRequestInProgress(roomId: string): VerificationRequest {
|
||||
const requestsByTxnId = this.requestsByRoomId.get(roomId);
|
||||
if (requestsByTxnId) {
|
||||
for (const request of requestsByTxnId.values()) {
|
||||
if (request.pending) {
|
||||
+77
-80
@@ -28,26 +28,32 @@ import {
|
||||
} from "./VerificationRequest";
|
||||
import { errorFromEvent, newUnexpectedMessageError } from "../Error";
|
||||
import { MatrixEvent } from "../../../models/event";
|
||||
import { IVerificationChannel } from "./Channel";
|
||||
import { MatrixClient } from "../../../client";
|
||||
import { IRequestsMap } from '../..';
|
||||
|
||||
export type Request = VerificationRequest<ToDeviceChannel>;
|
||||
|
||||
/**
|
||||
* A key verification channel that sends verification events over to_device messages.
|
||||
* Generates its own transaction ids.
|
||||
*/
|
||||
export class ToDeviceChannel {
|
||||
// userId and devices of user we're about to verify
|
||||
constructor(client, userId, devices, transactionId = null, deviceId = null) {
|
||||
this._client = client;
|
||||
this.userId = userId;
|
||||
this._devices = devices;
|
||||
this.transactionId = transactionId;
|
||||
this._deviceId = deviceId;
|
||||
}
|
||||
export class ToDeviceChannel implements IVerificationChannel {
|
||||
public request?: VerificationRequest;
|
||||
|
||||
isToDevices(devices) {
|
||||
if (devices.length === this._devices.length) {
|
||||
// userId and devices of user we're about to verify
|
||||
constructor(
|
||||
private readonly client: MatrixClient,
|
||||
public readonly userId: string,
|
||||
private readonly devices: string[],
|
||||
public transactionId: string = null,
|
||||
public deviceId: string = null,
|
||||
) {}
|
||||
|
||||
public isToDevices(devices: string[]): boolean {
|
||||
if (devices.length === this.devices.length) {
|
||||
for (const device of devices) {
|
||||
const d = this._devices.find(d => d.deviceId === device.deviceId);
|
||||
if (!d) {
|
||||
if (!this.devices.includes(device)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -57,11 +63,7 @@ export class ToDeviceChannel {
|
||||
}
|
||||
}
|
||||
|
||||
get deviceId() {
|
||||
return this._deviceId;
|
||||
}
|
||||
|
||||
static getEventType(event) {
|
||||
public static getEventType(event: MatrixEvent): string {
|
||||
return event.getType();
|
||||
}
|
||||
|
||||
@@ -70,7 +72,7 @@ export class ToDeviceChannel {
|
||||
* @param {MatrixEvent} event the event
|
||||
* @returns {string} the transaction id
|
||||
*/
|
||||
static getTransactionId(event) {
|
||||
public static getTransactionId(event: MatrixEvent): string {
|
||||
const content = event.getContent();
|
||||
return content && content.transaction_id;
|
||||
}
|
||||
@@ -78,12 +80,16 @@ export class ToDeviceChannel {
|
||||
/**
|
||||
* Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel
|
||||
* @param {string} type the event type to check
|
||||
* @returns {bool} boolean flag
|
||||
* @returns {boolean} boolean flag
|
||||
*/
|
||||
static canCreateRequest(type) {
|
||||
public static canCreateRequest(type: string): boolean {
|
||||
return type === REQUEST_TYPE || type === START_TYPE;
|
||||
}
|
||||
|
||||
public canCreateRequest(type: string): boolean {
|
||||
return ToDeviceChannel.canCreateRequest(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this event is a well-formed key verification event.
|
||||
* This only does checks that don't rely on the current state of a potentially already channel
|
||||
@@ -91,9 +97,9 @@ export class ToDeviceChannel {
|
||||
* `handleEvent` can do more checks and choose to ignore invalid events.
|
||||
* @param {MatrixEvent} event the event to validate
|
||||
* @param {MatrixClient} client the client to get the current user and device id from
|
||||
* @returns {bool} whether the event is valid and should be passed to handleEvent
|
||||
* @returns {boolean} whether the event is valid and should be passed to handleEvent
|
||||
*/
|
||||
static validateEvent(event, client) {
|
||||
public static validateEvent(event: MatrixEvent, client: MatrixClient): boolean {
|
||||
if (event.isCancelled()) {
|
||||
logger.warn("Ignoring flagged verification request from "
|
||||
+ event.getSender());
|
||||
@@ -134,7 +140,7 @@ export class ToDeviceChannel {
|
||||
* @param {MatrixEvent} event the event to get the timestamp of
|
||||
* @return {number} the timestamp when the event was sent
|
||||
*/
|
||||
getTimestamp(event) {
|
||||
public getTimestamp(event: MatrixEvent): number {
|
||||
const content = event.getContent();
|
||||
return content && content.timestamp;
|
||||
}
|
||||
@@ -143,10 +149,10 @@ export class ToDeviceChannel {
|
||||
* Changes the state of the channel, request, and verifier in response to a key verification event.
|
||||
* @param {MatrixEvent} event to handle
|
||||
* @param {VerificationRequest} request the request to forward handling to
|
||||
* @param {bool} isLiveEvent whether this is an even received through sync or not
|
||||
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
|
||||
* @param {boolean} isLiveEvent whether this is an even received through sync or not
|
||||
* @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent.
|
||||
*/
|
||||
async handleEvent(event, request, isLiveEvent) {
|
||||
public async handleEvent(event: MatrixEvent, request: Request, isLiveEvent = false): Promise<void> {
|
||||
const type = event.getType();
|
||||
const content = event.getContent();
|
||||
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
|
||||
@@ -155,17 +161,16 @@ export class ToDeviceChannel {
|
||||
}
|
||||
const deviceId = content.from_device;
|
||||
// adopt deviceId if not set before and valid
|
||||
if (!this._deviceId && this._devices.includes(deviceId)) {
|
||||
this._deviceId = deviceId;
|
||||
if (!this.deviceId && this.devices.includes(deviceId)) {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
// if no device id or different from addopted one, cancel with sender
|
||||
if (!this._deviceId || this._deviceId !== deviceId) {
|
||||
// if no device id or different from adopted one, cancel with sender
|
||||
if (!this.deviceId || this.deviceId !== deviceId) {
|
||||
// also check that message came from the device we sent the request to earlier on
|
||||
// and do send a cancel message to that device
|
||||
// (but don't cancel the request for the device we should be talking to)
|
||||
const cancelContent =
|
||||
this.completeContent(errorFromEvent(newUnexpectedMessageError()));
|
||||
return this._sendToDevices(CANCEL_TYPE, cancelContent, [deviceId]);
|
||||
const cancelContent = this.completeContent(CANCEL_TYPE, errorFromEvent(newUnexpectedMessageError()));
|
||||
return this.sendToDevices(CANCEL_TYPE, cancelContent, [deviceId]);
|
||||
}
|
||||
}
|
||||
const wasStarted = request.phase === PHASE_STARTED ||
|
||||
@@ -178,16 +183,16 @@ export class ToDeviceChannel {
|
||||
|
||||
const isAcceptingEvent = type === START_TYPE || type === READY_TYPE;
|
||||
// the request has picked a ready or start event, tell the other devices about it
|
||||
if (isAcceptingEvent && !wasStarted && isStarted && this._deviceId) {
|
||||
const nonChosenDevices = this._devices.filter(
|
||||
d => d !== this._deviceId && d !== this._client.getDeviceId(),
|
||||
if (isAcceptingEvent && !wasStarted && isStarted && this.deviceId) {
|
||||
const nonChosenDevices = this.devices.filter(
|
||||
d => d !== this.deviceId && d !== this.client.getDeviceId(),
|
||||
);
|
||||
if (nonChosenDevices.length) {
|
||||
const message = this.completeContent({
|
||||
const message = this.completeContent(CANCEL_TYPE, {
|
||||
code: "m.accepted",
|
||||
reason: "Verification request accepted by another device",
|
||||
});
|
||||
await this._sendToDevices(CANCEL_TYPE, message, nonChosenDevices);
|
||||
await this.sendToDevices(CANCEL_TYPE, message, nonChosenDevices);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,7 +202,7 @@ export class ToDeviceChannel {
|
||||
* @param {MatrixEvent} event the received event
|
||||
* @returns {Object} the content object
|
||||
*/
|
||||
completedContentFromEvent(event) {
|
||||
public completedContentFromEvent(event: MatrixEvent): Record<string, any> {
|
||||
return event.getContent();
|
||||
}
|
||||
|
||||
@@ -210,14 +215,14 @@ export class ToDeviceChannel {
|
||||
* @param {object} content the (incomplete) content
|
||||
* @returns {object} the complete content, as it will be sent.
|
||||
*/
|
||||
completeContent(type, content) {
|
||||
public completeContent(type: string, content: Record<string, any>): Record<string, any> {
|
||||
// make a copy
|
||||
content = Object.assign({}, content);
|
||||
if (this.transactionId) {
|
||||
content.transaction_id = this.transactionId;
|
||||
}
|
||||
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
|
||||
content.from_device = this._client.getDeviceId();
|
||||
content.from_device = this.client.getDeviceId();
|
||||
}
|
||||
if (type === REQUEST_TYPE) {
|
||||
content.timestamp = Date.now();
|
||||
@@ -231,7 +236,7 @@ export class ToDeviceChannel {
|
||||
* @param {object} uncompletedContent the (incomplete) content
|
||||
* @returns {Promise} the promise of the request
|
||||
*/
|
||||
send(type, uncompletedContent = {}) {
|
||||
public send(type: string, uncompletedContent: Record<string, any> = {}): Promise<void> {
|
||||
// create transaction id when sending request
|
||||
if ((type === REQUEST_TYPE || type === START_TYPE) && !this.transactionId) {
|
||||
this.transactionId = ToDeviceChannel.makeTransactionId();
|
||||
@@ -246,21 +251,21 @@ export class ToDeviceChannel {
|
||||
* @param {object} content
|
||||
* @returns {Promise} the promise of the request
|
||||
*/
|
||||
async sendCompleted(type, content) {
|
||||
public async sendCompleted(type: string, content: Record<string, any>): Promise<void> {
|
||||
let result;
|
||||
if (type === REQUEST_TYPE) {
|
||||
result = await this._sendToDevices(type, content, this._devices);
|
||||
if (type === REQUEST_TYPE || (type === CANCEL_TYPE && !this.deviceId)) {
|
||||
result = await this.sendToDevices(type, content, this.devices);
|
||||
} else {
|
||||
result = await this._sendToDevices(type, content, [this._deviceId]);
|
||||
result = await this.sendToDevices(type, content, [this.deviceId]);
|
||||
}
|
||||
// the VerificationRequest state machine requires remote echos of the event
|
||||
// the client sends itself, so we fake this for to_device messages
|
||||
const remoteEchoEvent = new MatrixEvent({
|
||||
sender: this._client.getUserId(),
|
||||
sender: this.client.getUserId(),
|
||||
content,
|
||||
type,
|
||||
});
|
||||
await this._request.handleEvent(
|
||||
await this.request.handleEvent(
|
||||
type,
|
||||
remoteEchoEvent,
|
||||
/*isLiveEvent=*/true,
|
||||
@@ -270,16 +275,14 @@ export class ToDeviceChannel {
|
||||
return result;
|
||||
}
|
||||
|
||||
_sendToDevices(type, content, devices) {
|
||||
private async sendToDevices(type: string, content: Record<string, any>, devices: string[]): Promise<void> {
|
||||
if (devices.length) {
|
||||
const msgMap = {};
|
||||
const msgMap: Record<string, Record<string, any>> = {};
|
||||
for (const deviceId of devices) {
|
||||
msgMap[deviceId] = content;
|
||||
}
|
||||
|
||||
return this._client.sendToDevice(type, { [this.userId]: msgMap });
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
await this.client.sendToDevice(type, { [this.userId]: msgMap });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,68 +290,62 @@ export class ToDeviceChannel {
|
||||
* Allow Crypto module to create and know the transaction id before the .start event gets sent.
|
||||
* @returns {string} the transaction id
|
||||
*/
|
||||
static makeTransactionId() {
|
||||
public static makeTransactionId(): string {
|
||||
return randomString(32);
|
||||
}
|
||||
}
|
||||
|
||||
export class ToDeviceRequests {
|
||||
constructor() {
|
||||
this._requestsByUserId = new Map();
|
||||
}
|
||||
export class ToDeviceRequests implements IRequestsMap {
|
||||
private requestsByUserId = new Map<string, Map<string, Request>>();
|
||||
|
||||
getRequest(event) {
|
||||
public getRequest(event: MatrixEvent): Request {
|
||||
return this.getRequestBySenderAndTxnId(
|
||||
event.getSender(),
|
||||
ToDeviceChannel.getTransactionId(event),
|
||||
);
|
||||
}
|
||||
|
||||
getRequestByChannel(channel) {
|
||||
public getRequestByChannel(channel: ToDeviceChannel): Request {
|
||||
return this.getRequestBySenderAndTxnId(channel.userId, channel.transactionId);
|
||||
}
|
||||
|
||||
getRequestBySenderAndTxnId(sender, txnId) {
|
||||
const requestsByTxnId = this._requestsByUserId.get(sender);
|
||||
public getRequestBySenderAndTxnId(sender: string, txnId: string): Request {
|
||||
const requestsByTxnId = this.requestsByUserId.get(sender);
|
||||
if (requestsByTxnId) {
|
||||
return requestsByTxnId.get(txnId);
|
||||
}
|
||||
}
|
||||
|
||||
setRequest(event, request) {
|
||||
this.setRequestBySenderAndTxnId(
|
||||
event.getSender(),
|
||||
ToDeviceChannel.getTransactionId(event),
|
||||
request,
|
||||
);
|
||||
public setRequest(event: MatrixEvent, request: Request): void {
|
||||
this.setRequestBySenderAndTxnId(event.getSender(), ToDeviceChannel.getTransactionId(event), request);
|
||||
}
|
||||
|
||||
setRequestByChannel(channel, request) {
|
||||
public setRequestByChannel(channel: ToDeviceChannel, request: Request): void {
|
||||
this.setRequestBySenderAndTxnId(channel.userId, channel.transactionId, request);
|
||||
}
|
||||
|
||||
setRequestBySenderAndTxnId(sender, txnId, request) {
|
||||
let requestsByTxnId = this._requestsByUserId.get(sender);
|
||||
public setRequestBySenderAndTxnId(sender: string, txnId: string, request: Request): void {
|
||||
let requestsByTxnId = this.requestsByUserId.get(sender);
|
||||
if (!requestsByTxnId) {
|
||||
requestsByTxnId = new Map();
|
||||
this._requestsByUserId.set(sender, requestsByTxnId);
|
||||
this.requestsByUserId.set(sender, requestsByTxnId);
|
||||
}
|
||||
requestsByTxnId.set(txnId, request);
|
||||
}
|
||||
|
||||
removeRequest(event) {
|
||||
public removeRequest(event: MatrixEvent): void {
|
||||
const userId = event.getSender();
|
||||
const requestsByTxnId = this._requestsByUserId.get(userId);
|
||||
const requestsByTxnId = this.requestsByUserId.get(userId);
|
||||
if (requestsByTxnId) {
|
||||
requestsByTxnId.delete(ToDeviceChannel.getTransactionId(event));
|
||||
if (requestsByTxnId.size === 0) {
|
||||
this._requestsByUserId.delete(userId);
|
||||
this.requestsByUserId.delete(userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findRequestInProgress(userId, devices) {
|
||||
const requestsByTxnId = this._requestsByUserId.get(userId);
|
||||
public findRequestInProgress(userId: string, devices: string[]): Request {
|
||||
const requestsByTxnId = this.requestsByUserId.get(userId);
|
||||
if (requestsByTxnId) {
|
||||
for (const request of requestsByTxnId.values()) {
|
||||
if (request.pending && request.channel.isToDevices(devices)) {
|
||||
@@ -358,8 +355,8 @@ export class ToDeviceRequests {
|
||||
}
|
||||
}
|
||||
|
||||
getRequestsInProgress(userId) {
|
||||
const requestsByTxnId = this._requestsByUserId.get(userId);
|
||||
public getRequestsInProgress(userId: string): Request[] {
|
||||
const requestsByTxnId = this.requestsByUserId.get(userId);
|
||||
if (requestsByTxnId) {
|
||||
return Array.from(requestsByTxnId.values()).filter(r => r.pending);
|
||||
}
|
||||
+245
-201
@@ -1,6 +1,5 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -16,7 +15,6 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from '../../../logger';
|
||||
import { EventEmitter } from 'events';
|
||||
import {
|
||||
errorFactory,
|
||||
errorFromEvent,
|
||||
@@ -24,6 +22,12 @@ import {
|
||||
newUnknownMethodError,
|
||||
} from "../Error";
|
||||
import { QRCodeData, SCAN_QR_CODE_METHOD } from "../QRCode";
|
||||
import { IVerificationChannel } from "./Channel";
|
||||
import { MatrixClient } from "../../../client";
|
||||
import { MatrixEvent } from "../../../models/event";
|
||||
import { VerificationBase } from "../Base";
|
||||
import { VerificationMethod } from "../../index";
|
||||
import { TypedEventEmitter } from "../../../models/typed-event-emitter";
|
||||
|
||||
// How long after the event's timestamp that the request times out
|
||||
const TIMEOUT_FROM_EVENT_TS = 10 * 60 * 1000; // 10 minutes
|
||||
@@ -44,12 +48,40 @@ export const CANCEL_TYPE = EVENT_PREFIX + "cancel";
|
||||
export const DONE_TYPE = EVENT_PREFIX + "done";
|
||||
export const READY_TYPE = EVENT_PREFIX + "ready";
|
||||
|
||||
export const PHASE_UNSENT = 1;
|
||||
export const PHASE_REQUESTED = 2;
|
||||
export const PHASE_READY = 3;
|
||||
export const PHASE_STARTED = 4;
|
||||
export const PHASE_CANCELLED = 5;
|
||||
export const PHASE_DONE = 6;
|
||||
export enum Phase {
|
||||
Unsent = 1,
|
||||
Requested,
|
||||
Ready,
|
||||
Started,
|
||||
Cancelled,
|
||||
Done,
|
||||
}
|
||||
|
||||
// Legacy export fields
|
||||
export const PHASE_UNSENT = Phase.Unsent;
|
||||
export const PHASE_REQUESTED = Phase.Requested;
|
||||
export const PHASE_READY = Phase.Ready;
|
||||
export const PHASE_STARTED = Phase.Started;
|
||||
export const PHASE_CANCELLED = Phase.Cancelled;
|
||||
export const PHASE_DONE = Phase.Done;
|
||||
|
||||
interface ITargetDevice {
|
||||
userId?: string;
|
||||
deviceId?: string;
|
||||
}
|
||||
|
||||
interface ITransition {
|
||||
phase: Phase;
|
||||
event?: MatrixEvent;
|
||||
}
|
||||
|
||||
export enum VerificationRequestEvent {
|
||||
Change = "change",
|
||||
}
|
||||
|
||||
type EventHandlerMap = {
|
||||
[VerificationRequestEvent.Change]: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* State machine for verification requests.
|
||||
@@ -57,32 +89,40 @@ export const PHASE_DONE = 6;
|
||||
* send and receive verification events are put in `InRoomChannel` or `ToDeviceChannel`.
|
||||
* @event "change" whenever the state of the request object has changed.
|
||||
*/
|
||||
export class VerificationRequest extends EventEmitter {
|
||||
constructor(channel, verificationMethods, client) {
|
||||
super();
|
||||
this.channel = channel;
|
||||
this.channel._request = this;
|
||||
this._verificationMethods = verificationMethods;
|
||||
this._client = client;
|
||||
this._commonMethods = [];
|
||||
this._setPhase(PHASE_UNSENT, false);
|
||||
this._eventsByUs = new Map();
|
||||
this._eventsByThem = new Map();
|
||||
this._observeOnly = false;
|
||||
this._timeoutTimer = null;
|
||||
this._accepting = false;
|
||||
this._declining = false;
|
||||
this._verifierHasFinished = false;
|
||||
this._cancelled = false;
|
||||
this._chosenMethod = null;
|
||||
// we keep a copy of the QR Code data (including other user master key) around
|
||||
// for QR reciprocate verification, to protect against
|
||||
// cross-signing identity reset between the .ready and .start event
|
||||
// and signing the wrong key after .start
|
||||
this._qrCodeData = null;
|
||||
export class VerificationRequest<
|
||||
C extends IVerificationChannel = IVerificationChannel,
|
||||
> extends TypedEventEmitter<VerificationRequestEvent, EventHandlerMap> {
|
||||
private eventsByUs = new Map<string, MatrixEvent>();
|
||||
private eventsByThem = new Map<string, MatrixEvent>();
|
||||
private _observeOnly = false;
|
||||
private timeoutTimer: number = null;
|
||||
private _accepting = false;
|
||||
private _declining = false;
|
||||
private verifierHasFinished = false;
|
||||
private _cancelled = false;
|
||||
private _chosenMethod: VerificationMethod = null;
|
||||
// we keep a copy of the QR Code data (including other user master key) around
|
||||
// for QR reciprocate verification, to protect against
|
||||
// cross-signing identity reset between the .ready and .start event
|
||||
// and signing the wrong key after .start
|
||||
private _qrCodeData: QRCodeData = null;
|
||||
|
||||
// The timestamp when we received the request event from the other side
|
||||
this._requestReceivedAt = null;
|
||||
// The timestamp when we received the request event from the other side
|
||||
private requestReceivedAt: number = null;
|
||||
|
||||
private commonMethods: VerificationMethod[] = [];
|
||||
private _phase: Phase;
|
||||
private _cancellingUserId: string;
|
||||
private _verifier: VerificationBase<any, any>;
|
||||
|
||||
constructor(
|
||||
public readonly channel: C,
|
||||
private readonly verificationMethods: Map<VerificationMethod, typeof VerificationBase>,
|
||||
private readonly client: MatrixClient,
|
||||
) {
|
||||
super();
|
||||
this.channel.request = this;
|
||||
this.setPhase(PHASE_UNSENT, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,9 +131,9 @@ export class VerificationRequest extends EventEmitter {
|
||||
* @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel.
|
||||
* @param {MatrixEvent} event the event to validate. Don't call getType() on it but use the `type` parameter instead.
|
||||
* @param {MatrixClient} client the client to get the current user and device id from
|
||||
* @returns {bool} whether the event is valid and should be passed to handleEvent
|
||||
* @returns {boolean} whether the event is valid and should be passed to handleEvent
|
||||
*/
|
||||
static validateEvent(type, event, client) {
|
||||
public static validateEvent(type: string, event: MatrixEvent, client: MatrixClient): boolean {
|
||||
const content = event.getContent();
|
||||
|
||||
if (!type || !type.startsWith(EVENT_PREFIX)) {
|
||||
@@ -128,53 +168,53 @@ export class VerificationRequest extends EventEmitter {
|
||||
return true;
|
||||
}
|
||||
|
||||
get invalid() {
|
||||
public get invalid(): boolean {
|
||||
return this.phase === PHASE_UNSENT;
|
||||
}
|
||||
|
||||
/** returns whether the phase is PHASE_REQUESTED */
|
||||
get requested() {
|
||||
public get requested(): boolean {
|
||||
return this.phase === PHASE_REQUESTED;
|
||||
}
|
||||
|
||||
/** returns whether the phase is PHASE_CANCELLED */
|
||||
get cancelled() {
|
||||
public get cancelled(): boolean {
|
||||
return this.phase === PHASE_CANCELLED;
|
||||
}
|
||||
|
||||
/** returns whether the phase is PHASE_READY */
|
||||
get ready() {
|
||||
public get ready(): boolean {
|
||||
return this.phase === PHASE_READY;
|
||||
}
|
||||
|
||||
/** returns whether the phase is PHASE_STARTED */
|
||||
get started() {
|
||||
public get started(): boolean {
|
||||
return this.phase === PHASE_STARTED;
|
||||
}
|
||||
|
||||
/** returns whether the phase is PHASE_DONE */
|
||||
get done() {
|
||||
public get done(): boolean {
|
||||
return this.phase === PHASE_DONE;
|
||||
}
|
||||
|
||||
/** once the phase is PHASE_STARTED (and !initiatedByMe) or PHASE_READY: common methods supported by both sides */
|
||||
get methods() {
|
||||
return this._commonMethods;
|
||||
public get methods(): VerificationMethod[] {
|
||||
return this.commonMethods;
|
||||
}
|
||||
|
||||
/** the method picked in the .start event */
|
||||
get chosenMethod() {
|
||||
public get chosenMethod(): VerificationMethod {
|
||||
return this._chosenMethod;
|
||||
}
|
||||
|
||||
calculateEventTimeout(event) {
|
||||
public calculateEventTimeout(event: MatrixEvent): number {
|
||||
let effectiveExpiresAt = this.channel.getTimestamp(event)
|
||||
+ TIMEOUT_FROM_EVENT_TS;
|
||||
|
||||
if (this._requestReceivedAt && !this.initiatedByMe &&
|
||||
if (this.requestReceivedAt && !this.initiatedByMe &&
|
||||
this.phase <= PHASE_REQUESTED
|
||||
) {
|
||||
const expiresAtByReceipt = this._requestReceivedAt
|
||||
const expiresAtByReceipt = this.requestReceivedAt
|
||||
+ TIMEOUT_FROM_EVENT_RECEIPT;
|
||||
effectiveExpiresAt = Math.min(effectiveExpiresAt, expiresAtByReceipt);
|
||||
}
|
||||
@@ -183,8 +223,8 @@ export class VerificationRequest extends EventEmitter {
|
||||
}
|
||||
|
||||
/** The current remaining amount of ms before the request should be automatically cancelled */
|
||||
get timeout() {
|
||||
const requestEvent = this._getEventByEither(REQUEST_TYPE);
|
||||
public get timeout(): number {
|
||||
const requestEvent = this.getEventByEither(REQUEST_TYPE);
|
||||
if (requestEvent) {
|
||||
return this.calculateEventTimeout(requestEvent);
|
||||
}
|
||||
@@ -195,41 +235,41 @@ export class VerificationRequest extends EventEmitter {
|
||||
* The key verification request event.
|
||||
* @returns {MatrixEvent} The request event, or falsey if not found.
|
||||
*/
|
||||
get requestEvent() {
|
||||
return this._getEventByEither(REQUEST_TYPE);
|
||||
public get requestEvent(): MatrixEvent {
|
||||
return this.getEventByEither(REQUEST_TYPE);
|
||||
}
|
||||
|
||||
/** current phase of the request. Some properties might only be defined in a current phase. */
|
||||
get phase() {
|
||||
public get phase(): Phase {
|
||||
return this._phase;
|
||||
}
|
||||
|
||||
/** The verifier to do the actual verification, once the method has been established. Only defined when the `phase` is PHASE_STARTED. */
|
||||
get verifier() {
|
||||
public get verifier(): VerificationBase<any, any> {
|
||||
return this._verifier;
|
||||
}
|
||||
|
||||
get canAccept() {
|
||||
public get canAccept(): boolean {
|
||||
return this.phase < PHASE_READY && !this._accepting && !this._declining;
|
||||
}
|
||||
|
||||
get accepting() {
|
||||
public get accepting(): boolean {
|
||||
return this._accepting;
|
||||
}
|
||||
|
||||
get declining() {
|
||||
public get declining(): boolean {
|
||||
return this._declining;
|
||||
}
|
||||
|
||||
/** whether this request has sent it's initial event and needs more events to complete */
|
||||
get pending() {
|
||||
public get pending(): boolean {
|
||||
return !this.observeOnly &&
|
||||
this._phase !== PHASE_DONE &&
|
||||
this._phase !== PHASE_CANCELLED;
|
||||
}
|
||||
|
||||
/** Only set after a .ready if the other party can scan a QR code */
|
||||
get qrCodeData() {
|
||||
public get qrCodeData(): QRCodeData {
|
||||
return this._qrCodeData;
|
||||
}
|
||||
|
||||
@@ -239,19 +279,19 @@ export class VerificationRequest extends EventEmitter {
|
||||
* For methods that need to be supported by both ends, use the `methods` property.
|
||||
* @param {string} method the method to check
|
||||
* @param {boolean} force to check even if the phase is not ready or started yet, internal usage
|
||||
* @return {bool} whether or not the other party said the supported the method */
|
||||
otherPartySupportsMethod(method, force = false) {
|
||||
* @return {boolean} whether or not the other party said the supported the method */
|
||||
public otherPartySupportsMethod(method: string, force = false): boolean {
|
||||
if (!force && !this.ready && !this.started) {
|
||||
return false;
|
||||
}
|
||||
const theirMethodEvent = this._eventsByThem.get(REQUEST_TYPE) ||
|
||||
this._eventsByThem.get(READY_TYPE);
|
||||
const theirMethodEvent = this.eventsByThem.get(REQUEST_TYPE) ||
|
||||
this.eventsByThem.get(READY_TYPE);
|
||||
if (!theirMethodEvent) {
|
||||
// if we started straight away with .start event,
|
||||
// we are assuming that the other side will support the
|
||||
// chosen method, so return true for that.
|
||||
if (this.started && this.initiatedByMe) {
|
||||
const myStartEvent = this._eventsByUs.get(START_TYPE);
|
||||
const myStartEvent = this.eventsByUs.get(START_TYPE);
|
||||
const content = myStartEvent && myStartEvent.getContent();
|
||||
const myStartMethod = content && content.method;
|
||||
return method == myStartMethod;
|
||||
@@ -274,22 +314,22 @@ export class VerificationRequest extends EventEmitter {
|
||||
* For InRoomChannel, this is who sent the .request event.
|
||||
* For ToDeviceChannel, this is who sent the .start event
|
||||
*/
|
||||
get initiatedByMe() {
|
||||
public get initiatedByMe(): boolean {
|
||||
// event created by us but no remote echo has been received yet
|
||||
const noEventsYet = (this._eventsByUs.size + this._eventsByThem.size) === 0;
|
||||
const noEventsYet = (this.eventsByUs.size + this.eventsByThem.size) === 0;
|
||||
if (this._phase === PHASE_UNSENT && noEventsYet) {
|
||||
return true;
|
||||
}
|
||||
const hasMyRequest = this._eventsByUs.has(REQUEST_TYPE);
|
||||
const hasTheirRequest = this._eventsByThem.has(REQUEST_TYPE);
|
||||
const hasMyRequest = this.eventsByUs.has(REQUEST_TYPE);
|
||||
const hasTheirRequest = this.eventsByThem.has(REQUEST_TYPE);
|
||||
if (hasMyRequest && !hasTheirRequest) {
|
||||
return true;
|
||||
}
|
||||
if (!hasMyRequest && hasTheirRequest) {
|
||||
return false;
|
||||
}
|
||||
const hasMyStart = this._eventsByUs.has(START_TYPE);
|
||||
const hasTheirStart = this._eventsByThem.has(START_TYPE);
|
||||
const hasMyStart = this.eventsByUs.has(START_TYPE);
|
||||
const hasTheirStart = this.eventsByThem.has(START_TYPE);
|
||||
if (hasMyStart && !hasTheirStart) {
|
||||
return true;
|
||||
}
|
||||
@@ -297,39 +337,39 @@ export class VerificationRequest extends EventEmitter {
|
||||
}
|
||||
|
||||
/** The id of the user that initiated the request */
|
||||
get requestingUserId() {
|
||||
public get requestingUserId(): string {
|
||||
if (this.initiatedByMe) {
|
||||
return this._client.getUserId();
|
||||
return this.client.getUserId();
|
||||
} else {
|
||||
return this.otherUserId;
|
||||
}
|
||||
}
|
||||
|
||||
/** The id of the user that (will) receive(d) the request */
|
||||
get receivingUserId() {
|
||||
public get receivingUserId(): string {
|
||||
if (this.initiatedByMe) {
|
||||
return this.otherUserId;
|
||||
} else {
|
||||
return this._client.getUserId();
|
||||
return this.client.getUserId();
|
||||
}
|
||||
}
|
||||
|
||||
/** The user id of the other party in this request */
|
||||
get otherUserId() {
|
||||
public get otherUserId(): string {
|
||||
return this.channel.userId;
|
||||
}
|
||||
|
||||
get isSelfVerification() {
|
||||
return this._client.getUserId() === this.otherUserId;
|
||||
public get isSelfVerification(): boolean {
|
||||
return this.client.getUserId() === this.otherUserId;
|
||||
}
|
||||
|
||||
/**
|
||||
* The id of the user that cancelled the request,
|
||||
* only defined when phase is PHASE_CANCELLED
|
||||
*/
|
||||
get cancellingUserId() {
|
||||
const myCancel = this._eventsByUs.get(CANCEL_TYPE);
|
||||
const theirCancel = this._eventsByThem.get(CANCEL_TYPE);
|
||||
public get cancellingUserId(): string {
|
||||
const myCancel = this.eventsByUs.get(CANCEL_TYPE);
|
||||
const theirCancel = this.eventsByThem.get(CANCEL_TYPE);
|
||||
|
||||
if (myCancel && (!theirCancel || myCancel.getId() < theirCancel.getId())) {
|
||||
return myCancel.getSender();
|
||||
@@ -343,12 +383,12 @@ export class VerificationRequest extends EventEmitter {
|
||||
/**
|
||||
* The cancellation code e.g m.user which is responsible for cancelling this verification
|
||||
*/
|
||||
get cancellationCode() {
|
||||
const ev = this._getEventByEither(CANCEL_TYPE);
|
||||
public get cancellationCode(): string {
|
||||
const ev = this.getEventByEither(CANCEL_TYPE);
|
||||
return ev ? ev.getContent().code : null;
|
||||
}
|
||||
|
||||
get observeOnly() {
|
||||
public get observeOnly(): boolean {
|
||||
return this._observeOnly;
|
||||
}
|
||||
|
||||
@@ -359,11 +399,11 @@ export class VerificationRequest extends EventEmitter {
|
||||
* verification to when no specific device is specified.
|
||||
* @returns {{userId: *, deviceId: *}} The device information
|
||||
*/
|
||||
get targetDevice() {
|
||||
public get targetDevice(): ITargetDevice {
|
||||
const theirFirstEvent =
|
||||
this._eventsByThem.get(REQUEST_TYPE) ||
|
||||
this._eventsByThem.get(READY_TYPE) ||
|
||||
this._eventsByThem.get(START_TYPE);
|
||||
this.eventsByThem.get(REQUEST_TYPE) ||
|
||||
this.eventsByThem.get(READY_TYPE) ||
|
||||
this.eventsByThem.get(START_TYPE);
|
||||
const theirFirstContent = theirFirstEvent.getContent();
|
||||
const fromDevice = theirFirstContent.from_device;
|
||||
return {
|
||||
@@ -379,21 +419,23 @@ export class VerificationRequest extends EventEmitter {
|
||||
* @param {string?} targetDevice.deviceId the id of the device to direct this request to
|
||||
* @returns {VerifierBase} the verifier of the given method
|
||||
*/
|
||||
beginKeyVerification(method, targetDevice = null) {
|
||||
public beginKeyVerification(
|
||||
method: VerificationMethod,
|
||||
targetDevice: ITargetDevice = null,
|
||||
): VerificationBase<any, any> {
|
||||
// need to allow also when unsent in case of to_device
|
||||
if (!this.observeOnly && !this._verifier) {
|
||||
const validStartPhase =
|
||||
this.phase === PHASE_REQUESTED ||
|
||||
this.phase === PHASE_READY ||
|
||||
(this.phase === PHASE_UNSENT &&
|
||||
this.channel.constructor.canCreateRequest(START_TYPE));
|
||||
(this.phase === PHASE_UNSENT && this.channel.canCreateRequest(START_TYPE));
|
||||
if (validStartPhase) {
|
||||
// when called on a request that was initiated with .request event
|
||||
// check the method is supported by both sides
|
||||
if (this._commonMethods.length && !this._commonMethods.includes(method)) {
|
||||
if (this.commonMethods.length && !this.commonMethods.includes(method)) {
|
||||
throw newUnknownMethodError();
|
||||
}
|
||||
this._verifier = this._createVerifier(method, null, targetDevice);
|
||||
this._verifier = this.createVerifier(method, null, targetDevice);
|
||||
if (!this._verifier) {
|
||||
throw newUnknownMethodError();
|
||||
}
|
||||
@@ -407,9 +449,9 @@ export class VerificationRequest extends EventEmitter {
|
||||
* sends the initial .request event.
|
||||
* @returns {Promise} resolves when the event has been sent.
|
||||
*/
|
||||
async sendRequest() {
|
||||
public async sendRequest(): Promise<void> {
|
||||
if (!this.observeOnly && this._phase === PHASE_UNSENT) {
|
||||
const methods = [...this._verificationMethods.keys()];
|
||||
const methods = [...this.verificationMethods.keys()];
|
||||
await this.channel.send(REQUEST_TYPE, { methods });
|
||||
}
|
||||
}
|
||||
@@ -420,14 +462,14 @@ export class VerificationRequest extends EventEmitter {
|
||||
* @param {string?} error.code the error code to send the cancellation with
|
||||
* @returns {Promise} resolves when the event has been sent.
|
||||
*/
|
||||
async cancel({ reason = "User declined", code = "m.user" } = {}) {
|
||||
public async cancel({ reason = "User declined", code = "m.user" } = {}): Promise<void> {
|
||||
if (!this.observeOnly && this._phase !== PHASE_CANCELLED) {
|
||||
this._declining = true;
|
||||
this.emit("change");
|
||||
this.emit(VerificationRequestEvent.Change);
|
||||
if (this._verifier) {
|
||||
return this._verifier.cancel(errorFactory(code, reason)());
|
||||
} else {
|
||||
this._cancellingUserId = this._client.getUserId();
|
||||
this._cancellingUserId = this.client.getUserId();
|
||||
await this.channel.send(CANCEL_TYPE, { code, reason });
|
||||
}
|
||||
}
|
||||
@@ -437,11 +479,11 @@ export class VerificationRequest extends EventEmitter {
|
||||
* Accepts the request, sending a .ready event to the other party
|
||||
* @returns {Promise} resolves when the event has been sent.
|
||||
*/
|
||||
async accept() {
|
||||
public async accept(): Promise<void> {
|
||||
if (!this.observeOnly && this.phase === PHASE_REQUESTED && !this.initiatedByMe) {
|
||||
const methods = [...this._verificationMethods.keys()];
|
||||
const methods = [...this.verificationMethods.keys()];
|
||||
this._accepting = true;
|
||||
this.emit("change");
|
||||
this.emit(VerificationRequestEvent.Change);
|
||||
await this.channel.send(READY_TYPE, { methods });
|
||||
}
|
||||
}
|
||||
@@ -453,7 +495,7 @@ export class VerificationRequest extends EventEmitter {
|
||||
* @returns {Promise} that resolves once the callback returns true
|
||||
* @throws {Error} when the request is cancelled
|
||||
*/
|
||||
waitFor(fn) {
|
||||
public waitFor(fn: (request: VerificationRequest) => boolean): Promise<VerificationRequest> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const check = () => {
|
||||
let handled = false;
|
||||
@@ -465,56 +507,56 @@ export class VerificationRequest extends EventEmitter {
|
||||
handled = true;
|
||||
}
|
||||
if (handled) {
|
||||
this.off("change", check);
|
||||
this.off(VerificationRequestEvent.Change, check);
|
||||
}
|
||||
return handled;
|
||||
};
|
||||
if (!check()) {
|
||||
this.on("change", check);
|
||||
this.on(VerificationRequestEvent.Change, check);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_setPhase(phase, notify = true) {
|
||||
private setPhase(phase: Phase, notify = true): void {
|
||||
this._phase = phase;
|
||||
if (notify) {
|
||||
this.emit("change");
|
||||
this.emit(VerificationRequestEvent.Change);
|
||||
}
|
||||
}
|
||||
|
||||
_getEventByEither(type) {
|
||||
return this._eventsByThem.get(type) || this._eventsByUs.get(type);
|
||||
private getEventByEither(type: string): MatrixEvent {
|
||||
return this.eventsByThem.get(type) || this.eventsByUs.get(type);
|
||||
}
|
||||
|
||||
_getEventBy(type, byThem) {
|
||||
private getEventBy(type: string, byThem = false): MatrixEvent {
|
||||
if (byThem) {
|
||||
return this._eventsByThem.get(type);
|
||||
return this.eventsByThem.get(type);
|
||||
} else {
|
||||
return this._eventsByUs.get(type);
|
||||
return this.eventsByUs.get(type);
|
||||
}
|
||||
}
|
||||
|
||||
_calculatePhaseTransitions() {
|
||||
const transitions = [{ phase: PHASE_UNSENT }];
|
||||
private calculatePhaseTransitions(): ITransition[] {
|
||||
const transitions: ITransition[] = [{ phase: PHASE_UNSENT }];
|
||||
const phase = () => transitions[transitions.length - 1].phase;
|
||||
|
||||
// always pass by .request first to be sure channel.userId has been set
|
||||
const hasRequestByThem = this._eventsByThem.has(REQUEST_TYPE);
|
||||
const requestEvent = this._getEventBy(REQUEST_TYPE, hasRequestByThem);
|
||||
const hasRequestByThem = this.eventsByThem.has(REQUEST_TYPE);
|
||||
const requestEvent = this.getEventBy(REQUEST_TYPE, hasRequestByThem);
|
||||
if (requestEvent) {
|
||||
transitions.push({ phase: PHASE_REQUESTED, event: requestEvent });
|
||||
}
|
||||
|
||||
const readyEvent =
|
||||
requestEvent && this._getEventBy(READY_TYPE, !hasRequestByThem);
|
||||
requestEvent && this.getEventBy(READY_TYPE, !hasRequestByThem);
|
||||
if (readyEvent && phase() === PHASE_REQUESTED) {
|
||||
transitions.push({ phase: PHASE_READY, event: readyEvent });
|
||||
}
|
||||
|
||||
let startEvent;
|
||||
if (readyEvent || !requestEvent) {
|
||||
const theirStartEvent = this._eventsByThem.get(START_TYPE);
|
||||
const ourStartEvent = this._eventsByUs.get(START_TYPE);
|
||||
const theirStartEvent = this.eventsByThem.get(START_TYPE);
|
||||
const ourStartEvent = this.eventsByUs.get(START_TYPE);
|
||||
// any party can send .start after a .ready or unsent
|
||||
if (theirStartEvent && ourStartEvent) {
|
||||
startEvent = theirStartEvent.getSender() < ourStartEvent.getSender() ?
|
||||
@@ -523,24 +565,22 @@ export class VerificationRequest extends EventEmitter {
|
||||
startEvent = theirStartEvent ? theirStartEvent : ourStartEvent;
|
||||
}
|
||||
} else {
|
||||
startEvent = this._getEventBy(START_TYPE, !hasRequestByThem);
|
||||
startEvent = this.getEventBy(START_TYPE, !hasRequestByThem);
|
||||
}
|
||||
if (startEvent) {
|
||||
const fromRequestPhase = phase() === PHASE_REQUESTED &&
|
||||
requestEvent.getSender() !== startEvent.getSender();
|
||||
const fromUnsentPhase = phase() === PHASE_UNSENT &&
|
||||
this.channel.constructor.canCreateRequest(START_TYPE);
|
||||
const fromRequestPhase = phase() === PHASE_REQUESTED && requestEvent.getSender() !== startEvent.getSender();
|
||||
const fromUnsentPhase = phase() === PHASE_UNSENT && this.channel.canCreateRequest(START_TYPE);
|
||||
if (fromRequestPhase || phase() === PHASE_READY || fromUnsentPhase) {
|
||||
transitions.push({ phase: PHASE_STARTED, event: startEvent });
|
||||
}
|
||||
}
|
||||
|
||||
const ourDoneEvent = this._eventsByUs.get(DONE_TYPE);
|
||||
if (this._verifierHasFinished || (ourDoneEvent && phase() === PHASE_STARTED)) {
|
||||
const ourDoneEvent = this.eventsByUs.get(DONE_TYPE);
|
||||
if (this.verifierHasFinished || (ourDoneEvent && phase() === PHASE_STARTED)) {
|
||||
transitions.push({ phase: PHASE_DONE });
|
||||
}
|
||||
|
||||
const cancelEvent = this._getEventByEither(CANCEL_TYPE);
|
||||
const cancelEvent = this.getEventByEither(CANCEL_TYPE);
|
||||
if ((this._cancelled || cancelEvent) && phase() !== PHASE_DONE) {
|
||||
transitions.push({ phase: PHASE_CANCELLED, event: cancelEvent });
|
||||
return transitions;
|
||||
@@ -549,14 +589,16 @@ export class VerificationRequest extends EventEmitter {
|
||||
return transitions;
|
||||
}
|
||||
|
||||
_transitionToPhase(transition) {
|
||||
private transitionToPhase(transition: ITransition): void {
|
||||
const { phase, event } = transition;
|
||||
// get common methods
|
||||
if (phase === PHASE_REQUESTED || phase === PHASE_READY) {
|
||||
if (!this._wasSentByOwnDevice(event)) {
|
||||
const content = event.getContent();
|
||||
this._commonMethods =
|
||||
content.methods.filter(m => this._verificationMethods.has(m));
|
||||
if (!this.wasSentByOwnDevice(event)) {
|
||||
const content = event.getContent<{
|
||||
methods: string[];
|
||||
}>();
|
||||
this.commonMethods =
|
||||
content.methods.filter(m => this.verificationMethods.has(m));
|
||||
}
|
||||
}
|
||||
// detect if we're not a party in the request, and we should just observe
|
||||
@@ -568,8 +610,8 @@ export class VerificationRequest extends EventEmitter {
|
||||
) {
|
||||
if (
|
||||
this.channel.receiveStartFromOtherDevices &&
|
||||
this._wasSentByOwnUser(event) &&
|
||||
!this._wasSentByOwnDevice(event)
|
||||
this.wasSentByOwnUser(event) &&
|
||||
!this.wasSentByOwnDevice(event)
|
||||
) {
|
||||
this._observeOnly = true;
|
||||
}
|
||||
@@ -579,7 +621,7 @@ export class VerificationRequest extends EventEmitter {
|
||||
if (phase === PHASE_STARTED) {
|
||||
const { method } = event.getContent();
|
||||
if (!this._verifier && !this.observeOnly) {
|
||||
this._verifier = this._createVerifier(method, event);
|
||||
this._verifier = this.createVerifier(method, event);
|
||||
if (!this._verifier) {
|
||||
this.cancel({
|
||||
code: "m.unknown_method",
|
||||
@@ -592,19 +634,19 @@ export class VerificationRequest extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
_applyPhaseTransitions() {
|
||||
const transitions = this._calculatePhaseTransitions();
|
||||
private applyPhaseTransitions(): ITransition[] {
|
||||
const transitions = this.calculatePhaseTransitions();
|
||||
const existingIdx = transitions.findIndex(t => t.phase === this.phase);
|
||||
// trim off phases we already went through, if any
|
||||
const newTransitions = transitions.slice(existingIdx + 1);
|
||||
// transition to all new phases
|
||||
for (const transition of newTransitions) {
|
||||
this._transitionToPhase(transition);
|
||||
this.transitionToPhase(transition);
|
||||
}
|
||||
return newTransitions;
|
||||
}
|
||||
|
||||
_isWinningStartRace(newEvent) {
|
||||
private isWinningStartRace(newEvent: MatrixEvent): boolean {
|
||||
if (newEvent.getType() !== START_TYPE) {
|
||||
return false;
|
||||
}
|
||||
@@ -620,13 +662,13 @@ export class VerificationRequest extends EventEmitter {
|
||||
const oldContent = oldEvent.getContent();
|
||||
oldRaceIdentifier = oldContent && oldContent.from_device;
|
||||
} else {
|
||||
oldRaceIdentifier = this._client.getDeviceId();
|
||||
oldRaceIdentifier = this.client.getDeviceId();
|
||||
}
|
||||
} else {
|
||||
if (oldEvent) {
|
||||
oldRaceIdentifier = oldEvent.getSender();
|
||||
} else {
|
||||
oldRaceIdentifier = this._client.getUserId();
|
||||
oldRaceIdentifier = this.client.getUserId();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -640,13 +682,13 @@ export class VerificationRequest extends EventEmitter {
|
||||
return newRaceIdentifier < oldRaceIdentifier;
|
||||
}
|
||||
|
||||
hasEventId(eventId) {
|
||||
for (const event of this._eventsByUs.values()) {
|
||||
public hasEventId(eventId: string): boolean {
|
||||
for (const event of this.eventsByUs.values()) {
|
||||
if (event.getId() === eventId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (const event of this._eventsByThem.values()) {
|
||||
for (const event of this.eventsByThem.values()) {
|
||||
if (event.getId() === eventId) {
|
||||
return true;
|
||||
}
|
||||
@@ -658,23 +700,29 @@ export class VerificationRequest extends EventEmitter {
|
||||
* Changes the state of the request and verifier in response to a key verification event.
|
||||
* @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel.
|
||||
* @param {MatrixEvent} event the event to handle. Don't call getType() on it but use the `type` parameter instead.
|
||||
* @param {bool} isLiveEvent whether this is an even received through sync or not
|
||||
* @param {bool} isRemoteEcho whether this is the remote echo of an event sent by the same device
|
||||
* @param {bool} isSentByUs whether this event is sent by a party that can accept and/or observe the request like one of our peers.
|
||||
* @param {boolean} isLiveEvent whether this is an even received through sync or not
|
||||
* @param {boolean} isRemoteEcho whether this is the remote echo of an event sent by the same device
|
||||
* @param {boolean} isSentByUs whether this event is sent by a party that can accept and/or observe the request like one of our peers.
|
||||
* For InRoomChannel this means any device for the syncing user. For ToDeviceChannel, just the syncing device.
|
||||
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
|
||||
* @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent.
|
||||
*/
|
||||
async handleEvent(type, event, isLiveEvent, isRemoteEcho, isSentByUs) {
|
||||
public async handleEvent(
|
||||
type: string,
|
||||
event: MatrixEvent,
|
||||
isLiveEvent: boolean,
|
||||
isRemoteEcho: boolean,
|
||||
isSentByUs: boolean,
|
||||
): Promise<void> {
|
||||
// if reached phase cancelled or done, ignore anything else that comes
|
||||
if (this.done || this.cancelled) {
|
||||
return;
|
||||
}
|
||||
const wasObserveOnly = this._observeOnly;
|
||||
|
||||
this._adjustObserveOnly(event, isLiveEvent);
|
||||
this.adjustObserveOnly(event, isLiveEvent);
|
||||
|
||||
if (!this.observeOnly && !isRemoteEcho) {
|
||||
if (await this._cancelOnError(type, event)) {
|
||||
if (await this.cancelOnError(type, event)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -685,27 +733,26 @@ export class VerificationRequest extends EventEmitter {
|
||||
// added here to prevent verification getting cancelled
|
||||
// when the server duplicates an event (https://github.com/matrix-org/synapse/issues/3365)
|
||||
const isDuplicateEvent = isSentByUs ?
|
||||
this._eventsByUs.has(type) :
|
||||
this._eventsByThem.has(type);
|
||||
this.eventsByUs.has(type) :
|
||||
this.eventsByThem.has(type);
|
||||
if (isDuplicateEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldPhase = this.phase;
|
||||
this._addEvent(type, event, isSentByUs);
|
||||
this.addEvent(type, event, isSentByUs);
|
||||
|
||||
// this will create if needed the verifier so needs to happen before calling it
|
||||
const newTransitions = this._applyPhaseTransitions();
|
||||
const newTransitions = this.applyPhaseTransitions();
|
||||
try {
|
||||
// only pass events from the other side to the verifier,
|
||||
// no remote echos of our own events
|
||||
if (this._verifier && !this.observeOnly) {
|
||||
const newEventWinsRace = this._isWinningStartRace(event);
|
||||
const newEventWinsRace = this.isWinningStartRace(event);
|
||||
if (this._verifier.canSwitchStartEvent(event) && newEventWinsRace) {
|
||||
this._verifier.switchStartEvent(event);
|
||||
} else if (!isRemoteEcho) {
|
||||
if (type === CANCEL_TYPE || (this._verifier.events
|
||||
&& this._verifier.events.includes(type))) {
|
||||
if (type === CANCEL_TYPE || this._verifier.events?.includes(type)) {
|
||||
this._verifier.handleEvent(event);
|
||||
}
|
||||
}
|
||||
@@ -722,18 +769,18 @@ export class VerificationRequest extends EventEmitter {
|
||||
const shouldGenerateQrCode =
|
||||
this.otherPartySupportsMethod(SCAN_QR_CODE_METHOD, true);
|
||||
if (shouldGenerateQrCode) {
|
||||
this._qrCodeData = await QRCodeData.create(this, this._client);
|
||||
this._qrCodeData = await QRCodeData.create(this, this.client);
|
||||
}
|
||||
}
|
||||
|
||||
const lastTransition = newTransitions[newTransitions.length - 1];
|
||||
const { phase } = lastTransition;
|
||||
|
||||
this._setupTimeout(phase);
|
||||
this.setupTimeout(phase);
|
||||
// set phase as last thing as this emits the "change" event
|
||||
this._setPhase(phase);
|
||||
this.setPhase(phase);
|
||||
} else if (this._observeOnly !== wasObserveOnly) {
|
||||
this.emit("change");
|
||||
this.emit(VerificationRequestEvent.Change);
|
||||
}
|
||||
} finally {
|
||||
// log events we processed so we can see from rageshakes what events were added to a request
|
||||
@@ -748,26 +795,26 @@ export class VerificationRequest extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
_setupTimeout(phase) {
|
||||
const shouldTimeout = !this._timeoutTimer && !this.observeOnly &&
|
||||
private setupTimeout(phase: Phase): void {
|
||||
const shouldTimeout = !this.timeoutTimer && !this.observeOnly &&
|
||||
phase === PHASE_REQUESTED;
|
||||
|
||||
if (shouldTimeout) {
|
||||
this._timeoutTimer = setTimeout(this._cancelOnTimeout, this.timeout);
|
||||
this.timeoutTimer = setTimeout(this.cancelOnTimeout, this.timeout);
|
||||
}
|
||||
if (this._timeoutTimer) {
|
||||
if (this.timeoutTimer) {
|
||||
const shouldClear = phase === PHASE_STARTED ||
|
||||
phase === PHASE_READY ||
|
||||
phase === PHASE_DONE ||
|
||||
phase === PHASE_CANCELLED;
|
||||
if (shouldClear) {
|
||||
clearTimeout(this._timeoutTimer);
|
||||
this._timeoutTimer = null;
|
||||
clearTimeout(this.timeoutTimer);
|
||||
this.timeoutTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_cancelOnTimeout = () => {
|
||||
private cancelOnTimeout = () => {
|
||||
try {
|
||||
if (this.initiatedByMe) {
|
||||
this.cancel({
|
||||
@@ -785,10 +832,10 @@ export class VerificationRequest extends EventEmitter {
|
||||
}
|
||||
};
|
||||
|
||||
async _cancelOnError(type, event) {
|
||||
private async cancelOnError(type: string, event: MatrixEvent): Promise<boolean> {
|
||||
if (type === START_TYPE) {
|
||||
const method = event.getContent().method;
|
||||
if (!this._verificationMethods.has(method)) {
|
||||
if (!this.verificationMethods.has(method)) {
|
||||
await this.cancel(errorFromEvent(newUnknownMethodError()));
|
||||
return true;
|
||||
}
|
||||
@@ -811,7 +858,7 @@ export class VerificationRequest extends EventEmitter {
|
||||
return false;
|
||||
}
|
||||
|
||||
_adjustObserveOnly(event, isLiveEvent) {
|
||||
private adjustObserveOnly(event: MatrixEvent, isLiveEvent = false): void {
|
||||
// don't send out events for historical requests
|
||||
if (!isLiveEvent) {
|
||||
this._observeOnly = true;
|
||||
@@ -821,83 +868,80 @@ export class VerificationRequest extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
_addEvent(type, event, isSentByUs) {
|
||||
private addEvent(type: string, event: MatrixEvent, isSentByUs = false): void {
|
||||
if (isSentByUs) {
|
||||
this._eventsByUs.set(type, event);
|
||||
this.eventsByUs.set(type, event);
|
||||
} else {
|
||||
this._eventsByThem.set(type, event);
|
||||
this.eventsByThem.set(type, event);
|
||||
}
|
||||
|
||||
// once we know the userId of the other party (from the .request event)
|
||||
// see if any event by anyone else crept into this._eventsByThem
|
||||
// see if any event by anyone else crept into this.eventsByThem
|
||||
if (type === REQUEST_TYPE) {
|
||||
for (const [type, event] of this._eventsByThem.entries()) {
|
||||
for (const [type, event] of this.eventsByThem.entries()) {
|
||||
if (event.getSender() !== this.otherUserId) {
|
||||
this._eventsByThem.delete(type);
|
||||
this.eventsByThem.delete(type);
|
||||
}
|
||||
}
|
||||
// also remember when we received the request event
|
||||
this._requestReceivedAt = Date.now();
|
||||
this.requestReceivedAt = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
_createVerifier(method, startEvent = null, targetDevice = null) {
|
||||
private createVerifier(
|
||||
method: VerificationMethod,
|
||||
startEvent: MatrixEvent = null,
|
||||
targetDevice: ITargetDevice = null,
|
||||
): VerificationBase<any, any> {
|
||||
if (!targetDevice) {
|
||||
targetDevice = this.targetDevice;
|
||||
}
|
||||
const { userId, deviceId } = targetDevice;
|
||||
|
||||
const VerifierCtor = this._verificationMethods.get(method);
|
||||
const VerifierCtor = this.verificationMethods.get(method);
|
||||
if (!VerifierCtor) {
|
||||
logger.warn("could not find verifier constructor for method", method);
|
||||
return;
|
||||
}
|
||||
return new VerifierCtor(
|
||||
this.channel,
|
||||
this._client,
|
||||
userId,
|
||||
deviceId,
|
||||
startEvent,
|
||||
this,
|
||||
);
|
||||
return new VerifierCtor(this.channel, this.client, userId, deviceId, startEvent, this);
|
||||
}
|
||||
|
||||
_wasSentByOwnUser(event) {
|
||||
return event.getSender() === this._client.getUserId();
|
||||
private wasSentByOwnUser(event: MatrixEvent): boolean {
|
||||
return event.getSender() === this.client.getUserId();
|
||||
}
|
||||
|
||||
// only for .request, .ready or .start
|
||||
_wasSentByOwnDevice(event) {
|
||||
if (!this._wasSentByOwnUser(event)) {
|
||||
private wasSentByOwnDevice(event: MatrixEvent): boolean {
|
||||
if (!this.wasSentByOwnUser(event)) {
|
||||
return false;
|
||||
}
|
||||
const content = event.getContent();
|
||||
if (!content || content.from_device !== this._client.getDeviceId()) {
|
||||
if (!content || content.from_device !== this.client.getDeviceId()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
onVerifierCancelled() {
|
||||
public onVerifierCancelled(): void {
|
||||
this._cancelled = true;
|
||||
// move to cancelled phase
|
||||
const newTransitions = this._applyPhaseTransitions();
|
||||
const newTransitions = this.applyPhaseTransitions();
|
||||
if (newTransitions.length) {
|
||||
this._setPhase(newTransitions[newTransitions.length - 1].phase);
|
||||
this.setPhase(newTransitions[newTransitions.length - 1].phase);
|
||||
}
|
||||
}
|
||||
|
||||
onVerifierFinished() {
|
||||
public onVerifierFinished(): void {
|
||||
this.channel.send("m.key.verification.done", {});
|
||||
this._verifierHasFinished = true;
|
||||
this.verifierHasFinished = true;
|
||||
// move to .done phase
|
||||
const newTransitions = this._applyPhaseTransitions();
|
||||
const newTransitions = this.applyPhaseTransitions();
|
||||
if (newTransitions.length) {
|
||||
this._setPhase(newTransitions[newTransitions.length - 1].phase);
|
||||
this.setPhase(newTransitions[newTransitions.length - 1].phase);
|
||||
}
|
||||
}
|
||||
|
||||
getEventFromOtherParty(type) {
|
||||
return this._eventsByThem.get(type);
|
||||
public getEventFromOtherParty(type: string): MatrixEvent {
|
||||
return this.eventsByThem.get(type);
|
||||
}
|
||||
}
|
||||
+12
-3
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "./client";
|
||||
import { IEvent, MatrixEvent } from "./models/event";
|
||||
import { IEvent, MatrixEvent, MatrixEventEvent } from "./models/event";
|
||||
|
||||
export type EventMapper = (obj: Partial<IEvent>) => MatrixEvent;
|
||||
|
||||
@@ -30,10 +30,16 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
|
||||
|
||||
function mapper(plainOldJsObject: Partial<IEvent>) {
|
||||
const event = new MatrixEvent(plainOldJsObject);
|
||||
|
||||
const room = client.getRoom(event.getRoomId());
|
||||
if (room?.threads.has(event.getId())) {
|
||||
event.setThread(room.threads.get(event.getId()));
|
||||
}
|
||||
|
||||
if (event.isEncrypted()) {
|
||||
if (!preventReEmit) {
|
||||
client.reEmitter.reEmit(event, [
|
||||
"Event.decrypted",
|
||||
MatrixEventEvent.Decrypted,
|
||||
]);
|
||||
}
|
||||
if (decrypt) {
|
||||
@@ -41,7 +47,10 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
|
||||
}
|
||||
}
|
||||
if (!preventReEmit) {
|
||||
client.reEmitter.reEmit(event, ["Event.replaced"]);
|
||||
client.reEmitter.reEmit(event, [
|
||||
MatrixEventEvent.Replaced,
|
||||
MatrixEventEvent.VisibilityChange,
|
||||
]);
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
+56
-4
@@ -14,6 +14,11 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { RelationType } from "./@types/event";
|
||||
import {
|
||||
UNSTABLE_FILTER_RELATED_BY_REL_TYPES,
|
||||
UNSTABLE_FILTER_RELATED_BY_SENDERS,
|
||||
} from "./filter";
|
||||
import { MatrixEvent } from "./models/event";
|
||||
|
||||
/**
|
||||
@@ -46,6 +51,8 @@ export interface IFilterComponent {
|
||||
not_senders?: string[];
|
||||
contains_url?: boolean;
|
||||
limit?: number;
|
||||
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]?: string[];
|
||||
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]?: Array<RelationType | string>;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
@@ -61,7 +68,7 @@ export interface IFilterComponent {
|
||||
* @param {Object} filterJson the definition of this filter JSON, e.g. { 'contains_url': true }
|
||||
*/
|
||||
export class FilterComponent {
|
||||
constructor(private filterJson: IFilterComponent) {}
|
||||
constructor(private filterJson: IFilterComponent, public readonly userId?: string) {}
|
||||
|
||||
/**
|
||||
* Checks with the filter component matches the given event
|
||||
@@ -69,11 +76,25 @@ export class FilterComponent {
|
||||
* @return {boolean} true if the event matches the filter
|
||||
*/
|
||||
public check(event: MatrixEvent): boolean {
|
||||
const bundledRelationships = event.getUnsigned()?.["m.relations"] || {};
|
||||
const relations: Array<string | RelationType> = Object.keys(bundledRelationships);
|
||||
// Relation senders allows in theory a look-up of any senders
|
||||
// however clients can only know about the current user participation status
|
||||
// as sending a whole list of participants could be proven problematic in terms
|
||||
// of performance
|
||||
// This should be improved when bundled relationships solve that problem
|
||||
const relationSenders = [];
|
||||
if (this.userId && bundledRelationships?.[RelationType.Thread]?.current_user_participated) {
|
||||
relationSenders.push(this.userId);
|
||||
}
|
||||
|
||||
return this.checkFields(
|
||||
event.getRoomId(),
|
||||
event.getSender(),
|
||||
event.getType(),
|
||||
event.getContent() ? event.getContent().url !== undefined : false,
|
||||
relations,
|
||||
relationSenders,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -89,6 +110,8 @@ export class FilterComponent {
|
||||
senders: this.filterJson.senders || null,
|
||||
not_senders: this.filterJson.not_senders || [],
|
||||
contains_url: this.filterJson.contains_url || null,
|
||||
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]: UNSTABLE_FILTER_RELATED_BY_SENDERS.findIn(this.filterJson),
|
||||
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]: UNSTABLE_FILTER_RELATED_BY_REL_TYPES.findIn(this.filterJson),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -98,9 +121,18 @@ export class FilterComponent {
|
||||
* @param {String} sender the sender of the event being checked
|
||||
* @param {String} eventType the type of the event being checked
|
||||
* @param {boolean} containsUrl whether the event contains a content.url field
|
||||
* @param {boolean} relationTypes whether has aggregated relation of the given type
|
||||
* @param {boolean} relationSenders whether one of the relation is sent by the user listed
|
||||
* @return {boolean} true if the event fields match the filter
|
||||
*/
|
||||
private checkFields(roomId: string, sender: string, eventType: string, containsUrl: boolean): boolean {
|
||||
private checkFields(
|
||||
roomId: string,
|
||||
sender: string,
|
||||
eventType: string,
|
||||
containsUrl: boolean,
|
||||
relationTypes: Array<RelationType | string>,
|
||||
relationSenders: string[],
|
||||
): boolean {
|
||||
const literalKeys = {
|
||||
"rooms": function(v: string): boolean {
|
||||
return roomId === v;
|
||||
@@ -133,15 +165,35 @@ export class FilterComponent {
|
||||
return false;
|
||||
}
|
||||
|
||||
const relationTypesFilter = this.filterJson[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name];
|
||||
if (relationTypesFilter !== undefined) {
|
||||
if (!this.arrayMatchesFilter(relationTypesFilter, relationTypes)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const relationSendersFilter = this.filterJson[UNSTABLE_FILTER_RELATED_BY_SENDERS.name];
|
||||
if (relationSendersFilter !== undefined) {
|
||||
if (!this.arrayMatchesFilter(relationSendersFilter, relationSenders)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private arrayMatchesFilter(filter: any[], values: any[]): boolean {
|
||||
return values.length > 0 && values.every(value => {
|
||||
return filter.includes(value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters a list of events down to those which match this filter component
|
||||
* @param {MatrixEvent[]} events Events to be checked against the filter component
|
||||
* @return {MatrixEvent[]} events which matched the filter component
|
||||
*/
|
||||
filter(events: MatrixEvent[]): MatrixEvent[] {
|
||||
public filter(events: MatrixEvent[]): MatrixEvent[] {
|
||||
return events.filter(this.check, this);
|
||||
}
|
||||
|
||||
@@ -150,7 +202,7 @@ export class FilterComponent {
|
||||
* 10 if none is otherwise specified. Cargo-culted from Synapse.
|
||||
* @return {Number} the limit for this filter component.
|
||||
*/
|
||||
limit(): number {
|
||||
public limit(): number {
|
||||
return this.filterJson.limit !== undefined ? this.filterJson.limit : 10;
|
||||
}
|
||||
}
|
||||
|
||||
+20
-2
@@ -18,8 +18,23 @@ limitations under the License.
|
||||
* @module filter
|
||||
*/
|
||||
|
||||
import {
|
||||
EventType,
|
||||
RelationType,
|
||||
} from "./@types/event";
|
||||
import { FilterComponent, IFilterComponent } from "./filter-component";
|
||||
import { MatrixEvent } from "./models/event";
|
||||
import { UnstableValue } from "./NamespacedValue";
|
||||
|
||||
export const UNSTABLE_FILTER_RELATED_BY_SENDERS = new UnstableValue(
|
||||
"related_by_senders",
|
||||
"io.element.relation_senders",
|
||||
);
|
||||
|
||||
export const UNSTABLE_FILTER_RELATED_BY_REL_TYPES = new UnstableValue(
|
||||
"related_by_rel_types",
|
||||
"io.element.relation_types",
|
||||
);
|
||||
|
||||
/**
|
||||
* @param {Object} obj
|
||||
@@ -50,6 +65,9 @@ export interface IFilterDefinition {
|
||||
export interface IRoomEventFilter extends IFilterComponent {
|
||||
lazy_load_members?: boolean;
|
||||
include_redundant_members?: boolean;
|
||||
types?: Array<EventType | string>;
|
||||
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]?: Array<RelationType | string>;
|
||||
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]?: string[];
|
||||
}
|
||||
|
||||
interface IStateFilter extends IRoomEventFilter {}
|
||||
@@ -167,8 +185,8 @@ export class Filter {
|
||||
}
|
||||
}
|
||||
|
||||
this.roomFilter = new FilterComponent(roomFilterFields);
|
||||
this.roomTimelineFilter = new FilterComponent(roomFilterJson?.timeline || {});
|
||||
this.roomFilter = new FilterComponent(roomFilterFields, this.userId);
|
||||
this.roomTimelineFilter = new FilterComponent(roomFilterJson?.timeline || {}, this.userId);
|
||||
|
||||
// don't bother porting this from synapse yet:
|
||||
// this._room_state_filter =
|
||||
|
||||
+360
-209
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -20,14 +20,21 @@ limitations under the License.
|
||||
* @module http-api
|
||||
*/
|
||||
|
||||
import { parse as parseContentType } from "content-type";
|
||||
import * as utils from "./utils";
|
||||
import { logger } from './logger';
|
||||
import { parse as parseContentType, ParsedMediaType } from "content-type";
|
||||
|
||||
import type { IncomingHttpHeaders, IncomingMessage } from "http";
|
||||
import type { Request as _Request, CoreOptions } from "request";
|
||||
// we use our own implementation of setTimeout, so that if we get suspended in
|
||||
// the middle of a /sync, we cancel the sync as soon as we awake, rather than
|
||||
// waiting for the delay to elapse.
|
||||
import * as callbacks from "./realtime-callbacks";
|
||||
import { IUploadOpts } from "./@types/requests";
|
||||
import { IAbortablePromise, IUsageLimit } from "./@types/partials";
|
||||
import { IDeferred } from "./utils";
|
||||
import { Callback } from "./client";
|
||||
import * as utils from "./utils";
|
||||
import { logger } from './logger';
|
||||
import { TypedEventEmitter } from "./models/typed-event-emitter";
|
||||
|
||||
/*
|
||||
TODO:
|
||||
@@ -40,6 +47,11 @@ TODO:
|
||||
*/
|
||||
export const PREFIX_R0 = "/_matrix/client/r0";
|
||||
|
||||
/**
|
||||
* A constant representing the URI path for release v1 of the Client-Server HTTP API.
|
||||
*/
|
||||
export const PREFIX_V1 = "/_matrix/client/v1";
|
||||
|
||||
/**
|
||||
* A constant representing the URI path for as-yet unspecified Client-Server HTTP APIs.
|
||||
*/
|
||||
@@ -61,10 +73,111 @@ export const PREFIX_IDENTITY_V2 = "/_matrix/identity/v2";
|
||||
*/
|
||||
export const PREFIX_MEDIA_R0 = "/_matrix/media/r0";
|
||||
|
||||
type RequestProps = "method"
|
||||
| "withCredentials"
|
||||
| "json"
|
||||
| "headers"
|
||||
| "qs"
|
||||
| "body"
|
||||
| "qsStringifyOptions"
|
||||
| "useQuerystring"
|
||||
| "timeout";
|
||||
|
||||
export interface IHttpOpts {
|
||||
baseUrl: string;
|
||||
idBaseUrl?: string;
|
||||
prefix: string;
|
||||
onlyData: boolean;
|
||||
accessToken?: string;
|
||||
extraParams?: Record<string, string>;
|
||||
localTimeoutMs?: number;
|
||||
useAuthorizationHeader?: boolean;
|
||||
request(opts: Pick<CoreOptions, RequestProps> & {
|
||||
uri: string;
|
||||
method: Method;
|
||||
// eslint-disable-next-line camelcase
|
||||
_matrix_opts: IHttpOpts;
|
||||
}, callback: RequestCallback): IRequest;
|
||||
}
|
||||
|
||||
interface IRequest extends _Request {
|
||||
onprogress?(e: unknown): void;
|
||||
}
|
||||
|
||||
interface IRequestOpts<T> {
|
||||
prefix?: string;
|
||||
localTimeoutMs?: number;
|
||||
headers?: Record<string, string>;
|
||||
json?: boolean; // defaults to true
|
||||
qsStringifyOptions?: CoreOptions["qsStringifyOptions"];
|
||||
bodyParser?(body: string): T;
|
||||
|
||||
// Set to true to prevent the request function from emitting
|
||||
// a Session.logged_out event. This is intended for use on
|
||||
// endpoints where M_UNKNOWN_TOKEN is a valid/notable error
|
||||
// response, such as with token refreshes.
|
||||
inhibitLogoutEmit?: boolean;
|
||||
}
|
||||
|
||||
export interface IUpload {
|
||||
loaded: number;
|
||||
total: number;
|
||||
promise: IAbortablePromise<unknown>;
|
||||
}
|
||||
|
||||
interface IContentUri {
|
||||
base: string;
|
||||
path: string;
|
||||
params: {
|
||||
// eslint-disable-next-line camelcase
|
||||
access_token: string;
|
||||
};
|
||||
}
|
||||
|
||||
type ResponseType<T, O extends IRequestOpts<T> | void = void> =
|
||||
O extends { bodyParser: (body: string) => T } ? T :
|
||||
O extends { json: false } ? string :
|
||||
T;
|
||||
|
||||
interface IUploadResponse {
|
||||
// eslint-disable-next-line camelcase
|
||||
content_uri: string;
|
||||
}
|
||||
|
||||
// This type's defaults only work for the Browser
|
||||
// in the Browser we default rawResponse = false & onlyContentUri = true
|
||||
// in Node we default rawResponse = true & onlyContentUri = false
|
||||
export type UploadContentResponseType<O extends IUploadOpts> =
|
||||
O extends undefined ? string :
|
||||
O extends { rawResponse: true } ? string :
|
||||
O extends { onlyContentUri: true } ? string :
|
||||
O extends { rawResponse: false } ? IUploadResponse :
|
||||
O extends { onlyContentUri: false } ? IUploadResponse :
|
||||
string;
|
||||
|
||||
export enum Method {
|
||||
Get = "GET",
|
||||
Put = "PUT",
|
||||
Post = "POST",
|
||||
Delete = "DELETE",
|
||||
}
|
||||
|
||||
export type FileType = Document | XMLHttpRequestBodyInit;
|
||||
|
||||
export enum HttpApiEvent {
|
||||
SessionLoggedOut = "Session.logged_out",
|
||||
NoConsent = "no_consent",
|
||||
}
|
||||
|
||||
export type HttpApiEventHandlerMap = {
|
||||
[HttpApiEvent.SessionLoggedOut]: (err: MatrixError) => void;
|
||||
[HttpApiEvent.NoConsent]: (message: string, consentUri: string) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a MatrixHttpApi.
|
||||
* @constructor
|
||||
* @param {EventEmitter} event_emitter The event emitter to use for emitting events
|
||||
* @param {EventEmitter} eventEmitter The event emitter to use for emitting events
|
||||
* @param {Object} opts The options to use for this HTTP API.
|
||||
* @param {string} opts.baseUrl Required. The base client-server URL e.g.
|
||||
* 'http://localhost:8008'.
|
||||
@@ -77,7 +190,7 @@ export const PREFIX_MEDIA_R0 = "/_matrix/media/r0";
|
||||
* response (e.g. the parsed HTTP body). If false, requests will return an
|
||||
* object with the properties <tt>code</tt>, <tt>headers</tt> and <tt>data</tt>.
|
||||
*
|
||||
* @param {string} opts.accessToken The access_token to send with requests. Can be
|
||||
* @param {string=} opts.accessToken The access_token to send with requests. Can be
|
||||
* null to not send an access token.
|
||||
* @param {Object=} opts.extraParams Optional. Extra query parameters to send on
|
||||
* requests.
|
||||
@@ -86,39 +199,40 @@ export const PREFIX_MEDIA_R0 = "/_matrix/media/r0";
|
||||
* @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use
|
||||
* Authorization header instead of query param to send the access token to the server.
|
||||
*/
|
||||
export function MatrixHttpApi(event_emitter, opts) {
|
||||
utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]);
|
||||
opts.onlyData = opts.onlyData || false;
|
||||
this.event_emitter = event_emitter;
|
||||
this.opts = opts;
|
||||
this.useAuthorizationHeader = Boolean(opts.useAuthorizationHeader);
|
||||
this.uploads = [];
|
||||
}
|
||||
export class MatrixHttpApi {
|
||||
private uploads: IUpload[] = [];
|
||||
|
||||
constructor(
|
||||
private eventEmitter: TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>,
|
||||
public readonly opts: IHttpOpts,
|
||||
) {
|
||||
utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]);
|
||||
opts.onlyData = !!opts.onlyData;
|
||||
opts.useAuthorizationHeader = !!opts.useAuthorizationHeader;
|
||||
}
|
||||
|
||||
MatrixHttpApi.prototype = {
|
||||
/**
|
||||
* Sets the baase URL for the identity server
|
||||
* Sets the base URL for the identity server
|
||||
* @param {string} url The new base url
|
||||
*/
|
||||
setIdBaseUrl: function(url) {
|
||||
public setIdBaseUrl(url: string): void {
|
||||
this.opts.idBaseUrl = url;
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content repository url with query parameters.
|
||||
* @return {Object} An object with a 'base', 'path' and 'params' for base URL,
|
||||
* path and query parameters respectively.
|
||||
*/
|
||||
getContentUri: function() {
|
||||
const params = {
|
||||
access_token: this.opts.accessToken,
|
||||
};
|
||||
public getContentUri(): IContentUri {
|
||||
return {
|
||||
base: this.opts.baseUrl,
|
||||
path: "/_matrix/media/r0/upload",
|
||||
params: params,
|
||||
params: {
|
||||
access_token: this.opts.accessToken,
|
||||
},
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload content to the homeserver
|
||||
@@ -160,14 +274,17 @@ MatrixHttpApi.prototype = {
|
||||
* determined by this.opts.onlyData, opts.rawResponse, and
|
||||
* opts.onlyContentUri. Rejects with an error (usually a MatrixError).
|
||||
*/
|
||||
uploadContent: function(file, opts) {
|
||||
public uploadContent<O extends IUploadOpts>(
|
||||
file: FileType,
|
||||
opts?: O,
|
||||
): IAbortablePromise<UploadContentResponseType<O>> {
|
||||
if (utils.isFunction(opts)) {
|
||||
// opts used to be callback
|
||||
// opts used to be callback, backwards compatibility
|
||||
opts = {
|
||||
callback: opts,
|
||||
};
|
||||
} else if (opts === undefined) {
|
||||
opts = {};
|
||||
callback: opts as unknown as IUploadOpts["callback"],
|
||||
} as O;
|
||||
} else if (!opts) {
|
||||
opts = {} as O;
|
||||
}
|
||||
|
||||
// default opts.includeFilename to true (ignoring falsey values)
|
||||
@@ -175,8 +292,8 @@ MatrixHttpApi.prototype = {
|
||||
|
||||
// if the file doesn't have a mime type, use a default since
|
||||
// the HS errors if we don't supply one.
|
||||
const contentType = opts.type || file.type || 'application/octet-stream';
|
||||
const fileName = opts.name || file.name;
|
||||
const contentType = opts.type || (file as File).type || 'application/octet-stream';
|
||||
const fileName = opts.name || (file as File).name;
|
||||
|
||||
// We used to recommend setting file.stream to the thing to upload on
|
||||
// Node.js. As of 2019-06-11, this is still in widespread use in various
|
||||
@@ -185,13 +302,14 @@ MatrixHttpApi.prototype = {
|
||||
// the browser now define a `stream` method, which leads to trouble
|
||||
// here, so we also check the type of `stream`.
|
||||
let body = file;
|
||||
if (body.stream && typeof body.stream !== "function") {
|
||||
const bodyStream = (body as File | Blob).stream; // this type is wrong but for legacy reasons is good enough
|
||||
if (bodyStream && typeof bodyStream !== "function") {
|
||||
logger.warn(
|
||||
"Using `file.stream` as the content to upload. Future " +
|
||||
"versions of the js-sdk will change this to expect `file` to " +
|
||||
"be the content directly.",
|
||||
);
|
||||
body = body.stream;
|
||||
body = bodyStream;
|
||||
}
|
||||
|
||||
// backwards-compatibility hacks where we used to do different things
|
||||
@@ -234,8 +352,8 @@ MatrixHttpApi.prototype = {
|
||||
// (browser-request doesn't support progress either, which is also kind
|
||||
// of important here)
|
||||
|
||||
const upload = { loaded: 0, total: 0 };
|
||||
let promise;
|
||||
const upload = { loaded: 0, total: 0 } as IUpload;
|
||||
let promise: IAbortablePromise<UploadContentResponseType<O>>;
|
||||
|
||||
// XMLHttpRequest doesn't parse JSON for us. request normally does, but
|
||||
// we're setting opts.json=false so that it doesn't JSON-encode the
|
||||
@@ -243,7 +361,7 @@ MatrixHttpApi.prototype = {
|
||||
// way, we have to JSON-parse the response ourselves.
|
||||
let bodyParser = null;
|
||||
if (!rawResponse) {
|
||||
bodyParser = function(rawBody) {
|
||||
bodyParser = function(rawBody: string) {
|
||||
let body = JSON.parse(rawBody);
|
||||
if (onlyContentUri) {
|
||||
body = body.content_uri;
|
||||
@@ -256,25 +374,23 @@ MatrixHttpApi.prototype = {
|
||||
}
|
||||
|
||||
if (global.XMLHttpRequest) {
|
||||
const defer = utils.defer();
|
||||
const defer = utils.defer<UploadContentResponseType<O>>();
|
||||
const xhr = new global.XMLHttpRequest();
|
||||
upload.xhr = xhr;
|
||||
const cb = requestCallback(defer, opts.callback, this.opts.onlyData);
|
||||
|
||||
const timeout_fn = function() {
|
||||
const timeoutFn = function() {
|
||||
xhr.abort();
|
||||
cb(new Error('Timeout'));
|
||||
};
|
||||
|
||||
// set an initial timeout of 30s; we'll advance it each time we get
|
||||
// a progress notification
|
||||
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
|
||||
// set an initial timeout of 30s; we'll advance it each time we get a progress notification
|
||||
let timeoutTimer = callbacks.setTimeout(timeoutFn, 30000);
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
let resp;
|
||||
let resp: string;
|
||||
switch (xhr.readyState) {
|
||||
case global.XMLHttpRequest.DONE:
|
||||
callbacks.clearTimeout(xhr.timeout_timer);
|
||||
callbacks.clearTimeout(timeoutTimer);
|
||||
try {
|
||||
if (xhr.status === 0) {
|
||||
throw new AbortError();
|
||||
@@ -296,10 +412,10 @@ MatrixHttpApi.prototype = {
|
||||
}
|
||||
};
|
||||
xhr.upload.addEventListener("progress", function(ev) {
|
||||
callbacks.clearTimeout(xhr.timeout_timer);
|
||||
callbacks.clearTimeout(timeoutTimer);
|
||||
upload.loaded = ev.loaded;
|
||||
upload.total = ev.total;
|
||||
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
|
||||
timeoutTimer = callbacks.setTimeout(timeoutFn, 30000);
|
||||
if (opts.progressHandler) {
|
||||
opts.progressHandler({
|
||||
loaded: ev.loaded,
|
||||
@@ -315,9 +431,8 @@ MatrixHttpApi.prototype = {
|
||||
queryArgs.push("filename=" + encodeURIComponent(fileName));
|
||||
}
|
||||
|
||||
if (!this.useAuthorizationHeader) {
|
||||
queryArgs.push("access_token="
|
||||
+ encodeURIComponent(this.opts.accessToken));
|
||||
if (!this.opts.useAuthorizationHeader) {
|
||||
queryArgs.push("access_token=" + encodeURIComponent(this.opts.accessToken));
|
||||
}
|
||||
|
||||
if (queryArgs.length > 0) {
|
||||
@@ -325,73 +440,79 @@ MatrixHttpApi.prototype = {
|
||||
}
|
||||
|
||||
xhr.open("POST", url);
|
||||
if (this.useAuthorizationHeader) {
|
||||
if (this.opts.useAuthorizationHeader) {
|
||||
xhr.setRequestHeader("Authorization", "Bearer " + this.opts.accessToken);
|
||||
}
|
||||
xhr.setRequestHeader("Content-Type", contentType);
|
||||
xhr.send(body);
|
||||
promise = defer.promise;
|
||||
promise = defer.promise as IAbortablePromise<UploadContentResponseType<O>>;
|
||||
|
||||
// dirty hack (as per _request) to allow the upload to be cancelled.
|
||||
// dirty hack (as per doRequest) to allow the upload to be cancelled.
|
||||
promise.abort = xhr.abort.bind(xhr);
|
||||
} else {
|
||||
const queryParams = {};
|
||||
const queryParams: Record<string, string> = {};
|
||||
|
||||
if (includeFilename && fileName) {
|
||||
queryParams.filename = fileName;
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = { "Content-Type": contentType };
|
||||
|
||||
// authedRequest uses `request` which is no longer maintained.
|
||||
// `request` has a bug where if the body is zero bytes then you get an error: `Argument error, options.body`.
|
||||
// See https://github.com/request/request/issues/920
|
||||
// if body looks like a byte array and empty then set the Content-Length explicitly as a workaround:
|
||||
if ((body as unknown as ArrayLike<number>).length === 0) {
|
||||
headers["Content-Length"] = "0";
|
||||
}
|
||||
|
||||
promise = this.authedRequest(
|
||||
opts.callback, "POST", "/upload", queryParams, body, {
|
||||
opts.callback, Method.Post, "/upload", queryParams, body, {
|
||||
prefix: "/_matrix/media/r0",
|
||||
headers: { "Content-Type": contentType },
|
||||
headers,
|
||||
json: false,
|
||||
bodyParser: bodyParser,
|
||||
bodyParser,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const self = this;
|
||||
|
||||
// remove the upload from the list on completion
|
||||
const promise0 = promise.finally(function() {
|
||||
for (let i = 0; i < self.uploads.length; ++i) {
|
||||
if (self.uploads[i] === upload) {
|
||||
self.uploads.splice(i, 1);
|
||||
upload.promise = promise.finally(() => {
|
||||
for (let i = 0; i < this.uploads.length; ++i) {
|
||||
if (this.uploads[i] === upload) {
|
||||
this.uploads.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}) as IAbortablePromise<UploadContentResponseType<O>>;
|
||||
|
||||
// copy our dirty abort() method to the new promise
|
||||
promise0.abort = promise.abort;
|
||||
|
||||
upload.promise = promise0;
|
||||
upload.promise.abort = promise.abort;
|
||||
this.uploads.push(upload);
|
||||
|
||||
return promise0;
|
||||
},
|
||||
return upload.promise as IAbortablePromise<UploadContentResponseType<O>>;
|
||||
}
|
||||
|
||||
cancelUpload: function(promise) {
|
||||
public cancelUpload(promise: IAbortablePromise<unknown>): boolean {
|
||||
if (promise.abort) {
|
||||
promise.abort();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
}
|
||||
|
||||
getCurrentUploads: function() {
|
||||
public getCurrentUploads(): IUpload[] {
|
||||
return this.uploads;
|
||||
},
|
||||
}
|
||||
|
||||
idServerRequest: function(
|
||||
callback,
|
||||
method,
|
||||
path,
|
||||
params,
|
||||
prefix,
|
||||
accessToken,
|
||||
) {
|
||||
public idServerRequest<T>(
|
||||
callback: Callback<T>,
|
||||
method: Method,
|
||||
path: string,
|
||||
params: Record<string, string | string[]>,
|
||||
prefix: string,
|
||||
accessToken: string,
|
||||
): Promise<T> {
|
||||
if (!this.opts.idBaseUrl) {
|
||||
throw new Error("No identity server base URL set");
|
||||
}
|
||||
@@ -406,28 +527,27 @@ MatrixHttpApi.prototype = {
|
||||
|
||||
const opts = {
|
||||
uri: fullUri,
|
||||
method: method,
|
||||
method,
|
||||
withCredentials: false,
|
||||
json: true, // we want a JSON response if we can
|
||||
_matrix_opts: this.opts,
|
||||
headers: {},
|
||||
};
|
||||
if (method === 'GET') {
|
||||
} as Parameters<IHttpOpts["request"]>[0];
|
||||
|
||||
if (method === Method.Get) {
|
||||
opts.qs = params;
|
||||
} else if (typeof params === "object") {
|
||||
opts.json = params;
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
opts.headers['Authorization'] = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
const defer = utils.defer();
|
||||
this.opts.request(
|
||||
opts,
|
||||
requestCallback(defer, callback, this.opts.onlyData),
|
||||
);
|
||||
const defer = utils.defer<T>();
|
||||
this.opts.request(opts, requestCallback(defer, callback, this.opts.onlyData));
|
||||
return defer.promise;
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform an authorised request to the homeserver.
|
||||
@@ -448,7 +568,7 @@ MatrixHttpApi.prototype = {
|
||||
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
|
||||
* timing out the request. If not specified, there is no timeout.
|
||||
*
|
||||
* @param {sting=} opts.prefix The full prefix to use e.g.
|
||||
* @param {string=} opts.prefix The full prefix to use e.g.
|
||||
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
|
||||
*
|
||||
* @param {Object=} opts.headers map of additional request headers
|
||||
@@ -460,56 +580,52 @@ MatrixHttpApi.prototype = {
|
||||
* @return {module:http-api.MatrixError} Rejects with an error if a problem
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
authedRequest: function(callback, method, path, queryParams, data, opts) {
|
||||
if (!queryParams) {
|
||||
queryParams = {};
|
||||
}
|
||||
if (this.useAuthorizationHeader) {
|
||||
if (isFinite(opts)) {
|
||||
public authedRequest<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
|
||||
callback: Callback<T>,
|
||||
method: Method,
|
||||
path: string,
|
||||
queryParams?: Record<string, string | string[]>,
|
||||
data?: CoreOptions["body"],
|
||||
opts?: O | number, // number is legacy
|
||||
): IAbortablePromise<ResponseType<T, O>> {
|
||||
if (!queryParams) queryParams = {};
|
||||
let requestOpts = (opts || {}) as O;
|
||||
|
||||
if (this.opts.useAuthorizationHeader) {
|
||||
if (isFinite(opts as number)) {
|
||||
// opts used to be localTimeoutMs
|
||||
opts = {
|
||||
localTimeoutMs: opts,
|
||||
};
|
||||
requestOpts = {
|
||||
localTimeoutMs: opts as number,
|
||||
} as O;
|
||||
}
|
||||
if (!opts) {
|
||||
opts = {};
|
||||
|
||||
if (!requestOpts.headers) {
|
||||
requestOpts.headers = {};
|
||||
}
|
||||
if (!opts.headers) {
|
||||
opts.headers = {};
|
||||
}
|
||||
if (!opts.headers.Authorization) {
|
||||
opts.headers.Authorization = "Bearer " + this.opts.accessToken;
|
||||
if (!requestOpts.headers.Authorization) {
|
||||
requestOpts.headers.Authorization = "Bearer " + this.opts.accessToken;
|
||||
}
|
||||
if (queryParams.access_token) {
|
||||
delete queryParams.access_token;
|
||||
}
|
||||
} else {
|
||||
if (!queryParams.access_token) {
|
||||
queryParams.access_token = this.opts.accessToken;
|
||||
}
|
||||
} else if (!queryParams.access_token) {
|
||||
queryParams.access_token = this.opts.accessToken;
|
||||
}
|
||||
|
||||
const requestPromise = this.request(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
);
|
||||
const requestPromise = this.request<T, O>(callback, method, path, queryParams, data, requestOpts);
|
||||
|
||||
const self = this;
|
||||
requestPromise.catch(function(err) {
|
||||
if (err.errcode == 'M_UNKNOWN_TOKEN') {
|
||||
self.event_emitter.emit("Session.logged_out", err);
|
||||
requestPromise.catch((err: MatrixError) => {
|
||||
if (err.errcode == 'M_UNKNOWN_TOKEN' && !requestOpts?.inhibitLogoutEmit) {
|
||||
this.eventEmitter.emit(HttpApiEvent.SessionLoggedOut, err);
|
||||
} else if (err.errcode == 'M_CONSENT_NOT_GIVEN') {
|
||||
self.event_emitter.emit(
|
||||
"no_consent",
|
||||
err.message,
|
||||
err.data.consent_uri,
|
||||
);
|
||||
this.eventEmitter.emit(HttpApiEvent.NoConsent, err.message, err.data.consent_uri);
|
||||
}
|
||||
});
|
||||
|
||||
// return the original promise, otherwise tests break due to it having to
|
||||
// go around the event loop one more time to process the result of the request
|
||||
return requestPromise;
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a request to the homeserver without any credentials.
|
||||
@@ -529,7 +645,7 @@ MatrixHttpApi.prototype = {
|
||||
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
|
||||
* timing out the request. If not specified, there is no timeout.
|
||||
*
|
||||
* @param {sting=} opts.prefix The full prefix to use e.g.
|
||||
* @param {string=} opts.prefix The full prefix to use e.g.
|
||||
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
|
||||
*
|
||||
* @param {Object=} opts.headers map of additional request headers
|
||||
@@ -541,15 +657,19 @@ MatrixHttpApi.prototype = {
|
||||
* @return {module:http-api.MatrixError} Rejects with an error if a problem
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
request: function(callback, method, path, queryParams, data, opts) {
|
||||
opts = opts || {};
|
||||
const prefix = opts.prefix !== undefined ? opts.prefix : this.opts.prefix;
|
||||
public request<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
|
||||
callback: Callback<T>,
|
||||
method: Method,
|
||||
path: string,
|
||||
queryParams?: CoreOptions["qs"],
|
||||
data?: CoreOptions["body"],
|
||||
opts?: O,
|
||||
): IAbortablePromise<ResponseType<T, O>> {
|
||||
const prefix = opts?.prefix ?? this.opts.prefix;
|
||||
const fullUri = this.opts.baseUrl + prefix + path;
|
||||
|
||||
return this.requestOtherUrl(
|
||||
callback, method, fullUri, queryParams, data, opts,
|
||||
);
|
||||
},
|
||||
return this.requestOtherUrl<T, O>(callback, method, fullUri, queryParams, data, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a request to an arbitrary URL.
|
||||
@@ -568,7 +688,7 @@ MatrixHttpApi.prototype = {
|
||||
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
|
||||
* timing out the request. If not specified, there is no timeout.
|
||||
*
|
||||
* @param {sting=} opts.prefix The full prefix to use e.g.
|
||||
* @param {string=} opts.prefix The full prefix to use e.g.
|
||||
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
|
||||
*
|
||||
* @param {Object=} opts.headers map of additional request headers
|
||||
@@ -580,21 +700,24 @@ MatrixHttpApi.prototype = {
|
||||
* @return {module:http-api.MatrixError} Rejects with an error if a problem
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
requestOtherUrl: function(callback, method, uri, queryParams, data,
|
||||
opts) {
|
||||
if (opts === undefined || opts === null) {
|
||||
opts = {};
|
||||
} else if (isFinite(opts)) {
|
||||
public requestOtherUrl<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
|
||||
callback: Callback<T>,
|
||||
method: Method,
|
||||
uri: string,
|
||||
queryParams?: CoreOptions["qs"],
|
||||
data?: CoreOptions["body"],
|
||||
opts?: O | number, // number is legacy
|
||||
): IAbortablePromise<ResponseType<T, O>> {
|
||||
let requestOpts = (opts || {}) as O;
|
||||
if (isFinite(opts as number)) {
|
||||
// opts used to be localTimeoutMs
|
||||
opts = {
|
||||
localTimeoutMs: opts,
|
||||
};
|
||||
requestOpts = {
|
||||
localTimeoutMs: opts as number,
|
||||
} as O;
|
||||
}
|
||||
|
||||
return this._request(
|
||||
callback, method, uri, queryParams, data, opts,
|
||||
);
|
||||
},
|
||||
return this.doRequest<T, O>(callback, method, uri, queryParams, data, requestOpts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Form and return a homeserver request URL based on the given path
|
||||
@@ -607,13 +730,13 @@ MatrixHttpApi.prototype = {
|
||||
* "/_matrix/client/v2_alpha".
|
||||
* @return {string} URL
|
||||
*/
|
||||
getUrl: function(path, queryParams, prefix) {
|
||||
public getUrl(path: string, queryParams: CoreOptions["qs"], prefix: string): string {
|
||||
let queryString = "";
|
||||
if (queryParams) {
|
||||
queryString = "?" + utils.encodeParams(queryParams);
|
||||
}
|
||||
return this.opts.baseUrl + prefix + path + queryString;
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
@@ -640,25 +763,32 @@ MatrixHttpApi.prototype = {
|
||||
* @return {Promise} a promise which resolves to either the
|
||||
* response object (if this.opts.onlyData is truthy), or the parsed
|
||||
* body. Rejects
|
||||
*
|
||||
* Generic T is the callback/promise resolve type
|
||||
* Generic O should be inferred
|
||||
*/
|
||||
_request: function(callback, method, uri, queryParams, data, opts) {
|
||||
private doRequest<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
|
||||
callback: Callback<T>,
|
||||
method: Method,
|
||||
uri: string,
|
||||
queryParams?: Record<string, string>,
|
||||
data?: CoreOptions["body"],
|
||||
opts?: O,
|
||||
): IAbortablePromise<ResponseType<T, O>> {
|
||||
if (callback !== undefined && !utils.isFunction(callback)) {
|
||||
throw Error(
|
||||
"Expected callback to be a function but got " + typeof callback,
|
||||
);
|
||||
throw Error("Expected callback to be a function but got " + typeof callback);
|
||||
}
|
||||
opts = opts || {};
|
||||
|
||||
const self = this;
|
||||
if (this.opts.extraParams) {
|
||||
queryParams = {
|
||||
...queryParams,
|
||||
...this.opts.extraParams,
|
||||
...(queryParams || {}),
|
||||
...this.opts.extraParams,
|
||||
};
|
||||
}
|
||||
|
||||
const headers = utils.extend({}, opts.headers || {});
|
||||
const json = opts.json === undefined ? true : opts.json;
|
||||
const headers = Object.assign({}, opts.headers || {});
|
||||
if (!opts) opts = {} as O;
|
||||
const json = opts.json ?? true;
|
||||
let bodyParser = opts.bodyParser;
|
||||
|
||||
// we handle the json encoding/decoding here, because request and
|
||||
@@ -677,17 +807,17 @@ MatrixHttpApi.prototype = {
|
||||
}
|
||||
|
||||
if (bodyParser === undefined) {
|
||||
bodyParser = function(rawBody) {
|
||||
bodyParser = function(rawBody: string) {
|
||||
return JSON.parse(rawBody);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const defer = utils.defer();
|
||||
const defer = utils.defer<T>();
|
||||
|
||||
let timeoutId;
|
||||
let timeoutId: number;
|
||||
let timedOut = false;
|
||||
let req;
|
||||
let req: IRequest;
|
||||
const localTimeoutMs = opts.localTimeoutMs || this.opts.localTimeoutMs;
|
||||
|
||||
const resetTimeout = () => {
|
||||
@@ -697,9 +827,7 @@ MatrixHttpApi.prototype = {
|
||||
}
|
||||
timeoutId = callbacks.setTimeout(function() {
|
||||
timedOut = true;
|
||||
if (req && req.abort) {
|
||||
req.abort();
|
||||
}
|
||||
req?.abort?.();
|
||||
defer.reject(new MatrixError({
|
||||
error: "Locally timed out waiting for a response",
|
||||
errcode: "ORG.MATRIX.JSSDK_TIMEOUT",
|
||||
@@ -710,7 +838,7 @@ MatrixHttpApi.prototype = {
|
||||
};
|
||||
resetTimeout();
|
||||
|
||||
const reqPromise = defer.promise;
|
||||
const reqPromise = defer.promise as IAbortablePromise<ResponseType<T, O>>;
|
||||
|
||||
try {
|
||||
req = this.opts.request(
|
||||
@@ -727,7 +855,7 @@ MatrixHttpApi.prototype = {
|
||||
headers: headers || {},
|
||||
_matrix_opts: this.opts,
|
||||
},
|
||||
function(err, response, body) {
|
||||
(err, response, body) => {
|
||||
if (localTimeoutMs) {
|
||||
callbacks.clearTimeout(timeoutId);
|
||||
if (timedOut) {
|
||||
@@ -735,16 +863,13 @@ MatrixHttpApi.prototype = {
|
||||
}
|
||||
}
|
||||
|
||||
const handlerFn = requestCallback(
|
||||
defer, callback, self.opts.onlyData,
|
||||
bodyParser,
|
||||
);
|
||||
const handlerFn = requestCallback(defer, callback, this.opts.onlyData, bodyParser);
|
||||
handlerFn(err, response, body);
|
||||
},
|
||||
);
|
||||
if (req) {
|
||||
// This will only work in a browser, where opts.request is the
|
||||
// `browser-request` import. Currently `request` does not support progress
|
||||
// `browser-request` import. Currently, `request` does not support progress
|
||||
// updates - see https://github.com/request/request/pull/2346.
|
||||
// `browser-request` returns an XHRHttpRequest which exposes `onprogress`
|
||||
if ('onprogress' in req) {
|
||||
@@ -757,7 +882,9 @@ MatrixHttpApi.prototype = {
|
||||
|
||||
// FIXME: This is EVIL, but I can't think of a better way to expose
|
||||
// abort() operations on underlying HTTP requests :(
|
||||
if (req.abort) reqPromise.abort = req.abort.bind(req);
|
||||
if (req.abort) {
|
||||
reqPromise.abort = req.abort.bind(req);
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
defer.reject(ex);
|
||||
@@ -766,8 +893,21 @@ MatrixHttpApi.prototype = {
|
||||
}
|
||||
}
|
||||
return reqPromise;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type RequestCallback = (err?: Error, response?: XMLHttpRequest | IncomingMessage, body?: string) => void;
|
||||
|
||||
// if using onlyData=false then wrap your expected data type in this generic
|
||||
export interface IResponse<T> {
|
||||
code: number;
|
||||
data: T;
|
||||
headers?: IncomingHttpHeaders;
|
||||
}
|
||||
|
||||
function getStatusCode(response: XMLHttpRequest | IncomingMessage): number {
|
||||
return (response as XMLHttpRequest).status || (response as IncomingMessage).statusCode;
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns a callback that can be invoked by an HTTP request on completion,
|
||||
@@ -783,17 +923,17 @@ MatrixHttpApi.prototype = {
|
||||
* response, otherwise the result object (with `code` and `data` fields)
|
||||
*
|
||||
*/
|
||||
const requestCallback = function(
|
||||
defer, userDefinedCallback, onlyData,
|
||||
bodyParser,
|
||||
) {
|
||||
userDefinedCallback = userDefinedCallback || function() {};
|
||||
|
||||
return function(err, response, body) {
|
||||
function requestCallback<T>(
|
||||
defer: IDeferred<T>,
|
||||
userDefinedCallback?: Callback<T>,
|
||||
onlyData = false,
|
||||
bodyParser?: (body: string) => T,
|
||||
): RequestCallback {
|
||||
return function(err: Error, response: XMLHttpRequest | IncomingMessage, body: string): void {
|
||||
if (err) {
|
||||
// the unit tests use matrix-mock-request, which throw the string "aborted" when aborting a request.
|
||||
// See https://github.com/matrix-org/matrix-mock-request/blob/3276d0263a561b5b8326b47bae720578a2c7473a/src/index.js#L48
|
||||
const aborted = err.name === "AbortError" || err === "aborted";
|
||||
const aborted = err.name === "AbortError" || (err as any as string) === "aborted";
|
||||
if (!aborted && !(err instanceof MatrixError)) {
|
||||
// browser-request just throws normal Error objects,
|
||||
// not `TypeError`s like fetch does. So just assume any
|
||||
@@ -801,13 +941,15 @@ const requestCallback = function(
|
||||
err = new ConnectionError("request failed", err);
|
||||
}
|
||||
}
|
||||
|
||||
let data: T | string = body;
|
||||
|
||||
if (!err) {
|
||||
try {
|
||||
const httpStatus = response.status || response.statusCode; // XMLHttpRequest vs http.IncomingMessage
|
||||
if (httpStatus >= 400) {
|
||||
if (getStatusCode(response) >= 400) {
|
||||
err = parseErrorResponse(response, body);
|
||||
} else if (bodyParser) {
|
||||
body = bodyParser(body);
|
||||
data = bodyParser(body);
|
||||
}
|
||||
} catch (e) {
|
||||
err = new Error(`Error parsing server response: ${e}`);
|
||||
@@ -816,21 +958,26 @@ const requestCallback = function(
|
||||
|
||||
if (err) {
|
||||
defer.reject(err);
|
||||
userDefinedCallback(err);
|
||||
userDefinedCallback?.(err);
|
||||
} else if (onlyData) {
|
||||
defer.resolve(data as T);
|
||||
userDefinedCallback?.(null, data as T);
|
||||
} else {
|
||||
const res = {
|
||||
code: response.status || response.statusCode, // XMLHttpRequest vs http.IncomingMessage
|
||||
const res: IResponse<T> = {
|
||||
code: getStatusCode(response),
|
||||
|
||||
// XXX: why do we bother with this? it doesn't work for
|
||||
// XMLHttpRequest, so clearly we don't use it.
|
||||
headers: response.headers,
|
||||
data: body,
|
||||
headers: (response as IncomingMessage).headers,
|
||||
data: data as T,
|
||||
};
|
||||
defer.resolve(onlyData ? body : res);
|
||||
userDefinedCallback(null, onlyData ? body : res);
|
||||
// XXX: the variations in caller-expected types here are horrible,
|
||||
// typescript doesn't do conditional types based on runtime values
|
||||
defer.resolve(res as any as T);
|
||||
userDefinedCallback?.(null, res as any as T);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to turn an HTTP error response into a Javascript Error.
|
||||
@@ -842,8 +989,8 @@ const requestCallback = function(
|
||||
* @param {String} body raw body of the response
|
||||
* @returns {Error}
|
||||
*/
|
||||
function parseErrorResponse(response, body) {
|
||||
const httpStatus = response.status || response.statusCode; // XMLHttpRequest vs http.IncomingMessage
|
||||
function parseErrorResponse(response: XMLHttpRequest | IncomingMessage, body?: string) {
|
||||
const httpStatus = getStatusCode(response);
|
||||
const contentType = getResponseContentType(response);
|
||||
|
||||
let err;
|
||||
@@ -872,14 +1019,14 @@ function parseErrorResponse(response, body) {
|
||||
* @param {XMLHttpRequest|http.IncomingMessage} response response object
|
||||
* @returns {{type: String, parameters: Object}?} parsed content-type header, or null if not found
|
||||
*/
|
||||
function getResponseContentType(response) {
|
||||
function getResponseContentType(response: XMLHttpRequest | IncomingMessage): ParsedMediaType {
|
||||
let contentType;
|
||||
if (response.getResponseHeader) {
|
||||
if ((response as XMLHttpRequest).getResponseHeader) {
|
||||
// XMLHttpRequest provides getResponseHeader
|
||||
contentType = response.getResponseHeader("Content-Type");
|
||||
} else if (response.headers) {
|
||||
contentType = (response as XMLHttpRequest).getResponseHeader("Content-Type");
|
||||
} else if ((response as IncomingMessage).headers) {
|
||||
// request provides http.IncomingMessage which has a message.headers map
|
||||
contentType = response.headers['content-type'] || null;
|
||||
contentType = (response as IncomingMessage).headers['content-type'] || null;
|
||||
}
|
||||
|
||||
if (!contentType) {
|
||||
@@ -893,6 +1040,12 @@ function getResponseContentType(response) {
|
||||
}
|
||||
}
|
||||
|
||||
interface IErrorJson extends Partial<IUsageLimit> {
|
||||
[key: string]: any; // extensible
|
||||
errcode?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a Matrix error. This is a JavaScript Error with additional
|
||||
* information specific to the standard Matrix error response.
|
||||
@@ -905,8 +1058,11 @@ function getResponseContentType(response) {
|
||||
* @prop {integer} httpStatus The numeric HTTP status code given
|
||||
*/
|
||||
export class MatrixError extends Error {
|
||||
constructor(errorJson) {
|
||||
errorJson = errorJson || {};
|
||||
public readonly errcode: string;
|
||||
public readonly data: IErrorJson;
|
||||
public httpStatus?: number; // set by http-api
|
||||
|
||||
constructor(errorJson: IErrorJson = {}) {
|
||||
super(`MatrixError: ${errorJson.errcode}`);
|
||||
this.errcode = errorJson.errcode;
|
||||
this.name = errorJson.errcode || "Unknown error code";
|
||||
@@ -923,18 +1079,13 @@ export class MatrixError extends Error {
|
||||
* @constructor
|
||||
*/
|
||||
export class ConnectionError extends Error {
|
||||
constructor(message, cause = undefined) {
|
||||
constructor(message: string, private readonly cause: Error = undefined) {
|
||||
super(message + (cause ? `: ${cause.message}` : ""));
|
||||
this._cause = cause;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return "ConnectionError";
|
||||
}
|
||||
|
||||
get cause() {
|
||||
return this._cause;
|
||||
}
|
||||
}
|
||||
|
||||
export class AbortError extends Error {
|
||||
@@ -954,7 +1105,7 @@ export class AbortError extends Error {
|
||||
* @return {any} the result of the network operation
|
||||
* @throws {ConnectionError} If after maxAttempts the callback still throws ConnectionError
|
||||
*/
|
||||
export async function retryNetworkOperation(maxAttempts, callback) {
|
||||
export async function retryNetworkOperation<T>(maxAttempts: number, callback: () => T): Promise<T> {
|
||||
let attempts = 0;
|
||||
let lastConnectionError = null;
|
||||
while (attempts < maxAttempts) {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user