Compare commits
40 Commits
v31.6.0-rc.0
...
v32.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| e4937e6222 | |||
| 7d5c003716 | |||
| dbab185f9d | |||
| cfcd191cbf | |||
| 514633c5fa | |||
| 5bffb7df4f | |||
| 9e1897dcd0 | |||
| 5f3ddc37a1 | |||
| 78a225795b | |||
| 467b49a0dc | |||
| 06e083874a | |||
| 0f25429849 | |||
| 32ddf2813d | |||
| 1ed082f3d4 | |||
| 706002cdcb | |||
| 731de1108c | |||
| 2da6c0c605 | |||
| 9f1d0c3896 | |||
| 0b290fffa1 | |||
| 97844f0e47 | |||
| 85a55c79cd | |||
| 63d4195453 | |||
| d5a35f8a99 | |||
| d1259b241c | |||
| a573727662 | |||
| dce8acbf17 | |||
| 4ba1341f8f | |||
| e517d009bf | |||
| dc2d03dea5 | |||
| d5bb9e7600 | |||
| d908036f50 | |||
| afc3c6213b | |||
| 7884c22e41 | |||
| 887d8a7663 | |||
| 2c68ee2254 | |||
| d445823d0b | |||
| abe4630687 | |||
| 8664b66238 | |||
| 596826ab4d | |||
| c8ec5421c7 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
|
||||
@@ -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: "",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
@@ -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",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`.
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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 + '".',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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
@@ -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`.
|
||||
*/
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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];
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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) &&
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user