Compare commits

...

86 Commits

Author SHA1 Message Date
RiotRobot ede0f696ee v15.5.0 2022-01-31 14:41:10 +00:00
RiotRobot d05214a169 Prepare changelog for v15.5.0 2022-01-31 14:41:09 +00:00
David Baker 1e93d0b19f Bump node-fetch from 2.6.6 to 2.6.7 (#2140) (#2145)
Bumps [node-fetch](https://github.com/node-fetch/node-fetch) from 2.6.6 to 2.6.7.
- [Release notes](https://github.com/node-fetch/node-fetch/releases)
- [Commits](https://github.com/node-fetch/node-fetch/compare/v2.6.6...v2.6.7)

---
updated-dependencies:
- dependency-name: node-fetch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-31 14:24:05 +00:00
RiotRobot e66430ada6 v15.5.0-rc.1 2022-01-26 16:56:34 +00:00
RiotRobot 52ca60142a Prepare changelog for v15.5.0-rc.1 2022-01-26 16:56:33 +00:00
Michael Telatynski 7d2a5afa6d Fix http-api butchering idServer requests (#2134) 2022-01-26 15:43:24 +00:00
Germain 02ba233644 Improved threads reliability with/without server side support (#2132) 2022-01-26 13:54:00 +00:00
Michael Telatynski 50493a3330 Improve typing for Room.timeline data parameter (#2131) 2022-01-26 13:24:10 +00:00
Germain 8434f29c54 Make threads reply chain filter out local event ID (#2129) 2022-01-25 10:51:12 +00:00
Michael Telatynski a34426a7f6 Improve signature of MatrixClient::isUsernameAvailable to not rely on throwing (#2130) 2022-01-25 10:45:39 +00:00
Germain 033693283d Fix local echo for reactions in threads (#2128) 2022-01-24 17:53:05 +00:00
Germain fab7b7f26a Add default IContent value in makeContentExtensible (#2126) 2022-01-24 09:28:58 +00:00
David Baker 230e3b4ace Add eslint camelcase ignores & remove unnecessary casts (#2110)
Presumbaly casts left over from typescript migration.
2022-01-19 18:19:08 +00:00
Andy Balaam a50a627300 Support m.asset in m.location event content (#2109) 2022-01-19 09:08:41 +00:00
Germain 80930f6690 Extend IEventRelation to define m.in_reply_to (#2108)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-01-19 08:57:32 +00:00
Michael Telatynski 652b3a9208 Revert "Remove getCapabilities call for guest users (#2100)" (#2107) 2022-01-18 09:55:54 +00:00
Travis Ralston 0a8b825702 Move polls to events-sdk, and invent a RelatedRelations type (#2102)
* Move polls handling to events-sdk & invent a multi-relation handler

Polls are now fully handled by the events-sdk instead of by the js-sdk.

The multi-relation handler (RelatedRelations) is primarily meant to handle unstable & stable support in polls when it becomes stable.

* update events-sdk for polls

* Update events-sdk for polls bugfix

* Update events-sdk for maintenance
2022-01-17 10:06:42 -07:00
RiotRobot f4e2a38f4b Resetting package fields for development 2022-01-17 14:10:20 +00:00
RiotRobot b3474531c9 Merge branch 'master' into develop 2022-01-17 14:10:20 +00:00
RiotRobot f0597e0124 v15.4.0 2022-01-17 14:06:06 +00:00
RiotRobot 2fdc91bd04 Prepare changelog for v15.4.0 2022-01-17 14:06:05 +00:00
David Baker 16ca09eed8 Don't remove streams that still have tracks (#2104)
If a renogotiation ends up with one track being removed, we removed
the whole stream, which would cause us to lose, for example, audio
rather than just video.
2022-01-14 09:43:50 +00:00
Travis Ralston 129fb7f10f Keep unknown fields when adding extensible formats (#2105)
* Keep unknown fields when adding extensible formats

This also properly handles `m.new_content` to also be extensible.

* appease the linter
2022-01-13 20:17:46 +00:00
Travis Ralston 016e24472a Send extensible events structure and support on-demand parsing (#2091)
* Parse extensible events on demand

* Decorate messages with MSC1767 when appropriate

We do this automatically to force a pool of messages in the wild we can use for testing.

* Include the SDK

* Appease linter and tests

* Change property name to appease linter

* Update SDK
2022-01-13 09:56:11 -07:00
David Teller 4acb98c496 [Fix] Nullcheck redactedEvent (#2103)
We should only check whether a redacted event is a visibility event *if the redacted event is not null*.
2022-01-12 13:27:03 +01:00
David Teller 96d1b30012 MSC3531: Hiding messages during moderation (#2041) 2022-01-12 11:27:33 +01:00
David Baker 6fc586598a Yarn upgrade (#2101) 2022-01-11 17:17:01 +00:00
Michael Telatynski 2d9c938765 Support cancelling events whilst they are in status = ENCRYPTING (#2095) 2022-01-11 15:03:33 +00:00
RiotRobot 83d1a6c046 v15.4.0-rc.1 2022-01-11 14:56:46 +00:00
RiotRobot 87393c7db8 Prepare changelog for v15.4.0-rc.1 2022-01-11 14:56:45 +00:00
Germain bd47667e63 Remove getCapabilities call for guest users (#2100) 2022-01-11 12:50:10 +00:00
Germain cfd865bf8b Fetch server capabilities during client initialisation (#2093) 2022-01-11 11:53:30 +00:00
Michael Telatynski 9b54df7b2b Don't consider alt_aliases when calculating room name (#2094) 2022-01-10 13:42:10 -07:00
Michael Telatynski 5da562fa6f Stop encrypting redactions as it isn't spec compliant (#2098) 2022-01-10 17:02:11 +00:00
J. Ryan Stinnett 83d952eae8 Fix spacing errors (#2096) 2022-01-10 12:57:11 +00:00
Hugh Nimmo-Smith f30be87879 Fix more function typings relating to key backup (#2086)
* Fix more function typings relating to key backup

* Use function overloads to specify allowed params
2021-12-23 17:23:31 +00:00
Hugh Nimmo-Smith ab19480cc5 Fix timeline search in MSC3089 getFileEvent() (#2085) 2021-12-23 17:23:19 +00:00
Šimon Brandner 99451698a4 Set a deviceId for VoIP example and use const/let (#2090) 2021-12-23 13:37:55 +00:00
Germain 48dbed8001 Make threads events indexed by the index manager (#2089) 2021-12-23 09:58:11 +00:00
Germain bae883a891 Update filters to reflect MSC3440 (threads) (#2065) 2021-12-22 13:58:21 +00:00
Hugh Nimmo-Smith 2aae2362e3 Fix incorrect TS return type for secret storage and key backup functions (#2082) 2021-12-20 16:52:50 +00:00
RiotRobot f780e1dbc3 Resetting package fields for development 2021-12-20 14:01:19 +00:00
RiotRobot fdc11b5e53 Merge branch 'master' into develop 2021-12-20 14:01:19 +00:00
RiotRobot 1a0bdb0f98 v15.3.0 2021-12-20 13:58:07 +00:00
RiotRobot e870bebd3d Prepare changelog for v15.3.0 2021-12-20 13:58:07 +00:00
Michael Telatynski dd0ca91aa9 Stop using v1-prefixed /hierarchy API due to Synapse bugs (#2080) 2021-12-17 15:33:55 +00:00
Hugh Nimmo-Smith 36906e2ddb Load room history if necessary when searching for MSC3089 getFileEvent() (#2066) 2021-12-16 21:50:33 -07:00
Michael Telatynski 3eaed30446 Iterate typing around sync, usage limits and errors (#2077) 2021-12-16 09:57:07 +00:00
Andy Balaam 72643026a3 Remove now-unused makePollContent from its old location (#2078) 2021-12-15 17:08:21 +00:00
Andy Balaam d6980cb8fa Provide a makeLocationContent to create a location content event (#2076) 2021-12-15 16:32:24 +00:00
Andy Balaam dcd24fd516 Add consts for location and extensible events types (#2074) 2021-12-15 13:49:26 +00:00
Andy Balaam 75cc873d77 Copy the polls constants to matrix-js-sdk from matrix-react-sdk (#2073) 2021-12-15 12:15:38 +00:00
Eric Eastwood dd23a1a401 Add support for MSC3030 /timestamp_to_event (#2072)
- `/jumptodate` slash command is being worked on in https://github.com/matrix-org/matrix-react-sdk/pull/7372
 - Jump to date headers are being worked on in https://github.com/matrix-org/matrix-react-sdk/pull/7339

Related to https://github.com/vector-im/element-web/issues/7677

Part of MSC3030: https://github.com/matrix-org/matrix-doc/pull/3030

Experimental Synapse implementation added in https://github.com/matrix-org/synapse/pull/9445
2021-12-15 05:06:26 -06:00
Michael Telatynski 6ac84a2465 Use v1 prefix for /hierarchy API, falling back to both previous variants (#2022) 2021-12-15 09:56:07 +00:00
Hugh Nimmo-Smith e4d3124e72 Move Room properties JSDoc into more usable comments on actual props (#2067) 2021-12-15 08:57:54 +00:00
Michael Telatynski df11a9e832 Fix sending undefined query args to Synapse (#2070) 2021-12-14 10:38:28 -07:00
David Baker e4703989fe Add git archive learnings to release script comment (#2069) 2021-12-14 15:46:34 +00:00
Michael Telatynski feb83ba161 Convert http-api to Typescript (#2063) 2021-12-14 15:34:50 +00:00
RiotRobot 22a98b3e4f v15.3.0-rc.1 2021-12-14 14:37:58 +00:00
RiotRobot 934a0e8943 Prepare changelog for v15.3.0-rc.1 2021-12-14 14:37:58 +00:00
Michael Telatynski 963c7690b6 Iterate typing to work towards noImplicitAny (#2061) 2021-12-14 14:32:35 +00:00
Michael Telatynski aeec4aa4a8 Update typescript-eslint plugin & parser (#2064) 2021-12-13 23:28:54 +00:00
Michael Telatynski 169b6b5572 Filter out falsey opts in /relations API hits (#2059) 2021-12-13 15:38:03 +00:00
RiotRobot 6244d77d44 Merge branch 'master' into develop 2021-12-13 15:22:07 +00:00
RiotRobot f4839a3b4f v15.2.1 2021-12-13 15:17:37 +00:00
RiotRobot 4b21b67a45 Prepare changelog for v15.2.1 2021-12-13 15:17:37 +00:00
David Baker 799a83a115 Update olm to 3.2.8 (#2062) 2021-12-13 14:59:03 +00:00
Michael Telatynski f3ebb2a314 Update Typescript to 4.5 (#2060) 2021-12-13 11:22:50 +00:00
Michael Telatynski c0fbed914e Update olm in yarn.lock (#2058) 2021-12-10 10:49:35 +00:00
Hubert Chathi dd8c157bb9 Improve fallback key behaviour (#2037) 2021-12-09 19:00:23 -05:00
Travis Ralston 8bd2d640f2 Update eslint 2021-12-09 16:01:07 -07:00
Aaron R 80aaa6c32b Fix imports to work with new lint rules (#2003)
* Add eslint-plugin-import

Signed-off-by: Aaron Raimist <aaron@raim.ist>

* Autofix

Signed-off-by: Aaron Raimist <aaron@raim.ist>

* Manual fix

Signed-off-by: Aaron Raimist <aaron@raim.ist>
2021-12-09 15:57:43 -07:00
Michael Telatynski 4e9fe8f53f Fix types (#2056) 2021-12-09 15:44:07 +00:00
Ajay Bura f926a2f777 Fix paginateEventTimeline resolve to boolean (#2054)
* Fix paginateEventTimeline resolve to boolean

Earlier paginateEventTimeline used to resolve to  chunk instead of returning boolean. Now it return boolean as specified in doc.

* added type
2021-12-09 20:00:55 +05:30
Michael Telatynski f8097221e6 Improve typing (#2055) 2021-12-09 14:22:58 +00:00
Callum Brown 95e7a76ba9 Finish adding registration token UIAA type (#2048)
Signed-off-by: Callum Brown <callum@calcuode.com>
2021-12-08 19:38:22 -07:00
Germain 72bc13eb39 Allow to omit optional params for /relations calls (#2053) 2021-12-08 08:23:53 +00:00
Germain 2206b80e65 Threads notifications after app startup (#2043) 2021-12-07 10:58:34 +00:00
Germain 39a8399977 Add new room event filter fields (#2051) 2021-12-07 09:42:52 +00:00
RiotRobot b7247fcedc Resetting package fields for development 2021-12-06 15:14:16 +00:00
RiotRobot e4494707fa Merge branch 'master' into develop 2021-12-06 15:14:16 +00:00
RiotRobot 45b7cab203 v15.2.0 2021-12-06 15:10:51 +00:00
RiotRobot 4dfc3b9dbb Prepare changelog for v15.2.0 2021-12-06 15:10:50 +00:00
Hugh Nimmo-Smith 15d05cd92f Fix incorrect MSC3089 typings and add null checks (#2049)
* Fix MSC3089TreeSpace typings to match actual code

* Implement strict null checks to resolve TS strictNullChecks=true issues
2021-12-06 10:54:43 +00:00
skyka13711 ba1b1cc6fa fix exports (#2047) 2021-12-02 19:40:10 -07:00
Michael Telatynski b716cde9b5 Add method to fetch /account/whoami (#2046) 2021-12-02 13:43:15 +00:00
96 changed files with 3893 additions and 2173 deletions
+96
View File
@@ -1,3 +1,99 @@
Changes in [15.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.0) (2022-01-31)
==================================================================================================
## ✨ Features
* Support m.asset in m.location event content ([\#2109](https://github.com/matrix-org/matrix-js-sdk/pull/2109)).
* Send extensible events structure and support on-demand parsing ([\#2091](https://github.com/matrix-org/matrix-js-sdk/pull/2091)).
* Support cancelling events whilst they are in status = ENCRYPTING ([\#2095](https://github.com/matrix-org/matrix-js-sdk/pull/2095)).
## 🐛 Bug Fixes
* Fix http-api butchering idServer requests ([\#2134](https://github.com/matrix-org/matrix-js-sdk/pull/2134)). Fixes vector-im/element-web#20680.
* Don't remove streams that still have tracks ([\#2104](https://github.com/matrix-org/matrix-js-sdk/pull/2104)).
Changes in [15.5.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.0-rc.1) (2022-01-26)
============================================================================================================
## ✨ Features
* Support m.asset in m.location event content ([\#2109](https://github.com/matrix-org/matrix-js-sdk/pull/2109)).
* Send extensible events structure and support on-demand parsing ([\#2091](https://github.com/matrix-org/matrix-js-sdk/pull/2091)).
* Support cancelling events whilst they are in status = ENCRYPTING ([\#2095](https://github.com/matrix-org/matrix-js-sdk/pull/2095)).
## 🐛 Bug Fixes
* Fix http-api butchering idServer requests ([\#2134](https://github.com/matrix-org/matrix-js-sdk/pull/2134)). Fixes vector-im/element-web#20680.
* Don't remove streams that still have tracks ([\#2104](https://github.com/matrix-org/matrix-js-sdk/pull/2104)).
Changes in [15.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.4.0) (2022-01-17)
==================================================================================================
## ✨ Features
* Don't consider alt_aliases when calculating room name ([\#2094](https://github.com/matrix-org/matrix-js-sdk/pull/2094)). Fixes vector-im/element-web#13887.
* Load room history if necessary when searching for MSC3089 getFileEvent() ([\#2066](https://github.com/matrix-org/matrix-js-sdk/pull/2066)).
* Add support for MSC3030 `/timestamp_to_event` ([\#2072](https://github.com/matrix-org/matrix-js-sdk/pull/2072)).
## 🐛 Bug Fixes
* Stop encrypting redactions as it isn't spec compliant ([\#2098](https://github.com/matrix-org/matrix-js-sdk/pull/2098)). Fixes vector-im/element-web#20460.
* Fix more function typings relating to key backup ([\#2086](https://github.com/matrix-org/matrix-js-sdk/pull/2086)).
* Fix timeline search in MSC3089 getFileEvent() ([\#2085](https://github.com/matrix-org/matrix-js-sdk/pull/2085)).
* Set a `deviceId` for VoIP example and use `const`/`let` ([\#2090](https://github.com/matrix-org/matrix-js-sdk/pull/2090)). Fixes #2083. Contributed by @SimonBrandner.
* Fix incorrect TS return type for secret storage and key backup functions ([\#2082](https://github.com/matrix-org/matrix-js-sdk/pull/2082)).
Changes in [15.4.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.4.0-rc.1) (2022-01-11)
============================================================================================================
## ✨ Features
* Don't consider alt_aliases when calculating room name ([\#2094](https://github.com/matrix-org/matrix-js-sdk/pull/2094)). Fixes vector-im/element-web#13887.
* Load room history if necessary when searching for MSC3089 getFileEvent() ([\#2066](https://github.com/matrix-org/matrix-js-sdk/pull/2066)).
* Add support for MSC3030 `/timestamp_to_event` ([\#2072](https://github.com/matrix-org/matrix-js-sdk/pull/2072)).
## 🐛 Bug Fixes
* Stop encrypting redactions as it isn't spec compliant ([\#2098](https://github.com/matrix-org/matrix-js-sdk/pull/2098)). Fixes vector-im/element-web#20460.
* Fix more function typings relating to key backup ([\#2086](https://github.com/matrix-org/matrix-js-sdk/pull/2086)).
* Fix timeline search in MSC3089 getFileEvent() ([\#2085](https://github.com/matrix-org/matrix-js-sdk/pull/2085)).
* Set a `deviceId` for VoIP example and use `const`/`let` ([\#2090](https://github.com/matrix-org/matrix-js-sdk/pull/2090)). Fixes #2083. Contributed by @SimonBrandner.
* Fix incorrect TS return type for secret storage and key backup functions ([\#2082](https://github.com/matrix-org/matrix-js-sdk/pull/2082)).
Changes in [15.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.3.0) (2021-12-20)
==================================================================================================
## ✨ Features
* Improve fallback key behaviour ([\#2037](https://github.com/matrix-org/matrix-js-sdk/pull/2037)).
* Add new room event filter fields ([\#2051](https://github.com/matrix-org/matrix-js-sdk/pull/2051)).
* Add method to fetch /account/whoami ([\#2046](https://github.com/matrix-org/matrix-js-sdk/pull/2046)).
## 🐛 Bug Fixes
* Filter out falsey opts in /relations API hits ([\#2059](https://github.com/matrix-org/matrix-js-sdk/pull/2059)). Fixes vector-im/element-web#20137.
* Fix paginateEventTimeline resolve to boolean ([\#2054](https://github.com/matrix-org/matrix-js-sdk/pull/2054)).
* Fix incorrect MSC3089 typings and add null checks ([\#2049](https://github.com/matrix-org/matrix-js-sdk/pull/2049)).
Changes in [15.3.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.3.0-rc.1) (2021-12-14)
============================================================================================================
## ✨ Features
* Improve fallback key behaviour ([\#2037](https://github.com/matrix-org/matrix-js-sdk/pull/2037)).
* Add new room event filter fields ([\#2051](https://github.com/matrix-org/matrix-js-sdk/pull/2051)).
* Add method to fetch /account/whoami ([\#2046](https://github.com/matrix-org/matrix-js-sdk/pull/2046)).
## 🐛 Bug Fixes
* Filter out falsey opts in /relations API hits ([\#2059](https://github.com/matrix-org/matrix-js-sdk/pull/2059)). Fixes vector-im/element-web#20137.
* Fix paginateEventTimeline resolve to boolean ([\#2054](https://github.com/matrix-org/matrix-js-sdk/pull/2054)).
* Fix incorrect MSC3089 typings and add null checks ([\#2049](https://github.com/matrix-org/matrix-js-sdk/pull/2049)).
Changes in [15.2.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.2.1) (2021-12-13)
==================================================================================================
* Security release with updated version of Olm to fix https://matrix.org/blog/2021/12/03/pre-disclosure-upcoming-security-release-of-libolm-and-matrix-js-sdk
Changes in [15.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.2.0) (2021-12-06)
==================================================================================================
## ✨ Features
* Remove support for `ArrayBuffer` in unstable MSC3089 `createFile()` and `createNewVersion()` and instead use same content types as handled by `MatrixClient.uploadContent()`. This enables support for Node.js. ([\#2014](https://github.com/matrix-org/matrix-js-sdk/pull/2014)).
* Support for password-based backup on Node.js ([\#2021](https://github.com/matrix-org/matrix-js-sdk/pull/2021)).
* Add optional force parameter when ensuring Olm sessions ([\#2027](https://github.com/matrix-org/matrix-js-sdk/pull/2027)).
## 🐛 Bug Fixes
* Fix call upgrades ([\#2024](https://github.com/matrix-org/matrix-js-sdk/pull/2024)). Contributed by @SimonBrandner.
Changes in [15.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.2.0-rc.1) (2021-11-30)
============================================================================================================
+10 -9
View File
@@ -1,16 +1,17 @@
console.log("Loading browser sdk");
var BASE_URL = "https://matrix.org";
var TOKEN = "accesstokengoeshere";
var USER_ID = "@username:localhost";
var ROOM_ID = "!room:id";
const BASE_URL = "https://matrix.org";
const TOKEN = "accesstokengoeshere";
const USER_ID = "@username:localhost";
const ROOM_ID = "!room:id";
const DEVICE_ID = "some_device_id";
var client = matrixcs.createClient({
const client = matrixcs.createClient({
baseUrl: BASE_URL,
accessToken: TOKEN,
userId: USER_ID
userId: USER_ID,
deviceId: DEVICE_ID
});
var call;
let call;
function disableButtons(place, answer, hangup) {
document.getElementById("hangup").disabled = hangup;
@@ -19,7 +20,7 @@ function disableButtons(place, answer, hangup) {
}
function addListeners(call) {
var lastError = "";
let lastError = "";
call.on("hangup", function() {
disableButtons(false, true, true);
document.getElementById("result").innerHTML = (
+10 -7
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "15.2.0-rc.1",
"version": "15.5.0",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepublishOnly": "yarn build",
@@ -15,7 +15,7 @@
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
"gendoc": "jsdoc -c jsdoc.json -P package.json",
"lint": "yarn lint:types && yarn lint:js",
"lint:js": "eslint --max-warnings 7 src spec",
"lint:js": "eslint --max-warnings 0 src spec",
"lint:js-fix": "eslint --fix src spec",
"lint:types": "tsc --noEmit",
"test": "jest",
@@ -56,6 +56,7 @@
"bs58": "^4.0.1",
"content-type": "^1.0.4",
"loglevel": "^1.7.1",
"matrix-events-sdk": "^0.0.1-beta.6",
"p-retry": "^4.5.0",
"qs": "^6.9.6",
"request": "^2.88.2",
@@ -74,13 +75,14 @@
"@babel/preset-env": "^7.12.11",
"@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.10",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
"@types/bs58": "^4.0.1",
"@types/content-type": "^1.1.5",
"@types/jest": "^26.0.20",
"@types/node": "12",
"@types/request": "^2.48.5",
"@typescript-eslint/eslint-plugin": "^4.17.0",
"@typescript-eslint/parser": "^4.17.0",
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",
"allchange": "^1.0.6",
"babel-jest": "^26.6.3",
"babelify": "^10.0.0",
@@ -89,7 +91,8 @@
"docdash": "^1.2.0",
"eslint": "7.18.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#2306b3d4da4eba908b256014b979f1d3d43d2945",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-matrix-org": "^0.4.0",
"exorcist": "^1.0.1",
"fake-indexeddb": "^3.1.2",
"jest": "^26.6.3",
@@ -99,7 +102,7 @@
"rimraf": "^3.0.2",
"terser": "^5.5.1",
"tsify": "^5.0.2",
"typescript": "^4.1.3"
"typescript": "^4.5.3"
},
"jest": {
"testEnvironment": "node",
+6
View File
@@ -255,6 +255,12 @@ if [ -n "$signing_id" ]; then
# the easiest way to check the validity of the tarball from git is to unzip
# it and compare it with our own idea of what the tar should look like.
# This uses git archive which seems to be what github uses. Specifically,
# the header fields are set in the same way: same file mode, uid & gid
# both zero and mtime set to the timestamp of the commit that the tag
# references. Also note that this puts the commit into the tar headers
# and can be extracted with gunzip -c foo.tar.gz | git get-tar-commit-id
# the name of the sig file we want to create
source_sigfile="${tag}-src.tar.gz.asc"
+2
View File
@@ -20,6 +20,7 @@ limitations under the License.
import './olm-loader';
import MockHttpBackend from 'matrix-mock-request';
import { LocalStorageCryptoStore } from '../src/crypto/store/localStorage-crypto-store';
import { logger } from '../src/logger';
import { WebStorageSessionStore } from "../src/store/session/webstorage";
@@ -85,6 +86,7 @@ TestClient.prototype.toString = function() {
*/
TestClient.prototype.start = function() {
logger.log(this + ': starting');
this.httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
this.httpBackend.when("GET", "/pushrules").respond(200, {});
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
this.expectDeviceKeyUpload();
+10 -32
View File
@@ -17,54 +17,34 @@ limitations under the License.
// load XmlHttpRequest mock
import "./setupTests";
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
import { MockStorageApi } from "../MockStorageApi";
import { WebStorageSessionStore } from "../../src/store/session/webstorage";
import MockHttpBackend from "matrix-mock-request";
import { LocalStorageCryptoStore } from "../../src/crypto/store/localStorage-crypto-store";
import * as utils from "../test-utils";
import { TestClient } from "../TestClient";
const USER_ID = "@user:test.server";
const DEVICE_ID = "device_id";
const ACCESS_TOKEN = "access_token";
const ROOM_ID = "!room_id:server.test";
/* global matrixcs */
describe("Browserify Test", function() {
let client;
let httpBackend;
async function createTestClient() {
const sessionStoreBackend = new MockStorageApi();
const sessionStore = new WebStorageSessionStore(sessionStoreBackend);
const httpBackend = new MockHttpBackend();
beforeEach(() => {
const testClient = new TestClient(USER_ID, DEVICE_ID, ACCESS_TOKEN);
const options = {
baseUrl: "http://" + USER_ID + ".test.server",
userId: USER_ID,
accessToken: ACCESS_TOKEN,
deviceId: DEVICE_ID,
sessionStore: sessionStore,
request: httpBackend.requestFn,
cryptoStore: new LocalStorageCryptoStore(sessionStoreBackend),
};
const client = matrixcs.createClient(options);
client = testClient.client;
httpBackend = testClient.httpBackend;
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
return { client, httpBackend };
}
beforeEach(async () => {
({ client, httpBackend } = await createTestClient());
await client.startClient();
client.startClient();
});
afterEach(async () => {
client.stopClient();
await httpBackend.stop();
httpBackend.stop();
});
it("Sync", async function() {
@@ -91,10 +71,8 @@ describe("Browserify Test", function() {
};
httpBackend.when("GET", "/sync").respond(200, syncData);
await Promise.race([
Promise.all([
httpBackend.flushAllExpected(),
]),
return await Promise.race([
httpBackend.flushAllExpected(),
new Promise((_, reject) => {
client.once("sync.unexpectedError", reject);
}),
+2 -1
View File
@@ -348,7 +348,7 @@ function recvMessage(httpBackend, client, sender, message) {
return testUtils.awaitDecryption(event);
}).then((event) => {
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent()).toEqual({
expect(event.getContent()).toMatchObject({
msgtype: "m.text",
body: "Hello, World",
});
@@ -722,6 +722,7 @@ describe("MatrixClient crypto", function() {
return Promise.resolve()
.then(() => {
logger.log(aliTestClient + ': starting');
httpBackend.when("GET", "/capabilities").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
aliTestClient.expectDeviceKeyUpload();
@@ -13,6 +13,7 @@ describe("MatrixClient events", function() {
httpBackend = testClient.httpBackend;
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
});
afterEach(function() {
@@ -71,6 +71,7 @@ const EVENTS = [
// start the client, and wait for it to initialise
function startClient(httpBackend, client) {
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA);
@@ -502,7 +503,7 @@ describe("MatrixClient event timelines", function() {
const params = req.queryParams;
expect(params.dir).toEqual("b");
expect(params.from).toEqual("start_token0");
expect(params.limit).toEqual(30);
expect(params.limit).toEqual("30");
}).respond(200, function() {
return {
chunk: [EVENTS[1], EVENTS[2]],
@@ -553,7 +554,7 @@ describe("MatrixClient event timelines", function() {
const params = req.queryParams;
expect(params.dir).toEqual("f");
expect(params.from).toEqual("end_token0");
expect(params.limit).toEqual(20);
expect(params.limit).toEqual("20");
}).respond(200, function() {
return {
chunk: [EVENTS[1], EVENTS[2]],
+5 -2
View File
@@ -1,5 +1,6 @@
import * as utils from "../test-utils";
import HttpBackend from "matrix-mock-request";
import * as utils from "../test-utils";
import { MatrixClient } from "../../src/matrix";
import { MatrixScheduler } from "../../src/scheduler";
import { MemoryStore } from "../../src/store/memory";
@@ -104,10 +105,12 @@ describe("MatrixClient opts", function() {
expectedEventTypes.indexOf(event.getType()), 1,
);
});
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "foo" });
httpBackend.when("GET", "/sync").respond(200, syncData);
await client.startClient();
client.startClient();
await httpBackend.flush("/capabilities", 1);
await httpBackend.flush("/pushrules", 1);
await httpBackend.flush("/filter", 1);
await Promise.all([
+6 -6
View File
@@ -4,13 +4,13 @@ import { Room } from "../../src/models/room";
import { TestClient } from "../TestClient";
describe("MatrixClient retrying", function() {
let client = null;
let httpBackend = null;
let client: TestClient = null;
let httpBackend: TestClient["httpBackend"] = null;
let scheduler;
const userId = "@alice:localhost";
const accessToken = "aseukfgwef";
const roomId = "!room:here";
let room;
let room: Room;
beforeEach(function() {
scheduler = new MatrixScheduler();
@@ -53,10 +53,10 @@ describe("MatrixClient retrying", function() {
const p1 = client.sendMessage(roomId, {
"msgtype": "m.text",
"body": "m1",
}).then(function(ev) {
}).then(function() {
// we expect the first message to fail
throw new Error('Message 1 unexpectedly sent successfully');
}, (e) => {
}, () => {
// this is expected
});
@@ -78,7 +78,7 @@ describe("MatrixClient retrying", function() {
expect(ev2.status).toEqual(EventStatus.SENDING);
// the first message should get sent, and the second should get queued
httpBackend.when("PUT", "/send/m.room.message/").check(function(rq) {
httpBackend.when("PUT", "/send/m.room.message/").check(function() {
// ev2 should now have been queued
expect(ev2.status).toEqual(EventStatus.QUEUED);
@@ -96,7 +96,7 @@ describe("MatrixClient room timelines", function() {
});
}
beforeEach(function() {
beforeEach(async function() {
// these tests should work with or without timelineSupport
const testClient = new TestClient(
userId,
@@ -109,6 +109,7 @@ describe("MatrixClient room timelines", function() {
client = testClient.client;
setNextSyncData();
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
@@ -116,9 +117,10 @@ describe("MatrixClient room timelines", function() {
return NEXT_SYNC_DATA;
});
client.startClient();
return httpBackend.flush("/pushrules").then(function() {
return httpBackend.flush("/filter");
});
await httpBackend.flush("/capabilities");
await httpBackend.flush("/pushrules");
await httpBackend.flush("/filter");
});
afterEach(function() {
+1
View File
@@ -19,6 +19,7 @@ describe("MatrixClient syncing", function() {
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
httpBackend = testClient.httpBackend;
client = testClient.client;
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
});
+1
View File
@@ -16,6 +16,7 @@ limitations under the License.
*/
import anotherjson from "another-json";
import * as testUtils from "../test-utils";
import { TestClient } from "../TestClient";
import { logger } from "../../src/logger";
+15 -14
View File
@@ -341,8 +341,15 @@ HttpResponse.SYNC_RESPONSE = {
data: HttpResponse.SYNC_DATA,
};
HttpResponse.CAPABILITIES_RESPONSE = {
method: "GET",
path: "/capabilities",
data: { capabilities: {} },
};
HttpResponse.defaultResponses = function(userId) {
return [
HttpResponse.CAPABILITIES_RESPONSE,
HttpResponse.PUSH_RULES_RESPONSE,
HttpResponse.filterResponse(userId),
HttpResponse.SYNC_RESPONSE,
@@ -350,19 +357,13 @@ HttpResponse.defaultResponses = function(userId) {
};
export function setHttpResponses(
client, responses, acceptKeepalives, ignoreUnhandledSyncs,
httpBackend, responses,
) {
const httpResponseObj = new HttpResponse(
responses, acceptKeepalives, ignoreUnhandledSyncs,
);
const httpReq = httpResponseObj.request.bind(httpResponseObj);
client.http = [
"authedRequest", "authedRequestWithPrefix", "getContentUri",
"request", "requestWithPrefix", "uploadContent",
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
client.http.authedRequest.mockImplementation(httpReq);
client.http.authedRequestWithPrefix.mockImplementation(httpReq);
client.http.requestWithPrefix.mockImplementation(httpReq);
client.http.request.mockImplementation(httpReq);
responses.forEach(response => {
httpBackend
.when(response.method, response.path)
.respond(200, response.data);
});
}
export const emitPromise = (e, k) => new Promise(r => e.once(k, r));
+1
View File
@@ -15,6 +15,7 @@ limitations under the License.
*/
import { EventEmitter } from "events";
import { ReEmitter } from "../../src/ReEmitter";
const EVENTNAME = "UnknownEntry";
+1
View File
@@ -16,6 +16,7 @@ limitations under the License.
*/
import MockHttpBackend from "matrix-mock-request";
import * as sdk from "../../src";
import { AutoDiscovery } from "../../src/autodiscovery";
+2 -1
View File
@@ -1,4 +1,6 @@
import '../olm-loader';
import { EventEmitter } from "events";
import { Crypto } from "../../src/crypto";
import { WebStorageSessionStore } from "../../src/store/session/webstorage";
import { MemoryCryptoStore } from "../../src/crypto/store/memory-crypto-store";
@@ -8,7 +10,6 @@ import { MatrixEvent } from "../../src/models/event";
import { Room } from "../../src/models/room";
import * as olmlib from "../../src/crypto/olmlib";
import { sleep } from "../../src/utils";
import { EventEmitter } from "events";
import { CRYPTO_ENABLED } from "../../src/client";
import { DeviceInfo } from "../../src/crypto/deviceinfo";
+25 -20
View File
@@ -17,6 +17,7 @@ limitations under the License.
import '../../olm-loader';
import anotherjson from 'another-json';
import * as olmlib from "../../../src/crypto/olmlib";
import { TestClient } from '../../TestClient';
import { HttpResponse, setHttpResponses } from '../../test-utils';
@@ -39,13 +40,14 @@ async function makeTestClient(userInfo, options, keys) {
options.cryptoCallbacks = Object.assign(
{}, { getCrossSigningKey, saveCrossSigningKeys }, options.cryptoCallbacks || {},
);
const client = (new TestClient(
const testClient = new TestClient(
userInfo.userId, userInfo.deviceId, undefined, undefined, options,
)).client;
);
const client = testClient.client;
await client.initCrypto();
return client;
return { client, httpBackend: testClient.httpBackend };
}
describe("Cross Signing", function() {
@@ -59,7 +61,7 @@ describe("Cross Signing", function() {
});
it("should sign the master key with the device key", async function() {
const alice = await makeTestClient(
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = jest.fn(async (auth, keys) => {
@@ -79,7 +81,7 @@ describe("Cross Signing", function() {
});
it("should abort bootstrap if device signing auth fails", async function() {
const alice = await makeTestClient(
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async (auth, keys) => {
@@ -130,7 +132,7 @@ describe("Cross Signing", function() {
});
it("should upload a signature when a user is verified", async function() {
const alice = await makeTestClient(
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async () => {};
@@ -160,7 +162,7 @@ describe("Cross Signing", function() {
await promise;
});
it("should get cross-signing keys from sync", async function() {
it.skip("should get cross-signing keys from sync", async function() {
const masterKey = new Uint8Array([
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82,
0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef,
@@ -174,7 +176,7 @@ describe("Cross Signing", function() {
0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f,
]);
const alice = await makeTestClient(
const { client: alice, httpBackend } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
{
cryptoCallbacks: {
@@ -235,6 +237,7 @@ describe("Cross Signing", function() {
// feed sync result that includes master key, ssk, device key
const responses = [
HttpResponse.CAPABILITIES_RESPONSE,
HttpResponse.PUSH_RULES_RESPONSE,
{
method: "POST",
@@ -310,9 +313,10 @@ describe("Cross Signing", function() {
},
},
];
setHttpResponses(alice, responses, true, true);
setHttpResponses(httpBackend, responses);
await alice.startClient();
alice.startClient();
httpBackend.flushAllExpected();
// once ssk is confirmed, device key should be trusted
await keyChangePromise;
@@ -331,7 +335,7 @@ describe("Cross Signing", function() {
});
it("should use trust chain to determine device verification", async function() {
const alice = await makeTestClient(
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async () => {};
@@ -414,9 +418,9 @@ describe("Cross Signing", function() {
expect(bobDeviceTrust2.isTofu()).toBeTruthy();
});
it("should trust signatures received from other devices", async function() {
it.skip("should trust signatures received from other devices", async function() {
const aliceKeys = {};
const alice = await makeTestClient(
const { client: alice, httpBackend } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
null,
aliceKeys,
@@ -490,6 +494,7 @@ describe("Cross Signing", function() {
// - master key signed by her usk (pretend that it was signed by another
// of Alice's devices)
const responses = [
HttpResponse.CAPABILITIES_RESPONSE,
HttpResponse.PUSH_RULES_RESPONSE,
{
method: "POST",
@@ -560,10 +565,10 @@ describe("Cross Signing", function() {
},
},
];
setHttpResponses(alice, responses);
await alice.startClient();
setHttpResponses(httpBackend, responses);
alice.startClient();
httpBackend.flushAllExpected();
await keyChangePromise;
// Bob's device key should be trusted
@@ -578,7 +583,7 @@ describe("Cross Signing", function() {
});
it("should dis-trust an unsigned device", async function() {
const alice = await makeTestClient(
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async () => {};
@@ -647,7 +652,7 @@ describe("Cross Signing", function() {
});
it("should dis-trust a user when their ssk changes", async function() {
const alice = await makeTestClient(
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async () => {};
@@ -785,7 +790,7 @@ describe("Cross Signing", function() {
it("should offer to upgrade device verifications to cross-signing", async function() {
let upgradeResolveFunc;
const alice = await makeTestClient(
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
{
cryptoCallbacks: {
@@ -797,7 +802,7 @@ describe("Cross Signing", function() {
},
},
);
const bob = await makeTestClient(
const { client: bob } = await makeTestClient(
{ userId: "@bob:example.com", deviceId: "Dynabook" },
);
@@ -18,11 +18,11 @@ import {
IndexedDBCryptoStore,
} from '../../../src/crypto/store/indexeddb-crypto-store';
import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store';
import { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager';
import 'fake-indexeddb/auto';
import 'jest-localstorage-mock';
import { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager';
const requests = [
{
requestId: "A",
-1
View File
@@ -23,7 +23,6 @@ import { makeTestClients } from './verification/util';
import { encryptAES } from "../../../src/crypto/aes";
import { resetCrossSigningKeys, createSecretStorageKey } from "./crypto-utils";
import { logger } from '../../../src/logger';
import * as utils from "../../../src/utils";
try {
@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
"../../../../src/crypto/verification/request/ToDeviceChannel";
import { MatrixEvent } from "../../../../src/models/event";
"../../../../src/crypto/verification/request/ToDeviceChannel";
describe("InRoomChannel tests", function() {
const ALICE = "@alice:hs.tld";
+2 -1
View File
@@ -15,9 +15,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import nodeCrypto from "crypto";
import { TestClient } from '../../../TestClient';
import { MatrixEvent } from "../../../../src/models/event";
import nodeCrypto from "crypto";
import { logger } from '../../../../src/logger';
export async function makeTestClients(userInfos, options) {
+56
View File
@@ -0,0 +1,56 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { makeLocationContent } from "../../src/content-helpers";
import {
ASSET_NODE_TYPE,
ASSET_TYPE_SELF,
LOCATION_EVENT_TYPE,
TIMESTAMP_NODE_TYPE,
} from "../../src/@types/location";
import { TEXT_NODE_TYPE } from "../../src/@types/extensible_events";
describe("Location", function() {
it("should create a valid location with defaults", function() {
const loc = makeLocationContent("txt", "geo:foo", 134235435);
expect(loc.body).toEqual("txt");
expect(loc.msgtype).toEqual("m.location");
expect(loc.geo_uri).toEqual("geo:foo");
expect(LOCATION_EVENT_TYPE.findIn(loc)).toEqual({
uri: "geo:foo",
description: undefined,
});
expect(ASSET_NODE_TYPE.findIn(loc)).toEqual({ type: ASSET_TYPE_SELF });
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual("txt");
expect(TIMESTAMP_NODE_TYPE.findIn(loc)).toEqual(134235435);
});
it("should create a valid location with explicit properties", function() {
const loc = makeLocationContent(
"txxt", "geo:bar", 134235436, "desc", "m.something");
expect(loc.body).toEqual("txxt");
expect(loc.msgtype).toEqual("m.location");
expect(loc.geo_uri).toEqual("geo:bar");
expect(LOCATION_EVENT_TYPE.findIn(loc)).toEqual({
uri: "geo:bar",
description: "desc",
});
expect(ASSET_NODE_TYPE.findIn(loc)).toEqual({ type: "m.something" });
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual("txxt");
expect(TIMESTAMP_NODE_TYPE.findIn(loc)).toEqual(134235436);
});
});
+225 -5
View File
@@ -11,8 +11,9 @@ import {
UNSTABLE_MSC3089_TREE_SUBTYPE,
} from "../../src/@types/event";
import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib";
import { MatrixEvent } from "../../src/models/event";
import { EventStatus, MatrixEvent } from "../../src/models/event";
import { Preset } from "../../src/@types/partials";
import * as testUtils from "../test-utils";
jest.useFakeTimers();
@@ -52,6 +53,12 @@ describe("MatrixClient", function() {
data: SYNC_DATA,
};
const CAPABILITIES_RESPONSE = {
method: "GET",
path: "/capabilities",
data: { capabilities: {} },
};
let httpLookups = [
// items are objects which look like:
// {
@@ -167,6 +174,7 @@ describe("MatrixClient", function() {
acceptKeepalives = true;
pendingLookup = null;
httpLookups = [];
httpLookups.push(CAPABILITIES_RESPONSE);
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
@@ -364,9 +372,11 @@ describe("MatrixClient", function() {
});
it("should not POST /filter if a matching filter already exists", async function() {
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
httpLookups = [
CAPABILITIES_RESPONSE,
PUSH_RULES_RESPONSE,
SYNC_RESPONSE,
];
const filterId = "ehfewf";
store.getFilterIdByName.mockReturnValue(filterId);
const filter = new Filter(0, filterId);
@@ -447,12 +457,15 @@ describe("MatrixClient", function() {
describe("retryImmediately", function() {
it("should return false if there is no request waiting", async function() {
httpLookups = [];
httpLookups.push(CAPABILITIES_RESPONSE);
await client.startClient();
expect(client.retryImmediately()).toBe(false);
});
it("should work on /filter", function(done) {
httpLookups = [];
httpLookups.push(CAPABILITIES_RESPONSE);
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push({
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" },
@@ -503,6 +516,7 @@ describe("MatrixClient", function() {
it("should work on /pushrules", function(done) {
httpLookups = [];
httpLookups.push(CAPABILITIES_RESPONSE);
httpLookups.push({
method: "GET", path: "/pushrules/", error: { errcode: "NOPE_NOPE_NOPE" },
});
@@ -559,6 +573,7 @@ describe("MatrixClient", function() {
it("should transition null -> ERROR after a failed /filter", function(done) {
const expectedStates = [];
httpLookups = [];
httpLookups.push(CAPABILITIES_RESPONSE);
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push({
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" },
@@ -573,6 +588,7 @@ describe("MatrixClient", function() {
const expectedStates = [];
acceptKeepalives = false;
httpLookups = [];
httpLookups.push(CAPABILITIES_RESPONSE);
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
httpLookups.push({
@@ -697,7 +713,8 @@ describe("MatrixClient", function() {
describe("guest rooms", function() {
it("should only do /sync calls (without filter/pushrules)", function(done) {
httpLookups = []; // no /pushrules or /filter
httpLookups = []; // no /pushrules or /filterw
httpLookups.push(CAPABILITIES_RESPONSE);
httpLookups.push({
method: "GET",
path: "/sync",
@@ -728,4 +745,207 @@ describe("MatrixClient", function() {
expect(httpLookups.length).toEqual(0);
});
});
describe("sendEvent", () => {
const roomId = "!room:example.org";
const body = "This is the body";
const content = { body };
it("overload without threadId works", async () => {
const eventId = "$eventId:example.org";
const txnId = client.makeTxnId();
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
data: { event_id: eventId },
expectBody: content,
}];
await client.sendEvent(roomId, EventType.RoomMessage, content, txnId);
});
it("overload with null threadId works", async () => {
const eventId = "$eventId:example.org";
const txnId = client.makeTxnId();
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
data: { event_id: eventId },
expectBody: content,
}];
await client.sendEvent(roomId, null, EventType.RoomMessage, content, txnId);
});
it("overload with threadId works", async () => {
const eventId = "$eventId:example.org";
const txnId = client.makeTxnId();
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
data: { event_id: eventId },
expectBody: content,
}];
await client.sendEvent(roomId, "$threadId:server", EventType.RoomMessage, content, txnId);
});
});
describe("redactEvent", () => {
const roomId = "!room:example.org";
const mockRoom = {
getMyMembership: () => "join",
currentState: {
getStateEvents: (eventType, stateKey) => {
if (eventType === EventType.RoomEncryption) {
expect(stateKey).toEqual("");
return new MatrixEvent({ content: {} });
} else {
throw new Error("Unexpected event type or state key");
}
},
},
threads: {
get: jest.fn(),
},
addPendingEvent: jest.fn(),
updatePendingEvent: jest.fn(),
};
beforeEach(() => {
client.getRoom = (getRoomId) => {
expect(getRoomId).toEqual(roomId);
return mockRoom;
};
});
it("overload without threadId works", async () => {
const eventId = "$eventId:example.org";
const txnId = client.makeTxnId();
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${txnId}`,
data: { event_id: eventId },
}];
await client.redactEvent(roomId, eventId, txnId);
});
it("overload with null threadId works", async () => {
const eventId = "$eventId:example.org";
const txnId = client.makeTxnId();
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${txnId}`,
data: { event_id: eventId },
}];
await client.redactEvent(roomId, null, eventId, txnId);
});
it("overload with threadId works", async () => {
const eventId = "$eventId:example.org";
const txnId = client.makeTxnId();
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${txnId}`,
data: { event_id: eventId },
}];
await client.redactEvent(roomId, "$threadId:server", eventId, txnId);
});
it("does not get wrongly encrypted", async () => {
const eventId = "$eventId:example.org";
const txnId = client.makeTxnId();
const reason = "This is the redaction reason";
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${txnId}`,
expectBody: { reason }, // NOT ENCRYPTED
data: { event_id: eventId },
}];
await client.redactEvent(roomId, eventId, txnId, { reason });
});
});
describe("cancelPendingEvent", () => {
const roomId = "!room:server";
const txnId = "m12345";
const mockRoom = {
getMyMembership: () => "join",
updatePendingEvent: (event, status) => event.setStatus(status),
currentState: {
getStateEvents: (eventType, stateKey) => {
if (eventType === EventType.RoomCreate) {
expect(stateKey).toEqual("");
return new MatrixEvent({
content: {
[RoomCreateTypeField]: RoomType.Space,
},
});
} else if (eventType === EventType.RoomEncryption) {
expect(stateKey).toEqual("");
return new MatrixEvent({ content: {} });
} else {
throw new Error("Unexpected event type or state key");
}
},
},
};
let event;
beforeEach(async () => {
event = new MatrixEvent({
event_id: "~" + roomId + ":" + txnId,
user_id: client.credentials.userId,
sender: client.credentials.userId,
room_id: roomId,
origin_server_ts: new Date().getTime(),
});
event.setTxnId(txnId);
client.getRoom = (getRoomId) => {
expect(getRoomId).toEqual(roomId);
return mockRoom;
};
client.crypto = { // mock crypto
encryptEvent: (event, room) => new Promise(() => {}),
};
});
function assertCancelled() {
expect(event.status).toBe(EventStatus.CANCELLED);
expect(client.scheduler.removeEventFromQueue(event)).toBeFalsy();
expect(httpLookups.filter(h => h.path.includes("/send/")).length).toBe(0);
}
it("should cancel an event which is queued", () => {
event.setStatus(EventStatus.QUEUED);
client.scheduler.queueEvent(event);
client.cancelPendingEvent(event);
assertCancelled();
});
it("should cancel an event which is encrypting", async () => {
client.encryptAndSendEvent(null, event);
await testUtils.emitPromise(event, "Event.status");
client.cancelPendingEvent(event);
assertCancelled();
});
it("should cancel an event which is not sent", () => {
event.setStatus(EventStatus.NOT_SENT);
client.cancelPendingEvent(event);
assertCancelled();
});
it("should error when given any other event status", () => {
event.setStatus(EventStatus.SENDING);
expect(() => client.cancelPendingEvent(event)).toThrow("cannot cancel an event with status sending");
expect(event.status).toBe(EventStatus.SENDING);
});
});
});
+1 -1
View File
@@ -312,7 +312,7 @@ describe("MSC3089Branch", () => {
} as MatrixEvent);
const events = [await branch.getFileEvent(), await branch2.getFileEvent(), {
replacingEventId: () => null,
replacingEventId: (): string => null,
getId: () => "$unknown",
}];
staticRoom.getLiveTimeline = () => ({ getEvents: () => events }) as EventTimeline;
+1 -1
View File
@@ -1018,7 +1018,7 @@ describe("MSC3089TreeSpace", () => {
it('should return falsy for unknown files', () => {
const fileEventId = "$file";
room.currentState = {
getStateEvents: (eventType: string, stateKey?: string) => {
getStateEvents: (eventType: string, stateKey?: string): MatrixEvent[] | MatrixEvent | null => {
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
expect(stateKey).toEqual(fileEventId);
return null;
+1 -1
View File
@@ -120,7 +120,7 @@ describe("RoomState", function() {
it("should return a single MatrixEvent if a state_key was specified",
function() {
const event = state.getStateEvents("m.room.member", userA);
expect(event.getContent()).toEqual({
expect(event.getContent()).toMatchObject({
membership: "join",
});
});
+15 -2
View File
@@ -622,6 +622,11 @@ describe("Room", function() {
}, event: true,
})]);
};
const setAlias = function(alias) {
room.addLiveEvents([utils.mkEvent({
type: "m.room.canonical_alias", room: roomId, skey: "", content: { alias }, event: true,
})]);
};
const setRoomName = function(name) {
room.addLiveEvents([utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, content: {
@@ -857,7 +862,7 @@ describe("Room", function() {
"(invite join_rules) rooms if a room name doesn't exist.", function() {
const alias = "#room_alias:here";
setJoinRule("invite");
setAltAliases([alias, "#another:here"]);
setAlias(alias);
room.recalculate();
const name = room.name;
expect(name).toEqual(alias);
@@ -867,12 +872,20 @@ describe("Room", function() {
"(public join_rules) rooms if a room name doesn't exist.", function() {
const alias = "#room_alias:here";
setJoinRule("public");
setAltAliases([alias, "#another:here"]);
setAlias(alias);
room.recalculate();
const name = room.name;
expect(name).toEqual(alias);
});
it("should not show alt aliases if a room name does not exist", () => {
const alias = "#room_alias:here";
setAltAliases([alias, "#another:here"]);
room.recalculate();
const name = room.name;
expect(name).not.toEqual(alias);
});
it("should show the room name if one exists for private " +
"(invite join_rules) rooms.", function() {
const roomName = "A mighty name indeed";
+15 -66
View File
@@ -26,6 +26,15 @@ describe("utils", function() {
"foo=bar&baz=beer%40",
);
});
it("should handle boolean and numeric values", function() {
const params = {
string: "foobar",
number: 12345,
boolean: false,
};
expect(utils.encodeParams(params)).toEqual("string=foobar&number=12345&boolean=false");
});
});
describe("encodeUri", function() {
@@ -111,10 +120,10 @@ describe("utils", function() {
describe("deepCompare", function() {
const assert = {
isTrue: function(x) {
isTrue: function(x: any) {
expect(x).toBe(true);
},
isFalse: function(x) {
isFalse: function(x: any) {
expect(x).toBe(false);
},
};
@@ -176,10 +185,10 @@ describe("utils", function() {
// no two different function is equal really, they capture their
// context variables so even if they have same toString(), they
// won't have same functionality
const func = function(x) {
const func = function() {
return true;
};
const func2 = function(x) {
const func2 = function() {
return true;
};
assert.isTrue(utils.deepCompare(func, func));
@@ -189,66 +198,6 @@ describe("utils", function() {
});
});
describe("extend", function() {
const SOURCE = { "prop2": 1, "string2": "x", "newprop": "new" };
it("should extend", function() {
const target = {
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
};
const merged = {
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
"newprop": "new",
};
const sourceOrig = JSON.stringify(SOURCE);
utils.extend(target, SOURCE);
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
// check the originial wasn't modified
expect(JSON.stringify(SOURCE)).toEqual(sourceOrig);
});
it("should ignore null", function() {
const target = {
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
};
const merged = {
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
"newprop": "new",
};
const sourceOrig = JSON.stringify(SOURCE);
utils.extend(target, null, SOURCE);
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
// check the originial wasn't modified
expect(JSON.stringify(SOURCE)).toEqual(sourceOrig);
});
it("should handle properties created with defineProperties", function() {
const source = Object.defineProperties({}, {
"enumerableProp": {
get: function() {
return true;
},
enumerable: true,
},
"nonenumerableProp": {
get: function() {
return true;
},
},
});
// TODO: Fix type
const target: any = {};
utils.extend(target, source);
expect(target.enumerableProp).toBe(true);
expect(target.nonenumerableProp).toBe(undefined);
});
});
describe("chunkPromises", function() {
it("should execute promises in chunks", async function() {
let promiseCount = 0;
@@ -273,7 +222,7 @@ describe("utils", function() {
it('should retry', async () => {
let count = 0;
const val = {};
const fn = (attempt) => {
const fn = (attempt: any) => {
count++;
// If this expectation fails then it can appear as a Jest Timeout due to
@@ -480,7 +429,7 @@ describe("utils", function() {
},
[72]: "test",
};
const output = [
const output: any = [
["72", "test"],
["a", 42],
["b", [
+3 -2
View File
@@ -17,6 +17,7 @@ limitations under the License.
import { TestClient } from '../../TestClient';
import { MatrixCall, CallErrorCode, CallEvent } from '../../../src/webrtc/call';
import { SDPStreamMetadataKey, SDPStreamMetadataPurpose } from '../../../src/webrtc/callEventTypes';
import { RoomMember } from "../../../src";
const DUMMY_SDP = (
"v=0\r\n" +
@@ -85,7 +86,7 @@ class MockRTCPeerConnection {
class MockMediaStream {
constructor(
public id,
public id: string,
) {}
getTracks() { return []; }
@@ -362,7 +363,7 @@ describe('Call', function() {
await callPromise;
call.getOpponentMember = () => {
return { userId: "@bob:bar.uk" };
return { userId: "@bob:bar.uk" } as RoomMember;
};
await call.onAnswerReceived({
+10
View File
@@ -177,6 +177,16 @@ export const UNSTABLE_ELEMENT_FUNCTIONAL_USERS = new UnstableValue(
"io.element.functional_members",
"io.element.functional_members");
/**
* A type of message that affects visibility of a message,
* as per https://github.com/matrix-org/matrix-doc/pull/3531
*
* @experimental
*/
export const EVENT_VISIBILITY_CHANGE_TYPE = new UnstableValue(
"m.visibility",
"org.matrix.msc3531.visibility");
export interface IEncryptedFile {
url: string;
mimetype?: string;
@@ -14,13 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// TODO: Merge this with sync.js once converted
// Types for MSC1767: Extensible events in Matrix
export enum SyncState {
Error = "ERROR",
Prepared = "PREPARED",
Stopped = "STOPPED",
Syncing = "SYNCING",
Catchup = "CATCHUP",
Reconnecting = "RECONNECTING",
}
import { UnstableValue } from "../NamespacedValue";
export const TEXT_NODE_TYPE = new UnstableValue("m.text", "org.matrix.msc1767.text");
-6
View File
@@ -62,12 +62,6 @@ declare global {
};
}
interface HTMLAudioElement {
// sinkId & setSinkId are experimental and typescript doesn't know about them
sinkId: string;
setSinkId(outputId: string);
}
interface DummyInterfaceWeShouldntBeUsingThis {}
interface Navigator {
+68
View File
@@ -0,0 +1,68 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Types for MSC3488 - m.location: Extending events with location data
import { UnstableValue } from "../NamespacedValue";
import { IContent } from "../models/event";
import { TEXT_NODE_TYPE } from "./extensible_events";
export const LOCATION_EVENT_TYPE = new UnstableValue(
"m.location", "org.matrix.msc3488.location");
export const ASSET_NODE_TYPE = new UnstableValue("m.asset", "org.matrix.msc3488.asset");
export const TIMESTAMP_NODE_TYPE = new UnstableValue("m.ts", "org.matrix.msc3488.ts");
export const ASSET_TYPE_SELF = "m.self";
/* From the spec at:
* https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md
{
"type": "m.room.message",
"content": {
"body": "Matthew was at geo:51.5008,0.1247;u=35 as of Sat Nov 13 18:50:58 2021",
"msgtype": "m.location",
"geo_uri": "geo:51.5008,0.1247;u=35",
"m.location": {
"uri": "geo:51.5008,0.1247;u=35",
"description": "Matthew's whereabouts",
},
"m.asset": {
"type": "m.self"
},
"m.text": "Matthew was at geo:51.5008,0.1247;u=35 as of Sat Nov 13 18:50:58 2021",
"m.ts": 1636829458432,
}
}
*/
/* eslint-disable camelcase */
export interface ILocationContent extends IContent {
body: string;
msgtype: string;
geo_uri: string;
[LOCATION_EVENT_TYPE.name]: {
uri: string;
description?: string;
};
[ASSET_NODE_TYPE.name]: {
type: string;
};
[TEXT_NODE_TYPE.name]: string;
[TIMESTAMP_NODE_TYPE.name]: number;
}
/* eslint-enable camelcase */
+9
View File
@@ -82,3 +82,12 @@ export enum HistoryVisibility {
Shared = "shared",
WorldReadable = "world_readable",
}
export interface IUsageLimit {
// "hs_disabled" is NOT a specced string, but is used in Synapse
// This is tracked over at https://github.com/matrix-org/synapse/issues/9237
// eslint-disable-next-line camelcase
limit_type: "monthly_active_user" | "hs_disabled" | string;
// eslint-disable-next-line camelcase
admin_contact?: string;
}
+15 -1
View File
@@ -15,7 +15,7 @@ limitations under the License.
*/
import { Callback } from "../client";
import { IContent } from "../models/event";
import { IContent, IEvent } from "../models/event";
import { Preset, Visibility } from "./partials";
import { SearchKey } from "./search";
import { IRoomEventFilter } from "../filter";
@@ -139,4 +139,18 @@ export interface IBindThreePidBody {
id_access_token: string;
sid: string;
}
export interface IRelationsRequestOpts {
from?: string;
to?: string;
limit?: number;
}
export interface IRelationsResponse {
original_event: IEvent;
chunk: IEvent[];
next_batch?: string;
prev_batch?: string;
}
/* eslint-enable camelcase */
-1
View File
@@ -44,7 +44,6 @@ export interface ISpaceSummaryEvent {
}
export interface IHierarchyRelation extends IStrippedState {
room_id: string;
origin_server_ts: number;
content: {
order?: string;
+1 -1
View File
@@ -30,7 +30,7 @@ export class ReEmitter {
// We include the source as the last argument for event handlers which may need it,
// such as read receipt listeners on the client class which won't have the context
// of the room.
const forSource = (...args) => {
const forSource = (...args: any[]) => {
// EventEmitter special cases 'error' to make the emit function throw if no
// handler is attached, which sort of makes sense for making sure that something
// handles an error, but for re-emitting, there could be a listener on the original
+2 -1
View File
@@ -17,9 +17,10 @@ limitations under the License.
/** @module auto-discovery */
import { URL as NodeURL } from "url";
import { IClientWellKnown, IWellKnownConfig } from "./client";
import { logger } from './logger';
import { URL as NodeURL } from "url";
// Dev note: Auto discovery is part of the spec.
// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
+2 -1
View File
@@ -14,10 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as matrixcs from "./matrix";
import request from "browser-request";
import queryString from "qs";
import * as matrixcs from "./matrix";
matrixcs.request(function(opts, fn) {
// We manually fix the query string for browser-request because
// it doesn't correctly handle cases like ?via=one&via=two. Instead
+588 -340
View File
File diff suppressed because it is too large Load Diff
+42 -2
View File
@@ -1,6 +1,5 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2018 - 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -18,6 +17,14 @@ limitations under the License.
/** @module ContentHelpers */
import { MsgType } from "./@types/event";
import { TEXT_NODE_TYPE } from "./@types/extensible_events";
import {
ASSET_NODE_TYPE,
ASSET_TYPE_SELF,
ILocationContent,
LOCATION_EVENT_TYPE,
TIMESTAMP_NODE_TYPE,
} from "./@types/location";
/**
* Generates the content for a HTML Message event
@@ -99,3 +106,36 @@ export function makeEmoteMessage(body: string) {
body: body,
};
}
/**
* Generates the content for a Location event
* @param text a text for of our location
* @param uri a geo:// uri for the location
* @param ts the timestamp when the location was correct (milliseconds since
* the UNIX epoch)
* @param description the (optional) label for this location on the map
* @param asset_type the (optional) asset type of this location e.g. "m.self"
*/
export function makeLocationContent(
text: string,
uri: string,
ts: number,
description?: string,
assetType?: string,
): ILocationContent {
return {
"body": text,
"msgtype": MsgType.Location,
"geo_uri": uri,
[LOCATION_EVENT_TYPE.name]: {
uri,
description,
},
[ASSET_NODE_TYPE.name]: {
type: assetType ?? ASSET_TYPE_SELF,
},
[TEXT_NODE_TYPE.name]: text,
[TIMESTAMP_NODE_TYPE.name]: ts,
// TODO: MSC1767 fallbacks m.image thumbnail
};
}
+3 -3
View File
@@ -53,13 +53,13 @@ export function getHttpUriForMxc(
}
let serverAndMediaId = mxc.slice(6); // strips mxc://
let prefix = "/_matrix/media/r0/download/";
const params = {};
const params: Record<string, string> = {};
if (width) {
params["width"] = Math.round(width);
params["width"] = Math.round(width).toString();
}
if (height) {
params["height"] = Math.round(height);
params["height"] = Math.round(height).toString();
}
if (resizeMethod) {
params["method"] = resizeMethod;
+3 -2
View File
@@ -20,12 +20,12 @@ limitations under the License.
*/
import { EventEmitter } from 'events';
import { PkSigning } from "@matrix-org/olm";
import { decodeBase64, encodeBase64, pkSign, pkVerify } from './olmlib';
import { logger } from '../logger';
import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store';
import { decryptAES, encryptAES } from './aes';
import { PkSigning } from "@matrix-org/olm";
import { DeviceInfo } from "./deviceinfo";
import { SecretStorage } from "./SecretStorage";
import { ICrossSigningKey, ISignedKey, MatrixClient } from "../client";
@@ -33,6 +33,7 @@ import { OlmDevice } from "./OlmDevice";
import { ICryptoCallbacks } from "../matrix";
import { ISignatures } from "../@types/signed";
import { CryptoStore } from "./store/base";
import { ISecretStorageKeyInfo } from "./api";
const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;
@@ -175,7 +176,7 @@ export class CrossSigningInfo extends EventEmitter {
// check what SSSS keys have encrypted the master key (if any)
const stored = await secretStorage.isStored("m.cross_signing.master", false) || {};
// then check which of those SSSS keys have also encrypted the SSK and USK
function intersect(s) {
function intersect(s: Record<string, ISecretStorageKeyInfo>) {
for (const k of Object.keys(stored)) {
if (!s[k]) {
delete stored[k];
+21 -13
View File
@@ -28,7 +28,7 @@ import { CrossSigningInfo, ICrossSigningInfo } from './CrossSigning';
import * as olmlib from './olmlib';
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { chunkPromises, defer, IDeferred, sleep } from '../utils';
import { MatrixClient } from "../client";
import { IDownloadKeyResult, MatrixClient } from "../client";
import { OlmDevice } from "./OlmDevice";
import { CryptoStore } from "./store/base";
@@ -265,8 +265,8 @@ export class DeviceList extends EventEmitter {
* module:crypto/deviceinfo|DeviceInfo}.
*/
public downloadKeys(userIds: string[], forceDownload: boolean): Promise<DeviceInfoMap> {
const usersToDownload = [];
const promises = [];
const usersToDownload: string[] = [];
const promises: Promise<unknown>[] = [];
userIds.forEach((u) => {
const trackingStatus = this.deviceTrackingStatus[u];
@@ -633,7 +633,7 @@ export class DeviceList extends EventEmitter {
}
});
const finished = (success) => {
const finished = (success: boolean): void => {
this.emit("crypto.willUpdateDevices", users, !this.hasFetched);
users.forEach((u) => {
this.dirty = true;
@@ -756,17 +756,21 @@ class DeviceListUpdateSerialiser {
opts.token = this.syncToken;
}
const factories = [];
const factories: Array<() => Promise<IDownloadKeyResult>> = [];
for (let i = 0; i < downloadUsers.length; i += this.deviceList.keyDownloadChunkSize) {
const userSlice = downloadUsers.slice(i, i + this.deviceList.keyDownloadChunkSize);
factories.push(() => this.baseApis.downloadKeysForUsers(userSlice, opts));
}
chunkPromises(factories, 3).then(async (responses: any[]) => {
const dk = Object.assign({}, ...(responses.map(res => res.device_keys || {})));
const masterKeys = Object.assign({}, ...(responses.map(res => res.master_keys || {})));
const ssks = Object.assign({}, ...(responses.map(res => res.self_signing_keys || {})));
const usks = Object.assign({}, ...(responses.map(res => res.user_signing_keys || {})));
chunkPromises(factories, 3).then(async (responses: IDownloadKeyResult[]) => {
const dk: IDownloadKeyResult["device_keys"]
= Object.assign({}, ...(responses.map(res => res.device_keys || {})));
const masterKeys: IDownloadKeyResult["master_keys"]
= Object.assign({}, ...(responses.map(res => res.master_keys || {})));
const ssks: IDownloadKeyResult["self_signing_keys"]
= Object.assign({}, ...(responses.map(res => res.self_signing_keys || {})));
const usks: IDownloadKeyResult["user_signing_keys"]
= Object.assign({}, ...(responses.map(res => res.user_signing_keys || {})));
// yield to other things that want to execute in between users, to
// avoid wedging the CPU
@@ -811,8 +815,12 @@ class DeviceListUpdateSerialiser {
private async processQueryResponseForUser(
userId: string,
dkResponse: object,
crossSigningResponse: any, // TODO types
dkResponse: IDownloadKeyResult["device_keys"]["user_id"],
crossSigningResponse: {
master: IDownloadKeyResult["master_keys"]["user_id"];
self_signing: IDownloadKeyResult["master_keys"]["user_id"]; // eslint-disable-line camelcase
user_signing: IDownloadKeyResult["user_signing_keys"]["user_id"]; // eslint-disable-line camelcase
},
): Promise<void> {
logger.log('got device keys for ' + userId + ':', dkResponse);
logger.log('got cross-signing keys for ' + userId + ':', crossSigningResponse);
@@ -869,7 +877,7 @@ async function updateStoredDeviceKeysForUser(
olmDevice: OlmDevice,
userId: string,
userStore: Record<string, DeviceInfo>,
userResult: object,
userResult: IDownloadKeyResult["device_keys"]["user_id"],
localUserId: string,
localDeviceId: string,
): Promise<boolean> {
+6 -11
View File
@@ -14,20 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { EventEmitter } from "events";
import { logger } from "../logger";
import { MatrixEvent } from "../models/event";
import { EventEmitter } from "events";
import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning";
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { PREFIX_UNSTABLE } from "../http-api";
import { Method, PREFIX_UNSTABLE } from "../http-api";
import { Crypto, IBootstrapCrossSigningOpts } from "./index";
import {
CrossSigningKeys,
ICrossSigningKey,
ICryptoCallbacks,
ISignedKey,
KeySignatures,
} from "../matrix";
import { CrossSigningKeys, ICrossSigningKey, ICryptoCallbacks, ISignedKey, KeySignatures } from "../matrix";
import { ISecretStorageKeyInfo } from "./api";
import { IKeyBackupInfo } from "./keybackup";
@@ -238,7 +233,7 @@ export class EncryptionSetupOperation {
// Sign the backup with the cross signing key so the key backup can
// be trusted via cross-signing.
await baseApis.http.authedRequest(
undefined, "PUT", "/room_keys/version/" + this.keyBackupInfo.version,
undefined, Method.Put, "/room_keys/version/" + this.keyBackupInfo.version,
undefined, {
algorithm: this.keyBackupInfo.algorithm,
auth_data: this.keyBackupInfo.auth_data,
@@ -248,7 +243,7 @@ export class EncryptionSetupOperation {
} else {
// add new key backup
await baseApis.http.authedRequest(
undefined, "POST", "/room_keys/version",
undefined, Method.Post, "/room_keys/version",
undefined, this.keyBackupInfo,
{ prefix: PREFIX_UNSTABLE },
);
+16 -3
View File
@@ -14,12 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { Account, InboundGroupSession, OutboundGroupSession, Session, Utility } from "@matrix-org/olm";
import { Logger } from "loglevel";
import { logger } from '../logger';
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import * as algorithms from './algorithms';
import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base";
import { Account, InboundGroupSession, OutboundGroupSession, Session, Utility } from "@matrix-org/olm";
import { Logger } from "loglevel";
import { IOlmDevice, IOutboundGroupSessionKey } from "./algorithms/megolm";
import { IMegolmSessionData } from "./index";
@@ -542,13 +543,25 @@ export class OlmDevice {
'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
this.getAccount(txn, (account: Account) => {
result = JSON.parse(account.fallback_key());
result = JSON.parse(account.unpublished_fallback_key());
});
},
);
return result;
}
public async forgetOldFallbackKey(): Promise<void> {
await this.cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
this.getAccount(txn, (account: Account) => {
account.forget_old_fallback_key();
this.storeAccount(txn, account);
});
},
);
}
/**
* Generate a new outbound session
*
+30 -20
View File
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { EventEmitter } from 'stream';
import { logger } from '../logger';
import * as olmlib from './olmlib';
import { randomString } from '../randomstring';
@@ -21,7 +23,6 @@ import { encryptAES, decryptAES, IEncryptedPayload, calculateKeyCheck } from './
import { encodeBase64 } from "./olmlib";
import { ICryptoCallbacks, MatrixClient, MatrixEvent } from '../matrix';
import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from './api';
import { EventEmitter } from 'stream';
export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2";
@@ -37,7 +38,7 @@ export interface ISecretRequest {
export interface IAccountDataClient extends EventEmitter {
// Subset of MatrixClient (which also uses any for the event content)
getAccountDataFromServer: (eventType: string) => Promise<Record<string, any>>;
getAccountDataFromServer: <T extends {[k: string]: any}>(eventType: string) => Promise<T>;
getAccountData: (eventType: string) => MatrixEvent;
setAccountData: (eventType: string, content: any) => Promise<{}>;
}
@@ -54,6 +55,13 @@ interface IDecryptors {
decrypt: (ciphertext: IEncryptedPayload) => Promise<string>;
}
interface ISecretInfo {
encrypted: {
// eslint-disable-next-line camelcase
key_id: IEncryptedPayload;
};
}
/**
* Implements Secure Secret Storage and Sharing (MSC1946)
* @module crypto/SecretStorage
@@ -75,8 +83,8 @@ export class SecretStorage {
private readonly baseApis?: MatrixClient,
) {}
public async getDefaultKeyId(): Promise<string> {
const defaultKey = await this.accountDataAdapter.getAccountDataFromServer(
public async getDefaultKeyId(): Promise<string | null> {
const defaultKey = await this.accountDataAdapter.getAccountDataFromServer<{ key: string }>(
'm.secret_storage.default_key',
);
if (!defaultKey) return null;
@@ -149,7 +157,7 @@ export class SecretStorage {
do {
keyId = randomString(32);
} while (
await this.accountDataAdapter.getAccountDataFromServer(
await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
`m.secret_storage.key.${keyId}`,
)
);
@@ -182,9 +190,9 @@ export class SecretStorage {
return null;
}
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
"m.secret_storage.key." + keyId,
) as ISecretStorageKeyInfo;
);
return keyInfo ? [keyId, keyInfo] : null;
}
@@ -230,7 +238,7 @@ export class SecretStorage {
* or null/undefined to use the default key.
*/
public async store(name: string, secret: string, keys?: string[]): Promise<void> {
const encrypted = {};
const encrypted: Record<string, IEncryptedPayload> = {};
if (!keys) {
const defaultKeyId = await this.getDefaultKeyId();
@@ -246,9 +254,9 @@ export class SecretStorage {
for (const keyId of keys) {
// get key information from key storage
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
"m.secret_storage.key." + keyId,
) as ISecretStorageKeyInfo;
);
if (!keyInfo) {
throw new Error("Unknown key: " + keyId);
}
@@ -277,7 +285,7 @@ export class SecretStorage {
* @return {string} the contents of the secret
*/
public async get(name: string): Promise<string> {
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name);
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name);
if (!secretInfo) {
return;
}
@@ -286,11 +294,13 @@ export class SecretStorage {
}
// get possible keys to decrypt
const keys = {};
const keys: Record<string, ISecretStorageKeyInfo> = {};
for (const keyId of Object.keys(secretInfo.encrypted)) {
// get key information from key storage
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
"m.secret_storage.key." + keyId,
const keyInfo = (
await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
"m.secret_storage.key." + keyId,
)
);
const encInfo = secretInfo.encrypted[keyId];
// only use keys we understand the encryption algorithm of
@@ -306,7 +316,7 @@ export class SecretStorage {
`the keys it is encrypted with are for a supported algorithm`);
}
let keyId;
let keyId: string;
let decryption;
try {
// fetch private key from app
@@ -335,9 +345,9 @@ export class SecretStorage {
* with, or null if it is not present or not encrypted with a trusted
* key
*/
public async isStored(name: string, checkKey: boolean): Promise<Record<string, ISecretStorageKeyInfo>> {
public async isStored(name: string, checkKey: boolean): Promise<Record<string, ISecretStorageKeyInfo> | null> {
// check if secret exists
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name);
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name);
if (!secretInfo) return null;
if (!secretInfo.encrypted) {
return null;
@@ -350,7 +360,7 @@ export class SecretStorage {
// filter secret encryption keys with supported algorithm
for (const keyId of Object.keys(secretInfo.encrypted)) {
// get key information from key storage
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
"m.secret_storage.key." + keyId,
);
if (!keyInfo) continue;
@@ -375,8 +385,8 @@ export class SecretStorage {
public request(name: string, devices: string[]): ISecretRequest {
const requestId = this.baseApis.makeTxnId();
let resolve: (string) => void;
let reject: (Error) => void;
let resolve: (s: string) => void;
let reject: (e: Error) => void;
const promise = new Promise<string>((res, rej) => {
resolve = res;
reject = rej;
-1
View File
@@ -15,7 +15,6 @@ limitations under the License.
*/
import type { BinaryLike } from "crypto";
import { getCrypto } from '../utils';
import { decodeBase64, encodeBase64 } from './olmlib';
+1 -1
View File
@@ -46,7 +46,7 @@ type DecryptionClassParams = Omit<IParams, "deviceId" | "config">;
*/
export const DECRYPTION_CLASSES: Record<string, new (params: DecryptionClassParams) => DecryptionAlgorithm> = {};
interface IParams {
export interface IParams {
userId: string;
deviceId: string;
crypto: Crypto;
+27 -21
View File
@@ -26,6 +26,7 @@ import {
DecryptionAlgorithm,
DecryptionError,
EncryptionAlgorithm,
IParams,
registerAlgorithm,
UnknownDeviceError,
} from "./base";
@@ -99,6 +100,12 @@ interface IPayload extends Partial<IMessage> {
algorithm?: string;
sender_key?: string;
}
interface IEncryptedContent {
algorithm: string;
sender_key: string;
ciphertext: Record<string, string>;
}
/* eslint-enable camelcase */
interface SharedWithData {
@@ -238,7 +245,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
startTime: number;
};
constructor(params) {
constructor(params: IParams) {
super(params);
this.sessionRotationPeriodMsgs = params.config?.rotation_period_msgs ?? 100;
@@ -263,7 +270,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
blocked: IBlockedMap,
singleOlmCreationPhase = false,
): Promise<OutboundSessionInfo> {
let session;
let session: OutboundSessionInfo;
// takes the previous OutboundSessionInfo, and considers whether to create
// a new one. Also shares the key with any (new) devices in the room.
@@ -302,7 +309,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
}
// now check if we need to share with any devices
const shareMap = {};
const shareMap: Record<string, DeviceInfo[]> = {};
for (const [userId, userDevices] of Object.entries(devicesInRoom)) {
for (const [deviceId, deviceInfo] of Object.entries(userDevices)) {
@@ -350,7 +357,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
`Sharing keys (start phase 1) with new Olm sessions in ${this.roomId}`,
devicesWithoutSession,
);
const errorDevices = [];
const errorDevices: IOlmDevice[] = [];
// meanwhile, establish olm sessions for devices that we don't
// already have a session for, and share keys with them. If
@@ -358,7 +365,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
// shorter timeout when fetching one-time keys for the first
// phase.
const start = Date.now();
const failedServers = [];
const failedServers: string[] = [];
await this.shareKeyWithDevices(
session, key, payload, devicesWithoutSession, errorDevices,
singleOlmCreationPhase ? 10000 : 2000, failedServers,
@@ -374,7 +381,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
// do this in the background and don't block anything else while we
// do this. We only need to retry users from servers that didn't
// respond the first time.
const retryDevices = {};
const retryDevices: Record<string, DeviceInfo[]> = {};
const failedServerMap = new Set;
for (const server of failedServers) {
failedServerMap.add(server);
@@ -584,12 +591,12 @@ class MegolmEncryption extends EncryptionAlgorithm {
userDeviceMap: IOlmDevice[],
payload: IPayload,
): Promise<void> {
const contentMap = {};
const contentMap: Record<string, Record<string, IEncryptedContent>> = {};
const deviceInfoByDeviceId = new Map<string, DeviceInfo>();
const promises = [];
const promises: Promise<unknown>[] = [];
for (let i = 0; i < userDeviceMap.length; i++) {
const encryptedContent = {
const encryptedContent: IEncryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key,
ciphertext: {},
@@ -679,7 +686,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
userDeviceMap: IOlmDevice<IBlockedDevice>[],
payload: IPayload,
): Promise<void> {
const contentMap = {};
const contentMap: Record<string, Record<string, IPayload>> = {};
for (const val of userDeviceMap) {
const userId = val.userId;
@@ -1105,10 +1112,10 @@ class MegolmEncryption extends EncryptionAlgorithm {
* devices we should shared the session with.
*/
private checkForUnknownDevices(devicesInRoom: DeviceInfoMap): void {
const unknownDevices = {};
const unknownDevices: Record<string, Record<string, DeviceInfo>> = {};
Object.keys(devicesInRoom).forEach((userId)=>{
Object.keys(devicesInRoom[userId]).forEach((deviceId)=>{
Object.keys(devicesInRoom).forEach((userId) => {
Object.keys(devicesInRoom[userId]).forEach((deviceId) => {
const device = devicesInRoom[userId][deviceId];
if (device.isUnverified() && !device.isKnown()) {
if (!unknownDevices[userId]) {
@@ -1304,8 +1311,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
content.sender_key, event.getTs() - 120000,
);
if (problem) {
let problemDescription = PROBLEM_DESCRIPTIONS[problem.type]
|| PROBLEM_DESCRIPTIONS.unknown;
let problemDescription = PROBLEM_DESCRIPTIONS[problem.type as "no_olm"] || PROBLEM_DESCRIPTIONS.unknown;
if (problem.fixed) {
problemDescription +=
" Trying to create a new secure channel and re-requesting the keys.";
@@ -1399,14 +1405,14 @@ class MegolmDecryption extends DecryptionAlgorithm {
const senderKey = content.sender_key;
const sessionId = content.session_id;
const senderPendingEvents = this.pendingEvents[senderKey];
const pendingEvents = senderPendingEvents && senderPendingEvents.get(sessionId);
const pendingEvents = senderPendingEvents?.get(sessionId);
if (!pendingEvents) {
return;
}
pendingEvents.delete(event);
if (pendingEvents.size === 0) {
senderPendingEvents.delete(senderKey);
senderPendingEvents.delete(sessionId);
}
if (senderPendingEvents.size === 0) {
delete this.pendingEvents[senderKey];
@@ -1760,7 +1766,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
}));
// If decrypted successfully, they'll have been removed from pendingEvents
return !((this.pendingEvents[senderKey] || {})[sessionId]);
return !this.pendingEvents[senderKey]?.has(sessionId);
}
public async retryDecryptionFromSender(senderKey: string): Promise<boolean> {
@@ -1794,12 +1800,12 @@ class MegolmDecryption extends DecryptionAlgorithm {
for (const [senderKey, sessionId] of sharedHistorySessions) {
const payload = await this.buildKeyForwardingMessage(this.roomId, senderKey, sessionId);
const promises = [];
const contentMap = {};
const promises: Promise<unknown>[] = [];
const contentMap: Record<string, Record<string, IEncryptedContent>> = {};
for (const [userId, devices] of Object.entries(devicesByUser)) {
contentMap[userId] = {};
for (const deviceInfo of devices) {
const encryptedContent = {
const encryptedContent: IEncryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key,
ciphertext: {},
+1 -1
View File
@@ -282,7 +282,7 @@ class OlmDecryption extends DecryptionAlgorithm {
const sessionIds = await this.olmDevice.getSessionIdsForDevice(theirDeviceIdentityKey);
// try each session in turn.
const decryptionErrors = {};
const decryptionErrors: Record<string, string> = {};
for (let i = 0; i < sessionIds.length; i++) {
const sessionId = sessionIds[i];
try {
+8 -10
View File
@@ -33,6 +33,7 @@ import { encryptAES, decryptAES, calculateKeyCheck } from './aes';
import { getCrypto } from '../utils';
import { ICurve25519AuthData, IAes256AuthData, IKeyBackupInfo, IKeyBackupSession } from "./keybackup";
import { UnstableValue } from "../NamespacedValue";
import { IMegolmSessionData } from "./index";
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
@@ -87,7 +88,7 @@ interface BackupAlgorithmClass {
interface BackupAlgorithm {
untrusted: boolean;
encryptSession(data: Record<string, any>): Promise<any>;
decryptSessions(ciphertexts: Record<string, IKeyBackupSession>): Promise<Record<string, any>[]>;
decryptSessions(ciphertexts: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]>;
authData: AuthData;
keyMatches(key: ArrayLike<number>): Promise<boolean>;
free(): void;
@@ -185,7 +186,6 @@ export class BackupManager {
public async prepareKeyBackupVersion(
key?: string | Uint8Array | null,
algorithm?: string | undefined,
// eslint-disable-next-line camelcase
): Promise<IPreparedKeyBackupVersion> {
const Algorithm = algorithm ? algorithmsByName[algorithm] : DefaultAlgorithm;
if (!Algorithm) {
@@ -300,7 +300,7 @@ export class BackupManager {
const ret = {
usable: false,
trusted_locally: false,
sigs: [],
sigs: [] as SigInfo[],
};
if (
@@ -320,7 +320,7 @@ export class BackupManager {
ret.trusted_locally = true;
}
const mySigs = backupInfo.auth_data.signatures[this.baseApis.getUserId()] || [];
const mySigs = backupInfo.auth_data.signatures[this.baseApis.getUserId()] || {};
for (const keyId of Object.keys(mySigs)) {
const keyIdParts = keyId.split(':');
@@ -645,9 +645,7 @@ export class Curve25519 implements BackupAlgorithm {
return this.publicKey.encrypt(JSON.stringify(plainText));
}
public async decryptSessions(
sessions: Record<string, IKeyBackupSession>,
): Promise<Record<string, any>[]> {
public async decryptSessions(sessions: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]> {
const privKey = await this.getKey();
const decryption = new global.Olm.PkDecryption();
try {
@@ -658,7 +656,7 @@ export class Curve25519 implements BackupAlgorithm {
throw { errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY };
}
const keys = [];
const keys: IMegolmSessionData[] = [];
for (const [sessionId, sessionData] of Object.entries(sessions)) {
try {
@@ -777,8 +775,8 @@ export class Aes256 implements BackupAlgorithm {
return await encryptAES(JSON.stringify(plainText), this.key, data.session_id);
}
async decryptSessions(sessions: Record<string, IKeyBackupSession>): Promise<Record<string, any>[]> {
const keys = [];
async decryptSessions(sessions: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]> {
const keys: IMegolmSessionData[] = [];
for (const [sessionId, sessionData] of Object.entries(sessions)) {
try {
+11 -10
View File
@@ -14,16 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import anotherjson from "another-json";
import { decodeBase64, encodeBase64 } from './olmlib';
import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store';
import { decryptAES, encryptAES } from './aes';
import anotherjson from "another-json";
import { logger } from '../logger';
import { ISecretStorageKeyInfo } from "./api";
import { Crypto } from "./index";
// FIXME: these types should eventually go in a different file
type Signatures = Record<string, Record<string, string>>;
import { Method } from "../http-api";
import { ISignatures } from "../@types/signed";
export interface IDehydratedDevice {
device_id: string; // eslint-disable-line camelcase
@@ -42,13 +42,13 @@ export interface IDeviceKeys {
device_id: string; // eslint-disable-line camelcase
user_id: string; // eslint-disable-line camelcase
keys: Record<string, string>;
signatures?: Signatures;
signatures?: ISignatures;
}
export interface IOneTimeKey {
key: string;
fallback?: boolean;
signatures?: Signatures;
signatures?: ISignatures;
}
export const DEHYDRATION_ALGORITHM = "org.matrix.msc2697.v1.olm.libolm_pickle";
@@ -206,9 +206,10 @@ export class DehydrationManager {
}
logger.log("Uploading account to server");
const dehydrateResult = await this.crypto.baseApis.http.authedRequest(
// eslint-disable-next-line camelcase
const dehydrateResult = await this.crypto.baseApis.http.authedRequest<{ device_id: string }>(
undefined,
"PUT",
Method.Put,
"/dehydrated_device",
undefined,
{
@@ -243,7 +244,7 @@ export class DehydrationManager {
}
logger.log("Preparing one-time keys");
const oneTimeKeys = {};
const oneTimeKeys: Record<string, IOneTimeKey> = {};
for (const [keyId, key] of Object.entries(otks.curve25519)) {
const k: IOneTimeKey = { key };
const signature = account.sign(anotherjson.stringify(k));
@@ -271,7 +272,7 @@ export class DehydrationManager {
logger.log("Uploading keys to server");
await this.crypto.baseApis.http.authedRequest(
undefined,
"POST",
Method.Post,
"/keys/upload/" + encodeURI(deviceId),
undefined,
{
+72 -56
View File
@@ -58,7 +58,7 @@ import { BackupManager } from "./backup";
import { IStore } from "../store";
import { Room } from "../models/room";
import { RoomMember } from "../models/room-member";
import { MatrixEvent, EventStatus } from "../models/event";
import { MatrixEvent, EventStatus, IClearEvent, IEvent } from "../models/event";
import { MatrixClient, IKeysUploadResponse, SessionStore, ISignedKey, ICrossSigningKey } from "../client";
import type { EncryptionAlgorithm, DecryptionAlgorithm } from "./algorithms/base";
import type { IRoomEncryption, RoomList } from "./RoomList";
@@ -172,13 +172,20 @@ interface ISignableObject {
}
export interface IEventDecryptionResult {
clearEvent: object;
clearEvent: IClearEvent;
forwardingCurve25519KeyChain?: string[];
senderCurve25519Key?: string;
claimedEd25519Key?: string;
forwardingCurve25519KeyChain?: string[];
untrusted?: boolean;
}
export interface IRequestsMap {
getRequest(event: MatrixEvent): VerificationRequest;
getRequestByChannel(channel: IVerificationChannel): VerificationRequest;
setRequest(event: MatrixEvent, request: VerificationRequest): void;
setRequestByChannel(channel: IVerificationChannel, request: VerificationRequest): void;
}
export class Crypto extends EventEmitter {
/**
* @return {string} The version of Olm.
@@ -248,6 +255,7 @@ export class Crypto extends EventEmitter {
private oneTimeKeyCount: number;
private needsNewFallback: boolean;
private fallbackCleanup?: number; // setTimeout ID
/**
* Cryptography bits
@@ -807,7 +815,7 @@ export class Crypto extends EventEmitter {
}
};
const signKeyBackupWithCrossSigning = async (keyBackupAuthData) => {
const signKeyBackupWithCrossSigning = async (keyBackupAuthData: IKeyBackupInfo["auth_data"]) => {
if (
this.crossSigningInfo.getId() &&
await this.crossSigningInfo.isStoredInKeyCache("master")
@@ -1016,7 +1024,7 @@ export class Crypto extends EventEmitter {
public isSecretStored(
name: string,
checkKey?: boolean,
): Promise<Record<string, ISecretStorageKeyInfo>> {
): Promise<Record<string, ISecretStorageKeyInfo> | null> {
return this.secretStorage.isStored(name, checkKey);
}
@@ -1027,7 +1035,7 @@ export class Crypto extends EventEmitter {
return this.secretStorage.request(name, devices);
}
public getDefaultSecretStorageKeyId(): Promise<string> {
public getDefaultSecretStorageKeyId(): Promise<string | null> {
return this.secretStorage.getDefaultKeyId();
}
@@ -1147,7 +1155,7 @@ export class Crypto extends EventEmitter {
const signedDevice = await this.crossSigningInfo.signDevice(this.userId, device);
logger.info(`Starting background key sig upload for ${this.deviceId}`);
const upload = ({ shouldEmit }) => {
const upload = ({ shouldEmit = false }) => {
return this.baseApis.uploadKeySignatures({
[this.userId]: {
[this.deviceId]: signedDevice,
@@ -1183,7 +1191,7 @@ export class Crypto extends EventEmitter {
// Check all users for signatures if upgrade callback present
// FIXME: do this in batches
const users = {};
const users: Record<string, IDeviceVerificationUpgrade> = {};
for (const [userId, crossSigningInfo]
of Object.entries(this.deviceList.crossSigningInfo)) {
const upgradeInfo = await this.checkForDeviceVerificationUpgrade(
@@ -1481,7 +1489,7 @@ export class Crypto extends EventEmitter {
!crossSigningPrivateKeys.has("user_signing")
);
const keySignatures = {};
const keySignatures: Record<string, ISignedKey> = {};
if (selfSigningChanged) {
logger.info("Got new self-signing key", newCrossSigning.getId("self_signing"));
@@ -1536,7 +1544,7 @@ export class Crypto extends EventEmitter {
// We may have existing signatures from deleted devices, which will cause
// the entire upload to fail.
keySignatures[this.crossSigningInfo.getId()] = Object.assign(
{},
{} as ISignedKey,
masterKey,
{
signatures: {
@@ -1550,7 +1558,7 @@ export class Crypto extends EventEmitter {
const keysToUpload = Object.keys(keySignatures);
if (keysToUpload.length) {
const upload = ({ shouldEmit }) => {
const upload = ({ shouldEmit = false }) => {
logger.info(`Starting background key sig upload for ${keysToUpload}`);
return this.baseApis.uploadKeySignatures({ [this.userId]: keySignatures })
.then((response) => {
@@ -1850,8 +1858,23 @@ export class Crypto extends EventEmitter {
}
if (this.getNeedsNewFallback()) {
logger.info("generating fallback key");
await this.olmDevice.generateFallbackKey();
const fallbackKeys = await this.olmDevice.getFallbackKey();
// if fallbackKeys is non-empty, we've already generated a
// fallback key, but it hasn't been published yet, so we
// can use that instead of generating a new one
if (!fallbackKeys.curve25519 ||
Object.keys(fallbackKeys.curve25519).length == 0) {
logger.info("generating fallback key");
if (this.fallbackCleanup) {
// cancel any pending fallback cleanup because generating
// a new fallback key will already drop the old fallback
// that would have been dropped, and we don't want to kill
// the current key
clearTimeout(this.fallbackCleanup);
delete this.fallbackCleanup;
}
await this.olmDevice.generateFallbackKey();
}
}
logger.info("calling uploadOneTimeKeys");
@@ -1898,8 +1921,9 @@ export class Crypto extends EventEmitter {
private async uploadOneTimeKeys() {
const promises = [];
const fallbackJson: Record<string, IOneTimeKey> = {};
let fallbackJson: Record<string, IOneTimeKey>;
if (this.getNeedsNewFallback()) {
fallbackJson = {};
const fallbackKeys = await this.olmDevice.getFallbackKey();
for (const [keyId, key] of Object.entries(fallbackKeys.curve25519)) {
const k = { key, fallback: true };
@@ -1910,7 +1934,7 @@ export class Crypto extends EventEmitter {
}
const oneTimeKeys = await this.olmDevice.getOneTimeKeys();
const oneTimeJson = {};
const oneTimeJson: Record<string, { key: string }> = {};
for (const keyId in oneTimeKeys.curve25519) {
if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) {
@@ -1924,10 +1948,23 @@ export class Crypto extends EventEmitter {
await Promise.all(promises);
const res = await this.baseApis.uploadKeysRequest({
const requestBody: Record<string, any> = {
"one_time_keys": oneTimeJson,
"org.matrix.msc2732.fallback_keys": fallbackJson,
});
};
if (fallbackJson) {
requestBody["org.matrix.msc2732.fallback_keys"] = fallbackJson;
requestBody["fallback_keys"] = fallbackJson;
}
const res = await this.baseApis.uploadKeysRequest(requestBody);
if (fallbackJson) {
this.fallbackCleanup = setTimeout(() => {
delete this.fallbackCleanup;
this.olmDevice.forgetOldFallbackKey();
}, 60*60*1000);
}
await this.olmDevice.markKeysAsPublished();
return res;
@@ -2046,7 +2083,7 @@ export class Crypto extends EventEmitter {
);
const device = await this.crossSigningInfo.signUser(xsk);
if (device) {
const upload = async ({ shouldEmit }) => {
const upload = async ({ shouldEmit = false }) => {
logger.info("Uploading signature for " + userId + "...");
const response = await this.baseApis.uploadKeySignatures({
[userId]: {
@@ -2119,7 +2156,7 @@ export class Crypto extends EventEmitter {
logger.info("Own device " + deviceId + " marked verified: signing");
// Signing only needed if other device not already signed
let device;
let device: ISignedKey;
const deviceTrust = this.checkDeviceTrust(userId, deviceId);
if (deviceTrust.isCrossSigningVerified()) {
logger.log(`Own device ${deviceId} already cross-signing verified`);
@@ -2130,7 +2167,7 @@ export class Crypto extends EventEmitter {
}
if (device) {
const upload = async ({ shouldEmit }) => {
const upload = async ({ shouldEmit = false }) => {
logger.info("Uploading signature for " + deviceId);
const response = await this.baseApis.uploadKeySignatures({
[userId]: {
@@ -2174,11 +2211,7 @@ export class Crypto extends EventEmitter {
return Promise.resolve(existingRequest);
}
const channel = new InRoomChannel(this.baseApis, roomId, userId);
return this.requestVerificationWithChannel(
userId,
channel,
this.inRoomVerificationRequests,
);
return this.requestVerificationWithChannel(userId, channel, this.inRoomVerificationRequests);
}
public requestVerification(userId: string, devices: string[]): Promise<VerificationRequest> {
@@ -2190,17 +2223,13 @@ export class Crypto extends EventEmitter {
return Promise.resolve(existingRequest);
}
const channel = new ToDeviceChannel(this.baseApis, userId, devices, ToDeviceChannel.makeTransactionId());
return this.requestVerificationWithChannel(
userId,
channel,
this.toDeviceVerificationRequests,
);
return this.requestVerificationWithChannel(userId, channel, this.toDeviceVerificationRequests);
}
private async requestVerificationWithChannel(
userId: string,
channel: IVerificationChannel,
requestsMap: any, // TODO types
requestsMap: IRequestsMap,
): Promise<VerificationRequest> {
let request = new VerificationRequest(channel, this.verificationMethods, this.baseApis);
// if transaction id is already known, add request
@@ -2588,7 +2617,7 @@ export class Crypto extends EventEmitter {
users: string[],
force?: boolean,
): Promise<Record<string, Record<string, olmlib.IOlmSessionResult>>> {
const devicesByUser = {};
const devicesByUser: Record<string, DeviceInfo[]> = {};
for (let i = 0; i < users.length; ++i) {
const userId = users[i];
@@ -2621,7 +2650,7 @@ export class Crypto extends EventEmitter {
* @return {module:crypto/OlmDevice.MegolmSessionData[]} a list of session export objects
*/
public async exportRoomKeys(): Promise<IMegolmSessionData[]> {
const exportedSessions = [];
const exportedSessions: IMegolmSessionData[] = [];
await this.cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
this.cryptoStore.getAllEndToEndInboundGroupSessions(txn, (s) => {
@@ -2699,7 +2728,6 @@ export class Crypto extends EventEmitter {
}
}
/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307
/**
* Encrypt an event according to the configuration of the room.
*
@@ -2710,8 +2738,6 @@ export class Crypto extends EventEmitter {
* @return {Promise?} Promise which resolves when the event has been
* encrypted, or null if nothing was needed
*/
/* eslint-enable valid-jsdoc */
// TODO this return type lies
public async encryptEvent(event: MatrixEvent, room: Room): Promise<void> {
if (!room) {
throw new Error("Cannot send encrypted messages in unknown rooms");
@@ -2753,8 +2779,7 @@ export class Crypto extends EventEmitter {
delete content['io.element.performance_metrics'];
}
const encryptedContent = await alg.encryptMessage(
room, event.getType(), content);
const encryptedContent = await alg.encryptMessage(room, event.getType(), content);
if (mRelatesTo) {
encryptedContent['m.relates_to'] = mRelatesTo;
@@ -2791,7 +2816,7 @@ export class Crypto extends EventEmitter {
type: "m.room.message",
content: {},
unsigned: {
redacted_because: decryptedEvent.clearEvent,
redacted_because: decryptedEvent.clearEvent as IEvent,
},
},
};
@@ -3124,7 +3149,7 @@ export class Crypto extends EventEmitter {
if (!ToDeviceChannel.validateEvent(event, this.baseApis)) {
return;
}
const createRequest = event => {
const createRequest = (event: MatrixEvent) => {
if (!ToDeviceChannel.canCreateRequest(ToDeviceChannel.getEventType(event))) {
return;
}
@@ -3142,11 +3167,7 @@ export class Crypto extends EventEmitter {
return new VerificationRequest(
channel, this.verificationMethods, this.baseApis);
};
this.handleVerificationEvent(
event,
this.toDeviceVerificationRequests,
createRequest,
);
this.handleVerificationEvent(event, this.toDeviceVerificationRequests, createRequest);
}
/**
@@ -3169,7 +3190,7 @@ export class Crypto extends EventEmitter {
if (!InRoomChannel.validateEvent(event, this.baseApis)) {
return;
}
const createRequest = event => {
const createRequest = (event: MatrixEvent) => {
const channel = new InRoomChannel(
this.baseApis,
event.getRoomId(),
@@ -3177,18 +3198,13 @@ export class Crypto extends EventEmitter {
return new VerificationRequest(
channel, this.verificationMethods, this.baseApis);
};
this.handleVerificationEvent(
event,
this.inRoomVerificationRequests,
createRequest,
liveEvent,
);
this.handleVerificationEvent(event, this.inRoomVerificationRequests, createRequest, liveEvent);
};
private async handleVerificationEvent(
event: MatrixEvent,
requestsMap: any, // TODO types
createRequest: any, // TODO types
requestsMap: IRequestsMap,
createRequest: (event: MatrixEvent) => VerificationRequest,
isLiveEvent = true,
): Promise<void> {
// Wait for event to get its final ID with pendingEventOrdering: "chronological", since DM channels depend on it.
@@ -3302,7 +3318,7 @@ export class Crypto extends EventEmitter {
return;
}
}
const devicesByUser = {};
const devicesByUser: Record<string, DeviceInfo[]> = {};
devicesByUser[sender] = [device];
await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser, true);
+1 -1
View File
@@ -67,5 +67,5 @@ export interface IKeyBackupRestoreResult {
export interface IKeyBackupRestoreOpts {
cacheCompleteCallback?: () => void;
progressCallback?: ({ stage: string }) => void;
progressCallback?: (progress: { stage: string }) => void;
}
+20 -17
View File
@@ -21,15 +21,15 @@ limitations under the License.
*/
import anotherjson from "another-json";
import type { PkSigning } from "@matrix-org/olm";
import { Logger } from "loglevel";
import type { PkSigning } from "@matrix-org/olm";
import { OlmDevice } from "./OlmDevice";
import { DeviceInfo } from "./deviceinfo";
import { logger } from '../logger';
import * as utils from "../utils";
import { IOneTimeKey } from "./dehydration";
import { MatrixClient } from "../client";
import { IClaimOTKsResult, MatrixClient } from "../client";
import { ISignatures } from "../@types/signed";
enum Algorithm {
Olm = "m.olm.v1.curve25519-aes-sha2",
@@ -126,13 +126,18 @@ export async function encryptMessageForDevice(
// involved in the session. If we're looking to reduce data transfer in the
// future, we could elide them for subsequent messages.
utils.extend(payload, payloadFields);
Object.assign(payload, payloadFields);
resultsObject[deviceKey] = await olmDevice.encryptMessage(
deviceKey, sessionId, JSON.stringify(payload),
);
}
interface IExistingOlmSession {
device: DeviceInfo;
sessionId?: string;
}
/**
* Get the existing olm sessions for the given devices, and the devices that
* don't have olm sessions.
@@ -153,11 +158,11 @@ export async function getExistingOlmSessions(
olmDevice: OlmDevice,
baseApis: MatrixClient,
devicesByUser: Record<string, DeviceInfo[]>,
) {
const devicesWithoutSession = {};
const sessions = {};
): Promise<[Record<string, DeviceInfo[]>, Record<string, Record<string, IExistingOlmSession>>]> {
const devicesWithoutSession: {[userId: string]: DeviceInfo[]} = {};
const sessions: {[userId: string]: {[deviceId: string]: IExistingOlmSession}} = {};
const promises = [];
const promises: Promise<void>[] = [];
for (const [userId, devices] of Object.entries(devicesByUser)) {
for (const deviceInfo of devices) {
@@ -231,10 +236,10 @@ export async function ensureOlmSessionsForDevices(
force = false;
}
const devicesWithoutSession = [
const devicesWithoutSession: [string, string][] = [
// [userId, deviceId], ...
];
const result = {};
const result: {[userId: string]: {[deviceId: string]: IExistingOlmSession}} = {};
const resolveSession: Record<string, (sessionId?: string) => void> = {};
// Mark all sessions this task intends to update as in progress. It is
@@ -322,9 +327,7 @@ export async function ensureOlmSessionsForDevices(
let taskDetail = `one-time keys for ${devicesWithoutSession.length} devices`;
try {
log.debug(`Claiming ${taskDetail}`);
res = await baseApis.claimOneTimeKeys(
devicesWithoutSession, oneTimeKeyAlgorithm, otkTimeout,
);
res = await baseApis.claimOneTimeKeys(devicesWithoutSession, oneTimeKeyAlgorithm, otkTimeout);
log.debug(`Claimed ${taskDetail}`);
} catch (e) {
for (const resolver of Object.values(resolveSession)) {
@@ -338,8 +341,8 @@ export async function ensureOlmSessionsForDevices(
failedServers.push(...Object.keys(res.failures));
}
const otkResult = res.one_time_keys || {};
const promises = [];
const otkResult = res.one_time_keys || {} as IClaimOTKsResult["one_time_keys"];
const promises: Promise<void>[] = [];
for (const [userId, devices] of Object.entries(devicesByUser)) {
const userRes = otkResult[userId] || {};
for (let j = 0; j < devices.length; j++) {
@@ -360,7 +363,7 @@ export async function ensureOlmSessionsForDevices(
}
const deviceRes = userRes[deviceId] || {};
let oneTimeKey = null;
let oneTimeKey: IOneTimeKey = null;
for (const keyId in deviceRes) {
if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) {
oneTimeKey = deviceRes[keyId];
@@ -442,7 +445,7 @@ async function _verifyKeyAndStartSession(
export interface IObject {
unsigned?: object;
signatures?: object;
signatures?: ISignatures;
}
/**
@@ -200,10 +200,10 @@ export class Backend implements CryptoStore {
// index into the wantedStates array
let stateIndex = 0;
let result;
let result: OutgoingRoomKeyRequest;
function onsuccess(ev) {
const cursor = ev.target.result;
function onsuccess(this: IDBRequest<IDBCursorWithValue>) {
const cursor = this.result;
if (cursor) {
// got a match
result = cursor.value;
@@ -218,7 +218,7 @@ export class Backend implements CryptoStore {
}
const wantedState = wantedStates[stateIndex];
const cursorReq = ev.target.source.openCursor(wantedState);
const cursorReq = (this.source as IDBIndex).openCursor(wantedState);
cursorReq.onsuccess = onsuccess;
}
@@ -255,10 +255,10 @@ export class Backend implements CryptoStore {
wantedStates: number[],
): Promise<OutgoingRoomKeyRequest[]> {
let stateIndex = 0;
const results = [];
const results: OutgoingRoomKeyRequest[] = [];
function onsuccess(ev) {
const cursor = ev.target.result;
function onsuccess(this: IDBRequest<IDBCursorWithValue>) {
const cursor = this.result;
if (cursor) {
const keyReq = cursor.value;
if (keyReq.recipients.includes({ userId, deviceId })) {
@@ -274,7 +274,7 @@ export class Backend implements CryptoStore {
}
const wantedState = wantedStates[stateIndex];
const cursorReq = ev.target.source.openCursor(wantedState);
const cursorReq = (this.source as IDBIndex).openCursor(wantedState);
cursorReq.onsuccess = onsuccess;
}
}
@@ -306,10 +306,10 @@ export class Backend implements CryptoStore {
expectedState: number,
updates: Partial<OutgoingRoomKeyRequest>,
): Promise<OutgoingRoomKeyRequest | null> {
let result = null;
let result: OutgoingRoomKeyRequest = null;
function onsuccess(ev) {
const cursor = ev.target.result;
function onsuccess(this: IDBRequest<IDBCursorWithValue>) {
const cursor = this.result;
if (!cursor) {
return;
}
@@ -444,7 +444,7 @@ export class Backend implements CryptoStore {
const objectStore = txn.objectStore("sessions");
const idx = objectStore.index("deviceKey");
const getReq = idx.openCursor(deviceKey);
const results = {};
const results: Parameters<Parameters<Backend["getEndToEndSessions"]>[2]>[0] = {};
getReq.onsuccess = function() {
const cursor = getReq.result;
if (cursor) {
@@ -734,7 +734,7 @@ export class Backend implements CryptoStore {
}
public getEndToEndRooms(txn: IDBTransaction, func: (rooms: Record<string, IRoomEncryption>) => void): void {
const rooms = {};
const rooms: Parameters<Parameters<Backend["getEndToEndRooms"]>[1]>[0] = {};
const objectStore = txn.objectStore("rooms");
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
@@ -756,7 +756,7 @@ export class Backend implements CryptoStore {
public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
return new Promise((resolve, reject) => {
const sessions = [];
const sessions: ISession[] = [];
const txn = this.db.transaction(
["sessions_needing_backup", "inbound_group_sessions"],
@@ -877,8 +877,8 @@ export class Backend implements CryptoStore {
func: (txn: IDBTransaction) => T,
log: PrefixedLogger = logger,
): Promise<T> {
let startTime;
let description;
let startTime: number;
let description: string;
if (PROFILE_TRANSACTIONS) {
const txnId = this.nextTxnId++;
startTime = Date.now();
@@ -175,8 +175,10 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
}
public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
const notifiedErrorDevices = getJsonItem<string[]>(this.store, KEY_NOTIFIED_ERROR_DEVICES) || {};
const ret = [];
const notifiedErrorDevices = getJsonItem<MemoryCryptoStore["notifiedErrorDevices"]>(
this.store, KEY_NOTIFIED_ERROR_DEVICES,
) || {};
const ret: IOlmDevice[] = [];
for (const device of devices) {
const { userId, deviceInfo } = device;
@@ -291,7 +293,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
}
public getEndToEndRooms(txn: unknown, func: (rooms: Record<string, IRoomEncryption>) => void): void {
const result = {};
const result: Record<string, IRoomEncryption> = {};
const prefix = keyEndToEndRoomsPrefix('');
for (let i = 0; i < this.store.length; ++i) {
@@ -306,7 +308,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
const sessionsNeedingBackup = getJsonItem<string[]>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
const sessions = [];
const sessions: ISession[] = [];
for (const session in sessionsNeedingBackup) {
if (Object.prototype.hasOwnProperty.call(sessionsNeedingBackup, session)) {
+2 -1
View File
@@ -20,8 +20,9 @@ limitations under the License.
* @module crypto/verification/Base
*/
import { MatrixEvent } from '../../models/event';
import { EventEmitter } from 'events';
import { MatrixEvent } from '../../models/event';
import { logger } from '../../logger';
import { DeviceInfo } from '../deviceinfo';
import { newTimeoutError } from "./Error";
+3 -2
View File
@@ -19,8 +19,10 @@ limitations under the License.
* @module crypto/verification/SAS
*/
import { VerificationBase as Base, SwitchStartEventError } from "./Base";
import anotherjson from 'another-json';
import { Utility, SAS as OlmSAS } from "@matrix-org/olm";
import { VerificationBase as Base, SwitchStartEventError } from "./Base";
import {
errorFactory,
newInvalidMessageError,
@@ -29,7 +31,6 @@ import {
newUserCancelledError,
} from './Error';
import { logger } from '../../logger';
import { Utility, SAS as OlmSAS } from "@matrix-org/olm";
import { IContent, MatrixEvent } from "../../models/event";
const START_TYPE = "m.key.verification.start";
@@ -30,4 +30,5 @@ export interface IVerificationChannel {
sendCompleted(type: string, content: Record<string, any>): Promise<void>;
completedContentFromEvent(event: MatrixEvent): Record<string, any>;
canCreateRequest(type: string): boolean;
handleEvent(event: MatrixEvent, request: VerificationRequest, isLiveEvent: boolean): Promise<void>;
}
@@ -26,6 +26,7 @@ import { IVerificationChannel } from "./Channel";
import { EventType } from "../../../@types/event";
import { MatrixClient } from "../../../client";
import { MatrixEvent } from "../../../models/event";
import { IRequestsMap } from "../..";
const MESSAGE_TYPE = EventType.RoomMessage;
const M_REFERENCE = "m.reference";
@@ -36,7 +37,7 @@ const M_RELATES_TO = "m.relates_to";
* Uses the event id of the initial m.key.verification.request event as a transaction id.
*/
export class InRoomChannel implements IVerificationChannel {
private requestEventId = null;
private requestEventId: string = null;
/**
* @param {MatrixClient} client the matrix client, to send messages with and get current user & device from.
@@ -304,7 +305,7 @@ export class InRoomChannel implements IVerificationChannel {
}
}
export class InRoomRequests {
export class InRoomRequests implements IRequestsMap {
private requestsByRoomId = new Map<string, Map<string, VerificationRequest>>();
public getRequest(event: MatrixEvent): VerificationRequest {
@@ -328,7 +329,7 @@ export class InRoomRequests {
this.doSetRequest(event.getRoomId(), InRoomChannel.getTransactionId(event), request);
}
public setRequestByChannel(channel: InRoomChannel, request: VerificationRequest): void {
public setRequestByChannel(channel: IVerificationChannel, request: VerificationRequest): void {
this.doSetRequest(channel.roomId, channel.transactionId, request);
}
@@ -30,8 +30,9 @@ import { errorFromEvent, newUnexpectedMessageError } from "../Error";
import { MatrixEvent } from "../../../models/event";
import { IVerificationChannel } from "./Channel";
import { MatrixClient } from "../../../client";
import { IRequestsMap } from '../..';
type Request = VerificationRequest<ToDeviceChannel>;
export type Request = VerificationRequest<ToDeviceChannel>;
/**
* A key verification channel that sends verification events over to_device messages.
@@ -276,7 +277,7 @@ export class ToDeviceChannel implements IVerificationChannel {
private async sendToDevices(type: string, content: Record<string, any>, devices: string[]): Promise<void> {
if (devices.length) {
const msgMap = {};
const msgMap: Record<string, Record<string, any>> = {};
for (const deviceId of devices) {
msgMap[deviceId] = content;
}
@@ -294,7 +295,7 @@ export class ToDeviceChannel implements IVerificationChannel {
}
}
export class ToDeviceRequests {
export class ToDeviceRequests implements IRequestsMap {
private requestsByUserId = new Map<string, Map<string, Request>>();
public getRequest(event: MatrixEvent): Request {
@@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { logger } from '../../../logger';
import { EventEmitter } from 'events';
import { logger } from '../../../logger';
import {
errorFactory,
errorFromEvent,
@@ -581,7 +582,9 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
// get common methods
if (phase === PHASE_REQUESTED || phase === PHASE_READY) {
if (!this.wasSentByOwnDevice(event)) {
const content = event.getContent();
const content = event.getContent<{
methods: string[];
}>();
this.commonMethods =
content.methods.filter(m => this.verificationMethods.has(m));
}
+1 -1
View File
@@ -41,7 +41,7 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
}
}
if (!preventReEmit) {
client.reEmitter.reEmit(event, ["Event.replaced"]);
client.reEmitter.reEmit(event, ["Event.replaced", "Event.visibilityChange"]);
}
return event;
}
+3
View File
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { UNSTABLE_FILTER_RELATION_SENDERS, UNSTABLE_FILTER_RELATION_TYPES } from "./filter";
import { MatrixEvent } from "./models/event";
/**
@@ -89,6 +90,8 @@ export class FilterComponent {
senders: this.filterJson.senders || null,
not_senders: this.filterJson.not_senders || [],
contains_url: this.filterJson.contains_url || null,
[UNSTABLE_FILTER_RELATION_SENDERS.name]: UNSTABLE_FILTER_RELATION_SENDERS.findIn(this.filterJson),
[UNSTABLE_FILTER_RELATION_TYPES.name]: UNSTABLE_FILTER_RELATION_TYPES.findIn(this.filterJson),
};
}
+18
View File
@@ -18,8 +18,23 @@ limitations under the License.
* @module filter
*/
import {
EventType,
RelationType,
} from "./@types/event";
import { FilterComponent, IFilterComponent } from "./filter-component";
import { MatrixEvent } from "./models/event";
import { UnstableValue } from "./NamespacedValue";
export const UNSTABLE_FILTER_RELATION_SENDERS = new UnstableValue(
"relation_senders",
"io.element.relation_senders",
);
export const UNSTABLE_FILTER_RELATION_TYPES = new UnstableValue(
"relation_types",
"io.element.relation_types",
);
/**
* @param {Object} obj
@@ -50,6 +65,9 @@ export interface IFilterDefinition {
export interface IRoomEventFilter extends IFilterComponent {
lazy_load_members?: boolean;
include_redundant_members?: boolean;
types?: Array<EventType | string>;
[UNSTABLE_FILTER_RELATION_TYPES.name]?: Array<RelationType | string>;
[UNSTABLE_FILTER_RELATION_SENDERS.name]?: string[];
}
interface IStateFilter extends IRoomEventFilter {}
+329 -203
View File
@@ -1,6 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -20,14 +20,21 @@ limitations under the License.
* @module http-api
*/
import { parse as parseContentType } from "content-type";
import * as utils from "./utils";
import { logger } from './logger';
import { parse as parseContentType, ParsedMediaType } from "content-type";
import EventEmitter from "events";
import type { IncomingHttpHeaders, IncomingMessage } from "http";
import type { Request as _Request, CoreOptions } from "request";
// we use our own implementation of setTimeout, so that if we get suspended in
// the middle of a /sync, we cancel the sync as soon as we awake, rather than
// waiting for the delay to elapse.
import * as callbacks from "./realtime-callbacks";
import { IUploadOpts } from "./@types/requests";
import { IAbortablePromise, IUsageLimit } from "./@types/partials";
import { IDeferred } from "./utils";
import { Callback } from "./client";
import * as utils from "./utils";
import { logger } from './logger';
/*
TODO:
@@ -40,6 +47,11 @@ TODO:
*/
export const PREFIX_R0 = "/_matrix/client/r0";
/**
* A constant representing the URI path for release v1 of the Client-Server HTTP API.
*/
export const PREFIX_V1 = "/_matrix/client/v1";
/**
* A constant representing the URI path for as-yet unspecified Client-Server HTTP APIs.
*/
@@ -61,10 +73,95 @@ export const PREFIX_IDENTITY_V2 = "/_matrix/identity/v2";
*/
export const PREFIX_MEDIA_R0 = "/_matrix/media/r0";
type RequestProps = "method"
| "withCredentials"
| "json"
| "headers"
| "qs"
| "body"
| "qsStringifyOptions"
| "useQuerystring"
| "timeout";
export interface IHttpOpts {
baseUrl: string;
idBaseUrl?: string;
prefix: string;
onlyData: boolean;
accessToken?: string;
extraParams?: Record<string, string>;
localTimeoutMs?: number;
useAuthorizationHeader?: boolean;
request(opts: Pick<CoreOptions, RequestProps> & {
uri: string;
method: Method;
// eslint-disable-next-line camelcase
_matrix_opts: IHttpOpts;
}, callback: RequestCallback): IRequest;
}
interface IRequest extends _Request {
onprogress?(e: unknown): void;
}
interface IRequestOpts<T> {
prefix?: string;
localTimeoutMs?: number;
headers?: Record<string, string>;
json?: boolean; // defaults to true
qsStringifyOptions?: CoreOptions["qsStringifyOptions"];
bodyParser?(body: string): T;
}
export interface IUpload {
loaded: number;
total: number;
promise: IAbortablePromise<unknown>;
}
interface IContentUri {
base: string;
path: string;
params: {
// eslint-disable-next-line camelcase
access_token: string;
};
}
type ResponseType<T, O extends IRequestOpts<T> | void = void> =
O extends { bodyParser: (body: string) => T } ? T :
O extends { json: false } ? string :
T;
interface IUploadResponse {
// eslint-disable-next-line camelcase
content_uri: string;
}
// This type's defaults only work for the Browser
// in the Browser we default rawResponse = false & onlyContentUri = true
// in Node we default rawResponse = true & onlyContentUri = false
export type UploadContentResponseType<O extends IUploadOpts> =
O extends undefined ? string :
O extends { rawResponse: true } ? string :
O extends { onlyContentUri: true } ? string :
O extends { rawResponse: false } ? IUploadResponse :
O extends { onlyContentUri: false } ? IUploadResponse :
string;
export enum Method {
Get = "GET",
Put = "PUT",
Post = "POST",
Delete = "DELETE",
}
export type FileType = Document | XMLHttpRequestBodyInit;
/**
* Construct a MatrixHttpApi.
* @constructor
* @param {EventEmitter} event_emitter The event emitter to use for emitting events
* @param {EventEmitter} eventEmitter The event emitter to use for emitting events
* @param {Object} opts The options to use for this HTTP API.
* @param {string} opts.baseUrl Required. The base client-server URL e.g.
* 'http://localhost:8008'.
@@ -77,7 +174,7 @@ export const PREFIX_MEDIA_R0 = "/_matrix/media/r0";
* response (e.g. the parsed HTTP body). If false, requests will return an
* object with the properties <tt>code</tt>, <tt>headers</tt> and <tt>data</tt>.
*
* @param {string} opts.accessToken The access_token to send with requests. Can be
* @param {string=} opts.accessToken The access_token to send with requests. Can be
* null to not send an access token.
* @param {Object=} opts.extraParams Optional. Extra query parameters to send on
* requests.
@@ -86,39 +183,37 @@ export const PREFIX_MEDIA_R0 = "/_matrix/media/r0";
* @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use
* Authorization header instead of query param to send the access token to the server.
*/
export function MatrixHttpApi(event_emitter, opts) {
utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]);
opts.onlyData = opts.onlyData || false;
this.event_emitter = event_emitter;
this.opts = opts;
this.useAuthorizationHeader = Boolean(opts.useAuthorizationHeader);
this.uploads = [];
}
export class MatrixHttpApi {
private uploads: IUpload[] = [];
constructor(private eventEmitter: EventEmitter, public readonly opts: IHttpOpts) {
utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]);
opts.onlyData = !!opts.onlyData;
opts.useAuthorizationHeader = !!opts.useAuthorizationHeader;
}
MatrixHttpApi.prototype = {
/**
* Sets the baase URL for the identity server
* Sets the base URL for the identity server
* @param {string} url The new base url
*/
setIdBaseUrl: function(url) {
public setIdBaseUrl(url: string): void {
this.opts.idBaseUrl = url;
},
}
/**
* Get the content repository url with query parameters.
* @return {Object} An object with a 'base', 'path' and 'params' for base URL,
* path and query parameters respectively.
*/
getContentUri: function() {
const params = {
access_token: this.opts.accessToken,
};
public getContentUri(): IContentUri {
return {
base: this.opts.baseUrl,
path: "/_matrix/media/r0/upload",
params: params,
params: {
access_token: this.opts.accessToken,
},
};
},
}
/**
* Upload content to the homeserver
@@ -160,14 +255,17 @@ MatrixHttpApi.prototype = {
* determined by this.opts.onlyData, opts.rawResponse, and
* opts.onlyContentUri. Rejects with an error (usually a MatrixError).
*/
uploadContent: function(file, opts) {
public uploadContent<O extends IUploadOpts>(
file: FileType,
opts?: O,
): IAbortablePromise<UploadContentResponseType<O>> {
if (utils.isFunction(opts)) {
// opts used to be callback
// opts used to be callback, backwards compatibility
opts = {
callback: opts,
};
} else if (opts === undefined) {
opts = {};
callback: opts as unknown as IUploadOpts["callback"],
} as O;
} else if (!opts) {
opts = {} as O;
}
// default opts.includeFilename to true (ignoring falsey values)
@@ -175,8 +273,8 @@ MatrixHttpApi.prototype = {
// if the file doesn't have a mime type, use a default since
// the HS errors if we don't supply one.
const contentType = opts.type || file.type || 'application/octet-stream';
const fileName = opts.name || file.name;
const contentType = opts.type || (file as File).type || 'application/octet-stream';
const fileName = opts.name || (file as File).name;
// We used to recommend setting file.stream to the thing to upload on
// Node.js. As of 2019-06-11, this is still in widespread use in various
@@ -185,13 +283,14 @@ MatrixHttpApi.prototype = {
// the browser now define a `stream` method, which leads to trouble
// here, so we also check the type of `stream`.
let body = file;
if (body.stream && typeof body.stream !== "function") {
const bodyStream = (body as File | Blob).stream; // this type is wrong but for legacy reasons is good enough
if (bodyStream && typeof bodyStream !== "function") {
logger.warn(
"Using `file.stream` as the content to upload. Future " +
"versions of the js-sdk will change this to expect `file` to " +
"be the content directly.",
);
body = body.stream;
body = bodyStream;
}
// backwards-compatibility hacks where we used to do different things
@@ -234,8 +333,8 @@ MatrixHttpApi.prototype = {
// (browser-request doesn't support progress either, which is also kind
// of important here)
const upload = { loaded: 0, total: 0 };
let promise;
const upload = { loaded: 0, total: 0 } as IUpload;
let promise: IAbortablePromise<UploadContentResponseType<O>>;
// XMLHttpRequest doesn't parse JSON for us. request normally does, but
// we're setting opts.json=false so that it doesn't JSON-encode the
@@ -243,7 +342,7 @@ MatrixHttpApi.prototype = {
// way, we have to JSON-parse the response ourselves.
let bodyParser = null;
if (!rawResponse) {
bodyParser = function(rawBody) {
bodyParser = function(rawBody: string) {
let body = JSON.parse(rawBody);
if (onlyContentUri) {
body = body.content_uri;
@@ -256,25 +355,23 @@ MatrixHttpApi.prototype = {
}
if (global.XMLHttpRequest) {
const defer = utils.defer();
const defer = utils.defer<UploadContentResponseType<O>>();
const xhr = new global.XMLHttpRequest();
upload.xhr = xhr;
const cb = requestCallback(defer, opts.callback, this.opts.onlyData);
const timeout_fn = function() {
const timeoutFn = function() {
xhr.abort();
cb(new Error('Timeout'));
};
// set an initial timeout of 30s; we'll advance it each time we get
// a progress notification
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
// set an initial timeout of 30s; we'll advance it each time we get a progress notification
let timeoutTimer = callbacks.setTimeout(timeoutFn, 30000);
xhr.onreadystatechange = function() {
let resp;
let resp: string;
switch (xhr.readyState) {
case global.XMLHttpRequest.DONE:
callbacks.clearTimeout(xhr.timeout_timer);
callbacks.clearTimeout(timeoutTimer);
try {
if (xhr.status === 0) {
throw new AbortError();
@@ -296,10 +393,10 @@ MatrixHttpApi.prototype = {
}
};
xhr.upload.addEventListener("progress", function(ev) {
callbacks.clearTimeout(xhr.timeout_timer);
callbacks.clearTimeout(timeoutTimer);
upload.loaded = ev.loaded;
upload.total = ev.total;
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
timeoutTimer = callbacks.setTimeout(timeoutFn, 30000);
if (opts.progressHandler) {
opts.progressHandler({
loaded: ev.loaded,
@@ -315,9 +412,8 @@ MatrixHttpApi.prototype = {
queryArgs.push("filename=" + encodeURIComponent(fileName));
}
if (!this.useAuthorizationHeader) {
queryArgs.push("access_token="
+ encodeURIComponent(this.opts.accessToken));
if (!this.opts.useAuthorizationHeader) {
queryArgs.push("access_token=" + encodeURIComponent(this.opts.accessToken));
}
if (queryArgs.length > 0) {
@@ -325,73 +421,69 @@ MatrixHttpApi.prototype = {
}
xhr.open("POST", url);
if (this.useAuthorizationHeader) {
if (this.opts.useAuthorizationHeader) {
xhr.setRequestHeader("Authorization", "Bearer " + this.opts.accessToken);
}
xhr.setRequestHeader("Content-Type", contentType);
xhr.send(body);
promise = defer.promise;
promise = defer.promise as IAbortablePromise<UploadContentResponseType<O>>;
// dirty hack (as per _request) to allow the upload to be cancelled.
// dirty hack (as per doRequest) to allow the upload to be cancelled.
promise.abort = xhr.abort.bind(xhr);
} else {
const queryParams = {};
const queryParams: Record<string, string> = {};
if (includeFilename && fileName) {
queryParams.filename = fileName;
}
promise = this.authedRequest(
opts.callback, "POST", "/upload", queryParams, body, {
opts.callback, Method.Post, "/upload", queryParams, body, {
prefix: "/_matrix/media/r0",
headers: { "Content-Type": contentType },
json: false,
bodyParser: bodyParser,
bodyParser,
},
);
}
const self = this;
// remove the upload from the list on completion
const promise0 = promise.finally(function() {
for (let i = 0; i < self.uploads.length; ++i) {
if (self.uploads[i] === upload) {
self.uploads.splice(i, 1);
upload.promise = promise.finally(() => {
for (let i = 0; i < this.uploads.length; ++i) {
if (this.uploads[i] === upload) {
this.uploads.splice(i, 1);
return;
}
}
});
}) as IAbortablePromise<UploadContentResponseType<O>>;
// copy our dirty abort() method to the new promise
promise0.abort = promise.abort;
upload.promise = promise0;
upload.promise.abort = promise.abort;
this.uploads.push(upload);
return promise0;
},
return upload.promise as IAbortablePromise<UploadContentResponseType<O>>;
}
cancelUpload: function(promise) {
public cancelUpload(promise: IAbortablePromise<unknown>): boolean {
if (promise.abort) {
promise.abort();
return true;
}
return false;
},
}
getCurrentUploads: function() {
public getCurrentUploads(): IUpload[] {
return this.uploads;
},
}
idServerRequest: function(
callback,
method,
path,
params,
prefix,
accessToken,
) {
public idServerRequest<T>(
callback: Callback<T>,
method: Method,
path: string,
params: Record<string, string | string[]>,
prefix: string,
accessToken: string,
): Promise<T> {
if (!this.opts.idBaseUrl) {
throw new Error("No identity server base URL set");
}
@@ -406,28 +498,27 @@ MatrixHttpApi.prototype = {
const opts = {
uri: fullUri,
method: method,
method,
withCredentials: false,
json: true, // we want a JSON response if we can
_matrix_opts: this.opts,
headers: {},
};
if (method === 'GET') {
} as Parameters<IHttpOpts["request"]>[0];
if (method === Method.Get) {
opts.qs = params;
} else if (typeof params === "object") {
opts.json = params;
}
if (accessToken) {
opts.headers['Authorization'] = `Bearer ${accessToken}`;
}
const defer = utils.defer();
this.opts.request(
opts,
requestCallback(defer, callback, this.opts.onlyData),
);
const defer = utils.defer<T>();
this.opts.request(opts, requestCallback(defer, callback, this.opts.onlyData));
return defer.promise;
},
}
/**
* Perform an authorised request to the homeserver.
@@ -448,7 +539,7 @@ MatrixHttpApi.prototype = {
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
*
* @param {sting=} opts.prefix The full prefix to use e.g.
* @param {string=} opts.prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
*
* @param {Object=} opts.headers map of additional request headers
@@ -460,45 +551,45 @@ MatrixHttpApi.prototype = {
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
authedRequest: function(callback, method, path, queryParams, data, opts) {
if (!queryParams) {
queryParams = {};
}
if (this.useAuthorizationHeader) {
if (isFinite(opts)) {
public authedRequest<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
callback: Callback<T>,
method: Method,
path: string,
queryParams?: Record<string, string | string[]>,
data?: CoreOptions["body"],
opts?: O | number, // number is legacy
): IAbortablePromise<ResponseType<T, O>> {
if (!queryParams) queryParams = {};
let requestOpts = (opts || {}) as O;
if (this.opts.useAuthorizationHeader) {
if (isFinite(opts as number)) {
// opts used to be localTimeoutMs
opts = {
localTimeoutMs: opts,
};
requestOpts = {
localTimeoutMs: opts as number,
} as O;
}
if (!opts) {
opts = {};
if (!requestOpts.headers) {
requestOpts.headers = {};
}
if (!opts.headers) {
opts.headers = {};
}
if (!opts.headers.Authorization) {
opts.headers.Authorization = "Bearer " + this.opts.accessToken;
if (!requestOpts.headers.Authorization) {
requestOpts.headers.Authorization = "Bearer " + this.opts.accessToken;
}
if (queryParams.access_token) {
delete queryParams.access_token;
}
} else {
if (!queryParams.access_token) {
queryParams.access_token = this.opts.accessToken;
}
} else if (!queryParams.access_token) {
queryParams.access_token = this.opts.accessToken;
}
const requestPromise = this.request(
callback, method, path, queryParams, data, opts,
);
const requestPromise = this.request<T, O>(callback, method, path, queryParams, data, requestOpts);
const self = this;
requestPromise.catch(function(err) {
requestPromise.catch((err: MatrixError) => {
if (err.errcode == 'M_UNKNOWN_TOKEN') {
self.event_emitter.emit("Session.logged_out", err);
this.eventEmitter.emit("Session.logged_out", err);
} else if (err.errcode == 'M_CONSENT_NOT_GIVEN') {
self.event_emitter.emit(
this.eventEmitter.emit(
"no_consent",
err.message,
err.data.consent_uri,
@@ -509,7 +600,7 @@ MatrixHttpApi.prototype = {
// return the original promise, otherwise tests break due to it having to
// go around the event loop one more time to process the result of the request
return requestPromise;
},
}
/**
* Perform a request to the homeserver without any credentials.
@@ -529,7 +620,7 @@ MatrixHttpApi.prototype = {
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
*
* @param {sting=} opts.prefix The full prefix to use e.g.
* @param {string=} opts.prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
*
* @param {Object=} opts.headers map of additional request headers
@@ -541,15 +632,19 @@ MatrixHttpApi.prototype = {
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
request: function(callback, method, path, queryParams, data, opts) {
opts = opts || {};
const prefix = opts.prefix !== undefined ? opts.prefix : this.opts.prefix;
public request<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
callback: Callback<T>,
method: Method,
path: string,
queryParams?: CoreOptions["qs"],
data?: CoreOptions["body"],
opts?: O,
): IAbortablePromise<ResponseType<T, O>> {
const prefix = opts?.prefix ?? this.opts.prefix;
const fullUri = this.opts.baseUrl + prefix + path;
return this.requestOtherUrl(
callback, method, fullUri, queryParams, data, opts,
);
},
return this.requestOtherUrl<T, O>(callback, method, fullUri, queryParams, data, opts);
}
/**
* Perform a request to an arbitrary URL.
@@ -568,7 +663,7 @@ MatrixHttpApi.prototype = {
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
*
* @param {sting=} opts.prefix The full prefix to use e.g.
* @param {string=} opts.prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
*
* @param {Object=} opts.headers map of additional request headers
@@ -580,21 +675,24 @@ MatrixHttpApi.prototype = {
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
requestOtherUrl: function(callback, method, uri, queryParams, data,
opts) {
if (opts === undefined || opts === null) {
opts = {};
} else if (isFinite(opts)) {
public requestOtherUrl<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
callback: Callback<T>,
method: Method,
uri: string,
queryParams?: CoreOptions["qs"],
data?: CoreOptions["body"],
opts?: O | number, // number is legacy
): IAbortablePromise<ResponseType<T, O>> {
let requestOpts = (opts || {}) as O;
if (isFinite(opts as number)) {
// opts used to be localTimeoutMs
opts = {
localTimeoutMs: opts,
};
requestOpts = {
localTimeoutMs: opts as number,
} as O;
}
return this._request(
callback, method, uri, queryParams, data, opts,
);
},
return this.doRequest<T, O>(callback, method, uri, queryParams, data, requestOpts);
}
/**
* Form and return a homeserver request URL based on the given path
@@ -607,13 +705,13 @@ MatrixHttpApi.prototype = {
* "/_matrix/client/v2_alpha".
* @return {string} URL
*/
getUrl: function(path, queryParams, prefix) {
public getUrl(path: string, queryParams: CoreOptions["qs"], prefix: string): string {
let queryString = "";
if (queryParams) {
queryString = "?" + utils.encodeParams(queryParams);
}
return this.opts.baseUrl + prefix + path + queryString;
},
}
/**
* @private
@@ -640,25 +738,32 @@ MatrixHttpApi.prototype = {
* @return {Promise} a promise which resolves to either the
* response object (if this.opts.onlyData is truthy), or the parsed
* body. Rejects
*
* Generic T is the callback/promise resolve type
* Generic O should be inferred
*/
_request: function(callback, method, uri, queryParams, data, opts) {
private doRequest<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
callback: Callback<T>,
method: Method,
uri: string,
queryParams?: Record<string, string>,
data?: CoreOptions["body"],
opts?: O,
): IAbortablePromise<ResponseType<T, O>> {
if (callback !== undefined && !utils.isFunction(callback)) {
throw Error(
"Expected callback to be a function but got " + typeof callback,
);
throw Error("Expected callback to be a function but got " + typeof callback);
}
opts = opts || {};
const self = this;
if (this.opts.extraParams) {
queryParams = {
...queryParams,
...this.opts.extraParams,
...(queryParams || {}),
...this.opts.extraParams,
};
}
const headers = utils.extend({}, opts.headers || {});
const json = opts.json === undefined ? true : opts.json;
const headers = Object.assign({}, opts.headers || {});
if (!opts) opts = {} as O;
const json = opts.json ?? true;
let bodyParser = opts.bodyParser;
// we handle the json encoding/decoding here, because request and
@@ -677,17 +782,17 @@ MatrixHttpApi.prototype = {
}
if (bodyParser === undefined) {
bodyParser = function(rawBody) {
bodyParser = function(rawBody: string) {
return JSON.parse(rawBody);
};
}
}
const defer = utils.defer();
const defer = utils.defer<T>();
let timeoutId;
let timeoutId: number;
let timedOut = false;
let req;
let req: IRequest;
const localTimeoutMs = opts.localTimeoutMs || this.opts.localTimeoutMs;
const resetTimeout = () => {
@@ -697,9 +802,7 @@ MatrixHttpApi.prototype = {
}
timeoutId = callbacks.setTimeout(function() {
timedOut = true;
if (req && req.abort) {
req.abort();
}
req?.abort?.();
defer.reject(new MatrixError({
error: "Locally timed out waiting for a response",
errcode: "ORG.MATRIX.JSSDK_TIMEOUT",
@@ -710,7 +813,7 @@ MatrixHttpApi.prototype = {
};
resetTimeout();
const reqPromise = defer.promise;
const reqPromise = defer.promise as IAbortablePromise<ResponseType<T, O>>;
try {
req = this.opts.request(
@@ -727,7 +830,7 @@ MatrixHttpApi.prototype = {
headers: headers || {},
_matrix_opts: this.opts,
},
function(err, response, body) {
(err, response, body) => {
if (localTimeoutMs) {
callbacks.clearTimeout(timeoutId);
if (timedOut) {
@@ -735,16 +838,13 @@ MatrixHttpApi.prototype = {
}
}
const handlerFn = requestCallback(
defer, callback, self.opts.onlyData,
bodyParser,
);
const handlerFn = requestCallback(defer, callback, this.opts.onlyData, bodyParser);
handlerFn(err, response, body);
},
);
if (req) {
// This will only work in a browser, where opts.request is the
// `browser-request` import. Currently `request` does not support progress
// `browser-request` import. Currently, `request` does not support progress
// updates - see https://github.com/request/request/pull/2346.
// `browser-request` returns an XHRHttpRequest which exposes `onprogress`
if ('onprogress' in req) {
@@ -757,7 +857,9 @@ MatrixHttpApi.prototype = {
// FIXME: This is EVIL, but I can't think of a better way to expose
// abort() operations on underlying HTTP requests :(
if (req.abort) reqPromise.abort = req.abort.bind(req);
if (req.abort) {
reqPromise.abort = req.abort.bind(req);
}
}
} catch (ex) {
defer.reject(ex);
@@ -766,8 +868,21 @@ MatrixHttpApi.prototype = {
}
}
return reqPromise;
},
};
}
}
type RequestCallback = (err?: Error, response?: XMLHttpRequest | IncomingMessage, body?: string) => void;
// if using onlyData=false then wrap your expected data type in this generic
export interface IResponse<T> {
code: number;
data: T;
headers?: IncomingHttpHeaders;
}
function getStatusCode(response: XMLHttpRequest | IncomingMessage): number {
return (response as XMLHttpRequest).status || (response as IncomingMessage).statusCode;
}
/*
* Returns a callback that can be invoked by an HTTP request on completion,
@@ -783,17 +898,17 @@ MatrixHttpApi.prototype = {
* response, otherwise the result object (with `code` and `data` fields)
*
*/
const requestCallback = function(
defer, userDefinedCallback, onlyData,
bodyParser,
) {
userDefinedCallback = userDefinedCallback || function() {};
return function(err, response, body) {
function requestCallback<T>(
defer: IDeferred<T>,
userDefinedCallback?: Callback<T>,
onlyData = false,
bodyParser?: (body: string) => T,
): RequestCallback {
return function(err: Error, response: XMLHttpRequest | IncomingMessage, body: string): void {
if (err) {
// the unit tests use matrix-mock-request, which throw the string "aborted" when aborting a request.
// See https://github.com/matrix-org/matrix-mock-request/blob/3276d0263a561b5b8326b47bae720578a2c7473a/src/index.js#L48
const aborted = err.name === "AbortError" || err === "aborted";
const aborted = err.name === "AbortError" || (err as any as string) === "aborted";
if (!aborted && !(err instanceof MatrixError)) {
// browser-request just throws normal Error objects,
// not `TypeError`s like fetch does. So just assume any
@@ -801,13 +916,15 @@ const requestCallback = function(
err = new ConnectionError("request failed", err);
}
}
let data: T | string = body;
if (!err) {
try {
const httpStatus = response.status || response.statusCode; // XMLHttpRequest vs http.IncomingMessage
if (httpStatus >= 400) {
if (getStatusCode(response) >= 400) {
err = parseErrorResponse(response, body);
} else if (bodyParser) {
body = bodyParser(body);
data = bodyParser(body);
}
} catch (e) {
err = new Error(`Error parsing server response: ${e}`);
@@ -816,21 +933,26 @@ const requestCallback = function(
if (err) {
defer.reject(err);
userDefinedCallback(err);
userDefinedCallback?.(err);
} else if (onlyData) {
defer.resolve(data as T);
userDefinedCallback?.(null, data as T);
} else {
const res = {
code: response.status || response.statusCode, // XMLHttpRequest vs http.IncomingMessage
const res: IResponse<T> = {
code: getStatusCode(response),
// XXX: why do we bother with this? it doesn't work for
// XMLHttpRequest, so clearly we don't use it.
headers: response.headers,
data: body,
headers: (response as IncomingMessage).headers,
data: data as T,
};
defer.resolve(onlyData ? body : res);
userDefinedCallback(null, onlyData ? body : res);
// XXX: the variations in caller-expected types here are horrible,
// typescript doesn't do conditional types based on runtime values
defer.resolve(res as any as T);
userDefinedCallback?.(null, res as any as T);
}
};
};
}
/**
* Attempt to turn an HTTP error response into a Javascript Error.
@@ -842,8 +964,8 @@ const requestCallback = function(
* @param {String} body raw body of the response
* @returns {Error}
*/
function parseErrorResponse(response, body) {
const httpStatus = response.status || response.statusCode; // XMLHttpRequest vs http.IncomingMessage
function parseErrorResponse(response: XMLHttpRequest | IncomingMessage, body?: string) {
const httpStatus = getStatusCode(response);
const contentType = getResponseContentType(response);
let err;
@@ -872,14 +994,14 @@ function parseErrorResponse(response, body) {
* @param {XMLHttpRequest|http.IncomingMessage} response response object
* @returns {{type: String, parameters: Object}?} parsed content-type header, or null if not found
*/
function getResponseContentType(response) {
function getResponseContentType(response: XMLHttpRequest | IncomingMessage): ParsedMediaType {
let contentType;
if (response.getResponseHeader) {
if ((response as XMLHttpRequest).getResponseHeader) {
// XMLHttpRequest provides getResponseHeader
contentType = response.getResponseHeader("Content-Type");
} else if (response.headers) {
contentType = (response as XMLHttpRequest).getResponseHeader("Content-Type");
} else if ((response as IncomingMessage).headers) {
// request provides http.IncomingMessage which has a message.headers map
contentType = response.headers['content-type'] || null;
contentType = (response as IncomingMessage).headers['content-type'] || null;
}
if (!contentType) {
@@ -893,6 +1015,12 @@ function getResponseContentType(response) {
}
}
interface IErrorJson extends Partial<IUsageLimit> {
[key: string]: any; // extensible
errcode?: string;
error?: string;
}
/**
* Construct a Matrix error. This is a JavaScript Error with additional
* information specific to the standard Matrix error response.
@@ -905,8 +1033,11 @@ function getResponseContentType(response) {
* @prop {integer} httpStatus The numeric HTTP status code given
*/
export class MatrixError extends Error {
constructor(errorJson) {
errorJson = errorJson || {};
public readonly errcode: string;
public readonly data: IErrorJson;
public httpStatus?: number; // set by http-api
constructor(errorJson: IErrorJson = {}) {
super(`MatrixError: ${errorJson.errcode}`);
this.errcode = errorJson.errcode;
this.name = errorJson.errcode || "Unknown error code";
@@ -923,18 +1054,13 @@ export class MatrixError extends Error {
* @constructor
*/
export class ConnectionError extends Error {
constructor(message, cause = undefined) {
constructor(message: string, private readonly cause: Error = undefined) {
super(message + (cause ? `: ${cause.message}` : ""));
this._cause = cause;
}
get name() {
return "ConnectionError";
}
get cause() {
return this._cause;
}
}
export class AbortError extends Error {
@@ -954,7 +1080,7 @@ export class AbortError extends Error {
* @return {any} the result of the network operation
* @throws {ConnectionError} If after maxAttempts the callback still throws ConnectionError
*/
export async function retryNetworkOperation(maxAttempts, callback) {
export async function retryNetworkOperation<T>(maxAttempts: number, callback: () => T): Promise<T> {
let attempts = 0;
let lastConnectionError = null;
while (attempts < maxAttempts) {
+2 -6
View File
@@ -14,10 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import request from "request";
import * as matrixcs from "./matrix";
import * as utils from "./utils";
import { logger } from './logger';
import request from "request";
matrixcs.request(request);
@@ -32,8 +33,3 @@ try {
export * from "./matrix";
export default matrixcs;
export * from './@types/event';
export * from './@types/PushRules';
export * from './@types/requests';
export * from './@types/search';
export * from './models/room-summary';
+6 -6
View File
@@ -18,11 +18,9 @@ limitations under the License.
/** @module interactive-auth */
import * as utils from "./utils";
import { logger } from './logger';
import { MatrixClient } from "./client";
import { defer, IDeferred } from "./utils";
import { MatrixError } from "./http-api";
const EMAIL_STAGE_TYPE = "m.login.email.identity";
const MSISDN_STAGE_TYPE = "m.login.msisdn";
@@ -35,6 +33,7 @@ export interface IInputs {
emailAddress?: string;
phoneCountry?: string;
phoneNumber?: string;
registrationToken?: string;
}
export interface IStageStatus {
@@ -49,7 +48,7 @@ export interface IAuthData {
flows?: IFlow[];
params?: Record<string, Record<string, any>>;
errcode?: string;
error?: MatrixError;
error?: string;
}
export enum AuthType {
@@ -67,7 +66,7 @@ export enum AuthType {
export interface IAuthDict {
// [key: string]: any;
type?: string;
// session?: string; // TODO
session?: string;
// TODO: Remove `user` once servers support proper UIA
// See https://github.com/vector-im/element-web/issues/10312
user?: string;
@@ -80,6 +79,7 @@ export interface IAuthDict {
// eslint-disable-next-line camelcase
threepid_creds?: any;
threepidCreds?: any;
registrationToken?: string;
}
class NoAuthFlowFoundError extends Error {
@@ -358,12 +358,12 @@ export class InteractiveAuth {
}
// use the sessionid from the last request, if one is present.
let auth;
let auth: IAuthDict;
if (this.data.session) {
auth = {
session: this.data.session,
};
utils.extend(auth, authData);
Object.assign(auth, authData);
} else {
auth = authData;
}
+1 -1
View File
@@ -76,7 +76,7 @@ function extendLogger(logger: PrefixedLogger) {
extendLogger(logger);
function getPrefixedLogger(prefix): PrefixedLogger {
function getPrefixedLogger(prefix: string): PrefixedLogger {
const prefixLogger: PrefixedLogger = log.getLogger(`${DEFAULT_NAMESPACE}-${prefix}`);
if (prefixLogger.prefix !== prefix) {
// Only do this setup work the first time through, as loggers are saved by name.
+6 -1
View File
@@ -46,6 +46,11 @@ export * from "./store/session/webstorage";
export * from "./crypto/store/memory-crypto-store";
export * from "./crypto/store/indexeddb-crypto-store";
export * from "./content-repo";
export * from './@types/event';
export * from './@types/PushRules';
export * from './@types/requests';
export * from './@types/search';
export * from './models/room-summary';
export * as ContentHelpers from "./content-helpers";
export {
createNewMatrixCall,
@@ -116,7 +121,7 @@ export interface ICryptoCallbacks {
) => Promise<string>;
getDehydrationKey?: (
keyInfo: ISecretStorageKeyInfo,
checkFunc: (Uint8Array) => void,
checkFunc: (key: Uint8Array) => void,
) => Promise<Uint8Array>;
getBackupKey?: () => Promise<Uint8Array>;
}
+21 -5
View File
@@ -18,7 +18,8 @@ import { MatrixClient } from "../client";
import { IEncryptedFile, RelationType, UNSTABLE_MSC3089_BRANCH } from "../@types/event";
import { IContent, MatrixEvent } from "./event";
import { MSC3089TreeSpace } from "./MSC3089TreeSpace";
import type { ReadStream } from "fs";
import { EventTimeline } from "./event-timeline";
import { FileType } from "../http-api";
/**
* Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) branch - a reference
@@ -38,7 +39,11 @@ export class MSC3089Branch {
* The file ID.
*/
public get id(): string {
return this.indexEvent.getStateKey();
const stateKey = this.indexEvent.getStateKey();
if (!stateKey) {
throw new Error("State key not found for branch");
}
return stateKey;
}
/**
@@ -121,6 +126,10 @@ export class MSC3089Branch {
const file = event.getOriginalContent()['file'];
const httpUrl = this.client.mxcUrlToHttp(file['url']);
if (!httpUrl) {
throw new Error(`No HTTP URL available for ${file['url']}`);
}
return { info: file, httpUrl: httpUrl };
}
@@ -132,7 +141,14 @@ export class MSC3089Branch {
const room = this.client.getRoom(this.roomId);
if (!room) throw new Error("Unknown room");
const event = room.getUnfilteredTimelineSet().findEventById(this.id);
let event: MatrixEvent | undefined = room.getUnfilteredTimelineSet().findEventById(this.id);
// keep scrolling back if needed until we find the event or reach the start of the room:
while (!event && room.getLiveTimeline().getState(EventTimeline.BACKWARDS).paginationToken) {
await this.client.scrollback(room, 100);
event = room.getUnfilteredTimelineSet().findEventById(this.id);
}
if (!event) throw new Error("Failed to find event");
// Sometimes the event isn't decrypted for us, so do that. We specifically set `emit: true`
@@ -152,7 +168,7 @@ export class MSC3089Branch {
*/
public async createNewVersion(
name: string,
encryptedContents: File | String | Buffer | ReadStream | Blob,
encryptedContents: FileType,
info: Partial<IEncryptedFile>,
additionalContent?: IContent,
): Promise<void> {
@@ -199,7 +215,7 @@ export class MSC3089Branch {
// XXX: This is a very inefficient search, but it's the best we can do with the
// relations structure we have in the SDK. As of writing, it is not worth the
// investment in improving the structure.
let childEvent: MatrixEvent;
let childEvent: MatrixEvent | undefined;
let parentEvent = await this.getFileEvent();
do {
childEvent = timelineEvents.find(e => e.replacingEventId() === parentEvent.getId());
+28 -15
View File
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import promiseRetry from "p-retry";
import { MatrixClient } from "../client";
import { EventType, IEncryptedFile, MsgType, UNSTABLE_MSC3089_BRANCH, UNSTABLE_MSC3089_LEAF } from "../@types/event";
import { Room } from "./room";
@@ -28,10 +30,9 @@ import {
simpleRetryOperation,
} from "../utils";
import { MSC3089Branch } from "./MSC3089Branch";
import promiseRetry from "p-retry";
import { isRoomSharedHistory } from "../crypto/algorithms/megolm";
import { ISendEventResponse } from "../@types/requests";
import type { ReadStream } from "fs";
import { FileType } from "../http-api";
/**
* The recommended defaults for a tree space's power levels. Note that this
@@ -245,8 +246,11 @@ export class MSC3089TreeSpace {
const children = this.room.currentState.getStateEvents(EventType.SpaceChild);
for (const child of children) {
try {
const tree = this.client.unstableGetFileTreeSpace(child.getStateKey());
if (tree) trees.push(tree);
const stateKey = child.getStateKey();
if (stateKey) {
const tree = this.client.unstableGetFileTreeSpace(stateKey);
if (tree) trees.push(tree);
}
} catch (e) {
logger.warn("Unable to create tree space instance for listing. Are we joined?", e);
}
@@ -258,9 +262,9 @@ export class MSC3089TreeSpace {
* Gets a subdirectory of a given ID under this tree space. Note that this will not recurse
* into children and instead only look one level deep.
* @param {string} roomId The room ID (directory ID) to find.
* @returns {MSC3089TreeSpace} The directory, or falsy if not found.
* @returns {MSC3089TreeSpace | undefined} The directory, or undefined if not found.
*/
public getDirectory(roomId: string): MSC3089TreeSpace {
public getDirectory(roomId: string): MSC3089TreeSpace | undefined {
return this.getDirectories().find(r => r.roomId === roomId);
}
@@ -278,8 +282,12 @@ export class MSC3089TreeSpace {
const members = this.room.currentState.getStateEvents(EventType.RoomMember);
for (const member of members) {
const isNotUs = member.getStateKey() !== this.client.getUserId();
if (isNotUs && kickMemberships.includes(member.getContent()['membership'])) {
await this.client.kick(this.roomId, member.getStateKey(), "Room deleted");
if (isNotUs && kickMemberships.includes(member.getContent().membership)) {
const stateKey = member.getStateKey();
if (!stateKey) {
throw new Error("State key not found for branch");
}
await this.client.kick(this.roomId, stateKey, "Room deleted");
}
}
@@ -288,7 +296,8 @@ export class MSC3089TreeSpace {
private getOrderedChildren(children: MatrixEvent[]): { roomId: string, order: string }[] {
const ordered: { roomId: string, order: string }[] = children
.map(c => ({ roomId: c.getStateKey(), order: c.getContent()['order'] }));
.map(c => ({ roomId: c.getStateKey(), order: c.getContent()['order'] }))
.filter(c => c.roomId) as { roomId: string, order: string }[];
ordered.sort((a, b) => {
if (a.order && !b.order) {
return -1;
@@ -321,7 +330,9 @@ export class MSC3089TreeSpace {
// XXX: We are assuming the parent is a valid tree space.
// We probably don't need to validate the parent room state for this usecase though.
const parentRoom = this.client.getRoom(parent.getStateKey());
const stateKey = parent.getStateKey();
if (!stateKey) throw new Error("No state key found for parent");
const parentRoom = this.client.getRoom(stateKey);
if (!parentRoom) throw new Error("Unable to locate room for parent");
return parentRoom;
@@ -413,7 +424,7 @@ export class MSC3089TreeSpace {
// We were asked by the order algorithm to prepare the moving space for a landing
// in the undefined order part of the order array, which means we need to update the
// spaces that come before it with a stable order value.
let lastOrder: string;
let lastOrder: string | undefined;
for (let i = 0; i <= index; i++) {
const target = ordered[i];
if (i === 0) {
@@ -432,7 +443,9 @@ export class MSC3089TreeSpace {
lastOrder = target.order;
}
}
newOrder = nextString(lastOrder);
if (lastOrder) {
newOrder = nextString(lastOrder);
}
}
// TODO: Deal with order conflicts by reordering
@@ -459,7 +472,7 @@ export class MSC3089TreeSpace {
*/
public async createFile(
name: string,
encryptedContents: File | String | Buffer | ReadStream | Blob,
encryptedContents: FileType,
info: Partial<IEncryptedFile>,
additionalContent?: IContent,
): Promise<ISendEventResponse> {
@@ -502,9 +515,9 @@ export class MSC3089TreeSpace {
/**
* Retrieves a file from the tree.
* @param {string} fileEventId The event ID of the file.
* @returns {MSC3089Branch} The file, or falsy if not found.
* @returns {MSC3089Branch | null} The file, or null if not found.
*/
public getFile(fileEventId: string): MSC3089Branch {
public getFile(fileEventId: string): MSC3089Branch | null {
const branch = this.room.currentState.getStateEvents(UNSTABLE_MSC3089_BRANCH.name, fileEventId);
return branch ? new MSC3089Branch(this.client, branch, this) : null;
}
+8 -3
View File
@@ -33,7 +33,7 @@ import { Thread } from "./thread";
// var DEBUG = false;
const DEBUG = true;
let debuglog;
let debuglog: (...args: any[]) => void;
if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console
debuglog = logger.log.bind(logger);
@@ -53,6 +53,11 @@ export enum DuplicateStrategy {
Replace = "replace",
}
export interface IRoomTimelineData {
timeline: EventTimeline;
liveEvent?: boolean;
}
export class EventTimelineSet extends EventEmitter {
private readonly timelineSupport: boolean;
private unstableClientRelationAggregation: boolean;
@@ -209,7 +214,7 @@ export class EventTimelineSet extends EventEmitter {
*
* @fires module:client~MatrixClient#event:"Room.timelineReset"
*/
public resetLiveTimeline(backPaginationToken: string, forwardPaginationToken?: string): void {
public resetLiveTimeline(backPaginationToken?: string, forwardPaginationToken?: string): void {
// Each EventTimeline has RoomState objects tracking the state at the start
// and end of that timeline. The copies at the end of the live timeline are
// special because they will have listeners attached to monitor changes to
@@ -594,7 +599,7 @@ export class EventTimelineSet extends EventEmitter {
this.setRelationsTarget(event);
this.aggregateRelations(event);
const data = {
const data: IRoomTimelineData = {
timeline: timeline,
liveEvent: !toStartOfTimeline && timeline == this.liveTimeline && !fromCache,
};
+2 -2
View File
@@ -34,13 +34,13 @@ export class EventTimeline {
* Symbolic constant for methods which take a 'direction' argument:
* refers to the start of the timeline, or backwards in time.
*/
static BACKWARDS = Direction.Backward;
public static readonly BACKWARDS = Direction.Backward;
/**
* Symbolic constant for methods which take a 'direction' argument:
* refers to the end of the timeline, or forwards in time.
*/
static FORWARDS = Direction.Forward;
public static readonly FORWARDS = Direction.Forward;
/**
* Static helper method to set sender and target properties
+247 -45
View File
@@ -1,5 +1,5 @@
/*
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2015 - 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@ limitations under the License.
*/
import { EventEmitter } from 'events';
import { ExtensibleEvent, ExtensibleEvents, Optional } from "matrix-events-sdk";
import { logger } from '../logger';
import { VerificationRequest } from "../crypto/verification/request/VerificationRequest";
@@ -28,13 +29,15 @@ import {
EventType,
MsgType,
RelationType,
EVENT_VISIBILITY_CHANGE_TYPE,
} from "../@types/event";
import { Crypto } from "../crypto";
import { Crypto, IEventDecryptionResult } from "../crypto";
import { deepSortedObjectEntries } from "../utils";
import { RoomMember } from "./room-member";
import { Thread, ThreadEvent } from "./thread";
import { IActionsObject } from '../pushprocessor';
import { ReEmitter } from '../ReEmitter';
import { MatrixError } from "../http-api";
/**
* Enum for event statuses.
@@ -88,6 +91,13 @@ export interface IUnsigned {
redacted_because?: IEvent;
transaction_id?: string;
invite_room_state?: StrippedState[];
"m.relations"?: Record<RelationType | string, any>; // No common pattern for aggregated relations
}
export interface IThreadBundledRelationship {
latest_event: IEvent;
count: number;
current_user_participated?: boolean;
}
export interface IEvent {
@@ -109,7 +119,7 @@ export interface IEvent {
age?: number;
}
interface IAggregatedRelation {
export interface IAggregatedRelation {
origin_server_ts: number;
event_id?: string;
sender?: string;
@@ -121,28 +131,49 @@ interface IAggregatedRelation {
export interface IEventRelation {
rel_type: RelationType | string;
event_id: string;
"m.in_reply_to"?: {
event_id: string;
"m.render_in"?: string[];
};
key?: string;
}
interface IDecryptionResult {
clearEvent: {
room_id?: string;
type: string;
content: IContent;
unsigned?: IUnsigned;
};
forwardingCurve25519KeyChain?: string[];
senderCurve25519Key?: string;
claimedEd25519Key?: string;
untrusted?: boolean;
export interface IVisibilityEventRelation extends IEventRelation {
visibility: "visible" | "hidden";
reason?: string;
}
/**
* When an event is a visibility change event, as per MSC3531,
* the visibility change implied by the event.
*/
export interface IVisibilityChange {
/**
* If `true`, the target event should be made visible.
* Otherwise, it should be hidden.
*/
visible: boolean;
/**
* The event id affected.
*/
eventId: string;
/**
* Optionally, a human-readable reason explaining why
* the event was hidden. Ignored if the event was made
* visible.
*/
reason: string | null;
}
/* eslint-enable camelcase */
export interface IClearEvent {
room_id?: string;
type: string;
content: Omit<IContent, "membership" | "avatar_url" | "displayname" | "m.relates_to">;
unsigned?: IUnsigned;
}
/* eslint-enable camelcase */
interface IKeyRequestRecipient {
userId: string;
@@ -154,6 +185,30 @@ export interface IDecryptOptions {
isRetry?: boolean;
}
/**
* Message hiding, as specified by https://github.com/matrix-org/matrix-doc/pull/3531.
*/
export type MessageVisibility = IMessageVisibilityHidden | IMessageVisibilityVisible;
/**
* Variant of `MessageVisibility` for the case in which the message should be displayed.
*/
export interface IMessageVisibilityVisible {
readonly visible: true;
}
/**
* Variant of `MessageVisibility` for the case in which the message should be hidden.
*/
export interface IMessageVisibilityHidden {
readonly visible: false;
/**
* Optionally, a human-readable reason to show to the user indicating why the
* message has been hidden (e.g. "Message Pending Moderation").
*/
readonly reason: string | null;
}
// A singleton implementing `IMessageVisibilityVisible`.
const MESSAGE_VISIBLE: IMessageVisibilityVisible = Object.freeze({ visible: true });
export class MatrixEvent extends EventEmitter {
private pushActions: IActionsObject = null;
private _replacingEvent: MatrixEvent = null;
@@ -161,6 +216,18 @@ export class MatrixEvent extends EventEmitter {
private _isCancelled = false;
private clearEvent?: IClearEvent;
/* Message hiding, as specified by https://github.com/matrix-org/matrix-doc/pull/3531.
Note: We're returning this object, so any value stored here MUST be frozen.
*/
private visibility: MessageVisibility = MESSAGE_VISIBLE;
// Not all events will be extensible-event compatible, so cache a flag in
// addition to a falsy cached event value. We check the flag later on in
// a public getter to decide if the cache is valid.
private _hasCachedExtEv = false;
private _cachedExtEv: Optional<ExtensibleEvent> = undefined;
/* curve25519 key which we believe belongs to the sender of the event. See
* getSenderKey()
*/
@@ -202,6 +269,7 @@ export class MatrixEvent extends EventEmitter {
* A reference to the thread this event belongs to
*/
private thread: Thread = null;
private threadId: string;
/* Set an approximate timestamp for the event relative the local clock.
* This will inherently be approximate because it doesn't take into account
@@ -215,14 +283,14 @@ export class MatrixEvent extends EventEmitter {
public sender: RoomMember = null;
public target: RoomMember = null;
public status: EventStatus = null;
public error = null;
public error: MatrixError = null;
public forwardLooking = true;
/* If the event is a `m.key.verification.request` (or to_device `m.key.verification.start`) event,
* `Crypto` will set this the `VerificationRequest` for the event
* so it can be easily accessed from the timeline.
*/
public verificationRequest = null;
public verificationRequest: VerificationRequest = null;
private readonly reEmitter: ReEmitter;
@@ -278,6 +346,25 @@ export class MatrixEvent extends EventEmitter {
this.reEmitter = new ReEmitter(this);
}
/**
* Unstable getter to try and get an extensible event. Note that this might
* return a falsy value if the event could not be parsed as an extensible
* event.
*
* @deprecated Use stable functions where possible.
*/
public get unstableExtensibleEvent(): Optional<ExtensibleEvent> {
if (!this._hasCachedExtEv) {
this._cachedExtEv = ExtensibleEvents.parse(this.getEffectiveEvent());
}
return this._cachedExtEv;
}
private invalidateExtensibleEvent() {
// just reset the flag - that'll trick the getter into parsing a new event
this._hasCachedExtEv = false;
}
/**
* Gets the event as though it would appear unencrypted. If the event is already not
* encrypted, it is simply returned as-is.
@@ -420,10 +507,13 @@ export class MatrixEvent extends EventEmitter {
* @experimental
* Get the event ID of the thread head
*/
public get threadRootId(): string {
public get threadRootId(): string | undefined {
const relatesTo = this.getWireContent()?.["m.relates_to"];
if (relatesTo?.rel_type === RelationType.Thread) {
return relatesTo.event_id;
} else {
return this.threadId
|| this.getThread()?.id;
}
}
@@ -431,17 +521,20 @@ export class MatrixEvent extends EventEmitter {
* @experimental
*/
public get isThreadRelation(): boolean {
return !!this.threadRootId;
return !!this.threadRootId && this.threadId !== this.getId();
}
/**
* @experimental
*/
public get isThreadRoot(): boolean {
// TODO, change the inner working of this getter for it to use the
// bundled relationship return on the event, view MSC3440
const thread = this.getThread();
return thread?.id === this.getId();
const threadDetails = this
.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
// Bundled relationships only returned when the sync response is limited
// hence us having to check both bundled relation and inspect the thread
// model
return !!threadDetails || (this.getThread()?.id === this.getId());
}
public get parentEventId(): string {
@@ -449,8 +542,13 @@ export class MatrixEvent extends EventEmitter {
}
public get replyEventId(): string {
const relations = this.getWireContent()["m.relates_to"];
return relations?.["m.in_reply_to"]?.["event_id"];
// We're prefer ev.getContent() over ev.getWireContent() to make sure
// we grab the latest edit with potentially new relations. But we also
// can't just rely on ev.getContent() by itself because historically we
// still show the reply from the original message even though the edit
// event does not include the relation reply.
const mRelatesTo = this.getContent()['m.relates_to'] || this.getWireContent()['m.relates_to'];
return mRelatesTo?.['m.in_reply_to']?.event_id;
}
public get relationEventId(): string {
@@ -690,8 +788,8 @@ export class MatrixEvent extends EventEmitter {
while (true) {
this.retryDecryption = false;
let res;
let err;
let res: IEventDecryptionResult;
let err: Error;
try {
if (!crypto) {
res = this.badEncryptedMessage("Encryption not enabled");
@@ -779,10 +877,10 @@ export class MatrixEvent extends EventEmitter {
}
}
private badEncryptedMessage(reason: string): IDecryptionResult {
private badEncryptedMessage(reason: string): IEventDecryptionResult {
return {
clearEvent: {
type: "m.room.message",
type: EventType.RoomMessage,
content: {
msgtype: "m.bad.encrypted",
body: "** Unable to decrypt: " + reason + " **",
@@ -803,7 +901,7 @@ export class MatrixEvent extends EventEmitter {
* @param {module:crypto~EventDecryptionResult} decryptionResult
* the decryption result, including the plaintext and some key info
*/
private setClearData(decryptionResult: IDecryptionResult): void {
private setClearData(decryptionResult: IEventDecryptionResult): void {
this.clearEvent = decryptionResult.clearEvent;
this.senderCurve25519Key =
decryptionResult.senderCurve25519Key || null;
@@ -812,6 +910,7 @@ export class MatrixEvent extends EventEmitter {
this.forwardingCurve25519KeyChain =
decryptionResult.forwardingCurve25519KeyChain || [];
this.untrusted = decryptionResult.untrusted || false;
this.invalidateExtensibleEvent();
}
/**
@@ -829,7 +928,7 @@ export class MatrixEvent extends EventEmitter {
* @return {boolean} True if this event is encrypted.
*/
public isEncrypted(): boolean {
return !this.isState() && this.event.type === "m.room.encrypted";
return !this.isState() && this.event.type === EventType.RoomMessageEncrypted;
}
/**
@@ -915,6 +1014,10 @@ export class MatrixEvent extends EventEmitter {
return this.event.unsigned || {};
}
public setUnsigned(unsigned: IUnsigned): void {
this.event.unsigned = unsigned;
}
public unmarkLocallyRedacted(): boolean {
const value = this._localRedactionEvent;
this._localRedactionEvent = null;
@@ -934,6 +1037,53 @@ export class MatrixEvent extends EventEmitter {
this.event.unsigned.redacted_because = redactionEvent.event as IEvent;
}
/**
* Change the visibility of an event, as per https://github.com/matrix-org/matrix-doc/pull/3531 .
*
* @fires module:models/event.MatrixEvent#"Event.visibilityChange" if `visibilityEvent`
* caused a change in the actual visibility of this event, either by making it
* visible (if it was hidden), by making it hidden (if it was visible) or by
* changing the reason (if it was hidden).
* @param visibilityEvent event holding a hide/unhide payload, or nothing
* if the event is being reset to its original visibility (presumably
* by a visibility event being redacted).
*/
public applyVisibilityEvent(visibilityChange?: IVisibilityChange): void {
const visible = visibilityChange ? visibilityChange.visible : true;
const reason = visibilityChange ? visibilityChange.reason : null;
let change = false;
if (this.visibility.visible !== visibilityChange.visible) {
change = true;
} else if (!this.visibility.visible && this.visibility["reason"] !== reason) {
change = true;
}
if (change) {
if (visible) {
this.visibility = MESSAGE_VISIBLE;
} else {
this.visibility = Object.freeze({
visible: false,
reason: reason,
});
}
if (change) {
this.emit("Event.visibilityChange", this, visible);
}
}
}
/**
* Return instructions to display or hide the message.
*
* @returns Instructions determining whether the message
* should be displayed.
*/
public messageVisibility(): MessageVisibility {
// Note: We may return `this.visibility` without fear, as
// this is a shallow frozen object.
return this.visibility;
}
/**
* Update the content of an event in the same way it would be by the server
* if it were redacted before it was sent to us
@@ -983,6 +1133,8 @@ export class MatrixEvent extends EventEmitter {
delete content[key];
}
}
this.invalidateExtensibleEvent();
}
/**
@@ -1000,7 +1152,55 @@ export class MatrixEvent extends EventEmitter {
* @return {boolean} True if this event is a redaction
*/
public isRedaction(): boolean {
return this.getType() === "m.room.redaction";
return this.getType() === EventType.RoomRedaction;
}
/**
* Return the visibility change caused by this event,
* as per https://github.com/matrix-org/matrix-doc/pull/3531.
*
* @returns If the event is a well-formed visibility change event,
* an instance of `IVisibilityChange`, otherwise `null`.
*/
public asVisibilityChange(): IVisibilityChange | null {
if (!EVENT_VISIBILITY_CHANGE_TYPE.matches(this.getType())) {
// Not a visibility change event.
return null;
}
const relation = this.getRelation();
if (!relation || relation.rel_type != "m.reference") {
// Ill-formed, ignore this event.
return null;
}
const eventId = relation.event_id;
if (!eventId) {
// Ill-formed, ignore this event.
return null;
}
const content = this.getWireContent();
const visible = !!content.visible;
const reason = content.reason;
if (reason && typeof reason != "string") {
// Ill-formed, ignore this event.
return null;
}
// Well-formed visibility change event.
return {
visible,
reason,
eventId,
};
}
/**
* Check if this event alters the visibility of another event,
* as per https://github.com/matrix-org/matrix-doc/pull/3531.
*
* @returns {boolean} True if this event alters the visibility
* of another event.
*/
public isVisibilityEvent(): boolean {
return EVENT_VISIBILITY_CHANGE_TYPE.matches(this.getType());
}
/**
@@ -1138,6 +1338,7 @@ export class MatrixEvent extends EventEmitter {
if (this._replacingEvent !== newEvent) {
this._replacingEvent = newEvent;
this.emit("Event.replaced", this);
this.invalidateExtensibleEvent();
}
}
@@ -1157,11 +1358,8 @@ export class MatrixEvent extends EventEmitter {
return this.status;
}
public getServerAggregatedRelation(relType: RelationType): IAggregatedRelation {
const relations = this.getUnsigned()["m.relations"];
if (relations) {
return relations[relType];
}
public getServerAggregatedRelation<T>(relType: RelationType): T | undefined {
return this.getUnsigned()["m.relations"]?.[relType];
}
/**
@@ -1170,7 +1368,7 @@ export class MatrixEvent extends EventEmitter {
* @return {string?}
*/
public replacingEventId(): string | undefined {
const replaceRelation = this.getServerAggregatedRelation(RelationType.Replace);
const replaceRelation = this.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
if (replaceRelation) {
return replaceRelation.event_id;
} else if (this._replacingEvent) {
@@ -1195,7 +1393,7 @@ export class MatrixEvent extends EventEmitter {
* @return {Date?}
*/
public replacingEventDate(): Date | undefined {
const replaceRelation = this.getServerAggregatedRelation(RelationType.Replace);
const replaceRelation = this.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
if (replaceRelation) {
const ts = replaceRelation.origin_server_ts;
if (Number.isFinite(ts)) {
@@ -1361,9 +1559,13 @@ export class MatrixEvent extends EventEmitter {
/**
* @experimental
*/
public getThread(): Thread {
public getThread(): Thread | undefined {
return this.thread;
}
public setThreadId(threadId: string): void {
this.threadId = threadId;
}
}
/* REDACT_KEEP_KEYS gives the keys we keep when an event is redacted
@@ -1382,15 +1584,15 @@ const REDACT_KEEP_KEYS = new Set([
// a map from event type to the .content keys we keep when an event is redacted
const REDACT_KEEP_CONTENT_MAP = {
'm.room.member': { 'membership': 1 },
'm.room.create': { 'creator': 1 },
'm.room.join_rules': { 'join_rule': 1 },
'm.room.power_levels': {
[EventType.RoomMember]: { 'membership': 1 },
[EventType.RoomCreate]: { 'creator': 1 },
[EventType.RoomJoinRules]: { 'join_rule': 1 },
[EventType.RoomPowerLevels]: {
'ban': 1, 'events': 1, 'events_default': 1,
'kick': 1, 'redact': 1, 'state_default': 1,
'users': 1, 'users_default': 1,
},
'm.room.aliases': { 'aliases': 1 },
[EventType.RoomAliases]: { 'aliases': 1 },
};
/**
+2 -1
View File
@@ -20,9 +20,10 @@ limitations under the License.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
import * as utils from "../utils";
import { EventEmitter } from "events";
import * as utils from "../utils";
/**
* Construct a new Group.
*
+38
View File
@@ -0,0 +1,38 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Relations } from "./relations";
import { MatrixEvent } from "./event";
export class RelatedRelations {
private relations: Relations[];
public constructor(relations: Relations[]) {
this.relations = relations.filter(r => !!r);
}
public getRelations(): MatrixEvent[] {
return this.relations.reduce((c, p) => [...c, ...p.getRelations()], []);
}
public on(ev: string, fn: (...params) => void) {
this.relations.forEach(r => r.on(ev, fn));
}
public off(ev: string, fn: (...params) => void) {
this.relations.forEach(r => r.off(ev, fn));
}
}
+2 -2
View File
@@ -16,7 +16,7 @@ limitations under the License.
import { EventEmitter } from 'events';
import { EventStatus, MatrixEvent } from './event';
import { EventStatus, MatrixEvent, IAggregatedRelation } from './event';
import { Room } from './room';
import { logger } from '../logger';
import { RelationType } from "../@types/event";
@@ -319,7 +319,7 @@ export class Relations extends EventEmitter {
// 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(RelationType.Replace);
const replaceRelation = this.targetEvent.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
const minTs = replaceRelation && replaceRelation.origin_server_ts;
const lastReplacement = this.getRelations().reduce((last, event) => {
+8 -8
View File
@@ -618,14 +618,14 @@ export class RoomState extends EventEmitter {
}
/**
* Returns true if the given MatrixClient has permission to send a state
* event of type `stateEventType` into this room.
* @param {string} stateEventType The type of state events to test
* @param {MatrixClient} cli The client to test permission for
* @return {boolean} true if the given client should be permitted to send
* the given type of state event into this room,
* according to the room's state.
*/
* Returns true if the given MatrixClient has permission to send a state
* event of type `stateEventType` into this room.
* @param {string} stateEventType The type of state events to test
* @param {MatrixClient} cli The client to test permission for
* @return {boolean} true if the given client should be permitted to send
* the given type of state event into this room,
* according to the room's state.
*/
public mayClientSendStateEvent(stateEventType: EventType | string, cli: MatrixClient): boolean {
if (cli.isGuest()) {
return false;
+374 -119
View File
@@ -21,7 +21,7 @@ limitations under the License.
import { EventEmitter } from "events";
import { EventTimelineSet, DuplicateStrategy } from "./event-timeline-set";
import { EventTimeline } from "./event-timeline";
import { Direction, EventTimeline } from "./event-timeline";
import { getHttpUriForMxc } from "../content-repo";
import * as utils from "../utils";
import { normalize } from "../utils";
@@ -30,12 +30,16 @@ import { RoomMember } from "./room-member";
import { IRoomSummary, RoomSummary } from "./room-summary";
import { logger } from '../logger';
import { ReEmitter } from '../ReEmitter';
import { EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../@types/event";
import {
EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS,
EVENT_VISIBILITY_CHANGE_TYPE,
} from "../@types/event";
import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials";
import { Filter } from "../filter";
import { RoomState } from "./room-state";
import { Thread, ThreadEvent } from "./thread";
import { Method } from "../http-api";
// These constants are used as sane defaults when the homeserver doesn't support
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
@@ -48,19 +52,19 @@ const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4', '5', '6'];
function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: string): MatrixEvent {
// console.log("synthesizing receipt for "+event.getId());
// This is really ugly because JS has no way to express an object literal
// where the name of a key comes from an expression
const fakeReceipt = {
content: {},
return new MatrixEvent({
content: {
[event.getId()]: {
[receiptType]: {
[userId]: {
ts: event.getTs(),
},
},
},
},
type: "m.receipt",
room_id: event.getRoomId(),
};
fakeReceipt.content[event.getId()] = {};
fakeReceipt.content[event.getId()][receiptType] = {};
fakeReceipt.content[event.getId()][receiptType][userId] = {
ts: event.getTs(),
};
return new MatrixEvent(fakeReceipt);
});
}
interface IOpts {
@@ -104,11 +108,34 @@ interface IReceiptContent {
type Receipts = Record<string, Record<string, IWrappedReceipt>>;
// When inserting a visibility event affecting event `eventId`, we
// need to scan through existing visibility events for `eventId`.
// In theory, this could take an unlimited amount of time if:
//
// - the visibility event was sent by a moderator; and
// - `eventId` already has many visibility changes (usually, it should
// be 2 or less); and
// - for some reason, the visibility changes are received out of order
// (usually, this shouldn't happen at all).
//
// For this reason, we limit the number of events to scan through,
// expecting that a broken visibility change for a single event in
// an extremely uncommon case (possibly a DoS) is a small
// price to pay to keep matrix-js-sdk responsive.
const MAX_NUMBER_OF_VISIBILITY_EVENTS_TO_SCAN_THROUGH = 30;
export enum NotificationCountType {
Highlight = "highlight",
Total = "total",
}
export interface ICreateFilterOpts {
// Populate the filtered timeline with already loaded events in the room
// timeline. Useful to disable for some filters that can't be achieved by the
// client in an efficient manner
prepopulateTimeline?: boolean;
}
export class Room extends EventEmitter {
private readonly reEmitter: ReEmitter;
private txnToEvent: Record<string, MatrixEvent> = {}; // Pending in-flight requests { string: MatrixEvent }
@@ -135,15 +162,49 @@ export class Room extends EventEmitter {
private membersPromise?: Promise<boolean>;
// XXX: These should be read-only
/**
* The human-readable display name for this room.
*/
public name: string;
/**
* The un-homoglyphed name for this room.
*/
public normalizedName: string;
/**
* Dict of room tags; the keys are the tag name and the values
* are any metadata associated with the tag - e.g. { "fav" : { order: 1 } }
*/
public tags: Record<string, Record<string, any>> = {}; // $tagName: { $metadata: $value }
/**
* accountData Dict of per-room account_data events; the keys are the
* event type and the values are the events.
*/
public accountData: Record<string, MatrixEvent> = {}; // $eventType: $event
/**
* The room summary.
*/
public summary: RoomSummary = null;
/**
* A token which a data store can use to remember the state of the room.
*/
public readonly storageToken?: string;
// legacy fields
/**
* The live event timeline for this room, with the oldest event at index 0.
* Present for backwards compatibility - prefer getLiveTimeline().getEvents()
*/
public timeline: MatrixEvent[];
/**
* oldState The state of the room at the time of the oldest
* event in the live timeline. Present for backwards compatibility -
* prefer getLiveTimeline().getState(EventTimeline.BACKWARDS).
*/
public oldState: RoomState;
/**
* currentState The state of the room at the time of the
* newest event in the timeline. Present for backwards compatibility -
* prefer getLiveTimeline().getState(EventTimeline.FORWARDS).
*/
public currentState: RoomState;
/**
@@ -151,6 +212,24 @@ export class Room extends EventEmitter {
*/
public threads = new Map<string, Thread>();
/**
* A mapping of eventId to all visibility changes to apply
* to the event, by chronological order, as per
* https://github.com/matrix-org/matrix-doc/pull/3531
*
* # Invariants
*
* - within each list, all events are classed by
* chronological order;
* - all events are events such that
* `asVisibilityEvent()` returns a non-null `IVisibilityChange`;
* - within each list with key `eventId`, all events
* are in relation to `eventId`.
*
* @experimental
*/
private visibilityEvents = new Map<string, MatrixEvent[]>();
/**
* Construct a new Room.
*
@@ -191,26 +270,6 @@ export class Room extends EventEmitter {
* Optional. Set to true to enable client-side aggregation of event relations
* via `EventTimelineSet#getRelationsForEvent`.
* This feature is currently unstable and the API may change without notice.
*
* @prop {string} roomId The ID of this room.
* @prop {string} name The human-readable display name for this room.
* @prop {string} normalizedName The un-homoglyphed name for this room.
* @prop {Array<MatrixEvent>} timeline The live event timeline for this room,
* with the oldest event at index 0. Present for backwards compatibility -
* prefer getLiveTimeline().getEvents().
* @prop {object} tags Dict of room tags; the keys are the tag name and the values
* are any metadata associated with the tag - e.g. { "fav" : { order: 1 } }
* @prop {object} accountData Dict of per-room account_data events; the keys are the
* event type and the values are the events.
* @prop {RoomState} oldState The state of the room at the time of the oldest
* event in the live timeline. Present for backwards compatibility -
* prefer getLiveTimeline().getState(EventTimeline.BACKWARDS).
* @prop {RoomState} currentState The state of the room at the time of the
* newest event in the timeline. Present for backwards compatibility -
* prefer getLiveTimeline().getState(EventTimeline.FORWARDS).
* @prop {RoomSummary} summary The room summary.
* @prop {*} storageToken A token which a data store can use to remember
* the state of the room.
*/
constructor(
public readonly roomId: string,
@@ -231,7 +290,9 @@ export class Room extends EventEmitter {
// all our per-room timeline sets. the first one is the unfiltered ones;
// the subsequent ones are the filtered ones in no particular order.
this.timelineSets = [new EventTimelineSet(this, opts)];
this.reEmitter.reEmit(this.getUnfilteredTimelineSet(), ["Room.timeline", "Room.timelineReset"]);
this.reEmitter.reEmit(this.getUnfilteredTimelineSet(), [
"Room.timeline", "Room.timelineReset",
]);
this.fixUpLegacyTimelineFields();
@@ -240,7 +301,7 @@ export class Room extends EventEmitter {
const serializedPendingEventList = client.sessionStore.store.getItem(pendingEventsKey(this.roomId));
if (serializedPendingEventList) {
JSON.parse(serializedPendingEventList)
.forEach(async serializedEvent => {
.forEach(async (serializedEvent: Partial<IEvent>) => {
const event = new MatrixEvent(serializedEvent);
if (event.getType() === EventType.RoomMessageEncrypted) {
await event.attemptDecryption(this.client.crypto);
@@ -660,7 +721,7 @@ export class Room extends EventEmitter {
const path = utils.encodeUri("/rooms/$roomId/members?" + queryString,
{ $roomId: this.roomId });
const http = this.client.http;
const response = await http.authedRequest(undefined, "GET", path);
const response = await http.authedRequest<{ chunk: IEvent[] }>(undefined, Method.Get, path);
return response.chunk;
}
@@ -778,7 +839,7 @@ export class Room extends EventEmitter {
* timeline which would otherwise be unable to paginate forwards without this token).
* Removing just the old live timeline whilst preserving previous ones is not supported.
*/
public resetLiveTimeline(backPaginationToken: string, forwardPaginationToken: string): void {
public resetLiveTimeline(backPaginationToken: string | null, forwardPaginationToken: string | null): void {
for (let i = 0; i < this.timelineSets.length; i++) {
this.timelineSets[i].resetLiveTimeline(
backPaginationToken, forwardPaginationToken,
@@ -997,14 +1058,14 @@ export class Room extends EventEmitter {
* @return {array} The room's alias as an array of strings
*/
public getAliases(): string[] {
const aliasStrings = [];
const aliasStrings: string[] = [];
const aliasEvents = this.currentState.getStateEvents(EventType.RoomAliases);
if (aliasEvents) {
for (let i = 0; i < aliasEvents.length; ++i) {
const aliasEvent = aliasEvents[i];
if (Array.isArray(aliasEvent.getContent().aliases)) {
const filteredAliases = aliasEvent.getContent().aliases.filter(a => {
const filteredAliases = aliasEvent.getContent<{ aliases: string[] }>().aliases.filter(a => {
if (typeof(a) !== "string") return false;
if (a[0] !== '#') return false;
if (!a.endsWith(`:${aliasEvent.getStateKey()}`)) return false;
@@ -1207,9 +1268,14 @@ export class Room extends EventEmitter {
/**
* Add a timelineSet for this room with the given filter
* @param {Filter} filter The filter to be applied to this timelineSet
* @param {Object=} opts Configuration options
* @param {*} opts.storageToken Optional.
* @return {EventTimelineSet} The timelineSet
*/
public getOrCreateFilteredTimelineSet(filter: Filter): EventTimelineSet {
public getOrCreateFilteredTimelineSet(
filter: Filter,
{ prepopulateTimeline = true }: ICreateFilterOpts = {},
): EventTimelineSet {
if (this.filteredTimelineSets[filter.filterId]) {
return this.filteredTimelineSets[filter.filterId];
}
@@ -1219,30 +1285,39 @@ export class Room extends EventEmitter {
this.filteredTimelineSets[filter.filterId] = timelineSet;
this.timelineSets.push(timelineSet);
// populate up the new timelineSet with filtered events from our live
// unfiltered timeline.
//
// XXX: This is risky as our timeline
// may have grown huge and so take a long time to filter.
// see https://github.com/vector-im/vector-web/issues/2109
const unfilteredLiveTimeline = this.getLiveTimeline();
// Not all filter are possible to replicate client-side only
// When that's the case we do not want to prepopulate from the live timeline
// as we would get incorrect results compared to what the server would send back
if (prepopulateTimeline) {
// populate up the new timelineSet with filtered events from our live
// unfiltered timeline.
//
// XXX: This is risky as our timeline
// may have grown huge and so take a long time to filter.
// see https://github.com/vector-im/vector-web/issues/2109
unfilteredLiveTimeline.getEvents().forEach(function(event) {
timelineSet.addLiveEvent(event);
});
unfilteredLiveTimeline.getEvents().forEach(function(event) {
timelineSet.addLiveEvent(event);
});
// find the earliest unfiltered timeline
let timeline = unfilteredLiveTimeline;
while (timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)) {
timeline = timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS);
// find the earliest unfiltered timeline
let timeline = unfilteredLiveTimeline;
while (timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)) {
timeline = timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS);
}
timelineSet.getLiveTimeline().setPaginationToken(
timeline.getPaginationToken(EventTimeline.BACKWARDS),
EventTimeline.BACKWARDS,
);
} else {
const livePaginationToken = unfilteredLiveTimeline.getPaginationToken(Direction.Forward);
timelineSet
.getLiveTimeline()
.setPaginationToken(livePaginationToken, Direction.Backward);
}
timelineSet.getLiveTimeline().setPaginationToken(
timeline.getPaginationToken(EventTimeline.BACKWARDS),
EventTimeline.BACKWARDS,
);
// alternatively, we could try to do something like this to try and re-paginate
// in the filtered events from nothing, but Mark says it's an abuse of the API
// to do so:
@@ -1268,7 +1343,7 @@ export class Room extends EventEmitter {
}
}
public findThreadForEvent(event: MatrixEvent): Thread {
public findThreadForEvent(event: MatrixEvent): Thread | null {
if (!event) {
return null;
}
@@ -1296,15 +1371,14 @@ export class Room extends EventEmitter {
let rootEvent = this.findEventById(event.threadRootId);
// If the rootEvent does not exist in the current sync, then look for
// it over the network
const eventData = await this.client.fetchRoomEvent(this.roomId, event.threadRootId);
if (!rootEvent) {
const eventData = await this.client.fetchRoomEvent(this.roomId, event.threadRootId);
rootEvent = new MatrixEvent(eventData);
} else {
rootEvent.setUnsigned(eventData.unsigned);
}
events.unshift(rootEvent);
thread = new Thread(events, this, this.client);
this.threads.set(thread.id, thread);
this.reEmitter.reEmit(thread, [ThreadEvent.Update, ThreadEvent.Ready]);
this.emit(ThreadEvent.New, thread);
thread = this.createThread(events);
}
if (event.getUnsigned().transaction_id) {
@@ -1319,6 +1393,19 @@ export class Room extends EventEmitter {
this.emit(ThreadEvent.Update, thread);
}
public createThread(events: MatrixEvent[]): Thread {
const thread = new Thread(events, this, this.client);
this.threads.set(thread.id, thread);
this.reEmitter.reEmit(thread, [
ThreadEvent.Update,
ThreadEvent.Ready,
"Room.timeline",
"Room.timelineReset",
]);
this.emit(ThreadEvent.New, thread);
return thread;
}
/**
* Add an event to the end of this room's live timelines. Will fire
* "Room.timeline".
@@ -1356,6 +1443,15 @@ export class Room extends EventEmitter {
// (in the sender and target fields). We should get those
// RoomMember objects to update themselves when the events that
// they are based on are changed.
// Remove any visibility change on this event.
this.visibilityEvents.delete(redactId);
// If this event is a visibility change event, remove it from the
// list of visibility changes and update any event affected by it.
if (redactedEvent.isVisibilityEvent()) {
this.redactVisibilityChangeEvent(event);
}
}
// FIXME: apply redactions to notification list
@@ -1365,6 +1461,15 @@ export class Room extends EventEmitter {
// this may be needed to trigger an update.
}
// Implement MSC3531: hiding messages.
if (event.isVisibilityEvent()) {
// This event changes the visibility of another event, record
// the visibility change, inform clients if necessary.
this.applyNewVisibilityEvent(event);
}
// If any pending visibility change is waiting for this (older) event,
this.applyPendingVisibilityEvents(event);
if (event.getUnsigned().transaction_id) {
const existingEvent = this.txnToEvent[event.getUnsigned().transaction_id];
if (existingEvent) {
@@ -1431,7 +1536,7 @@ export class Room extends EventEmitter {
EventTimeline.setEventMetadata(event, this.getLiveTimeline().getState(EventTimeline.FORWARDS), false);
this.txnToEvent[txnId] = event;
const thread = this.threads.get(event.threadRootId);
const thread = this.findThreadForEvent(event);
if (this.opts.pendingEventOrdering === PendingEventOrdering.Detached && !thread) {
if (this.pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) {
logger.warn("Setting event as NOT_SENT due to messages in the same state");
@@ -1460,8 +1565,7 @@ export class Room extends EventEmitter {
}
} else {
if (thread) {
thread.timelineSet.addEventToTimeline(event,
thread.timelineSet.getLiveTimeline(), false);
thread.addEvent(event, false);
} else {
for (let i = 0; i < this.timelineSets.length; i++) {
const timelineSet = this.timelineSets[i];
@@ -1586,7 +1690,7 @@ export class Room extends EventEmitter {
// any, which is good, because we don't want to try decoding it again).
localEvent.handleRemoteEcho(remoteEvent.event);
const thread = this.threads.get(remoteEvent.threadRootId);
const thread = this.findThreadForEvent(remoteEvent);
if (thread) {
thread.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
} else {
@@ -1747,7 +1851,7 @@ export class Room extends EventEmitter {
// TODO: We should have a filter to say "only add state event
// types X Y Z to the timeline".
this.addLiveEvent(events[i], duplicateStrategy, fromCache);
const thread = this.threads.get(events[i].getId());
const thread = this.findThreadForEvent(events[i]);
if (thread) {
thread.addEvent(events[i], true);
}
@@ -1852,16 +1956,7 @@ export class Room extends EventEmitter {
});
}
/**
* Get the ID of the event that a given user has read up to, or null if we
* have received no read receipts from them.
* @param {String} userId The user ID to get read receipt event ID for
* @param {Boolean} ignoreSynthesized If true, return only receipts that have been
* sent by the server, not implicit ones generated
* by the JS SDK.
* @return {String} ID of the latest event that the given user has read, or null.
*/
public getEventReadUpTo(userId: string, ignoreSynthesized = false): string | null {
public getReadReceiptForUserId(userId: string, ignoreSynthesized = false): IWrappedReceipt | null {
let receipts = this.receipts;
if (ignoreSynthesized) {
receipts = this.realReceipts;
@@ -1874,7 +1969,21 @@ export class Room extends EventEmitter {
return null;
}
return receipts["m.read"][userId].eventId;
return receipts["m.read"][userId];
}
/**
* Get the ID of the event that a given user has read up to, or null if we
* have received no read receipts from them.
* @param {String} userId The user ID to get read receipt event ID for
* @param {Boolean} ignoreSynthesized If true, return only receipts that have been
* sent by the server, not implicit ones generated
* by the JS SDK.
* @return {String} ID of the latest event that the given user has read, or null.
*/
public getEventReadUpTo(userId: string, ignoreSynthesized = false): string | null {
const readReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized);
return readReceipt?.eventId ?? null;
}
/**
@@ -1987,7 +2096,7 @@ export class Room extends EventEmitter {
* @return {Object} Map of receipts by event ID
*/
private buildReceiptCache(receipts: Receipts): ReceiptCache {
const receiptCacheByEventId = {};
const receiptCacheByEventId: ReceiptCache = {};
Object.keys(receipts).forEach(function(receiptType) {
Object.keys(receipts[receiptType]).forEach(function(userId) {
const receipt = receipts[receiptType][userId];
@@ -2154,15 +2263,7 @@ export class Room extends EventEmitter {
}
}
let alias = this.getCanonicalAlias();
if (!alias) {
const aliases = this.getAltAliases();
if (aliases.length) {
alias = aliases[0];
}
}
const alias = this.getCanonicalAlias();
if (alias) {
return alias;
}
@@ -2180,7 +2281,7 @@ export class Room extends EventEmitter {
}
// get members that are NOT ourselves and are actually in the room.
let otherNames = null;
let otherNames: string[] = null;
if (this.summaryHeroes) {
// if we have a summary, the member state events
// should be in the room state
@@ -2249,6 +2350,161 @@ export class Room extends EventEmitter {
return "Empty room";
}
}
/**
* When we receive a new visibility change event:
*
* - store this visibility change alongside the timeline, in case we
* later need to apply it to an event that we haven't received yet;
* - if we have already received the event whose visibility has changed,
* patch it to reflect the visibility change and inform listeners.
*/
private applyNewVisibilityEvent(event: MatrixEvent): void {
const visibilityChange = event.asVisibilityChange();
if (!visibilityChange) {
// The event is ill-formed.
return;
}
// Ignore visibility change events that are not emitted by moderators.
const userId = event.getSender();
if (!userId) {
return;
}
const isPowerSufficient =
(
EVENT_VISIBILITY_CHANGE_TYPE.name
&& this.currentState.maySendStateEvent(EVENT_VISIBILITY_CHANGE_TYPE.name, userId)
)
|| (
EVENT_VISIBILITY_CHANGE_TYPE.altName
&& this.currentState.maySendStateEvent(EVENT_VISIBILITY_CHANGE_TYPE.altName, userId)
);
if (!isPowerSufficient) {
// Powerlevel is insufficient.
return;
}
// Record this change in visibility.
// If the event is not in our timeline and we only receive it later,
// we may need to apply the visibility change at a later date.
const visibilityEventsOnOriginalEvent = this.visibilityEvents.get(visibilityChange.eventId);
if (visibilityEventsOnOriginalEvent) {
// It would be tempting to simply erase the latest visibility change
// but we need to record all of the changes in case the latest change
// is ever redacted.
//
// In practice, linear scans through `visibilityEvents` should be fast.
// However, to protect against a potential DoS attack, we limit the
// number of iterations in this loop.
let index = visibilityEventsOnOriginalEvent.length - 1;
const min = Math.max(0,
visibilityEventsOnOriginalEvent.length - MAX_NUMBER_OF_VISIBILITY_EVENTS_TO_SCAN_THROUGH);
for (; index >= min; --index) {
const target = visibilityEventsOnOriginalEvent[index];
if (target.getTs() < event.getTs()) {
break;
}
}
if (index === -1) {
visibilityEventsOnOriginalEvent.unshift(event);
} else {
visibilityEventsOnOriginalEvent.splice(index + 1, 0, event);
}
} else {
this.visibilityEvents.set(visibilityChange.eventId, [event]);
}
// Finally, let's check if the event is already in our timeline.
// If so, we need to patch it and inform listeners.
const originalEvent = this.findEventById(visibilityChange.eventId);
if (!originalEvent) {
return;
}
originalEvent.applyVisibilityEvent(visibilityChange);
}
private redactVisibilityChangeEvent(event: MatrixEvent) {
// Sanity checks.
if (!event.isVisibilityEvent) {
throw new Error("expected a visibility change event");
}
const relation = event.getRelation();
const originalEventId = relation.event_id;
const visibilityEventsOnOriginalEvent = this.visibilityEvents.get(originalEventId);
if (!visibilityEventsOnOriginalEvent) {
// No visibility changes on the original event.
// In particular, this change event was not recorded,
// most likely because it was ill-formed.
return;
}
const index = visibilityEventsOnOriginalEvent.findIndex(change => change.getId() === event.getId());
if (index === -1) {
// This change event was not recorded, most likely because
// it was ill-formed.
return;
}
// Remove visibility change.
visibilityEventsOnOriginalEvent.splice(index, 1);
// If we removed the latest visibility change event, propagate changes.
if (index === visibilityEventsOnOriginalEvent.length) {
const originalEvent = this.findEventById(originalEventId);
if (!originalEvent) {
return;
}
if (index === 0) {
// We have just removed the only visibility change event.
this.visibilityEvents.delete(originalEventId);
originalEvent.applyVisibilityEvent();
} else {
const newEvent = visibilityEventsOnOriginalEvent[visibilityEventsOnOriginalEvent.length - 1];
const newVisibility = newEvent.asVisibilityChange();
if (!newVisibility) {
// Event is ill-formed.
// This breaks our invariant.
throw new Error("at this stage, visibility changes should be well-formed");
}
originalEvent.applyVisibilityEvent(newVisibility);
}
}
}
/**
* When we receive an event whose visibility has been altered by
* a (more recent) visibility change event, patch the event in
* place so that clients now not to display it.
*
* @param event Any matrix event. If this event has at least one a
* pending visibility change event, apply the latest visibility
* change event.
*/
private applyPendingVisibilityEvents(event: MatrixEvent): void {
const visibilityEvents = this.visibilityEvents.get(event.getId());
if (!visibilityEvents || visibilityEvents.length == 0) {
// No pending visibility change in store.
return;
}
const visibilityEvent = visibilityEvents[visibilityEvents.length - 1];
const visibilityChange = visibilityEvent.asVisibilityChange();
if (!visibilityChange) {
return;
}
if (visibilityChange.visible) {
// Events are visible by default, no need to apply a visibility change.
// Note that we need to keep the visibility changes in `visibilityEvents`,
// in case we later fetch an older visibility change event that is superseded
// by `visibilityChange`.
}
if (visibilityEvent.getTs() < event.getTs()) {
// Something is wrong, the visibility change cannot happen before the
// event. Presumably an ill-formed event.
return;
}
event.applyVisibilityEvent(visibilityChange);
}
}
/**
@@ -2259,33 +2515,31 @@ function pendingEventsKey(roomId: string): string {
return `mx_pending_events_${roomId}`;
}
/* a map from current event status to a list of allowed next statuses
*/
const ALLOWED_TRANSITIONS = {};
ALLOWED_TRANSITIONS[EventStatus.ENCRYPTING] = [
EventStatus.SENDING,
EventStatus.NOT_SENT,
];
ALLOWED_TRANSITIONS[EventStatus.SENDING] = [
EventStatus.ENCRYPTING,
EventStatus.QUEUED,
EventStatus.NOT_SENT,
EventStatus.SENT,
];
ALLOWED_TRANSITIONS[EventStatus.QUEUED] =
[EventStatus.SENDING, EventStatus.CANCELLED];
ALLOWED_TRANSITIONS[EventStatus.SENT] =
[];
ALLOWED_TRANSITIONS[EventStatus.NOT_SENT] =
[EventStatus.SENDING, EventStatus.QUEUED, EventStatus.CANCELLED];
ALLOWED_TRANSITIONS[EventStatus.CANCELLED] =
[];
// a map from current event status to a list of allowed next statuses
const ALLOWED_TRANSITIONS: Record<EventStatus, EventStatus[]> = {
[EventStatus.ENCRYPTING]: [
EventStatus.SENDING,
EventStatus.NOT_SENT,
EventStatus.CANCELLED,
],
[EventStatus.SENDING]: [
EventStatus.ENCRYPTING,
EventStatus.QUEUED,
EventStatus.NOT_SENT,
EventStatus.SENT,
],
[EventStatus.QUEUED]: [
EventStatus.SENDING,
EventStatus.CANCELLED,
],
[EventStatus.SENT]: [],
[EventStatus.NOT_SENT]: [
EventStatus.SENDING,
EventStatus.QUEUED,
EventStatus.CANCELLED,
],
[EventStatus.CANCELLED]: [],
};
// TODO i18n
function memberNamesToRoomName(names: string[], count = (names.length + 1)) {
@@ -2419,3 +2673,4 @@ function memberNamesToRoomName(names: string[], count = (names.length + 1)) {
* @param {string} membership The new membership value
* @param {string} prevMembership The previous membership value
*/
+72 -11
View File
@@ -15,7 +15,9 @@ limitations under the License.
*/
import { MatrixClient } from "../matrix";
import { MatrixEvent } from "./event";
import { ReEmitter } from "../ReEmitter";
import { RelationType } from "../@types/event";
import { MatrixEvent, IThreadBundledRelationship } from "./event";
import { EventTimeline } from "./event-timeline";
import { EventTimelineSet } from './event-timeline-set';
import { Room } from './room';
@@ -25,6 +27,8 @@ export enum ThreadEvent {
New = "Thread.new",
Ready = "Thread.ready",
Update = "Thread.update",
NewReply = "Thread.newReply",
ViewThread = "Thred.viewThread",
}
/**
@@ -42,6 +46,11 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
private _currentUserParticipated = false;
private reEmitter: ReEmitter;
private lastEvent: MatrixEvent;
private replyCount = 0;
constructor(
events: MatrixEvent[] = [],
public readonly room: Room,
@@ -52,17 +61,30 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
throw new Error("Can't create an empty thread");
}
this.reEmitter = new ReEmitter(this);
this.timelineSet = new EventTimelineSet(this.room, {
unstableClientRelationAggregation: true,
timelineSupport: true,
pendingEvents: true,
});
this.reEmitter.reEmit(this.timelineSet, [
"Room.timeline",
"Room.timelineReset",
]);
events.forEach(event => this.addEvent(event));
room.on("Room.localEchoUpdated", this.onEcho);
room.on("Room.timeline", this.onEcho);
}
public get hasServerSideSupport(): boolean {
return this.client.cachedCapabilities
?.capabilities?.[RelationType.Thread]?.enabled;
}
onEcho = (event: MatrixEvent) => {
if (this.timelineSet.eventIdToTimeline(event.getId())) {
this.emit(ThreadEvent.Update, this);
@@ -76,7 +98,7 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
* @param event The event to add
*/
public async addEvent(event: MatrixEvent, toStartOfTimeline = false): Promise<void> {
if (this.timelineSet.findEventById(event.getId()) || event.status !== null) {
if (this.timelineSet.findEventById(event.getId())) {
return;
}
@@ -108,6 +130,45 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
}
await this.client.decryptEventIfNeeded(event, {});
const isThreadReply = event.getRelation()?.rel_type === RelationType.Thread;
// If no thread support exists we want to count all thread relation
// added as a reply. We can't rely on the bundled relationships count
if (!this.hasServerSideSupport && isThreadReply) {
this.replyCount++;
}
if (!this.lastEvent || (isThreadReply && event.getTs() > this.lastEvent.getTs())) {
this.lastEvent = event;
if (this.lastEvent.getId() !== this.root) {
// This counting only works when server side support is enabled
// as we started the counting from the value returned in the
// bundled relationship
if (this.hasServerSideSupport) {
this.replyCount++;
}
this.emit(ThreadEvent.NewReply, this, event);
}
}
if (event.getId() === this.root) {
const bundledRelationship = event
.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
if (this.hasServerSideSupport && bundledRelationship) {
this.replyCount = bundledRelationship.count;
this._currentUserParticipated = bundledRelationship.current_user_participated;
const lastReply = this.findEventById(bundledRelationship.latest_event.event_id);
if (lastReply) {
this.lastEvent = lastReply;
} else {
const event = new MatrixEvent(bundledRelationship.latest_event);
this.lastEvent = event;
}
}
}
this.emit(ThreadEvent.Update, this);
}
@@ -121,10 +182,13 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
/**
* Return last reply to the thread
*/
public get lastReply(): MatrixEvent {
const threadReplies = this.events
.filter(event => event.isThreadRelation);
return threadReplies[threadReplies.length - 1];
public lastReply(matches: (ev: MatrixEvent) => boolean = () => true): MatrixEvent {
for (let i = this.events.length - 1; i >= 0; i--) {
const event = this.events[i];
if (event.isThreadRelation && matches(event)) {
return event;
}
}
}
/**
@@ -151,17 +215,14 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
* exclude annotations from that number
*/
public get length(): number {
return this.events
.filter(event => event.isThreadRelation)
.length;
return this.replyCount;
}
/**
* A getter for the last event added to the thread
*/
public get replyToEvent(): MatrixEvent {
const events = this.events;
return events[events.length -1];
return this.lastEvent;
}
public get events(): MatrixEvent[] {
+1 -1
View File
@@ -112,7 +112,7 @@ export class RoomHierarchy {
if (!this.backRefs.has(childRoomId)) {
this.backRefs.set(childRoomId, []);
}
this.backRefs.get(childRoomId).push(ev.room_id);
this.backRefs.get(childRoomId).push(room.room_id);
// fill viaMap
if (Array.isArray(ev.content.via)) {
+2 -2
View File
@@ -70,7 +70,7 @@ export class MatrixScheduler<T = ISendEventResponse> {
}
// we ship with browser-request which returns { cors: rejected } when trying
// with no connection, so if we match that, give up since they have no conn.
if (err.cors === "rejected") {
if (err["cors"] === "rejected") {
return -1;
}
@@ -284,7 +284,7 @@ export class MatrixScheduler<T = ISendEventResponse> {
}
}
function debuglog(...args) {
function debuglog(...args: any[]) {
if (DEBUG) {
logger.log(...args);
}
+9 -9
View File
@@ -51,14 +51,14 @@ export interface IStore {
* Set the sync token.
* @param {string} token
*/
setSyncToken(token: string);
setSyncToken(token: string): void;
/**
* No-op.
* @param {Group} group
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
storeGroup(group: Group);
storeGroup(group: Group): void;
/**
* No-op.
@@ -79,7 +79,7 @@ export interface IStore {
* No-op.
* @param {Room} room
*/
storeRoom(room: Room);
storeRoom(room: Room): void;
/**
* No-op.
@@ -98,7 +98,7 @@ export interface IStore {
* Permanently delete a room.
* @param {string} roomId
*/
removeRoom(roomId: string);
removeRoom(roomId: string): void;
/**
* No-op.
@@ -110,7 +110,7 @@ export interface IStore {
* No-op.
* @param {User} user
*/
storeUser(user: User);
storeUser(user: User): void;
/**
* No-op.
@@ -140,13 +140,13 @@ export interface IStore {
* @param {string} token The token associated with these events.
* @param {boolean} toStart True if these are paginated results.
*/
storeEvents(room: Room, events: MatrixEvent[], token: string, toStart: boolean);
storeEvents(room: Room, events: MatrixEvent[], token: string, toStart: boolean): void;
/**
* Store a filter.
* @param {Filter} filter
*/
storeFilter(filter: Filter);
storeFilter(filter: Filter): void;
/**
* Retrieve a filter.
@@ -168,13 +168,13 @@ export interface IStore {
* @param {string} filterName
* @param {string} filterId
*/
setFilterIdByName(filterName: string, filterId: string);
setFilterIdByName(filterName: string, filterId: string): void;
/**
* Store user-scoped account data events
* @param {Array<MatrixEvent>} events The events to store.
*/
storeAccountDataEvents(events: MatrixEvent[]);
storeAccountDataEvents(events: MatrixEvent[]): void;
/**
* Get account data event by event type
+3 -3
View File
@@ -66,7 +66,7 @@ function selectQuery<T>(
): Promise<T[]> {
const query = store.openCursor(keyRange);
return new Promise((resolve, reject) => {
const results = [];
const results: T[] = [];
query.onerror = () => {
reject(new Error("Query failed: " + query.error));
};
@@ -231,14 +231,14 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
* @returns {null} in case the members for this room haven't been stored yet
*/
public getOutOfBandMembers(roomId: string): Promise<IEvent[] | null> {
return new Promise<IEvent[] | null>((resolve, reject) =>{
return new Promise<IEvent[] | null>((resolve, reject) => {
const tx = this.db.transaction(["oob_membership_events"], "readonly");
const store = tx.objectStore("oob_membership_events");
const roomIndex = store.index("room");
const range = IDBKeyRange.only(roomId);
const request = roomIndex.openCursor(range);
const membershipEvents = [];
const membershipEvents: IEvent[] = [];
// did we encounter the oob_written marker object
// amongst the results? That means OOB member
// loading already happened for this room
+2 -2
View File
@@ -256,8 +256,8 @@ function removeByPrefix(store, prefix) {
}
}
function debuglog() {
function debuglog(...args) {
if (DEBUG) {
logger.log(...arguments);
logger.log(...args);
}
}
+1
View File
@@ -32,6 +32,7 @@ interface IOpts {
export interface IMinimalEvent {
content: IContent;
type: EventType | string;
unsigned?: IUnsigned;
}
export interface IEphemeral {
+45 -26
View File
@@ -34,23 +34,22 @@ import { PushProcessor } from "./pushprocessor";
import { logger } from './logger';
import { InvalidStoreError } from './errors';
import { IStoredClientOpts, MatrixClient, PendingEventOrdering } from "./client";
import { SyncState } from "./sync.api";
import {
Category,
IEphemeral,
IInvitedRoom,
IInviteState,
IJoinedRoom,
ILeftRoom,
IStateEvent,
IMinimalEvent,
IRoomEvent,
IStateEvent,
IStrippedState,
ISyncResponse,
ITimeline,
IEphemeral,
IMinimalEvent,
} from "./sync-accumulator";
import { MatrixEvent } from "./models/event";
import { MatrixError } from "./http-api";
import { MatrixError, Method } from "./http-api";
import { ISavedSync } from "./store";
import { EventType } from "./@types/event";
import { IPushRules } from "./@types/PushRules";
@@ -68,6 +67,15 @@ const BUFFER_PERIOD_MS = 80 * 1000;
// keepAlive is successful but the server /sync fails.
const FAILED_SYNC_ERROR_THRESHOLD = 3;
export enum SyncState {
Error = "ERROR",
Prepared = "PREPARED",
Stopped = "STOPPED",
Syncing = "SYNCING",
Catchup = "CATCHUP",
Reconnecting = "RECONNECTING",
}
function getFilterName(userId: string, suffix?: string): string {
// scope this on the user ID because people may login on many accounts
// and they all need to be stored!
@@ -87,13 +95,19 @@ interface ISyncOptions {
}
export interface ISyncStateData {
error?: Error;
error?: MatrixError;
oldSyncToken?: string;
nextSyncToken?: string;
catchingUp?: boolean;
fromCache?: boolean;
}
enum SetPresence {
Offline = "offline",
Online = "online",
Unavailable = "unavailable",
}
interface ISyncParams {
filter?: string;
timeout: number;
@@ -101,7 +115,7 @@ interface ISyncParams {
// eslint-disable-next-line camelcase
full_state?: boolean;
// eslint-disable-next-line camelcase
set_presence?: "offline" | "online" | "unavailable";
set_presence?: SetPresence;
_cacheBuster?: string | number; // not part of the API itself
}
@@ -187,6 +201,7 @@ export class SyncApi {
"Room.accountData",
"Room.myMembership",
"Room.replaceEvent",
"Room.visibilityChange",
]);
this.registerStateListeners(room);
return room;
@@ -260,16 +275,16 @@ export class SyncApi {
getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter,
).then(function(filterId) {
qps.filter = filterId;
return client.http.authedRequest(
undefined, "GET", "/sync", qps, undefined, localTimeoutMs,
return client.http.authedRequest<any>( // TODO types
undefined, Method.Get, "/sync", qps as any, undefined, localTimeoutMs,
);
}).then((data) => {
let leaveRooms = [];
if (data.rooms && data.rooms.leave) {
if (data.rooms?.leave) {
leaveRooms = this.mapSyncResponseToRoomArray(data.rooms.leave);
}
const rooms = [];
leaveRooms.forEach((leaveObj) => {
leaveRooms.forEach(async (leaveObj) => {
const room = leaveObj.room;
rooms.push(room);
if (!leaveObj.isBrandNewRoom) {
@@ -295,7 +310,7 @@ export class SyncApi {
EventTimeline.BACKWARDS);
this.processRoomEvents(room, stateEvents, timelineEvents);
this.processThreadEvents(room, threadedEvents);
await this.processThreadEvents(room, threadedEvents);
room.recalculate();
client.store.storeRoom(room);
@@ -400,9 +415,10 @@ export class SyncApi {
}
// FIXME: gut wrenching; hard-coded timeout values
this.client.http.authedRequest(undefined, "GET", "/events", {
// TODO types
this.client.http.authedRequest<any>(undefined, Method.Get, "/events", {
room_id: peekRoom.roomId,
timeout: 30 * 1000,
timeout: String(30 * 1000),
from: token,
}, undefined, 50 * 1000).then((res) => {
if (this._peekRoom !== peekRoom) {
@@ -470,7 +486,7 @@ export class SyncApi {
return this.syncStateData;
}
public async recoverFromSyncStartupError(savedSyncPromise: Promise<void>, err: Error): Promise<void> {
public async recoverFromSyncStartupError(savedSyncPromise: Promise<void>, err: MatrixError): Promise<void> {
// Wait for the saved sync to complete - we send the pushrules and filter requests
// before the saved sync has finished so they can run in parallel, but only process
// the results after the saved sync is done. Equivalently, we wait for it to finish
@@ -865,8 +881,8 @@ export class SyncApi {
private doSyncRequest(syncOptions: ISyncOptions, syncToken: string): IRequestPromise<ISyncResponse> {
const qps = this.getSyncParams(syncOptions, syncToken);
return this.client.http.authedRequest(
undefined, "GET", "/sync", qps, undefined,
return this.client.http.authedRequest( // TODO types
undefined, Method.Get, "/sync", qps as any, undefined,
qps.timeout + BUFFER_PERIOD_MS,
);
}
@@ -901,7 +917,7 @@ export class SyncApi {
};
if (this.opts.disablePresence) {
qps.set_presence = "offline";
qps.set_presence = SetPresence.Offline;
}
if (syncToken) {
@@ -924,7 +940,7 @@ export class SyncApi {
return qps;
}
private onSyncError(err: Error, syncOptions: ISyncOptions): void {
private onSyncError(err: MatrixError, syncOptions: ISyncOptions): void {
if (!this.running) {
debuglog("Sync no longer running: exiting");
if (this.connectionReturnedDefer) {
@@ -1291,7 +1307,7 @@ export class SyncApi {
const [timelineEvents, threadedEvents] = this.client.partitionThreadedEvents(events);
this.processRoomEvents(room, stateEvents, timelineEvents, syncEventData.fromCache);
this.processThreadEvents(room, threadedEvents);
await this.processThreadEvents(room, threadedEvents);
// set summary after processing events,
// because it will trigger a name calculation
@@ -1350,7 +1366,7 @@ export class SyncApi {
});
// Handle leaves (e.g. kicked rooms)
leaveRooms.forEach((leaveObj) => {
leaveRooms.forEach(async (leaveObj) => {
const room = leaveObj.room;
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
@@ -1359,7 +1375,7 @@ export class SyncApi {
const [timelineEvents, threadedEvents] = this.client.partitionThreadedEvents(events);
this.processRoomEvents(room, stateEvents, timelineEvents);
this.processThreadEvents(room, threadedEvents);
await this.processThreadEvents(room, threadedEvents);
room.addAccountData(accountDataEvents);
room.recalculate();
@@ -1416,11 +1432,14 @@ export class SyncApi {
const currentCount = data.device_one_time_keys_count.signed_curve25519 || 0;
this.opts.crypto.updateOneTimeKeyCount(currentCount);
}
if (this.opts.crypto && data["org.matrix.msc2732.device_unused_fallback_key_types"]) {
if (this.opts.crypto &&
(data["device_unused_fallback_key_types"] ||
data["org.matrix.msc2732.device_unused_fallback_key_types"])) {
// The presence of device_unused_fallback_key_types indicates that the
// server supports fallback keys. If there's no unused
// signed_curve25519 fallback key we need a new one.
const unusedFallbackKeys = data["org.matrix.msc2732.device_unused_fallback_key_types"];
const unusedFallbackKeys = data["device_unused_fallback_key_types"] ||
data["org.matrix.msc2732.device_unused_fallback_key_types"];
this.opts.crypto.setNeedsNewFallback(
unusedFallbackKeys instanceof Array &&
!unusedFallbackKeys.includes("signed_curve25519"),
@@ -1474,7 +1493,7 @@ export class SyncApi {
this.client.http.request(
undefined, // callback
"GET", "/_matrix/client/versions",
Method.Get, "/_matrix/client/versions",
undefined, // queryParams
undefined, // data
{
@@ -1701,7 +1720,7 @@ export class SyncApi {
/**
* @experimental
*/
private processThreadEvents(room: Room, threadedEvents: MatrixEvent[]): void {
private processThreadEvents(room: Room, threadedEvents: MatrixEvent[]): Promise<void> {
return this.client.processThreadEvents(room, threadedEvents);
}
+14 -34
View File
@@ -22,16 +22,24 @@ limitations under the License.
import unhomoglyph from "unhomoglyph";
import promiseRetry from "p-retry";
import type NodeCrypto from "crypto";
/**
* Encode a dictionary of query parameters.
* Omits any undefined/null values.
* @param {Object} params A dict of key/values to encode e.g.
* {"foo": "bar", "baz": "taz"}
* @return {string} The encoded string e.g. foo=bar&baz=taz
*/
export function encodeParams(params: Record<string, string>): string {
return new URLSearchParams(params).toString();
export function encodeParams(params: Record<string, string | number | boolean>): string {
const searchParams = new URLSearchParams();
for (const [key, val] of Object.entries(params)) {
if (val !== undefined && val !== null) {
searchParams.set(key, String(val));
}
}
return searchParams.toString();
}
export type QueryDict = Record<string, string | string[]>;
@@ -90,23 +98,20 @@ export function removeElement<T>(
array: T[],
fn: (t: T, i?: number, a?: T[]) => boolean,
reverse?: boolean,
) {
let i;
let removed;
): boolean {
let i: number;
if (reverse) {
for (i = array.length - 1; i >= 0; i--) {
if (fn(array[i], i, array)) {
removed = array[i];
array.splice(i, 1);
return removed;
return true;
}
}
} else {
for (i = 0; i < array.length; i++) {
if (fn(array[i], i, array)) {
removed = array[i];
array.splice(i, 1);
return removed;
return true;
}
}
}
@@ -276,31 +281,6 @@ export function deepSortedObjectEntries(obj: any): [string, any][] {
return pairs;
}
/**
* Copy properties from one object to another.
*
* All enumerable properties, included inherited ones, are copied.
*
* This is approximately equivalent to ES6's Object.assign, except
* that the latter doesn't copy inherited properties.
*
* @param {Object} target The object that will receive new properties
* @param {...Object} source Objects from which to copy properties
*
* @return {Object} target
*/
export function extend(...restParams) {
const target = restParams[0] || {};
for (let i = 1; i < restParams.length; i++) {
const source = restParams[i];
if (!source) continue;
for (const propName in source) { // eslint-disable-line guard-for-in
target[propName] = source[propName];
}
}
return target;
}
/**
* Inherit the prototype methods from one constructor into another. This is a
* port of the Node.js implementation with an Object.create polyfill.
+8 -2
View File
@@ -21,8 +21,9 @@ limitations under the License.
* @module webrtc/call
*/
import { logger } from '../logger';
import { EventEmitter } from 'events';
import { logger } from '../logger';
import * as utils from '../utils';
import { MatrixEvent } from '../models/event';
import { EventType } from '../@types/event';
@@ -1716,7 +1717,12 @@ export class MatrixCall extends EventEmitter {
const stream = ev.streams[0];
this.pushRemoteFeed(stream);
stream.addEventListener("removetrack", () => this.deleteFeedByStream(stream));
stream.addEventListener("removetrack", () => {
if (stream.getTracks().length === 0) {
logger.info(`Stream ID ${stream.id} has no tracks remaining - removing`);
this.deleteFeedByStream(stream);
}
});
};
private onDataChannel = (ev: RTCDataChannelEvent): void => {
+1
View File
@@ -15,6 +15,7 @@ limitations under the License.
*/
import EventEmitter from "events";
import { SDPStreamMetadataPurpose } from "./callEventTypes";
import { MatrixClient } from "../client";
import { RoomMember } from "../models/room-member";
+1112 -921
View File
File diff suppressed because it is too large Load Diff