Compare commits
192 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| acd4dcb56e | |||
| c170456cde | |||
| 4e739e4b06 | |||
| df1539040c | |||
| 2a04459bb2 | |||
| 6367bf7c75 | |||
| 8263062fab | |||
| a9543df6db | |||
| aae388be93 | |||
| 26a8439ce4 | |||
| 2c2e8fa1ac | |||
| ddc2fa74b9 | |||
| 93d51b83c3 | |||
| f83eae4a46 | |||
| 87c6d11fca | |||
| e87ac86e48 | |||
| 7782e81101 | |||
| aa70687d9e | |||
| 38d32de06b | |||
| 7720c72b73 | |||
| ff9505073f | |||
| 21ee1c31a7 | |||
| fd01ba1fcf | |||
| fbf53524ed | |||
| 1f2a701ace | |||
| 0b87a573b3 | |||
| 74438716af | |||
| d10b348e74 | |||
| 68e9be47d9 | |||
| bddd03c2fd | |||
| e23ba50dd8 | |||
| 261ab7ae68 | |||
| 21c8c76dc3 | |||
| 266d0f9d05 | |||
| 69d25c1498 | |||
| 3cd2b3925a | |||
| 33e9eb371e | |||
| 07572d1e8d | |||
| dde4f558f3 | |||
| 79d2574ea7 | |||
| 51fb5c4a15 | |||
| 875c6b973b | |||
| 21e1312dd7 | |||
| a722ef3b03 | |||
| 80ba5d29f2 | |||
| a35e6a0f54 | |||
| bdc1958c08 | |||
| 3f2bac71c6 | |||
| 6b9a11b697 | |||
| f17ecba519 | |||
| ce0b014a5a | |||
| ad48d2997e | |||
| 1c1781ce76 | |||
| 5fd001354a | |||
| a18bdad44f | |||
| 600dff62e8 | |||
| db7a402e9b | |||
| 0e53f9052f | |||
| 62e69cacb7 | |||
| 852c88c341 | |||
| 455f52f1f5 | |||
| df6012c58d | |||
| f68a3dde46 | |||
| 25e6b1cac8 | |||
| 4ad20526db | |||
| 18cd017f58 | |||
| 0161664b6c | |||
| 25df31bf96 | |||
| 09438b440e | |||
| 6a5f5b249e | |||
| 3a20114c39 | |||
| f411d50253 | |||
| 00851df25c | |||
| 8822d255b3 | |||
| 4b4ba86167 | |||
| 7ea820f6e1 | |||
| 53d8cf0852 | |||
| 761806c678 | |||
| d6abd639f3 | |||
| 6078bbbe24 | |||
| c1c81df4de | |||
| ee8a4698a9 | |||
| c1956d3f05 | |||
| 56316dc5d9 | |||
| b5c74b5666 | |||
| dc946dffbc | |||
| 937baadb9b | |||
| 8d0c03b4f0 | |||
| a3fba73044 | |||
| 116cf31199 | |||
| cdb78e4c75 | |||
| 0bb9c56e94 | |||
| 821f1c876b | |||
| e9b95f8567 | |||
| 103d811441 | |||
| bb4f5a3fa1 | |||
| f5cbdeac8f | |||
| 56062e8e4e | |||
| 03c85d48e5 | |||
| dd8f0fbdcb | |||
| fac61a76e9 | |||
| 3b09ab3ca1 | |||
| 16bfe79305 | |||
| d668f97c98 | |||
| 18bd10b03d | |||
| 6b1d089caf | |||
| af93401385 | |||
| fa5add3d99 | |||
| fb971580e0 | |||
| dcaea98e33 | |||
| b95079d1c5 | |||
| e59f36cdc0 | |||
| 0f2f041d8b | |||
| 33d2837ec3 | |||
| 7cede221de | |||
| fe47435fc7 | |||
| 491226a916 | |||
| deb7433453 | |||
| 14973a35c2 | |||
| b5779f8654 | |||
| f7d1984257 | |||
| 3fba683090 | |||
| 430da8ac09 | |||
| dcd9b5c382 | |||
| 348c293962 | |||
| 34309da10c | |||
| 6db973f430 | |||
| 9f27bafa62 | |||
| b05136146a | |||
| 13d3be637b | |||
| 44de7fad6f | |||
| b99243406b | |||
| 0e9ec811b0 | |||
| 6979177fb2 | |||
| 3aa8bfa6ca | |||
| 17b356b08e | |||
| f72ae490a8 | |||
| b0c3d0d2e3 | |||
| 9dc344999e | |||
| 663c096400 | |||
| 420b4d119d | |||
| 58b752c63b | |||
| 4740232fa4 | |||
| 0cc9994b8b | |||
| 1e78628b23 | |||
| 00f5ddc93c | |||
| f79f2105fd | |||
| 828c51467f | |||
| 963e271bce | |||
| 0ab41215f0 | |||
| d153c4da07 | |||
| 91aa783c3d | |||
| a54845bf76 | |||
| 8a56a5f1ed | |||
| f585c80491 | |||
| c495b12cef | |||
| 1d6f7f862f | |||
| 0a82c84006 | |||
| a614a02b90 | |||
| 0945e2c5c6 | |||
| b1b49413d0 | |||
| 01af303d63 | |||
| 751060305c | |||
| 389fcfaf3d | |||
| 3eb0c534a5 | |||
| 8a2f84b678 | |||
| dd00735409 | |||
| 6ba7e85e24 | |||
| 941d93c2f4 | |||
| c73e9cbc7c | |||
| ae89e4bf21 | |||
| d1e64d0cfb | |||
| 8f9f9590d9 | |||
| ed68093310 | |||
| 23655e748d | |||
| 98624871bd | |||
| 7b154c0834 | |||
| d9f056242f | |||
| a4268d288e | |||
| f90c91dded | |||
| 5c8890c3c1 | |||
| cd3c6809a9 | |||
| eab074a27b | |||
| e2a3e3816f | |||
| 01dd57adab | |||
| 08e674b695 | |||
| 9f70970e61 | |||
| e9ffd5a125 | |||
| 20f4469361 | |||
| 42f181cc7b | |||
| b3d2d39b60 | |||
| e323d917a4 |
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"presets": ["es2015"],
|
||||
"plugins": [
|
||||
"transform-class-properties",
|
||||
|
||||
// this transforms async functions into generator functions, which
|
||||
// are then made to use the regenerator module by babel's
|
||||
// transform-regnerator plugin (which is enabled by es2015).
|
||||
|
||||
@@ -22,3 +22,13 @@ steps:
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
image: "node:10"
|
||||
|
||||
- wait
|
||||
|
||||
- label: "🐴 Trigger matrix-react-sdk"
|
||||
trigger: "matrix-react-sdk"
|
||||
branches: "develop"
|
||||
build:
|
||||
branch: "develop"
|
||||
message: "[js-sdk] ${BUILDKITE_MESSAGE}"
|
||||
async: true
|
||||
|
||||
@@ -14,6 +14,9 @@ module.exports = {
|
||||
es6: true,
|
||||
},
|
||||
extends: ["eslint:recommended", "google"],
|
||||
plugins: [
|
||||
"babel",
|
||||
],
|
||||
rules: {
|
||||
// rules we've always adhered to or now do
|
||||
"max-len": ["error", {
|
||||
@@ -73,5 +76,10 @@ module.exports = {
|
||||
"asyncArrow": "always",
|
||||
}],
|
||||
"arrow-parens": "off",
|
||||
|
||||
// eslint's built in no-invalid-this rule breaks with class properties
|
||||
"no-invalid-this": "off",
|
||||
// so we replace it with a version that is class property aware
|
||||
"babel/no-invalid-this": "error",
|
||||
}
|
||||
}
|
||||
|
||||
+126
-1
@@ -1,3 +1,128 @@
|
||||
Changes in [2.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.0.0) (2019-05-31)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.2.0...v2.0.0)
|
||||
|
||||
BREAKING CHANGES
|
||||
----------------
|
||||
|
||||
* This package now publishes in ES6 / ES2015 syntax to NPM
|
||||
* Saves access_token and user_id after login for all login types
|
||||
[\#932](https://github.com/matrix-org/matrix-js-sdk/pull/932)
|
||||
* Fix recovery key encoding for base-x 3.0.5
|
||||
[\#931](https://github.com/matrix-org/matrix-js-sdk/pull/931)
|
||||
|
||||
Changes in [1.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.2.0) (2019-05-29)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.2.0-rc.1...v1.2.0)
|
||||
|
||||
|
||||
Changes in [1.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.2.0-rc.1) (2019-05-23)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.1.0...v1.2.0-rc.1)
|
||||
|
||||
* interactive-auth now handles requesting email tokens
|
||||
[\#926](https://github.com/matrix-org/matrix-js-sdk/pull/926)
|
||||
* allow access to unreplaced message content
|
||||
[\#923](https://github.com/matrix-org/matrix-js-sdk/pull/923)
|
||||
* Add method to retrieve replacing event
|
||||
[\#922](https://github.com/matrix-org/matrix-js-sdk/pull/922)
|
||||
* More logging when signature verification fails
|
||||
[\#921](https://github.com/matrix-org/matrix-js-sdk/pull/921)
|
||||
* Local echo for m.replace relations
|
||||
[\#920](https://github.com/matrix-org/matrix-js-sdk/pull/920)
|
||||
* Track relations as pending and remove when cancelled
|
||||
[\#919](https://github.com/matrix-org/matrix-js-sdk/pull/919)
|
||||
* Add stringify helper to summarise events when debugging
|
||||
[\#916](https://github.com/matrix-org/matrix-js-sdk/pull/916)
|
||||
* Message editing: filter out replacements for senders that are not the
|
||||
original sender
|
||||
[\#918](https://github.com/matrix-org/matrix-js-sdk/pull/918)
|
||||
* Wait until decrypt before aggregating
|
||||
[\#917](https://github.com/matrix-org/matrix-js-sdk/pull/917)
|
||||
* Message editing: mark original event as replaced instead of replacing the
|
||||
event object
|
||||
[\#914](https://github.com/matrix-org/matrix-js-sdk/pull/914)
|
||||
* Support for replacing message through m.replace relationship.
|
||||
[\#913](https://github.com/matrix-org/matrix-js-sdk/pull/913)
|
||||
* Use a short timeout for .well-known requests
|
||||
[\#912](https://github.com/matrix-org/matrix-js-sdk/pull/912)
|
||||
* Redaction and change events for relations
|
||||
[\#911](https://github.com/matrix-org/matrix-js-sdk/pull/911)
|
||||
* Add basic read path for relations
|
||||
[\#910](https://github.com/matrix-org/matrix-js-sdk/pull/910)
|
||||
* Add a concept of default push rules, using it for tombstone notifications
|
||||
[\#860](https://github.com/matrix-org/matrix-js-sdk/pull/860)
|
||||
* yarn upgrade
|
||||
[\#907](https://github.com/matrix-org/matrix-js-sdk/pull/907)
|
||||
|
||||
Changes in [1.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.1.0) (2019-05-07)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.1.0-rc.1...v1.1.0)
|
||||
|
||||
* No Changes since rc.1
|
||||
|
||||
Changes in [1.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.1.0-rc.1) (2019-04-30)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.4...v1.1.0-rc.1)
|
||||
|
||||
* use the release version of olm 3.1.0
|
||||
[\#903](https://github.com/matrix-org/matrix-js-sdk/pull/903)
|
||||
* Use new Olm repo link in README
|
||||
[\#901](https://github.com/matrix-org/matrix-js-sdk/pull/901)
|
||||
* Support being fed a .well-known config object for validation
|
||||
[\#897](https://github.com/matrix-org/matrix-js-sdk/pull/897)
|
||||
* emit self-membership event at end of handling sync update
|
||||
[\#900](https://github.com/matrix-org/matrix-js-sdk/pull/900)
|
||||
* Use packages.matrix.org for Olm
|
||||
[\#898](https://github.com/matrix-org/matrix-js-sdk/pull/898)
|
||||
* Fix tests on develop
|
||||
[\#899](https://github.com/matrix-org/matrix-js-sdk/pull/899)
|
||||
* Stop syncing when the token is invalid
|
||||
[\#895](https://github.com/matrix-org/matrix-js-sdk/pull/895)
|
||||
* change event redact, POST request to PUT request
|
||||
[\#887](https://github.com/matrix-org/matrix-js-sdk/pull/887)
|
||||
* Expose better autodiscovery error messages
|
||||
[\#894](https://github.com/matrix-org/matrix-js-sdk/pull/894)
|
||||
* Explicitly guard store usage during sync startup
|
||||
[\#892](https://github.com/matrix-org/matrix-js-sdk/pull/892)
|
||||
* Flag v3 rooms as safe
|
||||
[\#893](https://github.com/matrix-org/matrix-js-sdk/pull/893)
|
||||
* Cache failed capabilities lookups for shorter amounts of time
|
||||
[\#890](https://github.com/matrix-org/matrix-js-sdk/pull/890)
|
||||
* Fix highlight notifications for unencrypted rooms
|
||||
[\#891](https://github.com/matrix-org/matrix-js-sdk/pull/891)
|
||||
* Document checking crypto state before using `hasUnverifiedDevices`
|
||||
[\#889](https://github.com/matrix-org/matrix-js-sdk/pull/889)
|
||||
* Add logging to sync startup path
|
||||
[\#888](https://github.com/matrix-org/matrix-js-sdk/pull/888)
|
||||
* Track e2e highlights better, particularly in 'Mentions Only' rooms
|
||||
[\#886](https://github.com/matrix-org/matrix-js-sdk/pull/886)
|
||||
* support both the incorrect and correct MAC methods
|
||||
[\#882](https://github.com/matrix-org/matrix-js-sdk/pull/882)
|
||||
* Refuse to set forwards pagination token on live timeline
|
||||
[\#885](https://github.com/matrix-org/matrix-js-sdk/pull/885)
|
||||
* Degrade `IndexedDBStore` back to memory only on failure
|
||||
[\#884](https://github.com/matrix-org/matrix-js-sdk/pull/884)
|
||||
* Refuse to link live timelines into the forwards/backwards position when
|
||||
either is invalid
|
||||
[\#877](https://github.com/matrix-org/matrix-js-sdk/pull/877)
|
||||
* Key backup logging improvements
|
||||
[\#883](https://github.com/matrix-org/matrix-js-sdk/pull/883)
|
||||
* Don't assume aborts are always from txn.abort()
|
||||
[\#880](https://github.com/matrix-org/matrix-js-sdk/pull/880)
|
||||
* Add a bunch of logging
|
||||
[\#878](https://github.com/matrix-org/matrix-js-sdk/pull/878)
|
||||
* Refuse splicing the live timeline into a broken position
|
||||
[\#873](https://github.com/matrix-org/matrix-js-sdk/pull/873)
|
||||
* Add existence check to local storage based crypto store
|
||||
[\#872](https://github.com/matrix-org/matrix-js-sdk/pull/872)
|
||||
|
||||
Changes in [1.0.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.4) (2019-04-08)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.3...v1.0.4)
|
||||
|
||||
* Hotfix: more logging and potential fixes for timeline corruption issue, see ticket https://github.com/vector-im/riot-web/issues/8593.
|
||||
|
||||
Changes in [1.0.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.3) (2019-04-01)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.3-rc.1...v1.0.3)
|
||||
@@ -211,7 +336,7 @@ Changes in [0.14.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/ta
|
||||
|
||||
BREAKING CHANGE
|
||||
----------------
|
||||
|
||||
|
||||
* js-sdk now uses Olm 3.0. Apps using Olm must update to 3.0 to
|
||||
continue using Olm with the js-sdk. The js-sdk will call Olm's
|
||||
init() method when the client is started.
|
||||
|
||||
@@ -297,9 +297,9 @@ Then visit ``http://localhost:8005`` to see the API docs.
|
||||
End-to-end encryption support
|
||||
=============================
|
||||
|
||||
The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
|
||||
[libolm](http://matrix.org/git/olm). It is left up to the application to make
|
||||
libolm available, via the ``Olm`` global.
|
||||
The SDK supports end-to-end encryption via the and Megolm protocols, using
|
||||
[libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the
|
||||
application to make libolm available, via the ``Olm`` global.
|
||||
|
||||
It is also necessry to call ``matrixClient.initCrypto()`` after creating a new
|
||||
``MatrixClient`` (but **before** calling ``matrixClient.startClient()``) to
|
||||
@@ -318,18 +318,18 @@ specification.
|
||||
|
||||
To provide the Olm library in a browser application:
|
||||
|
||||
* download the transpiled libolm (from https://matrix.org/packages/npm/olm/).
|
||||
* download the transpiled libolm (from https://packages.matrix.org/npm/olm/).
|
||||
* load ``olm.js`` as a ``<script>`` *before* ``browser-matrix.js``.
|
||||
|
||||
To provide the Olm library in a node.js application:
|
||||
|
||||
* ``yarn add https://matrix.org/packages/npm/olm/olm-3.0.0.tgz``
|
||||
* ``yarn add https://packages.matrix.org/npm/olm/olm-3.0.0.tgz``
|
||||
(replace the URL with the latest version you want to use from
|
||||
https://matrix.org/packages/npm/olm/)
|
||||
https://packages.matrix.org/npm/olm/)
|
||||
* ``global.Olm = require('olm');`` *before* loading ``matrix-js-sdk``.
|
||||
|
||||
If you want to package Olm as dependency for your node.js application, you can
|
||||
use ``yarn add https://matrix.org/packages/npm/olm/olm-3.0.0.tgz``. If your
|
||||
use ``yarn add https://packages.matrix.org/npm/olm/olm-3.0.0.tgz``. If your
|
||||
application also works without e2e crypto enabled, add ``--optional`` to mark it
|
||||
as an optional dependency.
|
||||
|
||||
|
||||
+7
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "1.0.3",
|
||||
"version": "2.0.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -9,12 +9,12 @@
|
||||
"test:watch": "mocha --watch --compilers js:babel-core/register --recursive spec --colors",
|
||||
"test": "yarn test:build && yarn test:run",
|
||||
"check": "yarn test:build && _mocha --recursive specbuild --colors",
|
||||
"gendoc": "babel --no-babelrc -d .jsdocbuild src && jsdoc -r .jsdocbuild -P package.json -R README.md -d .jsdoc",
|
||||
"gendoc": "babel --no-babelrc --plugins transform-class-properties -d .jsdocbuild src && jsdoc -r .jsdocbuild -P package.json -R README.md -d .jsdoc",
|
||||
"start": "yarn start:init && yarn start:watch",
|
||||
"start:watch": "babel -s -w --skip-initial-build -d lib src",
|
||||
"start:init": "babel -s -d lib src",
|
||||
"clean": "rimraf lib dist",
|
||||
"build": "babel -s -d lib src && rimraf dist && mkdir dist && browserify -d browser-index.js | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js && uglifyjs -c -m -o dist/browser-matrix.min.js --source-map dist/browser-matrix.min.js.map --in-source-map dist/browser-matrix.js.map dist/browser-matrix.js",
|
||||
"build": "babel -s -d lib src && rimraf dist && mkdir dist && browserify -d browser-index.js | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js && terser -c -m -o dist/browser-matrix.min.js --source-map \"content='dist/browser-matrix.js.map'\" dist/browser-matrix.js",
|
||||
"dist": "yarn build",
|
||||
"watch": "watchify -d browser-index.js -o 'exorcist dist/browser-matrix.js.map > dist/browser-matrix.js' -v",
|
||||
"lint": "eslint --max-warnings 101 src spec",
|
||||
@@ -54,7 +54,6 @@
|
||||
"dependencies": {
|
||||
"another-json": "^0.2.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"base-x": "3.0.4",
|
||||
"bluebird": "^3.5.0",
|
||||
"browser-request": "^0.3.3",
|
||||
"bs58": "^4.0.1",
|
||||
@@ -68,12 +67,14 @@
|
||||
"babel-cli": "^6.18.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-plugin-transform-async-to-bluebird": "^1.1.1",
|
||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-es2015": "^6.18.0",
|
||||
"browserify": "^16.2.3",
|
||||
"browserify-shim": "^3.8.13",
|
||||
"eslint": "^5.12.0",
|
||||
"eslint-config-google": "^0.7.1",
|
||||
"eslint-plugin-babel": "^5.3.0",
|
||||
"exorcist": "^0.4.0",
|
||||
"expect": "^1.20.2",
|
||||
"istanbul": "^0.4.5",
|
||||
@@ -82,11 +83,11 @@
|
||||
"matrix-mock-request": "^1.2.3",
|
||||
"mocha": "^5.2.0",
|
||||
"mocha-jenkins-reporter": "^0.4.0",
|
||||
"olm": "https://matrix.org/packages/npm/olm/olm-3.1.0-pre1.tgz",
|
||||
"olm": "https://packages.matrix.org/npm/olm/olm-3.1.0.tgz",
|
||||
"rimraf": "^2.5.4",
|
||||
"source-map-support": "^0.4.11",
|
||||
"sourceify": "^0.1.0",
|
||||
"uglify-js": "^2.8.26",
|
||||
"terser": "^4.0.0",
|
||||
"watchify": "^3.11.1"
|
||||
},
|
||||
"browserify": {
|
||||
|
||||
@@ -87,7 +87,7 @@ describe("DeviceList management:", function() {
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
// we create our own sessionStoreBackend so that we can use it for
|
||||
// another TestClient.
|
||||
|
||||
@@ -405,7 +405,7 @@ describe("MatrixClient crypto", function() {
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
aliTestClient = new TestClient(aliUserId, aliDeviceId, aliAccessToken);
|
||||
await aliTestClient.client.initCrypto();
|
||||
|
||||
@@ -15,7 +15,7 @@ describe("MatrixClient events", function() {
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
client = sdk.createClient({
|
||||
@@ -157,7 +157,7 @@ describe("MatrixClient events", function() {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
|
||||
expect(event.event).toMatch(SYNC_DATA.presence.events[0]);
|
||||
expect(user.presence).toEqual(
|
||||
SYNC_DATA.presence.events[0].content.presence,
|
||||
);
|
||||
|
||||
@@ -102,7 +102,7 @@ describe("getEventTimeline support", function() {
|
||||
let client;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
});
|
||||
@@ -227,7 +227,7 @@ describe("MatrixClient event timelines", function() {
|
||||
let httpBackend = null;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ describe("MatrixClient", function() {
|
||||
const accessToken = "aseukfgwef";
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
store = new MemoryStore();
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ describe("MatrixClient opts", function() {
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ describe("MatrixClient retrying", function() {
|
||||
let room;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
scheduler = new sdk.MatrixScheduler();
|
||||
|
||||
@@ -104,7 +104,7 @@ describe("MatrixClient room timelines", function() {
|
||||
}
|
||||
|
||||
beforeEach(function(done) {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
client = sdk.createClient({
|
||||
|
||||
@@ -23,7 +23,7 @@ describe("MatrixClient syncing", function() {
|
||||
const roomTwo = "!bar:localhost";
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
client = sdk.createClient({
|
||||
|
||||
@@ -282,7 +282,7 @@ describe("megolm", function() {
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
aliceTestClient = new TestClient(
|
||||
"@alice:localhost", "xzcvb", "akjgkrgjs",
|
||||
|
||||
@@ -30,7 +30,7 @@ describe("AutoDiscovery", function() {
|
||||
let httpBackend = null;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new MockHttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
});
|
||||
@@ -94,7 +94,7 @@ describe("AutoDiscovery", function() {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: "Invalid homeserver discovery response",
|
||||
error: AutoDiscovery.ERROR_INVALID,
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
@@ -117,7 +117,7 @@ describe("AutoDiscovery", function() {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: "Invalid homeserver discovery response",
|
||||
error: AutoDiscovery.ERROR_INVALID,
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
@@ -140,7 +140,7 @@ describe("AutoDiscovery", function() {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: "Invalid homeserver discovery response",
|
||||
error: AutoDiscovery.ERROR_INVALID,
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
@@ -163,7 +163,7 @@ describe("AutoDiscovery", function() {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: "Invalid homeserver discovery response",
|
||||
error: AutoDiscovery.ERROR_INVALID,
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
@@ -191,7 +191,7 @@ describe("AutoDiscovery", function() {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: "Invalid homeserver discovery response",
|
||||
error: AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
@@ -217,7 +217,7 @@ describe("AutoDiscovery", function() {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: "Invalid homeserver discovery response",
|
||||
error: AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
@@ -245,7 +245,7 @@ describe("AutoDiscovery", function() {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: "Invalid homeserver discovery response",
|
||||
error: AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
@@ -274,7 +274,7 @@ describe("AutoDiscovery", function() {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: "Invalid homeserver discovery response",
|
||||
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
@@ -303,7 +303,7 @@ describe("AutoDiscovery", function() {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: "Invalid homeserver discovery response",
|
||||
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
@@ -334,7 +334,7 @@ describe("AutoDiscovery", function() {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: "Invalid homeserver discovery response",
|
||||
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
@@ -439,14 +439,14 @@ describe("AutoDiscovery", function() {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: "Invalid identity server discovery response",
|
||||
error: AutoDiscovery.ERROR_INVALID_IS,
|
||||
|
||||
// We still expect the base_url to be here for debugging purposes.
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "FAIL_ERROR",
|
||||
error: "Invalid identity server discovery response",
|
||||
error: AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
@@ -479,14 +479,14 @@ describe("AutoDiscovery", function() {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: "Invalid identity server discovery response",
|
||||
error: AutoDiscovery.ERROR_INVALID_IS,
|
||||
|
||||
// We still expect the base_url to be here for debugging purposes.
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "FAIL_ERROR",
|
||||
error: "Invalid identity server discovery response",
|
||||
error: AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
@@ -520,14 +520,14 @@ describe("AutoDiscovery", function() {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: "Invalid identity server discovery response",
|
||||
error: AutoDiscovery.ERROR_INVALID_IS,
|
||||
|
||||
// We still expect the base_url to be here for debugging purposes.
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "FAIL_ERROR",
|
||||
error: "Invalid identity server discovery response",
|
||||
error: AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
@@ -561,14 +561,14 @@ describe("AutoDiscovery", function() {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: "Invalid identity server discovery response",
|
||||
error: AutoDiscovery.ERROR_INVALID_IS,
|
||||
|
||||
// We still expect the base_url to be here for debugging purposes
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "FAIL_ERROR",
|
||||
error: "Invalid identity server discovery response",
|
||||
error: AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ describe("ContentRepo", function() {
|
||||
const baseUrl = "https://my.home.server";
|
||||
|
||||
beforeEach(function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
});
|
||||
|
||||
describe("getHttpUriForMxc", function() {
|
||||
|
||||
@@ -59,7 +59,7 @@ describe('DeviceList', function() {
|
||||
let deviceLists = [];
|
||||
|
||||
beforeEach(function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
deviceLists = [];
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ describe("MegolmDecryption", function() {
|
||||
let mockBaseApis;
|
||||
|
||||
beforeEach(async function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
await Olm.init();
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ describe("OlmDecryption", function() {
|
||||
let bobOlmDevice;
|
||||
|
||||
beforeEach(async function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
await global.Olm.init();
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ describe("MegolmBackup", function() {
|
||||
let megolmDecryption;
|
||||
beforeEach(async function() {
|
||||
await Olm.init();
|
||||
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
mockCrypto = testUtils.mock(Crypto, 'Crypto');
|
||||
mockCrypto.backupKey = new Olm.PkEncryption();
|
||||
|
||||
@@ -58,105 +58,170 @@ describe("SAS verification", function() {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should verify a key", async function() {
|
||||
const [alice, bob] = await makeTestClients(
|
||||
[
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{userId: "@bob:example.com", deviceId: "Dynabook"},
|
||||
],
|
||||
{
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
},
|
||||
);
|
||||
|
||||
alice.setDeviceVerified = expect.createSpy();
|
||||
alice.getDeviceEd25519Key = () => {
|
||||
return "alice+base64+ed25519+key";
|
||||
};
|
||||
alice.getStoredDevice = () => {
|
||||
return DeviceInfo.fromStorage(
|
||||
{
|
||||
keys: {
|
||||
"ed25519:Dynabook": "bob+base64+ed25519+key",
|
||||
},
|
||||
},
|
||||
"Dynabook",
|
||||
);
|
||||
};
|
||||
alice.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
bob.setDeviceVerified = expect.createSpy();
|
||||
bob.getStoredDevice = () => {
|
||||
return DeviceInfo.fromStorage(
|
||||
{
|
||||
keys: {
|
||||
"ed25519:Osborne2": "alice+base64+ed25519+key",
|
||||
},
|
||||
},
|
||||
"Osborne2",
|
||||
);
|
||||
};
|
||||
bob.getDeviceEd25519Key = () => {
|
||||
return "bob+base64+ed25519+key";
|
||||
};
|
||||
bob.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
describe("verification", function() {
|
||||
let alice;
|
||||
let bob;
|
||||
let aliceSasEvent;
|
||||
let bobSasEvent;
|
||||
let aliceVerifier;
|
||||
let bobPromise;
|
||||
|
||||
const bobPromise = new Promise((resolve, reject) => {
|
||||
bob.on("crypto.verification.start", (verifier) => {
|
||||
verifier.on("show_sas", (e) => {
|
||||
if (!e.sas.emoji || !e.sas.decimal) {
|
||||
e.cancel();
|
||||
} else if (!aliceSasEvent) {
|
||||
bobSasEvent = e;
|
||||
} else {
|
||||
try {
|
||||
expect(e.sas).toEqual(aliceSasEvent.sas);
|
||||
e.confirm();
|
||||
aliceSasEvent.confirm();
|
||||
} catch (error) {
|
||||
e.mismatch();
|
||||
aliceSasEvent.mismatch();
|
||||
beforeEach(async function() {
|
||||
[alice, bob] = await makeTestClients(
|
||||
[
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{userId: "@bob:example.com", deviceId: "Dynabook"},
|
||||
],
|
||||
{
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
},
|
||||
);
|
||||
|
||||
alice.setDeviceVerified = expect.createSpy();
|
||||
alice.getDeviceEd25519Key = () => {
|
||||
return "alice+base64+ed25519+key";
|
||||
};
|
||||
alice.getStoredDevice = () => {
|
||||
return DeviceInfo.fromStorage(
|
||||
{
|
||||
keys: {
|
||||
"ed25519:Dynabook": "bob+base64+ed25519+key",
|
||||
},
|
||||
},
|
||||
"Dynabook",
|
||||
);
|
||||
};
|
||||
alice.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
bob.setDeviceVerified = expect.createSpy();
|
||||
bob.getStoredDevice = () => {
|
||||
return DeviceInfo.fromStorage(
|
||||
{
|
||||
keys: {
|
||||
"ed25519:Osborne2": "alice+base64+ed25519+key",
|
||||
},
|
||||
},
|
||||
"Osborne2",
|
||||
);
|
||||
};
|
||||
bob.getDeviceEd25519Key = () => {
|
||||
return "bob+base64+ed25519+key";
|
||||
};
|
||||
bob.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
aliceSasEvent = null;
|
||||
bobSasEvent = null;
|
||||
|
||||
bobPromise = new Promise((resolve, reject) => {
|
||||
bob.on("crypto.verification.start", (verifier) => {
|
||||
verifier.on("show_sas", (e) => {
|
||||
if (!e.sas.emoji || !e.sas.decimal) {
|
||||
e.cancel();
|
||||
} else if (!aliceSasEvent) {
|
||||
bobSasEvent = e;
|
||||
} else {
|
||||
try {
|
||||
expect(e.sas).toEqual(aliceSasEvent.sas);
|
||||
e.confirm();
|
||||
aliceSasEvent.confirm();
|
||||
} catch (error) {
|
||||
e.mismatch();
|
||||
aliceSasEvent.mismatch();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
resolve(verifier);
|
||||
});
|
||||
resolve(verifier);
|
||||
});
|
||||
|
||||
aliceVerifier = alice.beginKeyVerification(
|
||||
verificationMethods.SAS, bob.getUserId(), bob.deviceId,
|
||||
);
|
||||
aliceVerifier.on("show_sas", (e) => {
|
||||
if (!e.sas.emoji || !e.sas.decimal) {
|
||||
e.cancel();
|
||||
} else if (!bobSasEvent) {
|
||||
aliceSasEvent = e;
|
||||
} else {
|
||||
try {
|
||||
expect(e.sas).toEqual(bobSasEvent.sas);
|
||||
e.confirm();
|
||||
bobSasEvent.confirm();
|
||||
} catch (error) {
|
||||
e.mismatch();
|
||||
bobSasEvent.mismatch();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const aliceVerifier = alice.beginKeyVerification(
|
||||
verificationMethods.SAS, bob.getUserId(), bob.deviceId,
|
||||
);
|
||||
aliceVerifier.on("show_sas", (e) => {
|
||||
if (!e.sas.emoji || !e.sas.decimal) {
|
||||
e.cancel();
|
||||
} else if (!bobSasEvent) {
|
||||
aliceSasEvent = e;
|
||||
} else {
|
||||
try {
|
||||
expect(e.sas).toEqual(bobSasEvent.sas);
|
||||
e.confirm();
|
||||
bobSasEvent.confirm();
|
||||
} catch (error) {
|
||||
e.mismatch();
|
||||
bobSasEvent.mismatch();
|
||||
it("should verify a key", async function() {
|
||||
let macMethod;
|
||||
const origSendToDevice = alice.sendToDevice;
|
||||
bob.sendToDevice = function(type, map) {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.getUserId()][alice.deviceId]
|
||||
.message_authentication_code;
|
||||
}
|
||||
}
|
||||
return origSendToDevice.call(this, type, map);
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
aliceVerifier.verify(),
|
||||
bobPromise.then((verifier) => verifier.verify()),
|
||||
]);
|
||||
|
||||
// make sure that it uses the preferred method
|
||||
expect(macMethod).toBe("hkdf-hmac-sha256");
|
||||
|
||||
// make sure Alice and Bob verified each other
|
||||
expect(alice.setDeviceVerified)
|
||||
.toHaveBeenCalledWith(bob.getUserId(), bob.deviceId);
|
||||
expect(bob.setDeviceVerified)
|
||||
.toHaveBeenCalledWith(alice.getUserId(), alice.deviceId);
|
||||
});
|
||||
|
||||
it("should be able to verify using the old MAC", async function() {
|
||||
// pretend that Alice can only understand the old (incorrect) MAC,
|
||||
// and make sure that she can still verify with Bob
|
||||
let macMethod;
|
||||
const origSendToDevice = alice.sendToDevice;
|
||||
alice.sendToDevice = function(type, map) {
|
||||
if (type === "m.key.verification.start") {
|
||||
// Note: this modifies not only the message that Bob
|
||||
// receives, but also the copy of the message that Alice
|
||||
// has, since it is the same object. If this does not
|
||||
// happen, the verification will fail due to a hash
|
||||
// commitment mismatch.
|
||||
map[bob.getUserId()][bob.deviceId]
|
||||
.message_authentication_codes = ['hmac-sha256'];
|
||||
}
|
||||
return origSendToDevice.call(this, type, map);
|
||||
};
|
||||
bob.sendToDevice = function(type, map) {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.getUserId()][alice.deviceId]
|
||||
.message_authentication_code;
|
||||
}
|
||||
return origSendToDevice.call(this, type, map);
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
aliceVerifier.verify(),
|
||||
bobPromise.then((verifier) => verifier.verify()),
|
||||
]);
|
||||
|
||||
expect(macMethod).toBe("hmac-sha256");
|
||||
|
||||
expect(alice.setDeviceVerified)
|
||||
.toHaveBeenCalledWith(bob.getUserId(), bob.deviceId);
|
||||
expect(bob.setDeviceVerified)
|
||||
.toHaveBeenCalledWith(alice.getUserId(), alice.deviceId);
|
||||
});
|
||||
await Promise.all([
|
||||
aliceVerifier.verify(),
|
||||
bobPromise.then((verifier) => verifier.verify()),
|
||||
]);
|
||||
expect(alice.setDeviceVerified)
|
||||
.toHaveBeenCalledWith(bob.getUserId(), bob.deviceId);
|
||||
expect(bob.setDeviceVerified)
|
||||
.toHaveBeenCalledWith(alice.getUserId(), alice.deviceId);
|
||||
});
|
||||
|
||||
it("should send a cancellation message on error", async function() {
|
||||
|
||||
@@ -29,7 +29,7 @@ export async function makeTestClients(userInfos, options) {
|
||||
for (const [deviceId, msg] of Object.entries(devMap)) {
|
||||
if (deviceId in clientMap[userId]) {
|
||||
const event = new MatrixEvent({
|
||||
sender: this.getUserId(), // eslint-disable-line no-invalid-this
|
||||
sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
|
||||
type: type,
|
||||
content: msg,
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ describe("EventTimeline", function() {
|
||||
let timeline;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
// XXX: this is a horrid hack; should use sinon or something instead to mock
|
||||
const timelineSet = { room: { roomId: roomId }};
|
||||
|
||||
@@ -24,7 +24,7 @@ import Promise from 'bluebird';
|
||||
|
||||
describe("MatrixEvent", () => {
|
||||
beforeEach(function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
});
|
||||
|
||||
describe(".attemptDecryption", () => {
|
||||
|
||||
@@ -12,7 +12,7 @@ describe("Filter", function() {
|
||||
let filter;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
filter = new Filter(userId);
|
||||
});
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ class FakeClient {
|
||||
|
||||
describe("InteractiveAuth", function() {
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
});
|
||||
|
||||
it("should start an auth stage and complete it", function(done) {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import expect from 'expect';
|
||||
import TestClient from '../TestClient';
|
||||
|
||||
describe('Login request', function() {
|
||||
let client;
|
||||
|
||||
beforeEach(function() {
|
||||
client = new TestClient();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
client.stop();
|
||||
});
|
||||
|
||||
it('should store "access_token" and "user_id" if in response', async function() {
|
||||
const response = { user_id: 1, access_token: Date.now().toString(16) };
|
||||
|
||||
client.httpBackend.when('POST', '/login').respond(200, response);
|
||||
client.httpBackend.flush('/login', 1, 100);
|
||||
await client.client.login('m.login.any', { user: 'test', password: '12312za' });
|
||||
|
||||
expect(client.client.getAccessToken()).toBe(response.access_token);
|
||||
expect(client.client.getUserId()).toBe(response.user_id);
|
||||
});
|
||||
});
|
||||
@@ -124,7 +124,7 @@ describe("MatrixClient", function() {
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
clock = lolex.install();
|
||||
scheduler = [
|
||||
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
|
||||
|
||||
@@ -15,7 +15,7 @@ describe("realtime-callbacks", function() {
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
clock = lolex.install();
|
||||
const fakeDate = clock.Date;
|
||||
callbacks.setNow(fakeDate.now.bind(fakeDate));
|
||||
@@ -56,8 +56,8 @@ describe("realtime-callbacks", function() {
|
||||
it("should set 'this' to the global object", function() {
|
||||
let passed = false;
|
||||
const callback = function() {
|
||||
expect(this).toBe(global); // eslint-disable-line no-invalid-this
|
||||
expect(this.console).toBeTruthy(); // eslint-disable-line no-invalid-this
|
||||
expect(this).toBe(global); // eslint-disable-line babel/no-invalid-this
|
||||
expect(this.console).toBeTruthy(); // eslint-disable-line babel/no-invalid-this
|
||||
passed = true;
|
||||
};
|
||||
callbacks.setTimeout(callback);
|
||||
|
||||
@@ -14,7 +14,7 @@ describe("RoomMember", function() {
|
||||
let member;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
member = new RoomMember(roomId, userA);
|
||||
});
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ describe("RoomState", function() {
|
||||
let state;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
state = new RoomState(roomId);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({ // userA joined
|
||||
|
||||
@@ -19,7 +19,7 @@ describe("Room", function() {
|
||||
let room;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
room = new Room(roomId);
|
||||
// mock RoomStates
|
||||
room.oldState = room.getLiveTimeline()._startState =
|
||||
@@ -1318,6 +1318,9 @@ describe("Room", function() {
|
||||
// events should already be MatrixEvents
|
||||
return function(event) {return event;};
|
||||
},
|
||||
isCryptoEnabled() {
|
||||
return true;
|
||||
},
|
||||
isRoomEncrypted: function() {
|
||||
return false;
|
||||
},
|
||||
|
||||
@@ -26,7 +26,7 @@ describe("MatrixScheduler", function() {
|
||||
});
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
clock = lolex.install();
|
||||
scheduler = new MatrixScheduler(function(ev, attempts, err) {
|
||||
if (retryFn) {
|
||||
|
||||
@@ -26,7 +26,7 @@ describe("SyncAccumulator", function() {
|
||||
let sa;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
sa = new SyncAccumulator({
|
||||
maxTimelineEntries: 10,
|
||||
});
|
||||
|
||||
@@ -68,7 +68,7 @@ function createLinkedTimelines() {
|
||||
|
||||
describe("TimelineIndex", function() {
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
});
|
||||
|
||||
describe("minIndex", function() {
|
||||
@@ -164,7 +164,7 @@ describe("TimelineWindow", function() {
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
});
|
||||
|
||||
describe("load", function() {
|
||||
|
||||
@@ -11,7 +11,7 @@ describe("User", function() {
|
||||
let user;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
user = new User(userId);
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import expect from 'expect';
|
||||
|
||||
describe("utils", function() {
|
||||
beforeEach(function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
});
|
||||
|
||||
describe("encodeParams", function() {
|
||||
|
||||
+224
-99
@@ -102,6 +102,56 @@ 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";
|
||||
}
|
||||
|
||||
static get ERROR_GENERIC_FAILURE() {
|
||||
return "Failed to get autodiscovery configuration from server";
|
||||
}
|
||||
|
||||
static get ERROR_INVALID_HS_BASE_URL() {
|
||||
return "Invalid base_url for m.homeserver";
|
||||
}
|
||||
|
||||
static get ERROR_INVALID_HOMESERVER() {
|
||||
return "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";
|
||||
}
|
||||
|
||||
static get ERROR_INVALID_IDENTITY_SERVER() {
|
||||
return "Identity server URL does not appear to be a valid identity server";
|
||||
}
|
||||
|
||||
static get ERROR_INVALID_IS() {
|
||||
return "Invalid identity server discovery response";
|
||||
}
|
||||
|
||||
static get ERROR_MISSING_WELLKNOWN() {
|
||||
return "No .well-known JSON file found";
|
||||
}
|
||||
|
||||
static get ERROR_INVALID_JSON() {
|
||||
return "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,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The auto discovery failed. The client is expected to communicate
|
||||
* the error to the user and refuse logging in.
|
||||
@@ -137,6 +187,166 @@ export class AutoDiscovery {
|
||||
*/
|
||||
static get SUCCESS() { return "SUCCESS"; }
|
||||
|
||||
/**
|
||||
* Validates and verifies client configuration information for purposes
|
||||
* of logging in. Such information includes the homeserver URL
|
||||
* 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
|
||||
* 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) {
|
||||
// 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
|
||||
// write. We'll update the properties of this object over the duration
|
||||
// of this function.
|
||||
const clientConfig = {
|
||||
"m.homeserver": {
|
||||
state: AutoDiscovery.FAIL_ERROR,
|
||||
error: AutoDiscovery.ERROR_INVALID,
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
// Technically, we don't have a problem with the identity server
|
||||
// config at this point.
|
||||
state: AutoDiscovery.PROMPT,
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
if (!wellknown || !wellknown["m.homeserver"]) {
|
||||
logger.error("No m.homeserver key in config");
|
||||
|
||||
clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
|
||||
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID;
|
||||
|
||||
return Promise.resolve(clientConfig);
|
||||
}
|
||||
|
||||
if (!wellknown["m.homeserver"]["base_url"]) {
|
||||
logger.error("No m.homeserver base_url in config");
|
||||
|
||||
clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
|
||||
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HS_BASE_URL;
|
||||
|
||||
return Promise.resolve(clientConfig);
|
||||
}
|
||||
|
||||
// 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(
|
||||
wellknown["m.homeserver"]["base_url"],
|
||||
);
|
||||
if (!hsUrl) {
|
||||
logger.error("Invalid base_url for m.homeserver");
|
||||
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HS_BASE_URL;
|
||||
return Promise.resolve(clientConfig);
|
||||
}
|
||||
|
||||
// Step 3: Make sure the homeserver URL points to a homeserver.
|
||||
const hsVersions = await this._fetchWellKnownObject(
|
||||
`${hsUrl}/_matrix/client/versions`,
|
||||
);
|
||||
if (!hsVersions || !hsVersions.raw["versions"]) {
|
||||
logger.error("Invalid /versions response");
|
||||
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HOMESERVER;
|
||||
return Promise.resolve(clientConfig);
|
||||
}
|
||||
|
||||
// Step 4: Now that the homeserver looks valid, update our client config.
|
||||
clientConfig["m.homeserver"] = {
|
||||
state: AutoDiscovery.SUCCESS,
|
||||
error: null,
|
||||
base_url: hsUrl,
|
||||
};
|
||||
|
||||
// Step 5: Try to pull out the identity server configuration
|
||||
let isUrl = "";
|
||||
if (wellknown["m.identity_server"]) {
|
||||
// We prepare a failing identity server response to save lines later
|
||||
// in this branch. Note that we also fail the homeserver check in the
|
||||
// object because according to the spec we're supposed to FAIL_ERROR
|
||||
// if *anything* goes wrong with the IS validation, including invalid
|
||||
// format. This means we're supposed to stop discovery completely.
|
||||
const failingClientConfig = {
|
||||
"m.homeserver": {
|
||||
state: AutoDiscovery.FAIL_ERROR,
|
||||
error: AutoDiscovery.ERROR_INVALID_IS,
|
||||
|
||||
// We'll provide the base_url that was previously valid for
|
||||
// debugging purposes.
|
||||
base_url: clientConfig["m.homeserver"].base_url,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: AutoDiscovery.FAIL_ERROR,
|
||||
error: AutoDiscovery.ERROR_INVALID_IS,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
// 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(
|
||||
wellknown["m.identity_server"]["base_url"],
|
||||
);
|
||||
if (!isUrl) {
|
||||
logger.error("Invalid base_url for m.identity_server");
|
||||
failingClientConfig["m.identity_server"].error =
|
||||
AutoDiscovery.ERROR_INVALID_IS_BASE_URL;
|
||||
return Promise.resolve(failingClientConfig);
|
||||
}
|
||||
|
||||
// Step 5b: Verify there is an identity server listening on the provided
|
||||
// URL.
|
||||
const isResponse = await this._fetchWellKnownObject(
|
||||
`${isUrl}/_matrix/identity/api/v1`,
|
||||
);
|
||||
if (!isResponse || !isResponse.raw || isResponse.action !== "SUCCESS") {
|
||||
logger.error("Invalid /api/v1 response");
|
||||
failingClientConfig["m.identity_server"].error =
|
||||
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER;
|
||||
return Promise.resolve(failingClientConfig);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6: Now that the identity server is valid, or never existed,
|
||||
// populate the IS section.
|
||||
if (isUrl && isUrl.length > 0) {
|
||||
clientConfig["m.identity_server"] = {
|
||||
state: AutoDiscovery.SUCCESS,
|
||||
error: null,
|
||||
base_url: isUrl,
|
||||
};
|
||||
}
|
||||
|
||||
// Step 7: Copy any other keys directly into the clientConfig. This is for
|
||||
// things like custom configuration of services.
|
||||
Object.keys(wellknown)
|
||||
.map((k) => {
|
||||
if (k === "m.homeserver" || k === "m.identity_server") {
|
||||
// Only copy selected parts of the config to avoid overwriting
|
||||
// properties computed by the validation logic above.
|
||||
const notProps = ["error", "state", "base_url"];
|
||||
for (const prop of Object.keys(wellknown[k])) {
|
||||
if (notProps.includes(prop)) continue;
|
||||
clientConfig[k][prop] = wellknown[k][prop];
|
||||
}
|
||||
} else {
|
||||
// Just copy the whole thing over otherwise
|
||||
clientConfig[k] = wellknown[k];
|
||||
}
|
||||
});
|
||||
|
||||
// Step 8: Give the config to the caller (finally)
|
||||
return Promise.resolve(clientConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to automatically discover client configuration information
|
||||
* prior to logging in. Such information includes the homeserver URL
|
||||
@@ -171,7 +381,7 @@ export class AutoDiscovery {
|
||||
const clientConfig = {
|
||||
"m.homeserver": {
|
||||
state: AutoDiscovery.FAIL_ERROR,
|
||||
error: "Invalid homeserver discovery response",
|
||||
error: AutoDiscovery.ERROR_INVALID,
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
@@ -188,10 +398,8 @@ export class AutoDiscovery {
|
||||
const wellknown = await this._fetchWellKnownObject(
|
||||
`https://${domain}/.well-known/matrix/client`,
|
||||
);
|
||||
if (!wellknown || wellknown.action !== "SUCCESS"
|
||||
|| !wellknown.raw["m.homeserver"]
|
||||
|| !wellknown.raw["m.homeserver"]["base_url"]) {
|
||||
logger.error("No m.homeserver key in well-known response");
|
||||
if (!wellknown || wellknown.action !== "SUCCESS") {
|
||||
logger.error("No response or error when parsing .well-known");
|
||||
if (wellknown.reason) logger.error(wellknown.reason);
|
||||
if (wellknown.action === "IGNORE") {
|
||||
clientConfig["m.homeserver"] = {
|
||||
@@ -202,99 +410,13 @@ export class AutoDiscovery {
|
||||
} else {
|
||||
// this can only ever be FAIL_PROMPT at this point.
|
||||
clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
|
||||
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID;
|
||||
}
|
||||
return Promise.resolve(clientConfig);
|
||||
}
|
||||
|
||||
// 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(
|
||||
wellknown.raw["m.homeserver"]["base_url"],
|
||||
);
|
||||
if (!hsUrl) {
|
||||
logger.error("Invalid base_url for m.homeserver");
|
||||
return Promise.resolve(clientConfig);
|
||||
}
|
||||
|
||||
// Step 3: Make sure the homeserver URL points to a homeserver.
|
||||
const hsVersions = await this._fetchWellKnownObject(
|
||||
`${hsUrl}/_matrix/client/versions`,
|
||||
);
|
||||
if (!hsVersions || !hsVersions.raw["versions"]) {
|
||||
logger.error("Invalid /versions response");
|
||||
return Promise.resolve(clientConfig);
|
||||
}
|
||||
|
||||
// Step 4: Now that the homeserver looks valid, update our client config.
|
||||
clientConfig["m.homeserver"] = {
|
||||
state: AutoDiscovery.SUCCESS,
|
||||
error: null,
|
||||
base_url: hsUrl,
|
||||
};
|
||||
|
||||
// Step 5: Try to pull out the identity server configuration
|
||||
let isUrl = "";
|
||||
if (wellknown.raw["m.identity_server"]) {
|
||||
// We prepare a failing identity server response to save lines later
|
||||
// in this branch. Note that we also fail the homeserver check in the
|
||||
// object because according to the spec we're supposed to FAIL_ERROR
|
||||
// if *anything* goes wrong with the IS validation, including invalid
|
||||
// format. This means we're supposed to stop discovery completely.
|
||||
const failingClientConfig = {
|
||||
"m.homeserver": {
|
||||
state: AutoDiscovery.FAIL_ERROR,
|
||||
error: "Invalid identity server discovery response",
|
||||
|
||||
// We'll provide the base_url that was previously valid for
|
||||
// debugging purposes.
|
||||
base_url: clientConfig["m.homeserver"].base_url,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: AutoDiscovery.FAIL_ERROR,
|
||||
error: "Invalid identity server discovery response",
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
// 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(
|
||||
wellknown.raw["m.identity_server"]["base_url"],
|
||||
);
|
||||
if (!isUrl) {
|
||||
logger.error("Invalid base_url for m.identity_server");
|
||||
return Promise.resolve(failingClientConfig);
|
||||
}
|
||||
|
||||
// Step 5b: Verify there is an identity server listening on the provided
|
||||
// URL.
|
||||
const isResponse = await this._fetchWellKnownObject(
|
||||
`${isUrl}/_matrix/identity/api/v1`,
|
||||
);
|
||||
if (!isResponse || !isResponse.raw || isResponse.action !== "SUCCESS") {
|
||||
logger.error("Invalid /api/v1 response");
|
||||
return Promise.resolve(failingClientConfig);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6: Now that the identity server is valid, or never existed,
|
||||
// populate the IS section.
|
||||
if (isUrl && isUrl.length > 0) {
|
||||
clientConfig["m.identity_server"] = {
|
||||
state: AutoDiscovery.SUCCESS,
|
||||
error: null,
|
||||
base_url: isUrl,
|
||||
};
|
||||
}
|
||||
|
||||
// Step 7: Copy any other keys directly into the clientConfig. This is for
|
||||
// things like custom configuration of services.
|
||||
Object.keys(wellknown.raw)
|
||||
.filter((k) => k !== "m.homeserver" && k !== "m.identity_server")
|
||||
.map((k) => clientConfig[k] = wellknown.raw[k]);
|
||||
|
||||
// Step 8: Give the config to the caller (finally)
|
||||
return Promise.resolve(clientConfig);
|
||||
// Step 2: Validate and parse the config
|
||||
return AutoDiscovery.fromDiscoveryConfig(wellknown.raw);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -358,14 +480,14 @@ export class AutoDiscovery {
|
||||
const request = require("./matrix").getRequest();
|
||||
if (!request) throw new Error("No request library available");
|
||||
request(
|
||||
{ method: "GET", uri: url },
|
||||
{ method: "GET", uri: url, timeout: 5000 },
|
||||
(err, response, body) => {
|
||||
if (err || response.statusCode < 200 || response.statusCode >= 300) {
|
||||
let action = "FAIL_PROMPT";
|
||||
let reason = (err ? err.message : null) || "General failure";
|
||||
if (response.statusCode === 404) {
|
||||
action = "IGNORE";
|
||||
reason = "No .well-known JSON file found";
|
||||
reason = AutoDiscovery.ERROR_MISSING_WELLKNOWN;
|
||||
}
|
||||
resolve({raw: {}, action: action, reason: reason, error: err});
|
||||
return;
|
||||
@@ -374,12 +496,15 @@ export class AutoDiscovery {
|
||||
try {
|
||||
resolve({raw: JSON.parse(body), action: "SUCCESS"});
|
||||
} catch (e) {
|
||||
let reason = "General failure";
|
||||
if (e.name === "SyntaxError") reason = "Invalid JSON";
|
||||
let reason = AutoDiscovery.ERROR_INVALID;
|
||||
if (e.name === "SyntaxError") {
|
||||
reason = AutoDiscovery.ERROR_INVALID_JSON;
|
||||
}
|
||||
resolve({
|
||||
raw: {},
|
||||
action: "FAIL_PROMPT",
|
||||
reason: reason, error: e,
|
||||
reason: reason,
|
||||
error: e,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
+15
-5
@@ -263,8 +263,7 @@ MatrixBaseApis.prototype.login = function(loginType, data, callback) {
|
||||
|
||||
return this._http.authedRequest(
|
||||
(error, response) => {
|
||||
if (loginType === "m.login.password" && response &&
|
||||
response.access_token && response.user_id) {
|
||||
if (response && response.access_token && response.user_id) {
|
||||
this._http.opts.accessToken = response.access_token;
|
||||
this.credentials = {
|
||||
userId: response.user_id,
|
||||
@@ -891,18 +890,29 @@ MatrixBaseApis.prototype.sendStateEvent = function(roomId, eventType, content, s
|
||||
/**
|
||||
* @param {string} roomId
|
||||
* @param {string} eventId
|
||||
* @param {string} [txnId] transaction id. One will be made up if not
|
||||
* supplied.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.redactEvent = function(roomId, eventId, callback) {
|
||||
const path = utils.encodeUri("/rooms/$roomId/redact/$eventId", {
|
||||
MatrixBaseApis.prototype.redactEvent = function(
|
||||
roomId, eventId, txnId, callback,
|
||||
) {
|
||||
if (arguments.length === 3) {
|
||||
callback = txnId;
|
||||
}
|
||||
|
||||
const path = utils.encodeUri("/rooms/$roomId/redact/$eventId/$txnId", {
|
||||
$roomId: roomId,
|
||||
$eventId: eventId,
|
||||
$txnId: txnId ? txnId : this.makeTxnId(),
|
||||
});
|
||||
return this._http.authedRequest(callback, "POST", path, undefined, {});
|
||||
|
||||
return this._http.authedRequest(callback, "PUT", path, undefined, {});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} roomId
|
||||
* @param {Number} limit
|
||||
|
||||
+47
-11
@@ -148,6 +148,11 @@ function keyFromRecoverySession(session, decryptionKey) {
|
||||
* maintain support for back-paginating the live timeline after a '/sync'
|
||||
* result with a gap.
|
||||
*
|
||||
* @param {boolean} [opts.unstableClientRelationAggregation = false]
|
||||
* Optional. Set to true to enable client-side aggregation of event relations
|
||||
* via `EventTimelineSet#getRelationsForEvent`.
|
||||
* This feature is currently unstable and the API may change without notice.
|
||||
*
|
||||
* @param {Array} [opts.verificationMethods] Optional. The verification method
|
||||
* that the application can handle. Each element should be an item from {@link
|
||||
* module:crypto~verificationMethods verificationMethods}, or a class that
|
||||
@@ -213,6 +218,7 @@ function MatrixClient(opts) {
|
||||
this.timelineSupport = Boolean(opts.timelineSupport);
|
||||
this.urlPreviewCache = {};
|
||||
this._notifTimelineSet = null;
|
||||
this.unstableClientRelationAggregation = !!opts.unstableClientRelationAggregation;
|
||||
|
||||
this._crypto = null;
|
||||
this._cryptoStore = opts.cryptoStore;
|
||||
@@ -242,19 +248,32 @@ function MatrixClient(opts) {
|
||||
const actions = this._pushProcessor.actionsForEvent(event);
|
||||
event.setPushActions(actions); // Might as well while we're here
|
||||
|
||||
const room = this.getRoom(event.getRoomId());
|
||||
if (!room) return;
|
||||
|
||||
const currentCount = room.getUnreadNotificationCount("highlight");
|
||||
|
||||
// Ensure the unread counts are kept up to date if the event is encrypted
|
||||
// We also want to make sure that the notification count goes up if we already
|
||||
// have encrypted events to avoid other code from resetting 'highlight' to zero.
|
||||
const oldHighlight = oldActions && oldActions.tweaks
|
||||
? !!oldActions.tweaks.highlight : false;
|
||||
const newHighlight = actions && actions.tweaks
|
||||
? !!actions.tweaks.highlight : false;
|
||||
if (oldHighlight !== newHighlight) {
|
||||
const room = this.getRoom(event.getRoomId());
|
||||
if (oldHighlight !== newHighlight || currentCount > 0) {
|
||||
// TODO: Handle mentions received while the client is offline
|
||||
// See also https://github.com/vector-im/riot-web/issues/9069
|
||||
if (room && !room.hasUserReadEvent(this.getUserId(), event.getId())) {
|
||||
const current = room.getUnreadNotificationCount("highlight");
|
||||
const newCount = newHighlight ? current + 1 : current - 1;
|
||||
if (!room.hasUserReadEvent(this.getUserId(), event.getId())) {
|
||||
let newCount = currentCount;
|
||||
if (newHighlight && !oldHighlight) newCount++;
|
||||
if (!newHighlight && oldHighlight) newCount--;
|
||||
room.setUnreadNotificationCount("highlight", newCount);
|
||||
|
||||
// Fix 'Mentions Only' rooms from not having the right badge count
|
||||
const totalCount = room.getUnreadNotificationCount('total');
|
||||
if (totalCount < newCount) {
|
||||
room.setUnreadNotificationCount('total', newCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -432,9 +451,10 @@ MatrixClient.prototype.setNotifTimelineSet = function(notifTimelineSet) {
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.getCapabilities = function() {
|
||||
const now = new Date().getTime();
|
||||
|
||||
if (this._cachedCapabilities) {
|
||||
const now = new Date().getTime();
|
||||
if (now - this._cachedCapabilities.lastUpdated <= CAPABILITIES_CACHE_MS) {
|
||||
if (now < this._cachedCapabilities.expiration) {
|
||||
console.log("Returning cached capabilities");
|
||||
return Promise.resolve(this._cachedCapabilities.capabilities);
|
||||
}
|
||||
@@ -443,12 +463,22 @@ MatrixClient.prototype.getCapabilities = function() {
|
||||
// We swallow errors because we need a default object anyhow
|
||||
return this._http.authedRequest(
|
||||
undefined, "GET", "/capabilities",
|
||||
).catch(() => null).then((r) => {
|
||||
).catch((e) => {
|
||||
console.error(e);
|
||||
return null; // otherwise consume the error
|
||||
}).then((r) => {
|
||||
if (!r) r = {};
|
||||
const capabilities = r["capabilities"] || {};
|
||||
|
||||
// If the capabilities missed the cache, cache it for a shorter amount
|
||||
// of time to try and refresh them later.
|
||||
const cacheMs = Object.keys(capabilities).length
|
||||
? CAPABILITIES_CACHE_MS
|
||||
: 60000 + (Math.random() * 5000);
|
||||
|
||||
this._cachedCapabilities = {
|
||||
capabilities: capabilities,
|
||||
lastUpdated: new Date().getTime(),
|
||||
expiration: now + cacheMs,
|
||||
};
|
||||
|
||||
console.log("Caching capabilities: ", capabilities);
|
||||
@@ -491,6 +521,7 @@ MatrixClient.prototype.initCrypto = async function() {
|
||||
}
|
||||
|
||||
// initialise the list of encrypted rooms (whether or not crypto is enabled)
|
||||
console.log("Crypto: initialising roomlist...");
|
||||
await this._roomList.init();
|
||||
|
||||
const userId = this.getUserId();
|
||||
@@ -525,6 +556,7 @@ MatrixClient.prototype.initCrypto = async function() {
|
||||
"crypto.warning",
|
||||
]);
|
||||
|
||||
console.log("Crypto: initialising crypto object...");
|
||||
await crypto.init();
|
||||
|
||||
this.olmVersion = Crypto.getOlmVersion();
|
||||
@@ -1685,7 +1717,7 @@ MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId,
|
||||
content: content,
|
||||
});
|
||||
localEvent._txnId = txnId;
|
||||
localEvent.status = EventStatus.SENDING;
|
||||
localEvent.setStatus(EventStatus.SENDING);
|
||||
|
||||
// add this event immediately to the local store as 'sending'.
|
||||
if (room) {
|
||||
@@ -1815,7 +1847,7 @@ function _updatePendingEventStatus(room, event, newStatus) {
|
||||
if (room) {
|
||||
room.updatePendingEvent(event, newStatus);
|
||||
} else {
|
||||
event.status = newStatus;
|
||||
event.setStatus(newStatus);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4130,6 +4162,10 @@ function _PojoToMatrixEventMapper(client) {
|
||||
]);
|
||||
event.attemptDecryption(client._crypto);
|
||||
}
|
||||
const room = client.getRoom(event.getRoomId());
|
||||
if (room) {
|
||||
room.reEmitter.reEmit(event, ["Event.replaced"]);
|
||||
}
|
||||
return event;
|
||||
}
|
||||
return mapper;
|
||||
|
||||
+17
-2
@@ -197,8 +197,11 @@ utils.inherits(Crypto, EventEmitter);
|
||||
* Returns a promise which resolves once the crypto module is ready for use.
|
||||
*/
|
||||
Crypto.prototype.init = async function() {
|
||||
console.log("Crypto: initialising Olm...");
|
||||
await global.Olm.init();
|
||||
console.log("Crypto: initialising Olm device...");
|
||||
await this._olmDevice.init();
|
||||
console.log("Crypto: loading device list...");
|
||||
await this._deviceList.load();
|
||||
|
||||
// build our device keys: these will later be uploaded
|
||||
@@ -207,6 +210,7 @@ Crypto.prototype.init = async function() {
|
||||
this._deviceKeys["curve25519:" + this._deviceId] =
|
||||
this._olmDevice.deviceCurve25519Key;
|
||||
|
||||
console.log("Crypto: fetching own devices...");
|
||||
let myDevices = this._deviceList.getRawStoredDevicesForUser(
|
||||
this._userId,
|
||||
);
|
||||
@@ -217,6 +221,7 @@ Crypto.prototype.init = async function() {
|
||||
|
||||
if (!myDevices[this._deviceId]) {
|
||||
// add our own deviceinfo to the cryptoStore
|
||||
console.log("Crypto: adding this device to the store...");
|
||||
const deviceInfo = {
|
||||
keys: this._deviceKeys,
|
||||
algorithms: this._supportedAlgorithms,
|
||||
@@ -231,6 +236,7 @@ Crypto.prototype.init = async function() {
|
||||
this._deviceList.saveIfDirty();
|
||||
}
|
||||
|
||||
console.log("Crypto: checking for key backup...");
|
||||
this._checkAndStartKeyBackup();
|
||||
};
|
||||
|
||||
@@ -351,7 +357,12 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) {
|
||||
const mySigs = backupInfo.auth_data.signatures[this._userId] || [];
|
||||
|
||||
for (const keyId of Object.keys(mySigs)) {
|
||||
const sigInfo = { deviceId: keyId.split(':')[1] }; // XXX: is this how we're supposed to get the device ID?
|
||||
const keyIdParts = keyId.split(':');
|
||||
if (keyIdParts[0] !== 'ed25519') {
|
||||
console.log("Ignoring unknown signature type: " + keyIdParts[0]);
|
||||
continue;
|
||||
}
|
||||
const sigInfo = { deviceId: keyIdParts[1] }; // XXX: is this how we're supposed to get the device ID?
|
||||
const device = this._deviceList.getStoredDevice(
|
||||
this._userId, sigInfo.deviceId,
|
||||
);
|
||||
@@ -367,7 +378,11 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) {
|
||||
);
|
||||
sigInfo.valid = true;
|
||||
} catch (e) {
|
||||
logger.info("Bad signature from device " + device.deviceId, e);
|
||||
logger.info(
|
||||
"Bad signature from key ID " + keyId + " userID " + this._userId +
|
||||
" device ID " + device.deviceId + " fingerprint: " +
|
||||
device.getFingerprint(), backupInfo.auth_data, e,
|
||||
);
|
||||
sigInfo.valid = false;
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -21,7 +21,7 @@ import bs58 from 'bs58';
|
||||
const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01];
|
||||
|
||||
export function encodeRecoveryKey(key) {
|
||||
const buf = new Uint8Array(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
|
||||
const buf = new Buffer(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
|
||||
buf.set(OLM_RECOVERY_KEY_PREFIX, 0);
|
||||
buf.set(key, OLM_RECOVERY_KEY_PREFIX.length);
|
||||
|
||||
|
||||
@@ -694,9 +694,17 @@ function promiseifyTxn(txn) {
|
||||
if (txn._mx_abortexception !== undefined) {
|
||||
reject(txn._mx_abortexception);
|
||||
} else {
|
||||
console.log("Error performing indexeddb txn", event);
|
||||
reject(event.target.error);
|
||||
}
|
||||
};
|
||||
txn.onabort = (event) => {
|
||||
if (txn._mx_abortexception !== undefined) {
|
||||
reject(txn._mx_abortexception);
|
||||
} else {
|
||||
console.log("Error performing indexeddb txn", event);
|
||||
reject(event.target.error);
|
||||
}
|
||||
};
|
||||
txn.onabort = () => reject(txn._mx_abortexception);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ export default class IndexedDBCryptoStore {
|
||||
};
|
||||
|
||||
req.onerror = (ev) => {
|
||||
console.log("Error connecting to indexeddb", ev);
|
||||
reject(ev.target.error);
|
||||
};
|
||||
|
||||
@@ -159,6 +160,7 @@ export default class IndexedDBCryptoStore {
|
||||
};
|
||||
|
||||
req.onerror = (ev) => {
|
||||
console.log("Error deleting data from indexeddb", ev);
|
||||
reject(ev.target.error);
|
||||
};
|
||||
|
||||
|
||||
@@ -143,17 +143,44 @@ function generateEmojiSas(sasBytes) {
|
||||
return emojis.map((num) => emojiMapping[num]);
|
||||
}
|
||||
|
||||
const sasGenerators = {
|
||||
decimal: generateDecimalSas,
|
||||
emoji: generateEmojiSas,
|
||||
};
|
||||
|
||||
function generateSas(sasBytes, methods) {
|
||||
const sas = {};
|
||||
if (methods.includes("decimal")) {
|
||||
sas["decimal"] = generateDecimalSas(sasBytes);
|
||||
}
|
||||
if (methods.includes("emoji")) {
|
||||
sas["emoji"] = generateEmojiSas(sasBytes);
|
||||
for (const method of methods) {
|
||||
if (method in sasGenerators) {
|
||||
sas[method] = sasGenerators[method](sasBytes);
|
||||
}
|
||||
}
|
||||
return sas;
|
||||
}
|
||||
|
||||
const macMethods = {
|
||||
"hkdf-hmac-sha256": "calculate_mac",
|
||||
"hmac-sha256": "calculate_mac_long_kdf",
|
||||
};
|
||||
|
||||
/* lists of algorithms/methods that are supported. The key agreement, hashes,
|
||||
* and MAC lists should be sorted in order of preference (most preferred
|
||||
* first).
|
||||
*/
|
||||
const KEY_AGREEMENT_LIST = ["curve25519"];
|
||||
const HASHES_LIST = ["sha256"];
|
||||
const MAC_LIST = ["hkdf-hmac-sha256", "hmac-sha256"];
|
||||
const SAS_LIST = Object.keys(sasGenerators);
|
||||
|
||||
const KEY_AGREEMENT_SET = new Set(KEY_AGREEMENT_LIST);
|
||||
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) {
|
||||
return anArray instanceof Array ? anArray.filter(x => aSet.has(x)) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @alias module:crypto/verification/SAS
|
||||
* @extends {module:crypto/verification/Base}
|
||||
@@ -181,11 +208,11 @@ export default class SAS extends Base {
|
||||
const initialMessage = {
|
||||
method: SAS.NAME,
|
||||
from_device: this._baseApis.deviceId,
|
||||
key_agreement_protocols: ["curve25519"],
|
||||
hashes: ["sha256"],
|
||||
message_authentication_codes: ["hmac-sha256"],
|
||||
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: ["decimal", "emoji"],
|
||||
short_authentication_string: SAS_LIST,
|
||||
transaction_id: this.transactionId,
|
||||
};
|
||||
this._sendToDevice("m.key.verification.start", initialMessage);
|
||||
@@ -193,19 +220,19 @@ export default class SAS extends Base {
|
||||
|
||||
let e = await this._waitForEvent("m.key.verification.accept");
|
||||
let content = e.getContent();
|
||||
if (!(content.key_agreement_protocol === "curve25519"
|
||||
&& content.hash === "sha256"
|
||||
&& content.message_authentication_code === "hmac-sha256"
|
||||
&& content.short_authentication_string instanceof Array
|
||||
&& (content.short_authentication_string.includes("decimal")
|
||||
|| content.short_authentication_string.includes("emoji")))) {
|
||||
const sasMethods
|
||||
= intersection(content.short_authentication_string, SAS_SET);
|
||||
if (!(KEY_AGREEMENT_SET.has(content.key_agreement_protocol)
|
||||
&& HASHES_SET.has(content.hash)
|
||||
&& MAC_SET.has(content.message_authentication_code)
|
||||
&& sasMethods.length)) {
|
||||
throw newUnknownMethodError();
|
||||
}
|
||||
if (typeof content.commitment !== "string") {
|
||||
throw newInvalidMessageError();
|
||||
}
|
||||
const macMethod = content.message_authentication_code;
|
||||
const hashCommitment = content.commitment;
|
||||
const sasMethods = content.short_authentication_string;
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
try {
|
||||
this._sendToDevice("m.key.verification.key", {
|
||||
@@ -217,6 +244,7 @@ export default class SAS extends Base {
|
||||
// FIXME: make sure event is properly formed
|
||||
content = e.getContent();
|
||||
const commitmentStr = content.key + anotherjson.stringify(initialMessage);
|
||||
// TODO: use selected hash function (when we support multiple)
|
||||
if (olmutil.sha256(commitmentStr) !== hashCommitment) {
|
||||
throw newMismatchedCommitmentError();
|
||||
}
|
||||
@@ -231,7 +259,7 @@ export default class SAS extends Base {
|
||||
this.emit("show_sas", {
|
||||
sas: generateSas(sasBytes, sasMethods),
|
||||
confirm: () => {
|
||||
this._sendMAC(olmSAS);
|
||||
this._sendMAC(olmSAS, macMethod);
|
||||
resolve();
|
||||
},
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
@@ -245,7 +273,7 @@ export default class SAS extends Base {
|
||||
verifySAS,
|
||||
]);
|
||||
content = e.getContent();
|
||||
await this._checkMAC(olmSAS, content);
|
||||
await this._checkMAC(olmSAS, content, macMethod);
|
||||
} finally {
|
||||
olmSAS.free();
|
||||
}
|
||||
@@ -253,34 +281,37 @@ export default class SAS extends Base {
|
||||
|
||||
async _doRespondVerification() {
|
||||
let content = this.startEvent.getContent();
|
||||
if (!(content.key_agreement_protocols instanceof Array
|
||||
&& content.key_agreement_protocols.includes("curve25519")
|
||||
&& content.hashes instanceof Array
|
||||
&& content.hashes.includes("sha256")
|
||||
&& content.message_authentication_codes instanceof Array
|
||||
&& content.message_authentication_codes.includes("hmac-sha256")
|
||||
&& content.short_authentication_string instanceof Array
|
||||
&& (content.short_authentication_string.includes("decimal")
|
||||
|| content.short_authentication_string.includes("emoji")))) {
|
||||
// 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];
|
||||
// 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)) {
|
||||
throw newUnknownMethodError();
|
||||
}
|
||||
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
const sasMethods = [];
|
||||
// FIXME: allow app to specify what SAS methods can be used
|
||||
if (content.short_authentication_string.includes("decimal")) {
|
||||
sasMethods.push("decimal");
|
||||
}
|
||||
if (content.short_authentication_string.includes("emoji")) {
|
||||
sasMethods.push("emoji");
|
||||
}
|
||||
try {
|
||||
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
|
||||
this._sendToDevice("m.key.verification.accept", {
|
||||
key_agreement_protocol: "curve25519",
|
||||
hash: "sha256",
|
||||
message_authentication_code: "hmac-sha256",
|
||||
key_agreement_protocol: keyAgreement,
|
||||
hash: hashMethod,
|
||||
message_authentication_code: macMethod,
|
||||
short_authentication_string: sasMethods,
|
||||
// TODO: use selected hash function (when we support multiple)
|
||||
commitment: olmutil.sha256(commitmentStr),
|
||||
});
|
||||
|
||||
@@ -302,7 +333,7 @@ export default class SAS extends Base {
|
||||
this.emit("show_sas", {
|
||||
sas: generateSas(sasBytes, sasMethods),
|
||||
confirm: () => {
|
||||
this._sendMAC(olmSAS);
|
||||
this._sendMAC(olmSAS, macMethod);
|
||||
resolve();
|
||||
},
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
@@ -316,13 +347,13 @@ export default class SAS extends Base {
|
||||
verifySAS,
|
||||
]);
|
||||
content = e.getContent();
|
||||
await this._checkMAC(olmSAS, content);
|
||||
await this._checkMAC(olmSAS, content, macMethod);
|
||||
} finally {
|
||||
olmSAS.free();
|
||||
}
|
||||
}
|
||||
|
||||
_sendMAC(olmSAS) {
|
||||
_sendMAC(olmSAS, method) {
|
||||
const keyId = `ed25519:${this._baseApis.deviceId}`;
|
||||
const mac = {};
|
||||
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
|
||||
@@ -330,24 +361,24 @@ export default class SAS extends Base {
|
||||
+ this.userId + this.deviceId
|
||||
+ this.transactionId;
|
||||
|
||||
mac[keyId] = olmSAS.calculate_mac(
|
||||
mac[keyId] = olmSAS[macMethods[method]](
|
||||
this._baseApis.getDeviceEd25519Key(),
|
||||
baseInfo + keyId,
|
||||
);
|
||||
const keys = olmSAS.calculate_mac(
|
||||
const keys = olmSAS[macMethods[method]](
|
||||
keyId,
|
||||
baseInfo + "KEY_IDS",
|
||||
);
|
||||
this._sendToDevice("m.key.verification.mac", { mac, keys });
|
||||
}
|
||||
|
||||
async _checkMAC(olmSAS, content) {
|
||||
async _checkMAC(olmSAS, content, method) {
|
||||
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
|
||||
+ this.userId + this.deviceId
|
||||
+ this._baseApis.getUserId() + this._baseApis.deviceId
|
||||
+ this.transactionId;
|
||||
|
||||
if (content.keys !== olmSAS.calculate_mac(
|
||||
if (content.keys !== olmSAS[macMethods[method]](
|
||||
Object.keys(content.mac).sort().join(","),
|
||||
baseInfo + "KEY_IDS",
|
||||
)) {
|
||||
@@ -355,7 +386,7 @@ export default class SAS extends Base {
|
||||
}
|
||||
|
||||
await this._verifyKeys(this.userId, content.mac, (keyId, device, keyInfo) => {
|
||||
if (keyInfo !== olmSAS.calculate_mac(
|
||||
if (keyInfo !== olmSAS[macMethods[method]](
|
||||
device.keys[keyId],
|
||||
baseInfo + keyId,
|
||||
)) {
|
||||
|
||||
+91
-59
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -88,6 +89,12 @@ const MSISDN_STAGE_TYPE = "m.login.msisdn";
|
||||
* @param {string?} opts.emailSid If returning from having completed m.login.email.identity
|
||||
* auth, the sid for the email verification session.
|
||||
*
|
||||
* @param {function?} opts.requestEmailToken A function that takes the email address (string),
|
||||
* clientSecret (string), attempt number (int) and sessionId (string) and calls the
|
||||
* relevant requestToken function and returns the promise returned by that function.
|
||||
* If the resulting promise rejects, the rejection will propagate through to the
|
||||
* attemptAuth promise.
|
||||
*
|
||||
*/
|
||||
function InteractiveAuth(opts) {
|
||||
this._matrixClient = opts.matrixClient;
|
||||
@@ -95,14 +102,17 @@ function InteractiveAuth(opts) {
|
||||
this._requestCallback = opts.doRequest;
|
||||
// startAuthStage included for backwards compat
|
||||
this._stateUpdatedCallback = opts.stateUpdated || opts.startAuthStage;
|
||||
this._completionDeferred = null;
|
||||
this._resolveFunc = null;
|
||||
this._rejectFunc = null;
|
||||
this._inputs = opts.inputs || {};
|
||||
this._requestEmailTokenCallback = opts.requestEmailToken;
|
||||
|
||||
if (opts.sessionId) this._data.session = opts.sessionId;
|
||||
this._clientSecret = opts.clientSecret || this._matrixClient.generateClientSecret();
|
||||
this._emailSid = opts.emailSid;
|
||||
if (this._emailSid === undefined) this._emailSid = null;
|
||||
|
||||
this._chosenFlow = null;
|
||||
this._currentStage = null;
|
||||
}
|
||||
|
||||
@@ -115,11 +125,12 @@ InteractiveAuth.prototype = {
|
||||
* no suitable authentication flow can be found
|
||||
*/
|
||||
attemptAuth: function() {
|
||||
this._completionDeferred = Promise.defer();
|
||||
// This promise will be quite long-lived and will resolve when the
|
||||
// request is authenticated and completes successfully.
|
||||
return new Promise((resolve, reject) => {
|
||||
this._resolveFunc = resolve;
|
||||
this._rejectFunc = reject;
|
||||
|
||||
// wrap in a promise so that if _startNextAuthStage
|
||||
// throws, it rejects the promise in a consistent way
|
||||
return Promise.resolve().then(() => {
|
||||
// if we have no flows, try a request (we'll have
|
||||
// just a session ID in _data if resuming)
|
||||
if (!this._data.flows) {
|
||||
@@ -127,7 +138,6 @@ InteractiveAuth.prototype = {
|
||||
} else {
|
||||
this._startNextAuthStage();
|
||||
}
|
||||
return this._completionDeferred.promise;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -194,6 +204,10 @@ InteractiveAuth.prototype = {
|
||||
return params[loginType];
|
||||
},
|
||||
|
||||
getChosenFlow() {
|
||||
return this._chosenFlow;
|
||||
},
|
||||
|
||||
/**
|
||||
* submit a new auth dict and fire off the request. This will either
|
||||
* make attemptAuth resolve/reject, or cause the startAuthStage callback
|
||||
@@ -207,7 +221,7 @@ InteractiveAuth.prototype = {
|
||||
* for requests that just poll to see if auth has been completed elsewhere.
|
||||
*/
|
||||
submitAuthDict: function(authData, background) {
|
||||
if (!this._completionDeferred) {
|
||||
if (!this._resolveFunc) {
|
||||
throw new Error("submitAuthDict() called before attemptAuth()");
|
||||
}
|
||||
|
||||
@@ -253,58 +267,74 @@ InteractiveAuth.prototype = {
|
||||
* This can be set to true for requests that just poll to see if auth has
|
||||
* been completed elsewhere.
|
||||
*/
|
||||
_doRequest: function(auth, background) {
|
||||
const self = this;
|
||||
|
||||
// hackery to make sure that synchronous exceptions end up in the catch
|
||||
// handler (without the additional event loop entailed by q.fcall or an
|
||||
// extra Promise.resolve().then)
|
||||
let prom;
|
||||
_doRequest: async function(auth, background) {
|
||||
try {
|
||||
prom = this._requestCallback(auth, background);
|
||||
} catch (e) {
|
||||
prom = Promise.reject(e);
|
||||
}
|
||||
const result = await this._requestCallback(auth, background);
|
||||
this._resolveFunc(result);
|
||||
} catch (error) {
|
||||
// sometimes UI auth errors don't come with flows
|
||||
const errorFlows = error.data ? error.data.flows : null;
|
||||
const haveFlows = Boolean(this._data.flows) || Boolean(errorFlows);
|
||||
if (error.httpStatus !== 401 || !error.data || !haveFlows) {
|
||||
// doesn't look like an interactive-auth failure.
|
||||
if (!background) {
|
||||
this._rejectFunc(error);
|
||||
} else {
|
||||
// We ignore all failures here (even non-UI auth related ones)
|
||||
// since we don't want to suddenly fail if the internet connection
|
||||
// had a blip whilst we were polling
|
||||
console.log(
|
||||
"Background poll request failed doing UI auth: ignoring",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
// if the error didn't come with flows, completed flows or session ID,
|
||||
// copy over the ones we have. Synapse sometimes sends responses without
|
||||
// any UI auth data (eg. when polling for email validation, if the email
|
||||
// has not yet been validated). This appears to be a Synapse bug, which
|
||||
// we workaround here.
|
||||
if (!error.data.flows && !error.data.completed && !error.data.session) {
|
||||
error.data.flows = this._data.flows;
|
||||
error.data.completed = this._data.completed;
|
||||
error.data.session = this._data.session;
|
||||
}
|
||||
this._data = error.data;
|
||||
this._startNextAuthStage();
|
||||
|
||||
prom = prom.then(
|
||||
function(result) {
|
||||
console.log("result from request: ", result);
|
||||
self._completionDeferred.resolve(result);
|
||||
}, function(error) {
|
||||
// sometimes UI auth errors don't come with flows
|
||||
const errorFlows = error.data ? error.data.flows : null;
|
||||
const haveFlows = Boolean(self._data.flows) || Boolean(errorFlows);
|
||||
if (error.httpStatus !== 401 || !error.data || !haveFlows) {
|
||||
// doesn't look like an interactive-auth failure. fail the whole lot.
|
||||
throw error;
|
||||
if (
|
||||
!this._emailSid &&
|
||||
this._chosenFlow.stages.includes('m.login.email.identity')
|
||||
) {
|
||||
// If we've picked a flow with email auth, we send the email
|
||||
// now because we want the request to fail as soon as possible
|
||||
// if the email address is not valid (ie. already taken or not
|
||||
// registered, depending on what the operation is).
|
||||
try {
|
||||
const requestTokenResult = await this._requestEmailTokenCallback(
|
||||
this._inputs.emailAddress,
|
||||
this._clientSecret,
|
||||
1, // TODO: Multiple send attempts?
|
||||
this._data.session,
|
||||
);
|
||||
this._emailSid = requestTokenResult.sid;
|
||||
// NB. promise is not resolved here - at some point, doRequest
|
||||
// will be called again and if the user has jumped through all
|
||||
// the hoops correctly, auth will be complete and the request
|
||||
// will succeed.
|
||||
// Also, we should expose the fact that this request has compledted
|
||||
// so clients can know that the email has actually been sent.
|
||||
} catch (e) {
|
||||
// we failed to request an email token, so fail the request.
|
||||
// This could be due to the email already beeing registered
|
||||
// (or not being registered, depending on what we're trying
|
||||
// to do) or it could be a network failure. Either way, pass
|
||||
// the failure up as the user can't complete auth if we can't
|
||||
// send the email, foe whatever reason.
|
||||
this._rejectFunc(e);
|
||||
}
|
||||
// if the error didn't come with flows, completed flows or session ID,
|
||||
// copy over the ones we have. Synapse sometimes sends responses without
|
||||
// any UI auth data (eg. when polling for email validation, if the email
|
||||
// has not yet been validated). This appears to be a Synapse bug, which
|
||||
// we workaround here.
|
||||
if (!error.data.flows && !error.data.completed && !error.data.session) {
|
||||
error.data.flows = self._data.flows;
|
||||
error.data.completed = self._data.completed;
|
||||
error.data.session = self._data.session;
|
||||
}
|
||||
self._data = error.data;
|
||||
self._startNextAuthStage();
|
||||
},
|
||||
);
|
||||
if (!background) {
|
||||
prom = prom.catch((e) => {
|
||||
this._completionDeferred.reject(e);
|
||||
});
|
||||
} else {
|
||||
// We ignore all failures here (even non-UI auth related ones)
|
||||
// since we don't want to suddenly fail if the internet connection
|
||||
// had a blip whilst we were polling
|
||||
prom = prom.catch((error) => {
|
||||
console.log("Ignoring error from UI auth: " + error);
|
||||
});
|
||||
}
|
||||
}
|
||||
prom.done();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -320,7 +350,7 @@ InteractiveAuth.prototype = {
|
||||
}
|
||||
this._currentStage = nextStage;
|
||||
|
||||
if (nextStage == 'm.login.dummy') {
|
||||
if (nextStage === 'm.login.dummy') {
|
||||
this.submitAuthDict({
|
||||
type: 'm.login.dummy',
|
||||
});
|
||||
@@ -350,9 +380,11 @@ InteractiveAuth.prototype = {
|
||||
* @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found
|
||||
*/
|
||||
_chooseStage: function() {
|
||||
const flow = this._chooseFlow();
|
||||
console.log("Active flow => %s", JSON.stringify(flow));
|
||||
const nextStage = this._firstUncompletedStage(flow);
|
||||
if (this._chosenFlow === null) {
|
||||
this._chosenFlow = this._chooseFlow();
|
||||
}
|
||||
console.log("Active flow => %s", JSON.stringify(this._chosenFlow));
|
||||
const nextStage = this._firstUncompletedStage(this._chosenFlow);
|
||||
console.log("Next stage: %s", nextStage);
|
||||
return nextStage;
|
||||
},
|
||||
|
||||
@@ -20,6 +20,7 @@ limitations under the License.
|
||||
const EventEmitter = require("events").EventEmitter;
|
||||
const utils = require("../utils");
|
||||
const EventTimeline = require("./event-timeline");
|
||||
import Relations from './relations';
|
||||
|
||||
// var DEBUG = false;
|
||||
const DEBUG = true;
|
||||
@@ -54,22 +55,38 @@ if (DEBUG) {
|
||||
* map from event_id to timeline and index.
|
||||
*
|
||||
* @constructor
|
||||
* @param {?Room} room the optional room for this timelineSet
|
||||
* @param {Object} opts hash of options inherited from Room.
|
||||
* opts.timelineSupport gives whether timeline support is enabled
|
||||
* opts.filter is the filter object, if any, for this timelineSet.
|
||||
* @param {?Room} room
|
||||
* Room for this timelineSet. May be null for non-room cases, such as the
|
||||
* notification timeline.
|
||||
* @param {Object} opts Options inherited from Room.
|
||||
*
|
||||
* @param {boolean} [opts.timelineSupport = false]
|
||||
* Set to true to enable improved timeline support.
|
||||
* @param {Object} [opts.filter = null]
|
||||
* The filter object, if any, for this timelineSet.
|
||||
* @param {boolean} [opts.unstableClientRelationAggregation = false]
|
||||
* Optional. Set to true to enable client-side aggregation of event relations
|
||||
* via `getRelationsForEvent`.
|
||||
* This feature is currently unstable and the API may change without notice.
|
||||
*/
|
||||
function EventTimelineSet(room, opts) {
|
||||
this.room = room;
|
||||
|
||||
this._timelineSupport = Boolean(opts.timelineSupport);
|
||||
this._liveTimeline = new EventTimeline(this);
|
||||
this._unstableClientRelationAggregation = !!opts.unstableClientRelationAggregation;
|
||||
|
||||
// just a list - *not* ordered.
|
||||
this._timelines = [this._liveTimeline];
|
||||
this._eventIdToTimeline = {};
|
||||
|
||||
this._filter = opts.filter || null;
|
||||
|
||||
if (this._unstableClientRelationAggregation) {
|
||||
// A tree of objects to access a set of relations for an event, as in:
|
||||
// this._relations[relatesToEventId][relationType][relationEventType]
|
||||
this._relations = {};
|
||||
}
|
||||
}
|
||||
utils.inherits(EventTimelineSet, EventEmitter);
|
||||
|
||||
@@ -408,8 +425,36 @@ EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimel
|
||||
console.info("Already have timeline for " + eventId +
|
||||
" - joining timeline " + timeline + " to " +
|
||||
existingTimeline);
|
||||
|
||||
// Variables to keep the line length limited below.
|
||||
const existingIsLive = existingTimeline === this._liveTimeline;
|
||||
const timelineIsLive = timeline === this._liveTimeline;
|
||||
|
||||
const backwardsIsLive = direction === EventTimeline.BACKWARDS && existingIsLive;
|
||||
const forwardsIsLive = direction === EventTimeline.FORWARDS && timelineIsLive;
|
||||
|
||||
if (backwardsIsLive || forwardsIsLive) {
|
||||
// The live timeline should never be spliced into a non-live position.
|
||||
// We use independent logging to better discover the problem at a glance.
|
||||
console.warn({backwardsIsLive, forwardsIsLive}); // debugging
|
||||
if (backwardsIsLive) {
|
||||
console.warn(
|
||||
"Refusing to set a preceding existingTimeLine on our " +
|
||||
"timeline as the existingTimeLine is live (" + existingTimeline + ")",
|
||||
);
|
||||
}
|
||||
if (forwardsIsLive) {
|
||||
console.warn(
|
||||
"Refusing to set our preceding timeline on a existingTimeLine " +
|
||||
"as our timeline is live (" + timeline + ")",
|
||||
);
|
||||
}
|
||||
continue; // abort splicing - try next event
|
||||
}
|
||||
|
||||
timeline.setNeighbouringTimeline(existingTimeline, direction);
|
||||
existingTimeline.setNeighbouringTimeline(timeline, inverseDirection);
|
||||
|
||||
timeline = existingTimeline;
|
||||
didUpdate = true;
|
||||
}
|
||||
@@ -418,6 +463,14 @@ EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimel
|
||||
// new information, we update the pagination token for whatever
|
||||
// timeline we ended up on.
|
||||
if (lastEventWasNew || !didUpdate) {
|
||||
if (direction === EventTimeline.FORWARDS && timeline === this._liveTimeline) {
|
||||
console.warn({lastEventWasNew, didUpdate}); // for debugging
|
||||
console.warn(
|
||||
`Refusing to set forwards pagination token of live timeline ` +
|
||||
`${timeline} to ${paginationToken}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
timeline.setPaginationToken(paginationToken, direction);
|
||||
}
|
||||
};
|
||||
@@ -487,6 +540,9 @@ EventTimelineSet.prototype.addEventToTimeline = function(event, timeline,
|
||||
timeline.addEvent(event, toStartOfTimeline);
|
||||
this._eventIdToTimeline[eventId] = timeline;
|
||||
|
||||
this.setRelationsTarget(event);
|
||||
this.aggregateRelations(event);
|
||||
|
||||
const data = {
|
||||
timeline: timeline,
|
||||
liveEvent: !toStartOfTimeline && timeline == this._liveTimeline,
|
||||
@@ -621,6 +677,122 @@ EventTimelineSet.prototype.compareEventOrdering = function(eventId1, eventId2) {
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a collection of relations to a given event in this timeline set.
|
||||
*
|
||||
* @param {String} eventId
|
||||
* The ID of the event that you'd like to access relation events for.
|
||||
* For example, with annotations, this would be the ID of the event being annotated.
|
||||
* @param {String} relationType
|
||||
* The type of relation involved, such as "m.annotation", "m.reference", "m.replace", etc.
|
||||
* @param {String} eventType
|
||||
* The relation event's type, such as "m.reaction", etc.
|
||||
*
|
||||
* @returns {Relations}
|
||||
* A container for relation events.
|
||||
*/
|
||||
EventTimelineSet.prototype.getRelationsForEvent = function(
|
||||
eventId, relationType, eventType,
|
||||
) {
|
||||
if (!this._unstableClientRelationAggregation) {
|
||||
throw new Error("Client-side relation aggregation is disabled");
|
||||
}
|
||||
|
||||
if (!eventId || !relationType || !eventType) {
|
||||
throw new Error("Invalid arguments for `getRelationsForEvent`");
|
||||
}
|
||||
|
||||
// debuglog("Getting relations for: ", eventId, relationType, eventType);
|
||||
|
||||
const relationsForEvent = this._relations[eventId] || {};
|
||||
const relationsWithRelType = relationsForEvent[relationType] || {};
|
||||
return relationsWithRelType[eventType];
|
||||
};
|
||||
|
||||
/**
|
||||
* Set an event as the target event if any Relations exist for it already
|
||||
*
|
||||
* @param {MatrixEvent} event
|
||||
* The event to check as relation target.
|
||||
*/
|
||||
EventTimelineSet.prototype.setRelationsTarget = function(event) {
|
||||
if (!this._unstableClientRelationAggregation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relationsForEvent = this._relations[event.getId()];
|
||||
if (!relationsForEvent) {
|
||||
return;
|
||||
}
|
||||
// don't need it for non m.replace relations for now
|
||||
const relationsWithRelType = relationsForEvent["m.replace"];
|
||||
if (!relationsWithRelType) {
|
||||
return;
|
||||
}
|
||||
// only doing replacements for messages for now (e.g. edits)
|
||||
const relationsWithEventType = relationsWithRelType["m.room.message"];
|
||||
|
||||
if (relationsWithEventType) {
|
||||
relationsWithEventType.setTargetEvent(event);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add relation events to the relevant relation collection.
|
||||
*
|
||||
* @param {MatrixEvent} event
|
||||
* The new relation event to be aggregated.
|
||||
*/
|
||||
EventTimelineSet.prototype.aggregateRelations = function(event) {
|
||||
if (!this._unstableClientRelationAggregation) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the event is currently encrypted, wait until it has been decrypted.
|
||||
if (event.isBeingDecrypted()) {
|
||||
event.once("Event.decrypted", () => {
|
||||
this.aggregateRelations(event);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const relation = event.getRelation();
|
||||
if (!relation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relatesToEventId = relation.event_id;
|
||||
const relationType = relation.rel_type;
|
||||
const eventType = event.getType();
|
||||
|
||||
// debuglog("Aggregating relation: ", event.getId(), eventType, relation);
|
||||
|
||||
let relationsForEvent = this._relations[relatesToEventId];
|
||||
if (!relationsForEvent) {
|
||||
relationsForEvent = this._relations[relatesToEventId] = {};
|
||||
}
|
||||
let relationsWithRelType = relationsForEvent[relationType];
|
||||
if (!relationsWithRelType) {
|
||||
relationsWithRelType = relationsForEvent[relationType] = {};
|
||||
}
|
||||
let relationsWithEventType = relationsWithRelType[eventType];
|
||||
|
||||
if (!relationsWithEventType) {
|
||||
relationsWithEventType = relationsWithRelType[eventType] = new Relations(
|
||||
relationType,
|
||||
eventType,
|
||||
this.room,
|
||||
);
|
||||
const relatesToEvent = this.findEventById(relatesToEventId);
|
||||
if (relatesToEvent) {
|
||||
relationsWithEventType.setTargetEvent(relatesToEvent);
|
||||
relatesToEvent.emit("Event.relationsCreated", relationType, eventType);
|
||||
}
|
||||
}
|
||||
|
||||
relationsWithEventType.addEvent(event);
|
||||
};
|
||||
|
||||
/**
|
||||
* The EventTimelineSet class.
|
||||
*/
|
||||
|
||||
@@ -276,7 +276,7 @@ EventTimeline.prototype.getNeighbouringTimeline = function(direction) {
|
||||
EventTimeline.prototype.setNeighbouringTimeline = function(neighbour, direction) {
|
||||
if (this.getNeighbouringTimeline(direction)) {
|
||||
throw new Error("timeline already has a neighbouring timeline - " +
|
||||
"cannot reset neighbour");
|
||||
"cannot reset neighbour (direction: " + direction + ")");
|
||||
}
|
||||
|
||||
if (direction == EventTimeline.BACKWARDS) {
|
||||
|
||||
+176
-21
@@ -50,6 +50,12 @@ module.exports.EventStatus = {
|
||||
};
|
||||
|
||||
const interns = {};
|
||||
function intern(str) {
|
||||
if (!interns[str]) {
|
||||
interns[str] = str;
|
||||
}
|
||||
return interns[str];
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a Matrix Event object
|
||||
@@ -87,20 +93,25 @@ module.exports.MatrixEvent = function MatrixEvent(
|
||||
if (!event[prop]) {
|
||||
return;
|
||||
}
|
||||
if (!interns[event[prop]]) {
|
||||
interns[event[prop]] = event[prop];
|
||||
}
|
||||
event[prop] = interns[event[prop]];
|
||||
event[prop] = intern(event[prop]);
|
||||
});
|
||||
|
||||
["membership", "avatar_url", "displayname"].forEach((prop) => {
|
||||
if (!event.content || !event.content[prop]) {
|
||||
return;
|
||||
}
|
||||
if (!interns[event.content[prop]]) {
|
||||
interns[event.content[prop]] = event.content[prop];
|
||||
event.content[prop] = intern(event.content[prop]);
|
||||
});
|
||||
|
||||
["rel_type"].forEach((prop) => {
|
||||
if (
|
||||
!event.content ||
|
||||
!event.content["m.relates_to"] ||
|
||||
!event.content["m.relates_to"][prop]
|
||||
) {
|
||||
return;
|
||||
}
|
||||
event.content[prop] = interns[event.content[prop]];
|
||||
event.content["m.relates_to"][prop] = intern(event.content["m.relates_to"][prop]);
|
||||
});
|
||||
|
||||
this.event = event || {};
|
||||
@@ -111,6 +122,7 @@ module.exports.MatrixEvent = function MatrixEvent(
|
||||
this.error = null;
|
||||
this.forwardLooking = true;
|
||||
this._pushActions = null;
|
||||
this._replacingEvent = null;
|
||||
|
||||
this._clearEvent = {};
|
||||
|
||||
@@ -209,12 +221,28 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the (decrypted, if necessary) event content JSON.
|
||||
* Get the (decrypted, if necessary) event content JSON, even if the event
|
||||
* was replaced by another event.
|
||||
*
|
||||
* @return {Object} The event content JSON, or an empty object.
|
||||
*/
|
||||
getOriginalContent: function() {
|
||||
return this._clearEvent.content || this.event.content || {};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the (decrypted, if necessary) event content JSON,
|
||||
* or the content from the replacing event, if any.
|
||||
* See `makeReplaced`.
|
||||
*
|
||||
* @return {Object} The event content JSON, or an empty object.
|
||||
*/
|
||||
getContent: function() {
|
||||
return this._clearEvent.content || this.event.content || {};
|
||||
if (this._replacingEvent) {
|
||||
return this._replacingEvent.getContent()["m.new_content"] || {};
|
||||
} else {
|
||||
return this.getOriginalContent();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -352,7 +380,7 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
|
||||
if (
|
||||
this._clearEvent && this._clearEvent.content &&
|
||||
this._clearEvent.content.msgtype !== "m.bad.encrypted"
|
||||
this._clearEvent.content.msgtype !== "m.bad.encrypted"
|
||||
) {
|
||||
// we may want to just ignore this? let's start with rejecting it.
|
||||
throw new Error(
|
||||
@@ -650,6 +678,9 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
throw new Error("invalid redaction_event in makeRedacted");
|
||||
}
|
||||
|
||||
this.emit("Event.beforeRedaction", this, redaction_event);
|
||||
|
||||
this._replacingEvent = null;
|
||||
// we attempt to replicate what we would see from the server if
|
||||
// the event had been redacted before we saw it.
|
||||
//
|
||||
@@ -697,28 +728,152 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
*
|
||||
* @return {?Object} push actions
|
||||
*/
|
||||
getPushActions: function() {
|
||||
getPushActions: function() {
|
||||
return this._pushActions;
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the push actions for this event.
|
||||
*
|
||||
* @param {Object} pushActions push actions
|
||||
*/
|
||||
setPushActions: function(pushActions) {
|
||||
setPushActions: function(pushActions) {
|
||||
this._pushActions = pushActions;
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Replace the `event` property and recalculate any properties based on it.
|
||||
* @param {Object} event the object to assign to the `event` property
|
||||
*/
|
||||
handleRemoteEcho: function(event) {
|
||||
/**
|
||||
* Replace the `event` property and recalculate any properties based on it.
|
||||
* @param {Object} event the object to assign to the `event` property
|
||||
*/
|
||||
handleRemoteEcho: function(event) {
|
||||
this.event = event;
|
||||
// successfully sent.
|
||||
this.status = null;
|
||||
},
|
||||
this.setStatus(null);
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether the event is in any phase of sending, send failure, waiting for
|
||||
* remote echo, etc.
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
isSending() {
|
||||
return !!this.status;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the event's sending status and emit an event as well.
|
||||
*
|
||||
* @param {String} status The new status
|
||||
*/
|
||||
setStatus(status) {
|
||||
this.status = status;
|
||||
this.emit("Event.status", this, status);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get whether the event is a relation event, and of a given type if
|
||||
* `relType` is passed in.
|
||||
*
|
||||
* @param {string?} relType if given, checks that the relation is of the
|
||||
* given type
|
||||
* @return {boolean}
|
||||
*/
|
||||
isRelation(relType = undefined) {
|
||||
// Relation info is lifted out of the encrypted content when sent to
|
||||
// encrypted rooms, so we have to check `getWireContent` for this.
|
||||
const content = this.getWireContent();
|
||||
const relation = content && content["m.relates_to"];
|
||||
return relation && relation.rel_type && relation.event_id &&
|
||||
((relType && relation.rel_type === relType) || !relType);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get relation info for the event, if any.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
getRelation() {
|
||||
if (!this.isRelation()) {
|
||||
return null;
|
||||
}
|
||||
return this.getWireContent()["m.relates_to"];
|
||||
},
|
||||
|
||||
/**
|
||||
* Set an event that replaces the content of this event, through an m.replace relation.
|
||||
*
|
||||
* @param {MatrixEvent?} newEvent the event with the replacing content, if any.
|
||||
*/
|
||||
makeReplaced(newEvent) {
|
||||
if (this.isRedacted()) {
|
||||
return;
|
||||
}
|
||||
if (this._replacingEvent !== newEvent) {
|
||||
this._replacingEvent = newEvent;
|
||||
this.emit("Event.replaced", this);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the status of the event, or the replacing event in case `makeReplace` has been called.
|
||||
*
|
||||
* @return {EventStatus}
|
||||
*/
|
||||
replacementOrOwnStatus() {
|
||||
if (this._replacingEvent) {
|
||||
return this._replacingEvent.status;
|
||||
} else {
|
||||
return this.status;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the event ID of the event replacing the content of this event, if any.
|
||||
*
|
||||
* @return {string?}
|
||||
*/
|
||||
replacingEventId() {
|
||||
return this._replacingEvent && this._replacingEvent.getId();
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the event replacing the content of this event, if any.
|
||||
*
|
||||
* @return {MatrixEvent?}
|
||||
*/
|
||||
replacingEvent() {
|
||||
return this._replacingEvent;
|
||||
},
|
||||
|
||||
/**
|
||||
* Summarise the event as JSON for debugging. If encrypted, include both the
|
||||
* decrypted and encrypted view of the event. This is named `toJSON` for use
|
||||
* with `JSON.stringify` which checks objects for functions named `toJSON`
|
||||
* and will call them to customise the output if they are defined.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
toJSON() {
|
||||
const event = {
|
||||
type: this.getType(),
|
||||
sender: this.getSender(),
|
||||
content: this.getContent(),
|
||||
event_id: this.getId(),
|
||||
origin_server_ts: this.getTs(),
|
||||
unsigned: this.getUnsigned(),
|
||||
room_id: this.getRoomId(),
|
||||
};
|
||||
|
||||
if (!this.isEncrypted()) {
|
||||
return event;
|
||||
}
|
||||
|
||||
return {
|
||||
decrypted: event,
|
||||
encrypted: this.event,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import EventEmitter from 'events';
|
||||
import { EventStatus } from '../../lib/models/event';
|
||||
|
||||
/**
|
||||
* A container for relation events that supports easy access to common ways of
|
||||
* aggregating such events. Each instance holds events that of a single relation
|
||||
* type and event type. All of the events also relate to the same original event.
|
||||
*
|
||||
* The typical way to get one of these containers is via
|
||||
* EventTimelineSet#getRelationsForEvent.
|
||||
*/
|
||||
export default class Relations extends EventEmitter {
|
||||
/**
|
||||
* @param {String} relationType
|
||||
* The type of relation involved, such as "m.annotation", "m.reference",
|
||||
* "m.replace", etc.
|
||||
* @param {String} eventType
|
||||
* The relation event's type, such as "m.reaction", etc.
|
||||
* @param {?Room} room
|
||||
* Room for this container. May be null for non-room cases, such as the
|
||||
* notification timeline.
|
||||
*/
|
||||
constructor(relationType, eventType, room) {
|
||||
super();
|
||||
this.relationType = relationType;
|
||||
this.eventType = eventType;
|
||||
this._relations = new Set();
|
||||
this._annotationsByKey = {};
|
||||
this._annotationsBySender = {};
|
||||
this._sortedAnnotationsByKey = [];
|
||||
this._targetEvent = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add relation events to this collection.
|
||||
*
|
||||
* @param {MatrixEvent} event
|
||||
* The new relation event to be added.
|
||||
*/
|
||||
addEvent(event) {
|
||||
if (this._relations.has(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relation = event.getRelation();
|
||||
if (!relation) {
|
||||
console.error("Event must have relation info");
|
||||
return;
|
||||
}
|
||||
|
||||
const relationType = relation.rel_type;
|
||||
const eventType = event.getType();
|
||||
|
||||
if (this.relationType !== relationType || this.eventType !== eventType) {
|
||||
console.error("Event relation info doesn't match this container");
|
||||
return;
|
||||
}
|
||||
|
||||
// If the event is in the process of being sent, listen for cancellation
|
||||
// so we can remove the event from the collection.
|
||||
if (event.isSending()) {
|
||||
event.on("Event.status", this._onEventStatus);
|
||||
}
|
||||
|
||||
this._relations.add(event);
|
||||
|
||||
if (this.relationType === "m.annotation") {
|
||||
this._addAnnotationToAggregation(event);
|
||||
} else if (this.relationType === "m.replace" && this._targetEvent) {
|
||||
this._targetEvent.makeReplaced(this.getLastReplacement());
|
||||
}
|
||||
|
||||
event.on("Event.beforeRedaction", this._onBeforeRedaction);
|
||||
|
||||
this.emit("Relations.add", event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove relation event from this collection.
|
||||
*
|
||||
* @param {MatrixEvent} event
|
||||
* The relation event to remove.
|
||||
*/
|
||||
_removeEvent(event) {
|
||||
if (!this._relations.has(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relation = event.getRelation();
|
||||
if (!relation) {
|
||||
console.error("Event must have relation info");
|
||||
return;
|
||||
}
|
||||
|
||||
const relationType = relation.rel_type;
|
||||
const eventType = event.getType();
|
||||
|
||||
if (this.relationType !== relationType || this.eventType !== eventType) {
|
||||
console.error("Event relation info doesn't match this container");
|
||||
return;
|
||||
}
|
||||
|
||||
this._relations.delete(event);
|
||||
|
||||
if (this.relationType === "m.annotation") {
|
||||
this._removeAnnotationFromAggregation(event);
|
||||
} else if (this.relationType === "m.replace" && this._targetEvent) {
|
||||
this._targetEvent.makeReplaced(this.getLastReplacement());
|
||||
}
|
||||
|
||||
this.emit("Relations.remove", event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens for event status changes to remove cancelled events.
|
||||
*
|
||||
* @param {MatrixEvent} event The event whose status has changed
|
||||
* @param {EventStatus} status The new status
|
||||
*/
|
||||
_onEventStatus = (event, status) => {
|
||||
if (!event.isSending()) {
|
||||
// Sending is done, so we don't need to listen anymore
|
||||
event.removeListener("Event.status", this._onEventStatus);
|
||||
return;
|
||||
}
|
||||
if (status !== EventStatus.CANCELLED) {
|
||||
return;
|
||||
}
|
||||
// Event was cancelled, remove from the collection
|
||||
event.removeListener("Event.status", this._onEventStatus);
|
||||
this._removeEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all relation events in this collection.
|
||||
*
|
||||
* These are currently in the order of insertion to this collection, which
|
||||
* won't match timeline order in the case of scrollback.
|
||||
* TODO: Tweak `addEvent` to insert correctly for scrollback.
|
||||
*
|
||||
* @return {Array}
|
||||
* Relation events in insertion order.
|
||||
*/
|
||||
getRelations() {
|
||||
return [...this._relations];
|
||||
}
|
||||
|
||||
_addAnnotationToAggregation(event) {
|
||||
const { key } = event.getRelation();
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
let eventsForKey = this._annotationsByKey[key];
|
||||
if (!eventsForKey) {
|
||||
eventsForKey = this._annotationsByKey[key] = new Set();
|
||||
this._sortedAnnotationsByKey.push([key, eventsForKey]);
|
||||
}
|
||||
// Add the new event to the set for this key
|
||||
eventsForKey.add(event);
|
||||
// Re-sort the [key, events] pairs in descending order of event count
|
||||
this._sortedAnnotationsByKey.sort((a, b) => {
|
||||
const aEvents = a[1];
|
||||
const bEvents = b[1];
|
||||
return bEvents.size - aEvents.size;
|
||||
});
|
||||
|
||||
const sender = event.getSender();
|
||||
let eventsFromSender = this._annotationsBySender[sender];
|
||||
if (!eventsFromSender) {
|
||||
eventsFromSender = this._annotationsBySender[sender] = new Set();
|
||||
}
|
||||
// Add the new event to the set for this sender
|
||||
eventsFromSender.add(event);
|
||||
}
|
||||
|
||||
_removeAnnotationFromAggregation(event) {
|
||||
const { key } = event.getRelation();
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventsForKey = this._annotationsByKey[key];
|
||||
if (eventsForKey) {
|
||||
eventsForKey.delete(event);
|
||||
|
||||
// Re-sort the [key, events] pairs in descending order of event count
|
||||
this._sortedAnnotationsByKey.sort((a, b) => {
|
||||
const aEvents = a[1];
|
||||
const bEvents = b[1];
|
||||
return bEvents.size - aEvents.size;
|
||||
});
|
||||
}
|
||||
|
||||
const sender = event.getSender();
|
||||
const eventsFromSender = this._annotationsBySender[sender];
|
||||
if (eventsFromSender) {
|
||||
eventsFromSender.delete(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For relations that have been redacted, we want to remove them from
|
||||
* aggregation data sets and emit an update event.
|
||||
*
|
||||
* To do so, we listen for `Event.beforeRedaction`, which happens:
|
||||
* - after the server accepted the redaction and remote echoed back to us
|
||||
* - before the original event has been marked redacted in the client
|
||||
*
|
||||
* @param {MatrixEvent} redactedEvent
|
||||
* The original relation event that is about to be redacted.
|
||||
*/
|
||||
_onBeforeRedaction = (redactedEvent) => {
|
||||
if (!this._relations.has(redactedEvent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._relations.delete(redactedEvent);
|
||||
|
||||
if (this.relationType === "m.annotation") {
|
||||
// Remove the redacted annotation from aggregation by key
|
||||
this._removeAnnotationFromAggregation(redactedEvent);
|
||||
} else if (this.relationType === "m.replace" && this._targetEvent) {
|
||||
this._targetEvent.makeReplaced(this.getLastReplacement());
|
||||
}
|
||||
|
||||
redactedEvent.removeListener("Event.beforeRedaction", this._onBeforeRedaction);
|
||||
|
||||
// Dispatch a redaction event on this collection. `setTimeout` is used
|
||||
// to wait until the next event loop iteration by which time the event
|
||||
// has actually been marked as redacted.
|
||||
setTimeout(() => {
|
||||
this.emit("Relations.redaction");
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all events in this collection grouped by key and sorted by descending
|
||||
* event count in each group.
|
||||
*
|
||||
* This is currently only supported for the annotation relation type.
|
||||
*
|
||||
* @return {Array}
|
||||
* An array of [key, events] pairs sorted by descending event count.
|
||||
* The events are stored in a Set (which preserves insertion order).
|
||||
*/
|
||||
getSortedAnnotationsByKey() {
|
||||
if (this.relationType !== "m.annotation") {
|
||||
// Other relation types are not grouped currently.
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._sortedAnnotationsByKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all events in this collection grouped by sender.
|
||||
*
|
||||
* This is currently only supported for the annotation relation type.
|
||||
*
|
||||
* @return {Object}
|
||||
* An object with each relation sender as a key and the matching Set of
|
||||
* events for that sender as a value.
|
||||
*/
|
||||
getAnnotationsBySender() {
|
||||
if (this.relationType !== "m.annotation") {
|
||||
// Other relation types are not grouped currently.
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._annotationsBySender;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the most recent (and allowed) m.replace relation, if any.
|
||||
*
|
||||
* This is currently only supported for the m.replace relation type,
|
||||
* once the target event is known, see `addEvent`.
|
||||
*
|
||||
* @return {MatrixEvent?}
|
||||
*/
|
||||
getLastReplacement() {
|
||||
if (this.relationType !== "m.replace") {
|
||||
// Aggregating on last only makes sense for this relation type
|
||||
return null;
|
||||
}
|
||||
if (!this._targetEvent) {
|
||||
// Don't know which replacements to accept yet.
|
||||
// This method shouldn't be called before the original
|
||||
// event is known anyway.
|
||||
return null;
|
||||
}
|
||||
return this.getRelations().reduce((last, event) => {
|
||||
if (event.getSender() !== this._targetEvent.getSender()) {
|
||||
return last;
|
||||
}
|
||||
if (last && last.getTs() > event.getTs()) {
|
||||
return last;
|
||||
}
|
||||
return event;
|
||||
}, null);
|
||||
}
|
||||
|
||||
/*
|
||||
* @param {MatrixEvent} targetEvent the event the relations are related to.
|
||||
*/
|
||||
setTargetEvent(event) {
|
||||
if (this._targetEvent) {
|
||||
return;
|
||||
}
|
||||
this._targetEvent = event;
|
||||
if (this.relationType === "m.replace") {
|
||||
const replacement = this.getLastReplacement();
|
||||
// this is the initial update, so only call it if we already have something
|
||||
// to not emit Event.replaced needlessly
|
||||
if (replacement) {
|
||||
this._targetEvent.makeReplaced(replacement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
-8
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -38,7 +38,7 @@ import ReEmitter from '../ReEmitter';
|
||||
// to upgrade (ie: "stable"). Eventually, we should remove these when all homeservers
|
||||
// return an m.room_versions capability.
|
||||
const KNOWN_SAFE_ROOM_VERSION = '1';
|
||||
const SAFE_ROOM_VERSIONS = ['1', '2'];
|
||||
const SAFE_ROOM_VERSIONS = ['1', '2', '3'];
|
||||
|
||||
function synthesizeReceipt(userId, event, receiptType) {
|
||||
// console.log("synthesizing receipt for "+event.getId());
|
||||
@@ -92,9 +92,12 @@ function synthesizeReceipt(userId, event, receiptType) {
|
||||
* "<b>detached</b>", pending messages will appear in a separate list,
|
||||
* accessbile via {@link module:models/room#getPendingEvents}. Default:
|
||||
* "chronological".
|
||||
*
|
||||
* @param {boolean} [opts.timelineSupport = false] Set to true to enable improved
|
||||
* timeline support.
|
||||
* @param {boolean} [opts.unstableClientRelationAggregation = false]
|
||||
* Optional. Set to true to enable client-side aggregation of event relations
|
||||
* via `EventTimelineSet#getRelationsForEvent`.
|
||||
* This feature is currently unstable and the API may change without notice.
|
||||
*
|
||||
* @prop {string} roomId The ID of this room.
|
||||
* @prop {string} name The human-readable display name for this room.
|
||||
@@ -496,7 +499,7 @@ Room.prototype.loadMembersIfNeeded = function() {
|
||||
const inMemoryUpdate = this._loadMembers().then((result) => {
|
||||
this.currentState.setOutOfBandMembers(result.memberEvents);
|
||||
// now the members are loaded, start to track the e2e devices if needed
|
||||
if (this._client.isRoomEncrypted(this.roomId)) {
|
||||
if (this._client.isCryptoEnabled() && this._client.isRoomEncrypted(this.roomId)) {
|
||||
this._client._crypto.trackRoomDevices(this.roomId);
|
||||
}
|
||||
return result.fromServer;
|
||||
@@ -598,6 +601,11 @@ Room.prototype._fixUpLegacyTimelineFields = function() {
|
||||
|
||||
/**
|
||||
* Returns whether there are any devices in the room that are unverified
|
||||
*
|
||||
* Note: Callers should first check if crypto is enabled on this device. If it is
|
||||
* disabled, then we aren't tracking room devices at all, so we can't answer this, and an
|
||||
* error will be thrown.
|
||||
*
|
||||
* @return {bool} the result
|
||||
*/
|
||||
Room.prototype.hasUnverifiedDevices = async function() {
|
||||
@@ -996,7 +1004,6 @@ Room.prototype.removeFilteredTimelineSet = function(filter) {
|
||||
* @private
|
||||
*/
|
||||
Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
|
||||
let i;
|
||||
if (event.getType() === "m.room.redaction") {
|
||||
const redactId = event.event.redacts;
|
||||
|
||||
@@ -1030,7 +1037,7 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
|
||||
}
|
||||
|
||||
// add to our timeline sets
|
||||
for (i = 0; i < this._timelineSets.length; i++) {
|
||||
for (let i = 0; i < this._timelineSets.length; i++) {
|
||||
this._timelineSets[i].addLiveEvent(event, duplicateStrategy);
|
||||
}
|
||||
|
||||
@@ -1095,9 +1102,27 @@ Room.prototype.addPendingEvent = function(event, txnId) {
|
||||
if (this._opts.pendingEventOrdering == "detached") {
|
||||
if (this._pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) {
|
||||
console.warn("Setting event as NOT_SENT due to messages in the same state");
|
||||
event.status = EventStatus.NOT_SENT;
|
||||
event.setStatus(EventStatus.NOT_SENT);
|
||||
}
|
||||
this._pendingEventList.push(event);
|
||||
|
||||
if (event.isRelation()) {
|
||||
// For pending events, add them to the relations collection immediately.
|
||||
// (The alternate case below already covers this as part of adding to
|
||||
// the timeline set.)
|
||||
// TODO: We should consider whether this means it would be a better
|
||||
// design to lift the relations handling up to the room instead.
|
||||
for (let i = 0; i < this._timelineSets.length; i++) {
|
||||
const timelineSet = this._timelineSets[i];
|
||||
if (timelineSet.getFilter()) {
|
||||
if (this._filter.filterRoomTimeline([event]).length) {
|
||||
timelineSet.aggregateRelations(event);
|
||||
}
|
||||
} else {
|
||||
timelineSet.aggregateRelations(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < this._timelineSets.length; i++) {
|
||||
const timelineSet = this._timelineSets[i];
|
||||
@@ -1236,7 +1261,7 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
|
||||
newStatus);
|
||||
}
|
||||
|
||||
event.status = newStatus;
|
||||
event.setStatus(newStatus);
|
||||
|
||||
if (newStatus == EventStatus.SENT) {
|
||||
// update the event id
|
||||
|
||||
+60
-1
@@ -23,6 +23,37 @@ import {escapeRegExp, globToRegexp} from "./utils";
|
||||
|
||||
const RULEKINDS_IN_ORDER = ['override', 'content', 'room', 'sender', 'underride'];
|
||||
|
||||
// The default override rules to apply when calculating actions for an event. These
|
||||
// defaults apply under no other circumstances to avoid confusing the client with server
|
||||
// state. We do this for two reasons:
|
||||
// 1. Synapse is unlikely to send us the push rule in an incremental sync - see
|
||||
// https://github.com/matrix-org/synapse/pull/4867#issuecomment-481446072 for
|
||||
// more details.
|
||||
// 2. We often want to start using push rules ahead of the server supporting them,
|
||||
// and so we can put them here.
|
||||
const DEFAULT_OVERRIDE_RULES = [
|
||||
{
|
||||
// For homeservers which don't support MSC1930 yet
|
||||
rule_id: ".m.rule.tombstone",
|
||||
default: true,
|
||||
enabled: true,
|
||||
conditions: [
|
||||
{
|
||||
kind: "event_match",
|
||||
key: "type",
|
||||
pattern: "m.room.tombstone",
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Construct a Push Processor.
|
||||
* @constructor
|
||||
@@ -312,6 +343,33 @@ function PushProcessor(client) {
|
||||
return actionObj;
|
||||
};
|
||||
|
||||
const applyRuleDefaults = function(clientRuleset) {
|
||||
// Deep clone the object before we mutate it
|
||||
const ruleset = JSON.parse(JSON.stringify(clientRuleset));
|
||||
|
||||
if (!clientRuleset['global']) {
|
||||
clientRuleset['global'] = {};
|
||||
}
|
||||
if (!clientRuleset['global']['override']) {
|
||||
clientRuleset['global']['override'] = [];
|
||||
}
|
||||
|
||||
// Apply default overrides
|
||||
const globalOverrides = clientRuleset['global']['override'];
|
||||
for (const override of DEFAULT_OVERRIDE_RULES) {
|
||||
const existingRule = globalOverrides
|
||||
.find((r) => r.rule_id === override.rule_id);
|
||||
|
||||
if (!existingRule) {
|
||||
const ruleId = override.rule_id;
|
||||
console.warn(`Adding default global override for ${ruleId}`);
|
||||
globalOverrides.push(override);
|
||||
}
|
||||
}
|
||||
|
||||
return ruleset;
|
||||
};
|
||||
|
||||
this.ruleMatchesEvent = function(rule, ev) {
|
||||
let ret = true;
|
||||
for (let i = 0; i < rule.conditions.length; ++i) {
|
||||
@@ -331,7 +389,8 @@ function PushProcessor(client) {
|
||||
* @return {PushAction}
|
||||
*/
|
||||
this.actionsForEvent = function(ev) {
|
||||
return pushActionsForEventAndRulesets(ev, client.pushRules);
|
||||
const rules = applyRuleDefaults(client.pushRules);
|
||||
return pushActionsForEventAndRulesets(ev, rules);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+78
-25
@@ -15,9 +15,12 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable babel/no-invalid-this */
|
||||
|
||||
import Promise from 'bluebird';
|
||||
import {MemoryStore} from "./memory";
|
||||
import utils from "../utils";
|
||||
import {EventEmitter} from 'events';
|
||||
import LocalIndexedDBStoreBackend from "./indexeddb-local-backend.js";
|
||||
import RemoteIndexedDBStoreBackend from "./indexeddb-remote-backend.js";
|
||||
import User from "../models/user";
|
||||
@@ -110,6 +113,7 @@ const IndexedDBStore = function IndexedDBStore(opts) {
|
||||
};
|
||||
};
|
||||
utils.inherits(IndexedDBStore, MemoryStore);
|
||||
utils.extend(IndexedDBStore.prototype, EventEmitter.prototype);
|
||||
|
||||
IndexedDBStore.exists = function(indexedDB, dbName) {
|
||||
return LocalIndexedDBStoreBackend.exists(indexedDB, dbName);
|
||||
@@ -146,28 +150,28 @@ IndexedDBStore.prototype.startup = function() {
|
||||
* client state to where it was at the last save, or null if there
|
||||
* is no saved sync data.
|
||||
*/
|
||||
IndexedDBStore.prototype.getSavedSync = function() {
|
||||
IndexedDBStore.prototype.getSavedSync = degradable(function() {
|
||||
return this.backend.getSavedSync();
|
||||
};
|
||||
}, "getSavedSync");
|
||||
|
||||
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
|
||||
IndexedDBStore.prototype.isNewlyCreated = function() {
|
||||
IndexedDBStore.prototype.isNewlyCreated = degradable(function() {
|
||||
return this.backend.isNewlyCreated();
|
||||
};
|
||||
}, "isNewlyCreated");
|
||||
|
||||
/**
|
||||
* @return {Promise} If there is a saved sync, the nextBatch token
|
||||
* for this sync, otherwise null.
|
||||
*/
|
||||
IndexedDBStore.prototype.getSavedSyncToken = function() {
|
||||
IndexedDBStore.prototype.getSavedSyncToken = degradable(function() {
|
||||
return this.backend.getNextBatchToken();
|
||||
},
|
||||
}, "getSavedSyncToken"),
|
||||
|
||||
/**
|
||||
* Delete all data from this store.
|
||||
* @return {Promise} Resolves if the data was deleted from the database.
|
||||
*/
|
||||
IndexedDBStore.prototype.deleteAllData = function() {
|
||||
IndexedDBStore.prototype.deleteAllData = degradable(function() {
|
||||
MemoryStore.prototype.deleteAllData.call(this);
|
||||
return this.backend.clearDatabase().then(() => {
|
||||
console.log("Deleted indexeddb data.");
|
||||
@@ -175,7 +179,7 @@ IndexedDBStore.prototype.deleteAllData = function() {
|
||||
console.error(`Failed to delete indexeddb data: ${err}`);
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Whether this store would like to save its data
|
||||
@@ -203,7 +207,7 @@ IndexedDBStore.prototype.save = function() {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
IndexedDBStore.prototype._reallySave = function() {
|
||||
IndexedDBStore.prototype._reallySave = degradable(function() {
|
||||
this._syncTs = Date.now(); // set now to guard against multi-writes
|
||||
|
||||
// work out changed users (this doesn't handle deletions but you
|
||||
@@ -219,14 +223,12 @@ IndexedDBStore.prototype._reallySave = function() {
|
||||
this._userModifiedMap[u.userId] = u.getLastModifiedTime();
|
||||
}
|
||||
|
||||
return this.backend.syncToDatabase(userTuples).catch((err) => {
|
||||
console.error("sync fail:", err);
|
||||
});
|
||||
};
|
||||
return this.backend.syncToDatabase(userTuples);
|
||||
});
|
||||
|
||||
IndexedDBStore.prototype.setSyncData = function(syncData) {
|
||||
IndexedDBStore.prototype.setSyncData = degradable(function(syncData) {
|
||||
return this.backend.setSyncData(syncData);
|
||||
};
|
||||
}, "setSyncData");
|
||||
|
||||
/**
|
||||
* Returns the out-of-band membership events for this room that
|
||||
@@ -235,9 +237,9 @@ IndexedDBStore.prototype.setSyncData = function(syncData) {
|
||||
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
|
||||
* @returns {null} in case the members for this room haven't been stored yet
|
||||
*/
|
||||
IndexedDBStore.prototype.getOutOfBandMembers = function(roomId) {
|
||||
IndexedDBStore.prototype.getOutOfBandMembers = degradable(function(roomId) {
|
||||
return this.backend.getOutOfBandMembers(roomId);
|
||||
};
|
||||
}, "getOutOfBandMembers");
|
||||
|
||||
/**
|
||||
* Stores the out-of-band membership events for this room. Note that
|
||||
@@ -247,20 +249,71 @@ IndexedDBStore.prototype.getOutOfBandMembers = function(roomId) {
|
||||
* @param {event[]} membershipEvents the membership events to store
|
||||
* @returns {Promise} when all members have been stored
|
||||
*/
|
||||
IndexedDBStore.prototype.setOutOfBandMembers = function(roomId, membershipEvents) {
|
||||
IndexedDBStore.prototype.setOutOfBandMembers = degradable(function(
|
||||
roomId,
|
||||
membershipEvents,
|
||||
) {
|
||||
MemoryStore.prototype.setOutOfBandMembers.call(this, roomId, membershipEvents);
|
||||
return this.backend.setOutOfBandMembers(roomId, membershipEvents);
|
||||
};
|
||||
}, "setOutOfBandMembers");
|
||||
|
||||
IndexedDBStore.prototype.clearOutOfBandMembers = function(roomId) {
|
||||
IndexedDBStore.prototype.clearOutOfBandMembers = degradable(function(roomId) {
|
||||
MemoryStore.prototype.clearOutOfBandMembers.call(this);
|
||||
return this.backend.clearOutOfBandMembers(roomId);
|
||||
};
|
||||
}, "clearOutOfBandMembers");
|
||||
|
||||
IndexedDBStore.prototype.getClientOptions = function() {
|
||||
IndexedDBStore.prototype.getClientOptions = degradable(function() {
|
||||
return this.backend.getClientOptions();
|
||||
};
|
||||
}, "getClientOptions");
|
||||
|
||||
IndexedDBStore.prototype.storeClientOptions = function(options) {
|
||||
IndexedDBStore.prototype.storeClientOptions = degradable(function(options) {
|
||||
MemoryStore.prototype.storeClientOptions.call(this, options);
|
||||
return this.backend.storeClientOptions(options);
|
||||
};
|
||||
}, "storeClientOptions");
|
||||
|
||||
module.exports.IndexedDBStore = IndexedDBStore;
|
||||
|
||||
/**
|
||||
* All member functions of `IndexedDBStore` that access the backend use this wrapper to
|
||||
* watch for failures after initial store startup, including `QuotaExceededError` as
|
||||
* free disk space changes, etc.
|
||||
*
|
||||
* When IndexedDB fails via any of these paths, we degrade this back to a `MemoryStore`
|
||||
* in place so that the current operation and all future ones are in-memory only.
|
||||
*
|
||||
* @param {Function} func The degradable work to do.
|
||||
* @param {String} fallback The method name for fallback.
|
||||
* @returns {Function} A wrapped member function.
|
||||
*/
|
||||
function degradable(func, fallback) {
|
||||
return async function(...args) {
|
||||
try {
|
||||
return await func.call(this, ...args);
|
||||
} catch (e) {
|
||||
console.error("IndexedDBStore failure, degrading to MemoryStore", e);
|
||||
this.emit("degraded", e);
|
||||
try {
|
||||
// We try to delete IndexedDB after degrading since this store is only a
|
||||
// cache (the app will still function correctly without the data).
|
||||
// It's possible that deleting repair IndexedDB for the next app load,
|
||||
// potenially by making a little more space available.
|
||||
console.log("IndexedDBStore trying to delete degraded data");
|
||||
await this.backend.clearDatabase();
|
||||
console.log("IndexedDBStore delete after degrading succeeeded");
|
||||
} catch (e) {
|
||||
console.warn("IndexedDBStore delete after degrading failed", e);
|
||||
}
|
||||
// Degrade the store from being an instance of `IndexedDBStore` to instead be
|
||||
// an instance of `MemoryStore` so that future API calls use the memory path
|
||||
// directly and skip IndexedDB entirely. This should be safe as
|
||||
// `IndexedDBStore` already extends from `MemoryStore`, so we are making the
|
||||
// store become its parent type in a way. The mutator methods of
|
||||
// `IndexedDBStore` also maintain the state that `MemoryStore` uses (many are
|
||||
// not overridden at all).
|
||||
Object.setPrototypeOf(this, MemoryStore.prototype);
|
||||
if (fallback) {
|
||||
return await MemoryStore.prototype[fallback].call(this, ...args);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -385,6 +385,7 @@ module.exports.MemoryStore.prototype = {
|
||||
};
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the out-of-band membership events for this room that
|
||||
* were previously loaded.
|
||||
@@ -395,6 +396,7 @@ module.exports.MemoryStore.prototype = {
|
||||
getOutOfBandMembers: function(roomId) {
|
||||
return Promise.resolve(this._oobMembers[roomId] || null);
|
||||
},
|
||||
|
||||
/**
|
||||
* Stores the out-of-band membership events for this room. Note that
|
||||
* it still makes sense to store an empty array as the OOB status for the room is
|
||||
@@ -408,6 +410,11 @@ module.exports.MemoryStore.prototype = {
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
clearOutOfBandMembers: function() {
|
||||
this._oobMembers = {};
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
getClientOptions: function() {
|
||||
return Promise.resolve(this._clientOptions);
|
||||
},
|
||||
|
||||
+79
-15
@@ -116,10 +116,15 @@ function SyncApi(client, opts) {
|
||||
*/
|
||||
SyncApi.prototype.createRoom = function(roomId) {
|
||||
const client = this.client;
|
||||
const {
|
||||
timelineSupport,
|
||||
unstableClientRelationAggregation,
|
||||
} = client;
|
||||
const room = new Room(roomId, client, client.getUserId(), {
|
||||
lazyLoadMembers: this.opts.lazyLoadMembers,
|
||||
pendingEventOrdering: this.opts.pendingEventOrdering,
|
||||
timelineSupport: client.timelineSupport,
|
||||
timelineSupport,
|
||||
unstableClientRelationAggregation,
|
||||
});
|
||||
client.reEmitter.reEmit(room, ["Room.name", "Room.timeline", "Room.redaction",
|
||||
"Room.receipt", "Room.tags",
|
||||
@@ -127,6 +132,7 @@ SyncApi.prototype.createRoom = function(roomId) {
|
||||
"Room.localEchoUpdated",
|
||||
"Room.accountData",
|
||||
"Room.myMembership",
|
||||
"Room.replaceEvent",
|
||||
]);
|
||||
this._registerStateListeners(room);
|
||||
return room;
|
||||
@@ -445,6 +451,16 @@ SyncApi.prototype._wasLazyLoadingToggled = async function(lazyLoadMembers) {
|
||||
return false;
|
||||
};
|
||||
|
||||
SyncApi.prototype._shouldAbortSync = function(error) {
|
||||
if (error.errcode === "M_UNKNOWN_TOKEN") {
|
||||
// The logout already happened, we just need to stop.
|
||||
console.warn("Token no longer valid - assuming logout");
|
||||
this.stop();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Main entry point
|
||||
*/
|
||||
@@ -472,13 +488,17 @@ SyncApi.prototype.sync = function() {
|
||||
|
||||
async function getPushRules() {
|
||||
try {
|
||||
debuglog("Getting push rules...");
|
||||
const result = await client.getPushRules();
|
||||
debuglog("Got push rules");
|
||||
|
||||
client.pushRules = result;
|
||||
} catch (err) {
|
||||
console.error("Getting push rules failed", err);
|
||||
if (self._shouldAbortSync(err)) return;
|
||||
// wait for saved sync to complete before doing anything else,
|
||||
// otherwise the sync state will end up being incorrect
|
||||
debuglog("Waiting for saved sync before retrying push rules...");
|
||||
await self.recoverFromSyncStartupError(savedSyncPromise, err);
|
||||
getPushRules();
|
||||
return;
|
||||
@@ -487,22 +507,35 @@ SyncApi.prototype.sync = function() {
|
||||
}
|
||||
|
||||
const checkLazyLoadStatus = async () => {
|
||||
debuglog("Checking lazy load status...");
|
||||
if (this.opts.lazyLoadMembers && client.isGuest()) {
|
||||
this.opts.lazyLoadMembers = false;
|
||||
}
|
||||
if (this.opts.lazyLoadMembers) {
|
||||
debuglog("Checking server lazy load support...");
|
||||
const supported = await client.doesServerSupportLazyLoading();
|
||||
if (supported) {
|
||||
this.opts.filter = await client.createFilter(
|
||||
Filter.LAZY_LOADING_SYNC_FILTER,
|
||||
);
|
||||
try {
|
||||
debuglog("Creating and storing lazy load sync filter...");
|
||||
this.opts.filter = await client.createFilter(
|
||||
Filter.LAZY_LOADING_SYNC_FILTER,
|
||||
);
|
||||
debuglog("Created and stored lazy load sync filter");
|
||||
} catch (err) {
|
||||
console.error(
|
||||
"Creating and storing lazy load sync filter failed",
|
||||
err,
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
console.log("LL: lazy loading requested but not supported " +
|
||||
debuglog("LL: lazy loading requested but not supported " +
|
||||
"by server, so disabling");
|
||||
this.opts.lazyLoadMembers = false;
|
||||
}
|
||||
}
|
||||
// need to vape the store when enabling LL and wasn't enabled before
|
||||
debuglog("Checking whether lazy loading has changed in store...");
|
||||
const shouldClear = await this._wasLazyLoadingToggled(this.opts.lazyLoadMembers);
|
||||
if (shouldClear) {
|
||||
this._storeIsInvalid = true;
|
||||
@@ -519,12 +552,20 @@ SyncApi.prototype.sync = function() {
|
||||
if (this.opts.lazyLoadMembers && this.opts.crypto) {
|
||||
this.opts.crypto.enableLazyLoading();
|
||||
}
|
||||
await this.client._storeClientOptions();
|
||||
try {
|
||||
debuglog("Storing client options...");
|
||||
await this.client._storeClientOptions();
|
||||
debuglog("Stored client options");
|
||||
} catch (err) {
|
||||
console.error("Storing client options failed", err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
getFilter(); // Now get the filter and start syncing
|
||||
};
|
||||
|
||||
async function getFilter() {
|
||||
debuglog("Getting filter...");
|
||||
let filter;
|
||||
if (self.opts.filter) {
|
||||
filter = self.opts.filter;
|
||||
@@ -539,8 +580,11 @@ SyncApi.prototype.sync = function() {
|
||||
getFilterName(client.credentials.userId), filter,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Getting filter failed", err);
|
||||
if (self._shouldAbortSync(err)) return;
|
||||
// wait for saved sync to complete before doing anything else,
|
||||
// otherwise the sync state will end up being incorrect
|
||||
debuglog("Waiting for saved sync before retrying filter...");
|
||||
await self.recoverFromSyncStartupError(savedSyncPromise, err);
|
||||
getFilter();
|
||||
return;
|
||||
@@ -554,11 +598,12 @@ SyncApi.prototype.sync = function() {
|
||||
if (self._currentSyncRequest === null) {
|
||||
// Send this first sync request here so we can then wait for the saved
|
||||
// sync data to finish processing before we process the results of this one.
|
||||
console.log("Sending first sync request...");
|
||||
debuglog("Sending first sync request...");
|
||||
self._currentSyncRequest = self._doSyncRequest({ filterId }, savedSyncToken);
|
||||
}
|
||||
|
||||
// Now wait for the saved sync to finish...
|
||||
debuglog("Waiting for saved sync before starting sync processing...");
|
||||
await savedSyncPromise;
|
||||
self._sync({ filterId });
|
||||
}
|
||||
@@ -570,13 +615,19 @@ SyncApi.prototype.sync = function() {
|
||||
// Pull the saved sync token out first, before the worker starts sending
|
||||
// all the sync data which could take a while. This will let us send our
|
||||
// first incremental sync request before we've processed our saved data.
|
||||
debuglog("Getting saved sync token...");
|
||||
savedSyncPromise = client.store.getSavedSyncToken().then((tok) => {
|
||||
debuglog("Got saved sync token");
|
||||
savedSyncToken = tok;
|
||||
debuglog("Getting saved sync...");
|
||||
return client.store.getSavedSync();
|
||||
}).then((savedSync) => {
|
||||
debuglog(`Got reply from saved sync, exists? ${!!savedSync}`);
|
||||
if (savedSync) {
|
||||
return self._syncFromCache(savedSync);
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error("Getting saved sync failed", err);
|
||||
});
|
||||
// Now start the first incremental sync request: this can also
|
||||
// take a while so if we set it going now, we can wait for it
|
||||
@@ -840,6 +891,10 @@ SyncApi.prototype._onSyncError = function(err, syncOptions) {
|
||||
console.error("/sync error %s", err);
|
||||
console.error(err);
|
||||
|
||||
if(this._shouldAbortSync(err)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._failedSyncCount++;
|
||||
console.log('Number of consecutive failed sync requests:', this._failedSyncCount);
|
||||
|
||||
@@ -1051,7 +1106,6 @@ SyncApi.prototype._processSyncResponse = async function(
|
||||
const stateEvents =
|
||||
self._mapSyncEventsFormat(inviteObj.invite_state, room);
|
||||
|
||||
room.updateMyMembership("invite");
|
||||
self._processRoomEvents(room, stateEvents);
|
||||
if (inviteObj.isBrandNewRoom) {
|
||||
room.recalculate();
|
||||
@@ -1061,6 +1115,7 @@ SyncApi.prototype._processSyncResponse = async function(
|
||||
stateEvents.forEach(function(e) {
|
||||
client.emit("event", e);
|
||||
});
|
||||
room.updateMyMembership("invite");
|
||||
});
|
||||
|
||||
// Handle joins
|
||||
@@ -1076,12 +1131,19 @@ SyncApi.prototype._processSyncResponse = async function(
|
||||
room.setUnreadNotificationCount(
|
||||
'total', joinObj.unread_notifications.notification_count,
|
||||
);
|
||||
room.setUnreadNotificationCount(
|
||||
'highlight', joinObj.unread_notifications.highlight_count,
|
||||
);
|
||||
}
|
||||
|
||||
room.updateMyMembership("join");
|
||||
// We track unread notifications ourselves in encrypted rooms, so don't
|
||||
// bother setting it here. We trust our calculations better than the
|
||||
// server's for this case, and therefore will assume that our non-zero
|
||||
// count is accurate.
|
||||
const encrypted = client.isRoomEncrypted(room.roomId);
|
||||
if (!encrypted
|
||||
|| (encrypted && room.getUnreadNotificationCount('highlight') <= 0)) {
|
||||
room.setUnreadNotificationCount(
|
||||
'highlight', joinObj.unread_notifications.highlight_count,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
joinObj.timeline = joinObj.timeline || {};
|
||||
|
||||
@@ -1195,6 +1257,8 @@ SyncApi.prototype._processSyncResponse = async function(
|
||||
accountDataEvents.forEach(function(e) {
|
||||
client.emit("event", e);
|
||||
});
|
||||
|
||||
room.updateMyMembership("join");
|
||||
});
|
||||
|
||||
// Handle leaves (e.g. kicked rooms)
|
||||
@@ -1207,8 +1271,6 @@ SyncApi.prototype._processSyncResponse = async function(
|
||||
const accountDataEvents =
|
||||
self._mapSyncEventsFormat(leaveObj.account_data);
|
||||
|
||||
room.updateMyMembership("leave");
|
||||
|
||||
self._processRoomEvents(room, stateEvents, timelineEvents);
|
||||
room.addAccountData(accountDataEvents);
|
||||
|
||||
@@ -1229,6 +1291,8 @@ SyncApi.prototype._processSyncResponse = async function(
|
||||
accountDataEvents.forEach(function(e) {
|
||||
client.emit("event", e);
|
||||
});
|
||||
|
||||
room.updateMyMembership("leave");
|
||||
});
|
||||
|
||||
// update the notification timeline, if appropriate.
|
||||
|
||||
Reference in New Issue
Block a user