Compare commits

...

40 Commits

Author SHA1 Message Date
RiotRobot e4937e6222 v32.0.0 2024-04-09 10:06:41 +00:00
RiotRobot 7d5c003716 v32.0.0-rc.0 2024-04-02 16:15:04 +00:00
Richard van der Hoff dbab185f9d Refactoring and simplification in decryption error handling (#4138)
* Clean up decryption failure integ tests

* Fix the names
* Stop waiting as soon as the event is decrypted, even if code is wrong (so
  tests fail rather than time out if the code is wrong)

* Bump timeouts on some tests

These tend to fail due to slow init of wasm artifacts

* Factor out `onDecryptionKeyMissingError` call

* Factor out `onMegolmDecryptionError`
2024-04-02 13:39:49 +00:00
Richard van der Hoff cfcd191cbf Update matrix-rust-sdk-crypto-wasm to 4.9.0 (#4137) 2024-04-02 13:09:59 +00:00
RiotRobot 514633c5fa Merge branch 'master' into develop 2024-03-28 16:45:10 +00:00
RiotRobot 5bffb7df4f v31.6.1 2024-03-28 16:44:30 +00:00
David Baker 9e1897dcd0 Merge pull request #4136 from matrix-org/backport-4135-to-staging
Fix merging of default push rules (backport)
2024-03-28 16:35:36 +00:00
David Baker 5f3ddc37a1 Merge pull request #4135 from matrix-org/t3chguy/fix/27173
Fix merging of default push rules

(cherry picked from commit 78a225795b)
2024-03-28 16:28:33 +00:00
David Baker 78a225795b Merge pull request #4135 from matrix-org/t3chguy/fix/27173
Fix merging of default push rules
2024-03-28 16:23:58 +00:00
Michael Telatynski 467b49a0dc Add test
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-28 15:52:12 +00:00
Michael Telatynski 06e083874a Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-28 14:59:52 +00:00
Michael Telatynski 0f25429849 Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-28 13:41:31 +00:00
Michael Telatynski 32ddf2813d Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-28 12:55:44 +00:00
Michael Telatynski 1ed082f3d4 Fix merging of default push rules
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-28 11:58:52 +00:00
RiotRobot 706002cdcb Resetting package fields for development 2024-03-26 16:26:09 +00:00
RiotRobot 731de1108c Merge branch 'master' into develop 2024-03-26 16:26:07 +00:00
RiotRobot 2da6c0c605 v31.6.0 2024-03-26 16:25:15 +00:00
Richard van der Hoff 9f1d0c3896 Add new decryptExistingEvent test helper (#4133)
* grammar fix

* IEncryptionResult -> EncryptionResult

These are the same thing; the former is the old name.

* Support setting event IDs

* Helper for decrypting existing decryption failures
2024-03-25 14:10:58 +00:00
Gabri 0b290fffa1 Improve types for IPowerLevelsContent and hasSufficientPowerLevelFor (#4128)
Signed-off-by: Gabriele Messina <56839513+galash13@users.noreply.github.com>
2024-03-25 13:58:38 +00:00
Michael Telatynski 97844f0e47 Improve types for sendEvent (#4108) 2024-03-25 12:48:49 +00:00
Michael Telatynski 85a55c79cd Remove various deprecated methods & re-exports (#4125) 2024-03-25 12:21:11 +00:00
Johannes Marbach 63d4195453 Use RoomEvent.MyMembership in auto-join example (#4130)
This seems easier / more efficient than listening to all membership events and then filtering by user ID.
2024-03-25 09:10:42 +00:00
Richard van der Hoff d5a35f8a99 Add new enum for verification methods. (#4129)
* Define constants for the verification methods.

* Remove some confusing references to the *old* `VerificationMethod`
2024-03-22 17:17:31 +00:00
Richard van der Hoff d1259b241c Clean up code for handling decryption failures (#4126)
Various improvements, including:

* Defining an enum for decryption failure reasons
* Exposing the reason code as a property on Event
2024-03-22 17:15:27 +00:00
David Langley a573727662 Remove the logic that throws when the lazy loading options has changed. (#4124)
* remove InvalidStoreState and the logic that checks for the change in the lazyLoading client option

* lint
2024-03-22 16:36:23 +00:00
Richard van der Hoff dce8acbf17 Add some test utils in a new entrypoint (#4127)
* Clean up README a little

This just removes some of the most egregious lies and outdated stuff. There's a
*lot* more that can be done here.

* Add some test utils in a new entrypoint

* Fix comment

* Update src/testing.ts
2024-03-22 14:10:55 +00:00
David Baker 4ba1341f8f Fix highlights from threads disappearing on new messages (#4106)
* Fix highlights from threads disappearing on new messages

This changes interface of Room, so this is a BREAKING CHANGE.

Correctly mirrors the logic we use for room notifications for thread
notifications, ie. set only the total notifications count from the
server if it's zero.

I'm not delighted with this since it ends up with function on room
whose contract is to do something frankly, deeply weird and
unintuitive. However, this is the hack we use for room notifications
and it, empirically, works well enough. To do better, we'd need much
more complex logic to overlay notification counts for decrypted messages.

Fixes https://github.com/element-hq/element-web/issues/25523

* Add tests for the special notification behaviour in syncing

* Correctly copy the room logic for reseting notifications

We were always ignoring the highlight count, even for encrypted rooms,
which was broken because we don't do the local calculation for unencrypted
rooms.
2024-03-21 16:29:00 +00:00
David Baker e517d009bf Extend logic for local notification processing to threads (#4111)
* Move code for processing our own receipts to Room

This is some code to process our own receipts and recalculate our
notification counts.

There was no reason for this to be in client. Room is still rather
large, but at least it makes somewhat more sense there.

Moving as a refactor before I start work on it.

* Add test for the client-side e2e notifications code

* Extend logic for local notification processing to threads

There's collection of logic for for processing receipts and recomputing
notifications for encrypted rooms, but we didn't do the same for threads.
As a reasult, when I tried pulling some of the logic over in
https://github.com/matrix-org/matrix-js-sdk/pull/4106
clearing notifications on threads just broke.

This extends the logic of reprocessing local notifications when a receipt
arrives to threads.

Based on https://github.com/matrix-org/matrix-js-sdk/pull/4109

* simplify object literal

* Add tests & null guard

* Remove unused imports

* Add another skipped test

* Unused import

* enable tests

* Fix thread support nightmare

* Try this way

* Unused import

* Comment the bear trap

* expand comment
2024-03-21 12:22:19 +00:00
Ajay Bura dc2d03dea5 fix public rooms post request search params and body (#4110) 2024-03-21 10:29:51 +00:00
David Baker d5bb9e7600 Move code for processing our own receipts to Room (#4109)
* Move code for processing our own receipts to Room

This is some code to process our own receipts and recalculate our
notification counts.

There was no reason for this to be in client. Room is still rather
large, but at least it makes somewhat more sense there.

Moving as a refactor before I start work on it.

* Add test for the client-side e2e notifications code

* simplify object literal
2024-03-20 15:20:47 +00:00
Michael Telatynski d908036f50 Improve types for sendStateEvent (#4105) 2024-03-20 14:27:27 +00:00
David Baker afc3c6213b Fix bugs with the first reply to a thread (#4104)
* WIP fix for bugs first-thread-reply bugs

* Add re-emitter before we start adding events, as per comment

* Add test for notification bug

* Test for the bug that caused the dot to be the wrong colour

* Add comment

* elaborate

* Fix outdated comment

* Also fix this comment

* Fix another comment

* Fix typo

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Clarify comment

* More comment

* so much comment

also reformat (the bit that's actually added is s/it/this.addEvents/)

* The comments

* Maybe make comment clearer.

* Add comment about potential race

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-03-20 11:14:25 +00:00
Michael Telatynski 7884c22e41 Fix permissions for deploying docs to github pages (#4122)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-19 17:57:33 +00:00
renovate[bot] 887d8a7663 Update dependency typescript to v5.4.2 (#4123)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-19 17:25:31 +00:00
renovate[bot] 2c68ee2254 Update babel monorepo to v7.24.1 (#4119)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-19 16:33:19 +00:00
renovate[bot] d445823d0b Update typescript-eslint monorepo to v7.2.0 (#4121)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-19 16:25:50 +00:00
renovate[bot] abe4630687 Update typedoc (#4118)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-19 16:25:07 +00:00
renovate[bot] 8664b66238 Update dependency @matrix-org/matrix-sdk-crypto-wasm to v4.7.0 (#4120)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-19 16:24:49 +00:00
renovate[bot] 596826ab4d Update all non-major dependencies (#4116)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-19 16:22:03 +00:00
renovate[bot] c8ec5421c7 Update dependency @types/node to v18.19.24 (#4117)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-19 16:21:55 +00:00
60 changed files with 3062 additions and 1425 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ concurrency:
jobs:
build-element-web:
name: Build element-web
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.93.0
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.95.0
with:
matrix-js-sdk-sha: ${{ github.sha }}
react-sdk-repository: matrix-org/matrix-react-sdk
+5
View File
@@ -61,6 +61,11 @@ jobs:
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: docs
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
steps:
- name: Deploy to GitHub Pages
id: deployment
+50
View File
@@ -1,3 +1,53 @@
Changes in [32.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.0.0) (2024-04-09)
==================================================================================================
## 🚨 BREAKING CHANGES
* Remove various deprecated methods \& re-exports ([#4125](https://github.com/matrix-org/matrix-js-sdk/pull/4125)). Contributed by @t3chguy.
* Remove the logic that throws when the lazy loading options has changed. ([#4124](https://github.com/matrix-org/matrix-js-sdk/pull/4124)). Contributed by @langleyd.
* Fix highlights from threads disappearing on new messages ([#4106](https://github.com/matrix-org/matrix-js-sdk/pull/4106)). Contributed by @dbkr.
## ✨ Features
* Add new `decryptExistingEvent` test helper ([#4133](https://github.com/matrix-org/matrix-js-sdk/pull/4133)). Contributed by @richvdh.
* Improve types for `sendEvent` ([#4108](https://github.com/matrix-org/matrix-js-sdk/pull/4108)). Contributed by @t3chguy.
* Remove various deprecated methods \& re-exports ([#4125](https://github.com/matrix-org/matrix-js-sdk/pull/4125)). Contributed by @t3chguy.
* Add new enum for verification methods. ([#4129](https://github.com/matrix-org/matrix-js-sdk/pull/4129)). Contributed by @richvdh.
* Add some test utils in a new entrypoint ([#4127](https://github.com/matrix-org/matrix-js-sdk/pull/4127)). Contributed by @richvdh.
* Improve types for `sendStateEvent` ([#4105](https://github.com/matrix-org/matrix-js-sdk/pull/4105)). Contributed by @t3chguy.
## 🐛 Bug Fixes
* Improve types for `IPowerLevelsContent` and `hasSufficientPowerLevelFor` ([#4128](https://github.com/matrix-org/matrix-js-sdk/pull/4128)). Contributed by @galash13.
* Remove the logic that throws when the lazy loading options has changed. ([#4124](https://github.com/matrix-org/matrix-js-sdk/pull/4124)). Contributed by @langleyd.
* Fix highlights from threads disappearing on new messages ([#4106](https://github.com/matrix-org/matrix-js-sdk/pull/4106)). Contributed by @dbkr.
* Extend logic for local notification processing to threads ([#4111](https://github.com/matrix-org/matrix-js-sdk/pull/4111)). Contributed by @dbkr.
* Fix public rooms post request search params and body ([#4110](https://github.com/matrix-org/matrix-js-sdk/pull/4110)). Contributed by @ajbura.
* Fix bugs with the first reply to a thread ([#4104](https://github.com/matrix-org/matrix-js-sdk/pull/4104)). Contributed by @dbkr.
Changes in [31.6.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.6.1) (2024-03-28)
==================================================================================================
## 🐛 Bug Fixes
* Fix merging of default push rules ([#4136](https://github.com/matrix-org/matrix-js-sdk/pull/4136)).
Changes in [31.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.6.0) (2024-03-26)
==================================================================================================
## ✨ Features
* Introduce Membership TS type (take 2) ([#4107](https://github.com/matrix-org/matrix-js-sdk/pull/4107)). Contributed by @andybalaam.
* fix automatic DM avatar with functional members ([#4017](https://github.com/matrix-org/matrix-js-sdk/pull/4017)). Contributed by @HarHarLinks.
* Export types describing all specced media event formats ([#4092](https://github.com/matrix-org/matrix-js-sdk/pull/4092)). Contributed by @t3chguy.
* Add `.m.rule.is_room_mention` push rule to DEFAULT\_OVERRIDE\_RULES ([#4100](https://github.com/matrix-org/matrix-js-sdk/pull/4100)). Contributed by @t3chguy.
* Make sending ContentLoaded optional for a widgetClient ([#4086](https://github.com/matrix-org/matrix-js-sdk/pull/4086)). Contributed by @toger5.
## 🐛 Bug Fixes
* Migrate own identity local trust to rust crypto ([#4090](https://github.com/matrix-org/matrix-js-sdk/pull/4090)). Contributed by @BillCarsonFr.
* Fix race condition with sliding sync extensions ([#4089](https://github.com/matrix-org/matrix-js-sdk/pull/4089)). Contributed by @zzorba.
Changes in [31.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.5.0) (2024-03-12)
==================================================================================================
## ✨ Features
+27 -60
View File
@@ -21,16 +21,6 @@ endpoints from before Matrix 1.1, for example.
# Quickstart
## In a browser
### Note, the browserify build has been removed. Please use a bundler like webpack or vite instead.
## In Node.js
Ensure you have the latest LTS version of Node.js installed.
This library relies on `fetch` which is available in Node from v18.0.0 - it should work fine also with polyfills.
If you wish to use a ponyfill or adapter of some sort then pass it as `fetchFn` to the MatrixClient constructor options.
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
if you do not have it already.
@@ -47,8 +37,6 @@ client.publicRooms(function (err, data) {
See below for how to include libolm to enable end-to-end-encryption. Please check
[the Node.js terminal app](examples/node) for a more complex example.
You can also use the sdk with [Deno](https://deno.land/) (`import npm:matrix-js-sdk`) but its not officialy supported.
To start the client:
```javascript
@@ -106,7 +94,7 @@ Object.keys(client.store.rooms).forEach((roomId) => {
This SDK provides a full object model around the Matrix Client-Server API and emits
events for incoming data and state changes. Aside from wrapping the HTTP API, it:
- Handles syncing (via `/initialSync` and `/events`)
- Handles syncing (via `/sync`)
- Handles the generation of "friendly" room and member names.
- Handles historical `RoomMember` information (e.g. display names).
- Manages room member state across multiple events (e.g. it handles typing, power
@@ -127,20 +115,20 @@ events for incoming data and state changes. Aside from wrapping the HTTP API, it
- Handles room initial sync on accepting invites.
- Handles WebRTC calling.
Later versions of the SDK will:
- Expose a `RoomSummary` which would be suitable for a recents page.
- Provide different pluggable storage layers (e.g. local storage, database-backed)
# Usage
## Conventions
## Supported platforms
### Emitted events
`matrix-js-sdk` can be used in either Node.js applications (ensure you have the latest LTS version of Node.js installed),
or in browser applications, via a bundler such as Webpack or Vite.
The SDK will emit events using an `EventEmitter`. It also
emits object models (e.g. `Rooms`, `RoomMembers`) when they
are updated.
You can also use the sdk with [Deno](https://deno.land/) (`import npm:matrix-js-sdk`) but its not officialy supported.
## Emitted events
The SDK raises notifications to the application using
[`EventEmitter`s](https://nodejs.org/api/events.html#class-eventemitter). The `MatrixClient` itself
implements `EventEmitter`, as do many of the high-level abstractions such as `Room` and `RoomMember`.
```javascript
// Listen for low-level MatrixEvents
@@ -161,45 +149,21 @@ client.on(RoomMemberEvent.Typing, function (event, member) {
client.startClient();
```
### Promises and Callbacks
## Entry points
Most of the methods in the SDK are asynchronous: they do not directly return a
result, but instead return a [Promise](http://documentup.com/kriskowal/q/)
which will be fulfilled in the future.
As well as the primary entry point (`matrix-js-sdk`), there are several other entry points which may be useful:
The typical usage is something like:
```javascript
matrixClient.someMethod(arg1, arg2).then(function(result) {
...
});
```
Alternatively, if you have a Node.js-style `callback(err, result)` function,
you can pass the result of the promise into it with something like:
```javascript
matrixClient.someMethod(arg1, arg2).nodeify(callback);
```
The main thing to note is that it is problematic to discard the result of a
promise-returning function, as that will cause exceptions to go unobserved.
Methods which return a promise show this in their documentation.
Many methods in the SDK support _both_ Node.js-style callbacks _and_ Promises,
via an optional `callback` argument. The callback support is now deprecated:
new methods do not include a `callback` argument, and in the future it may be
removed from existing methods.
## Low level types
There are some low level TypeScript types exported via the `matrix-js-sdk/lib/types` entrypoint to not bloat the main entrypoint.
| Entry point | Description |
| ------------------------------ | --------------------------------------------------------------------------------------------------- |
| `matrix-js-sdk` | Primary entry point. High-level functionality, and lots of historical clutter in need of a cleanup. |
| `matrix-js-sdk/lib/crypto-api` | Cryptography functionality. |
| `matrix-js-sdk/lib/types` | Low-level types, reflecting data structures defined in the Matrix spec. |
| `matrix-js-sdk/lib/testing` | Test utilities, which may be useful in test code but should not be used in production code. |
## Examples
This section provides some useful code snippets which demonstrate the
core functionality of the SDK. These examples assume the SDK is setup like this:
core functionality of the SDK. These examples assume the SDK is set up like this:
```javascript
import * as sdk from "matrix-js-sdk";
@@ -215,10 +179,10 @@ const matrixClient = sdk.createClient({
### Automatically join rooms when invited
```javascript
matrixClient.on(RoomMemberEvent.Membership, function (event, member) {
if (member.membership === KnownMembership.Invite && member.userId === myUserId) {
matrixClient.joinRoom(member.roomId).then(function () {
console.log("Auto-joined %s", member.roomId);
matrixClient.on(RoomEvent.MyMembership, function (room, membership, prevMembership) {
if (membership === KnownMembership.Invite) {
matrixClient.joinRoom(room.roomId).then(function () {
console.log("Auto-joined %s", room.roomId);
});
}
});
@@ -306,6 +270,9 @@ Then visit `http://localhost:8005` to see the API docs.
# End-to-end encryption support
**This section is outdated.** Use of `libolm` is deprecated and we are replacing it with support
from the matrix-rust-sdk (https://github.com/element-hq/element-web/issues/21972).
The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
[libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the
application to make libolm available, via the `Olm` global.
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "31.6.0-rc.0",
"version": "32.0.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=18.0.0"
@@ -52,7 +52,7 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^4.6.0",
"@matrix-org/matrix-sdk-crypto-wasm": "^4.9.0",
"another-json": "^0.2.0",
"bs58": "^5.0.0",
"content-type": "^1.0.4",
+27 -23
View File
@@ -81,33 +81,37 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
};
}
beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
beforeEach(
async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
const homeserverUrl = "https://alice-server.com";
aliceClient = createClient({
baseUrl: homeserverUrl,
userId: TEST_USER_ID,
accessToken: "akjgkrgjs",
deviceId: TEST_DEVICE_ID,
cryptoCallbacks: createCryptoCallbacks(),
});
const homeserverUrl = "https://alice-server.com";
aliceClient = createClient({
baseUrl: homeserverUrl,
userId: TEST_USER_ID,
accessToken: "akjgkrgjs",
deviceId: TEST_DEVICE_ID,
cryptoCallbacks: createCryptoCallbacks(),
});
syncResponder = new SyncResponder(homeserverUrl);
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
/** an object which intercepts `/keys/upload` requests on the test homeserver */
new E2EKeyReceiver(homeserverUrl);
syncResponder = new SyncResponder(homeserverUrl);
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
/** an object which intercepts `/keys/upload` requests on the test homeserver */
new E2EKeyReceiver(homeserverUrl);
// Silence warnings from the backup manager
fetchMock.getOnce(new URL("/_matrix/client/v3/room_keys/version", homeserverUrl).toString(), {
status: 404,
body: { errcode: "M_NOT_FOUND" },
});
// Silence warnings from the backup manager
fetchMock.getOnce(new URL("/_matrix/client/v3/room_keys/version", homeserverUrl).toString(), {
status: 404,
body: { errcode: "M_NOT_FOUND" },
});
await initCrypto(aliceClient);
});
await initCrypto(aliceClient);
},
/* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */
10000,
);
afterEach(async () => {
await aliceClient.stopClient();
+19 -30
View File
@@ -26,6 +26,7 @@ import * as testUtils from "../../test-utils/test-utils";
import {
advanceTimersUntil,
CRYPTO_BACKENDS,
emitPromise,
getSyncResponse,
InitCrypto,
mkEventCustom,
@@ -50,6 +51,7 @@ import {
ClientEvent,
createClient,
CryptoEvent,
HistoryVisibility,
IClaimOTKsResult,
IContent,
IDownloadKeyResult,
@@ -59,11 +61,11 @@ import {
MatrixClient,
MatrixEvent,
MatrixEventEvent,
MsgType,
PendingEventOrdering,
Room,
RoomMember,
RoomStateEvent,
HistoryVisibility,
} from "../../../src/matrix";
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
@@ -80,12 +82,12 @@ import { SecretStorageKeyDescription } from "../../../src/secret-storage";
import {
CrossSigningKey,
CryptoCallbacks,
DecryptionFailureCode,
EventShieldColour,
EventShieldReason,
KeyBackupInfo,
} from "../../../src/crypto-api";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { DecryptionError } from "../../../src/crypto/algorithms";
import { IKeyBackup } from "../../../src/crypto/backup";
import {
createOlmAccount,
@@ -465,20 +467,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
});
describe("Unable to decrypt error codes", function () {
it("Encryption fails with expected UISI error", async () => {
it("Decryption fails with UISI error", async () => {
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const awaitUISI = new Promise<void>((resolve) => {
aliceClient.on(MatrixEventEvent.Decrypted, (ev, err) => {
const error = err as DecryptionError;
if (error.code == "MEGOLM_UNKNOWN_INBOUND_SESSION_ID") {
resolve();
}
});
});
// A promise which resolves, with the MatrixEvent which wraps the event, once the decryption fails.
const awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
// Alice gets both the events in a single sync
const syncResponse = {
next_batch: 1,
rooms: {
@@ -490,22 +485,16 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
syncResponder.sendOrQueueSyncResponse(syncResponse);
await syncPromise(aliceClient);
await awaitUISI;
const ev = await awaitDecryption;
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID);
});
it("Encryption fails with expected Unknown Index error", async () => {
it("Decryption fails with Unknown Index error", async () => {
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const awaitUnknownIndex = new Promise<void>((resolve) => {
aliceClient.on(MatrixEventEvent.Decrypted, (ev, err) => {
const error = err as DecryptionError;
if (error.code == "OLM_UNKNOWN_MESSAGE_INDEX") {
resolve();
}
});
});
// A promise which resolves, with the MatrixEvent which wraps the event, once the decryption fails.
const awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
await aliceClient.getCrypto()!.importRoomKeys([testData.RATCHTED_MEGOLM_SESSION_DATA]);
@@ -522,23 +511,23 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
syncResponder.sendOrQueueSyncResponse(syncResponse);
await syncPromise(aliceClient);
await awaitUnknownIndex;
const ev = await awaitDecryption;
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX);
});
it("Encryption fails with Unable to decrypt for other errors", async () => {
it("Decryption fails with Unable to decrypt for other errors", async () => {
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
await aliceClient.getCrypto()!.importRoomKeys([testData.MEGOLM_SESSION_DATA]);
const awaitDecryptionError = new Promise<void>((resolve) => {
aliceClient.on(MatrixEventEvent.Decrypted, (ev, err) => {
const error = err as DecryptionError;
aliceClient.on(MatrixEventEvent.Decrypted, (ev) => {
// rust and libolm can't have an exact 1:1 mapping for all errors,
// but some errors are part of API and should match
if (
error.code != "MEGOLM_UNKNOWN_INBOUND_SESSION_ID" &&
error.code != "OLM_UNKNOWN_MESSAGE_INDEX"
ev.decryptionFailureReason !== DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID &&
ev.decryptionFailureReason !== DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX
) {
resolve();
}
@@ -1928,7 +1917,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
expectAliceKeyQuery({ device_keys: { "@other:user": {} }, failures: {} });
aliceClient.on(RoomStateEvent.NewMember, (_e, _s, member: RoomMember) => {
if (member.userId == "@other:user") {
aliceClient.sendMessage(testRoomId, { msgtype: "m.text", body: "Hello, World" });
aliceClient.sendMessage(testRoomId, { msgtype: MsgType.Text, body: "Hello, World" });
}
});
+21 -18
View File
@@ -192,28 +192,31 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
}
}
beforeEach(async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
beforeEach(
async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
// ignore requests to send room key requests
fetchMock.put("express:/_matrix/client/v3/sendToDevice/m.room_key_request/:request_id", {});
// ignore requests to send room key requests
fetchMock.put("express:/_matrix/client/v3/sendToDevice/m.room_key_request/:request_id", {});
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceCrypto.storeSessionBackupPrivateKey(
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
testData.SIGNED_BACKUP_DATA.version!,
);
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceCrypto.storeSessionBackupPrivateKey(
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
testData.SIGNED_BACKUP_DATA.version!,
);
// start after saving the private key
await aliceClient.startClient();
// start after saving the private key
await aliceClient.startClient();
// tell Alice to trust the dummy device that signed the backup, and re-check the backup.
// XXX: should we automatically re-check after a device becomes verified?
await waitForDeviceList();
await aliceClient.getCrypto()!.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
});
// tell Alice to trust the dummy device that signed the backup, and re-check the backup.
// XXX: should we automatically re-check after a device becomes verified?
await waitForDeviceList();
await aliceClient.getCrypto()!.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
} /* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */,
10000,
);
it("Alice checks key backups when receiving a message she can't decrypt", async () => {
fetchMock.get("express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", (url, request) => {
+2 -2
View File
@@ -34,7 +34,7 @@ import { logger } from "../../../src/logger";
import * as testUtils from "../../test-utils/test-utils";
import { TestClient } from "../../TestClient";
import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../../src/client";
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../../src/matrix";
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent, MsgType } from "../../../src/matrix";
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
import { KnownMembership } from "../../../src/@types/membership";
@@ -217,7 +217,7 @@ async function expectBobSendMessageRequest(): Promise<OlmPayload> {
}
function sendMessage(client: MatrixClient): Promise<ISendEventResponse> {
return client.sendMessage(roomId, { msgtype: "m.text", body: "Hello, World" });
return client.sendMessage(roomId, { msgtype: MsgType.Text, body: "Hello, World" });
}
async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]): Promise<IContent> {
+3 -3
View File
@@ -16,7 +16,7 @@ limitations under the License.
import HttpBackend from "matrix-mock-request";
import { EventStatus, RoomEvent, MatrixClient, MatrixScheduler } from "../../src/matrix";
import { EventStatus, MatrixClient, MatrixScheduler, MsgType, RoomEvent } from "../../src/matrix";
import { Room } from "../../src/models/room";
import { TestClient } from "../TestClient";
@@ -60,7 +60,7 @@ describe("MatrixClient retrying", function () {
// send a couple of events; the second will be queued
const p1 = client!
.sendMessage(roomId, {
msgtype: "m.text",
msgtype: MsgType.Text,
body: "m1",
})
.then(
@@ -77,7 +77,7 @@ describe("MatrixClient retrying", function () {
// never gets resolved.
// https://github.com/matrix-org/matrix-js-sdk/issues/496
client!.sendMessage(roomId, {
msgtype: "m.text",
msgtype: MsgType.Text,
body: "m2",
});
+434 -52
View File
@@ -39,6 +39,7 @@ import {
IndexedDBStore,
RelationType,
EventType,
MatrixEventEvent,
} from "../../src";
import { ReceiptType } from "../../src/@types/read_receipts";
import { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync";
@@ -46,6 +47,7 @@ import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
import { emitPromise, mkEvent, mkMessage } from "../test-utils/test-utils";
import { THREAD_RELATION_TYPE } from "../../src/models/thread";
import { IActionsObject } from "../../src/pushprocessor";
import { KnownMembership } from "../../src/@types/membership";
describe("MatrixClient syncing", () => {
@@ -1646,6 +1648,99 @@ describe("MatrixClient syncing", () => {
});
});
it("should zero total notifications for threads when absent from the notifications object", async () => {
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
[THREAD_ID]: {
highlight_count: 2,
notification_count: 5,
},
};
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient();
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomOne);
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5);
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {};
httpBackend!.when("GET", "/sync").respond(200, syncData);
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(0);
});
it("should zero highlight notifications for threads in encrypted rooms", async () => {
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
[THREAD_ID]: {
highlight_count: 2,
notification_count: 5,
},
};
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient();
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomOne);
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5);
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
[THREAD_ID]: {
highlight_count: 0,
notification_count: 0,
},
};
httpBackend!.when("GET", "/sync").respond(200, syncData);
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
});
it("should not zero highlight notifications for threads in encrypted rooms", async () => {
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
[THREAD_ID]: {
highlight_count: 2,
notification_count: 5,
},
};
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient();
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomOne);
room!.hasEncryptionStateEvent = jest.fn().mockReturnValue(true);
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5);
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
[THREAD_ID]: {
highlight_count: 0,
notification_count: 0,
},
};
httpBackend!.when("GET", "/sync").respond(200, syncData);
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(0);
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(2);
});
it("caches unknown threads receipts and replay them when the thread is created", async () => {
const THREAD_ID = "$unknownthread:localhost";
@@ -1733,64 +1828,351 @@ describe("MatrixClient syncing", () => {
});
});
it("should apply encrypted notification logic for events within the same sync blob", async () => {
const roomId = "!room123:server";
const syncData = {
rooms: {
join: {
[roomId]: {
ephemeral: {
events: [],
},
timeline: {
events: [
utils.mkEvent({
room: roomId,
event: true,
skey: "",
type: EventType.RoomEncryption,
content: {},
}),
utils.mkMessage({
room: roomId,
user: otherUserId,
msg: "hello",
}),
],
},
state: {
events: [
utils.mkMembership({
room: roomId,
mship: KnownMembership.Join,
user: otherUserId,
}),
utils.mkMembership({
room: roomId,
mship: KnownMembership.Join,
user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create",
room: roomId,
user: selfUserId,
content: {},
}),
],
describe("encrypted notification logic", () => {
let roomId: string;
let syncData: ISyncResponse;
beforeEach(() => {
roomId = "!room123:server";
syncData = {
rooms: {
join: {
[roomId]: {
ephemeral: {
events: [],
},
timeline: {
events: [
utils.mkEvent({
room: roomId,
event: true,
skey: "",
type: EventType.RoomEncryption,
content: {},
}),
utils.mkMessage({
room: roomId,
user: otherUserId,
msg: "hello",
}),
],
},
state: {
events: [
utils.mkMembership({
room: roomId,
mship: KnownMembership.Join,
user: otherUserId,
}),
utils.mkMembership({
room: roomId,
mship: KnownMembership.Join,
user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create",
room: roomId,
user: selfUserId,
content: {},
}),
],
},
},
},
},
},
} as unknown as ISyncResponse;
} as unknown as ISyncResponse;
});
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient();
it("should apply encrypted notification logic for events within the same sync blob", async () => {
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient();
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomId)!;
expect(room).toBeInstanceOf(Room);
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
const room = client!.getRoom(roomId)!;
expect(room).toBeInstanceOf(Room);
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
});
it("should recalculate highlights on unthreaded receipt for encrypted rooms", async () => {
const myUserId = client!.getUserId()!;
const firstEventId = syncData.rooms.join[roomId].timeline.events[1].event_id;
// add a receipt for the first event in the room (let's say the user has already read that one)
syncData.rooms.join[roomId].ephemeral.events = [
{
content: {
[firstEventId]: {
"m.read": {
[myUserId]: { ts: 1 },
},
},
},
type: "m.receipt",
},
];
// Now add a highlighting event after that receipt
const pingEvent = utils.mkMessage({
room: roomId,
user: otherUserId,
msg: client?.getUserId() + " ping",
}) as IRoomEvent;
syncData.rooms.join[roomId].timeline.events.push(pingEvent);
// fudge this to make it a highlight
client!.getPushActionsForEvent = (ev: MatrixEvent): IActionsObject | null => {
if (ev.getId() === pingEvent.event_id) {
return {
notify: true,
tweaks: {
highlight: true,
},
};
}
return null;
};
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient();
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomId)!;
expect(room).toBeInstanceOf(Room);
// the room should now have one highlight since our receipt was before the ping message
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
});
it("should recalculate highlights on main thread receipt for encrypted rooms", async () => {
const myUserId = client!.getUserId()!;
const firstEventId = syncData.rooms.join[roomId].timeline.events[1].event_id;
// add a receipt for the first event in the room (let's say the user has already read that one)
syncData.rooms.join[roomId].ephemeral.events = [
{
content: {
[firstEventId]: {
"m.read": {
[myUserId]: { ts: 1, thread_id: "main" },
},
},
},
type: "m.receipt",
},
];
// Now add a highlighting event after that receipt
const pingEvent = utils.mkMessage({
room: roomId,
user: otherUserId,
msg: client?.getUserId() + " ping",
}) as IRoomEvent;
syncData.rooms.join[roomId].timeline.events.push(pingEvent);
// fudge this to make it a highlight
client!.getPushActionsForEvent = (ev: MatrixEvent): IActionsObject | null => {
if (ev.getId() === pingEvent.event_id) {
return {
notify: true,
tweaks: {
highlight: true,
},
};
}
return null;
};
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient();
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomId)!;
expect(room).toBeInstanceOf(Room);
// the room should now have one highlight since our receipt was before the ping message
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
});
describe("notification processing in threads", () => {
let threadEvent1: IRoomEvent;
let threadEvent2: IRoomEvent;
let firstEventId: string;
beforeEach(() => {
firstEventId = syncData.rooms.join[roomId].timeline.events[1].event_id;
// Add a threaded event off of the first event
threadEvent1 = utils.mkEvent({
type: EventType.RoomMessage,
user: otherUserId,
room: roomId,
ts: 500,
content: {
"body": "first thread response",
"m.relates_to": {
"event_id": firstEventId,
"m.in_reply_to": {
event_id: firstEventId,
},
"rel_type": "io.element.thread",
},
},
}) as IRoomEvent;
syncData.rooms.join[roomId].timeline.events.push(threadEvent1);
// ...and another
threadEvent2 = utils.mkEvent({
type: EventType.RoomMessage,
user: otherUserId,
room: roomId,
ts: 1500,
content: {
"body": "second thread response",
"m.relates_to": {
"event_id": firstEventId,
"m.in_reply_to": {
event_id: firstEventId,
},
"rel_type": "io.element.thread",
},
},
}) as IRoomEvent;
syncData.rooms.join[roomId].timeline.events.push(threadEvent2);
// fudge to make these highlights
client!.getPushActionsForEvent = (ev: MatrixEvent): IActionsObject | null => {
if ([threadEvent1.event_id, threadEvent2.event_id].includes(ev.getId()!)) {
return {
notify: true,
tweaks: {
highlight: true,
},
};
}
return null;
};
});
it("checks threads with notifications on unthreaded receipts", async () => {
const myUserId = client!.getUserId()!;
// add a receipt for a random, ficticious thread, otherwise the client will
// think that the thread is before any threaded receipts and ignore it.
syncData.rooms.join[roomId].ephemeral.events = [
{
content: {
[firstEventId]: {
"m.read": {
[myUserId]: { ts: 1, thread_id: "some_other_thread" },
},
},
},
type: "m.receipt",
},
];
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient({ threadSupport: true });
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomId)!;
// pretend that the client has decrypted an event to trigger it to compute
// local notifications
client?.emit(MatrixEventEvent.Decrypted, room.findEventById(firstEventId)!);
client?.emit(MatrixEventEvent.Decrypted, room.findEventById(threadEvent1.event_id)!);
client?.emit(MatrixEventEvent.Decrypted, room.findEventById(threadEvent2.event_id)!);
expect(room).toBeInstanceOf(Room);
// we should now have one highlight: the unread message that pings
expect(
room.getThreadUnreadNotificationCount(firstEventId, NotificationCountType.Highlight),
).toEqual(2);
const syncData2 = {
rooms: {
join: {
[roomId]: {
ephemeral: {
events: [
{
content: {
[firstEventId]: {
"m.read": {
[myUserId]: { ts: 1 },
},
},
},
type: "m.receipt",
},
],
},
},
},
},
} as unknown as ISyncResponse;
httpBackend!.when("GET", "/sync").respond(200, syncData2);
await Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]);
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)).toBe(0);
});
it("should recalculate highlights on threaded receipt for encrypted rooms", async () => {
const myUserId = client!.getUserId()!;
// add a receipt for the first message in the threadm leaving the second one unread
syncData.rooms.join[roomId].ephemeral.events = [
{
content: {
[threadEvent1.event_id]: {
"m.read": {
[myUserId]: { ts: 1, thread_id: firstEventId },
},
},
},
type: "m.receipt",
},
];
// fudge to make both thread replies highlights
client!.getPushActionsForEvent = (ev: MatrixEvent): IActionsObject | null => {
if ([threadEvent1.event_id, threadEvent2.event_id].includes(ev.getId()!)) {
return {
notify: true,
tweaks: {
highlight: true,
},
};
}
return null;
};
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient({ threadSupport: true });
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomId)!;
expect(room).toBeInstanceOf(Room);
// pretend that the client has decrypted an event to trigger it to compute
// local notifications
client?.emit(MatrixEventEvent.Decrypted, room.findEventById(firstEventId)!);
// the room should now have one highlight: the second thread message
expect(room.getThreadUnreadNotificationCount(firstEventId, NotificationCountType.Highlight)).toBe(
1,
);
});
});
});
});
@@ -152,7 +152,7 @@ describe("MatrixClient syncing", () => {
await client!.sendEvent(roomId, EventType.Reaction, {
"m.relates_to": {
rel_type: RelationType.Annotation,
event_id: threadReply.getId(),
event_id: threadReply.getId()!,
key: "",
},
});
+14
View File
@@ -75,6 +75,20 @@ class MockWidgetApi extends EventEmitter {
public transport = { reply: jest.fn() };
}
declare module "../../src/types" {
interface StateEvents {
"org.example.foo": {
hello: string;
};
}
interface TimelineEvents {
"org.matrix.rageshake_request": {
request_id: number;
};
}
}
describe("RoomWidgetClient", () => {
let widgetApi: MockedObject<WidgetApi>;
let client: MatrixClient;
+17 -15
View File
@@ -22,6 +22,7 @@ import { Filter } from "../../src/filter";
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace";
import {
EventType,
MsgType,
RelationType,
RoomCreateTypeField,
RoomType,
@@ -38,30 +39,31 @@ import * as testUtils from "../test-utils/test-utils";
import { makeBeaconInfoContent } from "../../src/content-helpers";
import { M_BEACON_INFO } from "../../src/@types/beacon";
import {
ContentHelpers,
ClientPrefix,
ConditionKind,
ContentHelpers,
Direction,
EventTimeline,
EventTimelineSet,
getHttpUriForMxc,
ICreateRoomOpts,
IPushRule,
IRequestOpts,
MatrixError,
MatrixHttpApi,
MatrixScheduler,
Method,
Room,
EventTimelineSet,
PushRuleActionName,
TweakName,
Room,
RuleId,
IPushRule,
ConditionKind,
getHttpUriForMxc,
TweakName,
} from "../../src";
import { supportsMatrixCall } from "../../src/webrtc/call";
import { makeBeaconEvent } from "../test-utils/beacon";
import {
IGNORE_INVITES_ACCOUNT_EVENT_KEY,
POLICIES_ACCOUNT_EVENT_TYPE,
PolicyRecommendation,
PolicyScope,
} from "../../src/models/invites-ignorer";
import { IOlmDevice } from "../../src/crypto/algorithms/megolm";
@@ -72,6 +74,7 @@ import { StubStore } from "../../src/store/stub";
import { SecretStorageKeyDescriptionAesV1, ServerSideSecretStorageImpl } from "../../src/secret-storage";
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
import { KnownMembership } from "../../src/@types/membership";
import { RoomMessageEventContent } from "../../src/@types/events";
jest.useFakeTimers();
@@ -566,7 +569,7 @@ describe("MatrixClient", function () {
describe("sendEvent", () => {
const roomId = "!room:example.org";
const body = "This is the body";
const content = { body };
const content = { body, msgtype: MsgType.Text } satisfies RoomMessageEventContent;
it("overload without threadId works", async () => {
const eventId = "$eventId:example.org";
@@ -661,12 +664,13 @@ describe("MatrixClient", function () {
const content = {
body,
"msgtype": MsgType.Text,
"m.relates_to": {
"m.in_reply_to": {
event_id: "$other:event",
},
},
};
} satisfies RoomMessageEventContent;
const room = new Room(roomId, client, userId);
mocked(store.getRoom).mockReturnValue(room);
@@ -1527,8 +1531,6 @@ describe("MatrixClient", function () {
{ startOpts: {}, hasThreadSupport: false },
{ startOpts: { threadSupport: true }, hasThreadSupport: true },
{ startOpts: { threadSupport: false }, hasThreadSupport: false },
{ startOpts: { experimentalThreadSupport: true }, hasThreadSupport: true },
{ startOpts: { experimentalThreadSupport: true, threadSupport: false }, hasThreadSupport: false },
])("enabled thread support for the SDK instance", async ({ startOpts, hasThreadSupport }) => {
await client.startClient(startOpts);
expect(client.supportsThreads()).toBe(hasThreadSupport);
@@ -2082,10 +2084,10 @@ describe("MatrixClient", function () {
await client.ignoredInvites.addSource(NEW_SOURCE_ROOM_ID);
// Add a rule in the new source room.
await client.sendStateEvent(NEW_SOURCE_ROOM_ID, PolicyScope.User, {
await client.sendStateEvent(NEW_SOURCE_ROOM_ID, EventType.PolicyRuleUser, {
entity: "*:example.org",
reason: "just a test",
recommendation: "m.ban",
recommendation: PolicyRecommendation.Ban,
});
// We should reject this invite.
@@ -2172,8 +2174,8 @@ describe("MatrixClient", function () {
// Check where it shows up.
const targetRoomId = ignoreInvites2.target;
const targetRoom = client.getRoom(targetRoomId) as WrappedRoom;
expect(targetRoom._state.get(PolicyScope.User)[eventId]).toBeTruthy();
expect(newSourceRoom._state.get(PolicyScope.User)?.[eventId]).toBeFalsy();
expect(targetRoom._state.get(EventType.PolicyRuleUser)[eventId]).toBeTruthy();
expect(newSourceRoom._state.get(EventType.PolicyRuleUser)?.[eventId]).toBeFalsy();
});
});
+3 -2
View File
@@ -26,6 +26,7 @@ import {
import { DEFAULT_ALPHABET } from "../../../src/utils";
import { MatrixError } from "../../../src/http-api";
import { KnownMembership } from "../../../src/@types/membership";
import { EncryptedFile } from "../../../src/@types/media";
describe("MSC3089TreeSpace", () => {
let client: MatrixClient;
@@ -947,7 +948,7 @@ describe("MSC3089TreeSpace", () => {
const fileInfo = {
mimetype: "text/plain",
// other fields as required by encryption, but ignored here
};
} as unknown as EncryptedFile;
const fileEventId = "$file";
const fileName = "My File.txt";
const fileContents = "This is a test file";
@@ -1007,7 +1008,7 @@ describe("MSC3089TreeSpace", () => {
const fileInfo = {
mimetype: "text/plain",
// other fields as required by encryption, but ignored here
};
} as unknown as EncryptedFile;
const fileEventId = "$file";
const fileName = "My File.txt";
const fileContents = "This is a test file";
+37 -49
View File
@@ -27,6 +27,8 @@ import {
THREAD_RELATION_TYPE,
TweakName,
} from "../../../src";
import { DecryptionFailureCode } from "../../../src/crypto-api";
import { DecryptionError } from "../../../src/common-crypto/CryptoBackend";
describe("MatrixEvent", () => {
it("should create copies of itself", () => {
@@ -360,20 +362,50 @@ describe("MatrixEvent", () => {
});
});
it("should report decryption errors", async () => {
it("should report unknown decryption errors", async () => {
const decryptionListener = jest.fn();
encryptedEvent.addListener(MatrixEventEvent.Decrypted, decryptionListener);
const testError = new Error("test error");
const crypto = {
decryptEvent: jest.fn().mockRejectedValue(new Error("test error")),
decryptEvent: jest.fn().mockRejectedValue(testError),
} as unknown as Crypto;
await encryptedEvent.attemptDecryption(crypto);
expect(encryptedEvent.isEncrypted()).toBeTruthy();
expect(encryptedEvent.isBeingDecrypted()).toBeFalsy();
expect(encryptedEvent.isDecryptionFailure()).toBeTruthy();
expect(encryptedEvent.decryptionFailureReason).toEqual(DecryptionFailureCode.UNKNOWN_ERROR);
expect(encryptedEvent.isEncryptedDisabledForUnverifiedDevices).toBeFalsy();
expect(encryptedEvent.getContent()).toEqual({
msgtype: "m.bad.encrypted",
body: "** Unable to decrypt: Error: test error **",
});
expect(decryptionListener).toHaveBeenCalledWith(encryptedEvent, testError);
});
it("should report known decryption errors", async () => {
const decryptionListener = jest.fn();
encryptedEvent.addListener(MatrixEventEvent.Decrypted, decryptionListener);
const testError = new DecryptionError(DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID, "uisi");
const crypto = {
decryptEvent: jest.fn().mockRejectedValue(testError),
} as unknown as Crypto;
await encryptedEvent.attemptDecryption(crypto);
expect(encryptedEvent.isEncrypted()).toBeTruthy();
expect(encryptedEvent.isBeingDecrypted()).toBeFalsy();
expect(encryptedEvent.isDecryptionFailure()).toBeTruthy();
expect(encryptedEvent.decryptionFailureReason).toEqual(
DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
);
expect(encryptedEvent.isEncryptedDisabledForUnverifiedDevices).toBeFalsy();
expect(encryptedEvent.getContent()).toEqual({
msgtype: "m.bad.encrypted",
body: "** Unable to decrypt: DecryptionError: uisi **",
});
expect(decryptionListener).toHaveBeenCalledWith(encryptedEvent, testError);
});
it(`should report "DecryptionError: The sender has disabled encrypting to unverified devices."`, async () => {
@@ -423,6 +455,8 @@ describe("MatrixEvent", () => {
expect(eventAttemptDecryptionSpy).toHaveBeenCalledTimes(2);
expect(crypto.decryptEvent).toHaveBeenCalledTimes(2);
expect(encryptedEvent.getType()).toEqual("m.room.message");
expect(encryptedEvent.isDecryptionFailure()).toBe(false);
expect(encryptedEvent.decryptionFailureReason).toBe(null);
});
});
@@ -469,52 +503,6 @@ describe("MatrixEvent", () => {
default: false,
enabled: true,
} as IAnnotatedPushRule;
describe("setPushActions()", () => {
it("sets actions on event", () => {
const actions = { notify: false, tweaks: {} };
const event = new MatrixEvent({
type: "com.example.test",
content: {
isTest: true,
},
});
event.setPushActions(actions);
expect(event.getPushActions()).toBe(actions);
});
it("sets actions to undefined", () => {
const event = new MatrixEvent({
type: "com.example.test",
content: {
isTest: true,
},
});
event.setPushActions(null);
// undefined is set on state
expect(event.getPushDetails().actions).toBe(undefined);
// but pushActions getter returns null when falsy
expect(event.getPushActions()).toBe(null);
});
it("clears existing push rule", () => {
const prevActions = { notify: true, tweaks: { highlight: true } };
const actions = { notify: false, tweaks: {} };
const event = new MatrixEvent({
type: "com.example.test",
content: {
isTest: true,
},
});
event.setPushDetails(prevActions, pushRule);
event.setPushActions(actions);
// rule is not in event push cache
expect(event.getPushDetails()).toEqual({ actions });
});
});
describe("setPushDetails()", () => {
it("sets actions and rule on event", () => {
@@ -543,7 +531,7 @@ describe("MatrixEvent", () => {
});
event.setPushDetails(prevActions, pushRule);
event.setPushActions(actions);
event.setPushDetails(actions);
// rule is not in event push cache
expect(event.getPushDetails()).toEqual({ actions });
+320 -38
View File
@@ -1,8 +1,35 @@
import * as utils from "../test-utils/test-utils";
import { IActionsObject, PushProcessor } from "../../src/pushprocessor";
import { ConditionKind, EventType, IContent, MatrixClient, MatrixEvent, PushRuleActionName, RuleId } from "../../src";
import {
ConditionKind,
EventType,
IContent,
IPushRule,
MatrixClient,
MatrixEvent,
PushRuleActionName,
RuleId,
TweakName,
} from "../../src";
import { mockClientMethodsUser } from "../test-utils/client";
const msc3914RoomCallRule: IPushRule = {
rule_id: ".org.matrix.msc3914.rule.room.call",
default: true,
enabled: true,
conditions: [
{
kind: ConditionKind.EventMatch,
key: "type",
pattern: "org.matrix.msc3401.call",
},
{
kind: ConditionKind.CallStarted,
},
],
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Sound, value: "default" }],
};
describe("NotificationService", function () {
const testUserId = "@ali:matrix.org";
const testDisplayName = "Alice M";
@@ -12,23 +39,6 @@ describe("NotificationService", function () {
let pushProcessor: PushProcessor;
const msc3914RoomCallRule = {
rule_id: ".org.matrix.msc3914.rule.room.call",
default: true,
enabled: true,
conditions: [
{
kind: "event_match",
key: "type",
pattern: "org.matrix.msc3401.call",
},
{
kind: "call_started",
},
],
actions: ["notify", { set_tweak: "sound", value: "default" }],
};
let matrixClient: MatrixClient;
beforeEach(function () {
@@ -187,26 +197,6 @@ describe("NotificationService", function () {
pushProcessor = new PushProcessor(matrixClient);
});
it("should add default rules in the correct order", () => {
// By the time we get here, we expect the PushProcessor to have merged the new .m.rule.is_room_mention rule into the existing list of rules.
// Check that has happened, and that it is in the right place.
const containsDisplayNameRuleIdx = matrixClient.pushRules?.global.override?.findIndex(
(rule) => rule.rule_id === RuleId.ContainsDisplayName,
);
expect(containsDisplayNameRuleIdx).toBeGreaterThan(-1);
const isRoomMentionRuleIdx = matrixClient.pushRules?.global.override?.findIndex(
(rule) => rule.rule_id === RuleId.IsRoomMention,
);
expect(isRoomMentionRuleIdx).toBeGreaterThan(-1);
const mReactionRuleIdx = matrixClient.pushRules?.global.override?.findIndex(
(rule) => rule.rule_id === ".m.rule.reaction",
);
expect(mReactionRuleIdx).toBeGreaterThan(-1);
expect(containsDisplayNameRuleIdx).toBeLessThan(isRoomMentionRuleIdx!);
expect(isRoomMentionRuleIdx).toBeLessThan(mReactionRuleIdx!);
});
// User IDs
it("should bing on a user ID.", function () {
@@ -722,3 +712,295 @@ describe("Test PushProcessor.partsForDottedKey", function () {
expect(PushProcessor.partsForDottedKey(path)).toStrictEqual(expected);
});
});
describe("rewriteDefaultRules", () => {
it("should add default rules in the correct order", () => {
const pushRules = PushProcessor.rewriteDefaultRules({
device: {},
global: {
content: [],
override: [
// Include user-defined push rules inbetween .m.rule.master and other default rules to assert they are maintained in-order.
{
rule_id: ".m.rule.master",
default: true,
enabled: false,
conditions: [],
actions: [],
},
{
actions: [
PushRuleActionName.Notify,
{
set_tweak: TweakName.Sound,
value: "default",
},
{
set_tweak: TweakName.Highlight,
},
],
enabled: true,
pattern: "coffee",
rule_id: "coffee",
default: false,
},
{
actions: [
PushRuleActionName.Notify,
{
set_tweak: TweakName.Sound,
value: "default",
},
{
set_tweak: TweakName.Highlight,
},
],
conditions: [
{
kind: ConditionKind.ContainsDisplayName,
},
],
enabled: true,
default: true,
rule_id: ".m.rule.contains_display_name",
},
{
actions: [
PushRuleActionName.Notify,
{
set_tweak: TweakName.Sound,
value: "default",
},
],
conditions: [
{
is: "2",
kind: ConditionKind.RoomMemberCount,
},
],
enabled: true,
rule_id: ".m.rule.room_one_to_one",
default: true,
},
],
room: [],
sender: [],
underride: [
{
actions: [
PushRuleActionName.Notify,
{
set_tweak: TweakName.Highlight,
value: false,
},
],
conditions: [],
enabled: true,
rule_id: "user-defined",
default: false,
},
msc3914RoomCallRule,
{
actions: [
PushRuleActionName.Notify,
{
set_tweak: TweakName.Highlight,
value: false,
},
],
conditions: [],
enabled: true,
rule_id: ".m.rule.fallback",
default: true,
},
],
},
});
// By the time we get here, we expect the PushProcessor to have merged the new .m.rule.is_room_mention rule into the existing list of rules.
// Check that has happened, and that it is in the right place.
const containsDisplayNameRuleIdx = pushRules.global.override?.findIndex(
(rule) => rule.rule_id === RuleId.ContainsDisplayName,
);
expect(containsDisplayNameRuleIdx).toBeGreaterThan(-1);
const isRoomMentionRuleIdx = pushRules.global.override?.findIndex(
(rule) => rule.rule_id === RuleId.IsRoomMention,
);
expect(isRoomMentionRuleIdx).toBeGreaterThan(-1);
const mReactionRuleIdx = pushRules.global.override?.findIndex((rule) => rule.rule_id === ".m.rule.reaction");
expect(mReactionRuleIdx).toBeGreaterThan(-1);
expect(containsDisplayNameRuleIdx).toBeLessThan(isRoomMentionRuleIdx!);
expect(isRoomMentionRuleIdx).toBeLessThan(mReactionRuleIdx!);
expect(pushRules.global.override?.map((r) => r.rule_id)).toEqual([
".m.rule.master",
"coffee",
".m.rule.contains_display_name",
".m.rule.room_one_to_one",
".m.rule.is_room_mention",
".m.rule.reaction",
".org.matrix.msc3786.rule.room.server_acl",
]);
expect(pushRules.global.underride?.map((r) => r.rule_id)).toEqual([
"user-defined",
".org.matrix.msc3914.rule.room.call",
// Assert that unknown default rules are maintained
".m.rule.fallback",
]);
});
it("should add missing msc3914 rule in correct place", () => {
const pushRules = PushProcessor.rewriteDefaultRules({
device: {},
global: {
// Sample push rules from a Synapse user.
// Note that rules 2 and 3 are backwards, this will trigger a warning in the console.
underride: [
{
conditions: [
{
kind: "event_match",
key: "type",
pattern: "m.call.invite",
},
],
actions: [
"notify",
{
set_tweak: "sound",
value: "ring",
},
{
set_tweak: "highlight",
value: false,
},
],
rule_id: ".m.rule.call",
default: true,
enabled: true,
},
{
conditions: [
{
kind: "event_match",
key: "type",
pattern: "m.room.message",
},
{
kind: "room_member_count",
is: "2",
},
],
actions: [
"notify",
{
set_tweak: "sound",
value: "TEST1",
},
{
set_tweak: "highlight",
value: false,
},
],
rule_id: ".m.rule.room_one_to_one",
default: true,
enabled: true,
},
{
conditions: [
{
kind: "event_match",
key: "type",
pattern: "m.room.encrypted",
},
{
kind: "room_member_count",
is: "2",
},
],
actions: [
"notify",
{
set_tweak: "sound",
value: "TEST2",
},
{
set_tweak: "highlight",
value: false,
},
],
rule_id: ".m.rule.encrypted_room_one_to_one",
default: true,
enabled: true,
},
{
conditions: [
{
kind: "event_match",
key: "type",
pattern: "m.room.message",
},
],
actions: ["dont_notify"],
rule_id: ".m.rule.message",
default: true,
enabled: true,
},
{
conditions: [
{
kind: "event_match",
key: "type",
pattern: "m.room.encrypted",
},
],
actions: ["dont_notify"],
rule_id: ".m.rule.encrypted",
default: true,
enabled: true,
},
{
conditions: [
{
kind: "event_match",
key: "type",
pattern: "im.vector.modular.widgets",
},
{
kind: "event_match",
key: "content.type",
pattern: "jitsi",
},
{
kind: "event_match",
key: "state_key",
pattern: "*",
},
],
actions: [
"notify",
{
set_tweak: "highlight",
value: false,
},
],
rule_id: ".im.vector.jitsi",
default: true,
enabled: true,
},
] as IPushRule[],
},
});
expect(pushRules.global.underride?.map((r) => r.rule_id)).toEqual([
".m.rule.call",
".org.matrix.msc3914.rule.room.call",
".m.rule.room_one_to_one",
".m.rule.encrypted_room_one_to_one",
".m.rule.message",
".m.rule.encrypted",
".im.vector.jitsi",
]);
});
});
+5 -2
View File
@@ -25,10 +25,11 @@ import { EventType, RelationType, UNSTABLE_MSC2716_MARKER } from "../../src/@typ
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { M_BEACON } from "../../src/@types/beacon";
import { MatrixClient } from "../../src/client";
import { DecryptionError } from "../../src/crypto/algorithms";
import { defer } from "../../src/utils";
import { Room } from "../../src/models/room";
import { KnownMembership } from "../../src/@types/membership";
import { DecryptionFailureCode } from "../../src/crypto-api";
import { DecryptionError } from "../../src/common-crypto/CryptoBackend";
describe("RoomState", function () {
const roomId = "!foo:bar";
@@ -1040,7 +1041,9 @@ describe("RoomState", function () {
content: beacon1RelationContent,
});
jest.spyOn(failedDecryptionRelatedEvent, "isDecryptionFailure").mockReturnValue(true);
mockClient.decryptEventIfNeeded.mockRejectedValue(new DecryptionError("ERR", "msg"));
mockClient.decryptEventIfNeeded.mockRejectedValue(
new DecryptionError(DecryptionFailureCode.UNKNOWN_ERROR, "msg"),
);
// spy on event.once
const eventOnceSpy = jest.spyOn(failedDecryptionRelatedEvent, "once");
+73 -23
View File
@@ -2831,11 +2831,41 @@ describe("Room", function () {
// XXX: If we add the relation to the thread response before the thread finishes fetching via /relations
// then the test will fail
await emitPromise(room, ThreadEvent.Update);
await emitPromise(room, ThreadEvent.Update);
await Promise.all([emitPromise(room, ThreadEvent.Update), room.addLiveEvents([threadResponseEdit])]);
expect(thread.replyToEvent!.getContent().body).toBe(threadResponseEdit.getContent()["m.new_content"].body);
});
it("emits event for the first event added to a thread", async () => {
room.client.supportsThreads = () => true;
Thread.setServerSideSupport(FeatureSupport.Stable);
const threadRoot = mkMessage();
const threadResponse1 = mkThreadResponse(threadRoot);
await room.addLiveEvents([threadRoot]);
const onEvent = jest.fn();
room.on(RoomEvent.Timeline, onEvent);
await room.addLiveEvents([threadResponse1]);
expect(onEvent).toHaveBeenCalled();
});
it("contains the events added as soon as it's created", async () => {
room.client.supportsThreads = () => true;
Thread.setServerSideSupport(FeatureSupport.Stable);
const threadRoot = mkMessage();
const threadResponse1 = mkThreadResponse(threadRoot);
const newThreadEventPromise = emitPromise(room, ThreadEvent.New);
await room.addLiveEvents([threadRoot, threadResponse1]);
const thread = await newThreadEventPromise;
expect(thread.timeline).toContain(threadResponse1);
});
it("Redactions to thread responses decrement the length", async () => {
room.client.supportsThreads = () => true;
Thread.setServerSideSupport(FeatureSupport.Stable);
@@ -2864,7 +2894,6 @@ describe("Room", function () {
let prom = emitPromise(room, ThreadEvent.New);
await room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]);
const thread = await prom;
await emitPromise(room, ThreadEvent.Update);
expect(thread).toHaveLength(2);
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
@@ -2929,6 +2958,10 @@ describe("Room", function () {
},
});
room.client.fetchRelations = jest.fn().mockResolvedValue({
chunk: [threadResponse2Reaction.event, threadResponse2.event, threadResponse1.event],
});
const prom = emitPromise(room, ThreadEvent.New);
await room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
const thread = await prom;
@@ -2969,18 +3002,20 @@ describe("Room", function () {
},
});
let prom = emitPromise(room, ThreadEvent.New);
const prom = emitPromise(room, ThreadEvent.New);
await room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
const thread = await prom;
await emitPromise(room, ThreadEvent.Update);
expect(thread).toHaveLength(2);
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
prom = emitPromise(room, ThreadEvent.Update);
const threadRootRedaction = mkRedaction(threadRoot);
await room.addLiveEvents([threadRootRedaction]);
await prom;
// We can't wait for a thread update here because there shouldn't be one (which is
// what we're asserting). Flush any promises to try to get more certainty that an
// update is not happening some time after the event is added.
await flushPromises();
expect(thread).toHaveLength(2);
});
@@ -3058,7 +3093,6 @@ describe("Room", function () {
await emitPromise(room, ThreadEvent.Update);
const threadResponse2Redaction = mkRedaction(threadResponse2);
await emitPromise(room, ThreadEvent.Update);
await room.addLiveEvents([threadResponse2Redaction]);
expect(thread).toHaveLength(1);
expect(thread.replyToEvent!.getId()).toBe(threadResponse1.getId());
@@ -3514,7 +3548,7 @@ describe("Room", function () {
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Highlight);
room.resetThreadUnreadNotificationCount();
room.resetThreadUnreadNotificationCountFromSync();
expect(room.threadsAggregateNotificationType).toBe(null);
@@ -3531,16 +3565,6 @@ describe("Room", function () {
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Highlight);
});
it("partially resets room notifications", () => {
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 666);
room.setThreadUnreadNotificationCount("456", NotificationCountType.Highlight, 123);
room.resetThreadUnreadNotificationCount(["123"]);
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBe(666);
expect(room.getThreadUnreadNotificationCount("456", NotificationCountType.Highlight)).toBe(0);
});
it("emits event on notifications reset", () => {
const cb = jest.fn();
@@ -3549,7 +3573,7 @@ describe("Room", function () {
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 666);
room.setThreadUnreadNotificationCount("456", NotificationCountType.Highlight, 123);
room.resetThreadUnreadNotificationCount();
room.resetThreadUnreadNotificationCountFromSync();
expect(cb).toHaveBeenLastCalledWith();
});
@@ -3571,10 +3595,10 @@ describe("Room", function () {
});
it("lets you reset", () => {
room.setThreadUnreadNotificationCount("123", NotificationCountType.Highlight, 1);
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 1);
expect(room.hasThreadUnreadNotification()).toBe(true);
room.resetThreadUnreadNotificationCount();
room.resetThreadUnreadNotificationCountFromSync();
expect(room.hasThreadUnreadNotification()).toBe(false);
});
@@ -3602,12 +3626,38 @@ describe("Room", function () {
it("allows reset", () => {
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Total, 1);
room.setThreadUnreadNotificationCount("$456", NotificationCountType.Total, 1);
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Total);
room.resetThreadUnreadNotificationCountFromSync();
expect(room.threadsAggregateNotificationType).toBeNull();
});
it("retains highlight for encrypted rooms on reset", () => {
room.hasEncryptionStateEvent = jest.fn().mockReturnValue(true);
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Total, 2);
room.setThreadUnreadNotificationCount("$456", NotificationCountType.Total, 1);
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Highlight, 1);
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Highlight);
room.resetThreadUnreadNotificationCount();
room.resetThreadUnreadNotificationCountFromSync();
expect(room.threadsAggregateNotificationType).toBeNull();
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Highlight);
});
it("resets highlight for unencrypted rooms on reset", () => {
room.hasEncryptionStateEvent = jest.fn().mockReturnValue(false);
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Total, 2);
room.setThreadUnreadNotificationCount("$456", NotificationCountType.Total, 1);
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Highlight, 1);
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Highlight);
room.resetThreadUnreadNotificationCountFromSync();
expect(room.threadsAggregateNotificationType).toBe(null);
expect(room.getThreadUnreadNotificationCount("$123", NotificationCountType.Highlight)).toBe(0);
});
});
+120
View File
@@ -0,0 +1,120 @@
/*
Copyright 2024 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 {
decryptExistingEvent,
mkDecryptionFailureMatrixEvent,
mkEncryptedMatrixEvent,
mkMatrixEvent,
} from "../../src/testing";
import { EventType } from "../../src";
import { DecryptionFailureCode } from "../../src/crypto-api";
describe("testing", () => {
describe("mkMatrixEvent", () => {
it("makes an event", () => {
const event = mkMatrixEvent({
content: { body: "blah" },
sender: "@alice:test",
type: EventType.RoomMessage,
roomId: "!test:room",
});
expect(event.getContent()).toEqual({ body: "blah" });
expect(event.sender?.userId).toEqual("@alice:test");
expect(event.isState()).toBe(false);
});
it("makes a state event", () => {
const event = mkMatrixEvent({
content: { body: "blah" },
sender: "@alice:test",
type: EventType.RoomTopic,
roomId: "!test:room",
stateKey: "",
});
expect(event.getContent()).toEqual({ body: "blah" });
expect(event.sender?.userId).toEqual("@alice:test");
expect(event.isState()).toBe(true);
expect(event.getStateKey()).toEqual("");
});
});
describe("mkEncryptedMatrixEvent", () => {
it("makes an event", async () => {
const event = await mkEncryptedMatrixEvent({
plainContent: { body: "blah" },
sender: "@alice:test",
plainType: EventType.RoomMessage,
roomId: "!test:room",
});
expect(event.sender?.userId).toEqual("@alice:test");
expect(event.isEncrypted()).toBe(true);
expect(event.isDecryptionFailure()).toBe(false);
expect(event.decryptionFailureReason).toBe(null);
expect(event.getContent()).toEqual({ body: "blah" });
expect(event.getType()).toEqual("m.room.message");
});
});
describe("mkDecryptionFailureMatrixEvent", () => {
it("makes an event", async () => {
const event = await mkDecryptionFailureMatrixEvent({
sender: "@alice:test",
roomId: "!test:room",
code: DecryptionFailureCode.UNKNOWN_ERROR,
msg: "blah",
});
expect(event.sender?.userId).toEqual("@alice:test");
expect(event.isEncrypted()).toBe(true);
expect(event.isDecryptionFailure()).toBe(true);
expect(event.decryptionFailureReason).toEqual(DecryptionFailureCode.UNKNOWN_ERROR);
expect(event.getContent()).toEqual({
body: "** Unable to decrypt: DecryptionError: blah **",
msgtype: "m.bad.encrypted",
});
expect(event.getType()).toEqual("m.room.message");
expect(event.isState()).toBe(false);
});
});
describe("decryptExistingEvent", () => {
it("decrypts an event", async () => {
const event = await mkDecryptionFailureMatrixEvent({
sender: "@alice:test",
roomId: "!test:room",
code: DecryptionFailureCode.UNKNOWN_ERROR,
msg: "blah",
});
expect(event.isEncrypted()).toBe(true);
expect(event.isDecryptionFailure()).toBe(true);
await decryptExistingEvent(event, {
plainContent: { body: "blah" },
plainType: "m.room.test",
});
expect(event.isEncrypted()).toBe(true);
expect(event.isDecryptionFailure()).toBe(false);
expect(event.decryptionFailureReason).toBe(null);
expect(event.getContent()).toEqual({ body: "blah" });
expect(event.getType()).toEqual("m.room.test");
});
});
});
+5
View File
@@ -15,3 +15,8 @@ limitations under the License.
*/
export type NonEmptyArray<T> = [T, ...T[]];
// Based on https://stackoverflow.com/a/53229857/3532235
export type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
export type XOR<T, U> = T | U extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
+116 -13
View File
@@ -15,6 +15,49 @@ limitations under the License.
*/
import { UnstableValue } from "../NamespacedValue";
import {
PolicyRuleEventContent,
RoomAvatarEventContent,
RoomCanonicalAliasEventContent,
RoomCreateEventContent,
RoomEncryptionEventContent,
RoomGuestAccessEventContent,
RoomHistoryVisibilityEventContent,
RoomJoinRulesEventContent,
RoomMemberEventContent,
RoomNameEventContent,
RoomPinnedEventsEventContent,
RoomPowerLevelsEventContent,
RoomServerAclEventContent,
RoomThirdPartyInviteEventContent,
RoomTombstoneEventContent,
RoomTopicEventContent,
SpaceChildEventContent,
SpaceParentEventContent,
} from "./state_events";
import {
ExperimentalGroupCallRoomMemberState,
IGroupCallRoomMemberState,
IGroupCallRoomState,
} from "../webrtc/groupCall";
import { MSC3089EventContent } from "../models/MSC3089Branch";
import { M_BEACON, M_BEACON_INFO, MBeaconEventContent, MBeaconInfoEventContent } from "./beacon";
import { XOR } from "./common";
import { ReactionEventContent, RoomMessageEventContent, StickerEventContent } from "./events";
import {
MCallAnswer,
MCallBase,
MCallCandidates,
MCallHangupReject,
MCallInviteNegotiate,
MCallReplacesEvent,
MCallSelectAnswer,
SDPStreamMetadata,
SDPStreamMetadataKey,
} from "../webrtc/callEventTypes";
import { EncryptionKeysEventContent, ICallNotifyContent } from "../matrixrtc/types";
import { EncryptedFile } from "./media";
import { M_POLL_END, M_POLL_START, PollEndEventContent, PollStartEventContent } from "./polls";
export enum EventType {
// Room state events
@@ -35,6 +78,11 @@ export enum EventType {
RoomTombstone = "m.room.tombstone",
RoomPredecessor = "org.matrix.msc3946.room_predecessor",
// Moderation policy lists
PolicyRuleUser = "m.policy.rule.user",
PolicyRuleRoom = "m.policy.rule.room",
PolicyRuleServer = "m.policy.rule.server",
SpaceChild = "m.space.child",
SpaceParent = "m.space.parent",
@@ -103,6 +151,10 @@ export enum RelationType {
Annotation = "m.annotation",
Replace = "m.replace",
Reference = "m.reference",
// Don't use this yet: it's only the stable version. The code still assumes we support the unstable prefix and,
// moreover, our tests currently use the unstable prefix. Use THREAD_RELATION_TYPE.name.
// Once we support *only* the stable prefix, THREAD_RELATION_TYPE can die and we can switch to this.
Thread = "m.thread",
}
@@ -246,17 +298,68 @@ export const LOCAL_NOTIFICATION_SETTINGS_PREFIX = new UnstableValue(
*/
export const UNSIGNED_THREAD_ID_FIELD = new UnstableValue("thread_id", "org.matrix.msc4023.thread_id");
export interface IEncryptedFile {
url: string;
mimetype?: string;
key: {
alg: string;
key_ops: string[]; // eslint-disable-line camelcase
kty: string;
k: string;
ext: boolean;
};
iv: string;
hashes: { [alg: string]: string };
v: string;
/**
* @deprecated in favour of {@link EncryptedFile}
*/
export type IEncryptedFile = EncryptedFile;
/**
* Mapped type from event type to content type for all specified non-state room events.
*/
export interface TimelineEvents {
[EventType.RoomMessage]: RoomMessageEventContent;
[EventType.Sticker]: StickerEventContent;
[EventType.Reaction]: ReactionEventContent;
[EventType.CallReplaces]: MCallReplacesEvent;
[EventType.CallAnswer]: MCallAnswer;
[EventType.CallSelectAnswer]: MCallSelectAnswer;
[EventType.CallNegotiate]: Omit<MCallInviteNegotiate, "offer">;
[EventType.CallInvite]: MCallInviteNegotiate;
[EventType.CallCandidates]: MCallCandidates;
[EventType.CallHangup]: MCallHangupReject;
[EventType.CallReject]: MCallHangupReject;
[EventType.CallSDPStreamMetadataChangedPrefix]: MCallBase & { [SDPStreamMetadataKey]: SDPStreamMetadata };
[EventType.CallEncryptionKeysPrefix]: EncryptionKeysEventContent;
[EventType.CallNotify]: ICallNotifyContent;
[M_BEACON.name]: MBeaconEventContent;
[M_POLL_START.name]: PollStartEventContent;
[M_POLL_END.name]: PollEndEventContent;
}
/**
* Mapped type from event type to content type for all specified room state events.
*/
export interface StateEvents {
[EventType.RoomCanonicalAlias]: RoomCanonicalAliasEventContent;
[EventType.RoomCreate]: RoomCreateEventContent;
[EventType.RoomJoinRules]: RoomJoinRulesEventContent;
[EventType.RoomMember]: RoomMemberEventContent;
// XXX: Spec says this event has 3 required fields but kicking such an invitation requires sending `{}`
[EventType.RoomThirdPartyInvite]: XOR<RoomThirdPartyInviteEventContent, {}>;
[EventType.RoomPowerLevels]: RoomPowerLevelsEventContent;
[EventType.RoomName]: RoomNameEventContent;
[EventType.RoomTopic]: RoomTopicEventContent;
[EventType.RoomAvatar]: RoomAvatarEventContent;
[EventType.RoomPinnedEvents]: RoomPinnedEventsEventContent;
[EventType.RoomEncryption]: RoomEncryptionEventContent;
[EventType.RoomHistoryVisibility]: RoomHistoryVisibilityEventContent;
[EventType.RoomGuestAccess]: RoomGuestAccessEventContent;
[EventType.RoomServerAcl]: RoomServerAclEventContent;
[EventType.RoomTombstone]: RoomTombstoneEventContent;
[EventType.SpaceChild]: SpaceChildEventContent;
[EventType.SpaceParent]: SpaceParentEventContent;
[EventType.PolicyRuleUser]: XOR<PolicyRuleEventContent, {}>;
[EventType.PolicyRuleRoom]: XOR<PolicyRuleEventContent, {}>;
[EventType.PolicyRuleServer]: XOR<PolicyRuleEventContent, {}>;
// MSC3401
[EventType.GroupCallPrefix]: IGroupCallRoomState;
[EventType.GroupCallMemberPrefix]: XOR<IGroupCallRoomMemberState, ExperimentalGroupCallRoomMemberState>;
// MSC3089
[UNSTABLE_MSC3089_BRANCH.name]: MSC3089EventContent;
// MSC3672
[M_BEACON_INFO.name]: MBeaconInfoEventContent;
}
+119
View File
@@ -0,0 +1,119 @@
/*
Copyright 2024 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 { MsgType, RelationType } from "./event";
import { FileInfo, ImageInfo, MediaEventContent } from "./media";
import { XOR } from "./common";
interface BaseTimelineEvent {
"body": string;
"m.mentions"?: {
user_ids?: string[];
room?: boolean;
};
}
interface ReplyEvent {
"m.relates_to"?: {
"m.in_reply_to"?: {
event_id: string;
};
};
}
interface NoRelationEvent {
"m.new_content"?: never;
"m.relates_to"?: never;
}
/**
* Partial content format of timeline events with rel_type `m.replace`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#event-replacements
*/
export interface ReplacementEvent<T> {
"m.new_content": T;
"m.relates_to": {
event_id: string;
rel_type: RelationType.Replace;
};
}
/**
* Partial content format of timeline events with rel_type other than `m.replace`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#forming-relationships-between-events
*/
export interface RelationEvent {
"m.new_content"?: never;
"m.relates_to": {
event_id: string;
rel_type: Exclude<RelationType, RelationType.Replace>;
};
}
/**
* Content format of timeline events with type `m.room.message` and `msgtype` `m.text`, `m.emote`, or `m.notice`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mroommessage
*/
export interface RoomMessageTextEventContent extends BaseTimelineEvent {
msgtype: MsgType.Text | MsgType.Emote | MsgType.Notice;
format?: "org.matrix.custom.html";
formatted_body?: string;
}
/**
* Content format of timeline events with type `m.room.message` and `msgtype` `m.location`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mlocation
*/
export interface RoomMessageLocationEventContent extends BaseTimelineEvent {
body: string;
geo_uri: string;
info: Pick<FileInfo, "thumbnail_info" | "thumbnail_file" | "thumbnail_url">;
msgtype: MsgType.Location;
}
type MessageEventContent = RoomMessageTextEventContent | RoomMessageLocationEventContent | MediaEventContent;
export type RoomMessageEventContent = BaseTimelineEvent &
XOR<XOR<ReplacementEvent<MessageEventContent>, RelationEvent>, XOR<ReplyEvent, NoRelationEvent>> &
MessageEventContent;
/**
* Content format of timeline events with type `m.sticker`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#msticker
*/
export interface StickerEventContent extends BaseTimelineEvent {
body: string;
info: ImageInfo;
url: string;
}
/**
* Content format of timeline events with type `m.reaction`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mreaction
*/
export interface ReactionEventContent {
"m.relates_to": {
event_id: string;
key: string;
rel_type: RelationType.Annotation;
};
}
-14
View File
@@ -14,20 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
export interface IImageInfo {
size?: number;
mimetype?: string;
thumbnail_info?: {
// eslint-disable-line camelcase
w?: number;
h?: number;
size?: number;
mimetype?: string;
};
w?: number;
h?: number;
}
export enum Visibility {
Public = "public",
Private = "private",
+144
View File
@@ -0,0 +1,144 @@
/*
Copyright 2024 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 { RoomType } from "./event";
import { GuestAccess, HistoryVisibility, RestrictedAllowType } from "./partials";
import { ImageInfo } from "./media";
import { PolicyRecommendation } from "../models/invites-ignorer";
export interface RoomCanonicalAliasEventContent {
alias?: string;
alt_aliases?: string[];
}
export interface RoomCreateEventContent {
"creator"?: string;
"m.federate"?: boolean;
"predecessor"?: {
event_id: string;
room_id: string;
};
"room_version"?: string;
"type"?: RoomType;
}
export interface RoomJoinRulesEventContent {
allow?: {
room_id: string;
type: RestrictedAllowType;
}[];
}
export interface RoomMemberEventContent {
avatar_url?: string;
displayname?: string;
is_direct?: boolean;
join_authorised_via_users_server?: string;
membership: "invite" | "join" | "knock" | "leave" | "ban";
reason?: string;
third_party_invite?: {
display_name: string;
signed: {
mxid: string;
token: string;
ts: number;
};
};
}
export interface RoomThirdPartyInviteEventContent {
display_name: string;
key_validity_url: string;
public_key: string;
public_keys: {
key_validity_url?: string;
public_key: string;
}[];
}
export interface RoomPowerLevelsEventContent {
ban?: number;
events?: { [eventType: string]: number };
events_default?: number;
invite?: number;
kick?: number;
notifications?: {
room?: number;
};
redact?: number;
state_default?: number;
users?: { [userId: string]: number };
users_default?: number;
}
export interface RoomNameEventContent {
name: string;
}
export interface RoomTopicEventContent {
topic: string;
}
export interface RoomAvatarEventContent {
url?: string;
info?: ImageInfo;
}
export interface RoomPinnedEventsEventContent {
pinned: string[];
}
export interface RoomEncryptionEventContent {
algorithm: "m.megolm.v1.aes-sha2";
rotation_period_ms?: number;
rotation_period_msgs?: number;
}
export interface RoomHistoryVisibilityEventContent {
history_visibility: HistoryVisibility;
}
export interface RoomGuestAccessEventContent {
guest_access: GuestAccess;
}
export interface RoomServerAclEventContent {
allow?: string[];
allow_ip_literals?: boolean;
deny?: string[];
}
export interface RoomTombstoneEventContent {
body: string;
replacement_room: string;
}
export interface SpaceChildEventContent {
order?: string;
suggested?: boolean;
via?: string[];
}
export interface SpaceParentEventContent {
canonical?: boolean;
via?: string[];
}
export interface PolicyRuleEventContent {
entity: string;
reason: string;
recommendation: PolicyRecommendation;
}
+2 -2
View File
@@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { IAuthDict, IAuthData } from "../interactive-auth";
import { AuthDict, IAuthData } from "../interactive-auth";
/**
* Helper type to represent HTTP request body for a UIA enabled endpoint
*/
export type UIARequest<T> = T & {
auth?: IAuthDict;
auth?: AuthDict;
};
/**
+65 -139
View File
@@ -35,14 +35,14 @@ import {
import { StubStore } from "./store/stub";
import { CallEvent, CallEventHandlerMap, createNewMatrixCall, MatrixCall, supportsMatrixCall } from "./webrtc/call";
import { Filter, IFilterDefinition, IRoomEventFilter } from "./filter";
import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from "./webrtc/callEventHandler";
import { CallEventHandler, CallEventHandlerEvent, CallEventHandlerEventHandlerMap } from "./webrtc/callEventHandler";
import {
GroupCallEventHandler,
GroupCallEventHandlerEvent,
GroupCallEventHandlerEventHandlerMap,
} from "./webrtc/groupCallEventHandler";
import * as utils from "./utils";
import { replaceParam, QueryDict, sleep, noUnsafeEventProps, safeSet } from "./utils";
import { noUnsafeEventProps, QueryDict, replaceParam, safeSet, sleep } from "./utils";
import { Direction, EventTimeline } from "./models/event-timeline";
import { IActionsObject, PushProcessor } from "./pushprocessor";
import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery";
@@ -64,12 +64,12 @@ import {
IdentityPrefix,
IHttpOpts,
IRequestOpts,
TokenRefreshFunction,
MatrixError,
MatrixHttpApi,
MediaPrefix,
Method,
retryNetworkOperation,
TokenRefreshFunction,
Upload,
UploadOpts,
UploadResponse,
@@ -83,7 +83,6 @@ import {
ICryptoCallbacks,
IRoomKeyRequestBody,
isCryptoAvailable,
VerificationMethod,
} from "./crypto";
import { DeviceInfo } from "./crypto/deviceinfo";
import { decodeRecoveryKey } from "./crypto/recoverykey";
@@ -145,11 +144,13 @@ import {
RelationType,
RoomCreateTypeField,
RoomType,
StateEvents,
TimelineEvents,
UNSTABLE_MSC3088_ENABLED,
UNSTABLE_MSC3088_PURPOSE,
UNSTABLE_MSC3089_TREE_SUBTYPE,
} from "./@types/event";
import { IdServerUnbindResult, IImageInfo, JoinRule, Preset, Visibility } from "./@types/partials";
import { GuestAccess, HistoryVisibility, IdServerUnbindResult, JoinRule, Preset, Visibility } from "./@types/partials";
import { EventMapper, eventMapperFor, MapperOpts } from "./event-mapper";
import { randomString } from "./randomstring";
import { BackupManager, IKeyBackup, IKeyBackupCheck, IPreparedKeyBackupVersion, TrustInfo } from "./crypto/backup";
@@ -223,6 +224,8 @@ import { RegisterRequest, RegisterResponse } from "./@types/registration";
import { MatrixRTCSessionManager } from "./matrixrtc/MatrixRTCSessionManager";
import { getRelationsThreadFilter } from "./thread-utils";
import { KnownMembership, Membership } from "./@types/membership";
import { RoomMessageEventContent, StickerEventContent } from "./@types/events";
import { ImageInfo } from "./@types/media";
export type Store = IStore;
@@ -369,8 +372,10 @@ export interface ICreateClientOpts {
* Verification methods we should offer to the other side when performing an interactive verification.
* If unset, we will offer all known methods. Currently these are: showing a QR code, scanning a QR code, and SAS
* (aka "emojis").
*
* See {@link types.VerificationMethod} for a set of useful constants for this parameter.
*/
verificationMethods?: Array<VerificationMethod>;
verificationMethods?: Array<string>;
/**
* Whether relaying calls through a TURN server should be forced. Default false.
@@ -499,11 +504,6 @@ export interface IStartClientOpts {
*/
clientWellKnownPollPeriod?: number;
/**
* @deprecated use `threadSupport` instead
*/
experimentalThreadSupport?: boolean;
/**
* Will organises events in threaded conversations when
* a thread relation is encountered
@@ -1262,7 +1262,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
protected ongoingScrollbacks: { [roomId: string]: { promise?: Promise<Room>; errorTs?: number } } = {};
protected notifTimelineSet: EventTimelineSet | null = null;
protected cryptoStore?: CryptoStore;
protected verificationMethods?: VerificationMethod[];
protected verificationMethods?: string[];
protected fallbackICEServerAllowed = false;
protected syncApi?: SlidingSyncSdk | SyncApi;
public roomNameGenerator?: ICreateClientOpts["roomNameGenerator"];
@@ -1437,55 +1437,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
fixNotificationCountOnDecryption(this, event);
});
// Like above, we have to listen for read receipts from ourselves in order to
// correctly handle notification counts on encrypted rooms.
// This fixes https://github.com/vector-im/element-web/issues/9421
this.on(RoomEvent.Receipt, (event, room) => {
if (room?.hasEncryptionStateEvent()) {
// Figure out if we've read something or if it's just informational
const content = event.getContent();
const isSelf =
Object.keys(content).filter((eid) => {
for (const [key, value] of Object.entries(content[eid])) {
if (!utils.isSupportedReceiptType(key)) continue;
if (!value) continue;
if (Object.keys(value).includes(this.getUserId()!)) return true;
}
return false;
}).length > 0;
if (!isSelf) return;
// Work backwards to determine how many events are unread. We also set
// a limit for how back we'll look to avoid spinning CPU for too long.
// If we hit the limit, we assume the count is unchanged.
const maxHistory = 20;
const events = room.getLiveTimeline().getEvents();
let highlightCount = 0;
for (let i = events.length - 1; i >= 0; i--) {
if (i === events.length - maxHistory) return; // limit reached
const event = events[i];
if (room.hasUserReadEvent(this.getUserId()!, event.getId()!)) {
// If the user has read the event, then the counting is done.
break;
}
const pushActions = this.getPushActionsForEvent(event);
highlightCount += pushActions?.tweaks?.highlight ? 1 : 0;
}
// Note: we don't need to handle 'total' notifications because the counts
// will come from the server.
room.setUnreadNotificationCount(NotificationCountType.Highlight, highlightCount);
}
});
this.ignoredInvites = new IgnoredInvites(this);
this._secretStorage = new ServerSideSecretStorageImpl(this, opts.cryptoCallbacks ?? {});
@@ -1575,19 +1526,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
this.syncApi = new SyncApi(this, this.clientOpts, this.buildSyncApiOptions());
}
if (this.clientOpts.hasOwnProperty("experimentalThreadSupport")) {
this.logger.warn("`experimentalThreadSupport` has been deprecated, use `threadSupport` instead");
}
// If `threadSupport` is omitted and the deprecated `experimentalThreadSupport` has been passed
// We should fallback to that value for backwards compatibility purposes
if (
!this.clientOpts.hasOwnProperty("threadSupport") &&
this.clientOpts.hasOwnProperty("experimentalThreadSupport")
) {
this.clientOpts.threadSupport = this.clientOpts.experimentalThreadSupport;
}
this.syncApi.sync().catch((e) => this.logger.info("Sync startup aborted with an error:", e));
if (this.clientOpts.clientWellKnownPollPeriod !== undefined) {
@@ -2817,11 +2755,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
// deprecated: use requestVerification instead
public legacyDeviceVerification(
userId: string,
deviceId: string,
method: VerificationMethod,
): Promise<VerificationRequest> {
public legacyDeviceVerification(userId: string, deviceId: string, method: string): Promise<VerificationRequest> {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
@@ -4549,7 +4483,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param roomId - the room to update power levels in
* @param userId - the ID of the user or users to update power levels of
* @param powerLevel - the numeric power level to update given users to
* @param event - deprecated and no longer used.
* @returns Promise which resolves: to an ISendEventResponse object
* @returns Rejects: with an error response.
*/
@@ -4557,10 +4490,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
roomId: string,
userId: string | string[],
powerLevel: number | undefined,
/**
* @deprecated no longer needed, unused.
*/
event?: MatrixEvent | null,
): Promise<ISendEventResponse> {
let content: IPowerLevelsContent | undefined;
if (this.clientRunning && this.isInitialSyncComplete()) {
@@ -4624,12 +4553,17 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.sendStateEvent(roomId, M_BEACON_INFO.name, beaconInfoContent, this.getUserId()!);
}
public sendEvent(roomId: string, eventType: string, content: IContent, txnId?: string): Promise<ISendEventResponse>;
public sendEvent(
public sendEvent<K extends keyof TimelineEvents>(
roomId: string,
eventType: K,
content: TimelineEvents[K],
txnId?: string,
): Promise<ISendEventResponse>;
public sendEvent<K extends keyof TimelineEvents>(
roomId: string,
threadId: string | null,
eventType: string,
content: IContent,
eventType: K,
content: TimelineEvents[K],
txnId?: string,
): Promise<ISendEventResponse>;
public sendEvent(
@@ -5016,27 +4950,27 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Promise which resolves: to an ISendEventResponse object
* @returns Rejects: with an error response.
*/
public sendMessage(roomId: string, content: IContent, txnId?: string): Promise<ISendEventResponse>;
public sendMessage(roomId: string, content: RoomMessageEventContent, txnId?: string): Promise<ISendEventResponse>;
public sendMessage(
roomId: string,
threadId: string | null,
content: IContent,
content: RoomMessageEventContent,
txnId?: string,
): Promise<ISendEventResponse>;
public sendMessage(
roomId: string,
threadId: string | null | IContent,
content?: IContent | string,
threadId: string | null | RoomMessageEventContent,
content?: RoomMessageEventContent | string,
txnId?: string,
): Promise<ISendEventResponse> {
if (typeof threadId !== "string" && threadId !== null) {
txnId = content as string;
content = threadId as IContent;
content = threadId as RoomMessageEventContent;
threadId = null;
}
const eventType: string = EventType.RoomMessage;
const sendContent: IContent = content as IContent;
const eventType = EventType.RoomMessage;
const sendContent = content as RoomMessageEventContent;
return this.sendEvent(roomId, threadId as string | null, eventType, sendContent, txnId);
}
@@ -5126,33 +5060,33 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Promise which resolves: to a ISendEventResponse object
* @returns Rejects: with an error response.
*/
public sendImageMessage(roomId: string, url: string, info?: IImageInfo, text?: string): Promise<ISendEventResponse>;
public sendImageMessage(roomId: string, url: string, info?: ImageInfo, text?: string): Promise<ISendEventResponse>;
public sendImageMessage(
roomId: string,
threadId: string | null,
url: string,
info?: IImageInfo,
info?: ImageInfo,
text?: string,
): Promise<ISendEventResponse>;
public sendImageMessage(
roomId: string,
threadId: string | null,
url?: string | IImageInfo,
info?: IImageInfo | string,
url?: string | ImageInfo,
info?: ImageInfo | string,
text = "Image",
): Promise<ISendEventResponse> {
if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) {
text = (info as string) || "Image";
info = url as IImageInfo;
info = url as ImageInfo;
url = threadId as string;
threadId = null;
}
const content = {
msgtype: MsgType.Image,
url: url,
info: info,
url: url as string,
info: info as ImageInfo,
body: text,
};
} satisfies RoomMessageEventContent;
return this.sendMessage(roomId, threadId, content);
}
@@ -5163,34 +5097,34 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public sendStickerMessage(
roomId: string,
url: string,
info?: IImageInfo,
info?: ImageInfo,
text?: string,
): Promise<ISendEventResponse>;
public sendStickerMessage(
roomId: string,
threadId: string | null,
url: string,
info?: IImageInfo,
info?: ImageInfo,
text?: string,
): Promise<ISendEventResponse>;
public sendStickerMessage(
roomId: string,
threadId: string | null,
url?: string | IImageInfo,
info?: IImageInfo | string,
url?: string | ImageInfo,
info?: ImageInfo | string,
text = "Sticker",
): Promise<ISendEventResponse> {
if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) {
text = (info as string) || "Sticker";
info = url as IImageInfo;
info = url as ImageInfo;
url = threadId as string;
threadId = null;
}
const content = {
url: url,
info: info,
url: url as string,
info: info as ImageInfo,
body: text,
};
} satisfies StickerEventContent;
return this.sendEvent(roomId, threadId, EventType.Sticker, content);
}
@@ -6704,7 +6638,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
roomId,
EventType.RoomGuestAccess,
{
guest_access: opts.allowJoin ? "can_join" : "forbidden",
guest_access: opts.allowJoin ? GuestAccess.CanJoin : GuestAccess.Forbidden,
},
"",
);
@@ -6715,7 +6649,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
roomId,
EventType.RoomHistoryVisibility,
{
history_visibility: "world_readable",
history_visibility: HistoryVisibility.WorldReadable,
},
"",
);
@@ -8386,14 +8320,19 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
/**
* Send a state event into a room
* @param roomId - ID of the room to send the event into
* @param eventType - type of the state event to send
* @param content - content of the event to send
* @param stateKey - the stateKey to send into the room
* @param opts - Options for the request function.
* @returns Promise which resolves: TODO
* @returns Rejects: with an error response.
*/
public sendStateEvent(
public sendStateEvent<K extends keyof StateEvents>(
roomId: string,
eventType: string,
content: IContent,
eventType: K,
content: StateEvents[K],
stateKey = "",
opts: IRequestOpts = {},
): Promise<ISendEventResponse> {
@@ -8406,7 +8345,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
if (stateKey !== undefined) {
path = utils.encodeUri(path + "/$stateKey", pathParams);
}
return this.http.authedRequest(Method.Put, path, undefined, content, opts);
return this.http.authedRequest(Method.Put, path, undefined, content as Body, opts);
}
/**
@@ -8492,11 +8431,17 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
since,
...options
}: IRoomDirectoryOptions = {}): Promise<IPublicRoomsResponse> {
const queryParams: QueryDict = { server, limit, since };
if (Object.keys(options).length === 0) {
const queryParams: QueryDict = { server, limit, since };
return this.http.authedRequest(Method.Get, "/publicRooms", queryParams);
} else {
return this.http.authedRequest(Method.Post, "/publicRooms", queryParams, options);
const queryParams: QueryDict = { server };
const body = {
limit,
since,
...options,
};
return this.http.authedRequest(Method.Post, "/publicRooms", queryParams, body);
}
}
@@ -8558,17 +8503,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.http.authedRequest(Method.Get, path);
}
/**
* @returns Promise which resolves: Object with room_id and servers.
* @returns Rejects: with an error response.
* @deprecated use `getRoomIdForAlias` instead
*/
// eslint-disable-next-line camelcase
public resolveRoomAlias(roomAlias: string): Promise<{ room_id: string; servers: string[] }> {
const path = utils.encodeUri("/directory/room/$alias", { $alias: roomAlias });
return this.http.request(Method.Get, path);
}
/**
* Get the visibility of a room in the current HS's room directory
* @returns Promise which resolves: TODO
@@ -8582,7 +8516,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
/**
* Set the visbility of a room in the current HS's room directory
* Set the visibility of a room in the current HS's room directory
* @param visibility - "public" to make the room visible
* in the public directory, or "private" to make
* it invisible.
@@ -9829,14 +9763,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
});
}
/**
* @deprecated use supportsThreads() instead
*/
public supportsExperimentalThreads(): boolean {
this.logger.warn(`supportsExperimentalThreads() is deprecated, use supportThreads() instead`);
return this.clientOpts?.experimentalThreadSupport || false;
}
/**
* A helper to determine thread support
* @returns a boolean to determine if threads are enabled
+41 -5
View File
@@ -17,7 +17,7 @@ limitations under the License.
import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator";
import { IClearEvent, MatrixEvent } from "../models/event";
import { Room } from "../models/room";
import { CryptoApi, ImportRoomKeysOpts } from "../crypto-api";
import { CryptoApi, DecryptionFailureCode, ImportRoomKeysOpts } from "../crypto-api";
import { CrossSigningInfo, UserTrustLevel } from "../crypto/CrossSigning";
import { IEncryptedEventInfo } from "../crypto/api";
import { KeyBackupInfo, KeyBackupSession } from "../crypto-api/keybackup";
@@ -226,10 +226,6 @@ export interface EventDecryptionResult {
* restored from backup)
*/
untrusted?: boolean;
/**
* The sender doesn't authorize the unverified devices to decrypt his messages
*/
encryptedDisabledForUnverifiedDevices?: boolean;
}
/**
@@ -263,3 +259,43 @@ export interface BackupDecryptor {
*/
free(): void;
}
/**
* Exception thrown when decryption fails
*
* @param code - Reason code for the failure.
*
* @param msg - user-visible message describing the problem
*
* @param details - key/value pairs reported in the logs but not shown
* to the user.
*/
export class DecryptionError extends Error {
public readonly detailedString: string;
public constructor(
public readonly code: DecryptionFailureCode,
msg: string,
details?: Record<string, string | Error>,
) {
super(msg);
this.name = "DecryptionError";
this.detailedString = detailedStringForDecryptionError(this, details);
}
}
function detailedStringForDecryptionError(err: DecryptionError, details?: Record<string, string | Error>): string {
let result = err.name + "[msg: " + err.message;
if (details) {
result +=
", " +
Object.keys(details)
.map((k) => k + ": " + details[k])
.join(", ");
}
result += "]";
return result;
}
+7 -7
View File
@@ -30,7 +30,7 @@ import {
LegacyLocationEventContent,
} from "./@types/location";
import { MRoomTopicEventContent, MTopicContent, M_TOPIC } from "./@types/topic";
import { IContent } from "./models/event";
import { RoomMessageEventContent } from "./@types/events";
/**
* Generates the content for a HTML Message event
@@ -38,7 +38,7 @@ import { IContent } from "./models/event";
* @param htmlBody - the HTML representation of the message
* @returns
*/
export function makeHtmlMessage(body: string, htmlBody: string): IContent {
export function makeHtmlMessage(body: string, htmlBody: string): RoomMessageEventContent {
return {
msgtype: MsgType.Text,
format: "org.matrix.custom.html",
@@ -53,7 +53,7 @@ export function makeHtmlMessage(body: string, htmlBody: string): IContent {
* @param htmlBody - the HTML representation of the notice
* @returns
*/
export function makeHtmlNotice(body: string, htmlBody: string): IContent {
export function makeHtmlNotice(body: string, htmlBody: string): RoomMessageEventContent {
return {
msgtype: MsgType.Notice,
format: "org.matrix.custom.html",
@@ -68,7 +68,7 @@ export function makeHtmlNotice(body: string, htmlBody: string): IContent {
* @param htmlBody - the HTML representation of the emote
* @returns
*/
export function makeHtmlEmote(body: string, htmlBody: string): IContent {
export function makeHtmlEmote(body: string, htmlBody: string): RoomMessageEventContent {
return {
msgtype: MsgType.Emote,
format: "org.matrix.custom.html",
@@ -82,7 +82,7 @@ export function makeHtmlEmote(body: string, htmlBody: string): IContent {
* @param body - the plaintext body of the emote
* @returns
*/
export function makeTextMessage(body: string): IContent {
export function makeTextMessage(body: string): RoomMessageEventContent {
return {
msgtype: MsgType.Text,
body: body,
@@ -94,7 +94,7 @@ export function makeTextMessage(body: string): IContent {
* @param body - the plaintext body of the notice
* @returns
*/
export function makeNotice(body: string): IContent {
export function makeNotice(body: string): RoomMessageEventContent {
return {
msgtype: MsgType.Notice,
body: body,
@@ -106,7 +106,7 @@ export function makeNotice(body: string): IContent {
* @param body - the plaintext body of the emote
* @returns
*/
export function makeEmoteMessage(body: string): IContent {
export function makeEmoteMessage(body: string): RoomMessageEventContent {
return {
msgtype: MsgType.Emote,
body: body,
+51
View File
@@ -498,6 +498,57 @@ export interface CryptoApi {
deleteKeyBackupVersion(version: string): Promise<void>;
}
/** A reason code for a failure to decrypt an event. */
export enum DecryptionFailureCode {
/** Message was encrypted with a Megolm session whose keys have not been shared with us. */
MEGOLM_UNKNOWN_INBOUND_SESSION_ID = "MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
/** Message was encrypted with a Megolm session which has been shared with us, but in a later ratchet state. */
OLM_UNKNOWN_MESSAGE_INDEX = "OLM_UNKNOWN_MESSAGE_INDEX",
/** Unknown or unclassified error. */
UNKNOWN_ERROR = "UNKNOWN_ERROR",
/** @deprecated only used in legacy crypto */
MEGOLM_BAD_ROOM = "MEGOLM_BAD_ROOM",
/** @deprecated only used in legacy crypto */
MEGOLM_MISSING_FIELDS = "MEGOLM_MISSING_FIELDS",
/** @deprecated only used in legacy crypto */
OLM_DECRYPT_GROUP_MESSAGE_ERROR = "OLM_DECRYPT_GROUP_MESSAGE_ERROR",
/** @deprecated only used in legacy crypto */
OLM_BAD_ENCRYPTED_MESSAGE = "OLM_BAD_ENCRYPTED_MESSAGE",
/** @deprecated only used in legacy crypto */
OLM_BAD_RECIPIENT = "OLM_BAD_RECIPIENT",
/** @deprecated only used in legacy crypto */
OLM_BAD_RECIPIENT_KEY = "OLM_BAD_RECIPIENT_KEY",
/** @deprecated only used in legacy crypto */
OLM_BAD_ROOM = "OLM_BAD_ROOM",
/** @deprecated only used in legacy crypto */
OLM_BAD_SENDER_CHECK_FAILED = "OLM_BAD_SENDER_CHECK_FAILED",
/** @deprecated only used in legacy crypto */
OLM_BAD_SENDER = "OLM_BAD_SENDER",
/** @deprecated only used in legacy crypto */
OLM_FORWARDED_MESSAGE = "OLM_FORWARDED_MESSAGE",
/** @deprecated only used in legacy crypto */
OLM_MISSING_CIPHERTEXT = "OLM_MISSING_CIPHERTEXT",
/** @deprecated only used in legacy crypto */
OLM_NOT_INCLUDED_IN_RECIPIENTS = "OLM_NOT_INCLUDED_IN_RECIPIENTS",
/** @deprecated only used in legacy crypto */
UNKNOWN_ENCRYPTION_ALGORITHM = "UNKNOWN_ENCRYPTION_ALGORITHM",
}
/**
* Options object for `CryptoApi.bootstrapCrossSigning`.
*/
+6 -5
View File
@@ -18,11 +18,12 @@ import { Account, InboundGroupSession, OutboundGroupSession, Session, Utility }
import { logger, Logger } from "../logger";
import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store";
import * as algorithms from "./algorithms";
import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base";
import { IOlmDevice, IOutboundGroupSessionKey } from "./algorithms/megolm";
import { IMegolmSessionData, OlmGroupSessionExtraData } from "../@types/crypto";
import { IMessage } from "./algorithms/olm";
import { DecryptionFailureCode } from "../crypto-api";
import { DecryptionError } from "../common-crypto/CryptoBackend";
// The maximum size of an event is 65K, and we base64 the content, so this is a
// reasonable approximation to the biggest plaintext we can encrypt.
@@ -1220,8 +1221,8 @@ export class OlmDevice {
this.getInboundGroupSession(roomId, senderKey, sessionId, txn, (session, sessionData, withheld) => {
if (session === null || sessionData === null) {
if (withheld) {
error = new algorithms.DecryptionError(
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
error = new DecryptionError(
DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
calculateWithheldMessage(withheld),
{
session: senderKey + "|" + sessionId,
@@ -1236,8 +1237,8 @@ export class OlmDevice {
res = session.decrypt(body);
} catch (e) {
if ((<Error>e)?.message === "OLM.UNKNOWN_MESSAGE_INDEX" && withheld) {
error = new algorithms.DecryptionError(
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
error = new DecryptionError(
DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
calculateWithheldMessage(withheld),
{
session: senderKey + "|" + sessionId,
+3 -39
View File
@@ -199,45 +199,6 @@ export abstract class DecryptionAlgorithm {
public sendSharedHistoryInboundSessions?(devicesByUser: Map<string, DeviceInfo[]>): Promise<void>;
}
/**
* Exception thrown when decryption fails
*
* @param msg - user-visible message describing the problem
*
* @param details - key/value pairs reported in the logs but not shown
* to the user.
*/
export class DecryptionError extends Error {
public readonly detailedString: string;
public constructor(
public readonly code: string,
msg: string,
details?: Record<string, string | Error>,
) {
super(msg);
this.code = code;
this.name = "DecryptionError";
this.detailedString = detailedStringForDecryptionError(this, details);
}
}
function detailedStringForDecryptionError(err: DecryptionError, details?: Record<string, string | Error>): string {
let result = err.name + "[msg: " + err.message;
if (details) {
result +=
", " +
Object.keys(details)
.map((k) => k + ": " + details[k])
.join(", ");
}
result += "]";
return result;
}
export class UnknownDeviceError extends Error {
/**
* Exception thrown specifically when we want to warn the user to consider
@@ -274,3 +235,6 @@ export function registerAlgorithm<P extends IParams = IParams>(
ENCRYPTION_CLASSES.set(algorithm, encryptor as new (params: IParams) => EncryptionAlgorithm);
DECRYPTION_CLASSES.set(algorithm, decryptor as new (params: DecryptionClassParams) => DecryptionAlgorithm);
}
/* Re-export for backwards compatibility. Deprecated: this is an internal class. */
export { DecryptionError } from "../../common-crypto/CryptoBackend";
+11 -7
View File
@@ -26,7 +26,6 @@ import * as olmlib from "../olmlib";
import {
DecryptionAlgorithm,
DecryptionClassParams,
DecryptionError,
EncryptionAlgorithm,
IParams,
registerAlgorithm,
@@ -45,6 +44,8 @@ import { OlmGroupSessionExtraData } from "../../@types/crypto";
import { MatrixError } from "../../http-api";
import { immediate, MapWithDefault } from "../../utils";
import { KnownMembership } from "../../@types/membership";
import { DecryptionFailureCode } from "../../crypto-api";
import { DecryptionError } from "../../common-crypto/CryptoBackend";
// determine whether the key can be shared with invitees
export function isRoomSharedHistory(room: Room): boolean {
@@ -1313,7 +1314,7 @@ export class MegolmDecryption extends DecryptionAlgorithm {
const content = event.getWireContent();
if (!content.sender_key || !content.session_id || !content.ciphertext) {
throw new DecryptionError("MEGOLM_MISSING_FIELDS", "Missing fields in input");
throw new DecryptionError(DecryptionFailureCode.MEGOLM_MISSING_FIELDS, "Missing fields in input");
}
// we add the event to the pending list *before* we start decryption.
@@ -1339,12 +1340,12 @@ export class MegolmDecryption extends DecryptionAlgorithm {
throw e;
}
let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR";
let errorCode = DecryptionFailureCode.OLM_DECRYPT_GROUP_MESSAGE_ERROR;
if ((<MatrixError>e)?.message === "OLM.UNKNOWN_MESSAGE_INDEX") {
this.requestKeysForEvent(event);
errorCode = "OLM_UNKNOWN_MESSAGE_INDEX";
errorCode = DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX;
}
throw new DecryptionError(errorCode, e instanceof Error ? e.message : "Unknown Error: Error is undefined", {
@@ -1377,13 +1378,13 @@ export class MegolmDecryption extends DecryptionAlgorithm {
if (problem.fixed) {
problemDescription += " Trying to create a new secure channel and re-requesting the keys.";
}
throw new DecryptionError("MEGOLM_UNKNOWN_INBOUND_SESSION_ID", problemDescription, {
throw new DecryptionError(DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID, problemDescription, {
session: content.sender_key + "|" + content.session_id,
});
}
throw new DecryptionError(
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
"The sender's device has not sent us the keys for this message.",
{
session: content.sender_key + "|" + content.session_id,
@@ -1405,7 +1406,10 @@ export class MegolmDecryption extends DecryptionAlgorithm {
// (this is somewhat redundant, since the megolm session is scoped to the
// room, so neither the sender nor a MITM can lie about the room_id).
if (payload.room_id !== event.getRoomId()) {
throw new DecryptionError("MEGOLM_BAD_ROOM", "Message intended for room " + payload.room_id);
throw new DecryptionError(
DecryptionFailureCode.MEGOLM_BAD_ROOM,
"Message intended for room " + payload.room_id,
);
}
return {
+50 -22
View File
@@ -22,11 +22,13 @@ import type { IEventDecryptionResult } from "../../@types/crypto";
import { logger } from "../../logger";
import * as olmlib from "../olmlib";
import { DeviceInfo } from "../deviceinfo";
import { DecryptionAlgorithm, DecryptionError, EncryptionAlgorithm, registerAlgorithm } from "./base";
import { DecryptionAlgorithm, EncryptionAlgorithm, registerAlgorithm } from "./base";
import { Room } from "../../models/room";
import { IContent, MatrixEvent } from "../../models/event";
import { IEncryptedContent, IOlmEncryptedContent } from "../index";
import { IInboundSession } from "../OlmDevice";
import { DecryptionFailureCode } from "../../crypto-api";
import { DecryptionError } from "../../common-crypto/CryptoBackend";
const DeviceVerification = DeviceInfo.DeviceVerification;
@@ -159,11 +161,14 @@ class OlmDecryption extends DecryptionAlgorithm {
const ciphertext = content.ciphertext;
if (!ciphertext) {
throw new DecryptionError("OLM_MISSING_CIPHERTEXT", "Missing ciphertext");
throw new DecryptionError(DecryptionFailureCode.OLM_MISSING_CIPHERTEXT, "Missing ciphertext");
}
if (!(this.olmDevice.deviceCurve25519Key! in ciphertext)) {
throw new DecryptionError("OLM_NOT_INCLUDED_IN_RECIPIENTS", "Not included in recipients");
throw new DecryptionError(
DecryptionFailureCode.OLM_NOT_INCLUDED_IN_RECIPIENTS,
"Not included in recipients",
);
}
const message = ciphertext[this.olmDevice.deviceCurve25519Key!];
let payloadString: string;
@@ -171,7 +176,7 @@ class OlmDecryption extends DecryptionAlgorithm {
try {
payloadString = await this.decryptMessage(deviceKey, message);
} catch (e) {
throw new DecryptionError("OLM_BAD_ENCRYPTED_MESSAGE", "Bad Encrypted Message", {
throw new DecryptionError(DecryptionFailureCode.OLM_BAD_ENCRYPTED_MESSAGE, "Bad Encrypted Message", {
sender: deviceKey,
err: e as Error,
});
@@ -182,14 +187,21 @@ class OlmDecryption extends DecryptionAlgorithm {
// check that we were the intended recipient, to avoid unknown-key attack
// https://github.com/vector-im/vector-web/issues/2483
if (payload.recipient != this.userId) {
throw new DecryptionError("OLM_BAD_RECIPIENT", "Message was intented for " + payload.recipient);
throw new DecryptionError(
DecryptionFailureCode.OLM_BAD_RECIPIENT,
"Message was intended for " + payload.recipient,
);
}
if (payload.recipient_keys.ed25519 != this.olmDevice.deviceEd25519Key) {
throw new DecryptionError("OLM_BAD_RECIPIENT_KEY", "Message not intended for this device", {
intended: payload.recipient_keys.ed25519,
our_key: this.olmDevice.deviceEd25519Key!,
});
throw new DecryptionError(
DecryptionFailureCode.OLM_BAD_RECIPIENT_KEY,
"Message not intended for this device",
{
intended: payload.recipient_keys.ed25519,
our_key: this.olmDevice.deviceEd25519Key!,
},
);
}
// check that the device that encrypted the event belongs to the user that the event claims it's from.
@@ -216,18 +228,26 @@ class OlmDecryption extends DecryptionAlgorithm {
try {
await this.crypto.deviceList.downloadKeys([event.getSender()!], false);
} catch (e) {
throw new DecryptionError("OLM_BAD_SENDER_CHECK_FAILED", "Could not verify sender identity", {
sender: deviceKey,
err: e as Error,
});
throw new DecryptionError(
DecryptionFailureCode.OLM_BAD_SENDER_CHECK_FAILED,
"Could not verify sender identity",
{
sender: deviceKey,
err: e as Error,
},
);
}
senderKeyUser = this.crypto.deviceList.getUserByIdentityKey(olmlib.OLM_ALGORITHM, deviceKey);
}
if (senderKeyUser !== event.getSender() && senderKeyUser !== undefined && senderKeyUser !== null) {
throw new DecryptionError("OLM_BAD_SENDER", "Message claimed to be from " + event.getSender(), {
real_sender: senderKeyUser,
});
throw new DecryptionError(
DecryptionFailureCode.OLM_BAD_SENDER,
"Message claimed to be from " + event.getSender(),
{
real_sender: senderKeyUser,
},
);
}
// check that the original sender matches what the homeserver told us, to
@@ -235,16 +255,24 @@ class OlmDecryption extends DecryptionAlgorithm {
// (this check is also provided via the sender's embedded ed25519 key,
// which is checked elsewhere).
if (payload.sender != event.getSender()) {
throw new DecryptionError("OLM_FORWARDED_MESSAGE", "Message forwarded from " + payload.sender, {
reported_sender: event.getSender()!,
});
throw new DecryptionError(
DecryptionFailureCode.OLM_FORWARDED_MESSAGE,
"Message forwarded from " + payload.sender,
{
reported_sender: event.getSender()!,
},
);
}
// Olm events intended for a room have a room_id.
if (payload.room_id !== event.getRoomId()) {
throw new DecryptionError("OLM_BAD_ROOM", "Message intended for room " + payload.room_id, {
reported_room: event.getRoomId() || "ROOM_ID_UNDEFINED",
});
throw new DecryptionError(
DecryptionFailureCode.OLM_BAD_ROOM,
"Message intended for room " + payload.room_id,
{
reported_room: event.getRoomId() || "ROOM_ID_UNDEFINED",
},
);
}
const claimedKeys = payload.keys || {};
+5 -4
View File
@@ -73,7 +73,7 @@ import { TypedEventEmitter } from "../models/typed-event-emitter";
import { IDeviceLists, ISyncResponse, IToDeviceEvent } from "../sync-accumulator";
import { ISignatures } from "../@types/signed";
import { IMessage } from "./algorithms/olm";
import { BackupDecryptor, CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
import { BackupDecryptor, CryptoBackend, DecryptionError, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
import { RoomState, RoomStateEvent } from "../models/room-state";
import { MapWithDefault, recursiveMapToObject } from "../utils";
import {
@@ -90,6 +90,7 @@ import {
BackupTrustInfo,
BootstrapCrossSigningOpts,
CrossSigningStatus,
DecryptionFailureCode,
DeviceVerificationStatus,
EventEncryptionInfo,
EventShieldColour,
@@ -97,8 +98,8 @@ import {
ImportRoomKeysOpts,
KeyBackupCheck,
KeyBackupInfo,
VerificationRequest as CryptoApiVerificationRequest,
OwnDeviceKeys,
VerificationRequest as CryptoApiVerificationRequest,
} from "../crypto-api";
import { Device, DeviceMap } from "../models/device";
import { deviceInfoToDevice } from "./device-converter";
@@ -4209,8 +4210,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
const AlgClass = algorithms.DECRYPTION_CLASSES.get(algorithm);
if (!AlgClass) {
throw new algorithms.DecryptionError(
"UNKNOWN_ENCRYPTION_ALGORITHM",
throw new DecryptionError(
DecryptionFailureCode.UNKNOWN_ENCRYPTION_ALGORITHM,
'Unknown encryption algorithm "' + algorithm + '".',
);
}
+3 -2
View File
@@ -28,9 +28,10 @@ import { MatrixClient } from "../../client";
import { IVerificationChannel } from "./request/Channel";
import { MatrixEvent } from "../../models/event";
import { ShowQrCodeCallbacks, VerifierEvent } from "../../crypto-api/verification";
import { VerificationMethod } from "../../types";
export const SHOW_QR_CODE_METHOD = "m.qr_code.show.v1";
export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1";
export const SHOW_QR_CODE_METHOD = VerificationMethod.ShowQrCode;
export const SCAN_QR_CODE_METHOD = VerificationMethod.ScanQrCode;
/** @deprecated use VerifierEvent */
export type QrCodeEvent = VerifierEvent;
+2 -1
View File
@@ -34,6 +34,7 @@ import { IContent, MatrixEvent } from "../../models/event";
import { generateDecimalSas } from "./SASDecimal";
import { EventType } from "../../@types/event";
import { EmojiMapping, GeneratedSas, ShowSasCallbacks, VerifierEvent } from "../../crypto-api/verification";
import { VerificationMethod } from "../../types";
// backwards-compatibility exports
export type {
@@ -233,7 +234,7 @@ export class SAS extends Base {
// eslint-disable-next-line @typescript-eslint/naming-convention
public static get NAME(): string {
return "m.sas.v1";
return VerificationMethod.Sas;
}
public get events(): string[] {
@@ -18,7 +18,7 @@ limitations under the License.
import { VerificationRequest, REQUEST_TYPE, READY_TYPE, START_TYPE } from "./VerificationRequest";
import { logger } from "../../../logger";
import { IVerificationChannel } from "./Channel";
import { EventType } from "../../../@types/event";
import { EventType, TimelineEvents } from "../../../@types/event";
import { MatrixClient } from "../../../client";
import { MatrixEvent } from "../../../models/event";
import { IRequestsMap } from "../..";
@@ -299,7 +299,11 @@ export class InRoomChannel implements IVerificationChannel {
if (type === REQUEST_TYPE) {
sendType = MESSAGE_TYPE;
}
const response = await this.client.sendEvent(this.roomId, sendType, content);
const response = await this.client.sendEvent(
this.roomId,
sendType as keyof TimelineEvents,
content as TimelineEvents[keyof TimelineEvents],
);
if (type === REQUEST_TYPE) {
this.requestEventId = response.event_id;
}
-19
View File
@@ -14,25 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
export enum InvalidStoreState {
ToggledLazyLoading,
}
export class InvalidStoreError extends Error {
public static TOGGLED_LAZY_LOADING = InvalidStoreState.ToggledLazyLoading;
public constructor(
public readonly reason: InvalidStoreState,
public readonly value: any,
) {
const message =
`Store is invalid because ${reason}, ` +
`please stop the client, delete all data and start the client again`;
super(message);
this.name = "InvalidStoreError";
}
}
export enum InvalidCryptoStoreState {
TooNew = "TOO_NEW",
}
+5 -11
View File
@@ -140,12 +140,6 @@ export type AuthDict =
| { type: Exclude<string, AuthType>; [key: string]: any }
| {};
/**
* Backwards compatible export
* @deprecated in favour of AuthDict
*/
export type IAuthDict = AuthDict;
export class NoAuthFlowFoundError extends Error {
public name = "NoAuthFlowFoundError";
@@ -168,7 +162,7 @@ export class NoAuthFlowFoundError extends Error {
*
* The generic parameter `T` is the type of the response of the endpoint, once it is eventually successful.
*/
export type UIAuthCallback<T> = (makeRequest: (authData: IAuthDict | null) => Promise<UIAResponse<T>>) => Promise<T>;
export type UIAuthCallback<T> = (makeRequest: (authData: AuthDict | null) => Promise<UIAResponse<T>>) => Promise<T>;
interface IOpts<T> {
/**
@@ -340,7 +334,7 @@ export class InteractiveAuth<T> {
// another just to check what the status is
if (this.submitPromise) return;
let authDict: IAuthDict = {};
let authDict: AuthDict = {};
if (this.currentStage == EMAIL_STAGE_TYPE) {
// The email can be validated out-of-band, but we need to provide the
// creds so the HS can go & check it.
@@ -410,7 +404,7 @@ export class InteractiveAuth<T> {
* in the attemptAuth promise being rejected. This can be set to true
* for requests that just poll to see if auth has been completed elsewhere.
*/
public async submitAuthDict(authData: IAuthDict, background = false): Promise<void> {
public async submitAuthDict(authData: AuthDict, background = false): Promise<void> {
if (!this.attemptAuthDeferred) {
throw new Error("submitAuthDict() called before attemptAuth()");
}
@@ -431,7 +425,7 @@ export class InteractiveAuth<T> {
}
// use the sessionid from the last request, if one is present.
let auth: IAuthDict;
let auth: AuthDict;
if ((this.data as IAuthData)?.session) {
auth = {
session: (this.data as IAuthData).session,
@@ -515,7 +509,7 @@ export class InteractiveAuth<T> {
* This can be set to true for requests that just poll to see if auth has
* been completed elsewhere.
*/
private async doRequest(auth: IAuthDict | null, background = false): Promise<void> {
private async doRequest(auth: AuthDict | null, background = false): Promise<void> {
try {
const result = await this.requestCallback(auth, background);
this.attemptAuthDeferred!.resolve(result);
+18 -3
View File
@@ -15,12 +15,27 @@ limitations under the License.
*/
import { MatrixClient } from "../client";
import { IEncryptedFile, RelationType, UNSTABLE_MSC3089_BRANCH } from "../@types/event";
import { RelationType, UNSTABLE_MSC3089_BRANCH } from "../@types/event";
import { IContent, MatrixEvent } from "./event";
import { MSC3089TreeSpace } from "./MSC3089TreeSpace";
import { EventTimeline } from "./event-timeline";
import { FileType } from "../http-api";
import type { ISendEventResponse } from "../@types/requests";
import { EncryptedFile } from "../@types/media";
export interface MSC3089EventContent {
active?: boolean;
name?: string;
locked?: boolean;
version?: number;
}
export interface MSC3089EventContent {
active?: boolean;
name?: string;
locked?: boolean;
version?: number;
}
/**
* Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) branch - a reference
@@ -131,7 +146,7 @@ export class MSC3089Branch {
* Gets information about the file needed to download it.
* @returns Information about the file.
*/
public async getFileInfo(): Promise<{ info: IEncryptedFile; httpUrl: string }> {
public async getFileInfo(): Promise<{ info: EncryptedFile; httpUrl: string }> {
const event = await this.getFileEvent();
const file = event.getOriginalContent()["file"];
@@ -179,7 +194,7 @@ export class MSC3089Branch {
public async createNewVersion(
name: string,
encryptedContents: FileType,
info: Partial<IEncryptedFile>,
info: EncryptedFile,
additionalContent?: IContent,
): Promise<ISendEventResponse> {
const fileEventResponse = await this.directory.createFile(name, encryptedContents, info, {
+19 -9
View File
@@ -17,7 +17,7 @@ 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 { EventType, MsgType, UNSTABLE_MSC3089_BRANCH, UNSTABLE_MSC3089_LEAF } from "../@types/event";
import { Room } from "./room";
import { logger } from "../logger";
import { IContent, MatrixEvent } from "./event";
@@ -34,6 +34,8 @@ import { isRoomSharedHistory } from "../crypto/algorithms/megolm";
import { ISendEventResponse } from "../@types/requests";
import { FileType } from "../http-api";
import { KnownMembership } from "../@types/membership";
import { RoomPowerLevelsEventContent, SpaceChildEventContent } from "../@types/state_events";
import { EncryptedFile, FileContent } from "../@types/media";
/**
* The recommended defaults for a tree space's power levels. Note that this
@@ -78,6 +80,12 @@ export enum TreePermissions {
Owner = "owner", // "Admin" or PL100
}
declare module "../@types/media" {
interface FileContent {
[UNSTABLE_MSC3089_LEAF.name]?: {};
}
}
/**
* Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089)
* file tree Space. Note that this is UNSTABLE and subject to breaking changes
@@ -176,7 +184,7 @@ export class MSC3089TreeSpace {
const currentPls = this.room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
if (Array.isArray(currentPls)) throw new Error("Unexpected return type for power levels");
const pls = currentPls?.getContent() || {};
const pls = currentPls?.getContent<RoomPowerLevelsEventContent>() || {};
const viewLevel = pls["users_default"] || 0;
const editLevel = pls["events_default"] || 50;
const adminLevel = pls["events"]?.[EventType.RoomPowerLevels] || 100;
@@ -234,7 +242,7 @@ export class MSC3089TreeSpace {
this.roomId,
EventType.SpaceChild,
{
via: [this.client.getDomain()],
via: [this.client.getDomain()!],
},
directory.roomId,
);
@@ -243,7 +251,7 @@ export class MSC3089TreeSpace {
directory.roomId,
EventType.SpaceParent,
{
via: [this.client.getDomain()],
via: [this.client.getDomain()!],
},
this.roomId,
);
@@ -450,7 +458,9 @@ export class MSC3089TreeSpace {
// XXX: We should be creating gaps to avoid conflicts
lastOrder = lastOrder ? nextString(lastOrder) : DEFAULT_ALPHABET[0];
const currentChild = parentRoom.currentState.getStateEvents(EventType.SpaceChild, target.roomId);
const content = currentChild?.getContent() ?? { via: [this.client.getDomain()] };
const content = currentChild?.getContent<SpaceChildEventContent>() ?? {
via: [this.client.getDomain()!],
};
await this.client.sendStateEvent(
parentRoom.roomId,
EventType.SpaceChild,
@@ -473,7 +483,7 @@ export class MSC3089TreeSpace {
// Now we can finally update our own order state
const currentChild = parentRoom.currentState.getStateEvents(EventType.SpaceChild, this.roomId);
const content = currentChild?.getContent() ?? { via: [this.client.getDomain()] };
const content = currentChild?.getContent<SpaceChildEventContent>() ?? { via: [this.client.getDomain()!] };
await this.client.sendStateEvent(
parentRoom.roomId,
EventType.SpaceChild,
@@ -499,7 +509,7 @@ export class MSC3089TreeSpace {
public async createFile(
name: string,
encryptedContents: FileType,
info: Partial<IEncryptedFile>,
info: EncryptedFile,
additionalContent?: IContent,
): Promise<ISendEventResponse> {
const { content_uri: mxc } = await this.client.uploadContent(encryptedContents, {
@@ -507,7 +517,7 @@ export class MSC3089TreeSpace {
});
info.url = mxc;
const fileContent = {
const fileContent: FileContent = {
msgtype: MsgType.File,
body: name,
url: mxc,
@@ -526,7 +536,7 @@ export class MSC3089TreeSpace {
...additionalContent,
...fileContent,
[UNSTABLE_MSC3089_LEAF.name]: {},
});
} as FileContent);
await this.client.sendStateEvent(
this.roomId,
+55 -69
View File
@@ -41,13 +41,13 @@ import { TypedReEmitter } from "../ReEmitter";
import { MatrixError } from "../http-api";
import { TypedEventEmitter } from "./typed-event-emitter";
import { EventStatus } from "./event-status";
import { DecryptionError } from "../crypto/algorithms";
import { CryptoBackend } from "../common-crypto/CryptoBackend";
import { CryptoBackend, DecryptionError } from "../common-crypto/CryptoBackend";
import { WITHHELD_MESSAGES } from "../crypto/OlmDevice";
import { IAnnotatedPushRule } from "../@types/PushRules";
import { Room } from "./room";
import { EventTimeline } from "./event-timeline";
import { Membership } from "../@types/membership";
import { DecryptionFailureCode } from "../crypto-api";
export { EventStatus } from "./event-status";
@@ -227,7 +227,18 @@ export interface IMessageVisibilityHidden {
const MESSAGE_VISIBLE: IMessageVisibilityVisible = Object.freeze({ visible: true });
export enum MatrixEventEvent {
/**
* An event has been decrypted, or we have failed to decrypt it.
*
* The payload consists of:
*
* * `event` - The {@link MatrixEvent} which we attempted to decrypt.
*
* * `err` - The error that occurred during decryption, or `undefined` if no error occurred.
* Avoid use of this: {@link MatrixEvent.decryptionFailureReason} is more useful.
*/
Decrypted = "Event.decrypted",
BeforeRedaction = "Event.beforeRedaction",
VisibilityChange = "Event.visibilityChange",
LocalEventIdReplaced = "Event.localEventIdReplaced",
@@ -239,12 +250,6 @@ export enum MatrixEventEvent {
export type MatrixEventEmittedEvents = MatrixEventEvent | ThreadEvent.Update;
export type MatrixEventHandlerMap = {
/**
* Fires when an event is decrypted
*
* @param event - The matrix event which has been decrypted
* @param err - The error that occurred during decryption, or `undefined` if no error occurred.
*/
[MatrixEventEvent.Decrypted]: (event: MatrixEvent, err?: Error) => void;
[MatrixEventEvent.BeforeRedaction]: (event: MatrixEvent, redactionEvent: MatrixEvent) => void;
[MatrixEventEvent.VisibilityChange]: (event: MatrixEvent, visible: boolean) => void;
@@ -274,6 +279,9 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
private _hasCachedExtEv = false;
private _cachedExtEv: Optional<ExtensibleEvent> = undefined;
/** If we failed to decrypt this event, the reason for the failure. Otherwise, `null`. */
private _decryptionFailureReason: DecryptionFailureCode | null = null;
/* curve25519 key which we believe belongs to the sender of the event. See
* getSenderKey()
*/
@@ -771,7 +779,12 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
* couldn't decrypt.
*/
public isDecryptionFailure(): boolean {
return this.clearEvent?.content?.msgtype === "m.bad.encrypted";
return this._decryptionFailureReason !== null;
}
/** If we failed to decrypt this event, the reason for the failure. Otherwise, `null`. */
public get decryptionFailureReason(): DecryptionFailureCode | null {
return this._decryptionFailureReason;
}
/*
@@ -884,17 +897,14 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
while (true) {
this.retryDecryption = false;
let res: IEventDecryptionResult;
let err: Error | undefined = undefined;
try {
if (!crypto) {
res = this.badEncryptedMessage("Encryption not enabled");
} else {
res = await crypto.decryptEvent(this);
if (options.isRetry === true) {
logger.info(`Decrypted event on retry (${this.getDetails()})`);
}
const res = await crypto.decryptEvent(this);
if (options.isRetry === true) {
logger.info(`Decrypted event on retry (${this.getDetails()})`);
}
this.setClearData(res);
this._decryptionFailureReason = null;
} catch (e) {
const detailedError = e instanceof DecryptionError ? (<DecryptionError>e).detailedString : String(e);
@@ -927,14 +937,12 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
// so we don't bother to log `e` separately.
logger.warn(`Error decrypting event (${this.getDetails()}): ${detailedError}`);
res = this.badEncryptedMessage(String(e));
this.setClearDataForDecryptionFailure(String(e));
this._decryptionFailureReason =
e instanceof DecryptionError ? (<DecryptionError>e).code : DecryptionFailureCode.UNKNOWN_ERROR;
}
// at this point, we've either successfully decrypted the event, or have given up
// (and set res to a 'badEncryptedMessage'). Either way, we can now set the
// cleartext of the event and raise Event.decrypted.
//
// make sure we clear 'decryptionPromise' before sending the 'Event.decrypted' event,
// Make sure we clear 'decryptionPromise' before sending the 'Event.decrypted' event,
// otherwise the app will be confused to see `isBeingDecrypted` still set when
// there isn't an `Event.decrypted` on the way.
//
@@ -942,7 +950,6 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
//
this.decryptionPromise = null;
this.retryDecryption = false;
this.setClearData(res);
// Before we emit the event, clear the push actions so that they can be recalculated
// by relevant code. We do this because the clear event has now changed, making it
@@ -960,19 +967,6 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
}
}
private badEncryptedMessage(reason: string): IEventDecryptionResult {
return {
clearEvent: {
type: EventType.RoomMessage,
content: {
msgtype: "m.bad.encrypted",
body: "** Unable to decrypt: " + reason + " **",
},
},
encryptedDisabledForUnverifiedDevices: reason === `DecryptionError: ${WITHHELD_MESSAGES["m.unverified"]}`,
};
}
/**
* Update the cleartext data on this event.
*
@@ -981,9 +975,6 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
* @internal
*
* @param decryptionResult - the decryption result, including the plaintext and some key info
*
* @remarks
* Fires {@link MatrixEventEvent.Decrypted}
*/
private setClearData(decryptionResult: IEventDecryptionResult): void {
this.clearEvent = decryptionResult.clearEvent;
@@ -991,7 +982,28 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
this.claimedEd25519Key = decryptionResult.claimedEd25519Key ?? null;
this.forwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain || [];
this.untrusted = decryptionResult.untrusted || false;
this.encryptedDisabledForUnverifiedDevices = decryptionResult.encryptedDisabledForUnverifiedDevices || false;
this.encryptedDisabledForUnverifiedDevices = false;
this.invalidateExtensibleEvent();
}
/**
* Update the cleartext data on this event after a decryption failure.
*
* @param reason - the textual reason for the failure
*/
private setClearDataForDecryptionFailure(reason: string): void {
this.clearEvent = {
type: EventType.RoomMessage,
content: {
msgtype: "m.bad.encrypted",
body: `** Unable to decrypt: ${reason} **`,
},
};
this.senderCurve25519Key = null;
this.claimedEd25519Key = null;
this.forwardingCurve25519KeyChain = [];
this.untrusted = false;
this.encryptedDisabledForUnverifiedDevices = reason === `DecryptionError: ${WITHHELD_MESSAGES["m.unverified"]}`;
this.invalidateExtensibleEvent();
}
@@ -1160,10 +1172,6 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
return this.visibility;
}
/**
* @deprecated In favor of the overload that includes a Room argument
*/
public makeRedacted(redactionEvent: MatrixEvent): void;
/**
* 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
@@ -1171,8 +1179,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
* @param redactionEvent - event causing the redaction
* @param room - the room in which the event exists
*/
public makeRedacted(redactionEvent: MatrixEvent, room: Room): void;
public makeRedacted(redactionEvent: MatrixEvent, room?: Room): void {
public makeRedacted(redactionEvent: MatrixEvent, room: Room): void {
// quick sanity-check
if (!redactionEvent.event) {
throw new Error("invalid redactionEvent in makeRedacted");
@@ -1218,7 +1225,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
// If the redacted event was in a thread (but not thread root), move it
// to the main timeline. This will change if MSC3389 is merged.
if (room && !this.isThreadRoot && this.threadRootId && this.threadRootId !== this.getId()) {
if (!this.isThreadRoot && this.threadRootId && this.threadRootId !== this.getId()) {
this.moveAllRelatedToMainTimeline(room);
redactionEvent.moveToMainTimeline(room);
}
@@ -1355,19 +1362,6 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
return this.pushDetails;
}
/**
* Set the push actions for this event.
* Clears rule from push details if present
* @deprecated use `setPushDetails`
*
* @param pushActions - push actions
*/
public setPushActions(pushActions: IActionsObject | null): void {
this.pushDetails = {
actions: pushActions || undefined,
};
}
/**
* Set the push details for this event.
*
@@ -1568,14 +1562,6 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
}
}
/**
* Checks if this event is associated with another event. See `getAssociatedId`.
* @deprecated use hasAssociation instead.
*/
public hasAssocation(): boolean {
return !!this.getAssociatedId();
}
/**
* Checks if this event is associated with another event. See `getAssociatedId`.
*/
+10 -3
View File
@@ -22,6 +22,7 @@ import { EventTimeline } from "./event-timeline";
import { Preset } from "../@types/partials";
import { globToRegexp } from "../utils";
import { Room } from "./room";
import { EventType, StateEvents } from "../@types/event";
/// The event type storing the user's individual policies.
///
@@ -37,7 +38,7 @@ export const IGNORE_INVITES_ACCOUNT_EVENT_KEY = new UnstableValue(
);
/// The types of recommendations understood.
enum PolicyRecommendation {
export enum PolicyRecommendation {
Ban = "m.ban",
}
@@ -64,6 +65,12 @@ export enum PolicyScope {
Server = "m.policy.server",
}
const scopeToEventTypeMap: Record<PolicyScope, keyof StateEvents> = {
[PolicyScope.User]: EventType.PolicyRuleUser,
[PolicyScope.Room]: EventType.PolicyRuleRoom,
[PolicyScope.Server]: EventType.PolicyRuleServer,
};
/**
* A container for ignored invites.
*
@@ -87,7 +94,7 @@ export class IgnoredInvites {
*/
public async addRule(scope: PolicyScope, entity: string, reason: string): Promise<string> {
const target = await this.getOrCreateTargetRoom();
const response = await this.client.sendStateEvent(target.roomId, scope, {
const response = await this.client.sendStateEvent(target.roomId, scopeToEventTypeMap[scope], {
entity,
reason,
recommendation: PolicyRecommendation.Ban,
@@ -173,7 +180,7 @@ export class IgnoredInvites {
{ scope: PolicyScope.User, entities: [sender] },
{ scope: PolicyScope.Server, entities: [senderServer, roomServer] },
]) {
const events = state.getStateEvents(scope);
const events = state.getStateEvents(scopeToEventTypeMap[scope]);
for (const event of events) {
const content = event.getContent();
if (content?.recommendation != PolicyRecommendation.Ban) {
+8 -1
View File
@@ -53,6 +53,7 @@ enum OobStatus {
export interface IPowerLevelsContent {
users?: Record<string, number>;
events?: Record<string, number>;
notifications?: Partial<Record<"room", number>>;
// eslint-disable-next-line camelcase
users_default?: number;
// eslint-disable-next-line camelcase
@@ -60,6 +61,7 @@ export interface IPowerLevelsContent {
// eslint-disable-next-line camelcase
state_default?: number;
ban?: number;
invite?: number;
kick?: number;
redact?: number;
}
@@ -130,6 +132,8 @@ export type RoomStateEventHandlerMap = {
type EmittedEvents = RoomStateEvent | BeaconEvent;
type EventHandlerMap = RoomStateEventHandlerMap & BeaconEventHandlerMap;
type KeysMatching<T, V> = { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T];
export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
public readonly reEmitter = new TypedReEmitter<EmittedEvents, EventHandlerMap>(this);
private sentinels: Record<string, RoomMember> = {}; // userId: RoomMember
@@ -800,7 +804,10 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
* @param powerLevel - The power level of the member
* @returns true if the given power level is sufficient
*/
public hasSufficientPowerLevelFor(action: "ban" | "kick" | "redact", powerLevel: number): boolean {
public hasSufficientPowerLevelFor(
action: KeysMatching<Required<IPowerLevelsContent>, number>,
powerLevel: number,
): boolean {
const powerLevelsEvent = this.getStateEvents(EventType.RoomPowerLevels, "");
let powerLevels: IPowerLevelsContent = {};
+128 -16
View File
@@ -68,6 +68,7 @@ import { ReadReceipt, synthesizeReceipt } from "./read-receipt";
import { isPollEvent, Poll, PollEvent } from "./poll";
import { RoomReceipts } from "./room-receipts";
import { compareEventOrdering } from "./compare-event-ordering";
import * as utils from "../utils";
import { KnownMembership, Membership } from "../@types/membership";
// These constants are used as sane defaults when the homeserver doesn't support
@@ -474,6 +475,10 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
this.name = roomId;
this.normalizedName = roomId;
// Listen to our own receipt event as a more modular way of processing our own
// receipts. No need to remove the listener: it's on ourself anyway.
this.on(RoomEvent.Receipt, this.onReceipt);
// 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)];
@@ -1306,6 +1311,98 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
}
}
private onReceipt(event: MatrixEvent): void {
if (this.hasEncryptionStateEvent()) {
this.clearNotificationsOnReceipt(event);
}
}
private clearNotificationsOnReceipt(event: MatrixEvent): void {
// Like above, we have to listen for read receipts from ourselves in order to
// correctly handle notification counts on encrypted rooms.
// This fixes https://github.com/vector-im/element-web/issues/9421
// Figure out if we've read something or if it's just informational
// We need to work out what threads we've just recieved receipts for, so we
// know which ones to update. If we've received an unthreaded receipt, we'll
// need to update all threads.
let threadIds: string[] = [];
let hasUnthreadedReceipt = false;
const content = event.getContent();
for (const receiptGroup of Object.values(content)) {
for (const [receiptType, userReceipt] of Object.entries(receiptGroup)) {
if (!utils.isSupportedReceiptType(receiptType)) continue;
if (!userReceipt) continue;
for (const [userId, singleReceipt] of Object.entries(userReceipt)) {
if (!singleReceipt || typeof singleReceipt !== "object") continue;
const typedSingleReceipt = singleReceipt as Record<string, any>;
if (userId !== this.client.getUserId()) continue;
if (typedSingleReceipt.thread_id === undefined) {
hasUnthreadedReceipt = true;
} else if (typeof typedSingleReceipt.thread_id === "string") {
threadIds.push(typedSingleReceipt.thread_id);
}
}
}
}
if (hasUnthreadedReceipt) {
// If we have an unthreaded receipt, we need to update any threads that have a notification
// in them (because we know the receipt can't go backwards so we don't need to check any with
// no notifications: the number can only decrease from a receipt).
threadIds = this.getThreads()
.filter(
(thread) =>
this.getThreadUnreadNotificationCount(thread.id, NotificationCountType.Total) > 0 ||
this.getThreadUnreadNotificationCount(thread.id, NotificationCountType.Highlight) > 0,
)
.map((thread) => thread.id);
threadIds.push("main");
}
for (const threadId of threadIds) {
// Work backwards to determine how many events are unread. We also set
// a limit for how back we'll look to avoid spinning CPU for too long.
// If we hit the limit, we assume the count is unchanged.
const maxHistory = 20;
const timeline = threadId === "main" ? this.getLiveTimeline() : this.getThread(threadId)?.liveTimeline;
if (!timeline) {
logger.warn(`Couldn't find timeline for thread ID ${threadId} in room ${this.roomId}`);
continue;
}
const events = timeline.getEvents();
let highlightCount = 0;
for (let i = events.length - 1; i >= 0; i--) {
if (i === events.length - maxHistory) return; // limit reached
const event = events[i];
if (this.hasUserReadEvent(this.client.getUserId()!, event.getId()!)) {
// If the user has read the event, then the counting is done.
break;
}
const pushActions = this.client.getPushActionsForEvent(event);
highlightCount += pushActions?.tweaks?.highlight ? 1 : 0;
}
// Note: we don't need to handle 'total' notifications because the counts
// will come from the server.
if (threadId === "main") {
this.setUnreadNotificationCount(NotificationCountType.Highlight, highlightCount);
} else {
this.setThreadUnreadNotificationCount(threadId, NotificationCountType.Highlight, highlightCount);
}
}
}
/**
* Returns whether there are any devices in the room that are unverified
*
@@ -1512,18 +1609,31 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
}
/**
* Resets the thread notifications for this room
* Resets the total thread notifications for all threads in this room to zero,
* excluding any threads whose IDs are given in `exceptThreadIds`.
*
* If the room is not encrypted, also resets the highlight notification count to zero
* for the same set of threads.
*
* This is intended for use from the sync code since we calculate highlight notification
* counts locally from decrypted messages. We want to partially trust the total from the
* server such that we clear notifications when read receipts arrive. The weird name is
* intended to reflect this. You probably do not want to use this.
*
* @param exceptThreadIds - The thread IDs to exclude from the reset.
*/
public resetThreadUnreadNotificationCount(notificationsToKeep?: string[]): void {
if (notificationsToKeep) {
for (const [threadId] of this.threadNotifications) {
if (!notificationsToKeep.includes(threadId)) {
this.threadNotifications.delete(threadId);
public resetThreadUnreadNotificationCountFromSync(exceptThreadIds: string[] = []): void {
const isEncrypted = this.hasEncryptionStateEvent();
for (const [threadId, notifs] of this.threadNotifications) {
if (!exceptThreadIds.includes(threadId)) {
notifs.total = 0;
if (!isEncrypted) {
notifs.highlight = 0;
}
}
} else {
this.threadNotifications.clear();
}
this.emit(RoomEvent.UnreadNotifications);
}
@@ -2345,6 +2455,15 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
receipts: this.cachedThreadReadReceipts.get(threadId) ?? [],
});
// Add the re-emitter before we start adding events to the thread so we don't miss events
this.reEmitter.reEmit(thread, [
ThreadEvent.Delete,
ThreadEvent.Update,
ThreadEvent.NewReply,
RoomEvent.Timeline,
RoomEvent.TimelineReset,
]);
// All read receipts should now come down from sync, we do not need to keep
// a reference to the cached receipts anymore.
this.cachedThreadReadReceipts.delete(threadId);
@@ -2360,13 +2479,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
// and pass the event through this.
thread.addEvents(events, false);
this.reEmitter.reEmit(thread, [
ThreadEvent.Delete,
ThreadEvent.Update,
ThreadEvent.NewReply,
RoomEvent.Timeline,
RoomEvent.TimelineReset,
]);
const isNewer =
this.lastThread?.rootEvent &&
rootEvent?.localTimestamp &&
@@ -2966,7 +3078,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
[[], [], []],
);
} else {
// When `experimentalThreadSupport` is disabled treat all events as timelineEvents
// When `threadSupport` is disabled treat all events as timelineEvents
return [events as MatrixEvent[], [] as MatrixEvent[], [] as MatrixEvent[]];
}
}
+54 -39
View File
@@ -47,11 +47,6 @@ export type ThreadEventHandlerMap = {
[ThreadEvent.Delete]: (thread: Thread) => void;
} & EventTimelineSetHandlerMap;
/**
* @deprecated please use ThreadEventHandlerMap instead
*/
export type EventHandlerMap = ThreadEventHandlerMap;
interface IThreadOpts {
room: Room;
client: MatrixClient;
@@ -105,7 +100,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
*
* So it looks like this is only really relevant when initialEventsFetched
* is false, because as soon as the initial events have been fetched, we
* should have a timeline (I think).
* should have a proper chunk of timeline from the pagination fetch.
*
* If all replies in this thread are redacted, this is set to the root
* event. I'm not clear what the meaning of this is, since usually after the
@@ -138,6 +133,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
* that we've already fetched them.
*/
public initialEventsFetched = !Thread.hasServerSideSupport;
private initalEventFetchProm: Promise<boolean> | undefined;
/**
* An array of events to add to the timeline once the thread has been initialised
@@ -377,20 +373,20 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
// When there's no server-side support, just add it to the end of the timeline.
this.addEventToTimeline(event, toStartOfTimeline);
this.client.decryptEventIfNeeded(event);
} else if (!toStartOfTimeline && this.initialEventsFetched && isNewestReply) {
// When we've asked for the event to be added to the end, and we're
// not in the initial state, and this event belongs at the end, add it.
this.addEventToTimeline(event, false);
this.fetchEditsWhereNeeded(event);
} else if (event.isRelation(RelationType.Annotation) || event.isRelation(RelationType.Replace)) {
this.addRelatedThreadEvent(event, toStartOfTimeline);
return;
} else if (this.initialEventsFetched) {
// If initial events have not been fetched, we are OK to throw away
// this event, because we are about to fetch all the events for this
// thread from the server.
} else if (!toStartOfTimeline && isNewestReply) {
// When we've asked for the event to be added to the end,
// and this event belongs at the end, add it.
this.addEventToTimeline(event, false);
this.fetchEditsWhereNeeded(event);
} else {
// Otherwise, we should add it, but we suspect it is out of order.
// This may be because we've just created the thread object and are
// still fetching events, in which case add it where we think is sensible
// and it will be removed and replaced with the events from the pagination
// request once that completes.
if (toStartOfTimeline) {
// If we're adding at the start of the timeline, it doesn't
// matter that it's out of order.
@@ -606,29 +602,48 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
await this.processRootEventPromise;
if (!this.initialEventsFetched) {
this.initialEventsFetched = true;
// fetch initial event to allow proper pagination
try {
// if the thread has regular events, this will just load the last reply.
// if the thread is newly created, this will load the root event.
if (this.replyCount === 0 && this.rootEvent) {
this.timelineSet.addEventsToTimeline([this.rootEvent], true, this.liveTimeline, null);
this.liveTimeline.setPaginationToken(null, Direction.Backward);
} else {
await this.client.paginateEventTimeline(this.liveTimeline, {
backwards: true,
});
if (this.initalEventFetchProm) {
await this.initalEventFetchProm;
} else {
// fetch initial events to allow proper pagination
try {
// clear out any events that were added before the pagination request
// completed (eg. from sync). They'll be replaced by those from the pagination.
// Really, we should do this after the pagination request completes, but
// paginateEventTimeline does the request and adds the events in one go, so
// this would need a refactor in order to do. It's therefore possible there's
// a remaining race where an event comes in while the pagination request is
// happening.
this.timelineSet.resetLiveTimeline();
// if the thread has regular events, this will just load the last reply.
// if the thread is newly created, this will load the root event.
if (this.replyCount === 0 && this.rootEvent) {
this.timelineSet.addEventsToTimeline([this.rootEvent], true, this.liveTimeline, null);
this.liveTimeline.setPaginationToken(null, Direction.Backward);
} else {
this.initalEventFetchProm = this.client.paginateEventTimeline(this.liveTimeline, {
backwards: true,
});
await this.initalEventFetchProm;
}
// We have now fetched the initial events, so set the flag. We need to do this before
// we actually add the events, so `this.addEvents` knows that it can now safely add
// them rather than buffer them in the pending event list. The main thing is that this
// must remain false while the async fetch happens, so we don't try to add events before
// the pagination has finished. The important thing is that we're not await-ing anything
// else between setting this and adding events, so no races.
this.initialEventsFetched = true;
for (const event of this.replayEvents!) {
this.addEvent(event, false);
}
this.replayEvents = null;
// just to make sure that, if we've created a timeline window for this thread before the thread itself
// existed (e.g. when creating a new thread), we'll make sure the panel is force refreshed correctly.
this.emit(RoomEvent.TimelineReset, this.room, this.timelineSet, true);
} catch (e) {
logger.error("Failed to load start of newly created thread: ", e);
this.initialEventsFetched = false;
}
for (const event of this.replayEvents!) {
this.addEvent(event, false);
}
this.replayEvents = null;
// just to make sure that, if we've created a timeline window for this thread before the thread itself
// existed (e.g. when creating a new thread), we'll make sure the panel is force refreshed correctly.
this.emit(RoomEvent.TimelineReset, this.room, this.timelineSet, true);
} catch (e) {
logger.error("Failed to load start of newly created thread: ", e);
this.initialEventsFetched = false;
}
}
@@ -689,7 +704,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
* Return last reply to the thread, if known.
*/
public lastReply(
matches: (ev: MatrixEvent) => boolean = (ev): boolean => ev.isRelation(RelationType.Thread),
matches: (ev: MatrixEvent) => boolean = (ev): boolean => ev.isRelation(THREAD_RELATION_TYPE.name),
): MatrixEvent | null {
for (let i = this.timeline.length - 1; i >= 0; i--) {
const event = this.timeline[i];
-6
View File
@@ -20,12 +20,6 @@ import { OidcMetadata, SigninResponse } from "oidc-client-ts";
import { logger } from "../logger";
import { OidcError } from "./error";
/**
* re-export for backwards compatibility
* @deprecated use OidcError
*/
export { OidcError as OidcDiscoveryError };
export type ValidatedIssuerConfig = {
authorizationEndpoint: string;
tokenEndpoint: string;
+32 -19
View File
@@ -25,8 +25,8 @@ import {
ICallStartedPrefixCondition,
IContainsDisplayNameCondition,
IEventMatchCondition,
IEventPropertyIsCondition,
IEventPropertyContainsCondition,
IEventPropertyIsCondition,
IPushRule,
IPushRules,
IRoomMemberCountCondition,
@@ -115,8 +115,14 @@ const DEFAULT_OVERRIDE_RULES: Record<string, IPushRule> = {
},
};
const EXPECTED_DEFAULT_OVERRIDE_RULE_IDS = [
// A special rule id for `EXPECTED_DEFAULT_OVERRIDE_RULE_IDS` and friends which denotes where user-defined rules live in the order.
const UserDefinedRules = Symbol("UserDefinedRules");
type OrderedRules = Array<string | typeof UserDefinedRules>;
const EXPECTED_DEFAULT_OVERRIDE_RULE_IDS: OrderedRules = [
RuleId.Master,
UserDefinedRules,
RuleId.SuppressNotices,
RuleId.InviteToSelf,
RuleId.MemberEvent,
@@ -151,8 +157,10 @@ const DEFAULT_UNDERRIDE_RULES: Record<string, IPushRule> = {
},
};
const EXPECTED_DEFAULT_UNDERRIDE_RULE_IDS = [
const EXPECTED_DEFAULT_UNDERRIDE_RULE_IDS: OrderedRules = [
UserDefinedRules,
RuleId.IncomingCall,
".org.matrix.msc3914.rule.room.call",
RuleId.EncryptedDM,
RuleId.DM,
RuleId.Message,
@@ -162,35 +170,40 @@ const EXPECTED_DEFAULT_UNDERRIDE_RULE_IDS = [
/**
* Make sure that each of the rules listed in `defaultRuleIds` is listed in the given set of push rules.
*
* @param kind - the kind of push rule set being merged.
* @param incomingRules - the existing set of known push rules for the user.
* @param defaultRules - a lookup table for the default definitions of push rules.
* @param defaultRuleIds - the IDs of the expected default push rules, in order.
* @param orderedRuleIds - the IDs of the expected push rules, in order.
*
* @returns A copy of `incomingRules`, with any missing default rules inserted in the right place.
*/
function mergeRulesWithDefaults(
kind: PushRuleKind,
incomingRules: IPushRule[],
defaultRules: Record<string, IPushRule>,
defaultRuleIds: string[],
orderedRuleIds: OrderedRules,
): IPushRule[] {
// Calculate the index after the last default rule in `incomingRules`
// to allow us to split the incomingRules into defaults and custom
let firstCustomRuleIndex = incomingRules.findIndex((r) => !r.default);
if (firstCustomRuleIndex < 0) firstCustomRuleIndex = incomingRules.length;
// Split the incomingRules into defaults and custom
const incomingDefaultRules = incomingRules.filter((rule) => rule.default);
const incomingCustomRules = incomingRules.filter((rule) => !rule.default);
function insertDefaultPushRule(ruleId: string): void {
if (ruleId in defaultRules) {
logger.warn(`Adding default global push rule ${ruleId}`);
function insertDefaultPushRule(ruleId: OrderedRules[number]): void {
if (ruleId === UserDefinedRules) {
// Re-insert any user-defined rules that were in `incomingRules`
newRules.push(...incomingCustomRules);
} else if (ruleId in defaultRules) {
logger.warn(`Adding default global ${kind} push rule ${ruleId}`);
newRules.push(defaultRules[ruleId]);
} else {
logger.warn(`Missing default global push rule ${ruleId}`);
logger.warn(`Missing default global ${kind} push rule ${ruleId}`);
}
}
let nextExpectedRuleIdIndex = 0;
const newRules: IPushRule[] = [];
for (const rule of incomingRules.slice(0, firstCustomRuleIndex)) {
const ruleIndex = defaultRuleIds.indexOf(rule.rule_id);
// Merge our expected rules (including the incoming custom rules) into the incoming default rules.
for (const rule of incomingDefaultRules) {
const ruleIndex = orderedRuleIds.indexOf(rule.rule_id);
if (ruleIndex === -1) {
// an unrecognised rule; copy it over
newRules.push(rule);
@@ -198,7 +211,7 @@ function mergeRulesWithDefaults(
}
while (ruleIndex > nextExpectedRuleIdIndex) {
// insert new rules
const defaultRuleId = defaultRuleIds[nextExpectedRuleIdIndex];
const defaultRuleId = orderedRuleIds[nextExpectedRuleIdIndex];
insertDefaultPushRule(defaultRuleId);
nextExpectedRuleIdIndex += 1;
}
@@ -208,12 +221,10 @@ function mergeRulesWithDefaults(
}
// Now copy over any remaining default rules
for (const ruleId of defaultRuleIds.slice(nextExpectedRuleIdIndex)) {
for (const ruleId of orderedRuleIds.slice(nextExpectedRuleIdIndex)) {
insertDefaultPushRule(ruleId);
}
// Finally any non-default rules that were in `incomingRules`
newRules.push(...incomingRules.slice(firstCustomRuleIndex));
return newRules;
}
@@ -281,12 +292,14 @@ export class PushProcessor {
// Merge the client-level defaults with the ones from the server
newRules.global.override = mergeRulesWithDefaults(
PushRuleKind.Override,
newRules.global.override,
DEFAULT_OVERRIDE_RULES,
EXPECTED_DEFAULT_OVERRIDE_RULE_IDS,
);
newRules.global.underride = mergeRulesWithDefaults(
PushRuleKind.Underride,
newRules.global.underride,
DEFAULT_UNDERRIDE_RULES,
EXPECTED_DEFAULT_UNDERRIDE_RULE_IDS,
+2 -2
View File
@@ -29,7 +29,7 @@ import {
import { logger } from "../logger";
import { calculateRetryBackoff, IHttpOpts, MatrixHttpApi, Method } from "../http-api";
import { logDuration, QueryDict, sleep } from "../utils";
import { IAuthDict, UIAuthCallback } from "../interactive-auth";
import { AuthDict, UIAuthCallback } from "../interactive-auth";
import { UIAResponse } from "../@types/uia";
import { ToDeviceMessageId } from "../@types/event";
@@ -169,7 +169,7 @@ export class OutgoingRequestProcessor {
}
const parsedBody = JSON.parse(body);
const makeRequest = async (auth: IAuthDict | null): Promise<UIAResponse<T>> => {
const makeRequest = async (auth: AuthDict | null): Promise<UIAResponse<T>> => {
const newBody: Record<string, any> = {
...parsedBody,
};
+51 -47
View File
@@ -23,7 +23,7 @@ import type { IEncryptedEventInfo } from "../crypto/api";
import { IContent, MatrixEvent, MatrixEventEvent } from "../models/event";
import { Room } from "../models/room";
import { RoomMember } from "../models/room-member";
import { BackupDecryptor, CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
import { BackupDecryptor, CryptoBackend, DecryptionError, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
import { logger, Logger } from "../logger";
import { IHttpOpts, MatrixHttpApi, Method } from "../http-api";
import { RoomEncryptor } from "./RoomEncryptor";
@@ -39,6 +39,7 @@ import {
CrossSigningStatus,
CryptoCallbacks,
Curve25519AuthData,
DecryptionFailureCode,
DeviceVerificationStatus,
EventEncryptionInfo,
EventShieldColour,
@@ -70,11 +71,16 @@ import { randomString } from "../randomstring";
import { ClientStoppedError } from "../errors";
import { ISignatures } from "../@types/signed";
import { encodeBase64 } from "../base64";
import { DecryptionError } from "../crypto/algorithms";
import { OutgoingRequestsManager } from "./OutgoingRequestsManager";
import { PerSessionKeyBackupDownloader } from "./PerSessionKeyBackupDownloader";
import { VerificationMethod } from "../types";
const ALL_VERIFICATION_METHODS = ["m.sas.v1", "m.qr_code.scan.v1", "m.qr_code.show.v1", "m.reciprocate.v1"];
const ALL_VERIFICATION_METHODS = [
VerificationMethod.Sas,
VerificationMethod.ScanQrCode,
VerificationMethod.ShowQrCode,
VerificationMethod.Reciprocate,
];
interface ISignableObject {
signatures?: ISignatures;
@@ -1677,52 +1683,50 @@ class EventDecryptor {
forwardingCurve25519KeyChain: res.forwardingCurve25519KeyChain,
};
} catch (err) {
// We need to map back to regular decryption errors (used for analytics for example)
// The DecryptionErrors are used by react-sdk so is implicitly part of API, but poorly typed
if (err instanceof RustSdkCryptoJs.MegolmDecryptionError) {
const content = event.getWireContent();
let jsError;
switch (err.code) {
case RustSdkCryptoJs.DecryptionErrorCode.MissingRoomKey: {
jsError = new DecryptionError(
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
"The sender's device has not sent us the keys for this message.",
{
session: content.sender_key + "|" + content.session_id,
},
);
this.perSessionBackupDownloader.onDecryptionKeyMissingError(
event.getRoomId()!,
event.getWireContent().session_id!,
);
break;
}
case RustSdkCryptoJs.DecryptionErrorCode.UnknownMessageIndex: {
jsError = new DecryptionError(
"OLM_UNKNOWN_MESSAGE_INDEX",
"The sender's device has not sent us the keys for this message at this index.",
{
session: content.sender_key + "|" + content.session_id,
},
);
this.perSessionBackupDownloader.onDecryptionKeyMissingError(
event.getRoomId()!,
event.getWireContent().session_id!,
);
break;
}
// We don't map MismatchedIdentityKeys for now, as there is no equivalent in legacy.
// Just put it on the `UNABLE_TO_DECRYPT` bucket.
default: {
jsError = new DecryptionError("UNABLE_TO_DECRYPT", err.description, {
session: content.sender_key + "|" + content.session_id,
});
break;
}
}
throw jsError;
this.onMegolmDecryptionError(event, err);
} else {
throw new DecryptionError(DecryptionFailureCode.UNKNOWN_ERROR, "Unknown error");
}
throw new DecryptionError("UNABLE_TO_DECRYPT", "Unknown error");
}
}
/**
* Handle a `MegolmDecryptionError` returned by the rust SDK.
*
* Fires off a request to the `perSessionBackupDownloader`, if appropriate, and then throws a `DecryptionError`.
*/
private onMegolmDecryptionError(event: MatrixEvent, err: RustSdkCryptoJs.MegolmDecryptionError): never {
const content = event.getWireContent();
// If the error looks like it might be recoverable from backup, queue up a request to try that.
if (
err.code === RustSdkCryptoJs.DecryptionErrorCode.MissingRoomKey ||
err.code === RustSdkCryptoJs.DecryptionErrorCode.UnknownMessageIndex
) {
this.perSessionBackupDownloader.onDecryptionKeyMissingError(event.getRoomId()!, content.session_id!);
}
const errorDetails = { session: content.sender_key + "|" + content.session_id };
switch (err.code) {
case RustSdkCryptoJs.DecryptionErrorCode.MissingRoomKey:
throw new DecryptionError(
DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
"The sender's device has not sent us the keys for this message.",
errorDetails,
);
case RustSdkCryptoJs.DecryptionErrorCode.UnknownMessageIndex:
throw new DecryptionError(
DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX,
"The sender's device has not sent us the keys for this message at this index.",
errorDetails,
);
// We don't map MismatchedIdentityKeys for now, as there is no equivalent in legacy.
// Just put it on the `UNKNOWN_ERROR` bucket.
default:
throw new DecryptionError(DecryptionFailureCode.UNKNOWN_ERROR, err.description, errorDetails);
}
}
+8 -7
View File
@@ -35,6 +35,7 @@ import { TypedReEmitter } from "../ReEmitter";
import { MatrixEvent } from "../models/event";
import { EventType, MsgType } from "../@types/event";
import { defer, IDeferred } from "../utils";
import { VerificationMethod } from "../types";
/**
* An incoming, or outgoing, request to verify a user or a device via cross-signing.
@@ -230,9 +231,9 @@ export class RustVerificationRequest
const verification: RustSdkCryptoJs.Qr | RustSdkCryptoJs.Sas | undefined = this.inner.getVerification();
if (verification instanceof RustSdkCryptoJs.Sas) {
return "m.sas.v1";
return VerificationMethod.Sas;
} else if (verification instanceof RustSdkCryptoJs.Qr) {
return "m.reciprocate.v1";
return VerificationMethod.Reciprocate;
} else {
return null;
}
@@ -336,7 +337,7 @@ export class RustVerificationRequest
* @param method - the name of the verification method to use.
*/
public async startVerification(method: string): Promise<Verifier> {
if (method !== "m.sas.v1") {
if (method !== VerificationMethod.Sas) {
throw new Error(`Unsupported verification method ${method}`);
}
@@ -756,10 +757,10 @@ export class RustSASVerifier extends BaseRustVerifer<RustSdkCryptoJs.Sas> implem
/** For each specced verification method, the rust-side `VerificationMethod` corresponding to it */
const verificationMethodsByIdentifier: Record<string, RustSdkCryptoJs.VerificationMethod> = {
"m.sas.v1": RustSdkCryptoJs.VerificationMethod.SasV1,
"m.qr_code.scan.v1": RustSdkCryptoJs.VerificationMethod.QrCodeScanV1,
"m.qr_code.show.v1": RustSdkCryptoJs.VerificationMethod.QrCodeShowV1,
"m.reciprocate.v1": RustSdkCryptoJs.VerificationMethod.ReciprocateV1,
[VerificationMethod.Sas]: RustSdkCryptoJs.VerificationMethod.SasV1,
[VerificationMethod.ScanQrCode]: RustSdkCryptoJs.VerificationMethod.QrCodeScanV1,
[VerificationMethod.ShowQrCode]: RustSdkCryptoJs.VerificationMethod.QrCodeShowV1,
[VerificationMethod.Reciprocate]: RustSdkCryptoJs.VerificationMethod.ReciprocateV1,
};
/**
+17 -46
View File
@@ -32,7 +32,6 @@ import { deepCopy, defer, IDeferred, noUnsafeEventProps, promiseMapSeries, unsaf
import { Filter } from "./filter";
import { EventTimeline } from "./models/event-timeline";
import { logger } from "./logger";
import { InvalidStoreError, InvalidStoreState } from "./errors";
import { ClientEvent, IStoredClientOpts, MatrixClient, PendingEventOrdering, ResetTimelineCallback } from "./client";
import {
IEphemeral,
@@ -593,25 +592,6 @@ export class SyncApi {
await keepaliveProm;
}
/**
* Is the lazy loading option different than in previous session?
* @param lazyLoadMembers - current options for lazy loading
* @returns whether or not the option has changed compared to the previous session */
private async wasLazyLoadingToggled(lazyLoadMembers = false): Promise<boolean> {
// assume it was turned off before
// if we don't know any better
let lazyLoadMembersBefore = false;
const isStoreNewlyCreated = await this.client.store.isNewlyCreated();
if (!isStoreNewlyCreated) {
const prevClientOptions = await this.client.store.getClientOptions();
if (prevClientOptions) {
lazyLoadMembersBefore = !!prevClientOptions.lazyLoadMembers;
}
return lazyLoadMembersBefore !== lazyLoadMembers;
}
return false;
}
private shouldAbortSync(error: MatrixError): boolean {
if (error.errcode === "M_UNKNOWN_TOKEN") {
// The logout already happened, we just need to stop.
@@ -649,9 +629,9 @@ export class SyncApi {
return filter;
};
private checkLazyLoadStatus = async (): Promise<void> => {
debuglog("Checking lazy load status...");
if (this.opts.lazyLoadMembers && this.client.isGuest()) {
private prepareLazyLoadingForSync = async (): Promise<void> => {
debuglog("Prepare lazy loading for sync...");
if (this.client.isGuest()) {
this.opts.lazyLoadMembers = false;
}
if (this.opts.lazyLoadMembers) {
@@ -661,23 +641,12 @@ export class SyncApi {
}
this.opts.filter.setLazyLoadMembers(true);
}
// need to vape the store when enabling LL and wasn't enabled before
debuglog("Checking whether lazy loading has changed in store...");
const shouldClear = await this.wasLazyLoadingToggled(this.opts.lazyLoadMembers);
if (shouldClear) {
this.storeIsInvalid = true;
const error = new InvalidStoreError(InvalidStoreState.ToggledLazyLoading, !!this.opts.lazyLoadMembers);
this.updateSyncState(SyncState.Error, { error });
// bail out of the sync loop now: the app needs to respond to this error.
// we leave the state as 'ERROR' which isn't great since this normally means
// we're retrying. The client must be stopped before clearing the stores anyway
// so the app should stop the client, clear the store and start it again.
logger.warn("InvalidStoreError: store is not usable: stopping sync.");
return;
}
if (this.opts.lazyLoadMembers) {
this.syncOpts.crypto?.enableLazyLoading();
}
};
private storeClientOptions = async (): Promise<void> => {
try {
debuglog("Storing client options...");
await this.client.storeClientOptions();
@@ -757,14 +726,16 @@ export class SyncApi {
// 1) We need to get push rules so we can check if events should bing as we get
// them from /sync.
// 2) We need to get/create a filter which we can use for /sync.
// 3) We need to check the lazy loading option matches what was used in the
// stored sync. If it doesn't, we can't use the stored sync.
// 3) We need to prepare lazy loading for sync
// 4) We need to store the client options
// Now start the first incremental sync request: this can also
// take a while so if we set it going now, we can wait for it
// to finish while we process our saved sync data.
await this.getPushRules();
await this.checkLazyLoadStatus();
await this.prepareLazyLoadingForSync();
await this.storeClientOptions();
const { filterId, filter } = await this.getFilter();
if (!filter) return; // bail, getFilter failed
@@ -1355,11 +1326,11 @@ export class SyncApi {
const unreadThreadNotifications =
joinObj[UNREAD_THREAD_NOTIFICATIONS.name] ?? joinObj[UNREAD_THREAD_NOTIFICATIONS.altName!];
if (unreadThreadNotifications) {
// Only partially reset unread notification
// We want to keep the client-generated count. Particularly important
// for encrypted room that refresh their notification count on event
// decryption
room.resetThreadUnreadNotificationCount(Object.keys(unreadThreadNotifications));
// This mirrors the logic above for rooms: take the *total* notification count from
// the server for unencrypted rooms or is it's zero. Any threads not present in this
// object implicitly have zero notifications, so start by clearing the total counts
// for all such threads.
room.resetThreadUnreadNotificationCountFromSync(Object.keys(unreadThreadNotifications));
for (const [threadId, unreadNotification] of Object.entries(unreadThreadNotifications)) {
if (!encrypted || unreadNotification.notification_count === 0) {
room.setThreadUnreadNotificationCount(
@@ -1380,7 +1351,7 @@ export class SyncApi {
}
}
} else {
room.resetThreadUnreadNotificationCount();
room.resetThreadUnreadNotificationCountFromSync();
}
joinObj.timeline = joinObj.timeline || ({} as ITimeline);
+191
View File
@@ -0,0 +1,191 @@
/*
Copyright 2024 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* This file is a secondary entrypoint for the js-sdk library, exposing utilities which might be useful for writing tests.
*
* In general, it should not be included in runtime applications.
*
* @packageDocumentation
*/
import { IContent, IEvent, IUnsigned, MatrixEvent } from "./models/event";
import { RoomMember } from "./models/room-member";
import { EventType } from "./@types/event";
import { DecryptionError } from "./crypto/algorithms";
import { DecryptionFailureCode } from "./crypto-api";
import { EventDecryptionResult } from "./common-crypto/CryptoBackend";
/**
* Create a {@link MatrixEvent}.
*
* @param opts - Values for the event.
*/
export function mkMatrixEvent(opts: {
/** Room ID of the event. */
roomId: string;
/** The sender of the event. */
sender: string;
/** The type of the event. */
type: EventType | string;
/** Optional `state_key` for the event. If unspecified, a non-state event is created. */
stateKey?: string;
/** Optional `origin_server_ts` for the event. If unspecified, the timestamp will be set to 0. */
ts?: number;
/** Optional `event_id` for the event. If provided will be used as event ID; else an ID is generated. */
eventId?: string;
/** Content of the event. */
content: IContent;
/** Optional `unsigned` data for the event. */
unsigned?: IUnsigned;
}): MatrixEvent {
const event: Partial<IEvent> = {
type: opts.type,
room_id: opts.roomId,
sender: opts.sender,
content: opts.content,
event_id: opts.eventId ?? "$" + Math.random() + "-" + Math.random(),
origin_server_ts: opts.ts ?? 0,
unsigned: opts.unsigned,
};
if (opts.stateKey !== undefined) {
event.state_key = opts.stateKey;
}
const mxEvent = new MatrixEvent(event);
mxEvent.sender = {
userId: opts.sender,
membership: "join",
name: opts.sender,
rawDisplayName: opts.sender,
roomId: opts.sender,
getAvatarUrl: () => {},
getMxcAvatarUrl: () => {},
} as unknown as RoomMember;
return mxEvent;
}
/**
* Create a `MatrixEvent` representing a successfully-decrypted `m.room.encrypted` event.
*
* @param opts - Values for the event.
*/
export async function mkEncryptedMatrixEvent(opts: {
/** Room ID of the event. */
roomId: string;
/** The sender of the event. */
sender: string;
/** The type the event will have, once it has been decrypted. */
plainType: EventType | string;
/** The content the event will have, once it has been decrypted. */
plainContent: IContent;
/** Optional `event_id` for the event. If provided will be used as event ID; else an ID is generated. */
eventId?: string;
}): Promise<MatrixEvent> {
const mxEvent = mkMatrixEvent({
type: EventType.RoomMessageEncrypted,
roomId: opts.roomId,
sender: opts.sender,
content: { algorithm: "m.megolm.v1.aes-sha2" },
eventId: opts.eventId,
});
await decryptExistingEvent(mxEvent, { plainType: opts.plainType, plainContent: opts.plainContent });
return mxEvent;
}
/**
* Create a `MatrixEvent` representing a `m.room.encrypted` event which could not be decrypted.
*
* @param opts - Values for the event.
*/
export async function mkDecryptionFailureMatrixEvent(opts: {
/** Room ID of the event. */
roomId: string;
/** The sender of the event. */
sender: string;
/** The reason code for the failure */
code: DecryptionFailureCode;
/** A textual reason for the failure */
msg: string;
/** Optional `event_id` for the event. If provided will be used as event ID; else an ID is generated. */
eventId?: string;
}): Promise<MatrixEvent> {
const mxEvent = mkMatrixEvent({
type: EventType.RoomMessageEncrypted,
roomId: opts.roomId,
sender: opts.sender,
content: { algorithm: "m.megolm.v1.aes-sha2" },
eventId: opts.eventId,
});
const mockCrypto = {
decryptEvent: async (_ev): Promise<EventDecryptionResult> => {
throw new DecryptionError(opts.code, opts.msg);
},
} as Parameters<MatrixEvent["attemptDecryption"]>[0];
await mxEvent.attemptDecryption(mockCrypto);
return mxEvent;
}
/**
* Given an event previously returned by {@link mkDecryptionFailureMatrixEvent}, simulate a successful re-decryption
* attempt.
*
* @param mxEvent - The event that will be decrypted.
* @param opts - New data for the successful decryption.
*/
export async function decryptExistingEvent(
mxEvent: MatrixEvent,
opts: {
/** The type the event will have, once it has been decrypted. */
plainType: EventType | string;
/** The content the event will have, once it has been decrypted. */
plainContent: IContent;
},
): Promise<void> {
const decryptionResult: EventDecryptionResult = {
claimedEd25519Key: "",
clearEvent: {
type: opts.plainType,
content: opts.plainContent,
},
forwardingCurve25519KeyChain: [],
senderCurve25519Key: "",
untrusted: false,
};
const mockCrypto = {
decryptEvent: async (_ev): Promise<EventDecryptionResult> => decryptionResult,
} as Parameters<MatrixEvent["attemptDecryption"]>[0];
await mxEvent.attemptDecryption(mockCrypto);
}
+33
View File
@@ -23,3 +23,36 @@ limitations under the License.
export type * from "./@types/media";
export * from "./@types/membership";
export type * from "./@types/event";
export type * from "./@types/events";
export type * from "./@types/state_events";
/** The different methods for device and user verification */
export enum VerificationMethod {
/** Short authentication string (emoji or decimals).
*
* @see https://spec.matrix.org/v1.9/client-server-api/#short-authentication-string-sas-verification
*/
Sas = "m.sas.v1",
/**
* Verification by showing a QR code which is scanned by the other device.
*
* @see https://spec.matrix.org/v1.9/client-server-api/#qr-codes
*/
ShowQrCode = "m.qr_code.show.v1",
/**
* Verification by scanning a QR code that is shown by the other device.
*
* @see https://spec.matrix.org/v1.9/client-server-api/#qr-codes
*/
ScanQrCode = "m.qr_code.scan.v1",
/**
* Verification by confirming that we have scanned a QR code.
*
* @see https://spec.matrix.org/v1.9/client-server-api/#qr-codes
*/
Reciprocate = "m.reciprocate.v1",
}
+29 -11
View File
@@ -26,8 +26,8 @@ import { parse as parseSdp, write as writeSdp } from "sdp-transform";
import { logger } from "../logger";
import { checkObjectHasKeys, isNullOrUndefined, recursivelyAssign } from "../utils";
import { IContent, MatrixEvent } from "../models/event";
import { EventType, ToDeviceMessageId } from "../@types/event";
import { MatrixEvent } from "../models/event";
import { EventType, TimelineEvents, ToDeviceMessageId } from "../@types/event";
import { RoomMember } from "../models/room-member";
import { randomString } from "../randomstring";
import {
@@ -293,13 +293,24 @@ function getCodecParamMods(isPtt: boolean): CodecParamsMod[] {
return mods;
}
type CallEventType =
| EventType.CallReplaces
| EventType.CallAnswer
| EventType.CallSelectAnswer
| EventType.CallNegotiate
| EventType.CallInvite
| EventType.CallCandidates
| EventType.CallHangup
| EventType.CallReject
| EventType.CallSDPStreamMetadataChangedPrefix;
export interface VoipEvent {
type: "toDevice" | "sendEvent";
eventType: string;
userId?: string;
opponentDeviceId?: string;
roomId?: string;
content: Record<string, unknown>;
content: TimelineEvents[CallEventType];
}
/**
@@ -406,7 +417,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// If candidates arrive before we've picked an opponent (which, in particular,
// will happen if the opponent sends candidates eagerly before the user answers
// the call) we buffer them up here so we can then add the ones from the party we pick
private remoteCandidateBuffer = new Map<string, RTCIceCandidate[]>();
private remoteCandidateBuffer = new Map<string, MCallCandidates["candidates"]>();
private remoteAssertedIdentity?: AssertedIdentity;
private remoteSDPStreamMetadata?: SDPStreamMetadata;
@@ -1156,7 +1167,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.terminate(CallParty.Local, reason, !suppressEvent);
// We don't want to send hangup here if we didn't even get to sending an invite
if ([CallState.Fledgling, CallState.WaitLocalMedia].includes(this.state)) return;
const content: IContent = {};
const content: Omit<MCallHangupReject, "version" | "call_id" | "party_id" | "conf_id"> = {};
// Don't send UserHangup reason to older clients
if ((this.opponentVersion && this.opponentVersion !== 0) || reason !== CallErrorCode.UserHangup) {
content["reason"] = reason;
@@ -1916,7 +1927,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
if (this.opponentPartyId !== null) {
try {
await this.sendVoipEvent(EventType.CallSelectAnswer, {
selected_party_id: this.opponentPartyId,
selected_party_id: this.opponentPartyId!,
});
} catch (err) {
// This isn't fatal, and will just mean that if another party has raced to answer
@@ -2012,6 +2023,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
logger.debug(`Call ${this.callId} onNegotiateReceived() create an answer`);
this.sendVoipEvent(EventType.CallNegotiate, {
lifetime: CALL_TIMEOUT_MS,
description: this.peerConn!.localDescription?.toJSON(),
[SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(true),
});
@@ -2444,13 +2456,17 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
/**
* @internal
*/
private async sendVoipEvent(eventType: string, content: object): Promise<void> {
const realContent = Object.assign({}, content, {
private async sendVoipEvent<K extends keyof Pick<TimelineEvents, CallEventType>>(
eventType: K,
content: Omit<TimelineEvents[K], "version" | "call_id" | "party_id" | "conf_id">,
): Promise<void> {
const realContent = {
...content,
version: VOIP_PROTO_VERSION,
call_id: this.callId,
party_id: this.ourPartyId,
conf_id: this.groupCallId,
});
} as TimelineEvents[K];
if (this.opponentDeviceId) {
const toDeviceSeq = this.toDeviceSeq++;
@@ -2729,7 +2745,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
const candidates = this.candidateSendQueue;
this.candidateSendQueue = [];
++this.candidateSendTries;
const content = { candidates: candidates.map((candidate) => candidate.toJSON()) };
const content: Pick<MCallCandidates, "candidates"> = {
candidates: candidates.map((candidate) => candidate.toJSON()),
};
if (this.candidatesEnded) {
// If there are no more candidates, signal this by adding an empty string candidate
content.candidates.push({
@@ -2923,7 +2941,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.remoteCandidateBuffer.clear();
}
private async addIceCandidates(candidates: RTCIceCandidate[]): Promise<void> {
private async addIceCandidates(candidates: RTCIceCandidate[] | MCallCandidates["candidates"]): Promise<void> {
for (const candidate of candidates) {
if (
(candidate.sdpMid === null || candidate.sdpMid === undefined) &&
+2 -1
View File
@@ -34,6 +34,7 @@ export interface CallReplacesTarget {
export interface MCallBase {
call_id: string;
conf_id?: string;
version: string | number;
party_id?: string;
sender_session_id?: string;
@@ -82,7 +83,7 @@ export interface MCAllAssertedIdentity extends MCallBase {
}
export interface MCallCandidates extends MCallBase {
candidates: RTCIceCandidate[];
candidates: Omit<RTCIceCandidateInit, "usernameFragment">[];
}
export interface MCallHangupReject extends MCallBase {
+7
View File
@@ -35,6 +35,7 @@ import {
import { SummaryStatsReportGatherer } from "./stats/summaryStatsReportGatherer";
import { CallFeedStatsReporter } from "./stats/callFeedStatsReporter";
import { KnownMembership } from "../@types/membership";
import { CallMembershipData } from "../matrixrtc/CallMembership";
export enum GroupCallIntent {
Ring = "m.ring",
@@ -167,6 +168,7 @@ export interface IGroupCallDataChannelOptions {
export interface IGroupCallRoomState {
"m.intent": GroupCallIntent;
"m.type": GroupCallType;
"m.terminated"?: GroupCallTerminationReason;
"io.element.ptt"?: boolean;
// TODO: Specify data-channels
"dataChannelsEnabled"?: boolean;
@@ -196,6 +198,11 @@ export interface IGroupCallRoomMemberState {
"m.calls": IGroupCallRoomMemberCallState[];
}
// XXX: this hasn't made it into the MSC yet
export interface ExperimentalGroupCallRoomMemberState {
memberships: CallMembershipData[];
}
export enum GroupCallState {
LocalCallFeedUninitialized = "local_call_feed_uninitialized",
InitializingLocalCallFeed = "initializing_local_call_feed",
+1 -1
View File
@@ -2,7 +2,7 @@
"$schema": "https://typedoc.org/schema.json",
"plugin": ["typedoc-plugin-mdn-links", "typedoc-plugin-missing-exports", "typedoc-plugin-coverage"],
"coverageLabel": "TypeDoc",
"entryPoints": ["src/matrix.ts", "src/types.ts"],
"entryPoints": ["src/matrix.ts", "src/types.ts", "src/testing.ts"],
"excludeExternals": true,
"out": "_docs"
}
+575 -532
View File
File diff suppressed because it is too large Load Diff