1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-26 17:03:12 +03:00

Merge remote-tracking branch 'origin/develop' into dbkr/cross_signing

This commit is contained in:
David Baker
2019-10-28 16:47:16 +00:00
47 changed files with 2968 additions and 862 deletions

View File

@@ -1,3 +1,298 @@
Changes in [2.4.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.2) (2019-10-18)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.2-rc.1...v2.4.2)
* No changes since v2.4.2-rc.1
Changes in [2.4.2-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.2-rc.1) (2019-10-09)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.1...v2.4.2-rc.1)
* Log state of Olm sessions
[\#1047](https://github.com/matrix-org/matrix-js-sdk/pull/1047)
* Add method to get access to all timelines
[\#1048](https://github.com/matrix-org/matrix-js-sdk/pull/1048)
Changes in [2.4.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.1) (2019-10-01)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.0...v2.4.1)
* Upgrade deps
[\#1046](https://github.com/matrix-org/matrix-js-sdk/pull/1046)
* Ignore crypto events with no content
[\#1043](https://github.com/matrix-org/matrix-js-sdk/pull/1043)
Changes in [2.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.0) (2019-09-27)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.0-rc.1...v2.4.0)
* Clean Yarn cache during release
[\#1045](https://github.com/matrix-org/matrix-js-sdk/pull/1045)
Changes in [2.4.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.0-rc.1) (2019-09-25)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.3.2...v2.4.0-rc.1)
* Remove id_server from creds for interactive auth
[\#1044](https://github.com/matrix-org/matrix-js-sdk/pull/1044)
* Remove IS details from requestToken to HS
[\#1041](https://github.com/matrix-org/matrix-js-sdk/pull/1041)
* Add support for sending MSISDN tokens to alternate URLs
[\#1040](https://github.com/matrix-org/matrix-js-sdk/pull/1040)
* Add separate 3PID add and bind APIs
[\#1038](https://github.com/matrix-org/matrix-js-sdk/pull/1038)
* Bump eslint-utils from 1.4.0 to 1.4.2
[\#1037](https://github.com/matrix-org/matrix-js-sdk/pull/1037)
* Handle WebRTC security errors as non-fatal
[\#1036](https://github.com/matrix-org/matrix-js-sdk/pull/1036)
* Check for r0.6.0 support in addition to unstable feature flags
[\#1035](https://github.com/matrix-org/matrix-js-sdk/pull/1035)
* Update room members on member event redaction
[\#1030](https://github.com/matrix-org/matrix-js-sdk/pull/1030)
* Support hidden read receipts
[\#1028](https://github.com/matrix-org/matrix-js-sdk/pull/1028)
* Do 3pid lookups in lowercase
[\#1029](https://github.com/matrix-org/matrix-js-sdk/pull/1029)
* Add Synapse admin functions for deactivating a user
[\#1027](https://github.com/matrix-org/matrix-js-sdk/pull/1027)
* Fix addPendingEvent with pending event order == chronological
[\#1026](https://github.com/matrix-org/matrix-js-sdk/pull/1026)
* Add AutoDiscovery.getRawClientConfig() for easy .well-known lookups
[\#1024](https://github.com/matrix-org/matrix-js-sdk/pull/1024)
* Don't convert errors to JSON if they are JSON already
[\#1025](https://github.com/matrix-org/matrix-js-sdk/pull/1025)
* Send id_access_token to HS for use in proxied IS requests
[\#1022](https://github.com/matrix-org/matrix-js-sdk/pull/1022)
* Clean up JSON handling in identity server requests
[\#1023](https://github.com/matrix-org/matrix-js-sdk/pull/1023)
* Use the v2 (hashed) lookup for identity server queries
[\#1021](https://github.com/matrix-org/matrix-js-sdk/pull/1021)
* Add getIdServer() & doesServerRequireIdServerParam()
[\#1018](https://github.com/matrix-org/matrix-js-sdk/pull/1018)
* Make requestToken endpoints work without ID Server
[\#1019](https://github.com/matrix-org/matrix-js-sdk/pull/1019)
* Fix setIdentityServer
[\#1016](https://github.com/matrix-org/matrix-js-sdk/pull/1016)
* Change ICE fallback server and make fallback opt-in
[\#1015](https://github.com/matrix-org/matrix-js-sdk/pull/1015)
* Throw an exception if trying to do an ID server request with no ID server
[\#1014](https://github.com/matrix-org/matrix-js-sdk/pull/1014)
* Add setIdentityServerUrl
[\#1013](https://github.com/matrix-org/matrix-js-sdk/pull/1013)
* Add matrix base API to report an event
[\#1011](https://github.com/matrix-org/matrix-js-sdk/pull/1011)
* Fix POST body for v2 IS requests
[\#1010](https://github.com/matrix-org/matrix-js-sdk/pull/1010)
* Add API for bulk lookup on the Identity Server
[\#1009](https://github.com/matrix-org/matrix-js-sdk/pull/1009)
* Remove deprecated authedRequestWithPrefix and requestWithPrefix
[\#1000](https://github.com/matrix-org/matrix-js-sdk/pull/1000)
* Add API for checking IS account info
[\#1007](https://github.com/matrix-org/matrix-js-sdk/pull/1007)
* Support rewriting push rules when our internal defaults change
[\#1006](https://github.com/matrix-org/matrix-js-sdk/pull/1006)
* Upgrade dependencies
[\#1005](https://github.com/matrix-org/matrix-js-sdk/pull/1005)
Changes in [2.3.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.3.2) (2019-09-16)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.3.2-rc.1...v2.3.2)
* [Release] Fix addPendingEvent with pending event order == chronological
[\#1034](https://github.com/matrix-org/matrix-js-sdk/pull/1034)
Changes in [2.3.2-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.3.2-rc.1) (2019-09-13)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.3.1...v2.3.2-rc.1)
* Synapse admin functions to release
[\#1033](https://github.com/matrix-org/matrix-js-sdk/pull/1033)
* [To Release] Add matrix base API to report an event
[\#1032](https://github.com/matrix-org/matrix-js-sdk/pull/1032)
Changes in [2.3.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.3.1) (2019-09-12)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.3.1-rc.1...v2.3.1)
* No changes since rc.1
Changes in [2.3.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.3.1-rc.1) (2019-09-11)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.3.0...v2.3.1-rc.1)
* Update room members on member event redaction
[\#1031](https://github.com/matrix-org/matrix-js-sdk/pull/1031)
Changes in [2.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.3.0) (2019-08-05)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.3.0-rc.1...v2.3.0)
* [release] Support rewriting push rules when our internal defaults change
[\#1008](https://github.com/matrix-org/matrix-js-sdk/pull/1008)
Changes in [2.3.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.3.0-rc.1) (2019-07-31)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.2.0...v2.3.0-rc.1)
* Add support for IS v2 API with authentication
[\#1002](https://github.com/matrix-org/matrix-js-sdk/pull/1002)
* Tombstone bugfixes
[\#1001](https://github.com/matrix-org/matrix-js-sdk/pull/1001)
* Support for MSC2140 (terms of service for IS/IM)
[\#988](https://github.com/matrix-org/matrix-js-sdk/pull/988)
* Add a request method to /devices
[\#994](https://github.com/matrix-org/matrix-js-sdk/pull/994)
Changes in [2.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.2.0) (2019-07-18)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.2.0-rc.2...v2.2.0)
* Upgrade lodash dependencies
Changes in [2.2.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.2.0-rc.2) (2019-07-12)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.2.0-rc.1...v2.2.0-rc.2)
* Fix regression from 2.2.0-rc.1 in request to /devices
[\#995](https://github.com/matrix-org/matrix-js-sdk/pull/995)
Changes in [2.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.2.0-rc.1) (2019-07-12)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.1.1...v2.2.0-rc.1)
* End the verification timer when verification is done
[\#993](https://github.com/matrix-org/matrix-js-sdk/pull/993)
* Stabilize usage of stably stable APIs (in a stable way)
[\#990](https://github.com/matrix-org/matrix-js-sdk/pull/990)
* Expose original_event for /relations
[\#987](https://github.com/matrix-org/matrix-js-sdk/pull/987)
* Process ephemeral events outside timeline handling
[\#989](https://github.com/matrix-org/matrix-js-sdk/pull/989)
* Don't accept any locally known edits earlier than the last known server-side
aggregated edit
[\#986](https://github.com/matrix-org/matrix-js-sdk/pull/986)
* Get edit date transparently from server aggregations or local echo
[\#984](https://github.com/matrix-org/matrix-js-sdk/pull/984)
* Add a function to flag keys for backup without scheduling a backup
[\#982](https://github.com/matrix-org/matrix-js-sdk/pull/982)
* Block read marker and read receipt from advancing into pending events
[\#981](https://github.com/matrix-org/matrix-js-sdk/pull/981)
* Upgrade dependencies
[\#977](https://github.com/matrix-org/matrix-js-sdk/pull/977)
* Add default push rule to ignore reactions
[\#976](https://github.com/matrix-org/matrix-js-sdk/pull/976)
* Fix exception whilst syncing
[\#979](https://github.com/matrix-org/matrix-js-sdk/pull/979)
* Include the error object when raising Session.logged_out
[\#975](https://github.com/matrix-org/matrix-js-sdk/pull/975)
Changes in [2.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.1.1) (2019-07-11)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.1.0...v2.1.1)
* Process emphemeral events outside timeline handling
[\#989](https://github.com/matrix-org/matrix-js-sdk/pull/989)
Changes in [2.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.1.0) (2019-07-08)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.1.0-rc.1...v2.1.0)
* Fix exception whilst syncing
[\#979](https://github.com/matrix-org/matrix-js-sdk/pull/979)
Changes in [2.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.1.0-rc.1) (2019-07-03)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.0.1...v2.1.0-rc.1)
* Handle self read receipts for fixing e2e notification counts
[\#974](https://github.com/matrix-org/matrix-js-sdk/pull/974)
* Add redacts field to event.toJSON
[\#973](https://github.com/matrix-org/matrix-js-sdk/pull/973)
* Handle associated event send failures
[\#972](https://github.com/matrix-org/matrix-js-sdk/pull/972)
* Remove irrelevant debug line from timeline handling
[\#971](https://github.com/matrix-org/matrix-js-sdk/pull/971)
* Handle relations in encrypted rooms
[\#969](https://github.com/matrix-org/matrix-js-sdk/pull/969)
* Relations endpoint support
[\#967](https://github.com/matrix-org/matrix-js-sdk/pull/967)
* Disable event encryption for reactions
[\#968](https://github.com/matrix-org/matrix-js-sdk/pull/968)
* Change the known safe room version to version 4
[\#966](https://github.com/matrix-org/matrix-js-sdk/pull/966)
* Check for lazy-loading support in the spec versions instead
[\#965](https://github.com/matrix-org/matrix-js-sdk/pull/965)
* Use camelCase instead of underscore
[\#963](https://github.com/matrix-org/matrix-js-sdk/pull/963)
* Time out verification attempts after 10 minutes of inactivity
[\#961](https://github.com/matrix-org/matrix-js-sdk/pull/961)
* Don't handle key verification requests which are immediately cancelled
[\#962](https://github.com/matrix-org/matrix-js-sdk/pull/962)
Changes in [2.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.0.1) (2019-06-19)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.0.1-rc.2...v2.0.1)
No changes since rc.2
Changes in [2.0.1-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.0.1-rc.2) (2019-06-18)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.0.1-rc.1...v2.0.1-rc.2)
* return 'sending' status for an event that is only locally redacted
[\#960](https://github.com/matrix-org/matrix-js-sdk/pull/960)
* Key verification request fixes
[\#954](https://github.com/matrix-org/matrix-js-sdk/pull/954)
* Add flag to force saving sync store
[\#956](https://github.com/matrix-org/matrix-js-sdk/pull/956)
* Expose the inhibit_login flag to register
[\#953](https://github.com/matrix-org/matrix-js-sdk/pull/953)
* Support redactions and relations of/with unsent events.
[\#947](https://github.com/matrix-org/matrix-js-sdk/pull/947)
Changes in [2.0.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.0.1-rc.1) (2019-06-12)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.0.0...v2.0.1-rc.1)
* Fix content uploads for modern browsers
[\#952](https://github.com/matrix-org/matrix-js-sdk/pull/952)
* Don't overlap auth submissions with polls
[\#951](https://github.com/matrix-org/matrix-js-sdk/pull/951)
* Add funding details for GitHub sponsor button
[\#945](https://github.com/matrix-org/matrix-js-sdk/pull/945)
* Fix backup sig validation with multiple sigs
[\#944](https://github.com/matrix-org/matrix-js-sdk/pull/944)
* Don't send another token request while one's in flight
[\#943](https://github.com/matrix-org/matrix-js-sdk/pull/943)
* Don't poll UI auth again until current poll finishes
[\#942](https://github.com/matrix-org/matrix-js-sdk/pull/942)
* Provide the discovered URLs when a liveliness error occurs
[\#938](https://github.com/matrix-org/matrix-js-sdk/pull/938)
* Encode event IDs when redacting events
[\#941](https://github.com/matrix-org/matrix-js-sdk/pull/941)
* add missing logger
[\#940](https://github.com/matrix-org/matrix-js-sdk/pull/940)
* verification: don't error if we don't know about some keys
[\#939](https://github.com/matrix-org/matrix-js-sdk/pull/939)
* Local echo for redactions
[\#937](https://github.com/matrix-org/matrix-js-sdk/pull/937)
* Refresh safe room versions when the server looks more modern than us
[\#934](https://github.com/matrix-org/matrix-js-sdk/pull/934)
* Add v4 as a safe room version
[\#935](https://github.com/matrix-org/matrix-js-sdk/pull/935)
* Disable guard-for-in rule
[\#933](https://github.com/matrix-org/matrix-js-sdk/pull/933)
* Extend loglevel logging for the whole project
[\#924](https://github.com/matrix-org/matrix-js-sdk/pull/924)
* fix(login): saves access_token and user_id after login for all login types
[\#930](https://github.com/matrix-org/matrix-js-sdk/pull/930)
* Do not try to request thumbnails with non-integer sizes
[\#929](https://github.com/matrix-org/matrix-js-sdk/pull/929)
* Revert "Add a bunch of debugging to .well-known IS validation"
[\#928](https://github.com/matrix-org/matrix-js-sdk/pull/928)
* Add a bunch of debugging to .well-known IS validation
[\#927](https://github.com/matrix-org/matrix-js-sdk/pull/927)
Changes in [2.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.0.0) (2019-05-31) 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) [Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.2.0...v2.0.0)

View File

@@ -322,13 +322,13 @@ To provide the Olm library in a browser application:
To provide the Olm library in a node.js application: To provide the Olm library in a node.js application:
* ``yarn add https://packages.matrix.org/npm/olm/olm-3.0.0.tgz`` * ``yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz``
(replace the URL with the latest version you want to use from (replace the URL with the latest version you want to use from
https://packages.matrix.org/npm/olm/) https://packages.matrix.org/npm/olm/)
* ``global.Olm = require('olm');`` *before* loading ``matrix-js-sdk``. * ``global.Olm = require('olm');`` *before* loading ``matrix-js-sdk``.
If you want to package Olm as dependency for your node.js application, you can If you want to package Olm as dependency for your node.js application, you can
use ``yarn add https://packages.matrix.org/npm/olm/olm-3.0.0.tgz``. If your use ``yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz``. If your
application also works without e2e crypto enabled, add ``--optional`` to mark it application also works without e2e crypto enabled, add ``--optional`` to mark it
as an optional dependency. as an optional dependency.

View File

@@ -1,6 +1,6 @@
{ {
"name": "matrix-js-sdk", "name": "matrix-js-sdk",
"version": "2.0.0", "version": "2.4.2",
"description": "Matrix Client-Server SDK for Javascript", "description": "Matrix Client-Server SDK for Javascript",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@@ -17,7 +17,7 @@
"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", "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", "dist": "yarn build",
"watch": "watchify -d browser-index.js -o 'exorcist dist/browser-matrix.js.map > dist/browser-matrix.js' -v", "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", "lint": "eslint --max-warnings 93 src spec",
"prepare": "yarn clean && yarn build && git rev-parse HEAD > git-revision.txt" "prepare": "yarn clean && yarn build && git rev-parse HEAD > git-revision.txt"
}, },
"repository": { "repository": {
@@ -54,11 +54,11 @@
"dependencies": { "dependencies": {
"another-json": "^0.2.0", "another-json": "^0.2.0",
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
"bluebird": "^3.5.0", "bluebird": "3.5.5",
"browser-request": "^0.3.3", "browser-request": "^0.3.3",
"bs58": "^4.0.1", "bs58": "^4.0.1",
"content-type": "^1.0.2", "content-type": "^1.0.2",
"loglevel": "1.6.1", "loglevel": "^1.6.4",
"qs": "^6.5.2", "qs": "^6.5.2",
"request": "^2.88.0", "request": "^2.88.0",
"unhomoglyph": "^1.0.2" "unhomoglyph": "^1.0.2"
@@ -76,19 +76,19 @@
"eslint": "^5.12.0", "eslint": "^5.12.0",
"eslint-config-google": "^0.7.1", "eslint-config-google": "^0.7.1",
"eslint-plugin-babel": "^5.3.0", "eslint-plugin-babel": "^5.3.0",
"exorcist": "^0.4.0", "exorcist": "^1.0.1",
"expect": "^1.20.2", "expect": "^1.20.2",
"istanbul": "^0.4.5", "istanbul": "^0.4.5",
"jsdoc": "^3.5.5", "jsdoc": "^3.5.5",
"lolex": "^1.5.2", "lolex": "^1.5.2",
"matrix-mock-request": "^1.2.3", "matrix-mock-request": "^1.2.3",
"mocha": "^5.2.0", "mocha": "^6.2.1",
"mocha-jenkins-reporter": "^0.4.0", "mocha-jenkins-reporter": "^0.4.0",
"olm": "https://packages.matrix.org/npm/olm/olm-3.1.0.tgz", "olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz",
"rimraf": "^2.5.4", "rimraf": "^3.0.0",
"source-map-support": "^0.4.11", "source-map-support": "^0.5.13",
"sourceify": "^0.1.0", "sourceify": "^1.0.0",
"terser": "^4.0.0", "terser": "^4.3.8",
"watchify": "^3.11.1" "watchify": "^3.11.1"
}, },
"browserify": { "browserify": {

View File

@@ -195,6 +195,11 @@ if [ $dodist -eq 0 ]; then
pushd "$builddir" pushd "$builddir"
git clone "$projdir" . git clone "$projdir" .
git checkout "$rel_branch" git checkout "$rel_branch"
# We use Git branch / commit dependencies for some packages, and Yarn seems
# to have a hard time getting that right. See also
# https://github.com/yarnpkg/yarn/issues/4734. As a workaround, we clean the
# global cache here to ensure we get the right thing.
yarn cache clean
yarn install yarn install
# We haven't tagged yet, so tell the dist script what version # We haven't tagged yet, so tell the dist script what version
# it's building # it's building

View File

@@ -302,11 +302,32 @@ describe("MatrixClient events", function() {
}); });
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function() { it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function() {
httpBackend.when("GET", "/sync").respond(401, { errcode: 'M_UNKNOWN_TOKEN' }); const error = { errcode: 'M_UNKNOWN_TOKEN' };
httpBackend.when("GET", "/sync").respond(401, error);
let sessionLoggedOutCount = 0; let sessionLoggedOutCount = 0;
client.on("Session.logged_out", function(event, member) { client.on("Session.logged_out", function(errObj) {
sessionLoggedOutCount++; sessionLoggedOutCount++;
expect(errObj.data).toEqual(error);
});
client.startClient();
return httpBackend.flushAllExpected().then(function() {
expect(sessionLoggedOutCount).toEqual(
1, "Session.logged_out fired wrong number of times",
);
});
});
it("should emit Session.logged_out on M_UNKNOWN_TOKEN (soft logout)", function() {
const error = { errcode: 'M_UNKNOWN_TOKEN', soft_logout: true };
httpBackend.when("GET", "/sync").respond(401, error);
let sessionLoggedOutCount = 0;
client.on("Session.logged_out", function(errObj) {
sessionLoggedOutCount++;
expect(errObj.data).toEqual(error);
}); });
client.startClient(); client.startClient();

View File

@@ -48,7 +48,7 @@ describe("MatrixClient", function() {
const buf = new Buffer('hello world'); const buf = new Buffer('hello world');
it("should upload the file", function(done) { it("should upload the file", function(done) {
httpBackend.when( httpBackend.when(
"POST", "/_matrix/media/v1/upload", "POST", "/_matrix/media/r0/upload",
).check(function(req) { ).check(function(req) {
expect(req.rawData).toEqual(buf); expect(req.rawData).toEqual(buf);
expect(req.queryParams.filename).toEqual("hi.txt"); expect(req.queryParams.filename).toEqual("hi.txt");
@@ -87,7 +87,7 @@ describe("MatrixClient", function() {
it("should parse the response if rawResponse=false", function(done) { it("should parse the response if rawResponse=false", function(done) {
httpBackend.when( httpBackend.when(
"POST", "/_matrix/media/v1/upload", "POST", "/_matrix/media/r0/upload",
).check(function(req) { ).check(function(req) {
expect(req.opts.json).toBeFalsy(); expect(req.opts.json).toBeFalsy();
}).respond(200, { "content_uri": "uri" }); }).respond(200, { "content_uri": "uri" });
@@ -107,7 +107,7 @@ describe("MatrixClient", function() {
it("should parse errors into a MatrixError", function(done) { it("should parse errors into a MatrixError", function(done) {
httpBackend.when( httpBackend.when(
"POST", "/_matrix/media/v1/upload", "POST", "/_matrix/media/r0/upload",
).check(function(req) { ).check(function(req) {
expect(req.rawData).toEqual(buf); expect(req.rawData).toEqual(buf);
expect(req.opts.json).toBeFalsy(); expect(req.opts.json).toBeFalsy();
@@ -396,7 +396,7 @@ describe("MatrixClient", function() {
const auth = {a: 1}; const auth = {a: 1};
it("should pass through an auth dict", function(done) { it("should pass through an auth dict", function(done) {
httpBackend.when( httpBackend.when(
"DELETE", "/_matrix/client/unstable/devices/my_device", "DELETE", "/_matrix/client/r0/devices/my_device",
).check(function(req) { ).check(function(req) {
expect(req.data).toEqual({auth: auth}); expect(req.data).toEqual({auth: auth});
}).respond(200); }).respond(200);

View File

@@ -31,7 +31,7 @@ describe("ContentRepo", function() {
function() { function() {
const mxcUri = "mxc://server.name/resourceid"; const mxcUri = "mxc://server.name/resourceid";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual( expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/v1/download/server.name/resourceid", baseUrl + "/_matrix/media/r0/download/server.name/resourceid",
); );
}); });
@@ -43,7 +43,7 @@ describe("ContentRepo", function() {
function() { function() {
const mxcUri = "mxc://server.name/resourceid"; const mxcUri = "mxc://server.name/resourceid";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual( expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
baseUrl + "/_matrix/media/v1/thumbnail/server.name/resourceid" + baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
"?width=32&height=64&method=crop", "?width=32&height=64&method=crop",
); );
}); });
@@ -52,7 +52,7 @@ describe("ContentRepo", function() {
function() { function() {
const mxcUri = "mxc://server.name/resourceid#automade"; const mxcUri = "mxc://server.name/resourceid#automade";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual( expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
baseUrl + "/_matrix/media/v1/thumbnail/server.name/resourceid" + baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
"?width=32#automade", "?width=32#automade",
); );
}); });
@@ -61,7 +61,7 @@ describe("ContentRepo", function() {
function() { function() {
const mxcUri = "mxc://server.name/resourceid#automade"; const mxcUri = "mxc://server.name/resourceid#automade";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual( expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/v1/download/server.name/resourceid#automade", baseUrl + "/_matrix/media/r0/download/server.name/resourceid#automade",
); );
}); });
}); });
@@ -73,21 +73,21 @@ describe("ContentRepo", function() {
it("should set w/h by default to 96", function() { it("should set w/h by default to 96", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar")).toEqual( expect(ContentRepo.getIdenticonUri(baseUrl, "foobar")).toEqual(
baseUrl + "/_matrix/media/v1/identicon/foobar" + baseUrl + "/_matrix/media/unstable/identicon/foobar" +
"?width=96&height=96", "?width=96&height=96",
); );
}); });
it("should be able to set custom w/h", function() { it("should be able to set custom w/h", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar", 32, 64)).toEqual( expect(ContentRepo.getIdenticonUri(baseUrl, "foobar", 32, 64)).toEqual(
baseUrl + "/_matrix/media/v1/identicon/foobar" + baseUrl + "/_matrix/media/unstable/identicon/foobar" +
"?width=32&height=64", "?width=32&height=64",
); );
}); });
it("should URL encode the identicon string", function() { it("should URL encode the identicon string", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foo#bar", 32, 64)).toEqual( expect(ContentRepo.getIdenticonUri(baseUrl, "foo#bar", 32, 64)).toEqual(
baseUrl + "/_matrix/media/v1/identicon/foo%23bar" + baseUrl + "/_matrix/media/unstable/identicon/foo%23bar" +
"?width=32&height=64", "?width=32&height=64",
); );
}); });

View File

@@ -69,8 +69,14 @@ describe("verification request", function() {
bob.client.on("crypto.verification.request", (request) => { bob.client.on("crypto.verification.request", (request) => {
const bobVerifier = request.beginKeyVerification(verificationMethods.SAS); const bobVerifier = request.beginKeyVerification(verificationMethods.SAS);
bobVerifier.verify(); bobVerifier.verify();
// XXX: Private function access (but it's a test, so we're okay)
bobVerifier._endTimer();
}); });
const aliceVerifier = await alice.client.requestVerification("@bob:example.com"); const aliceVerifier = await alice.client.requestVerification("@bob:example.com");
expect(aliceVerifier).toBeAn(SAS); expect(aliceVerifier).toBeAn(SAS);
// XXX: Private function access (but it's a test, so we're okay)
aliceVerifier._endTimer();
}); });
}); });

View File

@@ -60,6 +60,9 @@ describe("SAS verification", function() {
await sas.verify() await sas.verify()
.catch(spy); .catch(spy);
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
// Cancel the SAS for cleanup (we started a verification, so abort)
sas.cancel();
}); });
describe("verification", function() { describe("verification", function() {
@@ -370,4 +373,127 @@ describe("SAS verification", function() {
expect(bob.client.setDeviceVerified) expect(bob.client.setDeviceVerified)
.toNotHaveBeenCalled(); .toNotHaveBeenCalled();
}); });
describe("verification in DM", function() {
let alice;
let bob;
let aliceSasEvent;
let bobSasEvent;
let aliceVerifier;
let bobPromise;
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("event", async (event) => {
const content = event.getContent();
if (event.getType() === "m.room.message"
&& content.msgtype === "m.key.verification.request") {
expect(content.methods).toInclude(SAS.NAME);
expect(content.to).toBe(bob.getUserId());
const verifier = bob.acceptVerificationDM(event, SAS.NAME);
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();
}
}
});
await verifier.verify();
resolve();
}
});
});
aliceVerifier = await alice.requestVerificationDM(
bob.getUserId(), "!room_id", [verificationMethods.SAS],
);
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() {
await Promise.all([
aliceVerifier.verify(),
bobPromise,
]);
// make sure Alice and Bob verified each other
expect(alice.setDeviceVerified)
.toHaveBeenCalledWith(bob.getUserId(), bob.deviceId);
expect(bob.setDeviceVerified)
.toHaveBeenCalledWith(alice.getUserId(), alice.deviceId);
});
});
}); });

View File

@@ -48,6 +48,25 @@ export async function makeTestClients(userInfos, options) {
} }
} }
}; };
const sendEvent = function(room, type, content) {
// make up a unique ID as the event ID
const eventId = "$" + this.makeTxnId(); // eslint-disable-line babel/no-invalid-this
const event = new MatrixEvent({
sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
type: type,
content: content,
room_id: room,
event_id: eventId,
});
for (const client of clients) {
setTimeout(
() => client.emit("event", event),
0,
);
}
return {event_id: eventId};
};
for (const userInfo of userInfos) { for (const userInfo of userInfos) {
const testClient = new TestClient( const testClient = new TestClient(
@@ -59,6 +78,7 @@ export async function makeTestClients(userInfos, options) {
} }
clientMap[userInfo.userId][userInfo.deviceId] = testClient.client; clientMap[userInfo.userId][userInfo.deviceId] = testClient.client;
testClient.client.sendToDevice = sendToDevice; testClient.client.sendToDevice = sendToDevice;
testClient.client.sendEvent = sendEvent;
clients.push(testClient); clients.push(testClient);
} }

View File

@@ -154,12 +154,9 @@ describe("MatrixClient", function() {
}); });
// FIXME: We shouldn't be yanking _http like this. // FIXME: We shouldn't be yanking _http like this.
client._http = [ client._http = [
"authedRequest", "authedRequestWithPrefix", "getContentUri", "authedRequest", "getContentUri", "request", "uploadContent",
"request", "requestWithPrefix", "uploadContent",
].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {}); ].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
client._http.authedRequest.andCall(httpReq); client._http.authedRequest.andCall(httpReq);
client._http.authedRequestWithPrefix.andCall(httpReq);
client._http.requestWithPrefix.andCall(httpReq);
client._http.request.andCall(httpReq); client._http.request.andCall(httpReq);
// set reasonable working defaults // set reasonable working defaults
@@ -181,9 +178,6 @@ describe("MatrixClient", function() {
client._http.authedRequest.andCall(function() { client._http.authedRequest.andCall(function() {
return Promise.defer().promise; return Promise.defer().promise;
}); });
client._http.authedRequestWithPrefix.andCall(function() {
return Promise.defer().promise;
});
}); });
it("should not POST /filter if a matching filter already exists", async function() { it("should not POST /filter if a matching filter already exists", async function() {

View File

@@ -104,7 +104,7 @@ describe("Room", function() {
user_ids: [userA], user_ids: [userA],
}, },
}); });
room.addLiveEvents([typing]); room.addEphemeralEvents([typing]);
expect(room.currentState.setTypingEvent).toHaveBeenCalledWith(typing); expect(room.currentState.setTypingEvent).toHaveBeenCalledWith(typing);
}); });

View File

@@ -34,12 +34,18 @@ export default class Reemitter {
} }
reEmit(source, eventNames) { reEmit(source, eventNames) {
// We include the source as the last argument for event handlers which may need it,
// such as read receipt listeners on the client class which won't have the context
// of the room.
const forSource = (handler, ...args) => {
handler(...args, source);
};
for (const eventName of eventNames) { for (const eventName of eventNames) {
if (this.boundHandlers[eventName] === undefined) { if (this.boundHandlers[eventName] === undefined) {
this.boundHandlers[eventName] = this._handleEvent.bind(this, eventName); this.boundHandlers[eventName] = this._handleEvent.bind(this, eventName);
} }
const boundHandler = this.boundHandlers[eventName];
const boundHandler = forSource.bind(this, this.boundHandlers[eventName]);
source.on(eventName, boundHandler); source.on(eventName, boundHandler);
} }
} }

View File

@@ -429,6 +429,26 @@ export class AutoDiscovery {
return AutoDiscovery.fromDiscoveryConfig(wellknown.raw); return AutoDiscovery.fromDiscoveryConfig(wellknown.raw);
} }
/**
* Gets the raw discovery client configuration for the given domain name.
* Should only be used if there's no validation to be done on the resulting
* object, otherwise use findClientConfig().
* @param {string} domain The domain to get the client config for.
* @returns {Promise<object>} Resolves to the domain's client config. Can
* be an empty object.
*/
static async getRawClientConfig(domain) {
if (!domain || typeof(domain) !== "string" || domain.length === 0) {
throw new Error("'domain' must be a string of non-zero length");
}
const response = await this._fetchWellKnownObject(
`https://${domain}/.well-known/matrix/client`,
);
if (!response) return {};
return response.raw || {};
}
/** /**
* Sanitizes a given URL to ensure it is either an HTTP or HTTP URL and * Sanitizes a given URL to ensure it is either an HTTP or HTTP URL and
* is suitable for the requirements laid out by .well-known auto discovery. * is suitable for the requirements laid out by .well-known auto discovery.

View File

@@ -1,6 +1,8 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -23,8 +25,23 @@ limitations under the License.
* @module base-apis * @module base-apis
*/ */
import { SERVICE_TYPES } from './service-types';
import logger from './logger';
const httpApi = require("./http-api"); const httpApi = require("./http-api");
const utils = require("./utils"); const utils = require("./utils");
const PushProcessor = require("./pushprocessor");
function termsUrlForService(serviceType, baseUrl) {
switch (serviceType) {
case SERVICE_TYPES.IS:
return baseUrl + httpApi.PREFIX_IDENTITY_V2 + '/terms';
case SERVICE_TYPES.IM:
return baseUrl + '/_matrix/integrations/v1/terms';
default:
throw new Error('Unsupported service type');
}
}
/** /**
* Low-level wrappers for the Matrix APIs * Low-level wrappers for the Matrix APIs
@@ -46,6 +63,15 @@ const utils = require("./utils");
* *
* @param {string} opts.accessToken The access_token for this user. * @param {string} opts.accessToken The access_token for this user.
* *
* @param {IdentityServerProvider} [opts.identityServer]
* Optional. A provider object with one function `getAccessToken`, which is a
* callback that returns a Promise<String> of an identity access token to supply
* with identity requests. If the object is unset, no access token will be
* supplied.
* See also https://github.com/vector-im/riot-web/issues/10615 which seeks to
* replace the previous approach of manual access tokens params with this
* callback throughout the SDK.
*
* @param {Number=} opts.localTimeoutMs Optional. The default maximum amount of * @param {Number=} opts.localTimeoutMs Optional. The default maximum amount of
* time to wait before timing out HTTP requests. If not specified, there is no * time to wait before timing out HTTP requests. If not specified, there is no
* timeout. * timeout.
@@ -62,6 +88,7 @@ function MatrixBaseApis(opts) {
this.baseUrl = opts.baseUrl; this.baseUrl = opts.baseUrl;
this.idBaseUrl = opts.idBaseUrl; this.idBaseUrl = opts.idBaseUrl;
this.identityServer = opts.identityServer;
const httpOpts = { const httpOpts = {
baseUrl: opts.baseUrl, baseUrl: opts.baseUrl,
@@ -100,6 +127,15 @@ MatrixBaseApis.prototype.getIdentityServerUrl = function(stripProto=false) {
return this.idBaseUrl; return this.idBaseUrl;
}; };
/**
* Set the Identity Server URL of this client
* @param {string} url New Identity Server URL
*/
MatrixBaseApis.prototype.setIdentityServerUrl = function(url) {
this.idBaseUrl = utils.ensureNoTrailingSlash(url);
this._http.setIdBaseUrl(this.idBaseUrl);
};
/** /**
* Get the access token associated with this account. * Get the access token associated with this account.
* @return {?String} The access_token or null * @return {?String} The access_token or null
@@ -391,9 +427,8 @@ MatrixBaseApis.prototype.deactivateAccount = function(auth, erase) {
body.erase = erase; body.erase = erase;
} }
return this._http.authedRequestWithPrefix( return this._http.authedRequest(
undefined, "POST", '/account/deactivate', undefined, body, undefined, "POST", '/account/deactivate', undefined, body,
httpApi.PREFIX_R0,
); );
}; };
@@ -438,6 +473,37 @@ MatrixBaseApis.prototype.createRoom = function(options, callback) {
callback, "POST", "/createRoom", undefined, options, callback, "POST", "/createRoom", undefined, options,
); );
}; };
/**
* Fetches relations for a given event
* @param {string} roomId the room of the event
* @param {string} eventId the id of the event
* @param {string} relationType the rel_type of the relations requested
* @param {string} eventType the event type of the relations requested
* @param {Object} opts options with optional values for the request.
* @param {Object} opts.from the pagination token returned from a previous request as `next_batch` to return following relations.
* @return {Object} the response, with chunk and next_batch.
*/
MatrixBaseApis.prototype.fetchRelations =
async function(roomId, eventId, relationType, eventType, opts) {
const queryParams = {};
if (opts.from) {
queryParams.from = opts.from;
}
const queryString = utils.encodeParams(queryParams);
const path = utils.encodeUri(
"/rooms/$roomId/relations/$eventId/$relationType/$eventType?" + queryString, {
$roomId: roomId,
$eventId: eventId,
$relationType: relationType,
$eventType: eventType,
});
const response = await this._http.authedRequest(
undefined, "GET", path, null, null, {
prefix: httpApi.PREFIX_UNSTABLE,
},
);
return response;
};
/** /**
* @param {string} roomId * @param {string} roomId
@@ -927,10 +993,13 @@ MatrixBaseApis.prototype.roomInitialSync = function(roomId, limit, callback) {
* @param {string} rrEventId ID of the event tracked by the read receipt. This is here * @param {string} rrEventId ID of the event tracked by the read receipt. This is here
* for convenience because the RR and the RM are commonly updated at the same time as * for convenience because the RR and the RM are commonly updated at the same time as
* each other. Optional. * each other. Optional.
* @param {object} opts Options for the read markers.
* @param {object} opts.hidden True to hide the read receipt from other users. <b>This
* property is currently unstable and may change in the future.</b>
* @return {module:client.Promise} Resolves: the empty object, {}. * @return {module:client.Promise} Resolves: the empty object, {}.
*/ */
MatrixBaseApis.prototype.setRoomReadMarkersHttpRequest = MatrixBaseApis.prototype.setRoomReadMarkersHttpRequest =
function(roomId, rmEventId, rrEventId) { function(roomId, rmEventId, rrEventId, opts) {
const path = utils.encodeUri("/rooms/$roomId/read_markers", { const path = utils.encodeUri("/rooms/$roomId/read_markers", {
$roomId: roomId, $roomId: roomId,
}); });
@@ -938,6 +1007,7 @@ MatrixBaseApis.prototype.setRoomReadMarkersHttpRequest =
const content = { const content = {
"m.fully_read": rmEventId, "m.fully_read": rmEventId,
"m.read": rrEventId, "m.read": rrEventId,
"m.hidden": Boolean(opts ? opts.hidden : false),
}; };
return this._http.authedRequest( return this._http.authedRequest(
@@ -1269,10 +1339,16 @@ MatrixBaseApis.prototype.getThreePids = function(callback) {
}; };
/** /**
* Add a 3PID to your homeserver account and optionally bind it to an identity
* server as well. An identity server is required as part of the `creds` object.
*
* This API is deprecated, and you should instead use `addThreePidOnly`
* for homeservers that support it.
*
* @param {Object} creds * @param {Object} creds
* @param {boolean} bind * @param {boolean} bind
* @param {module:client.callback} callback Optional. * @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO * @return {module:client.Promise} Resolves: on success
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
MatrixBaseApis.prototype.addThreePid = function(creds, bind, callback) { MatrixBaseApis.prototype.addThreePid = function(creds, bind, callback) {
@@ -1286,6 +1362,75 @@ MatrixBaseApis.prototype.addThreePid = function(creds, bind, callback) {
); );
}; };
/**
* Add a 3PID to your homeserver account. This API does not use an identity
* server, as the homeserver is expected to handle 3PID ownership validation.
*
* You can check whether a homeserver supports this API via
* `doesServerSupportSeparateAddAndBind`.
*
* @param {Object} data A object with 3PID validation data from having called
* `account/3pid/<medium>/requestToken` on the homeserver.
* @return {module:client.Promise} Resolves: on success
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.addThreePidOnly = function(data) {
const path = "/account/3pid/add";
return this._http.authedRequest(
undefined, "POST", path, null, data, {
prefix: httpApi.PREFIX_UNSTABLE,
},
);
};
/**
* Bind a 3PID for discovery onto an identity server via the homeserver. The
* identity server handles 3PID ownership validation and the homeserver records
* the new binding to track where all 3PIDs for the account are bound.
*
* You can check whether a homeserver supports this API via
* `doesServerSupportSeparateAddAndBind`.
*
* @param {Object} data A object with 3PID validation data from having called
* `validate/<medium>/requestToken` on the identity server. It should also
* contain `id_server` and `id_access_token` fields as well.
* @return {module:client.Promise} Resolves: on success
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.bindThreePid = function(data) {
const path = "/account/3pid/bind";
return this._http.authedRequest(
undefined, "POST", path, null, data, {
prefix: httpApi.PREFIX_UNSTABLE,
},
);
};
/**
* Unbind a 3PID for discovery on an identity server via the homeserver. The
* homeserver removes its record of the binding to keep an updated record of
* where all 3PIDs for the account are bound.
*
* @param {string} medium The threepid medium (eg. 'email')
* @param {string} address The threepid address (eg. 'bob@example.com')
* this must be as returned by getThreePids.
* @return {module:client.Promise} Resolves: on success
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.unbindThreePid = function(medium, address) {
const path = "/account/3pid/unbind";
const data = {
medium,
address,
id_server: this.getIdentityServerUrl(true),
};
return this._http.authedRequest(
undefined, "POST", path, null, data, {
prefix: httpApi.PREFIX_UNSTABLE,
},
);
};
/** /**
* @param {string} medium The threepid medium (eg. 'email') * @param {string} medium The threepid medium (eg. 'email')
* @param {string} address The threepid address (eg. 'bob@example.com') * @param {string} address The threepid address (eg. 'bob@example.com')
@@ -1300,9 +1445,7 @@ MatrixBaseApis.prototype.deleteThreePid = function(medium, address) {
'medium': medium, 'medium': medium,
'address': address, 'address': address,
}; };
return this._http.authedRequestWithPrefix( return this._http.authedRequest(undefined, "POST", path, null, data);
undefined, "POST", path, null, data, httpApi.PREFIX_UNSTABLE,
);
}; };
/** /**
@@ -1335,10 +1478,8 @@ MatrixBaseApis.prototype.setPassword = function(authDict, newPassword, callback)
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
MatrixBaseApis.prototype.getDevices = function() { MatrixBaseApis.prototype.getDevices = function() {
const path = "/devices"; return this._http.authedRequest(
return this._http.authedRequestWithPrefix( undefined, 'GET', "/devices", undefined, undefined,
undefined, "GET", path, undefined, undefined,
httpApi.PREFIX_UNSTABLE,
); );
}; };
@@ -1355,11 +1496,7 @@ MatrixBaseApis.prototype.setDeviceDetails = function(device_id, body) {
$device_id: device_id, $device_id: device_id,
}); });
return this._http.authedRequest(undefined, "PUT", path, undefined, body);
return this._http.authedRequestWithPrefix(
undefined, "PUT", path, undefined, body,
httpApi.PREFIX_UNSTABLE,
);
}; };
/** /**
@@ -1381,10 +1518,7 @@ MatrixBaseApis.prototype.deleteDevice = function(device_id, auth) {
body.auth = auth; body.auth = auth;
} }
return this._http.authedRequestWithPrefix( return this._http.authedRequest(undefined, "DELETE", path, undefined, body);
undefined, "DELETE", path, undefined, body,
httpApi.PREFIX_UNSTABLE,
);
}; };
/** /**
@@ -1402,10 +1536,8 @@ MatrixBaseApis.prototype.deleteMultipleDevices = function(devices, auth) {
body.auth = auth; body.auth = auth;
} }
return this._http.authedRequestWithPrefix( const path = "/delete_devices";
undefined, "POST", "/delete_devices", undefined, body, return this._http.authedRequest(undefined, "POST", path, undefined, body);
httpApi.PREFIX_UNSTABLE,
);
}; };
@@ -1447,7 +1579,9 @@ MatrixBaseApis.prototype.setPusher = function(pusher, callback) {
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
MatrixBaseApis.prototype.getPushRules = function(callback) { MatrixBaseApis.prototype.getPushRules = function(callback) {
return this._http.authedRequest(callback, "GET", "/pushrules/"); return this._http.authedRequest(callback, "GET", "/pushrules/").then(rules => {
return PushProcessor.rewriteDefaultRules(rules);
});
}; };
/** /**
@@ -1581,9 +1715,7 @@ MatrixBaseApis.prototype.uploadKeysRequest = function(content, opts, callback) {
} else { } else {
path = "/keys/upload"; path = "/keys/upload";
} }
return this._http.authedRequestWithPrefix( return this._http.authedRequest(callback, "POST", path, undefined, content);
callback, "POST", path, undefined, content, httpApi.PREFIX_UNSTABLE,
);
}; };
MatrixBaseApis.prototype.uploadKeySignatures = function(content) { MatrixBaseApis.prototype.uploadKeySignatures = function(content) {
@@ -1625,10 +1757,7 @@ MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, opts) {
content.device_keys[u] = {}; content.device_keys[u] = {};
}); });
return this._http.authedRequestWithPrefix( return this._http.authedRequest(undefined, "POST", "/keys/query", undefined, content);
undefined, "POST", "/keys/query", undefined, content,
httpApi.PREFIX_UNSTABLE,
);
}; };
/** /**
@@ -1656,10 +1785,8 @@ MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm) {
query[deviceId] = key_algorithm; query[deviceId] = key_algorithm;
} }
const content = {one_time_keys: queries}; const content = {one_time_keys: queries};
return this._http.authedRequestWithPrefix( const path = "/keys/claim";
undefined, "POST", "/keys/claim", undefined, content, return this._http.authedRequest(undefined, "POST", path, undefined, content);
httpApi.PREFIX_UNSTABLE,
);
}; };
/** /**
@@ -1678,10 +1805,8 @@ MatrixBaseApis.prototype.getKeyChanges = function(oldToken, newToken) {
to: newToken, to: newToken,
}; };
return this._http.authedRequestWithPrefix( const path = "/keys/changes";
undefined, "GET", "/keys/changes", qps, undefined, return this._http.authedRequest(undefined, "GET", path, qps, undefined);
httpApi.PREFIX_UNSTABLE,
);
}; };
MatrixBaseApis.prototype.uploadDeviceSigningKeys = function(auth, keys) { MatrixBaseApis.prototype.uploadDeviceSigningKeys = function(auth, keys) {
@@ -1696,10 +1821,36 @@ MatrixBaseApis.prototype.uploadDeviceSigningKeys = function(auth, keys) {
// ========================== // ==========================
/** /**
* Requests an email verification token directly from an Identity Server. * Register with an Identity Server using the OpenID token from the user's
* Homeserver, which can be retrieved via
* {@link module:client~MatrixClient#getOpenIdToken}.
* *
* Note that the Home Server offers APIs to proxy this API for specific * Note that the `/account/register` endpoint (as well as IS authentication in
* situations, allowing for better feedback to the user. * general) was added as part of the v2 API version.
*
* @param {object} hsOpenIdToken
* @return {module:client.Promise} Resolves: with object containing an Identity
* Server access token.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.registerWithIdentityServer = function(hsOpenIdToken) {
if (!this.idBaseUrl) {
throw new Error("No Identity Server base URL set");
}
const uri = this.idBaseUrl + httpApi.PREFIX_IDENTITY_V2 + "/account/register";
return this._http.requestOtherUrl(
undefined, "POST", uri,
null, hsOpenIdToken,
);
};
/**
* Requests an email verification token directly from an identity server.
*
* This API is used as part of binding an email for discovery on an identity
* server. The validation data that results should be passed to the
* `bindThreePid` method to complete the binding process.
* *
* @param {string} email The email address to request a token for * @param {string} email The email address to request a token for
* @param {string} clientSecret A secret binary string generated by the client. * @param {string} clientSecret A secret binary string generated by the client.
@@ -1711,26 +1862,122 @@ MatrixBaseApis.prototype.uploadDeviceSigningKeys = function(auth, keys) {
* @param {string} nextLink Optional If specified, the client will be redirected * @param {string} nextLink Optional If specified, the client will be redirected
* to this link after validation. * to this link after validation.
* @param {module:client.callback} callback Optional. * @param {module:client.callback} callback Optional.
* @param {string} identityAccessToken The `access_token` field of the identity
* server `/account/register` response (see {@link registerWithIdentityServer}).
*
* @return {module:client.Promise} Resolves: TODO * @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
* @throws Error if No ID server is set * @throws Error if no identity server is set
*/ */
MatrixBaseApis.prototype.requestEmailToken = function(email, clientSecret, MatrixBaseApis.prototype.requestEmailToken = async function(
sendAttempt, nextLink, callback) { email,
clientSecret,
sendAttempt,
nextLink,
callback,
identityAccessToken,
) {
const params = { const params = {
client_secret: clientSecret, client_secret: clientSecret,
email: email, email: email,
send_attempt: sendAttempt, send_attempt: sendAttempt,
next_link: nextLink, next_link: nextLink,
}; };
return this._http.idServerRequest(
callback, "POST", "/validate/email/requestToken", try {
params, httpApi.PREFIX_IDENTITY_V1, const response = await this._http.idServerRequest(
); undefined, "POST", "/validate/email/requestToken",
params, httpApi.PREFIX_IDENTITY_V2, identityAccessToken,
);
// TODO: Fold callback into above call once v1 path below is removed
if (callback) callback(null, response);
return response;
} catch (err) {
if (err.cors === "rejected" || err.httpStatus === 404) {
// Fall back to deprecated v1 API for now
// TODO: Remove this path once v2 is only supported version
// See https://github.com/vector-im/riot-web/issues/10443
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
callback, "POST", "/validate/email/requestToken",
params, httpApi.PREFIX_IDENTITY_V1,
);
}
if (callback) callback(err);
throw err;
}
}; };
/** /**
* Submits an MSISDN token to the identity server * Requests a MSISDN verification token directly from an identity server.
*
* This API is used as part of binding a MSISDN for discovery on an identity
* server. The validation data that results should be passed to the
* `bindThreePid` method to complete the binding process.
*
* @param {string} phoneCountry The ISO 3166-1 alpha-2 code for the country in
* which phoneNumber should be parsed relative to.
* @param {string} phoneNumber The phone number, in national or international
* format
* @param {string} clientSecret A secret binary string generated by the client.
* It is recommended this be around 16 ASCII characters.
* @param {number} sendAttempt If an identity server sees a duplicate request
* with the same sendAttempt, it will not send another SMS.
* To request another SMS to be sent, use a larger value for
* the sendAttempt param as was used in the previous request.
* @param {string} nextLink Optional If specified, the client will be redirected
* to this link after validation.
* @param {module:client.callback} callback Optional.
* @param {string} identityAccessToken The `access_token` field of the Identity
* Server `/account/register` response (see {@link registerWithIdentityServer}).
*
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @throws Error if no identity server is set
*/
MatrixBaseApis.prototype.requestMsisdnToken = async function(
phoneCountry,
phoneNumber,
clientSecret,
sendAttempt,
nextLink,
callback,
identityAccessToken,
) {
const params = {
client_secret: clientSecret,
country: phoneCountry,
phone_number: phoneNumber,
send_attempt: sendAttempt,
next_link: nextLink,
};
try {
const response = await this._http.idServerRequest(
undefined, "POST", "/validate/msisdn/requestToken",
params, httpApi.PREFIX_IDENTITY_V2, identityAccessToken,
);
// TODO: Fold callback into above call once v1 path below is removed
if (callback) callback(null, response);
return response;
} catch (err) {
if (err.cors === "rejected" || err.httpStatus === 404) {
// Fall back to deprecated v1 API for now
// TODO: Remove this path once v2 is only supported version
// See https://github.com/vector-im/riot-web/issues/10443
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
callback, "POST", "/validate/msisdn/requestToken",
params, httpApi.PREFIX_IDENTITY_V1,
);
}
if (callback) callback(err);
throw err;
}
};
/**
* Submits a MSISDN token to the identity server
* *
* This is used when submitting the code sent by SMS to a phone number. * This is used when submitting the code sent by SMS to a phone number.
* The ID server has an equivalent API for email but the js-sdk does * The ID server has an equivalent API for email but the js-sdk does
@@ -1740,46 +1987,323 @@ MatrixBaseApis.prototype.requestEmailToken = function(email, clientSecret,
* @param {string} sid The sid given in the response to requestToken * @param {string} sid The sid given in the response to requestToken
* @param {string} clientSecret A secret binary string generated by the client. * @param {string} clientSecret A secret binary string generated by the client.
* This must be the same value submitted in the requestToken call. * This must be the same value submitted in the requestToken call.
* @param {string} token The token, as enetered by the user. * @param {string} msisdnToken The MSISDN token, as enetered by the user.
* @param {string} identityAccessToken The `access_token` field of the Identity
* Server `/account/register` response (see {@link registerWithIdentityServer}).
* *
* @return {module:client.Promise} Resolves: Object, currently with no parameters. * @return {module:client.Promise} Resolves: Object, currently with no parameters.
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
* @throws Error if No ID server is set * @throws Error if No ID server is set
*/ */
MatrixBaseApis.prototype.submitMsisdnToken = function(sid, clientSecret, token) { MatrixBaseApis.prototype.submitMsisdnToken = async function(
sid,
clientSecret,
msisdnToken,
identityAccessToken,
) {
const params = { const params = {
sid: sid, sid: sid,
client_secret: clientSecret, client_secret: clientSecret,
token: token, token: msisdnToken,
}; };
return this._http.idServerRequest(
undefined, "POST", "/validate/msisdn/submitToken", try {
params, httpApi.PREFIX_IDENTITY_V1, return await this._http.idServerRequest(
undefined, "POST", "/validate/msisdn/submitToken",
params, httpApi.PREFIX_IDENTITY_V2, identityAccessToken,
);
} catch (err) {
if (err.cors === "rejected" || err.httpStatus === 404) {
// Fall back to deprecated v1 API for now
// TODO: Remove this path once v2 is only supported version
// See https://github.com/vector-im/riot-web/issues/10443
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
undefined, "POST", "/validate/msisdn/submitToken",
params, httpApi.PREFIX_IDENTITY_V1,
);
}
throw err;
}
};
/**
* Submits a MSISDN token to an arbitrary URL.
*
* This is used when submitting the code sent by SMS to a phone number in the
* newer 3PID flow where the homeserver validates 3PID ownership (as part of
* `requestAdd3pidMsisdnToken`). The homeserver response may include a
* `submit_url` to specify where the token should be sent, and this helper can
* be used to pass the token to this URL.
*
* @param {string} url The URL to submit the token to
* @param {string} sid The sid given in the response to requestToken
* @param {string} clientSecret A secret binary string generated by the client.
* This must be the same value submitted in the requestToken call.
* @param {string} msisdnToken The MSISDN token, as enetered by the user.
*
* @return {module:client.Promise} Resolves: Object, currently with no parameters.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.submitMsisdnTokenOtherUrl = function(
url,
sid,
clientSecret,
msisdnToken,
) {
const params = {
sid: sid,
client_secret: clientSecret,
token: msisdnToken,
};
return this._http.requestOtherUrl(
undefined, "POST", url, undefined, params,
); );
}; };
/**
* Gets the V2 hashing information from the identity server. Primarily useful for
* lookups.
* @param {string} identityAccessToken The access token for the identity server.
* @returns {Promise<object>} The hashing information for the identity server.
*/
MatrixBaseApis.prototype.getIdentityHashDetails = function(identityAccessToken) {
return this._http.idServerRequest(
undefined, "GET", "/hash_details",
null, httpApi.PREFIX_IDENTITY_V2, identityAccessToken,
);
};
/**
* Performs a hashed lookup of addresses against the identity server. This is
* only supported on identity servers which have at least the version 2 API.
* @param {Array<Array<string,string>>} addressPairs An array of 2 element arrays.
* The first element of each pair is the address, the second is the 3PID medium.
* Eg: ["email@example.org", "email"]
* @param {string} identityAccessToken The access token for the identity server.
* @returns {Promise<Array<{address, mxid}>>} A collection of address mappings to
* found MXIDs. Results where no user could be found will not be listed.
*/
MatrixBaseApis.prototype.identityHashedLookup = async function(
addressPairs, // [["email@example.org", "email"], ["10005550000", "msisdn"]]
identityAccessToken,
) {
const params = {
// addresses: ["email@example.org", "10005550000"],
// algorithm: "sha256",
// pepper: "abc123"
};
// Get hash information first before trying to do a lookup
const hashes = await this.getIdentityHashDetails(identityAccessToken);
if (!hashes || !hashes['lookup_pepper'] || !hashes['algorithms']) {
throw new Error("Unsupported identity server: bad response");
}
params['pepper'] = hashes['lookup_pepper'];
const localMapping = {
// hashed identifier => plain text address
// For use in this function's return format
};
// When picking an algorithm, we pick the hashed over no hashes
if (hashes['algorithms'].includes('sha256')) {
// Abuse the olm hashing
const olmutil = new global.Olm.Utility();
params["addresses"] = addressPairs.map(p => {
const addr = p[0].toLowerCase(); // lowercase to get consistent hashes
const med = p[1].toLowerCase();
const hashed = olmutil.sha256(`${addr} ${med} ${params['pepper']}`)
.replace(/\+/g, '-').replace(/\//g, '_'); // URL-safe base64
// Map the hash to a known (case-sensitive) address. We use the case
// sensitive version because the caller might be expecting that.
localMapping[hashed] = p[0];
return hashed;
});
params["algorithm"] = "sha256";
} else if (hashes['algorithms'].includes('none')) {
params["addresses"] = addressPairs.map(p => {
const addr = p[0].toLowerCase(); // lowercase to get consistent hashes
const med = p[1].toLowerCase();
const unhashed = `${addr} ${med}`;
// Map the unhashed values to a known (case-sensitive) address. We use
// the case sensitive version because the caller might be expecting that.
localMapping[unhashed] = p[0];
return unhashed;
});
params["algorithm"] = "none";
} else {
throw new Error("Unsupported identity server: unknown hash algorithm");
}
const response = await this._http.idServerRequest(
undefined, "POST", "/lookup",
params, httpApi.PREFIX_IDENTITY_V2, identityAccessToken,
);
if (!response || !response['mappings']) return []; // no results
const foundAddresses = [/* {address: "plain@example.org", mxid} */];
for (const hashed of Object.keys(response['mappings'])) {
const mxid = response['mappings'][hashed];
const plainAddress = localMapping[hashed];
if (!plainAddress) {
throw new Error("Identity server returned more results than expected");
}
foundAddresses.push({address: plainAddress, mxid});
}
return foundAddresses;
};
/** /**
* Looks up the public Matrix ID mapping for a given 3rd party * Looks up the public Matrix ID mapping for a given 3rd party
* identifier from the Identity Server * identifier from the Identity Server
*
* @param {string} medium The medium of the threepid, eg. 'email' * @param {string} medium The medium of the threepid, eg. 'email'
* @param {string} address The textual address of the threepid * @param {string} address The textual address of the threepid
* @param {module:client.callback} callback Optional. * @param {module:client.callback} callback Optional.
* @param {string} identityAccessToken The `access_token` field of the Identity
* Server `/account/register` response (see {@link registerWithIdentityServer}).
*
* @return {module:client.Promise} Resolves: A threepid mapping * @return {module:client.Promise} Resolves: A threepid mapping
* object or the empty object if no mapping * object or the empty object if no mapping
* exists * exists
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
MatrixBaseApis.prototype.lookupThreePid = function(medium, address, callback) { MatrixBaseApis.prototype.lookupThreePid = async function(
const params = { medium,
medium: medium, address,
address: address, callback,
}; identityAccessToken,
return this._http.idServerRequest( ) {
callback, "GET", "/lookup", try {
params, httpApi.PREFIX_IDENTITY_V1, // Note: we're using the V2 API by calling this function, but our
); // function contract requires a V1 response. We therefore have to
// convert it manually.
const response = await this.identityHashedLookup(
[[address, medium]], identityAccessToken,
);
const result = response.find(p => p.address === address);
if (!result) {
// TODO: Fold callback into above call once v1 path below is removed
if (callback) callback(null, {});
return {};
}
const mapping = {
address,
medium,
mxid: result.mxid,
// We can't reasonably fill these parameters:
// not_before
// not_after
// ts
// signatures
};
// TODO: Fold callback into above call once v1 path below is removed
if (callback) callback(null, mapping);
return mapping;
} catch (err) {
if (err.cors === "rejected" || err.httpStatus === 404) {
// Fall back to deprecated v1 API for now
// TODO: Remove this path once v2 is only supported version
// See https://github.com/vector-im/riot-web/issues/10443
const params = {
medium: medium,
address: address,
};
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
callback, "GET", "/lookup",
params, httpApi.PREFIX_IDENTITY_V1,
);
}
if (callback) callback(err, undefined);
throw err;
}
}; };
/**
* Looks up the public Matrix ID mappings for multiple 3PIDs.
*
* @param {Array.<Array.<string>>} query Array of arrays containing
* [medium, address]
* @param {string} identityAccessToken The `access_token` field of the Identity
* Server `/account/register` response (see {@link registerWithIdentityServer}).
*
* @return {module:client.Promise} Resolves: Lookup results from IS.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.bulkLookupThreePids = async function(
query,
identityAccessToken,
) {
try {
// Note: we're using the V2 API by calling this function, but our
// function contract requires a V1 response. We therefore have to
// convert it manually.
const response = await this.identityHashedLookup(
// We have to reverse the query order to get [address, medium] pairs
query.map(p => [p[1], p[0]]), identityAccessToken,
);
const v1results = [];
for (const mapping of response) {
const originalQuery = query.find(p => p[1] === mapping.address);
if (!originalQuery) {
throw new Error("Identity sever returned unexpected results");
}
v1results.push([
originalQuery[0], // medium
mapping.address,
mapping.mxid,
]);
}
return {threepids: v1results};
} catch (err) {
if (err.cors === "rejected" || err.httpStatus === 404) {
// Fall back to deprecated v1 API for now
// TODO: Remove this path once v2 is only supported version
// See https://github.com/vector-im/riot-web/issues/10443
const params = {
threepids: query,
};
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
undefined, "POST", "/bulk_lookup", params,
httpApi.PREFIX_IDENTITY_V1, identityAccessToken,
);
}
throw err;
}
};
/**
* Get account info from the Identity Server. This is useful as a neutral check
* to verify that other APIs are likely to approve access by testing that the
* token is valid, terms have been agreed, etc.
*
* @param {string} identityAccessToken The `access_token` field of the Identity
* Server `/account/register` response (see {@link registerWithIdentityServer}).
*
* @return {module:client.Promise} Resolves: an object with account info.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getIdentityAccount = function(
identityAccessToken,
) {
return this._http.idServerRequest(
undefined, "GET", "/account",
undefined, httpApi.PREFIX_IDENTITY_V2, identityAccessToken,
);
};
// Direct-to-device messaging // Direct-to-device messaging
// ========================== // ==========================
@@ -1806,10 +2330,7 @@ MatrixBaseApis.prototype.sendToDevice = function(
messages: contentMap, messages: contentMap,
}; };
return this._http.authedRequestWithPrefix( return this._http.authedRequest(undefined, "PUT", path, undefined, body);
undefined, "PUT", path, undefined, body,
httpApi.PREFIX_UNSTABLE,
);
}; };
// Third party Lookup API // Third party Lookup API
@@ -1821,9 +2342,8 @@ MatrixBaseApis.prototype.sendToDevice = function(
* @return {module:client.Promise} Resolves to the result object * @return {module:client.Promise} Resolves to the result object
*/ */
MatrixBaseApis.prototype.getThirdpartyProtocols = function() { MatrixBaseApis.prototype.getThirdpartyProtocols = function() {
return this._http.authedRequestWithPrefix( return this._http.authedRequest(
undefined, "GET", "/thirdparty/protocols", undefined, undefined, undefined, "GET", "/thirdparty/protocols", undefined, undefined,
httpApi.PREFIX_UNSTABLE,
).then((response) => { ).then((response) => {
// sanity check // sanity check
if (!response || typeof(response) !== 'object') { if (!response || typeof(response) !== 'object') {
@@ -1848,10 +2368,7 @@ MatrixBaseApis.prototype.getThirdpartyLocation = function(protocol, params) {
$protocol: protocol, $protocol: protocol,
}); });
return this._http.authedRequestWithPrefix( return this._http.authedRequest(undefined, "GET", path, params, undefined);
undefined, "GET", path, params, undefined,
httpApi.PREFIX_UNSTABLE,
);
}; };
/** /**
@@ -1867,12 +2384,45 @@ MatrixBaseApis.prototype.getThirdpartyUser = function(protocol, params) {
$protocol: protocol, $protocol: protocol,
}); });
return this._http.authedRequestWithPrefix( return this._http.authedRequest(undefined, "GET", path, params, undefined);
undefined, "GET", path, params, undefined, };
httpApi.PREFIX_UNSTABLE,
MatrixBaseApis.prototype.getTerms = function(serviceType, baseUrl) {
const url = termsUrlForService(serviceType, baseUrl);
return this._http.requestOtherUrl(
undefined, 'GET', url,
); );
}; };
MatrixBaseApis.prototype.agreeToTerms = function(
serviceType, baseUrl, accessToken, termsUrls,
) {
const url = termsUrlForService(serviceType, baseUrl);
const headers = {
Authorization: "Bearer " + accessToken,
};
return this._http.requestOtherUrl(
undefined, 'POST', url, null, { user_accepts: termsUrls }, { headers },
);
};
/**
* Reports an event as inappropriate to the server, which may then notify the appropriate people.
* @param {string} roomId The room in which the event being reported is located.
* @param {string} eventId The event to report.
* @param {number} score The score to rate this content as where -100 is most offensive and 0 is inoffensive.
* @param {string} reason The reason the content is being reported. May be blank.
* @returns {module:client.Promise} Resolves to an empty object if successful
*/
MatrixBaseApis.prototype.reportEvent = function(roomId, eventId, score, reason) {
const path = utils.encodeUri("/rooms/$roomId/report/$eventId", {
$roomId: roomId,
$eventId: eventId,
});
return this._http.authedRequest(undefined, "POST", path, null, {score, reason});
};
/** /**
* MatrixBaseApis object * MatrixBaseApis object
*/ */

View File

@@ -2,6 +2,7 @@
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2018-2019 New Vector Ltd Copyright 2018-2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -45,7 +46,7 @@ const olmlib = require("./crypto/olmlib");
import ReEmitter from './ReEmitter'; import ReEmitter from './ReEmitter';
import RoomList from './crypto/RoomList'; import RoomList from './crypto/RoomList';
import logger from '../src/logger'; import logger from './logger';
import Crypto from './crypto'; import Crypto from './crypto';
import { isCryptoAvailable } from './crypto'; import { isCryptoAvailable } from './crypto';
@@ -107,6 +108,15 @@ function keyFromRecoverySession(session, decryptionKey) {
* *
* @param {string} opts.userId The user ID for this user. * @param {string} opts.userId The user ID for this user.
* *
* @param {IdentityServerProvider} [opts.identityServer]
* Optional. A provider object with one function `getAccessToken`, which is a
* callback that returns a Promise<String> of an identity access token to supply
* with identity requests. If the object is unset, no access token will be
* supplied.
* See also https://github.com/vector-im/riot-web/issues/10615 which seeks to
* replace the previous approach of manual access tokens params with this
* callback throughout the SDK.
*
* @param {Object=} opts.store * @param {Object=} opts.store
* The data store used for sync data from the homeserver. If not specified, * The data store used for sync data from the homeserver. If not specified,
* this client will not store any HTTP responses. The `createClient` helper * this client will not store any HTTP responses. The `createClient` helper
@@ -158,17 +168,17 @@ function keyFromRecoverySession(session, decryptionKey) {
* that the application can handle. Each element should be an item from {@link * that the application can handle. Each element should be an item from {@link
* module:crypto~verificationMethods verificationMethods}, or a class that * module:crypto~verificationMethods verificationMethods}, or a class that
* implements the {$link module:crypto/verification/Base verifier interface}. * implements the {$link module:crypto/verification/Base verifier interface}.
*
* @param {boolean} [opts.forceTURN]
* Optional. Whether relaying calls through a TURN server should be forced.
*
* @param {boolean} [opts.fallbackICEServerAllowed]
* Optional. Whether to allow a fallback ICE server should be used for negotiating a
* WebRTC connection if the homeserver doesn't provide any servers. Defaults to false.
*/ */
function MatrixClient(opts) { function MatrixClient(opts) {
// Allow trailing slash in HS url opts.baseUrl = utils.ensureNoTrailingSlash(opts.baseUrl);
if (opts.baseUrl && opts.baseUrl.endsWith("/")) { opts.idBaseUrl = utils.ensureNoTrailingSlash(opts.idBaseUrl);
opts.baseUrl = opts.baseUrl.substr(0, opts.baseUrl.length - 1);
}
// Allow trailing slash in IS url
if (opts.idBaseUrl && opts.idBaseUrl.endsWith("/")) {
opts.idBaseUrl = opts.idBaseUrl.substr(0, opts.idBaseUrl.length - 1);
}
MatrixBaseApis.call(this, opts); MatrixBaseApis.call(this, opts);
@@ -227,6 +237,7 @@ function MatrixClient(opts) {
this._verificationMethods = opts.verificationMethods; this._verificationMethods = opts.verificationMethods;
this._forceTURN = opts.forceTURN || false; this._forceTURN = opts.forceTURN || false;
this._fallbackICEServerAllowed = opts.fallbackICEServerAllowed || false;
// List of which rooms have encryption enabled: separate from crypto because // List of which rooms have encryption enabled: separate from crypto because
// we still want to know which rooms are encrypted even if crypto is disabled: // we still want to know which rooms are encrypted even if crypto is disabled:
@@ -236,7 +247,9 @@ function MatrixClient(opts) {
// The pushprocessor caches useful things, so keep one and re-use it // The pushprocessor caches useful things, so keep one and re-use it
this._pushProcessor = new PushProcessor(this); this._pushProcessor = new PushProcessor(this);
this._serverSupportsLazyLoading = null; // Cache of the server's /versions response
// TODO: This should expire: https://github.com/matrix-org/matrix-js-sdk/issues/1020
this._serverVersionsCache = null;
this._cachedCapabilities = null; // { capabilities: {}, lastUpdated: timestamp } this._cachedCapabilities = null; // { capabilities: {}, lastUpdated: timestamp }
@@ -278,6 +291,48 @@ function MatrixClient(opts) {
} }
} }
}); });
// Like above, we have to listen for read receipts from ourselves in order to
// correctly handle notification counts on encrypted rooms.
// This fixes https://github.com/vector-im/riot-web/issues/9421
this.on("Room.receipt", (event, room) => {
if (room && this.isRoomEncrypted(room.roomId)) {
// Figure out if we've read something or if it's just informational
const content = event.getContent();
const isSelf = Object.keys(content).filter(eid => {
return Object.keys(content[eid]['m.read']).includes(this.getUserId());
}).length > 0;
if (!isSelf) return;
// Work backwards to determine how many events are unread. We also set
// a limit for how back we'll look to avoid spinning CPU for too long.
// If we hit the limit, we assume the count is unchanged.
const maxHistory = 20;
const events = room.getLiveTimeline().getEvents();
let highlightCount = 0;
for (let i = events.length - 1; i >= 0; i--) {
if (i === events.length - maxHistory) return; // limit reached
const event = events[i];
if (room.hasUserReadEvent(this.getUserId(), event.getId())) {
// If the user has read the event, then the counting is done.
break;
}
highlightCount += this.getPushActionsForEvent(
event,
).tweaks.highlight ? 1 : 0;
}
// Note: we don't need to handle 'total' notifications because the counts
// will come from the server.
room.setUnreadNotificationCount("highlight", highlightCount);
}
});
} }
utils.inherits(MatrixClient, EventEmitter); utils.inherits(MatrixClient, EventEmitter);
utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype); utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype);
@@ -734,6 +789,40 @@ async function _setDeviceVerification(
client.emit("deviceVerificationChanged", userId, deviceId, dev); client.emit("deviceVerificationChanged", userId, deviceId, dev);
} }
/**
* Request a key verification from another user, using a DM.
*
* @param {string} userId the user to request verification with
* @param {string} roomId the room to use for verification
* @param {Array} methods array of verification methods to use. Defaults to
* all known methods
*
* @returns {Promise<module:crypto/verification/Base>} resolves to a verifier
* when the request is accepted by the other user
*/
MatrixClient.prototype.requestVerificationDM = function(userId, roomId, methods) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.requestVerificationDM(userId, roomId, methods);
};
/**
* Accept a key verification request from a DM.
*
* @param {module:models/event~MatrixEvent} event the verification request
* that is accepted
* @param {string} method the verification mmethod to use
*
* @returns {module:crypto/verification/Base} a verifier
*/
MatrixClient.prototype.acceptVerificationDM = function(event, method) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.acceptVerificationDM(event, method);
};
/** /**
* Request a key verification from another user. * Request a key verification from another user.
* *
@@ -1107,7 +1196,8 @@ MatrixClient.prototype.checkKeyBackup = function() {
*/ */
MatrixClient.prototype.getKeyBackupVersion = function() { MatrixClient.prototype.getKeyBackupVersion = function() {
return this._http.authedRequest( return this._http.authedRequest(
undefined, "GET", "/room_keys/version", undefined, "GET", "/room_keys/version", undefined, undefined,
{prefix: httpApi.PREFIX_UNSTABLE},
).then((res) => { ).then((res) => {
if (res.algorithm !== olmlib.MEGOLM_BACKUP_ALGORITHM) { if (res.algorithm !== olmlib.MEGOLM_BACKUP_ALGORITHM) {
const err = "Unknown backup algorithm: " + res.algorithm; const err = "Unknown backup algorithm: " + res.algorithm;
@@ -1262,6 +1352,7 @@ MatrixClient.prototype.createKeyBackupVersion = async function(info) {
const res = await this._http.authedRequest( const res = await this._http.authedRequest(
undefined, "POST", "/room_keys/version", undefined, data, undefined, "POST", "/room_keys/version", undefined, data,
{prefix: httpApi.PREFIX_UNSTABLE},
); );
this.enableKeyBackup({ this.enableKeyBackup({
algorithm: info.algorithm, algorithm: info.algorithm,
@@ -1289,6 +1380,7 @@ MatrixClient.prototype.deleteKeyBackupVersion = function(version) {
return this._http.authedRequest( return this._http.authedRequest(
undefined, "DELETE", path, undefined, undefined, undefined, "DELETE", path, undefined, undefined,
{prefix: httpApi.PREFIX_UNSTABLE},
); );
}; };
@@ -1330,6 +1422,7 @@ MatrixClient.prototype.sendKeyBackup = function(roomId, sessionId, version, data
const path = this._makeKeyBackupPath(roomId, sessionId, version); const path = this._makeKeyBackupPath(roomId, sessionId, version);
return this._http.authedRequest( return this._http.authedRequest(
undefined, "PUT", path.path, path.queryData, data, undefined, "PUT", path.path, path.queryData, data,
{prefix: httpApi.PREFIX_UNSTABLE},
); );
}; };
@@ -1345,6 +1438,19 @@ MatrixClient.prototype.scheduleAllGroupSessionsForBackup = async function() {
await this._crypto.scheduleAllGroupSessionsForBackup(); await this._crypto.scheduleAllGroupSessionsForBackup();
}; };
/**
* Marks all group sessions as needing to be backed up without scheduling
* them to upload in the background.
* @returns {Promise<int>} Resolves to the number of sessions requiring a backup.
*/
MatrixClient.prototype.flagAllGroupSessionsForBackup = function() {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.flagAllGroupSessionsForBackup();
};
MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) { MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) {
try { try {
decodeRecoveryKey(recoveryKey); decodeRecoveryKey(recoveryKey);
@@ -1404,7 +1510,8 @@ MatrixClient.prototype._restoreKeyBackup = function(
} }
return this._http.authedRequest( return this._http.authedRequest(
undefined, "GET", path.path, path.queryData, undefined, "GET", path.path, path.queryData, undefined,
{prefix: httpApi.PREFIX_UNSTABLE},
).then((res) => { ).then((res) => {
if (res.rooms) { if (res.rooms) {
for (const [roomId, roomData] of Object.entries(res.rooms)) { for (const [roomId, roomData] of Object.entries(res.rooms)) {
@@ -1453,7 +1560,8 @@ MatrixClient.prototype.deleteKeysFromBackup = function(roomId, sessionId, versio
const path = this._makeKeyBackupPath(roomId, sessionId, version); const path = this._makeKeyBackupPath(roomId, sessionId, version);
return this._http.authedRequest( return this._http.authedRequest(
undefined, "DELETE", path.path, path.queryData, undefined, "DELETE", path.path, path.queryData, undefined,
{prefix: httpApi.PREFIX_UNSTABLE},
); );
}; };
@@ -1487,8 +1595,10 @@ MatrixClient.prototype.getGroups = function() {
* @return {module:client.Promise} Resolves with an object containing the config. * @return {module:client.Promise} Resolves with an object containing the config.
*/ */
MatrixClient.prototype.getMediaConfig = function(callback) { MatrixClient.prototype.getMediaConfig = function(callback) {
return this._http.authedRequestWithPrefix( return this._http.authedRequest(
callback, "GET", "/config", undefined, undefined, httpApi.PREFIX_MEDIA_R0, callback, "GET", "/config", undefined, undefined, {
prefix: httpApi.PREFIX_MEDIA_R0,
},
); );
}; };
@@ -2043,6 +2153,20 @@ function _encryptEventIfNeeded(client, event, room) {
return null; return null;
} }
if (event.getType() === "m.reaction") {
// For reactions, there is a very little gained by encrypting the entire
// event, as relation data is already kept in the clear. Event
// encryption for a reaction effectively only obscures the event type,
// but the purpose is still obvious from the relation data, so nothing
// is really gained. It also causes quite a few problems, such as:
// * triggers notifications via default push rules
// * prevents server-side bundling for reactions
// The reaction key / content / emoji value does warrant encrypting, but
// this will be handled separately by encrypting just this value.
// See https://github.com/matrix-org/matrix-doc/pull/1849#pullrequestreview-248763642
return null;
}
if (!client._crypto) { if (!client._crypto) {
throw new Error( throw new Error(
"This room is configured to use encryption, but your client does " + "This room is configured to use encryption, but your client does " +
@@ -2052,6 +2176,21 @@ function _encryptEventIfNeeded(client, event, room) {
return client._crypto.encryptEvent(event, room); return client._crypto.encryptEvent(event, room);
} }
/**
* Returns the eventType that should be used taking encryption into account
* for a given eventType.
* @param {MatrixClient} client the client
* @param {string} roomId the room for the events `eventType` relates to
* @param {string} eventType the event type
* @return {string} the event type taking encryption into account
*/
function _getEncryptedIfNeededEventType(client, roomId, eventType) {
if (eventType === "m.reaction") {
return eventType;
}
const isEncrypted = client.isRoomEncrypted(roomId);
return isEncrypted ? "m.room.encrypted" : eventType;
}
function _updatePendingEventStatus(room, event, newStatus) { function _updatePendingEventStatus(room, event, newStatus) {
if (room) { if (room) {
@@ -2267,11 +2406,17 @@ MatrixClient.prototype.sendHtmlEmote = function(roomId, body, htmlBody, callback
* Send a receipt. * Send a receipt.
* @param {Event} event The event being acknowledged * @param {Event} event The event being acknowledged
* @param {string} receiptType The kind of receipt e.g. "m.read" * @param {string} receiptType The kind of receipt e.g. "m.read"
* @param {object} opts Additional content to send alongside the receipt.
* @param {module:client.callback} callback Optional. * @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO * @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
MatrixClient.prototype.sendReceipt = function(event, receiptType, callback) { MatrixClient.prototype.sendReceipt = function(event, receiptType, opts, callback) {
if (typeof(opts) === 'function') {
callback = opts;
opts = {};
}
if (this.isGuest()) { if (this.isGuest()) {
return Promise.resolve({}); // guests cannot send receipts so don't bother. return Promise.resolve({}); // guests cannot send receipts so don't bother.
} }
@@ -2282,7 +2427,7 @@ MatrixClient.prototype.sendReceipt = function(event, receiptType, callback) {
$eventId: event.getId(), $eventId: event.getId(),
}); });
const promise = this._http.authedRequest( const promise = this._http.authedRequest(
callback, "POST", path, undefined, {}, callback, "POST", path, undefined, opts || {},
); );
const room = this.getRoom(event.getRoomId()); const room = this.getRoom(event.getRoomId());
@@ -2295,12 +2440,32 @@ MatrixClient.prototype.sendReceipt = function(event, receiptType, callback) {
/** /**
* Send a read receipt. * Send a read receipt.
* @param {Event} event The event that has been read. * @param {Event} event The event that has been read.
* @param {object} opts The options for the read receipt.
* @param {boolean} opts.hidden True to prevent the receipt from being sent to
* other users and homeservers. Default false (send to everyone). <b>This
* property is unstable and may change in the future.</b>
* @param {module:client.callback} callback Optional. * @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO * @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
MatrixClient.prototype.sendReadReceipt = function(event, callback) { MatrixClient.prototype.sendReadReceipt = async function(event, opts, callback) {
return this.sendReceipt(event, "m.read", callback); if (typeof(opts) === 'function') {
callback = opts;
opts = {};
}
if (!opts) opts = {};
const eventId = event.getId();
const room = this.getRoom(event.getRoomId());
if (room && room.hasPendingEvent(eventId)) {
throw new Error(`Cannot set read receipt to a pending event (${eventId})`);
}
const addlContent = {
"m.hidden": Boolean(opts.hidden),
};
return this.sendReceipt(event, "m.read", addlContent, callback);
}; };
/** /**
@@ -2309,26 +2474,36 @@ MatrixClient.prototype.sendReadReceipt = function(event, callback) {
* and displayed as a horizontal line in the timeline that is visually distinct to the * and displayed as a horizontal line in the timeline that is visually distinct to the
* position of the user's own read receipt. * position of the user's own read receipt.
* @param {string} roomId ID of the room that has been read * @param {string} roomId ID of the room that has been read
* @param {string} eventId ID of the event that has been read * @param {string} rmEventId ID of the event that has been read
* @param {string} rrEvent the event tracked by the read receipt. This is here for * @param {string} rrEvent the event tracked by the read receipt. This is here for
* convenience because the RR and the RM are commonly updated at the same time as each * convenience because the RR and the RM are commonly updated at the same time as each
* other. The local echo of this receipt will be done if set. Optional. * other. The local echo of this receipt will be done if set. Optional.
* @param {object} opts Options for the read markers
* @param {object} opts.hidden True to hide the receipt from other users and homeservers.
* <b>This property is unstable and may change in the future.</b>
* @return {module:client.Promise} Resolves: the empty object, {}. * @return {module:client.Promise} Resolves: the empty object, {}.
*/ */
MatrixClient.prototype.setRoomReadMarkers = function(roomId, eventId, rrEvent) { MatrixClient.prototype.setRoomReadMarkers = async function(
const rmEventId = eventId; roomId, rmEventId, rrEvent, opts,
let rrEventId; ) {
const room = this.getRoom(roomId);
if (room && room.hasPendingEvent(rmEventId)) {
throw new Error(`Cannot set read marker to a pending event (${rmEventId})`);
}
// Add the optional RR update, do local echo like `sendReceipt` // Add the optional RR update, do local echo like `sendReceipt`
let rrEventId;
if (rrEvent) { if (rrEvent) {
rrEventId = rrEvent.getId(); rrEventId = rrEvent.getId();
const room = this.getRoom(roomId); if (room && room.hasPendingEvent(rrEventId)) {
throw new Error(`Cannot set read receipt to a pending event (${rrEventId})`);
}
if (room) { if (room) {
room._addLocalEchoReceipt(this.credentials.userId, rrEvent, "m.read"); room._addLocalEchoReceipt(this.credentials.userId, rrEvent, "m.read");
} }
} }
return this.setRoomReadMarkersHttpRequest(roomId, rmEventId, rrEventId); return this.setRoomReadMarkersHttpRequest(roomId, rmEventId, rrEventId, opts);
}; };
/** /**
@@ -2354,11 +2529,13 @@ MatrixClient.prototype.getUrlPreview = function(url, ts, callback) {
} }
const self = this; const self = this;
return this._http.authedRequestWithPrefix( return this._http.authedRequest(
callback, "GET", "/preview_url", { callback, "GET", "/preview_url", {
url: url, url: url,
ts: ts, ts: ts,
}, undefined, httpApi.PREFIX_MEDIA_R0, }, undefined, {
prefix: httpApi.PREFIX_MEDIA_R0,
},
).then(function(response) { ).then(function(response) {
// TODO: expire cache occasionally // TODO: expire cache occasionally
self.urlPreviewCache[key] = response; self.urlPreviewCache[key] = response;
@@ -2448,6 +2625,7 @@ MatrixClient.prototype.getRoomUpgradeHistory = function(roomId, verifyLinks=fals
while (tombstoneEvent) { while (tombstoneEvent) {
const refRoom = this.getRoom(tombstoneEvent.getContent()['replacement_room']); const refRoom = this.getRoom(tombstoneEvent.getContent()['replacement_room']);
if (!refRoom) break; // end of the chain if (!refRoom) break; // end of the chain
if (refRoom.roomId === currentRoom.roomId) break; // Tombstone is referencing it's own room
if (verifyLinks) { if (verifyLinks) {
createEvent = refRoom.currentState.getStateEvents("m.room.create", ""); createEvent = refRoom.currentState.getStateEvents("m.room.create", "");
@@ -2459,6 +2637,12 @@ MatrixClient.prototype.getRoomUpgradeHistory = function(roomId, verifyLinks=fals
// Push to the end because we're looking forwards // Push to the end because we're looking forwards
upgradeHistory.push(refRoom); upgradeHistory.push(refRoom);
const roomIds = new Set(upgradeHistory.map((ref) => ref.roomId));
if (roomIds.size < upgradeHistory.length) {
// The last room added to the list introduced a previous roomId
// To avoid recursion, return the last rooms - 1
return upgradeHistory.slice(0, upgradeHistory.length - 1);
}
// Set the current room to the reference room so we know where we're at // Set the current room to the reference room so we know where we're at
currentRoom = refRoom; currentRoom = refRoom;
@@ -2503,7 +2687,12 @@ MatrixClient.prototype.inviteByEmail = function(roomId, email, callback) {
* @return {module:client.Promise} Resolves: TODO * @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
MatrixClient.prototype.inviteByThreePid = function(roomId, medium, address, callback) { MatrixClient.prototype.inviteByThreePid = async function(
roomId,
medium,
address,
callback,
) {
const path = utils.encodeUri( const path = utils.encodeUri(
"/rooms/$roomId/invite", "/rooms/$roomId/invite",
{ $roomId: roomId }, { $roomId: roomId },
@@ -2516,12 +2705,24 @@ MatrixClient.prototype.inviteByThreePid = function(roomId, medium, address, call
errcode: "ORG.MATRIX.JSSDK_MISSING_PARAM", errcode: "ORG.MATRIX.JSSDK_MISSING_PARAM",
})); }));
} }
const params = {
return this._http.authedRequest(callback, "POST", path, undefined, {
id_server: identityServerUrl, id_server: identityServerUrl,
medium: medium, medium: medium,
address: address, address: address,
}); };
if (
this.identityServer &&
this.identityServer.getAccessToken &&
await this.doesServerAcceptIdentityAccessToken()
) {
const identityAccessToken = await this.identityServer.getAccessToken();
if (identityAccessToken) {
params.id_access_token = identityAccessToken;
}
}
return this._http.authedRequest(callback, "POST", path, undefined, params);
}; };
/** /**
@@ -3150,9 +3351,8 @@ MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) {
params.from = token; params.from = token;
} }
promise = promise = this._http.authedRequest(
this._http.authedRequestWithPrefix(undefined, "GET", path, params, undefined, "GET", path, params, undefined,
undefined, httpApi.PREFIX_UNSTABLE,
).then(function(res) { ).then(function(res) {
const token = res.next_token; const token = res.next_token;
const matrixEvents = []; const matrixEvents = [];
@@ -3311,14 +3511,9 @@ MatrixClient.prototype.setGuestAccess = function(roomId, opts) {
/** /**
* Requests an email verification token for the purposes of registration. * Requests an email verification token for the purposes of registration.
* This API proxies the Identity Server /validate/email/requestToken API, * This API requests a token from the homeserver.
* adding registration-specific behaviour. Specifically, if an account with * The doesServerRequireIdServerParam() method can be used to determine if
* the given email address already exists, it will either send an email * the server requires the id_server parameter to be provided.
* to the address informing them of this or return M_THREEPID_IN_USE
* (which one is up to the Home Server).
*
* requestEmailToken calls the equivalent API directly on the ID server,
* therefore bypassing the registration-specific logic.
* *
* Parameters and return value are as for requestEmailToken * Parameters and return value are as for requestEmailToken
@@ -3343,8 +3538,9 @@ MatrixClient.prototype.requestRegisterEmailToken = function(email, clientSecret,
/** /**
* Requests a text message verification token for the purposes of registration. * Requests a text message verification token for the purposes of registration.
* This API proxies the Identity Server /validate/msisdn/requestToken API, * This API requests a token from the homeserver.
* adding registration-specific behaviour, as with requestRegisterEmailToken. * The doesServerRequireIdServerParam() method can be used to determine if
* the server requires the id_server parameter to be provided.
* *
* @param {string} phoneCountry The ISO 3166-1 alpha-2 code for the country in which * @param {string} phoneCountry The ISO 3166-1 alpha-2 code for the country in which
* phoneNumber should be parsed relative to. * phoneNumber should be parsed relative to.
@@ -3371,15 +3567,13 @@ MatrixClient.prototype.requestRegisterMsisdnToken = function(phoneCountry, phone
/** /**
* Requests an email verification token for the purposes of adding a * Requests an email verification token for the purposes of adding a
* third party identifier to an account. * third party identifier to an account.
* This API proxies the Identity Server /validate/email/requestToken API, * This API requests a token from the homeserver.
* adding specific behaviour for the addition of email addresses to an * The doesServerRequireIdServerParam() method can be used to determine if
* account. Specifically, if an account with * the server requires the id_server parameter to be provided.
* the given email address already exists, it will either send an email * If an account with the given email address already exists and is
* to the address informing them of this or return M_THREEPID_IN_USE * associated with an account other than the one the user is authed as,
* (which one is up to the Home Server). * it will either send an email to the address informing them of this
* * or return M_THREEPID_IN_USE (which one is up to the Home Server).
* requestEmailToken calls the equivalent API directly on the ID server,
* therefore bypassing the email addition specific logic.
* *
* @param {string} email As requestEmailToken * @param {string} email As requestEmailToken
* @param {string} clientSecret As requestEmailToken * @param {string} clientSecret As requestEmailToken
@@ -3495,15 +3689,30 @@ MatrixClient.prototype.requestPasswordMsisdnToken = function(phoneCountry, phone
* @param {object} params Parameters for the POST request * @param {object} params Parameters for the POST request
* @return {module:client.Promise} Resolves: As requestEmailToken * @return {module:client.Promise} Resolves: As requestEmailToken
*/ */
MatrixClient.prototype._requestTokenFromEndpoint = function(endpoint, params) { MatrixClient.prototype._requestTokenFromEndpoint = async function(endpoint, params) {
const id_server_url = url.parse(this.idBaseUrl); const postParams = Object.assign({}, params);
if (id_server_url.host === null) {
throw new Error("Invalid ID server URL: " + this.idBaseUrl); // If the HS supports separate add and bind, then requestToken endpoints
// don't need an IS as they are all validated by the HS directly.
if (!await this.doesServerSupportSeparateAddAndBind() && this.idBaseUrl) {
const idServerUrl = url.parse(this.idBaseUrl);
if (!idServerUrl.host) {
throw new Error("Invalid ID server URL: " + this.idBaseUrl);
}
postParams.id_server = idServerUrl.host;
if (
this.identityServer &&
this.identityServer.getAccessToken &&
await this.doesServerAcceptIdentityAccessToken()
) {
const identityAccessToken = await this.identityServer.getAccessToken();
if (identityAccessToken) {
postParams.id_access_token = identityAccessToken;
}
}
} }
const postParams = Object.assign({}, params, {
id_server: id_server_url.host,
});
return this._http.request( return this._http.request(
undefined, "POST", endpoint, undefined, undefined, "POST", endpoint, undefined,
postParams, postParams,
@@ -3959,6 +4168,77 @@ MatrixClient.prototype.getTurnServers = function() {
return this._turnServers || []; return this._turnServers || [];
}; };
/**
* Set whether to allow a fallback ICE server should be used for negotiating a
* WebRTC connection if the homeserver doesn't provide any servers. Defaults to
* false.
*
* @param {boolean} allow
*/
MatrixClient.prototype.setFallbackICEServerAllowed = function(allow) {
this._fallbackICEServerAllowed = allow;
};
/**
* Get whether to allow a fallback ICE server should be used for negotiating a
* WebRTC connection if the homeserver doesn't provide any servers. Defaults to
* false.
*
* @returns {boolean}
*/
MatrixClient.prototype.isFallbackICEServerAllowed = function() {
return this._fallbackICEServerAllowed;
};
// Synapse-specific APIs
// =====================
/**
* Determines if the current user is an administrator of the Synapse homeserver.
* Returns false if untrue or the homeserver does not appear to be a Synapse
* homeserver. <strong>This function is implementation specific and may change
* as a result.</strong>
* @return {boolean} true if the user appears to be a Synapse administrator.
*/
MatrixClient.prototype.isSynapseAdministrator = function() {
return this.whoisSynapseUser(this.getUserId())
.then(() => true)
.catch(() => false);
};
/**
* Performs a whois lookup on a user using Synapse's administrator API.
* <strong>This function is implementation specific and may change as a
* result.</strong>
* @param {string} userId the User ID to look up.
* @return {object} the whois response - see Synapse docs for information.
*/
MatrixClient.prototype.whoisSynapseUser = function(userId) {
const path = utils.encodeUri(
"/_synapse/admin/v1/whois/$userId",
{ $userId: userId },
);
return this._http.authedRequest(
undefined, 'GET', path, undefined, undefined, {prefix: ''},
);
};
/**
* Deactivates a user using Synapse's administrator API. <strong>This
* function is implementation specific and may change as a result.</strong>
* @param {string} userId the User ID to deactivate.
* @return {object} the deactivate response - see Synapse docs for information.
*/
MatrixClient.prototype.deactivateSynapseUser = function(userId) {
const path = utils.encodeUri(
"/_synapse/admin/v1/deactivate/$userId",
{ $userId: userId },
);
return this._http.authedRequest(
undefined, 'POST', path, undefined, undefined, {prefix: ''},
);
};
// Higher level APIs // Higher level APIs
// ================= // =================
@@ -4090,13 +4370,14 @@ MatrixClient.prototype.stopClient = function() {
global.clearTimeout(this._checkTurnServersTimeoutID); global.clearTimeout(this._checkTurnServersTimeoutID);
}; };
/* /**
* Query the server to see if it support members lazy loading * Get the API versions supported by the server, along with any
* @return {Promise<boolean>} true if server supports lazy loading * unstable APIs it supports
* @return {Promise<object>} The server /versions response
*/ */
MatrixClient.prototype.doesServerSupportLazyLoading = async function() { MatrixClient.prototype.getVersions = async function() {
if (this._serverSupportsLazyLoading === null) { if (this._serverVersionsCache === null) {
const response = await this._http.request( this._serverVersionsCache = await this._http.request(
undefined, // callback undefined, // callback
"GET", "/_matrix/client/versions", "GET", "/_matrix/client/versions",
undefined, // queryParams undefined, // queryParams
@@ -4105,14 +4386,80 @@ MatrixClient.prototype.doesServerSupportLazyLoading = async function() {
prefix: '', prefix: '',
}, },
); );
const unstableFeatures = response["unstable_features"];
this._serverSupportsLazyLoading =
unstableFeatures && unstableFeatures["m.lazy_load_members"];
} }
return this._serverSupportsLazyLoading; return this._serverVersionsCache;
}; };
/* /**
* Query the server to see if it support members lazy loading
* @return {Promise<boolean>} true if server supports lazy loading
*/
MatrixClient.prototype.doesServerSupportLazyLoading = async function() {
const response = await this.getVersions();
const versions = response["versions"];
const unstableFeatures = response["unstable_features"];
return (versions && versions.includes("r0.5.0"))
|| (unstableFeatures && unstableFeatures["m.lazy_load_members"]);
};
/**
* Query the server to see if the `id_server` parameter is required
* when registering with an 3pid, adding a 3pid or resetting password.
* @return {Promise<boolean>} true if id_server parameter is required
*/
MatrixClient.prototype.doesServerRequireIdServerParam = async function() {
const response = await this.getVersions();
const versions = response["versions"];
// Supporting r0.6.0 is the same as having the flag set to false
if (versions && versions.includes("r0.6.0")) {
return false;
}
const unstableFeatures = response["unstable_features"];
if (unstableFeatures["m.require_identity_server"] === undefined) {
return true;
} else {
return unstableFeatures["m.require_identity_server"];
}
};
/**
* Query the server to see if the `id_access_token` parameter can be safely
* passed to the homeserver. Some homeservers may trigger errors if they are not
* prepared for the new parameter.
* @return {Promise<boolean>} true if id_access_token can be sent
*/
MatrixClient.prototype.doesServerAcceptIdentityAccessToken = async function() {
const response = await this.getVersions();
const versions = response["versions"];
const unstableFeatures = response["unstable_features"];
return (versions && versions.includes("r0.6.0"))
|| (unstableFeatures && unstableFeatures["m.id_access_token"]);
};
/**
* Query the server to see if it supports separate 3PID add and bind functions.
* This affects the sequence of API calls clients should use for these operations,
* so it's helpful to be able to check for support.
* @return {Promise<boolean>} true if separate functions are supported
*/
MatrixClient.prototype.doesServerSupportSeparateAddAndBind = async function() {
const response = await this.getVersions();
const versions = response["versions"];
const unstableFeatures = response["unstable_features"];
return (versions && versions.includes("r0.6.0"))
|| (unstableFeatures && unstableFeatures["m.separate_add_and_bind"]);
};
/**
* Get if lazy loading members is being used. * Get if lazy loading members is being used.
* @return {boolean} Whether or not members are lazy loaded by this client * @return {boolean} Whether or not members are lazy loaded by this client
*/ */
@@ -4120,7 +4467,7 @@ MatrixClient.prototype.hasLazyLoadMembersEnabled = function() {
return !!this._clientOpts.lazyLoadMembers; return !!this._clientOpts.lazyLoadMembers;
}; };
/* /**
* Set a function which is called when /sync returns a 'limited' response. * Set a function which is called when /sync returns a 'limited' response.
* It is called with a room ID and returns a boolean. It should return 'true' if the SDK * It is called with a room ID and returns a boolean. It should return 'true' if the SDK
* can SAFELY remove events from this room. It may not be safe to remove events if there * can SAFELY remove events from this room. It may not be safe to remove events if there
@@ -4141,6 +4488,47 @@ MatrixClient.prototype.getCanResetTimelineCallback = function() {
return this._canResetTimelineCallback; return this._canResetTimelineCallback;
}; };
/**
* Returns relations for a given event. Handles encryption transparently,
* with the caveat that the amount of events returned might be 0, even though you get a nextBatch.
* When the returned promise resolves, all messages should have finished trying to decrypt.
* @param {string} roomId the room of the event
* @param {string} eventId the id of the event
* @param {string} relationType the rel_type of the relations requested
* @param {string} eventType the event type of the relations requested
* @param {Object} opts options with optional values for the request.
* @param {Object} opts.from the pagination token returned from a previous request as `nextBatch` to return following relations.
* @return {Object} an object with `events` as `MatrixEvent[]` and optionally `nextBatch` if more relations are available.
*/
MatrixClient.prototype.relations =
async function(roomId, eventId, relationType, eventType, opts = {}) {
const fetchedEventType = _getEncryptedIfNeededEventType(this, roomId, eventType);
const result = await this.fetchRelations(
roomId,
eventId,
relationType,
fetchedEventType,
opts);
const mapper = this.getEventMapper();
let originalEvent;
if (result.original_event) {
originalEvent = mapper(result.original_event);
}
let events = result.chunk.map(mapper);
if (fetchedEventType === "m.room.encrypted") {
const allEvents = originalEvent ? events.concat(originalEvent) : events;
await Promise.all(allEvents.map(e => {
return new Promise(resolve => e.once("Event.decrypted", resolve));
}));
events = events.filter(e => e.getType() === eventType);
}
return {
originalEvent,
events,
nextBatch: result.next_batch,
};
};
function setupCallEventHandler(client) { function setupCallEventHandler(client) {
const candidatesByCall = { const candidatesByCall = {
// callId: [Candidate] // callId: [Candidate]
@@ -4364,10 +4752,9 @@ function checkTurnServers(client) {
} }
}, function(err) { }, function(err) {
logger.error("Failed to get TURN URIs"); logger.error("Failed to get TURN URIs");
client._checkTurnServersTimeoutID = client._checkTurnServersTimeoutID = setTimeout(function() {
setTimeout(function() { checkTurnServers(client);
checkTurnServers(client); }, 60000);
}, 60000);
}); });
} }
@@ -4627,7 +5014,7 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
* when then login session can be renewed by using a refresh token. * when then login session can be renewed by using a refresh token.
* @event module:client~MatrixClient#"Session.logged_out" * @event module:client~MatrixClient#"Session.logged_out"
* @example * @example
* matrixClient.on("Session.logged_out", function(call){ * matrixClient.on("Session.logged_out", function(errorObj){
* // show the login screen * // show the login screen
* }); * });
*/ */

View File

@@ -47,7 +47,7 @@ module.exports = {
} }
} }
let serverAndMediaId = mxc.slice(6); // strips mxc:// let serverAndMediaId = mxc.slice(6); // strips mxc://
let prefix = "/_matrix/media/v1/download/"; let prefix = "/_matrix/media/r0/download/";
const params = {}; const params = {};
if (width) { if (width) {
@@ -62,7 +62,7 @@ module.exports = {
if (utils.keys(params).length > 0) { if (utils.keys(params).length > 0) {
// these are thumbnailing params so they probably want the // these are thumbnailing params so they probably want the
// thumbnailing API... // thumbnailing API...
prefix = "/_matrix/media/v1/thumbnail/"; prefix = "/_matrix/media/r0/thumbnail/";
} }
const fragmentOffset = serverAndMediaId.indexOf("#"); const fragmentOffset = serverAndMediaId.indexOf("#");
@@ -83,6 +83,7 @@ module.exports = {
* @param {Number} width The desired width of the image in pixels. Default: 96. * @param {Number} width The desired width of the image in pixels. Default: 96.
* @param {Number} height The desired height of the image in pixels. Default: 96. * @param {Number} height The desired height of the image in pixels. Default: 96.
* @return {string} The complete URL to the identicon. * @return {string} The complete URL to the identicon.
* @deprecated This is no longer in the specification.
*/ */
getIdenticonUri: function(baseUrl, identiconString, width, height) { getIdenticonUri: function(baseUrl, identiconString, width, height) {
if (!identiconString) { if (!identiconString) {
@@ -99,7 +100,7 @@ module.exports = {
height: height, height: height,
}; };
const path = utils.encodeUri("/_matrix/media/v1/identicon/$ident", { const path = utils.encodeUri("/_matrix/media/unstable/identicon/$ident", {
$ident: identiconString, $ident: identiconString,
}); });
return baseUrl + path + return baseUrl + path +

View File

@@ -594,6 +594,11 @@ OlmDevice.prototype.encryptMessage = async function(
'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS], 'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => { (txn) => {
this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => { this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => {
const sessionDesc = sessionInfo.session.describe();
console.log(
"Session ID " + sessionId + " to " +
theirDeviceIdentityKey + ": " + sessionDesc,
);
res = sessionInfo.session.encrypt(payloadString); res = sessionInfo.session.encrypt(payloadString);
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn); this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
}); });
@@ -621,6 +626,11 @@ OlmDevice.prototype.decryptMessage = async function(
'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS], 'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => { (txn) => {
this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => { this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => {
const sessionDesc = sessionInfo.session.describe();
console.log(
"Session ID " + sessionId + " to " +
theirDeviceIdentityKey + ": " + sessionDesc,
);
payloadString = sessionInfo.session.decrypt(messageType, ciphertext); payloadString = sessionInfo.session.decrypt(messageType, ciphertext);
sessionInfo.lastReceivedMessageTs = Date.now(); sessionInfo.lastReceivedMessageTs = Date.now();
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn); this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);

View File

@@ -23,7 +23,7 @@ limitations under the License.
*/ */
import Promise from 'bluebird'; import Promise from 'bluebird';
import logger from '../../../src/logger'; import logger from '../../logger';
const utils = require("../../utils"); const utils = require("../../utils");
const olmlib = require("../olmlib"); const olmlib = require("../olmlib");

View File

@@ -1224,6 +1224,132 @@ Crypto.prototype.setDeviceVerification = async function(
}; };
function verificationEventHandler(target, userId, roomId, eventId) {
return function(event) {
// listen for events related to this verification
if (event.getRoomId() !== roomId
|| event.getSender() !== userId) {
return;
}
const content = event.getContent();
if (!content["m.relates_to"]) {
return;
}
const relatesTo
= content["m.relationship"] || content["m.relates_to"];
if (!relatesTo.rel_type
|| relatesTo.rel_type !== "m.reference"
|| !relatesTo.event_id
|| relatesTo.event_id !== eventId) {
return;
}
// the event seems to be related to this verification, so pass it on to
// the verification handler
target.handleEvent(event);
};
}
Crypto.prototype.requestVerificationDM = async function(userId, roomId, methods) {
let methodMap;
if (methods) {
methodMap = new Map();
for (const method of methods) {
if (typeof method === "string") {
methodMap.set(method, defaultVerificationMethods[method]);
} else if (method.NAME) {
methodMap.set(method.NAME, method);
}
}
} else {
methodMap = this._baseApis._crypto._verificationMethods;
}
let eventId = undefined;
const listenPromise = new Promise((_resolve, _reject) => {
const listener = (event) => {
// listen for events related to this verification
if (event.getRoomId() !== roomId
|| event.getSender() !== userId) {
return;
}
const relatesTo = event.getRelation();
if (!relatesTo || !relatesTo.rel_type
|| relatesTo.rel_type !== "m.reference"
|| !relatesTo.event_id
|| relatesTo.event_id !== eventId) {
return;
}
const content = event.getContent();
// the event seems to be related to this verification
switch (event.getType()) {
case "m.key.verification.start": {
const verifier = new (methodMap.get(content.method))(
this._baseApis, userId, content.from_device, eventId,
roomId, event,
);
verifier.handler = verificationEventHandler(
verifier, userId, roomId, eventId,
);
// this handler gets removed when the verification finishes
// (see the verify method of crypto/verification/Base.js)
this._baseApis.on("event", verifier.handler);
resolve(verifier);
break;
}
case "m.key.verification.cancel": {
reject(event);
break;
}
}
};
this._baseApis.on("event", listener);
const resolve = (...args) => {
this._baseApis.off("event", listener);
_resolve(...args);
};
const reject = (...args) => {
this._baseApis.off("event", listener);
_reject(...args);
};
});
const res = await this._baseApis.sendEvent(
roomId, "m.room.message",
{
body: this._baseApis.getUserId() + " is requesting to verify " +
"your key, but your client does not support in-chat key " +
"verification. You will need to use legacy key " +
"verification to verify keys.",
msgtype: "m.key.verification.request",
to: userId,
from_device: this._baseApis.getDeviceId(),
methods: [...methodMap.keys()],
},
);
eventId = res.event_id;
return listenPromise;
};
Crypto.prototype.acceptVerificationDM = function(event, Method) {
if (typeof(Method) === "string") {
Method = defaultVerificationMethods[Method];
}
const content = event.getContent();
const verifier = new Method(
this._baseApis, event.getSender(), content.from_device, event.getId(),
event.getRoomId(),
);
verifier.handler = verificationEventHandler(
verifier, event.getSender(), event.getRoomId(), event.getId(),
);
this._baseApis.on("event", verifier.handler);
return verifier;
};
Crypto.prototype.requestVerification = function(userId, methods, devices) { Crypto.prototype.requestVerification = function(userId, methods, devices) {
if (!methods) { if (!methods) {
// .keys() returns an iterator, so we need to explicitly turn it into an array // .keys() returns an iterator, so we need to explicitly turn it into an array
@@ -1271,20 +1397,7 @@ Crypto.prototype.beginKeyVerification = function(
this._verificationTransactions.set(userId, new Map()); this._verificationTransactions.set(userId, new Map());
} }
transactionId = transactionId || randomString(32); transactionId = transactionId || randomString(32);
if (method instanceof Array) { if (this._verificationMethods.has(method)) {
if (method.length !== 2
|| !this._verificationMethods.has(method[0])
|| !this._verificationMethods.has(method[1])) {
throw newUnknownMethodError();
}
/*
return new TwoPartVerification(
this._verificationMethods[method[0]],
this._verificationMethods[method[1]],
userId, deviceId, transactionId,
);
*/
} else if (this._verificationMethods.has(method)) {
const verifier = new (this._verificationMethods.get(method))( const verifier = new (this._verificationMethods.get(method))(
this._baseApis, userId, deviceId, transactionId, this._baseApis, userId, deviceId, transactionId,
); );
@@ -1419,6 +1532,15 @@ Crypto.prototype.forceDiscardSession = function(roomId) {
* the device query is always inhibited as the members are not tracked. * the device query is always inhibited as the members are not tracked.
*/ */
Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDeviceQuery) { Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDeviceQuery) {
// ignore crypto events with no algorithm defined
// This will happen if a crypto event is redacted before we fetch the room state
// It would otherwise just throw later as an unknown algorithm would, but we may
// as well catch this here
if (!config.algorithm) {
console.log("Ignoring setRoomEncryption with no algorithm");
return;
}
// if state is being replayed from storage, we might already have a configuration // if state is being replayed from storage, we might already have a configuration
// for this room as they are persisted as well. // for this room as they are persisted as well.
// We just need to make sure the algorithm is initialized in this case. // We just need to make sure the algorithm is initialized in this case.
@@ -1756,6 +1878,18 @@ Crypto.prototype.backupGroupSession = async function(
* upload in the background as soon as possible. * upload in the background as soon as possible.
*/ */
Crypto.prototype.scheduleAllGroupSessionsForBackup = async function() { Crypto.prototype.scheduleAllGroupSessionsForBackup = async function() {
await this.flagAllGroupSessionsForBackup();
// Schedule keys to upload in the background as soon as possible.
this.scheduleKeyBackupSend(0 /* maxDelay */);
};
/**
* Marks all group sessions as needing to be backed up without scheduling
* them to upload in the background.
* @returns {Promise<int>} Resolves to the number of sessions requiring a backup.
*/
Crypto.prototype.flagAllGroupSessionsForBackup = async function() {
await this._cryptoStore.doTxn( await this._cryptoStore.doTxn(
'readwrite', 'readwrite',
[ [
@@ -1773,9 +1907,7 @@ Crypto.prototype.scheduleAllGroupSessionsForBackup = async function() {
const remaining = await this._cryptoStore.countSessionsNeedingBackup(); const remaining = await this._cryptoStore.countSessionsNeedingBackup();
this.emit("crypto.keyBackupSessionsRemaining", remaining); this.emit("crypto.keyBackupSessionsRemaining", remaining);
return remaining;
// Schedule keys to upload in the background as soon as possible.
this.scheduleKeyBackupSend(0 /* maxDelay */);
}; };
/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307 /* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307
@@ -2122,6 +2254,11 @@ Crypto.prototype._onRoomKeyEvent = function(event) {
* @param {module:models/event.MatrixEvent} event verification request event * @param {module:models/event.MatrixEvent} event verification request event
*/ */
Crypto.prototype._onKeyVerificationRequest = function(event) { Crypto.prototype._onKeyVerificationRequest = function(event) {
if (event.isCancelled()) {
logger.warn("Ignoring flagged verification request from " + event.getSender());
return;
}
const content = event.getContent(); const content = event.getContent();
if (!("from_device" in content) || typeof content.from_device !== "string" if (!("from_device" in content) || typeof content.from_device !== "string"
|| !("transaction_id" in content) || typeof content.from_device !== "string" || !("transaction_id" in content) || typeof content.from_device !== "string"
@@ -2238,6 +2375,11 @@ Crypto.prototype._onKeyVerificationRequest = function(event) {
* @param {module:models/event.MatrixEvent} event verification start event * @param {module:models/event.MatrixEvent} event verification start event
*/ */
Crypto.prototype._onKeyVerificationStart = function(event) { Crypto.prototype._onKeyVerificationStart = function(event) {
if (event.isCancelled()) {
logger.warn("Ignoring flagged verification start from " + event.getSender());
return;
}
const sender = event.getSender(); const sender = event.getSender();
const content = event.getContent(); const content = event.getContent();
const transactionId = content.transaction_id; const transactionId = content.transaction_id;
@@ -2271,22 +2413,6 @@ Crypto.prototype._onKeyVerificationStart = function(event) {
transaction_id: content.transactionId, transaction_id: content.transactionId,
})); }));
return; return;
} else if (content.next_method) {
if (!this._verificationMethods.has(content.next_method)) {
cancel(newUnknownMethodError({
transaction_id: content.transactionId,
}));
return;
} else {
/* TODO:
const verification = new TwoPartVerification(
this._verificationMethods[content.method],
this._verificationMethods[content.next_method],
userId, deviceId,
);
this.emit(verification.event_type, verification);
this.emit(verification.first.event_type, verification);*/
}
} else { } else {
const verifier = new (this._verificationMethods.get(content.method))( const verifier = new (this._verificationMethods.get(content.method))(
this._baseApis, sender, deviceId, content.transaction_id, this._baseApis, sender, deviceId, content.transaction_id,
@@ -2341,8 +2467,6 @@ Crypto.prototype._onKeyVerificationStart = function(event) {
handler.request.resolve(verifier); handler.request.resolve(verifier);
} }
} else {
// FIXME: make sure we're in a two-part verification, and the start matches the second part
} }
} }
this._baseApis.emit("crypto.verification.start", verifier); this._baseApis.emit("crypto.verification.start", verifier);

View File

@@ -719,7 +719,7 @@ function promiseifyTxn(txn) {
if (txn._mx_abortexception !== undefined) { if (txn._mx_abortexception !== undefined) {
reject(txn._mx_abortexception); reject(txn._mx_abortexception);
} else { } else {
localStorage.log("Error performing indexeddb txn", event); logger.log("Error performing indexeddb txn", event);
reject(event.target.error); reject(event.target.error);
} }
}; };

View File

@@ -17,7 +17,7 @@ limitations under the License.
import Promise from 'bluebird'; import Promise from 'bluebird';
import logger from '../../logger'; import logger from '../../logger';
import MemoryCryptoStore from './memory-crypto-store.js'; import MemoryCryptoStore from './memory-crypto-store';
/** /**
* Internal module. Partial localStorage backed storage for e2e. * Internal module. Partial localStorage backed storage for e2e.

View File

@@ -23,6 +23,9 @@ import {MatrixEvent} from '../../models/event';
import {EventEmitter} from 'events'; import {EventEmitter} from 'events';
import logger from '../../logger'; import logger from '../../logger';
import DeviceInfo from '../deviceinfo'; import DeviceInfo from '../deviceinfo';
import {newTimeoutError} from "./Error";
const timeoutException = new Error("Verification timed out");
export default class VerificationBase extends EventEmitter { export default class VerificationBase extends EventEmitter {
/** /**
@@ -45,27 +48,66 @@ export default class VerificationBase extends EventEmitter {
* *
* @param {string} transactionId the transaction ID to be used when sending events * @param {string} transactionId the transaction ID to be used when sending events
* *
* @param {object} startEvent the m.key.verification.start event that * @param {string} [roomId] the room to use for verification
*
* @param {object} [startEvent] the m.key.verification.start event that
* initiated this verification, if any * initiated this verification, if any
* *
* @param {object} request the key verification request object related to * @param {object} [request] the key verification request object related to
* this verification, if any * this verification, if any
*
* @param {object} parent parent verification for this verification, if any
*/ */
constructor(baseApis, userId, deviceId, transactionId, startEvent, request, parent) { constructor(baseApis, userId, deviceId, transactionId, roomId, startEvent, request) {
super(); super();
this._baseApis = baseApis; this._baseApis = baseApis;
this.userId = userId; this.userId = userId;
this.deviceId = deviceId; this.deviceId = deviceId;
this.transactionId = transactionId; this.transactionId = transactionId;
this.startEvent = startEvent; if (typeof(roomId) === "string" || roomId instanceof String) {
this.request = request; this.roomId = roomId;
this._parent = parent; this.startEvent = startEvent;
this.request = request;
} else {
// if room ID was omitted, but start event and request were not
this.startEvent= roomId;
this.request = startEvent;
}
this.cancelled = false;
this._done = false; this._done = false;
this._promise = null; this._promise = null;
this._transactionTimeoutTimer = null;
// At this point, the verification request was received so start the timeout timer.
this._resetTimer();
if (this.roomId) {
this._send = this._sendMessage;
} else {
this._send = this._sendToDevice;
}
} }
_resetTimer() {
logger.info("Refreshing/starting the verification transaction timeout timer");
if (this._transactionTimeoutTimer !== null) {
clearTimeout(this._transactionTimeoutTimer);
}
this._transactionTimeoutTimer = setTimeout(() => {
if (!this._done && !this.cancelled) {
logger.info("Triggering verification timeout");
this.cancel(timeoutException);
}
}, 10 * 60 * 1000); // 10 minutes
}
_endTimer() {
if (this._transactionTimeoutTimer !== null) {
clearTimeout(this._transactionTimeoutTimer);
this._transactionTimeoutTimer = null;
}
}
/* send a message to the other participant, using to-device messages
*/
_sendToDevice(type, content) { _sendToDevice(type, content) {
if (this._done) { if (this._done) {
return Promise.reject(new Error("Verification is already done")); return Promise.reject(new Error("Verification is already done"));
@@ -76,6 +118,21 @@ export default class VerificationBase extends EventEmitter {
}); });
} }
/* send a message to the other participant, using in-roomm messages
*/
_sendMessage(type, content) {
if (this._done) {
return Promise.reject(new Error("Verification is already done"));
}
// FIXME: if MSC1849 decides to use m.relationship instead of
// m.relates_to, we should follow suit here
content["m.relates_to"] = {
rel_type: "m.reference",
event_id: this.transactionId,
};
return this._baseApis.sendEvent(this.roomId, type, content);
}
_waitForEvent(type) { _waitForEvent(type) {
if (this._done) { if (this._done) {
return Promise.reject(new Error("Verification is already done")); return Promise.reject(new Error("Verification is already done"));
@@ -93,6 +150,7 @@ export default class VerificationBase extends EventEmitter {
} else if (e.getType() === this._expectedEvent) { } else if (e.getType() === this._expectedEvent) {
this._expectedEvent = undefined; this._expectedEvent = undefined;
this._rejectEvent = undefined; this._rejectEvent = undefined;
this._resetTimer();
this._resolveEvent(e); this._resolveEvent(e);
} else { } else {
this._expectedEvent = undefined; this._expectedEvent = undefined;
@@ -110,17 +168,27 @@ export default class VerificationBase extends EventEmitter {
} }
done() { done() {
this._endTimer(); // always kill the activity timer
if (!this._done) { if (!this._done) {
if (this.roomId) {
// verification in DM requires a done message
this._send("m.key.verification.done", {});
}
this._resolve(); this._resolve();
} }
} }
cancel(e) { cancel(e) {
this._endTimer(); // always kill the activity timer
if (!this._done) { if (!this._done) {
this.cancelled = true;
if (this.userId && this.deviceId && this.transactionId) { if (this.userId && this.deviceId && this.transactionId) {
// send a cancellation to the other user (if it wasn't // send a cancellation to the other user (if it wasn't
// cancelled by the other user) // cancelled by the other user)
if (e instanceof MatrixEvent) { if (e === timeoutException) {
const timeoutEvent = newTimeoutError();
this._send(timeoutEvent.getType(), timeoutEvent.getContent());
} else if (e instanceof MatrixEvent) {
const sender = e.getSender(); const sender = e.getSender();
if (sender !== this.userId) { if (sender !== this.userId) {
const content = e.getContent(); const content = e.getContent();
@@ -129,9 +197,9 @@ export default class VerificationBase extends EventEmitter {
content.reason = content.reason || content.body content.reason = content.reason || content.body
|| "Unknown reason"; || "Unknown reason";
content.transaction_id = this.transactionId; content.transaction_id = this.transactionId;
this._sendToDevice("m.key.verification.cancel", content); this._send("m.key.verification.cancel", content);
} else { } else {
this._sendToDevice("m.key.verification.cancel", { this._send("m.key.verification.cancel", {
code: "m.unknown", code: "m.unknown",
reason: content.body || "Unknown reason", reason: content.body || "Unknown reason",
transaction_id: this.transactionId, transaction_id: this.transactionId,
@@ -139,7 +207,7 @@ export default class VerificationBase extends EventEmitter {
} }
} }
} else { } else {
this._sendToDevice("m.key.verification.cancel", { this._send("m.key.verification.cancel", {
code: "m.unknown", code: "m.unknown",
reason: e.toString(), reason: e.toString(),
transaction_id: this.transactionId, transaction_id: this.transactionId,
@@ -147,7 +215,9 @@ export default class VerificationBase extends EventEmitter {
} }
} }
if (this._promise !== null) { if (this._promise !== null) {
this._reject(e); // when we cancel without a promise, we end up with a promise
// but no reject function. If cancel is called again, we'd error.
if (this._reject) this._reject(e);
} else { } else {
this._promise = Promise.reject(e); this._promise = Promise.reject(e);
} }
@@ -169,15 +239,24 @@ export default class VerificationBase extends EventEmitter {
this._promise = new Promise((resolve, reject) => { this._promise = new Promise((resolve, reject) => {
this._resolve = (...args) => { this._resolve = (...args) => {
this._done = true; this._done = true;
this._endTimer();
if (this.handler) {
this._baseApis.off("event", this.handler);
}
resolve(...args); resolve(...args);
}; };
this._reject = (...args) => { this._reject = (...args) => {
this._done = true; this._done = true;
this._endTimer();
if (this.handler) {
this._baseApis.off("event", this.handler);
}
reject(...args); reject(...args);
}; };
}); });
if (this._doVerification && !this._started) { if (this._doVerification && !this._started) {
this._started = true; this._started = true;
this._resetTimer(); // restart the timeout
Promise.resolve(this._doVerification()) Promise.resolve(this._doVerification())
.then(this.done.bind(this), this.cancel.bind(this)); .then(this.done.bind(this), this.cancel.bind(this));
} }

View File

@@ -213,9 +213,10 @@ export default class SAS extends Base {
message_authentication_codes: MAC_LIST, message_authentication_codes: MAC_LIST,
// FIXME: allow app to specify what SAS methods can be used // FIXME: allow app to specify what SAS methods can be used
short_authentication_string: SAS_LIST, short_authentication_string: SAS_LIST,
transaction_id: this.transactionId,
}; };
this._sendToDevice("m.key.verification.start", initialMessage); // NOTE: this._send will modify initialMessage to include the
// transaction_id field, or the m.relationship/m.relates_to field
this._send("m.key.verification.start", initialMessage);
let e = await this._waitForEvent("m.key.verification.accept"); let e = await this._waitForEvent("m.key.verification.accept");
@@ -235,7 +236,7 @@ export default class SAS extends Base {
const hashCommitment = content.commitment; const hashCommitment = content.commitment;
const olmSAS = new global.Olm.SAS(); const olmSAS = new global.Olm.SAS();
try { try {
this._sendToDevice("m.key.verification.key", { this._send("m.key.verification.key", {
key: olmSAS.get_pubkey(), key: olmSAS.get_pubkey(),
}); });
@@ -306,7 +307,7 @@ export default class SAS extends Base {
const olmSAS = new global.Olm.SAS(); const olmSAS = new global.Olm.SAS();
try { try {
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content); const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
this._sendToDevice("m.key.verification.accept", { this._send("m.key.verification.accept", {
key_agreement_protocol: keyAgreement, key_agreement_protocol: keyAgreement,
hash: hashMethod, hash: hashMethod,
message_authentication_code: macMethod, message_authentication_code: macMethod,
@@ -320,7 +321,7 @@ export default class SAS extends Base {
// FIXME: make sure event is properly formed // FIXME: make sure event is properly formed
content = e.getContent(); content = e.getContent();
olmSAS.set_their_key(content.key); olmSAS.set_their_key(content.key);
this._sendToDevice("m.key.verification.key", { this._send("m.key.verification.key", {
key: olmSAS.get_pubkey(), key: olmSAS.get_pubkey(),
}); });
@@ -382,7 +383,7 @@ export default class SAS extends Base {
keyList.sort().join(","), keyList.sort().join(","),
baseInfo + "KEY_IDS", baseInfo + "KEY_IDS",
); );
this._sendToDevice("m.key.verification.mac", { mac, keys }); this._send("m.key.verification.mac", { mac, keys });
} }
async _checkMAC(olmSAS, content, method) { async _checkMAC(olmSAS, content, method) {

View File

@@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -22,7 +23,7 @@ import Promise from 'bluebird';
const parseContentType = require('content-type').parse; const parseContentType = require('content-type').parse;
const utils = require("./utils"); const utils = require("./utils");
import logger from '../src/logger'; import logger from './logger';
// we use our own implementation of setTimeout, so that if we get suspended in // we use our own implementation of setTimeout, so that if we get suspended in
// the middle of a /sync, we cancel the sync as soon as we awake, rather than // the middle of a /sync, we cancel the sync as soon as we awake, rather than
@@ -46,10 +47,15 @@ module.exports.PREFIX_R0 = "/_matrix/client/r0";
module.exports.PREFIX_UNSTABLE = "/_matrix/client/unstable"; module.exports.PREFIX_UNSTABLE = "/_matrix/client/unstable";
/** /**
* URI path for the identity API * URI path for v1 of the the identity API
*/ */
module.exports.PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1"; module.exports.PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1";
/**
* URI path for the v2 identity API
*/
module.exports.PREFIX_IDENTITY_V2 = "/_matrix/identity/v2";
/** /**
* URI path for the media repo API * URI path for the media repo API
*/ */
@@ -90,6 +96,13 @@ module.exports.MatrixHttpApi = function MatrixHttpApi(event_emitter, opts) {
}; };
module.exports.MatrixHttpApi.prototype = { module.exports.MatrixHttpApi.prototype = {
/**
* Sets the baase URL for the identity server
* @param {string} url The new base url
*/
setIdBaseUrl: function(url) {
this.opts.idBaseUrl = url;
},
/** /**
* Get the content repository url with query parameters. * Get the content repository url with query parameters.
@@ -102,7 +115,7 @@ module.exports.MatrixHttpApi.prototype = {
}; };
return { return {
base: this.opts.baseUrl, base: this.opts.baseUrl,
path: "/_matrix/media/v1/upload", path: "/_matrix/media/r0/upload",
params: params, params: params,
}; };
}, },
@@ -291,7 +304,7 @@ module.exports.MatrixHttpApi.prototype = {
}); });
} }
}); });
let url = this.opts.baseUrl + "/_matrix/media/v1/upload"; let url = this.opts.baseUrl + "/_matrix/media/r0/upload";
const queryArgs = []; const queryArgs = [];
@@ -327,7 +340,7 @@ module.exports.MatrixHttpApi.prototype = {
promise = this.authedRequest( promise = this.authedRequest(
opts.callback, "POST", "/upload", queryParams, body, { opts.callback, "POST", "/upload", queryParams, body, {
prefix: "/_matrix/media/v1", prefix: "/_matrix/media/r0",
headers: {"Content-Type": contentType}, headers: {"Content-Type": contentType},
json: false, json: false,
bodyParser: bodyParser, bodyParser: bodyParser,
@@ -368,7 +381,18 @@ module.exports.MatrixHttpApi.prototype = {
return this.uploads; return this.uploads;
}, },
idServerRequest: function(callback, method, path, params, prefix) { idServerRequest: function(
callback,
method,
path,
params,
prefix,
accessToken,
) {
if (!this.opts.idBaseUrl) {
throw new Error("No Identity Server base URL set");
}
const fullUri = this.opts.idBaseUrl + prefix + path; const fullUri = this.opts.idBaseUrl + prefix + path;
if (callback !== undefined && !utils.isFunction(callback)) { if (callback !== undefined && !utils.isFunction(callback)) {
@@ -381,13 +405,17 @@ module.exports.MatrixHttpApi.prototype = {
uri: fullUri, uri: fullUri,
method: method, method: method,
withCredentials: false, withCredentials: false,
json: false, json: true, // we want a JSON response if we can
_matrix_opts: this.opts, _matrix_opts: this.opts,
headers: {},
}; };
if (method == 'GET') { if (method === 'GET') {
opts.qs = params; opts.qs = params;
} else { } else if (typeof params === "object") {
opts.form = params; opts.json = params;
}
if (accessToken) {
opts.headers['Authorization'] = `Bearer ${accessToken}`;
} }
const defer = Promise.defer(); const defer = Promise.defer();
@@ -395,12 +423,7 @@ module.exports.MatrixHttpApi.prototype = {
opts, opts,
requestCallback(defer, callback, this.opts.onlyData), requestCallback(defer, callback, this.opts.onlyData),
); );
// ID server does not always take JSON, so we can't use requests' 'json' return defer.promise;
// option as we do with the home server, but it does return JSON, so
// parse it manually
return defer.promise.then(function(response) {
return JSON.parse(response);
});
}, },
/** /**
@@ -470,7 +493,7 @@ module.exports.MatrixHttpApi.prototype = {
const self = this; const self = this;
requestPromise.catch(function(err) { requestPromise.catch(function(err) {
if (err.errcode == 'M_UNKNOWN_TOKEN') { if (err.errcode == 'M_UNKNOWN_TOKEN') {
self.event_emitter.emit("Session.logged_out"); self.event_emitter.emit("Session.logged_out", err);
} else if (err.errcode == 'M_CONSENT_NOT_GIVEN') { } else if (err.errcode == 'M_CONSENT_NOT_GIVEN') {
self.event_emitter.emit( self.event_emitter.emit(
"no_consent", "no_consent",
@@ -525,76 +548,6 @@ module.exports.MatrixHttpApi.prototype = {
); );
}, },
/**
* Perform an authorised request to the homeserver with a specific path
* prefix which overrides the default for this call only. Useful for hitting
* different Matrix Client-Server versions.
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
* @param {Object} data The HTTP JSON body.
* @param {string} prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha".
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*
* @deprecated prefer authedRequest with opts.prefix
*/
authedRequestWithPrefix: function(callback, method, path, queryParams, data,
prefix, localTimeoutMs) {
return this.authedRequest(
callback, method, path, queryParams, data, {
localTimeoutMs: localTimeoutMs,
prefix: prefix,
},
);
},
/**
* Perform a request to the homeserver without any credentials but with a
* specific path prefix which overrides the default for this call only.
* Useful for hitting different Matrix Client-Server versions.
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
* @param {Object} data The HTTP JSON body.
* @param {string} prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha".
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*
* @deprecated prefer request with opts.prefix
*/
requestWithPrefix: function(callback, method, path, queryParams, data, prefix,
localTimeoutMs) {
return this.request(
callback, method, path, queryParams, data, {
localTimeoutMs: localTimeoutMs,
prefix: prefix,
},
);
},
/** /**
* Perform a request to an arbitrary URL. * Perform a request to an arbitrary URL.
* @param {Function} callback Optional. The callback to invoke on * @param {Function} callback Optional. The callback to invoke on
@@ -883,7 +836,8 @@ function parseErrorResponse(response, body) {
let err; let err;
if (contentType) { if (contentType) {
if (contentType.type === 'application/json') { if (contentType.type === 'application/json') {
err = new module.exports.MatrixError(JSON.parse(body)); const jsonBody = typeof(body) === 'object' ? body : JSON.parse(body);
err = new module.exports.MatrixError(jsonBody);
} else if (contentType.type === 'text/plain') { } else if (contentType.type === 'text/plain') {
err = new Error(`Server returned ${httpStatus} error: ${body}`); err = new Error(`Server returned ${httpStatus} error: ${body}`);
} }

View File

@@ -22,7 +22,7 @@ import Promise from 'bluebird';
const url = require("url"); const url = require("url");
const utils = require("./utils"); const utils = require("./utils");
import logger from '../src/logger'; import logger from './logger';
const EMAIL_STAGE_TYPE = "m.login.email.identity"; const EMAIL_STAGE_TYPE = "m.login.email.identity";
const MSISDN_STAGE_TYPE = "m.login.msisdn"; const MSISDN_STAGE_TYPE = "m.login.msisdn";
@@ -174,16 +174,19 @@ InteractiveAuth.prototype = {
// The email can be validated out-of-band, but we need to provide the // The email can be validated out-of-band, but we need to provide the
// creds so the HS can go & check it. // creds so the HS can go & check it.
if (this._emailSid) { if (this._emailSid) {
const idServerParsedUrl = url.parse( const creds = {
this._matrixClient.getIdentityServerUrl(), sid: this._emailSid,
); client_secret: this._clientSecret,
};
if (await this._matrixClient.doesServerRequireIdServerParam()) {
const idServerParsedUrl = url.parse(
this._matrixClient.getIdentityServerUrl(),
);
creds.id_server = idServerParsedUrl.host;
}
authDict = { authDict = {
type: EMAIL_STAGE_TYPE, type: EMAIL_STAGE_TYPE,
threepid_creds: { threepid_creds: creds,
sid: this._emailSid,
client_secret: this._clientSecret,
id_server: idServerParsedUrl.host,
},
}; };
} }
} }

View File

@@ -1,6 +1,7 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -76,6 +77,7 @@ module.exports.InteractiveAuth = require("./interactive-auth");
/** The {@link module:auto-discovery|AutoDiscovery} class. */ /** The {@link module:auto-discovery|AutoDiscovery} class. */
module.exports.AutoDiscovery = require("./autodiscovery").AutoDiscovery; module.exports.AutoDiscovery = require("./autodiscovery").AutoDiscovery;
module.exports.SERVICE_TYPES = require('./service-types').SERVICE_TYPES;
module.exports.MemoryCryptoStore = module.exports.MemoryCryptoStore =
require("./crypto/store/memory-crypto-store").default; require("./crypto/store/memory-crypto-store").default;

View File

@@ -21,7 +21,7 @@ const EventEmitter = require("events").EventEmitter;
const utils = require("../utils"); const utils = require("../utils");
const EventTimeline = require("./event-timeline"); const EventTimeline = require("./event-timeline");
import {EventStatus} from "./event"; import {EventStatus} from "./event";
import logger from '../../src/logger'; import logger from '../logger';
import Relations from './relations'; import Relations from './relations';
// var DEBUG = false; // var DEBUG = false;
@@ -92,6 +92,13 @@ function EventTimelineSet(room, opts) {
} }
utils.inherits(EventTimelineSet, EventEmitter); utils.inherits(EventTimelineSet, EventEmitter);
/**
* Get all the timelines in this set
* @return {module:models/event-timeline~EventTimeline[]} the timelines in this set
*/
EventTimelineSet.prototype.getTimelines = function() {
return this._timelines;
};
/** /**
* Get the filter object this timeline set is filtered on, if any * Get the filter object this timeline set is filtered on, if any
* @return {?Filter} the optional filter for this timelineSet * @return {?Filter} the optional filter for this timelineSet
@@ -438,7 +445,6 @@ EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimel
if (backwardsIsLive || forwardsIsLive) { if (backwardsIsLive || forwardsIsLive) {
// The live timeline should never be spliced into a non-live position. // The live timeline should never be spliced into a non-live position.
// We use independent logging to better discover the problem at a glance. // We use independent logging to better discover the problem at a glance.
logger.warn({backwardsIsLive, forwardsIsLive}); // debugging
if (backwardsIsLive) { if (backwardsIsLive) {
logger.warn( logger.warn(
"Refusing to set a preceding existingTimeLine on our " + "Refusing to set a preceding existingTimeLine on our " +
@@ -689,9 +695,12 @@ EventTimelineSet.prototype.compareEventOrdering = function(eventId1, eventId2) {
* The type of relation involved, such as "m.annotation", "m.reference", "m.replace", etc. * The type of relation involved, such as "m.annotation", "m.reference", "m.replace", etc.
* @param {String} eventType * @param {String} eventType
* The relation event's type, such as "m.reaction", etc. * The relation event's type, such as "m.reaction", etc.
* @throws If <code>eventId</code>, <code>relationType</code> or <code>eventType</code>
* are not valid.
* *
* @returns {Relations} * @returns {?Relations}
* A container for relation events. * A container for relation events or undefined if there are no relation events for
* the relationType.
*/ */
EventTimelineSet.prototype.getRelationsForEvent = function( EventTimelineSet.prototype.getRelationsForEvent = function(
eventId, relationType, eventType, eventId, relationType, eventType,

View File

@@ -24,14 +24,14 @@ limitations under the License.
import Promise from 'bluebird'; import Promise from 'bluebird';
import {EventEmitter} from 'events'; import {EventEmitter} from 'events';
import utils from '../utils.js'; import utils from '../utils.js';
import logger from '../../src/logger'; import logger from '../logger';
/** /**
* Enum for event statuses. * Enum for event statuses.
* @readonly * @readonly
* @enum {string} * @enum {string}
*/ */
module.exports.EventStatus = { const EventStatus = {
/** The event was not sent and will no longer be retried. */ /** The event was not sent and will no longer be retried. */
NOT_SENT: "not_sent", NOT_SENT: "not_sent",
@@ -49,6 +49,7 @@ module.exports.EventStatus = {
/** The event was cancelled before it was successfully sent. */ /** The event was cancelled before it was successfully sent. */
CANCELLED: "cancelled", CANCELLED: "cancelled",
}; };
module.exports.EventStatus = EventStatus;
const interns = {}; const interns = {};
function intern(str) { function intern(str) {
@@ -124,7 +125,8 @@ module.exports.MatrixEvent = function MatrixEvent(
this.forwardLooking = true; this.forwardLooking = true;
this._pushActions = null; this._pushActions = null;
this._replacingEvent = null; this._replacingEvent = null;
this._locallyRedacted = false; this._localRedactionEvent = null;
this._isCancelled = false;
this._clearEvent = {}; this._clearEvent = {};
@@ -229,7 +231,7 @@ utils.extend(module.exports.MatrixEvent.prototype, {
* @return {Object} The event content JSON, or an empty object. * @return {Object} The event content JSON, or an empty object.
*/ */
getOriginalContent: function() { getOriginalContent: function() {
if (this._locallyRedacted) { if (this._localRedactionEvent) {
return {}; return {};
} }
return this._clearEvent.content || this.event.content || {}; return this._clearEvent.content || this.event.content || {};
@@ -243,7 +245,7 @@ utils.extend(module.exports.MatrixEvent.prototype, {
* @return {Object} The event content JSON, or an empty object. * @return {Object} The event content JSON, or an empty object.
*/ */
getContent: function() { getContent: function() {
if (this._locallyRedacted) { if (this._localRedactionEvent) {
return {}; return {};
} else if (this._replacingEvent) { } else if (this._replacingEvent) {
return this._replacingEvent.getContent()["m.new_content"] || {}; return this._replacingEvent.getContent()["m.new_content"] || {};
@@ -673,20 +675,20 @@ utils.extend(module.exports.MatrixEvent.prototype, {
}, },
unmarkLocallyRedacted: function() { unmarkLocallyRedacted: function() {
const value = this._locallyRedacted; const value = this._localRedactionEvent;
this._locallyRedacted = false; this._localRedactionEvent = null;
if (this.event.unsigned) { if (this.event.unsigned) {
this.event.unsigned.redacted_because = null; this.event.unsigned.redacted_because = null;
} }
return value; return !!value;
}, },
markLocallyRedacted: function(redactionEvent) { markLocallyRedacted: function(redactionEvent) {
if (this._locallyRedacted) { if (this._localRedactionEvent) {
return; return;
} }
this.emit("Event.beforeRedaction", this, redactionEvent); this.emit("Event.beforeRedaction", this, redactionEvent);
this._locallyRedacted = true; this._localRedactionEvent = redactionEvent;
if (!this.event.unsigned) { if (!this.event.unsigned) {
this.event.unsigned = {}; this.event.unsigned = {};
} }
@@ -706,7 +708,7 @@ utils.extend(module.exports.MatrixEvent.prototype, {
throw new Error("invalid redaction_event in makeRedacted"); throw new Error("invalid redaction_event in makeRedacted");
} }
this._locallyRedacted = false; this._localRedactionEvent = null;
this.emit("Event.beforeRedaction", this, redaction_event); this.emit("Event.beforeRedaction", this, redaction_event);
@@ -867,7 +869,11 @@ utils.extend(module.exports.MatrixEvent.prototype, {
* @param {MatrixEvent?} newEvent the event with the replacing content, if any. * @param {MatrixEvent?} newEvent the event with the replacing content, if any.
*/ */
makeReplaced(newEvent) { makeReplaced(newEvent) {
if (this.isRedacted()) { // don't allow redacted events to be replaced.
// if newEvent is null we allow to go through though,
// as with local redaction, the replacing event might get
// cancelled, which should be reflected on the target event.
if (this.isRedacted() && newEvent) {
return; return;
} }
if (this._replacingEvent !== newEvent) { if (this._replacingEvent !== newEvent) {
@@ -877,15 +883,25 @@ utils.extend(module.exports.MatrixEvent.prototype, {
}, },
/** /**
* Returns the status of the event, or the replacing event in case `makeReplace` has been called. * Returns the status of any associated edit or redaction
* (not for reactions/annotations as their local echo doesn't affect the orignal event),
* or else the status of the event.
* *
* @return {EventStatus} * @return {EventStatus}
*/ */
replacementOrOwnStatus() { getAssociatedStatus() {
if (this._replacingEvent) { if (this._replacingEvent) {
return this._replacingEvent.status; return this._replacingEvent.status;
} else { } else if (this._localRedactionEvent) {
return this.status; return this._localRedactionEvent.status;
}
return this.status;
},
getServerAggregatedRelation(relType) {
const relations = this.getUnsigned()["m.relations"];
if (relations) {
return relations[relType];
} }
}, },
@@ -895,11 +911,18 @@ utils.extend(module.exports.MatrixEvent.prototype, {
* @return {string?} * @return {string?}
*/ */
replacingEventId() { replacingEventId() {
return this._replacingEvent && this._replacingEvent.getId(); const replaceRelation = this.getServerAggregatedRelation("m.replace");
if (replaceRelation) {
return replaceRelation.event_id;
} else if (this._replacingEvent) {
return this._replacingEvent.getId();
}
}, },
/** /**
* Returns the event replacing the content of this event, if any. * Returns the event replacing the content of this event, if any.
* Replacements are aggregated on the server, so this would only
* return an event in case it came down the sync, or for local echo of edits.
* *
* @return {MatrixEvent?} * @return {MatrixEvent?}
*/ */
@@ -907,6 +930,31 @@ utils.extend(module.exports.MatrixEvent.prototype, {
return this._replacingEvent; return this._replacingEvent;
}, },
/**
* Returns the origin_server_ts of the event replacing the content of this event, if any.
*
* @return {Date?}
*/
replacingEventDate() {
const replaceRelation = this.getServerAggregatedRelation("m.replace");
if (replaceRelation) {
const ts = replaceRelation.origin_server_ts;
if (Number.isFinite(ts)) {
return new Date(ts);
}
} else if (this._replacingEvent) {
return this._replacingEvent.getDate();
}
},
/**
* Returns the event that wants to redact this event, but hasn't been sent yet.
* @return {MatrixEvent} the event
*/
localRedactionEvent() {
return this._localRedactionEvent;
},
/** /**
* For relations and redactions, returns the event_id this event is referring to. * For relations and redactions, returns the event_id this event is referring to.
* *
@@ -947,6 +995,25 @@ utils.extend(module.exports.MatrixEvent.prototype, {
} }
}, },
/**
* Flags an event as cancelled due to future conditions. For example, a verification
* request event in the same sync transaction may be flagged as cancelled to warn
* listeners that a cancellation event is coming down the same pipe shortly.
* @param {boolean} cancelled Whether the event is to be cancelled or not.
*/
flagCancelled(cancelled = true) {
this._isCancelled = cancelled;
},
/**
* Gets whether or not the event is flagged as cancelled. See flagCancelled() for
* more information.
* @returns {boolean} True if the event is cancelled, false otherwise.
*/
isCancelled() {
return this._isCancelled;
},
/** /**
* Summarise the event as JSON for debugging. If encrypted, include both the * 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 * decrypted and encrypted view of the event. This is named `toJSON` for use
@@ -966,6 +1033,11 @@ utils.extend(module.exports.MatrixEvent.prototype, {
room_id: this.getRoomId(), room_id: this.getRoomId(),
}; };
// if this is a redaction then attach the redacts key
if (this.isRedaction()) {
event.redacts = this.event.redacts;
}
if (!this.isEncrypted()) { if (!this.isEncrypted()) {
return event; return event;
} }
@@ -981,7 +1053,7 @@ utils.extend(module.exports.MatrixEvent.prototype, {
/* _REDACT_KEEP_KEY_MAP gives the keys we keep when an event is redacted /* _REDACT_KEEP_KEY_MAP gives the keys we keep when an event is redacted
* *
* This is specified here: * This is specified here:
* http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#redactions * http://matrix.org/speculator/spec/HEAD/client_server/latest.html#redactions
* *
* Also: * Also:
* - We keep 'unsigned' since that is created by the local server * - We keep 'unsigned' since that is created by the local server

View File

@@ -301,10 +301,20 @@ export default class Relations extends EventEmitter {
// event is known anyway. // event is known anyway.
return null; return null;
} }
// the all-knowning server tells us that the event at some point had
// this timestamp for its replacement, so any following replacement should definitely not be less
const replaceRelation =
this._targetEvent.getServerAggregatedRelation("m.replace");
const minTs = replaceRelation && replaceRelation.origin_server_ts;
return this.getRelations().reduce((last, event) => { return this.getRelations().reduce((last, event) => {
if (event.getSender() !== this._targetEvent.getSender()) { if (event.getSender() !== this._targetEvent.getSender()) {
return last; return last;
} }
if (minTs && minTs > event.getTs()) {
return last;
}
if (last && last.getTs() > event.getTs()) { if (last && last.getTs() > event.getTs()) {
return last; return last;
} }

View File

@@ -249,7 +249,7 @@ RoomMember.prototype.getDMInviter = function() {
* "crop" or "scale". * "crop" or "scale".
* @param {Boolean} allowDefault (optional) Passing false causes this method to * @param {Boolean} allowDefault (optional) Passing false causes this method to
* return null if the user has no avatar image. Otherwise, a default image URL * return null if the user has no avatar image. Otherwise, a default image URL
* will be returned. Default: true. * will be returned. Default: true. (Deprecated)
* @param {Boolean} allowDirectLinks (optional) If true, the avatar URL will be * @param {Boolean} allowDirectLinks (optional) If true, the avatar URL will be
* returned even if it is a direct hyperlink rather than a matrix content URL. * returned even if it is a direct hyperlink rather than a matrix content URL.
* If false, any non-matrix content URLs will be ignored. Setting this option to * If false, any non-matrix content URLs will be ignored. Setting this option to

View File

@@ -21,7 +21,7 @@ const EventEmitter = require("events").EventEmitter;
const utils = require("../utils"); const utils = require("../utils");
const RoomMember = require("./room-member"); const RoomMember = require("./room-member");
import logger from '../../src/logger'; import logger from '../logger';
// possible statuses for out-of-band member loading // possible statuses for out-of-band member loading
const OOB_STATUS_NOTSTARTED = 1; const OOB_STATUS_NOTSTARTED = 1;

View File

@@ -1,6 +1,7 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018, 2019 New Vector Ltd Copyright 2018, 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -29,7 +30,7 @@ const ContentRepo = require("../content-repo");
const EventTimeline = require("./event-timeline"); const EventTimeline = require("./event-timeline");
const EventTimelineSet = require("./event-timeline-set"); const EventTimelineSet = require("./event-timeline-set");
import logger from '../../src/logger'; import logger from '../logger';
import ReEmitter from '../ReEmitter'; import ReEmitter from '../ReEmitter';
// These constants are used as sane defaults when the homeserver doesn't support // These constants are used as sane defaults when the homeserver doesn't support
@@ -38,7 +39,7 @@ import ReEmitter from '../ReEmitter';
// room versions which are considered okay for people to run without being asked // room versions which are considered okay for people to run without being asked
// to upgrade (ie: "stable"). Eventually, we should remove these when all homeservers // to upgrade (ie: "stable"). Eventually, we should remove these when all homeservers
// return an m.room_versions capability. // return an m.room_versions capability.
const KNOWN_SAFE_ROOM_VERSION = '1'; const KNOWN_SAFE_ROOM_VERSION = '4';
const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4']; const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4'];
function synthesizeReceipt(userId, event, receiptType) { function synthesizeReceipt(userId, event, receiptType) {
@@ -346,13 +347,30 @@ Room.prototype.userMayUpgradeRoom = function(userId) {
Room.prototype.getPendingEvents = function() { Room.prototype.getPendingEvents = function() {
if (this._opts.pendingEventOrdering !== "detached") { if (this._opts.pendingEventOrdering !== "detached") {
throw new Error( throw new Error(
"Cannot call getPendingEventList with pendingEventOrdering == " + "Cannot call getPendingEvents with pendingEventOrdering == " +
this._opts.pendingEventOrdering); this._opts.pendingEventOrdering);
} }
return this._pendingEventList; return this._pendingEventList;
}; };
/**
* Check whether the pending event list contains a given event by ID.
*
* @param {string} eventId The event ID to check for.
* @return {boolean}
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
*/
Room.prototype.hasPendingEvent = function(eventId) {
if (this._opts.pendingEventOrdering !== "detached") {
throw new Error(
"Cannot call hasPendingEvent with pendingEventOrdering == " +
this._opts.pendingEventOrdering);
}
return this._pendingEventList.some(event => event.getId() === eventId);
};
/** /**
* Get the live unfiltered timeline for this room. * Get the live unfiltered timeline for this room.
* *
@@ -763,7 +781,7 @@ Room.prototype.getBlacklistUnverifiedDevices = function() {
* @param {string} resizeMethod The thumbnail resize method to use, either * @param {string} resizeMethod The thumbnail resize method to use, either
* "crop" or "scale". * "crop" or "scale".
* @param {boolean} allowDefault True to allow an identicon for this room if an * @param {boolean} allowDefault True to allow an identicon for this room if an
* avatar URL wasn't explicitly set. Default: true. * avatar URL wasn't explicitly set. Default: true. (Deprecated)
* @return {?string} the avatar URL or null. * @return {?string} the avatar URL or null.
*/ */
Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod, Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
@@ -797,20 +815,20 @@ Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
* @return {array} The room's alias as an array of strings * @return {array} The room's alias as an array of strings
*/ */
Room.prototype.getAliases = function() { Room.prototype.getAliases = function() {
const alias_strings = []; const aliasStrings = [];
const alias_events = this.currentState.getStateEvents("m.room.aliases"); const aliasEvents = this.currentState.getStateEvents("m.room.aliases");
if (alias_events) { if (aliasEvents) {
for (let i = 0; i < alias_events.length; ++i) { for (let i = 0; i < aliasEvents.length; ++i) {
const alias_event = alias_events[i]; const aliasEvent = aliasEvents[i];
if (utils.isArray(alias_event.getContent().aliases)) { if (utils.isArray(aliasEvent.getContent().aliases)) {
Array.prototype.push.apply( Array.prototype.push.apply(
alias_strings, alias_event.getContent().aliases, aliasStrings, aliasEvent.getContent().aliases,
); );
} }
} }
} }
return alias_strings; return aliasStrings;
}; };
/** /**
@@ -1039,6 +1057,18 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
const redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId); const redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId);
if (redactedEvent) { if (redactedEvent) {
redactedEvent.makeRedacted(event); redactedEvent.makeRedacted(event);
// If this is in the current state, replace it with the redacted version
if (redactedEvent.getStateKey()) {
const currentStateEvent = this.currentState.getStateEvents(
redactedEvent.getType(),
redactedEvent.getStateKey(),
);
if (currentStateEvent.getId() === redactedEvent.getId()) {
this.currentState.setStateEvents([redactedEvent]);
}
}
this.emit("Room.redaction", event, this); this.emit("Room.redaction", event, this);
// TODO: we stash user displaynames (among other things) in // TODO: we stash user displaynames (among other things) in
@@ -1157,7 +1187,7 @@ Room.prototype.addPendingEvent = function(event, txnId) {
for (let i = 0; i < this._timelineSets.length; i++) { for (let i = 0; i < this._timelineSets.length; i++) {
const timelineSet = this._timelineSets[i]; const timelineSet = this._timelineSets[i];
if (timelineSet.getFilter()) { if (timelineSet.getFilter()) {
if (this._filter.filterRoomTimeline([event]).length) { if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
timelineSet.addEventToTimeline(event, timelineSet.addEventToTimeline(event,
timelineSet.getLiveTimeline(), false); timelineSet.getLiveTimeline(), false);
} }
@@ -1186,7 +1216,7 @@ Room.prototype._aggregateNonLiveRelation = function(event) {
for (let i = 0; i < this._timelineSets.length; i++) { for (let i = 0; i < this._timelineSets.length; i++) {
const timelineSet = this._timelineSets[i]; const timelineSet = this._timelineSets[i];
if (timelineSet.getFilter()) { if (timelineSet.getFilter()) {
if (this._filter.filterRoomTimeline([event]).length) { if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
timelineSet.aggregateRelations(event); timelineSet.aggregateRelations(event);
} }
} else { } else {
@@ -1402,28 +1432,33 @@ Room.prototype.addLiveEvents = function(events, duplicateStrategy) {
} }
for (i = 0; i < events.length; i++) { for (i = 0; i < events.length; i++) {
if (events[i].getType() === "m.typing") { // TODO: We should have a filter to say "only add state event
this.currentState.setTypingEvent(events[i]); // types X Y Z to the timeline".
} else if (events[i].getType() === "m.receipt") { this._addLiveEvent(events[i], duplicateStrategy);
this.addReceipt(events[i]); }
} };
// N.B. account_data is added directly by /sync to avoid
// having to maintain an event.isAccountData() here /**
else { * Adds/handles ephemeral events such as typing notifications and read receipts.
// TODO: We should have a filter to say "only add state event * @param {MatrixEvent[]} events A list of events to process
// types X Y Z to the timeline". */
this._addLiveEvent(events[i], duplicateStrategy); Room.prototype.addEphemeralEvents = function(events) {
} for (const event of events) {
if (event.getType() === 'm.typing') {
this.currentState.setTypingEvent(event);
} else if (event.getType() === 'm.receipt') {
this.addReceipt(event);
} // else ignore - life is too short for us to care about these events
} }
}; };
/** /**
* Removes events from this room. * Removes events from this room.
* @param {String[]} event_ids A list of event_ids to remove. * @param {String[]} eventIds A list of eventIds to remove.
*/ */
Room.prototype.removeEvents = function(event_ids) { Room.prototype.removeEvents = function(eventIds) {
for (let i = 0; i < event_ids.length; ++i) { for (let i = 0; i < eventIds.length; ++i) {
this.removeEvent(event_ids[i]); this.removeEvent(eventIds[i]);
} }
}; };

View File

@@ -43,6 +43,11 @@ const DEFAULT_OVERRIDE_RULES = [
key: "type", key: "type",
pattern: "m.room.tombstone", pattern: "m.room.tombstone",
}, },
{
kind: "event_match",
key: "state_key",
pattern: "",
},
], ],
actions: [ actions: [
"notify", "notify",
@@ -52,6 +57,22 @@ const DEFAULT_OVERRIDE_RULES = [
}, },
], ],
}, },
{
// For homeservers which don't support MSC2153 yet
rule_id: ".m.rule.reaction",
default: true,
enabled: true,
conditions: [
{
kind: "event_match",
key: "type",
pattern: "m.reaction",
},
],
actions: [
"dont_notify",
],
},
]; ];
/** /**
@@ -439,6 +460,38 @@ PushProcessor.actionListToActionsObject = function(actionlist) {
return actionobj; return actionobj;
}; };
/**
* Rewrites conditions on a client's push rules to match the defaults
* where applicable. Useful for upgrading push rules to more strict
* conditions when the server is falling behind on defaults.
* @param {object} incomingRules The client's existing push rules
* @returns {object} The rewritten rules
*/
PushProcessor.rewriteDefaultRules = function(incomingRules) {
let newRules = JSON.parse(JSON.stringify(incomingRules)); // deep clone
// These lines are mostly to make the tests happy. We shouldn't run into these
// properties missing in practice.
if (!newRules) newRules = {};
if (!newRules.global) newRules.global = {};
if (!newRules.global.override) newRules.global.override = [];
// Fix default override rules
newRules.global.override = newRules.global.override.map(r => {
const defaultRule = DEFAULT_OVERRIDE_RULES.find(d => d.rule_id === r.rule_id);
if (!defaultRule) return r;
// Copy over the actions, default, and conditions. Don't touch the user's
// preference.
r.default = defaultRule.default;
r.conditions = defaultRule.conditions;
r.actions = defaultRule.actions;
return r;
});
return newRules;
};
/** /**
* @typedef {Object} PushAction * @typedef {Object} PushAction
* @type {Object} * @type {Object}

View File

@@ -24,7 +24,7 @@ limitations under the License.
*/ */
"use strict"; "use strict";
import logger from '../src/logger'; import logger from './logger';
// we schedule a callback at least this often, to check if we've missed out on // we schedule a callback at least this often, to check if we've missed out on
// some wall-clock time due to being suspended. // some wall-clock time due to being suspended.

View File

@@ -21,7 +21,7 @@ limitations under the License.
*/ */
const utils = require("./utils"); const utils = require("./utils");
import Promise from 'bluebird'; import Promise from 'bluebird';
import logger from '../src/logger'; import logger from './logger';
const DEBUG = false; // set true to enable console logging. const DEBUG = false; // set true to enable console logging.

20
src/service-types.js Normal file
View File

@@ -0,0 +1,20 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export const SERVICE_TYPES = Object.freeze({
IS: 'SERVICE_TYPE_IS', // An Identity Service
IM: 'SERVICE_TYPE_IM', // An Integration Manager
});

View File

@@ -19,7 +19,7 @@ import Promise from 'bluebird';
import SyncAccumulator from "../sync-accumulator"; import SyncAccumulator from "../sync-accumulator";
import utils from "../utils"; import utils from "../utils";
import * as IndexedDBHelpers from "../indexeddb-helpers"; import * as IndexedDBHelpers from "../indexeddb-helpers";
import logger from '../../src/logger'; import logger from '../logger';
const VERSION = 3; const VERSION = 3;

View File

@@ -16,7 +16,7 @@ limitations under the License.
*/ */
import Promise from 'bluebird'; import Promise from 'bluebird';
import logger from '../../src/logger'; import logger from '../logger';
/** /**
* An IndexedDB store backend where the actual backend sits in a web * An IndexedDB store backend where the actual backend sits in a web

View File

@@ -17,7 +17,7 @@ limitations under the License.
import Promise from 'bluebird'; import Promise from 'bluebird';
import LocalIndexedDBStoreBackend from "./indexeddb-local-backend.js"; import LocalIndexedDBStoreBackend from "./indexeddb-local-backend.js";
import logger from '../../src/logger'; import logger from '../logger';
/** /**
* This class lives in the webworker and drives a LocalIndexedDBStoreBackend * This class lives in the webworker and drives a LocalIndexedDBStoreBackend

View File

@@ -25,7 +25,7 @@ import LocalIndexedDBStoreBackend from "./indexeddb-local-backend.js";
import RemoteIndexedDBStoreBackend from "./indexeddb-remote-backend.js"; import RemoteIndexedDBStoreBackend from "./indexeddb-remote-backend.js";
import User from "../models/user"; import User from "../models/user";
import {MatrixEvent} from "../models/event"; import {MatrixEvent} from "../models/event";
import logger from '../../src/logger'; import logger from '../logger';
/** /**
* This is an internal module. See {@link IndexedDBStore} for the public class. * This is an internal module. See {@link IndexedDBStore} for the public class.

View File

@@ -21,7 +21,7 @@ limitations under the License.
*/ */
import utils from "./utils"; import utils from "./utils";
import logger from '../src/logger'; import logger from './logger';
/** /**

View File

@@ -32,7 +32,8 @@ const Group = require('./models/group');
const utils = require("./utils"); const utils = require("./utils");
const Filter = require("./filter"); const Filter = require("./filter");
const EventTimeline = require("./models/event-timeline"); const EventTimeline = require("./models/event-timeline");
import logger from '../src/logger'; const PushProcessor = require("./pushprocessor");
import logger from './logger';
import {InvalidStoreError} from './errors'; import {InvalidStoreError} from './errors';
@@ -1030,8 +1031,9 @@ SyncApi.prototype._processSyncResponse = async function(
// honour push rules that were previously cached. Base rules // honour push rules that were previously cached. Base rules
// will be updated when we recieve push rules via getPushRules // will be updated when we recieve push rules via getPushRules
// (see SyncApi.prototype.sync) before syncing over the network. // (see SyncApi.prototype.sync) before syncing over the network.
if (accountDataEvent.getType() == 'm.push_rules') { if (accountDataEvent.getType() === 'm.push_rules') {
client.pushRules = accountDataEvent.getContent(); const rules = accountDataEvent.getContent();
client.pushRules = PushProcessor.rewriteDefaultRules(rules);
} }
client.emit("accountData", accountDataEvent); client.emit("accountData", accountDataEvent);
return accountDataEvent; return accountDataEvent;
@@ -1043,8 +1045,26 @@ SyncApi.prototype._processSyncResponse = async function(
if (data.to_device && utils.isArray(data.to_device.events) && if (data.to_device && utils.isArray(data.to_device.events) &&
data.to_device.events.length > 0 data.to_device.events.length > 0
) { ) {
const cancelledKeyVerificationTxns = [];
data.to_device.events data.to_device.events
.map(client.getEventMapper()) .map(client.getEventMapper())
.map((toDeviceEvent) => { // map is a cheap inline forEach
// We want to flag m.key.verification.start events as cancelled
// if there's an accompanying m.key.verification.cancel event, so
// we pull out the transaction IDs from the cancellation events
// so we can flag the verification events as cancelled in the loop
// below.
if (toDeviceEvent.getType() === "m.key.verification.cancel") {
const txnId = toDeviceEvent.getContent()['transaction_id'];
if (txnId) {
cancelledKeyVerificationTxns.push(txnId);
}
}
// as mentioned above, .map is a cheap inline forEach, so return
// the unmodified event.
return toDeviceEvent;
})
.forEach( .forEach(
function(toDeviceEvent) { function(toDeviceEvent) {
const content = toDeviceEvent.getContent(); const content = toDeviceEvent.getContent();
@@ -1060,6 +1080,14 @@ SyncApi.prototype._processSyncResponse = async function(
return; return;
} }
if (toDeviceEvent.getType() === "m.key.verification.start"
|| toDeviceEvent.getType() === "m.key.verification.request") {
const txnId = content['transaction_id'];
if (cancelledKeyVerificationTxns.includes(txnId)) {
toDeviceEvent.flagCancelled();
}
}
client.emit("toDeviceEvent", toDeviceEvent); client.emit("toDeviceEvent", toDeviceEvent);
}, },
); );
@@ -1219,10 +1247,8 @@ SyncApi.prototype._processSyncResponse = async function(
room.setSummary(joinObj.summary); room.setSummary(joinObj.summary);
} }
// XXX: should we be adding ephemeralEvents to the timeline? // we deliberately don't add ephemeral events to the timeline
// It feels like that for symmetry with room.addAccountData() room.addEphemeralEvents(ephemeralEvents);
// there should be a room.addEphemeralEvents() or similar.
room.addLiveEvents(ephemeralEvents);
// we deliberately don't add accountData to the timeline // we deliberately don't add accountData to the timeline
room.addAccountData(accountDataEvents); room.addAccountData(accountDataEvents);

View File

@@ -19,7 +19,7 @@ limitations under the License.
import Promise from 'bluebird'; import Promise from 'bluebird';
const EventTimeline = require("./models/event-timeline"); const EventTimeline = require("./models/event-timeline");
import logger from '../src/logger'; import logger from './logger';
/** /**
* @private * @private

View File

@@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -699,3 +700,11 @@ module.exports.globToRegexp = function(glob, extended) {
} }
return pat; return pat;
}; };
module.exports.ensureNoTrailingSlash = function(url) {
if (url && url.endsWith("/")) {
return url.substr(0, url.length - 1);
} else {
return url;
}
};

View File

@@ -21,7 +21,7 @@ limitations under the License.
*/ */
const utils = require("../utils"); const utils = require("../utils");
const EventEmitter = require("events").EventEmitter; const EventEmitter = require("events").EventEmitter;
import logger from '../../src/logger'; import logger from '../logger';
const DEBUG = true; // set true to enable console logging. const DEBUG = true; // set true to enable console logging.
// events: hangup, error(err), replaced(call), state(state, oldState) // events: hangup, error(err), replaced(call), state(state, oldState)
@@ -61,9 +61,9 @@ function MatrixCall(opts) {
this.URL = opts.URL; this.URL = opts.URL;
// Array of Objects with urls, username, credential keys // Array of Objects with urls, username, credential keys
this.turnServers = opts.turnServers || []; this.turnServers = opts.turnServers || [];
if (this.turnServers.length === 0) { if (this.turnServers.length === 0 && this.client.isFallbackICEServerAllowed()) {
this.turnServers.push({ this.turnServers.push({
urls: [MatrixCall.FALLBACK_STUN_SERVER], urls: [MatrixCall.FALLBACK_ICE_SERVER],
}); });
} }
utils.forEach(this.turnServers, function(server) { utils.forEach(this.turnServers, function(server) {
@@ -92,8 +92,8 @@ function MatrixCall(opts) {
} }
/** The length of time a call can be ringing for. */ /** The length of time a call can be ringing for. */
MatrixCall.CALL_TIMEOUT_MS = 60000; MatrixCall.CALL_TIMEOUT_MS = 60000;
/** The fallback server to use for STUN. */ /** The fallback ICE server to use for STUN or TURN protocols. */
MatrixCall.FALLBACK_STUN_SERVER = 'stun:stun.l.google.com:19302'; MatrixCall.FALLBACK_ICE_SERVER = 'stun:turn.matrix.org';
/** An error code when the local client failed to create an offer. */ /** An error code when the local client failed to create an offer. */
MatrixCall.ERR_LOCAL_OFFER_FAILED = "local_offer_failed"; MatrixCall.ERR_LOCAL_OFFER_FAILED = "local_offer_failed";
/** /**
@@ -665,7 +665,7 @@ MatrixCall.prototype._maybeGotUserMediaForAnswer = function(stream) {
}, },
}; };
self.peerConn.createAnswer(function(description) { self.peerConn.createAnswer(function(description) {
debuglog("Created answer: " + description); debuglog("Created answer: ", description);
self.peerConn.setLocalDescription(description, function() { self.peerConn.setLocalDescription(description, function() {
self._answerContent = { self._answerContent = {
version: 0, version: 0,
@@ -754,7 +754,7 @@ MatrixCall.prototype._receivedAnswer = function(msg) {
*/ */
MatrixCall.prototype._gotLocalOffer = function(description) { MatrixCall.prototype._gotLocalOffer = function(description) {
const self = this; const self = this;
debuglog("Created offer: " + description); debuglog("Created offer: ", description);
if (self.state == 'ended') { if (self.state == 'ended') {
debuglog("Ignoring newly created offer on call ID " + self.callId + debuglog("Ignoring newly created offer on call ID " + self.callId +
@@ -1217,24 +1217,9 @@ const _placeCallWithConstraints = function(self, constraints) {
}; };
const _createPeerConnection = function(self) { const _createPeerConnection = function(self) {
let servers = self.turnServers;
if (self.webRtc.vendor === "mozilla") {
// modify turnServers struct to match what mozilla expects.
servers = [];
for (let i = 0; i < self.turnServers.length; i++) {
for (let j = 0; j < self.turnServers[i].urls.length; j++) {
servers.push({
url: self.turnServers[i].urls[j],
username: self.turnServers[i].username,
credential: self.turnServers[i].credential,
});
}
}
}
const pc = new self.webRtc.RtcPeerConnection({ const pc = new self.webRtc.RtcPeerConnection({
iceTransportPolicy: self.forceTURN ? 'relay' : undefined, iceTransportPolicy: self.forceTURN ? 'relay' : undefined,
iceServers: servers, iceServers: self.turnServers,
}); });
pc.oniceconnectionstatechange = hookCallback(self, self._onIceConnectionStateChanged); pc.oniceconnectionstatechange = hookCallback(self, self._onIceConnectionStateChanged);
pc.onsignalingstatechange = hookCallback(self, self._onSignallingStateChanged); pc.onsignalingstatechange = hookCallback(self, self._onSignallingStateChanged);
@@ -1352,7 +1337,9 @@ module.exports.setVideoInput = function(deviceId) { videoInput = deviceId; };
* @param {MatrixClient} client The client instance to use. * @param {MatrixClient} client The client instance to use.
* @param {string} roomId The room the call is in. * @param {string} roomId The room the call is in.
* @param {Object?} options DEPRECATED optional options map. * @param {Object?} options DEPRECATED optional options map.
* @param {boolean} options.forceTURN DEPRECATED whether relay through TURN should be forced. This option is deprecated - use opts.forceTURN when creating the matrix client since it's only possible to set this option on outbound calls. * @param {boolean} options.forceTURN DEPRECATED whether relay through TURN should be
* forced. This option is deprecated - use opts.forceTURN when creating the matrix client
* since it's only possible to set this option on outbound calls.
* @return {MatrixCall} the call or null if the browser doesn't support calling. * @return {MatrixCall} the call or null if the browser doesn't support calling.
*/ */
module.exports.createNewMatrixCall = function(client, roomId, options) { module.exports.createNewMatrixCall = function(client, roomId, options) {
@@ -1383,24 +1370,36 @@ module.exports.createNewMatrixCall = function(client, roomId, options) {
return getUserMedia.apply(w.navigator, arguments); return getUserMedia.apply(w.navigator, arguments);
}; };
} }
webRtc.RtcPeerConnection = (
w.RTCPeerConnection || w.webkitRTCPeerConnection || w.mozRTCPeerConnection // Firefox throws on so little as accessing the RTCPeerConnection when operating in
); // a secure mode. There's some information at https://bugzilla.mozilla.org/show_bug.cgi?id=1542616
webRtc.RtcSessionDescription = ( // though the concern is that the browser throwing a SecurityError will brick the
w.RTCSessionDescription || w.webkitRTCSessionDescription || // client creation process.
w.mozRTCSessionDescription try {
); webRtc.RtcPeerConnection = (
webRtc.RtcIceCandidate = ( w.RTCPeerConnection || w.webkitRTCPeerConnection || w.mozRTCPeerConnection
w.RTCIceCandidate || w.webkitRTCIceCandidate || w.mozRTCIceCandidate );
); webRtc.RtcSessionDescription = (
webRtc.vendor = null; w.RTCSessionDescription || w.webkitRTCSessionDescription ||
if (w.mozRTCPeerConnection) { w.mozRTCSessionDescription
webRtc.vendor = "mozilla"; );
} else if (w.webkitRTCPeerConnection) { webRtc.RtcIceCandidate = (
webRtc.vendor = "webkit"; w.RTCIceCandidate || w.webkitRTCIceCandidate || w.mozRTCIceCandidate
} else if (w.RTCPeerConnection) { );
webRtc.vendor = "generic"; webRtc.vendor = null;
if (w.mozRTCPeerConnection) {
webRtc.vendor = "mozilla";
} else if (w.webkitRTCPeerConnection) {
webRtc.vendor = "webkit";
} else if (w.RTCPeerConnection) {
webRtc.vendor = "generic";
}
} catch (e) {
logger.error("Failed to set up WebRTC object: possible browser interference?");
logger.error(e);
return null;
} }
if (!webRtc.RtcIceCandidate || !webRtc.RtcSessionDescription || if (!webRtc.RtcIceCandidate || !webRtc.RtcSessionDescription ||
!webRtc.RtcPeerConnection || !webRtc.getUserMedia) { !webRtc.RtcPeerConnection || !webRtc.getUserMedia) {
return null; // WebRTC is not supported. return null; // WebRTC is not supported.

1075
yarn.lock

File diff suppressed because it is too large Load Diff