Compare commits
64 Commits
v31.5.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 | |||
| b8078f9916 | |||
| 92342c07ed | |||
| 3e989006aa | |||
| 8e0ef5ff2c | |||
| 78d05942a3 | |||
| c22a6858c8 | |||
| 461aeae281 | |||
| 0511e313d3 | |||
| da3d5c4a43 | |||
| 4c26b55c9a | |||
| 3711ad7e61 | |||
| 3031152444 | |||
| 51ebd2fcde | |||
| 27dd856778 | |||
| 7fee37680f | |||
| 2541ca04c2 | |||
| d55c6a36df | |||
| 8c0736a719 | |||
| 50b042d1ff | |||
| a818dc1e9d | |||
| d8dae65a4d | |||
| 8be286308c | |||
| 84498bf77d | |||
| a1f4b07b7d |
@@ -0,0 +1 @@
|
||||
_docs
|
||||
@@ -103,11 +103,8 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
{
|
||||
// We don't need amazing docs in our spec files
|
||||
files: ["src/**/*.ts"],
|
||||
rules: {
|
||||
"tsdoc/syntax": "error",
|
||||
// We use some select jsdoc rules as the tsdoc linter has only one rule
|
||||
"jsdoc/no-types": "error",
|
||||
"jsdoc/empty-tags": "error",
|
||||
"jsdoc/check-property-names": "error",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Tests written for new code (and old code if feasible)
|
||||
- [ ] Linter and other CI checks pass
|
||||
- [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md))
|
||||
- [ ] Tests written for new code (and old code if feasible).
|
||||
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
|
||||
- [ ] Linter and other CI checks pass.
|
||||
- [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md)).
|
||||
|
||||
@@ -22,7 +22,7 @@ runs:
|
||||
|
||||
- name: Upload tarball signature
|
||||
if: ${{ inputs.upload-url }}
|
||||
uses: shogo82148/actions-upload-release-asset@dccd6d23e64fd6a746dce6814c0bde0a04886085 # v1
|
||||
uses: shogo82148/actions-upload-release-asset@5bd52f05dd8076794da5975d4c0a4f3bce7dd8f5 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ env.VERSION }}.tar.gz.asc
|
||||
|
||||
@@ -29,13 +29,13 @@ runs:
|
||||
|
||||
- name: Upload asset signatures
|
||||
if: inputs.gpg-fingerprint
|
||||
uses: shogo82148/actions-upload-release-asset@dccd6d23e64fd6a746dce6814c0bde0a04886085 # v1
|
||||
uses: shogo82148/actions-upload-release-asset@5bd52f05dd8076794da5975d4c0a4f3bce7dd8f5 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ inputs.asset-path }}.asc
|
||||
|
||||
- name: Upload assets
|
||||
uses: shogo82148/actions-upload-release-asset@dccd6d23e64fd6a746dce6814c0bde0a04886085 # v1
|
||||
uses: shogo82148/actions-upload-release-asset@5bd52f05dd8076794da5975d4c0a4f3bce7dd8f5 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ inputs.asset-path }}
|
||||
|
||||
@@ -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.92.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
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
steps:
|
||||
- name: Check membership
|
||||
if: github.event.pull_request.user.login != 'renovate[bot]'
|
||||
uses: tspascoal/get-user-teams-membership@ba78054988f58bea69b7c6136d563236f8ed2fc0 # v3
|
||||
uses: tspascoal/get-user-teams-membership@57e9f42acd78f4d0f496b3be4368fc5f62696662 # v3
|
||||
id: teams
|
||||
with:
|
||||
username: ${{ github.event.pull_request.user.login }}
|
||||
|
||||
@@ -294,15 +294,15 @@ jobs:
|
||||
remove-labels: X-Upcoming-Release-Blocker
|
||||
add-labels: X-Release-Blocker
|
||||
|
||||
- name: Wait for master->develop gitflow merge
|
||||
if: inputs.final
|
||||
uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
|
||||
with:
|
||||
ref: master
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
wait-interval: 10
|
||||
check-name: merge
|
||||
allowed-conclusions: success
|
||||
# - name: Wait for master->develop gitflow merge
|
||||
# if: inputs.final
|
||||
# uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
|
||||
# with:
|
||||
# ref: master
|
||||
# repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# wait-interval: 10
|
||||
# check-name: merge
|
||||
# allowed-conclusions: success
|
||||
|
||||
bump-downstreams:
|
||||
name: Update npm dependency in downstream projects
|
||||
|
||||
@@ -39,12 +39,6 @@ jobs:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 🧮 Checkout gh-pages
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: gh-pages
|
||||
path: _docs
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -53,26 +47,26 @@ jobs:
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --frozen-lockfile"
|
||||
|
||||
- name: 🔨 Install symlinks
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y symlinks
|
||||
|
||||
- name: 📖 Generate docs
|
||||
run: |
|
||||
yarn tpv purge --yes --out _docs --stale --major 10
|
||||
yarn gendoc
|
||||
symlinks -rc _docs
|
||||
find _docs -xtype l -delete
|
||||
run: yarn gendoc
|
||||
|
||||
- name: 🔨 Set up git
|
||||
run: |
|
||||
git config --global user.email "releases@riot.im"
|
||||
git config --global user.name "RiotRobot"
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: _docs
|
||||
|
||||
- name: 🚀 Deploy
|
||||
run: |
|
||||
git add . --all
|
||||
git commit -m "Update docs"
|
||||
git push
|
||||
working-directory: _docs
|
||||
docs-deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
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
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
@@ -83,13 +83,6 @@ jobs:
|
||||
- name: Generate Docs
|
||||
run: "yarn run gendoc --treatWarningsAsErrors"
|
||||
|
||||
# Upload artifact duplicates symlink contents so we do this to save 75% space
|
||||
- name: Flatten symlink and write _redirects
|
||||
run: |
|
||||
find _docs -mindepth 1 -maxdepth 1 ! -type f ! -name stable -printf '/%f/* /stable/:splat\n' > _docs/_redirects
|
||||
find _docs -mindepth 1 -maxdepth 1 -type l -delete
|
||||
find _docs -mindepth 1 -maxdepth 1 -type d -execdir mv {} stable \; -quit
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
@@ -1,3 +1,64 @@
|
||||
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
|
||||
|
||||
* Update MSC2965 OIDC Discovery implementation ([#4064](https://github.com/matrix-org/matrix-js-sdk/pull/4064)). Contributed by @t3chguy.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Add basic retry for rust crypto outgoing requests ([#4061](https://github.com/matrix-org/matrix-js-sdk/pull/4061)). Contributed by @BillCarsonFr.
|
||||
|
||||
|
||||
Changes in [31.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.4.0) (2024-02-27)
|
||||
==================================================================================================
|
||||
## ✨ 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,41 +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.
|
||||
| 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";
|
||||
@@ -211,10 +179,10 @@ const matrixClient = sdk.createClient({
|
||||
### Automatically join rooms when invited
|
||||
|
||||
```javascript
|
||||
matrixClient.on(RoomMemberEvent.Membership, function (event, member) {
|
||||
if (member.membership === "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);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -294,7 +262,7 @@ host the API reference from the source files like this:
|
||||
|
||||
```
|
||||
$ yarn gendoc
|
||||
$ cd _docs
|
||||
$ cd docs
|
||||
$ python -m http.server 8005
|
||||
```
|
||||
|
||||
@@ -302,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.
|
||||
|
||||
@@ -115,7 +115,7 @@ rl.on("line", function (line) {
|
||||
if (line.indexOf("/join ") === 0) {
|
||||
var roomIndex = line.split(" ")[1];
|
||||
viewingRoom = roomList[roomIndex];
|
||||
if (viewingRoom.getMember(myUserId).membership === "invite") {
|
||||
if (viewingRoom.getMember(myUserId).membership === KnownMembership.Invite) {
|
||||
// join the room first
|
||||
matrixClient.joinRoom(viewingRoom.roomId).then(
|
||||
function (room) {
|
||||
|
||||
+7
-9
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "31.5.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",
|
||||
@@ -67,8 +67,8 @@
|
||||
"uuid": "9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@action-validator/cli": "^0.5.3",
|
||||
"@action-validator/core": "^0.5.3",
|
||||
"@action-validator/cli": "^0.6.0",
|
||||
"@action-validator/core": "^0.6.0",
|
||||
"@babel/cli": "^7.12.10",
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/eslint-parser": "^7.12.10",
|
||||
@@ -97,7 +97,7 @@
|
||||
"babel-jest": "^29.0.0",
|
||||
"debug": "^4.3.4",
|
||||
"domexception": "^4.0.0",
|
||||
"eslint": "8.56.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
@@ -121,13 +121,11 @@
|
||||
"prettier": "3.2.5",
|
||||
"rimraf": "^5.0.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typedoc": "^0.24.0",
|
||||
"typedoc": "^0.25.10",
|
||||
"typedoc-plugin-coverage": "^3.0.0",
|
||||
"typedoc-plugin-mdn-links": "^3.0.3",
|
||||
"typedoc-plugin-missing-exports": "^2.0.0",
|
||||
"typedoc-plugin-versions": "^0.2.3",
|
||||
"typedoc-plugin-versions-cli": "^0.1.12",
|
||||
"typescript": "^5.0.0"
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"@casualbot/jest-sonar-reporter": {
|
||||
"outputDirectory": "coverage",
|
||||
|
||||
@@ -45,7 +45,7 @@ async function getRelease(github, dependency) {
|
||||
|
||||
if (dependency.includes("/")) {
|
||||
let rest;
|
||||
[owner, rest] = dependency.split("/")[0];
|
||||
[owner, rest] = dependency.split("/");
|
||||
|
||||
if (dependency.includes("@")) {
|
||||
[repo, tag] = rest.split("@");
|
||||
|
||||
@@ -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,
|
||||
@@ -99,6 +101,7 @@ import {
|
||||
} from "./olm-utils";
|
||||
import { ToDevicePayload } from "../../../src/models/ToDeviceMessage";
|
||||
import { AccountDataAccumulator } from "../../test-utils/AccountDataAccumulator";
|
||||
import { KnownMembership } from "../../../src/@types/membership";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
@@ -464,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: {
|
||||
@@ -489,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]);
|
||||
|
||||
@@ -521,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();
|
||||
}
|
||||
@@ -1242,7 +1232,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
content: { algorithm: "m.megolm.v1.aes-sha2" },
|
||||
}),
|
||||
testUtils.mkMembership({
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
sender: aliceClient.getUserId()!,
|
||||
}),
|
||||
],
|
||||
@@ -1661,7 +1651,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
type: "m.room.member",
|
||||
state_key: "@alice:localhost",
|
||||
content: {
|
||||
membership: "invite",
|
||||
membership: KnownMembership.Invite,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -1810,7 +1800,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
type: "m.room.member",
|
||||
state_key: "@alice:localhost",
|
||||
content: {
|
||||
membership: "invite",
|
||||
membership: KnownMembership.Invite,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -1886,7 +1876,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
{
|
||||
type: "m.room.member",
|
||||
state_key: aliceClient.getUserId(),
|
||||
content: { membership: "join" },
|
||||
content: { membership: KnownMembership.Join },
|
||||
event_id: "$alijoin",
|
||||
},
|
||||
],
|
||||
@@ -1913,7 +1903,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
{
|
||||
type: "m.room.member",
|
||||
state_key: "@other:user",
|
||||
content: { membership: "invite" },
|
||||
content: { membership: KnownMembership.Invite },
|
||||
event_id: "$otherinvite",
|
||||
},
|
||||
],
|
||||
@@ -1927,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" });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2061,7 +2051,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
fetchMock.getOnce(new RegExp(membersPath), {
|
||||
chunk: [
|
||||
testUtils.mkMembershipCustom({
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
sender: "@bob:xyz",
|
||||
}),
|
||||
],
|
||||
@@ -3090,7 +3080,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
const roomResponse = {
|
||||
state: {
|
||||
events: [
|
||||
mkMembershipCustom({ membership: "join", sender: persistentStoreClient.getSafeUserId() }),
|
||||
mkMembershipCustom({
|
||||
membership: KnownMembership.Join,
|
||||
sender: persistentStoreClient.getSafeUserId(),
|
||||
}),
|
||||
...stateEvents,
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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,8 +34,9 @@ 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";
|
||||
|
||||
let aliTestClient: TestClient;
|
||||
const roomId = "!room:localhost";
|
||||
@@ -216,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> {
|
||||
@@ -316,11 +317,11 @@ function firstSync(testClient: TestClient): Promise<void> {
|
||||
state: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: aliUserId,
|
||||
}),
|
||||
testUtils.mkMembership({
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: bobUserId,
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -20,6 +20,9 @@ import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import { createClient, CryptoEvent, IndexedDBCryptoStore } from "../../../src";
|
||||
import { populateStore } from "../../test-utils/test_indexeddb_cryptostore_dump";
|
||||
import { MSK_NOT_CACHED_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/no_cached_msk_dump";
|
||||
import { IDENTITY_NOT_TRUSTED_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/unverified";
|
||||
import { FULL_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/full_account";
|
||||
|
||||
jest.setTimeout(15000);
|
||||
|
||||
@@ -93,150 +96,197 @@ describe("MatrixClient.initRustCrypto", () => {
|
||||
await matrixClient.initRustCrypto();
|
||||
});
|
||||
|
||||
it("should migrate from libolm", async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
auth_data: {
|
||||
public_key: "q+HZiJdHl2Yopv9GGvv7EYSzDMrAiRknK4glSdoaomI",
|
||||
signatures: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI":
|
||||
"reDp6Mu+j+tfUL3/T6f5OBT3N825Lzpc43vvG+RvjX6V+KxXzodBQArgCoeEHLtL9OgSBmNrhTkSOX87MWCKAw",
|
||||
"ed25519:KMFSTJSMLB":
|
||||
"F8tyV5W6wNi0GXTdSg+gxSCULQi0EYxdAAqfkyNq58KzssZMw5i+PRA0aI2b+D7NH/aZaJrtiYNHJ0gWLSQvAw",
|
||||
},
|
||||
},
|
||||
},
|
||||
version: "7",
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
etag: "1",
|
||||
count: 79,
|
||||
describe("Libolm Migration", () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", {
|
||||
device_keys: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
KMFSTJSMLB: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "KMFSTJSMLB",
|
||||
keys: {
|
||||
"curve25519:KMFSTJSMLB": "LKv0bKbc0EC4h0jknbemv3QalEkeYvuNeUXVRgVVTTU",
|
||||
"ed25519:KMFSTJSMLB": "qK70DEqIXq7T+UU3v/al47Ab4JkMEBLpNrTBMbS5rrw",
|
||||
},
|
||||
user_id: "@vdhtest200713:matrix.org",
|
||||
signatures: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
"ed25519:KMFSTJSMLB":
|
||||
"aE+PdxLAdwQ/xfJwLmqebvt/lrT97fZas2SQFFrM+dPmHxQtjyS8csm88BLfGRjJKK1B/vWev3AaKqQZwLTUAw",
|
||||
"ed25519:lDvg6vi3P80L9XFNpUSU+5Y87m3p6yHcC83jhSU4Q5k":
|
||||
"lCd4SA/JT1nnxsgN9yQaLJQhH5hkLMVVx6ba5JAjL1wpWVqyPxzMJHImX6vTztk6S8rybcdfYkea5W/Ii+4HCQ",
|
||||
},
|
||||
},
|
||||
it("should migrate from libolm", async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", FULL_ACCOUNT_DATASET.backupResponse);
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", FULL_ACCOUNT_DATASET.keyQueryResponse);
|
||||
|
||||
const testStoreName = "test-store";
|
||||
await populateStore(testStoreName, FULL_ACCOUNT_DATASET.dumpPath);
|
||||
const cryptoStore = new IndexedDBCryptoStore(indexedDB, testStoreName);
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: FULL_ACCOUNT_DATASET.userId,
|
||||
deviceId: FULL_ACCOUNT_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: FULL_ACCOUNT_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
const progressListener = jest.fn();
|
||||
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const verificationStatus = await matrixClient
|
||||
.getCrypto()!
|
||||
.getDeviceVerificationStatus(FULL_ACCOUNT_DATASET.userId, FULL_ACCOUNT_DATASET.deviceId);
|
||||
|
||||
// Check that the current device and identity trust is migrated correctly just after migration
|
||||
expect(verificationStatus).toBeDefined();
|
||||
expect(verificationStatus!.crossSigningVerified).toEqual(true);
|
||||
expect(verificationStatus!.signedByOwner).toEqual(true);
|
||||
|
||||
// Do some basic checks on the imported data
|
||||
const deviceKeys = await matrixClient.getCrypto()!.getOwnDeviceKeys();
|
||||
expect(deviceKeys.curve25519).toEqual("LKv0bKbc0EC4h0jknbemv3QalEkeYvuNeUXVRgVVTTU");
|
||||
expect(deviceKeys.ed25519).toEqual("qK70DEqIXq7T+UU3v/al47Ab4JkMEBLpNrTBMbS5rrw");
|
||||
|
||||
expect(await matrixClient.getCrypto()!.getActiveSessionBackupVersion()).toEqual("7");
|
||||
|
||||
expect(await matrixClient.getCrypto()!.isEncryptionEnabledInRoom("!CWLUCoEWXSFyTCOtfL:matrix.org")).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
// check the progress callback
|
||||
expect(progressListener.mock.calls.length).toBeGreaterThan(50);
|
||||
|
||||
// The first call should have progress == 0
|
||||
const [firstProgress, totalSteps] = progressListener.mock.calls[0];
|
||||
expect(totalSteps).toBeGreaterThan(3000);
|
||||
expect(firstProgress).toEqual(0);
|
||||
|
||||
for (let i = 1; i < progressListener.mock.calls.length - 1; i++) {
|
||||
const [progress, total] = progressListener.mock.calls[i];
|
||||
expect(total).toEqual(totalSteps);
|
||||
expect(progress).toBeGreaterThan(progressListener.mock.calls[i - 1][0]);
|
||||
expect(progress).toBeLessThanOrEqual(totalSteps);
|
||||
}
|
||||
|
||||
// The final call should have progress == total == -1
|
||||
expect(progressListener).toHaveBeenLastCalledWith(-1, -1);
|
||||
}, 60000);
|
||||
|
||||
describe("Legacy trust migration", () => {
|
||||
async function populateAndStartLegacyCryptoStore(dumpPath: string): Promise<IndexedDBCryptoStore> {
|
||||
const testStoreName = "test-store";
|
||||
await populateStore(testStoreName, dumpPath);
|
||||
const cryptoStore = new IndexedDBCryptoStore(indexedDB, testStoreName);
|
||||
await cryptoStore.startup();
|
||||
return cryptoStore;
|
||||
}
|
||||
|
||||
it("should not revert to untrusted if legacy was trusted but msk not in cache, big account", async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "No backup found",
|
||||
},
|
||||
},
|
||||
},
|
||||
master_keys: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
user_id: "@vdhtest200713:matrix.org",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI":
|
||||
"gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI",
|
||||
});
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", FULL_ACCOUNT_DATASET.keyQueryResponse);
|
||||
|
||||
const cryptoStore = await populateAndStartLegacyCryptoStore(FULL_ACCOUNT_DATASET.dumpPath);
|
||||
|
||||
// Remove the master key from the cache
|
||||
await cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
|
||||
const objectStore = txn.objectStore("account");
|
||||
objectStore.delete(`ssss_cache:master`);
|
||||
});
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: FULL_ACCOUNT_DATASET.userId,
|
||||
deviceId: FULL_ACCOUNT_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: FULL_ACCOUNT_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const verificationStatus = await matrixClient
|
||||
.getCrypto()!
|
||||
.getUserVerificationStatus(FULL_ACCOUNT_DATASET.userId);
|
||||
|
||||
expect(verificationStatus.isCrossSigningVerified()).toBe(true);
|
||||
}, 60000);
|
||||
|
||||
it("should not revert to untrusted if legacy was trusted but msk not in cache", async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", MSK_NOT_CACHED_DATASET.backupResponse);
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
|
||||
|
||||
const cryptoStore = await populateAndStartLegacyCryptoStore(MSK_NOT_CACHED_DATASET.dumpPath);
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: MSK_NOT_CACHED_DATASET.userId,
|
||||
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const verificationStatus = await matrixClient
|
||||
.getCrypto()!
|
||||
.getUserVerificationStatus("@migration:localhost");
|
||||
|
||||
expect(verificationStatus.isCrossSigningVerified()).toBe(true);
|
||||
});
|
||||
|
||||
it("should not migrate local trust if key has changed", async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", MSK_NOT_CACHED_DATASET.backupResponse);
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.rotatedKeyQueryResponse);
|
||||
|
||||
const cryptoStore = await populateAndStartLegacyCryptoStore(MSK_NOT_CACHED_DATASET.dumpPath);
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: MSK_NOT_CACHED_DATASET.userId,
|
||||
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const verificationStatus = await matrixClient
|
||||
.getCrypto()!
|
||||
.getUserVerificationStatus("@migration:localhost");
|
||||
|
||||
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
||||
});
|
||||
|
||||
it("should not migrate local trust if was not trusted in legacy", async () => {
|
||||
// Just 404 here for the test
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "No backup found",
|
||||
},
|
||||
signatures: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
"ed25519:MWOGVUTXZN":
|
||||
"stOu1aHbhsWB/Aj5M/HqBR83QzME+682C995Uc8JxSmmyrlWmgG8QrnoUDG2OFR1t6zNQ+QLEilU4WNEOV73DQ",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
self_signing_keys: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
user_id: "@vdhtest200713:matrix.org",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
"ed25519:lDvg6vi3P80L9XFNpUSU+5Y87m3p6yHcC83jhSU4Q5k":
|
||||
"lDvg6vi3P80L9XFNpUSU+5Y87m3p6yHcC83jhSU4Q5k",
|
||||
},
|
||||
signatures: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI":
|
||||
"HKTC7NoBhAkfJtmemmkn/HvCCgBQViWZ0uH7aGPRaWMDFgD8T7Q+y1j3FKZv4mhSopR85Fq3FRyXsG8OVvGeBA",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user_signing_keys: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
user_id: "@vdhtest200713:matrix.org",
|
||||
usage: ["user_signing"],
|
||||
keys: {
|
||||
"ed25519:YShqO/3u5vQ0uucojraWrtoLrek0CYrurN/vH/YPMg8":
|
||||
"YShqO/3u5vQ0uucojraWrtoLrek0CYrurN/vH/YPMg8",
|
||||
},
|
||||
signatures: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI":
|
||||
"u8VOi4IaeRJwDgy2ftK02NJQPdBijy8f/0+WnHGG72yfOvMthwWzEw8SrRSNG8glBNrfHinKwCyJJzAJwyepCQ",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", IDENTITY_NOT_TRUSTED_DATASET.keyQueryResponse);
|
||||
|
||||
const cryptoStore = await populateAndStartLegacyCryptoStore(IDENTITY_NOT_TRUSTED_DATASET.dumpPath);
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: IDENTITY_NOT_TRUSTED_DATASET.userId,
|
||||
deviceId: IDENTITY_NOT_TRUSTED_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: IDENTITY_NOT_TRUSTED_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const verificationStatus = await matrixClient
|
||||
.getCrypto()!
|
||||
.getUserVerificationStatus("@untrusted:localhost");
|
||||
|
||||
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
const testStoreName = "test-store";
|
||||
await populateStore(testStoreName);
|
||||
const cryptoStore = new IndexedDBCryptoStore(indexedDB, testStoreName);
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@vdhtest200713:matrix.org",
|
||||
deviceId: "KMFSTJSMLB",
|
||||
cryptoStore,
|
||||
pickleKey: "+1k2Ppd7HIisUY824v7JtV3/oEE4yX0TqtmNPyhaD7o",
|
||||
});
|
||||
|
||||
const progressListener = jest.fn();
|
||||
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const verificationStatus = await matrixClient
|
||||
.getCrypto()!
|
||||
.getDeviceVerificationStatus("@vdhtest200713:matrix.org", "KMFSTJSMLB");
|
||||
|
||||
// Check that the current device and identity trust is migrated correctly just after migration
|
||||
expect(verificationStatus).toBeDefined();
|
||||
expect(verificationStatus!.crossSigningVerified).toEqual(true);
|
||||
expect(verificationStatus!.signedByOwner).toEqual(true);
|
||||
|
||||
// Do some basic checks on the imported data
|
||||
const deviceKeys = await matrixClient.getCrypto()!.getOwnDeviceKeys();
|
||||
expect(deviceKeys.curve25519).toEqual("LKv0bKbc0EC4h0jknbemv3QalEkeYvuNeUXVRgVVTTU");
|
||||
expect(deviceKeys.ed25519).toEqual("qK70DEqIXq7T+UU3v/al47Ab4JkMEBLpNrTBMbS5rrw");
|
||||
|
||||
expect(await matrixClient.getCrypto()!.getActiveSessionBackupVersion()).toEqual("7");
|
||||
|
||||
expect(await matrixClient.getCrypto()!.isEncryptionEnabledInRoom("!CWLUCoEWXSFyTCOtfL:matrix.org")).toBe(true);
|
||||
|
||||
// check the progress callback
|
||||
expect(progressListener.mock.calls.length).toBeGreaterThan(50);
|
||||
|
||||
// The first call should have progress == 0
|
||||
const [firstProgress, totalSteps] = progressListener.mock.calls[0];
|
||||
expect(totalSteps).toBeGreaterThan(3000);
|
||||
expect(firstProgress).toEqual(0);
|
||||
|
||||
for (let i = 1; i < progressListener.mock.calls.length - 1; i++) {
|
||||
const [progress, total] = progressListener.mock.calls[i];
|
||||
expect(total).toEqual(totalSteps);
|
||||
expect(progress).toBeGreaterThan(progressListener.mock.calls[i - 1][0]);
|
||||
expect(progress).toBeLessThanOrEqual(totalSteps);
|
||||
}
|
||||
|
||||
// The final call should have progress == total == -1
|
||||
expect(progressListener).toHaveBeenLastCalledWith(-1, -1);
|
||||
}, 60000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MatrixClient.clearStores", () => {
|
||||
|
||||
@@ -19,6 +19,7 @@ limitations under the License.
|
||||
import { TestClient } from "../TestClient";
|
||||
import * as testUtils from "../test-utils/test-utils";
|
||||
import { logger } from "../../src/logger";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
@@ -43,7 +44,7 @@ function getSyncResponse(roomMembers: string[]) {
|
||||
stateEvents,
|
||||
roomMembers.map((m) =>
|
||||
testUtils.mkMembership({
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
sender: m,
|
||||
}),
|
||||
),
|
||||
@@ -323,7 +324,7 @@ describe("DeviceList management:", function () {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: "leave",
|
||||
mship: KnownMembership.Leave,
|
||||
sender: "@bob:xyz",
|
||||
}),
|
||||
],
|
||||
@@ -357,7 +358,7 @@ describe("DeviceList management:", function () {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: "leave",
|
||||
mship: KnownMembership.Leave,
|
||||
sender: "@bob:xyz",
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from "../../src";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
describe("MatrixClient events", function () {
|
||||
const selfUserId = "@alice:localhost";
|
||||
@@ -85,7 +86,7 @@ describe("MatrixClient events", function () {
|
||||
events: [
|
||||
utils.mkMembership({
|
||||
room: "!erufh:bar",
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: "@foo:bar",
|
||||
}),
|
||||
utils.mkEvent({
|
||||
@@ -272,7 +273,7 @@ describe("MatrixClient events", function () {
|
||||
membersInvokeCount++;
|
||||
expect(member.roomId).toEqual("!erufh:bar");
|
||||
expect(member.userId).toEqual("@foo:bar");
|
||||
expect(member.membership).toEqual("join");
|
||||
expect(member.membership).toEqual(KnownMembership.Join);
|
||||
});
|
||||
client!.on(RoomStateEvent.NewMember, function (event, state, member) {
|
||||
newMemberInvokeCount++;
|
||||
@@ -310,7 +311,7 @@ describe("MatrixClient events", function () {
|
||||
});
|
||||
client!.on(RoomMemberEvent.Membership, function (event, member) {
|
||||
membershipInvokeCount++;
|
||||
expect(member.membership).toEqual("join");
|
||||
expect(member.membership).toEqual(KnownMembership.Join);
|
||||
});
|
||||
|
||||
client!.startClient();
|
||||
|
||||
@@ -36,6 +36,7 @@ import { TestClient } from "../TestClient";
|
||||
import { FeatureSupport, Thread, ThreadEvent } from "../../src/models/thread";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
import { Feature, ServerSupport } from "../../src/feature";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
const userId = "@alice:localhost";
|
||||
const userName = "Alice";
|
||||
@@ -63,7 +64,7 @@ const buildRelationPaginationQuery = (params: QueryDict): string => {
|
||||
|
||||
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: userId,
|
||||
name: userName,
|
||||
event: false,
|
||||
@@ -98,7 +99,7 @@ const INITIAL_SYNC_DATA = {
|
||||
events: [
|
||||
withoutRoomId(ROOM_NAME_EVENT),
|
||||
utils.mkMembership({
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: otherUserId,
|
||||
name: "Bob",
|
||||
event: false,
|
||||
@@ -607,11 +608,6 @@ describe("MatrixClient event timelines", function () {
|
||||
await client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId)!;
|
||||
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
@@ -1545,9 +1541,7 @@ describe("MatrixClient event timelines", function () {
|
||||
expect(timelineSets).not.toBeNull();
|
||||
respondToThreads(threadsResponse);
|
||||
respondToThreads(threadsResponse);
|
||||
respondToEvent(THREAD_ROOT);
|
||||
respondToEvent(THREAD2_ROOT);
|
||||
respondToThread(THREAD_ROOT, [THREAD_REPLY]);
|
||||
respondToThread(THREAD2_ROOT, [THREAD2_REPLY]);
|
||||
await flushHttp(room.fetchRoomThreads());
|
||||
const threadIds = room.getThreads().map((thread) => thread.id);
|
||||
@@ -1566,7 +1560,6 @@ describe("MatrixClient event timelines", function () {
|
||||
thread.initialEventsFetched = true;
|
||||
const prom = emitPromise(room, ThreadEvent.NewReply);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD2_ROOT);
|
||||
await room.addLiveEvents([THREAD_REPLY2]);
|
||||
await httpBackend.flushAllExpected();
|
||||
await prom;
|
||||
@@ -1643,7 +1636,7 @@ describe("MatrixClient event timelines", function () {
|
||||
...THREAD_ROOT.unsigned!["m.relations"],
|
||||
"io.element.thread": {
|
||||
...THREAD_ROOT.unsigned!["m.relations"]!["io.element.thread"],
|
||||
count: 2,
|
||||
count: 1,
|
||||
latest_event: THREAD_REPLY,
|
||||
},
|
||||
},
|
||||
@@ -1692,7 +1685,6 @@ describe("MatrixClient event timelines", function () {
|
||||
thread.initialEventsFetched = true;
|
||||
const prom = emitPromise(room, ThreadEvent.Update);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD2_ROOT);
|
||||
await room.addLiveEvents([THREAD_REPLY_REACTION]);
|
||||
await httpBackend.flushAllExpected();
|
||||
await prom;
|
||||
@@ -1930,7 +1922,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
// a state event, followed by a redaction thereof
|
||||
const event = utils.mkMembership({
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: otherUserId,
|
||||
});
|
||||
const redaction = utils.mkEvent({
|
||||
@@ -2007,11 +1999,6 @@ describe("MatrixClient event timelines", function () {
|
||||
},
|
||||
},
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function () {
|
||||
|
||||
@@ -27,6 +27,7 @@ import { ISearchResults } from "../../src/@types/search";
|
||||
import { IStore } from "../../src/store";
|
||||
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
|
||||
import { SetPresence } from "../../src/sync";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
describe("MatrixClient", function () {
|
||||
const userId = "@alice:localhost";
|
||||
@@ -162,7 +163,7 @@ describe("MatrixClient", function () {
|
||||
utils.mkMembership({
|
||||
user: userId,
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
event: true,
|
||||
}),
|
||||
]);
|
||||
@@ -182,7 +183,7 @@ describe("MatrixClient", function () {
|
||||
utils.mkMembership({
|
||||
user: userId,
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
event: true,
|
||||
}),
|
||||
]);
|
||||
@@ -269,7 +270,7 @@ describe("MatrixClient", function () {
|
||||
utils.mkMembership({
|
||||
user: userId,
|
||||
room: roomId,
|
||||
mship: "knock",
|
||||
mship: KnownMembership.Knock,
|
||||
event: true,
|
||||
}),
|
||||
]);
|
||||
@@ -1912,7 +1913,7 @@ const buildEventJoinRules = () =>
|
||||
new MatrixEvent({
|
||||
age: 80123696,
|
||||
content: {
|
||||
join_rule: "invite",
|
||||
join_rule: KnownMembership.Invite,
|
||||
},
|
||||
event_id: "$6JDDeDp7fEc0F6YnTWMruNcKWFltR3e9wk7wWDDJrAU",
|
||||
origin_server_ts: 1643815441191,
|
||||
@@ -1966,7 +1967,7 @@ const buildEventMember = () =>
|
||||
content: {
|
||||
avatar_url: "mxc://matrix.org/aNtbVcFfwotudypZcHsIcPOc",
|
||||
displayname: "andybalaam-test1",
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
},
|
||||
event_id: "$Ex5eVmMs_ti784mo8bgddynbwLvy6231lCycJr7Cl9M",
|
||||
origin_server_ts: 1643815439608,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { MatrixScheduler } from "../../src/scheduler";
|
||||
import { MemoryStore } from "../../src/store/memory";
|
||||
import { MatrixError } from "../../src/http-api";
|
||||
import { IStore } from "../../src/store";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
describe("MatrixClient opts", function () {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
@@ -43,13 +44,13 @@ describe("MatrixClient opts", function () {
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: userB,
|
||||
name: "Bob",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: userId,
|
||||
name: "Alice",
|
||||
}),
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
Room,
|
||||
} from "../../src";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
describe("MatrixClient room timelines", function () {
|
||||
const userId = "@alice:localhost";
|
||||
@@ -42,7 +43,7 @@ describe("MatrixClient room timelines", function () {
|
||||
|
||||
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: userId,
|
||||
name: userName,
|
||||
});
|
||||
@@ -76,7 +77,7 @@ describe("MatrixClient room timelines", function () {
|
||||
ROOM_NAME_EVENT,
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: otherUserId,
|
||||
name: "Bob",
|
||||
}),
|
||||
@@ -316,7 +317,7 @@ describe("MatrixClient room timelines", function () {
|
||||
|
||||
// make an m.room.member event for alice's join
|
||||
const joinMshipEvent = utils.mkMembership({
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: userId,
|
||||
room: roomId,
|
||||
name: "Old Alice",
|
||||
@@ -326,7 +327,7 @@ describe("MatrixClient room timelines", function () {
|
||||
// make an m.room.member event with prev_content for alice's nick
|
||||
// change
|
||||
const oldMshipEvent = utils.mkMembership({
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: userId,
|
||||
room: roomId,
|
||||
name: userName,
|
||||
@@ -335,7 +336,7 @@ describe("MatrixClient room timelines", function () {
|
||||
oldMshipEvent.prev_content = {
|
||||
displayname: "Old Alice",
|
||||
avatar_url: undefined,
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
};
|
||||
|
||||
// set the list of events to return on scrollback (/messages)
|
||||
@@ -487,7 +488,7 @@ describe("MatrixClient room timelines", function () {
|
||||
utils.mkMembership({
|
||||
user: userId,
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
name: "New Name",
|
||||
}),
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
@@ -554,13 +555,13 @@ describe("MatrixClient room timelines", function () {
|
||||
utils.mkMembership({
|
||||
user: userC,
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
name: "C",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userC,
|
||||
room: roomId,
|
||||
mship: "invite",
|
||||
mship: KnownMembership.Invite,
|
||||
skey: userD,
|
||||
}),
|
||||
];
|
||||
@@ -571,9 +572,9 @@ describe("MatrixClient room timelines", function () {
|
||||
return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]).then(function () {
|
||||
expect(room.currentState.getMembers().length).toEqual(4);
|
||||
expect(room.currentState.getMember(userC)!.name).toEqual("C");
|
||||
expect(room.currentState.getMember(userC)!.membership).toEqual("join");
|
||||
expect(room.currentState.getMember(userC)!.membership).toEqual(KnownMembership.Join);
|
||||
expect(room.currentState.getMember(userD)!.name).toEqual(userD);
|
||||
expect(room.currentState.getMember(userD)!.membership).toEqual("invite");
|
||||
expect(room.currentState.getMember(userD)!.membership).toEqual(KnownMembership.Invite);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -598,9 +599,9 @@ describe("MatrixClient room timelines", function () {
|
||||
expect(room.timeline[0].event).toEqual(eventData[0]);
|
||||
expect(room.currentState.getMembers().length).toEqual(2);
|
||||
expect(room.currentState.getMember(userId)!.name).toEqual(userName);
|
||||
expect(room.currentState.getMember(userId)!.membership).toEqual("join");
|
||||
expect(room.currentState.getMember(userId)!.membership).toEqual(KnownMembership.Join);
|
||||
expect(room.currentState.getMember(otherUserId)!.name).toEqual("Bob");
|
||||
expect(room.currentState.getMember(otherUserId)!.membership).toEqual("join");
|
||||
expect(room.currentState.getMember(otherUserId)!.membership).toEqual(KnownMembership.Join);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,8 @@ 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", () => {
|
||||
const selfUserId = "@alice:localhost";
|
||||
@@ -123,7 +126,7 @@ describe("MatrixClient syncing", () => {
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "invite",
|
||||
membership: KnownMembership.Invite,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -151,10 +154,10 @@ describe("MatrixClient syncing", () => {
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "leave",
|
||||
membership: KnownMembership.Leave,
|
||||
},
|
||||
prev_content: {
|
||||
membership: "invite",
|
||||
membership: KnownMembership.Invite,
|
||||
},
|
||||
// XXX: And other fields required on an event
|
||||
},
|
||||
@@ -167,10 +170,10 @@ describe("MatrixClient syncing", () => {
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "leave",
|
||||
membership: KnownMembership.Leave,
|
||||
},
|
||||
prev_content: {
|
||||
membership: "invite",
|
||||
membership: KnownMembership.Invite,
|
||||
},
|
||||
// XXX: And other fields required on an event
|
||||
},
|
||||
@@ -193,22 +196,22 @@ describe("MatrixClient syncing", () => {
|
||||
// Room, string, string
|
||||
fires++;
|
||||
expect(room.roomId).toBe(roomId);
|
||||
expect(membership).toBe("invite");
|
||||
expect(membership).toBe(KnownMembership.Invite);
|
||||
expect(oldMembership).toBeFalsy();
|
||||
|
||||
// Second fire: a leave
|
||||
client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => {
|
||||
fires++;
|
||||
expect(room.roomId).toBe(roomId);
|
||||
expect(membership).toBe("leave");
|
||||
expect(oldMembership).toBe("invite");
|
||||
expect(membership).toBe(KnownMembership.Leave);
|
||||
expect(oldMembership).toBe(KnownMembership.Invite);
|
||||
|
||||
// Third/final fire: a second invite
|
||||
client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => {
|
||||
fires++;
|
||||
expect(room.roomId).toBe(roomId);
|
||||
expect(membership).toBe("invite");
|
||||
expect(oldMembership).toBe("leave");
|
||||
expect(membership).toBe(KnownMembership.Invite);
|
||||
expect(oldMembership).toBe(KnownMembership.Leave);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -238,7 +241,7 @@ describe("MatrixClient syncing", () => {
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "knock",
|
||||
membership: KnownMembership.Knock,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -266,10 +269,10 @@ describe("MatrixClient syncing", () => {
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "leave",
|
||||
membership: KnownMembership.Leave,
|
||||
},
|
||||
prev_content: {
|
||||
membership: "knock",
|
||||
membership: KnownMembership.Knock,
|
||||
},
|
||||
// XXX: And other fields required on an event
|
||||
},
|
||||
@@ -282,10 +285,10 @@ describe("MatrixClient syncing", () => {
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "leave",
|
||||
membership: KnownMembership.Leave,
|
||||
},
|
||||
prev_content: {
|
||||
membership: "knock",
|
||||
membership: KnownMembership.Knock,
|
||||
},
|
||||
// XXX: And other fields required on an event
|
||||
},
|
||||
@@ -308,22 +311,22 @@ describe("MatrixClient syncing", () => {
|
||||
// Room, string, string
|
||||
fires++;
|
||||
expect(room.roomId).toBe(roomId);
|
||||
expect(membership).toBe("knock");
|
||||
expect(membership).toBe(KnownMembership.Knock);
|
||||
expect(oldMembership).toBeFalsy();
|
||||
|
||||
// Second fire: a leave
|
||||
client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => {
|
||||
fires++;
|
||||
expect(room.roomId).toBe(roomId);
|
||||
expect(membership).toBe("leave");
|
||||
expect(oldMembership).toBe("knock");
|
||||
expect(membership).toBe(KnownMembership.Leave);
|
||||
expect(oldMembership).toBe(KnownMembership.Knock);
|
||||
|
||||
// Third/final fire: a second knock
|
||||
client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => {
|
||||
fires++;
|
||||
expect(room.roomId).toBe(roomId);
|
||||
expect(membership).toBe("knock");
|
||||
expect(oldMembership).toBe("leave");
|
||||
expect(membership).toBe(KnownMembership.Knock);
|
||||
expect(oldMembership).toBe(KnownMembership.Leave);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -381,7 +384,7 @@ describe("MatrixClient syncing", () => {
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "invite",
|
||||
membership: KnownMembership.Invite,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -421,7 +424,7 @@ describe("MatrixClient syncing", () => {
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "knock",
|
||||
membership: KnownMembership.Knock,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -533,12 +536,12 @@ describe("MatrixClient syncing", () => {
|
||||
events: [
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: otherUserId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
@@ -556,7 +559,7 @@ describe("MatrixClient syncing", () => {
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: "invite",
|
||||
mship: KnownMembership.Invite,
|
||||
user: userC,
|
||||
}) as IStateEvent,
|
||||
);
|
||||
@@ -589,7 +592,7 @@ describe("MatrixClient syncing", () => {
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: "invite",
|
||||
mship: KnownMembership.Invite,
|
||||
user: userC,
|
||||
}) as IStateEvent,
|
||||
);
|
||||
@@ -617,7 +620,7 @@ describe("MatrixClient syncing", () => {
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: "invite",
|
||||
mship: KnownMembership.Invite,
|
||||
user: userC,
|
||||
}) as IStateEvent,
|
||||
);
|
||||
@@ -644,7 +647,7 @@ describe("MatrixClient syncing", () => {
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: "invite",
|
||||
mship: KnownMembership.Invite,
|
||||
user: userC,
|
||||
}) as IStateEvent,
|
||||
);
|
||||
@@ -719,12 +722,12 @@ describe("MatrixClient syncing", () => {
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: otherUserId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
@@ -750,13 +753,13 @@ describe("MatrixClient syncing", () => {
|
||||
events: [
|
||||
utils.mkMembership({
|
||||
room: roomTwo,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: otherUserId,
|
||||
name: otherDisplayName,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomTwo,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
@@ -1247,7 +1250,7 @@ describe("MatrixClient syncing", () => {
|
||||
|
||||
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: userA,
|
||||
});
|
||||
|
||||
@@ -1508,12 +1511,12 @@ describe("MatrixClient syncing", () => {
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: otherUserId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
@@ -1605,12 +1608,12 @@ describe("MatrixClient syncing", () => {
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: otherUserId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
@@ -1645,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";
|
||||
|
||||
@@ -1732,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: "join",
|
||||
user: otherUserId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1901,7 +2284,7 @@ describe("MatrixClient syncing", () => {
|
||||
it("should return a room based on the room initialSync API", async () => {
|
||||
httpBackend!.when("GET", `/rooms/${encodeURIComponent(roomOne)}/initialSync`).respond(200, {
|
||||
room_id: roomOne,
|
||||
membership: "leave",
|
||||
membership: KnownMembership.Leave,
|
||||
messages: {
|
||||
start: "start",
|
||||
end: "end",
|
||||
@@ -1950,7 +2333,7 @@ describe("MatrixClient syncing", () => {
|
||||
const room = await prom;
|
||||
|
||||
expect(room.roomId).toBe(roomOne);
|
||||
expect(room.getMyMembership()).toBe("leave");
|
||||
expect(room.getMyMembership()).toBe(KnownMembership.Leave);
|
||||
expect(room.name).toBe("Room Name");
|
||||
expect(room.currentState.getStateEvents("m.room.name", "")?.getId()).toBe("$eventId");
|
||||
expect(room.timeline[0].getContent().body).toBe("Message 1");
|
||||
@@ -2042,7 +2425,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "invite",
|
||||
membership: KnownMembership.Invite,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -34,6 +34,7 @@ import { TestClient } from "../TestClient";
|
||||
import { ReceiptType } from "../../src/@types/read_receipts";
|
||||
import { mkThread } from "../test-utils/thread";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
const userA = "@alice:localhost";
|
||||
const userB = "@bob:localhost";
|
||||
@@ -151,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: "",
|
||||
},
|
||||
});
|
||||
@@ -229,7 +230,7 @@ describe("MatrixClient syncing", () => {
|
||||
content: {
|
||||
avatar_url: "",
|
||||
displayname: userB,
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
},
|
||||
origin_server_ts: 2,
|
||||
sender: userB,
|
||||
@@ -270,7 +271,7 @@ describe("MatrixClient syncing", () => {
|
||||
},
|
||||
{
|
||||
content: {
|
||||
join_rule: "invite",
|
||||
join_rule: KnownMembership.Invite,
|
||||
},
|
||||
origin_server_ts: 4,
|
||||
sender: userB,
|
||||
@@ -316,7 +317,7 @@ describe("MatrixClient syncing", () => {
|
||||
avatar_url: "",
|
||||
displayname: userA,
|
||||
is_direct: true,
|
||||
membership: "invite",
|
||||
membership: KnownMembership.Invite,
|
||||
},
|
||||
origin_server_ts: 8,
|
||||
sender: userB,
|
||||
@@ -338,7 +339,7 @@ describe("MatrixClient syncing", () => {
|
||||
content: {
|
||||
avatar_url: "",
|
||||
displayname: userA,
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
},
|
||||
origin_server_ts: 10,
|
||||
sender: userA,
|
||||
|
||||
@@ -43,6 +43,7 @@ import { IStoredClientOpts } from "../../src";
|
||||
import { logger } from "../../src/logger";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
import { defer } from "../../src/utils";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
describe("SlidingSyncSdk", () => {
|
||||
let client: MatrixClient | undefined;
|
||||
@@ -189,7 +190,7 @@ describe("SlidingSyncSdk", () => {
|
||||
name: "A",
|
||||
required_state: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnStateEvent(EventType.RoomName, { name: "A" }, ""),
|
||||
],
|
||||
@@ -204,7 +205,7 @@ describe("SlidingSyncSdk", () => {
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello B" }),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "world B" }),
|
||||
@@ -216,7 +217,7 @@ describe("SlidingSyncSdk", () => {
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello C" }),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "world C" }),
|
||||
@@ -229,7 +230,7 @@ describe("SlidingSyncSdk", () => {
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello D" }),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "world D" }),
|
||||
@@ -244,7 +245,7 @@ describe("SlidingSyncSdk", () => {
|
||||
invite_state: [
|
||||
{
|
||||
type: EventType.RoomMember,
|
||||
content: { membership: "invite" },
|
||||
content: { membership: KnownMembership.Invite },
|
||||
state_key: selfUserId,
|
||||
sender: "@bob:localhost",
|
||||
event_id: "$room_e_invite",
|
||||
@@ -265,7 +266,7 @@ describe("SlidingSyncSdk", () => {
|
||||
name: "#foo:localhost",
|
||||
required_state: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnStateEvent(EventType.RoomCanonicalAlias, { alias: "#foo:localhost" }, ""),
|
||||
mkOwnStateEvent(EventType.RoomName, { name: "This should be ignored" }, ""),
|
||||
@@ -281,7 +282,7 @@ describe("SlidingSyncSdk", () => {
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
],
|
||||
joined_count: 5,
|
||||
@@ -293,7 +294,7 @@ describe("SlidingSyncSdk", () => {
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "live event" }),
|
||||
],
|
||||
@@ -308,7 +309,7 @@ describe("SlidingSyncSdk", () => {
|
||||
const gotRoom = client!.getRoom(roomA);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.name).toEqual(data[roomA].name);
|
||||
expect(gotRoom!.getMyMembership()).toEqual("join");
|
||||
expect(gotRoom!.getMyMembership()).toEqual(KnownMembership.Join);
|
||||
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline);
|
||||
});
|
||||
|
||||
@@ -318,7 +319,7 @@ describe("SlidingSyncSdk", () => {
|
||||
const gotRoom = client!.getRoom(roomB);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.name).toEqual(data[roomB].name);
|
||||
expect(gotRoom!.getMyMembership()).toEqual("join");
|
||||
expect(gotRoom!.getMyMembership()).toEqual(KnownMembership.Join);
|
||||
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline);
|
||||
});
|
||||
|
||||
@@ -372,7 +373,7 @@ describe("SlidingSyncSdk", () => {
|
||||
const gotRoom = client!.getRoom(roomH);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.name).toEqual(data[roomH].name);
|
||||
expect(gotRoom!.getMyMembership()).toEqual("join");
|
||||
expect(gotRoom!.getMyMembership()).toEqual(KnownMembership.Join);
|
||||
// check the entire timeline is correct
|
||||
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents(), data[roomH].timeline);
|
||||
await expect(seenLiveEventDeferred.promise).resolves.toBeTruthy();
|
||||
@@ -383,7 +384,7 @@ describe("SlidingSyncSdk", () => {
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
const gotRoom = client!.getRoom(roomE);
|
||||
expect(gotRoom).toBeTruthy();
|
||||
expect(gotRoom!.getMyMembership()).toEqual("invite");
|
||||
expect(gotRoom!.getMyMembership()).toEqual(KnownMembership.Invite);
|
||||
expect(gotRoom!.currentState.getJoinRule()).toEqual(JoinRule.Invite);
|
||||
});
|
||||
|
||||
@@ -603,9 +604,9 @@ describe("SlidingSyncSdk", () => {
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "invite" }, invitee),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Invite }, invitee),
|
||||
],
|
||||
});
|
||||
await httpBackend!.flush("/profile", 1, 1000);
|
||||
@@ -719,7 +720,7 @@ describe("SlidingSyncSdk", () => {
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
|
||||
],
|
||||
@@ -923,7 +924,7 @@ describe("SlidingSyncSdk", () => {
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
|
||||
],
|
||||
@@ -964,7 +965,7 @@ describe("SlidingSyncSdk", () => {
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
|
||||
],
|
||||
@@ -1050,12 +1051,12 @@ describe("SlidingSyncSdk", () => {
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
{
|
||||
type: EventType.RoomMember,
|
||||
state_key: alice,
|
||||
content: { membership: "join" },
|
||||
content: { membership: KnownMembership.Join },
|
||||
sender: alice,
|
||||
origin_server_ts: Date.now(),
|
||||
event_id: "$alice",
|
||||
|
||||
@@ -107,8 +107,8 @@ describe("SlidingSync", () => {
|
||||
onRequest: (initial) => {
|
||||
return { initial: initial };
|
||||
},
|
||||
onResponse: (res) => {
|
||||
return {};
|
||||
onResponse: async (res) => {
|
||||
return;
|
||||
},
|
||||
when: () => ExtensionState.PreProcess,
|
||||
};
|
||||
@@ -1572,7 +1572,7 @@ describe("SlidingSync", () => {
|
||||
onPreExtensionRequest = () => {
|
||||
return extReq;
|
||||
};
|
||||
onPreExtensionResponse = (resp) => {
|
||||
onPreExtensionResponse = async (resp) => {
|
||||
extensionOnResponseCalled = true;
|
||||
callbackOrder.push("onPreExtensionResponse");
|
||||
expect(resp).toEqual(extResp);
|
||||
@@ -1613,7 +1613,7 @@ describe("SlidingSync", () => {
|
||||
return undefined;
|
||||
};
|
||||
let responseCalled = false;
|
||||
onPreExtensionResponse = (resp) => {
|
||||
onPreExtensionResponse = async (resp) => {
|
||||
responseCalled = true;
|
||||
};
|
||||
httpBackend!
|
||||
@@ -1649,7 +1649,7 @@ describe("SlidingSync", () => {
|
||||
};
|
||||
let responseCalled = false;
|
||||
const callbackOrder: string[] = [];
|
||||
onPostExtensionResponse = (resp) => {
|
||||
onPostExtensionResponse = async (resp) => {
|
||||
expect(resp).toEqual(extResp);
|
||||
responseCalled = true;
|
||||
callbackOrder.push("onPostExtensionResponse");
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { eventMapperFor } from "../../src/event-mapper";
|
||||
import { TEST_ROOM_ID } from "./test-data";
|
||||
import { KnownMembership, Membership } from "../../src/@types/membership";
|
||||
|
||||
/**
|
||||
* Return a promise that is resolved when the client next emits a
|
||||
@@ -87,7 +88,7 @@ export function getSyncResponse(roomMembers: string[], roomId = TEST_ROOM_ID): I
|
||||
for (let i = 0; i < roomMembers.length; i++) {
|
||||
roomResponse.state.events.push(
|
||||
mkMembershipCustom({
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
sender: roomMembers[i],
|
||||
}),
|
||||
);
|
||||
@@ -251,7 +252,7 @@ export function mkPresence(opts: IPresenceOpts & { event?: boolean }): Partial<I
|
||||
|
||||
interface IMembershipOpts {
|
||||
room?: string;
|
||||
mship: string;
|
||||
mship: Membership;
|
||||
sender?: string;
|
||||
user?: string;
|
||||
skey?: string;
|
||||
@@ -297,7 +298,7 @@ export function mkMembership(opts: IMembershipOpts & { event?: boolean }): Parti
|
||||
}
|
||||
|
||||
export function mkMembershipCustom<T>(
|
||||
base: T & { membership: string; sender: string; content?: IContent },
|
||||
base: T & { membership: Membership; sender: string; content?: IContent },
|
||||
): T & { type: EventType; sender: string; state_key: string; content: IContent } & GeneratedMetadata {
|
||||
const content = base.content || {};
|
||||
return mkEventCustom({
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
## Dump of libolm indexeddb cryptostore
|
||||
## Dumps of libolm indexeddb cryptostore
|
||||
|
||||
This directory contains a dump of a real indexeddb store from a session using
|
||||
This directory contains several dumps of real indexeddb stores from a session using
|
||||
libolm crypto.
|
||||
|
||||
The corresponding pickle key is `+1k2Ppd7HIisUY824v7JtV3/oEE4yX0TqtmNPyhaD7o`.
|
||||
Each directory contains, in dump.json, a dump of data created by pasting the following
|
||||
code into the browser console; and in index.ts, details of the user, pickle key,
|
||||
and corresponding key query and backup responses (`DumpDataSetInfo`).
|
||||
|
||||
It was created by pasting the following into the browser console:
|
||||
The dump is created by pasting the following into the browser console:
|
||||
|
||||
```javascript
|
||||
async function exportIndexedDb(name) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,4 @@
|
||||
## Dump of a libolm indexeddb cryptostore to test migration of a full account
|
||||
|
||||
A dump of an account containing a complete set of data to migrate.
|
||||
The data set is substantial enough to allow for testing of chunking mechanisms and progress reporting during the migration process.
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,109 @@
|
||||
import { DumpDataSetInfo } from "../index";
|
||||
|
||||
/**
|
||||
* A key query response containing the current keys of the tested user.
|
||||
* To be used during tests with fetchmock.
|
||||
*/
|
||||
const KEYS_QUERY_RESPONSE: any = {
|
||||
device_keys: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
KMFSTJSMLB: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "KMFSTJSMLB",
|
||||
keys: {
|
||||
"curve25519:KMFSTJSMLB": "LKv0bKbc0EC4h0jknbemv3QalEkeYvuNeUXVRgVVTTU",
|
||||
"ed25519:KMFSTJSMLB": "qK70DEqIXq7T+UU3v/al47Ab4JkMEBLpNrTBMbS5rrw",
|
||||
},
|
||||
user_id: "@vdhtest200713:matrix.org",
|
||||
signatures: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
"ed25519:KMFSTJSMLB":
|
||||
"aE+PdxLAdwQ/xfJwLmqebvt/lrT97fZas2SQFFrM+dPmHxQtjyS8csm88BLfGRjJKK1B/vWev3AaKqQZwLTUAw",
|
||||
"ed25519:lDvg6vi3P80L9XFNpUSU+5Y87m3p6yHcC83jhSU4Q5k":
|
||||
"lCd4SA/JT1nnxsgN9yQaLJQhH5hkLMVVx6ba5JAjL1wpWVqyPxzMJHImX6vTztk6S8rybcdfYkea5W/Ii+4HCQ",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
master_keys: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
user_id: "@vdhtest200713:matrix.org",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI": "gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI",
|
||||
},
|
||||
signatures: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
"ed25519:MWOGVUTXZN":
|
||||
"stOu1aHbhsWB/Aj5M/HqBR83QzME+682C995Uc8JxSmmyrlWmgG8QrnoUDG2OFR1t6zNQ+QLEilU4WNEOV73DQ",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
self_signing_keys: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
user_id: "@vdhtest200713:matrix.org",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
"ed25519:lDvg6vi3P80L9XFNpUSU+5Y87m3p6yHcC83jhSU4Q5k": "lDvg6vi3P80L9XFNpUSU+5Y87m3p6yHcC83jhSU4Q5k",
|
||||
},
|
||||
signatures: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI":
|
||||
"HKTC7NoBhAkfJtmemmkn/HvCCgBQViWZ0uH7aGPRaWMDFgD8T7Q+y1j3FKZv4mhSopR85Fq3FRyXsG8OVvGeBA",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user_signing_keys: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
user_id: "@vdhtest200713:matrix.org",
|
||||
usage: ["user_signing"],
|
||||
keys: {
|
||||
"ed25519:YShqO/3u5vQ0uucojraWrtoLrek0CYrurN/vH/YPMg8": "YShqO/3u5vQ0uucojraWrtoLrek0CYrurN/vH/YPMg8",
|
||||
},
|
||||
signatures: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI":
|
||||
"u8VOi4IaeRJwDgy2ftK02NJQPdBijy8f/0+WnHGG72yfOvMthwWzEw8SrRSNG8glBNrfHinKwCyJJzAJwyepCQ",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* A `/room_keys/version` response containing the current server-side backup info.
|
||||
* To be used during tests with fetchmock.
|
||||
*/
|
||||
const BACKUP_RESPONSE: any = {
|
||||
auth_data: {
|
||||
public_key: "q+HZiJdHl2Yopv9GGvv7EYSzDMrAiRknK4glSdoaomI",
|
||||
signatures: {
|
||||
"@vdhtest200713:matrix.org": {
|
||||
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI":
|
||||
"reDp6Mu+j+tfUL3/T6f5OBT3N825Lzpc43vvG+RvjX6V+KxXzodBQArgCoeEHLtL9OgSBmNrhTkSOX87MWCKAw",
|
||||
"ed25519:KMFSTJSMLB":
|
||||
"F8tyV5W6wNi0GXTdSg+gxSCULQi0EYxdAAqfkyNq58KzssZMw5i+PRA0aI2b+D7NH/aZaJrtiYNHJ0gWLSQvAw",
|
||||
},
|
||||
},
|
||||
},
|
||||
version: "7",
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
etag: "1",
|
||||
count: 79,
|
||||
};
|
||||
|
||||
/**
|
||||
* A dataset containing the information for the tested user.
|
||||
* To be used during tests.
|
||||
*/
|
||||
export const FULL_ACCOUNT_DATASET: DumpDataSetInfo = {
|
||||
userId: "@vdhtest200713:matrix.org",
|
||||
deviceId: "KMFSTJSMLB",
|
||||
pickleKey: "+1k2Ppd7HIisUY824v7JtV3/oEE4yX0TqtmNPyhaD7o",
|
||||
backupResponse: BACKUP_RESPONSE,
|
||||
keyQueryResponse: KEYS_QUERY_RESPONSE,
|
||||
dumpPath: "spec/test-utils/test_indexeddb_cryptostore_dump/full_account/dump.json",
|
||||
};
|
||||
@@ -18,11 +18,12 @@ import { readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
/**
|
||||
* Populate an IndexedDB store with the test data from this directory.
|
||||
* Populate an IndexedDB store with a set of test data.
|
||||
*
|
||||
* @param name - Name of the IndexedDB database to create.
|
||||
* @param dumpPath - The path to the dump file to import.
|
||||
*/
|
||||
export async function populateStore(name: string): Promise<IDBDatabase> {
|
||||
export async function populateStore(name: string, dumpPath: string): Promise<IDBDatabase> {
|
||||
const req = indexedDB.open(name, 11);
|
||||
|
||||
const db = await new Promise<IDBDatabase>((resolve, reject) => {
|
||||
@@ -42,7 +43,7 @@ export async function populateStore(name: string): Promise<IDBDatabase> {
|
||||
};
|
||||
});
|
||||
|
||||
await importData(db);
|
||||
await importData(db, dumpPath);
|
||||
|
||||
return db;
|
||||
}
|
||||
@@ -100,8 +101,8 @@ function upgradeDatabase(oldVersion: number, db: IDBDatabase) {
|
||||
}
|
||||
}
|
||||
|
||||
async function importData(db: IDBDatabase) {
|
||||
const path = resolve("spec/test-utils/test_indexeddb_cryptostore_dump/dump.json");
|
||||
async function importData(db: IDBDatabase, dumpPath: string) {
|
||||
const path = resolve(dumpPath);
|
||||
const json: Record<string, Array<{ key?: any; value: any }>> = JSON.parse(
|
||||
await readFile(path, { encoding: "utf8" }),
|
||||
);
|
||||
@@ -134,3 +135,20 @@ async function importData(db: IDBDatabase) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export interface DumpDataSetInfo {
|
||||
/** The user ID to use for the test.*/
|
||||
userId: string;
|
||||
/** The device ID to use for the test.*/
|
||||
deviceId: string;
|
||||
/** The path to the dump file to import via {@link populateStore}.*/
|
||||
dumpPath: string;
|
||||
/** The pickle key to use for the dumped account.*/
|
||||
pickleKey: string;
|
||||
/** The response to use for the keys query. */
|
||||
keyQueryResponse: any;
|
||||
/** The response to use for the backup query.*/
|
||||
backupResponse?: any;
|
||||
/** Additional dump info specific for some tests.*/
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
## Dump of a libolm indexeddb cryptostore where the msk is not cached
|
||||
|
||||
A dump simulating an account where the identity was verified, but the msk was not in cache.
|
||||
Used to test that the owner identity local trust is migrated correctly.
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,260 @@
|
||||
import { KeyBackupInfo } from "../../../../src/crypto-api/keybackup";
|
||||
import { DumpDataSetInfo } from "../index";
|
||||
|
||||
/**
|
||||
* A key query response containing the current keys of the tested user.
|
||||
* To be used during tests with fetchmock.
|
||||
*/
|
||||
const KEY_QUERY_RESPONSE: any = {
|
||||
device_keys: {
|
||||
"@migration:localhost": {
|
||||
CBGTADUILV: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "CBGTADUILV",
|
||||
keys: {
|
||||
"curve25519:CBGTADUILV": "gqhFlc7Wzc1wmmmAu3ySIEe4LtDcBK/bdzrtZg+mMSg",
|
||||
"ed25519:CBGTADUILV": "q1q3L1Il4l61c/6TmI4fYWMsseNMJJYE2Y0r+5ajKQI",
|
||||
},
|
||||
signatures: {
|
||||
"@migration:localhost": {
|
||||
"ed25519:CBGTADUILV":
|
||||
"ppSmA0slyQ7RJOFn+qZSLCGeHN6/jAmqKvUZo5Q1hWk0ugkKycRoSUi9TOfbfAVSf8xvFirXy2VGXQbEVPJqAA",
|
||||
"ed25519:d+4HhsodR2Zqv4Z5V0VxPfy8zbjLjUCdCyv5qme5Ygc":
|
||||
"cFLWl1fjehLrzrEn3UnmZMIgy3C23WMgGRsn4e6Z/55vmen4KMs8bLpgZaDoWhIdn/8siHRWafA5sFdzK2NsBQ",
|
||||
"ed25519:bmFmNcVPvaqrlNzmyKn9uU+QRHyx2QRbn/bUAlTH760":
|
||||
"C6EeqNPcaQyuZgo8+HOUywc/TMkW5IMjg7aoxyu93X//KcNNXKRfj1banYP6XqyPuQITLamBYc1089Jpt9g4Cw",
|
||||
"ed25519:RkQzi0+aKIL9Y+GzsN23xMz3i3QRkH03G5aqqEbbuy4":
|
||||
"YwBN/SbCxO8hPgv1B9JY2WVFK4LNK9vq1UNVrkF2j0ZDw9LrvaOws72mbmzZ0nbD3ohcEZ8rXsEosxEVr5r7AQ",
|
||||
},
|
||||
},
|
||||
user_id: "@migration:localhost",
|
||||
unsigned: {
|
||||
device_display_name: "localhost:8080: Chrome on macOS",
|
||||
},
|
||||
},
|
||||
TMWBMDZPFT: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "TMWBMDZPFT",
|
||||
keys: {
|
||||
"curve25519:TMWBMDZPFT": "oYP9EXvHMbliFdfk8jPvUw0KhAd0+PBqdMslJAt/ZGQ",
|
||||
"ed25519:TMWBMDZPFT": "IyfPT67JutFWJsUxrxSqEWxgRjKn9B/w78uKU4OBj1E",
|
||||
},
|
||||
signatures: {
|
||||
"@migration:localhost": {
|
||||
"ed25519:TMWBMDZPFT":
|
||||
"IWIuuDag4ZMDhMObYV63X7dBYEUYNHYR0Yu/bwLvQh5ieDjQSrZSLOzDrgCyPCM4hkc4JlhneQpJsYo1lUH7DA",
|
||||
"ed25519:d+4HhsodR2Zqv4Z5V0VxPfy8zbjLjUCdCyv5qme5Ygc":
|
||||
"iEcTKElQu4CAsQIXmBaZmXwfB6Diut+4ZXakP1ob7OIDMrCYBcgXsBFYg6GuxwL0LCTVcUgbUw7VuPKSvM8UAA",
|
||||
"ed25519:MYgcP5P7P6KucWjLvTRofY5PWxsf+WDj2BiXtqOO5Gw":
|
||||
"KcBLDWkCwZyIzlBkC29PNzHxx7Br14TYlhBfREEEQo/Rd34ZZUYwbQ8iPhB8S1GVq3YwgAV6piYIcxpQin+dBA",
|
||||
"ed25519:HGN9m99VprMuQBDA3o+KZKcEYTaGmiaujrkygjScMnY":
|
||||
"VqrvA148Uxib9TNFI1rc9r8qpwTojCkqLofEz9dMLc/XV3U14WD5/LDEhMuCwNu6wsu/uO+dS4AmJlJnN/iAAg",
|
||||
"ed25519:Nt0L/p+UVHMx603sYHXwXja+VyQIUVFvu0vDBYn56Zk":
|
||||
"D1COHzROOTNlCn8b1zI9+6phUtF0OVqWxLfOLnX5t14H2oENYV2ASgaxsdmXcSZPrGzaJkmSOginHHzsabe5CA",
|
||||
"ed25519:bmFmNcVPvaqrlNzmyKn9uU+QRHyx2QRbn/bUAlTH760":
|
||||
"SFSDrsi3GQ9jjBYUc2aUSzf777/0NfQWrOBi2CK+v5VQY3FkyHBln3K4YzvxIKSVIhOaQtBlEDtfQb33kwTgDg",
|
||||
"ed25519:RkQzi0+aKIL9Y+GzsN23xMz3i3QRkH03G5aqqEbbuy4":
|
||||
"BtJkzQe0YFAa8gJiYXYtzGtktl9vZMNYl5jd4DA8Toi4VxgosJNZQE7lT5qpYU0BrlFn46QIs/38X8JhSt+wAQ",
|
||||
},
|
||||
},
|
||||
user_id: "@migration:localhost",
|
||||
unsigned: {
|
||||
device_display_name: "localhost:8080: Chrome on macOS",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
failures: {},
|
||||
master_keys: {
|
||||
"@migration:localhost": {
|
||||
keys: {
|
||||
"ed25519:cFjUBAhAZ2tjYF1TpQtYNA3x9XRzTiIdP2N2EvRaOH4": "cFjUBAhAZ2tjYF1TpQtYNA3x9XRzTiIdP2N2EvRaOH4",
|
||||
},
|
||||
signatures: {
|
||||
"@migration:localhost": {
|
||||
"ed25519:TMWBMDZPFT":
|
||||
"RrPUnYoekK7wZGrLNXshgoupF8v53S/vJyvkBJi+q9THh4Qrf3CieuVJFx8mwtmEZgGoA2tSroAVnRqvEQ+IBQ",
|
||||
"ed25519:cFjUBAhAZ2tjYF1TpQtYNA3x9XRzTiIdP2N2EvRaOH4":
|
||||
"o4CbtdU3IqJK90UXAEBtxps2m4XBYvWJI2nbVlzBaGRr+Xt/3vtwDMlc5G970kPQWBbs/koYJh8MSaE7Fm1mAg",
|
||||
"ed25519:CBGTADUILV":
|
||||
"AgZoG+ix8aW3FAW6v+/Xu+QJpxzvsx5itbB8RyqMet9YlNqX90vYIbBV7IoV2WFY2WdANYEffX2CE0FpR6NnCg",
|
||||
},
|
||||
},
|
||||
usage: ["master"],
|
||||
user_id: "@migration:localhost",
|
||||
},
|
||||
},
|
||||
self_signing_keys: {
|
||||
"@migration:localhost": {
|
||||
keys: {
|
||||
"ed25519:RkQzi0+aKIL9Y+GzsN23xMz3i3QRkH03G5aqqEbbuy4": "RkQzi0+aKIL9Y+GzsN23xMz3i3QRkH03G5aqqEbbuy4",
|
||||
},
|
||||
signatures: {
|
||||
"@migration:localhost": {
|
||||
"ed25519:cFjUBAhAZ2tjYF1TpQtYNA3x9XRzTiIdP2N2EvRaOH4":
|
||||
"hs8VqoTfipDjC2pzFdmzb1aENhDjVV+gc86fuYftczaCcsXUWop/NPwoF51Ie6Nb3YL0N7ZZAUrycuJP5hFbDg",
|
||||
},
|
||||
},
|
||||
usage: ["self_signing"],
|
||||
user_id: "@migration:localhost",
|
||||
},
|
||||
},
|
||||
user_signing_keys: {
|
||||
"@migration:localhost": {
|
||||
keys: {
|
||||
"ed25519:WNJ2G3Ig5EdC4wYiRKcK7bhLP2+I4wI6V7SKgJTXdw8": "WNJ2G3Ig5EdC4wYiRKcK7bhLP2+I4wI6V7SKgJTXdw8",
|
||||
},
|
||||
signatures: {
|
||||
"@migration:localhost": {
|
||||
"ed25519:cFjUBAhAZ2tjYF1TpQtYNA3x9XRzTiIdP2N2EvRaOH4":
|
||||
"Vlba5rJQxG+ussVLoycvHcin7Ghv0uUeClDqDbM+RPF+jx9w4ozbcuEOTJdyzyPA+GxN9Kzh2lmVFMMQGyvNAw",
|
||||
},
|
||||
},
|
||||
usage: ["user_signing"],
|
||||
user_id: "@migration:localhost",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* A new key query response for the same user simulating a cross-signing key reset.
|
||||
* To be used during tests with fetchmock.
|
||||
*/
|
||||
const ROTATED_KEY_QUERY_RESPONSE: any = {
|
||||
device_keys: {
|
||||
"@migration:localhost": {
|
||||
TMWBMDZPFT: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "TMWBMDZPFT",
|
||||
keys: {
|
||||
"curve25519:TMWBMDZPFT": "oYP9EXvHMbliFdfk8jPvUw0KhAd0+PBqdMslJAt/ZGQ",
|
||||
"ed25519:TMWBMDZPFT": "IyfPT67JutFWJsUxrxSqEWxgRjKn9B/w78uKU4OBj1E",
|
||||
},
|
||||
signatures: {
|
||||
"@migration:localhost": {
|
||||
"ed25519:TMWBMDZPFT":
|
||||
"IWIuuDag4ZMDhMObYV63X7dBYEUYNHYR0Yu/bwLvQh5ieDjQSrZSLOzDrgCyPCM4hkc4JlhneQpJsYo1lUH7DA",
|
||||
"ed25519:d+4HhsodR2Zqv4Z5V0VxPfy8zbjLjUCdCyv5qme5Ygc":
|
||||
"iEcTKElQu4CAsQIXmBaZmXwfB6Diut+4ZXakP1ob7OIDMrCYBcgXsBFYg6GuxwL0LCTVcUgbUw7VuPKSvM8UAA",
|
||||
"ed25519:MYgcP5P7P6KucWjLvTRofY5PWxsf+WDj2BiXtqOO5Gw":
|
||||
"KcBLDWkCwZyIzlBkC29PNzHxx7Br14TYlhBfREEEQo/Rd34ZZUYwbQ8iPhB8S1GVq3YwgAV6piYIcxpQin+dBA",
|
||||
"ed25519:HGN9m99VprMuQBDA3o+KZKcEYTaGmiaujrkygjScMnY":
|
||||
"VqrvA148Uxib9TNFI1rc9r8qpwTojCkqLofEz9dMLc/XV3U14WD5/LDEhMuCwNu6wsu/uO+dS4AmJlJnN/iAAg",
|
||||
"ed25519:Nt0L/p+UVHMx603sYHXwXja+VyQIUVFvu0vDBYn56Zk":
|
||||
"D1COHzROOTNlCn8b1zI9+6phUtF0OVqWxLfOLnX5t14H2oENYV2ASgaxsdmXcSZPrGzaJkmSOginHHzsabe5CA",
|
||||
"ed25519:bmFmNcVPvaqrlNzmyKn9uU+QRHyx2QRbn/bUAlTH760":
|
||||
"SFSDrsi3GQ9jjBYUc2aUSzf777/0NfQWrOBi2CK+v5VQY3FkyHBln3K4YzvxIKSVIhOaQtBlEDtfQb33kwTgDg",
|
||||
"ed25519:RkQzi0+aKIL9Y+GzsN23xMz3i3QRkH03G5aqqEbbuy4":
|
||||
"BtJkzQe0YFAa8gJiYXYtzGtktl9vZMNYl5jd4DA8Toi4VxgosJNZQE7lT5qpYU0BrlFn46QIs/38X8JhSt+wAQ",
|
||||
},
|
||||
},
|
||||
user_id: "@migration:localhost",
|
||||
unsigned: {
|
||||
device_display_name: "localhost:8080: Chrome on macOS",
|
||||
},
|
||||
},
|
||||
XFZFSCUOFL: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "XFZFSCUOFL",
|
||||
keys: {
|
||||
"curve25519:XFZFSCUOFL": "aN2Ty+0rutNkrRtxhV+ciI8GhF4epSxzL7bAOr8zfkc",
|
||||
"ed25519:XFZFSCUOFL": "V7CPhXdfLFk+qAOFivrpFskmunVTeuM+EOM3DMlDxkI",
|
||||
},
|
||||
signatures: {
|
||||
"@migration:localhost": {
|
||||
"ed25519:XFZFSCUOFL":
|
||||
"4Pqc2FWJ5p/L/tSlfUBIlcQzLmN5CksJriAibY8LSDAXdGYiQJ7hvKqneEuVhrMYwqyIxb4bAad+r6wnY0/7Cg",
|
||||
"ed25519:RkQzi0+aKIL9Y+GzsN23xMz3i3QRkH03G5aqqEbbuy4":
|
||||
"yH8pKnD+E8YaawS+1NCjwy0cf2WzBRff9BBNX4YnAuTyc6s5b1QqNfu9DP5qblw8TZ7hZmaziePZKsjRiqJLBg",
|
||||
"ed25519:OEv0wHLusJx7zTCc0h3HbNIHLIxlGZKh63tc2ptKb+Y":
|
||||
"M8SfAiEUzd7AsWp8InS7BxV3cRqV3MjMxks4DwSxsVxvkCco2JWybKgev+vTZyM6XDg930o0FObQOxWm4+CkBw",
|
||||
},
|
||||
},
|
||||
user_id: "@migration:localhost",
|
||||
unsigned: {
|
||||
device_display_name: "localhost:8080: Chrome on macOS",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
failures: {},
|
||||
master_keys: {
|
||||
"@migration:localhost": {
|
||||
user_id: "@migration:localhost",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
"ed25519:rXCrBin/+xyh+yW//vWte+2UV0et1ZHTWfalp/Ekack": "rXCrBin/+xyh+yW//vWte+2UV0et1ZHTWfalp/Ekack",
|
||||
},
|
||||
signatures: {
|
||||
"@migration:localhost": {
|
||||
"ed25519:XFZFSCUOFL":
|
||||
"C8aswtyUABWvj2DInehVoh2P/EDbwRhlIk51LtV3L71POUCh7pZuyXRMMWKZeyRvHRmEllXBtRkH1iol/p56Bg",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
self_signing_keys: {
|
||||
"@migration:localhost": {
|
||||
user_id: "@migration:localhost",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
"ed25519:OEv0wHLusJx7zTCc0h3HbNIHLIxlGZKh63tc2ptKb+Y": "OEv0wHLusJx7zTCc0h3HbNIHLIxlGZKh63tc2ptKb+Y",
|
||||
},
|
||||
signatures: {
|
||||
"@migration:localhost": {
|
||||
"ed25519:rXCrBin/+xyh+yW//vWte+2UV0et1ZHTWfalp/Ekack":
|
||||
"dH596pGp8+f8dlwd81UrKDWoRDd24yAqqMSLqR4fJHyfszbn7qCvQA6LYZ023TLmk33FKcJqRtd2v/ykTmS3Bg",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user_signing_keys: {
|
||||
"@migration:localhost": {
|
||||
user_id: "@migration:localhost",
|
||||
usage: ["user_signing"],
|
||||
keys: {
|
||||
"ed25519:8XHpC3MeMReIfYneWIRX8c4ANgJuQ1+oFrktBcLka4o": "8XHpC3MeMReIfYneWIRX8c4ANgJuQ1+oFrktBcLka4o",
|
||||
},
|
||||
signatures: {
|
||||
"@migration:localhost": {
|
||||
"ed25519:rXCrBin/+xyh+yW//vWte+2UV0et1ZHTWfalp/Ekack":
|
||||
"FX6ylagvx3IG1zMf/ayYgDb/1+x0/F28pHQqzQMGGssAmc15nat/R6AF0QO7Qg7uqTAf04ohuZtWax3dTwjNDQ",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* A `/room_keys/version` response containing the current server-side backup info.
|
||||
* To be used during tests with fetchmock.
|
||||
*/
|
||||
const BACKUP_RESPONSE: KeyBackupInfo = {
|
||||
auth_data: {
|
||||
public_key: "2ffIfIB4oryqZpsJQjQNUaxgCzxliC6A4PJvnrN+XAA",
|
||||
signatures: {
|
||||
"@migration:localhost": {
|
||||
"ed25519:TMWBMDZPFT":
|
||||
"qBvalid/G4hnSF3hAeX4TtRN6/BqprgiYnLEtDuatyQ5WxWr0s4uSOyvHSglsRdpoo32FDBHfTIZkCOVxSLwAA",
|
||||
},
|
||||
},
|
||||
},
|
||||
version: "2",
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
etag: "0",
|
||||
count: 0,
|
||||
};
|
||||
|
||||
/**
|
||||
* A dataset containing the information for the tested user.
|
||||
* To be used during tests.
|
||||
*/
|
||||
export const MSK_NOT_CACHED_DATASET: DumpDataSetInfo = {
|
||||
userId: "@migration:localhost",
|
||||
deviceId: "CBGTADUILV",
|
||||
pickleKey: "qEURMepfkMvoBQGaWlI9MZKYnDMsSAiW8aFTKXaeDV0",
|
||||
keyQueryResponse: KEY_QUERY_RESPONSE,
|
||||
rotatedKeyQueryResponse: ROTATED_KEY_QUERY_RESPONSE,
|
||||
backupResponse: BACKUP_RESPONSE,
|
||||
dumpPath: "spec/test-utils/test_indexeddb_cryptostore_dump/no_cached_msk_dump/dump.json",
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
## Dump of a libolm indexeddb cryptostore where the identity is not trusted.
|
||||
|
||||
A dump of an account where the identity was not verified.
|
||||
Used as a test case for migration of the identity local trust.
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,110 @@
|
||||
import { DumpDataSetInfo } from "../index";
|
||||
|
||||
/**
|
||||
* A key query response containing the current keys of the tested user.
|
||||
* To be used during tests with fetchmock.
|
||||
*/
|
||||
const KEY_QUERY_RESPONSE = {
|
||||
device_keys: {
|
||||
"@untrusted:localhost": {
|
||||
IXNYALOZWU: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "IXNYALOZWU",
|
||||
keys: {
|
||||
"curve25519:IXNYALOZWU": "EHMQEtJd9INJg28HwKK8Te1EX8obR3VTtyNwf/rcczM",
|
||||
"ed25519:IXNYALOZWU": "OxMfZHsYJvroTp1RtjUOejpWbRBryN6VsojC5dKR74U",
|
||||
},
|
||||
signatures: {
|
||||
"@untrusted:localhost": {
|
||||
"ed25519:IXNYALOZWU":
|
||||
"tWaTiRKc95ZCqM2qrKTdq1sQ3DPFgw3vdrOVmWIHQwj92DCgJtnQ9uymLMOq+MSb80bdBBjXwrNeOufgaL/6CQ",
|
||||
"ed25519:+ik0n/QnBPq8H/48wAT+54slKk1SL7NIk/HtiN/cNEg":
|
||||
"+QXZFLiAv+k7UXgAP6AXLk/PdZ3TlJ77M23m73v8qvavAlnkLBAjKNA3BG39JTQET5UhW5DnCohwsbGP+aY1Cw",
|
||||
},
|
||||
},
|
||||
user_id: "@untrusted:localhost",
|
||||
unsigned: {
|
||||
device_display_name: "localhost:8080: Chrome on macOS",
|
||||
},
|
||||
},
|
||||
VJPSPVPWZT: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "VJPSPVPWZT",
|
||||
keys: {
|
||||
"curve25519:VJPSPVPWZT": "+RxCNRFPqBZJm6PLjEJsSdFixGWQJygD5Os11/+6PC0",
|
||||
"ed25519:VJPSPVPWZT": "wqH7xK/DQya8m05Vy4rnacjugGNBiY+7Ml6wyRVkM9U",
|
||||
},
|
||||
signatures: {
|
||||
"@untrusted:localhost": {
|
||||
"ed25519:VJPSPVPWZT":
|
||||
"XC+RoKL/zVZOIwk/bGEQJlJu49QicY1v6vSDMHA2y0/fpX/MD4KiWGD7+W5DFD54E8FrFVTsIgkzat561qdTBQ",
|
||||
},
|
||||
},
|
||||
user_id: "@untrusted:localhost",
|
||||
unsigned: {
|
||||
device_display_name: "localhost:8080: Chrome on macOS",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
failures: {},
|
||||
master_keys: {
|
||||
"@untrusted:localhost": {
|
||||
keys: {
|
||||
"ed25519:Uahbc3+Rk65y0ku6T2RL/29fEA9Bum6+OaqptG6df3g": "Uahbc3+Rk65y0ku6T2RL/29fEA9Bum6+OaqptG6df3g",
|
||||
},
|
||||
signatures: {
|
||||
"@untrusted:localhost": {
|
||||
"ed25519:IXNYALOZWU":
|
||||
"KdAdyKO2sb3Di3bdK+oxf+gjMSmW/sisRNvpKZORPKwmy2SGaKGYkecBtslunoFjnb+hjIESgweQu6cHoNX4AA",
|
||||
"ed25519:Uahbc3+Rk65y0ku6T2RL/29fEA9Bum6+OaqptG6df3g":
|
||||
"b0R9Id5HxHYo+MA22Vlq0OckTrWnSWhgHLvF8Wr4e154JdtOyK7N0aXPQPkrLB0fmyVmGdbDa9xs9jsfINGmDw",
|
||||
},
|
||||
},
|
||||
usage: ["master"],
|
||||
user_id: "@untrusted:localhost",
|
||||
},
|
||||
},
|
||||
self_signing_keys: {
|
||||
"@untrusted:localhost": {
|
||||
keys: {
|
||||
"ed25519:+ik0n/QnBPq8H/48wAT+54slKk1SL7NIk/HtiN/cNEg": "+ik0n/QnBPq8H/48wAT+54slKk1SL7NIk/HtiN/cNEg",
|
||||
},
|
||||
signatures: {
|
||||
"@untrusted:localhost": {
|
||||
"ed25519:Uahbc3+Rk65y0ku6T2RL/29fEA9Bum6+OaqptG6df3g":
|
||||
"z/5z51jbRpyDQhYnfUHhhb5fUbzRDlfjD8mZA2ZGStpE/F41lDyxjlvF2W/E2CJ27bmJFdk7nC+ZCwriYfYxBw",
|
||||
},
|
||||
},
|
||||
usage: ["self_signing"],
|
||||
user_id: "@untrusted:localhost",
|
||||
},
|
||||
},
|
||||
user_signing_keys: {
|
||||
"@untrusted:localhost": {
|
||||
keys: {
|
||||
"ed25519:L/8HbQWnK9OidAcDVB+Az9b0Mx3OdBtIMFsUjV6qgSQ": "L/8HbQWnK9OidAcDVB+Az9b0Mx3OdBtIMFsUjV6qgSQ",
|
||||
},
|
||||
signatures: {
|
||||
"@untrusted:localhost": {
|
||||
"ed25519:Uahbc3+Rk65y0ku6T2RL/29fEA9Bum6+OaqptG6df3g":
|
||||
"UuNvzebLQn31LYGbx+ADe60BB25kWy4SVVyd9BXlY/tAZMoA8Tmq1e2R2tJJtPdJxC/Oogktj2+iikZV/YMjAQ",
|
||||
},
|
||||
},
|
||||
usage: ["user_signing"],
|
||||
user_id: "@untrusted:localhost",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* A dataset containing the information for the tested user.
|
||||
* To be used during tests.
|
||||
*/
|
||||
export const IDENTITY_NOT_TRUSTED_DATASET: DumpDataSetInfo = {
|
||||
userId: "@untrusted:localhost",
|
||||
deviceId: "VJPSPVPWZT",
|
||||
pickleKey: "WVllQb4Lk/WwP4Q7iBfeTUHpgydZm9YqXI1B5bTvnIM",
|
||||
keyQueryResponse: KEY_QUERY_RESPONSE,
|
||||
dumpPath: "spec/test-utils/test_indexeddb_cryptostore_dump/unverified/dump.json",
|
||||
};
|
||||
@@ -25,6 +25,7 @@ import { UserTrustLevel } from "../../src/crypto/CrossSigning";
|
||||
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
|
||||
import { EventDecryptionResult } from "../../src/common-crypto/CryptoBackend";
|
||||
import * as testData from "../test-utils/test-data";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -463,7 +464,7 @@ describe("Crypto", function () {
|
||||
type: "m.room.member",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
content: { membership: "invite" },
|
||||
content: { membership: KnownMembership.Invite },
|
||||
state_key: "@bob:example.com",
|
||||
}),
|
||||
]);
|
||||
@@ -795,7 +796,7 @@ describe("Crypto", function () {
|
||||
type: "m.room.member",
|
||||
sender: "@clara:example.com",
|
||||
room_id: roomId,
|
||||
content: { membership: "invite" },
|
||||
content: { membership: KnownMembership.Invite },
|
||||
state_key: "@bob:example.com",
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -36,6 +36,7 @@ import { DeviceTrustLevel } from "../../../../src/crypto/CrossSigning";
|
||||
import { MegolmEncryption as MegolmEncryptionClass } from "../../../../src/crypto/algorithms/megolm";
|
||||
import { recursiveMapToObject } from "../../../../src/utils";
|
||||
import { sleep } from "../../../../src/utils";
|
||||
import { KnownMembership } from "../../../../src/@types/membership";
|
||||
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!;
|
||||
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!;
|
||||
@@ -806,11 +807,11 @@ describe("MegolmDecryption", function () {
|
||||
aliceRoom.getEncryptionTargetMembers = jest.fn().mockResolvedValue([
|
||||
{
|
||||
userId: "@alice:example.com",
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
},
|
||||
{
|
||||
userId: "@bob:example.com",
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
},
|
||||
]);
|
||||
const BOB_DEVICES = {
|
||||
|
||||
@@ -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;
|
||||
@@ -87,9 +101,12 @@ describe("RoomWidgetClient", () => {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
const makeClient = async (capabilities: ICapabilities): Promise<void> => {
|
||||
const makeClient = async (
|
||||
capabilities: ICapabilities,
|
||||
sendContentLoaded: boolean | undefined = undefined,
|
||||
): Promise<void> => {
|
||||
const baseUrl = "https://example.org";
|
||||
client = createRoomWidgetClient(widgetApi, capabilities, "!1:example.org", { baseUrl });
|
||||
client = createRoomWidgetClient(widgetApi, capabilities, "!1:example.org", { baseUrl }, sendContentLoaded);
|
||||
expect(widgetApi.start).toHaveBeenCalled(); // needs to have been called early in order to not miss messages
|
||||
widgetApi.emit("ready");
|
||||
await client.startClient();
|
||||
@@ -143,7 +160,7 @@ describe("RoomWidgetClient", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("messages", () => {
|
||||
describe("initialization", () => {
|
||||
it("requests permissions for specific message types", async () => {
|
||||
await makeClient({ sendMessage: [MsgType.Text], receiveMessage: [MsgType.Text] });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
@@ -158,6 +175,15 @@ describe("RoomWidgetClient", () => {
|
||||
expect(widgetApi.requestCapabilityToReceiveMessage).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it("sends content loaded when configured", async () => {
|
||||
await makeClient({});
|
||||
expect(widgetApi.sendContentLoaded).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not sent content loaded when configured", async () => {
|
||||
await makeClient({}, false);
|
||||
expect(widgetApi.sendContentLoaded).not.toHaveBeenCalled();
|
||||
});
|
||||
// No point in testing sending and receiving since it's done exactly the
|
||||
// same way as non-message events
|
||||
});
|
||||
@@ -305,12 +331,14 @@ describe("RoomWidgetClient", () => {
|
||||
expect(await emittedSync).toEqual(SyncState.Syncing);
|
||||
});
|
||||
});
|
||||
|
||||
describe("oidc token", () => {
|
||||
it("requests an oidc token", async () => {
|
||||
await makeClient({});
|
||||
expect(await client.getOpenIdToken()).toStrictEqual(testOIDCToken);
|
||||
});
|
||||
});
|
||||
|
||||
it("gets TURN servers", async () => {
|
||||
const server1: ITurnServer = {
|
||||
uris: [
|
||||
|
||||
@@ -7,6 +7,7 @@ import { MatrixClient } from "../../src/matrix";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { RoomMember } from "../../src/models/room-member";
|
||||
import { EventTimelineSet } from "../../src/models/event-timeline-set";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
describe("EventTimeline", function () {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -50,7 +51,7 @@ describe("EventTimeline", function () {
|
||||
const events = [
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "invite",
|
||||
mship: KnownMembership.Invite,
|
||||
user: userB,
|
||||
skey: userA,
|
||||
event: true,
|
||||
@@ -87,7 +88,7 @@ describe("EventTimeline", function () {
|
||||
const state = [
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "invite",
|
||||
mship: KnownMembership.Invite,
|
||||
user: userB,
|
||||
skey: userA,
|
||||
event: true,
|
||||
@@ -203,11 +204,11 @@ describe("EventTimeline", function () {
|
||||
it("should set event.sender for new and old events", function () {
|
||||
const sentinel = new RoomMember(roomId, userA);
|
||||
sentinel.name = "Alice";
|
||||
sentinel.membership = "join";
|
||||
sentinel.membership = KnownMembership.Join;
|
||||
|
||||
const oldSentinel = new RoomMember(roomId, userA);
|
||||
sentinel.name = "Old Alice";
|
||||
sentinel.membership = "join";
|
||||
sentinel.membership = KnownMembership.Join;
|
||||
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
if (uid === userA) {
|
||||
@@ -246,11 +247,11 @@ describe("EventTimeline", function () {
|
||||
it("should set event.target for new and old m.room.member events", function () {
|
||||
const sentinel = new RoomMember(roomId, userA);
|
||||
sentinel.name = "Alice";
|
||||
sentinel.membership = "join";
|
||||
sentinel.membership = KnownMembership.Join;
|
||||
|
||||
const oldSentinel = new RoomMember(roomId, userA);
|
||||
sentinel.name = "Old Alice";
|
||||
sentinel.membership = "join";
|
||||
sentinel.membership = KnownMembership.Join;
|
||||
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
if (uid === userA) {
|
||||
@@ -267,14 +268,14 @@ describe("EventTimeline", function () {
|
||||
|
||||
const newEv = utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "invite",
|
||||
mship: KnownMembership.Invite,
|
||||
user: userB,
|
||||
skey: userA,
|
||||
event: true,
|
||||
});
|
||||
const oldEv = utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "ban",
|
||||
mship: KnownMembership.Ban,
|
||||
user: userB,
|
||||
skey: userA,
|
||||
event: true,
|
||||
@@ -291,7 +292,7 @@ describe("EventTimeline", function () {
|
||||
const events = [
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "invite",
|
||||
mship: KnownMembership.Invite,
|
||||
user: userB,
|
||||
skey: userA,
|
||||
event: true,
|
||||
@@ -330,7 +331,7 @@ describe("EventTimeline", function () {
|
||||
const events = [
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: "invite",
|
||||
mship: KnownMembership.Invite,
|
||||
user: userB,
|
||||
skey: userA,
|
||||
event: true,
|
||||
|
||||
@@ -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";
|
||||
@@ -71,6 +73,8 @@ import * as featureUtils from "../../src/feature";
|
||||
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();
|
||||
|
||||
@@ -565,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";
|
||||
@@ -660,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);
|
||||
@@ -750,7 +755,7 @@ describe("MatrixClient", function () {
|
||||
it("should get (unstable) file trees with valid state", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "join",
|
||||
getMyMembership: () => KnownMembership.Join,
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
@@ -789,7 +794,7 @@ describe("MatrixClient", function () {
|
||||
it("should not get (unstable) file trees if not joined", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "leave", // "not join"
|
||||
getMyMembership: () => KnownMembership.Leave, // "not join"
|
||||
} as unknown as Room;
|
||||
client.getRoom = (getRoomId) => {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
@@ -812,7 +817,7 @@ describe("MatrixClient", function () {
|
||||
it("should not get (unstable) file trees with invalid create contents", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "join",
|
||||
getMyMembership: () => KnownMembership.Join,
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
@@ -849,7 +854,7 @@ describe("MatrixClient", function () {
|
||||
it("should not get (unstable) file trees with invalid purpose/subtype contents", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "join",
|
||||
getMyMembership: () => KnownMembership.Join,
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
@@ -1309,7 +1314,7 @@ describe("MatrixClient", function () {
|
||||
describe("redactEvent", () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "join",
|
||||
getMyMembership: () => KnownMembership.Join,
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
if (eventType === EventType.RoomEncryption) {
|
||||
@@ -1448,7 +1453,7 @@ describe("MatrixClient", function () {
|
||||
const txnId = "m12345";
|
||||
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "join",
|
||||
getMyMembership: () => KnownMembership.Join,
|
||||
updatePendingEvent: (event: MatrixEvent, status: EventStatus) => event.setStatus(status),
|
||||
hasEncryptionStateEvent: jest.fn().mockReturnValue(true),
|
||||
} as unknown as Room;
|
||||
@@ -1526,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);
|
||||
@@ -2081,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.
|
||||
@@ -2171,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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventTimeline, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src";
|
||||
import { KnownMembership } from "../../../src/@types/membership";
|
||||
import { CallMembershipData } from "../../../src/matrixrtc/CallMembership";
|
||||
import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
|
||||
import { EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
|
||||
@@ -76,7 +77,7 @@ describe("MatrixRTCSession", () => {
|
||||
|
||||
it("ignores memberships events of members not in the room", () => {
|
||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||
mockRoom.hasMembershipState = (state) => state === "join";
|
||||
mockRoom.hasMembershipState = (state) => state === KnownMembership.Join;
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
expect(sess?.memberships.length).toEqual(0);
|
||||
});
|
||||
|
||||
@@ -25,6 +25,8 @@ import {
|
||||
} from "../../../src/models/MSC3089TreeSpace";
|
||||
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;
|
||||
@@ -458,14 +460,14 @@ describe("MSC3089TreeSpace", () => {
|
||||
expect(stateKey).toBeUndefined();
|
||||
return [
|
||||
// Partial implementations
|
||||
{ getContent: () => ({ membership: "join" }), getStateKey: () => joinMemberId },
|
||||
{ getContent: () => ({ membership: "knock" }), getStateKey: () => knockMemberId },
|
||||
{ getContent: () => ({ membership: "invite" }), getStateKey: () => inviteMemberId },
|
||||
{ getContent: () => ({ membership: "leave" }), getStateKey: () => leaveMemberId },
|
||||
{ getContent: () => ({ membership: "ban" }), getStateKey: () => banMemberId },
|
||||
{ getContent: () => ({ membership: KnownMembership.Join }), getStateKey: () => joinMemberId },
|
||||
{ getContent: () => ({ membership: KnownMembership.Knock }), getStateKey: () => knockMemberId },
|
||||
{ getContent: () => ({ membership: KnownMembership.Invite }), getStateKey: () => inviteMemberId },
|
||||
{ getContent: () => ({ membership: KnownMembership.Leave }), getStateKey: () => leaveMemberId },
|
||||
{ getContent: () => ({ membership: KnownMembership.Ban }), getStateKey: () => banMemberId },
|
||||
|
||||
// ensure we don't kick ourselves
|
||||
{ getContent: () => ({ membership: "join" }), getStateKey: () => selfUserId },
|
||||
{ getContent: () => ({ membership: KnownMembership.Join }), getStateKey: () => selfUserId },
|
||||
];
|
||||
},
|
||||
};
|
||||
@@ -946,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";
|
||||
@@ -1006,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 });
|
||||
|
||||
+462
-157
@@ -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,164 +39,150 @@ 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" }],
|
||||
};
|
||||
|
||||
// These would be better if individual rules were configured in the tests themselves.
|
||||
const matrixClient = {
|
||||
getRoom: function () {
|
||||
return {
|
||||
currentState: {
|
||||
getMember: function () {
|
||||
return {
|
||||
name: testDisplayName,
|
||||
};
|
||||
},
|
||||
getJoinedMemberCount: function () {
|
||||
return 0;
|
||||
},
|
||||
members: {},
|
||||
},
|
||||
};
|
||||
},
|
||||
...mockClientMethodsUser(testUserId),
|
||||
supportsIntentionalMentions: () => true,
|
||||
pushRules: {
|
||||
device: {},
|
||||
global: {
|
||||
content: [
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "sound",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
pattern: "ali",
|
||||
rule_id: ".m.rule.contains_user_name",
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "sound",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
pattern: "coffee",
|
||||
rule_id: "coffee",
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "sound",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
pattern: "foo*bar",
|
||||
rule_id: "foobar",
|
||||
},
|
||||
],
|
||||
override: [
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "sound",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
},
|
||||
],
|
||||
conditions: [
|
||||
{
|
||||
kind: "contains_display_name",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
rule_id: ".m.rule.contains_display_name",
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "sound",
|
||||
value: "default",
|
||||
},
|
||||
],
|
||||
conditions: [
|
||||
{
|
||||
is: "2",
|
||||
kind: "room_member_count",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
rule_id: ".m.rule.room_one_to_one",
|
||||
},
|
||||
],
|
||||
room: [],
|
||||
sender: [],
|
||||
underride: [
|
||||
msc3914RoomCallRule,
|
||||
{
|
||||
actions: ["dont-notify"],
|
||||
conditions: [
|
||||
{
|
||||
key: "content.msgtype",
|
||||
kind: "event_match",
|
||||
pattern: "m.notice",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
rule_id: ".m.rule.suppress_notices",
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
conditions: [],
|
||||
enabled: true,
|
||||
rule_id: ".m.rule.fallback",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} as unknown as MatrixClient;
|
||||
let matrixClient: MatrixClient;
|
||||
|
||||
beforeEach(function () {
|
||||
// These would be better if individual rules were configured in the tests themselves.
|
||||
matrixClient = {
|
||||
getRoom: function () {
|
||||
return {
|
||||
currentState: {
|
||||
getMember: function () {
|
||||
return {
|
||||
name: testDisplayName,
|
||||
};
|
||||
},
|
||||
getJoinedMemberCount: function () {
|
||||
return 0;
|
||||
},
|
||||
members: {},
|
||||
},
|
||||
};
|
||||
},
|
||||
...mockClientMethodsUser(testUserId),
|
||||
supportsIntentionalMentions: () => true,
|
||||
pushRules: {
|
||||
device: {},
|
||||
global: {
|
||||
content: [
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "sound",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
pattern: "ali",
|
||||
rule_id: ".m.rule.contains_user_name",
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "sound",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
pattern: "coffee",
|
||||
rule_id: "coffee",
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "sound",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
pattern: "foo*bar",
|
||||
rule_id: "foobar",
|
||||
},
|
||||
],
|
||||
override: [
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "sound",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
},
|
||||
],
|
||||
conditions: [
|
||||
{
|
||||
kind: "contains_display_name",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
default: true,
|
||||
rule_id: ".m.rule.contains_display_name",
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "sound",
|
||||
value: "default",
|
||||
},
|
||||
],
|
||||
conditions: [
|
||||
{
|
||||
is: "2",
|
||||
kind: "room_member_count",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
rule_id: ".m.rule.room_one_to_one",
|
||||
},
|
||||
],
|
||||
room: [],
|
||||
sender: [],
|
||||
underride: [
|
||||
msc3914RoomCallRule,
|
||||
{
|
||||
actions: ["dont-notify"],
|
||||
conditions: [
|
||||
{
|
||||
key: "content.msgtype",
|
||||
kind: "event_match",
|
||||
pattern: "m.notice",
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
rule_id: ".m.rule.suppress_notices",
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
"notify",
|
||||
{
|
||||
set_tweak: "highlight",
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
conditions: [],
|
||||
enabled: true,
|
||||
rule_id: ".m.rule.fallback",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} as unknown as MatrixClient;
|
||||
|
||||
testEvent = utils.mkEvent({
|
||||
type: "m.room.message",
|
||||
room: testRoomId,
|
||||
@@ -699,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",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS,
|
||||
UNSTABLE_MSC2666_SHARED_ROOMS,
|
||||
} from "../../src";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
describe("RoomMember", function () {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -50,7 +51,7 @@ describe("RoomMember", function () {
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
avatar_url: "mxc://flibble/wibble",
|
||||
},
|
||||
});
|
||||
@@ -282,11 +283,11 @@ describe("RoomMember", function () {
|
||||
describe("isKicked", () => {
|
||||
it("should return false if membership is not `leave`", () => {
|
||||
const member1 = new RoomMember(roomId, userA);
|
||||
member1.membership = "join";
|
||||
member1.membership = KnownMembership.Join;
|
||||
expect(member1.isKicked()).toBeFalsy();
|
||||
|
||||
const member2 = new RoomMember(roomId, userA);
|
||||
member2.membership = "invite";
|
||||
member2.membership = KnownMembership.Invite;
|
||||
expect(member2.isKicked()).toBeFalsy();
|
||||
|
||||
const member3 = new RoomMember(roomId, userA);
|
||||
@@ -295,17 +296,17 @@ describe("RoomMember", function () {
|
||||
|
||||
it("should return false if the membership event is unknown", () => {
|
||||
const member = new RoomMember(roomId, userA);
|
||||
member.membership = "leave";
|
||||
member.membership = KnownMembership.Leave;
|
||||
expect(member.isKicked()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return false if the member left of their own accord", () => {
|
||||
const member = new RoomMember(roomId, userA);
|
||||
member.membership = "leave";
|
||||
member.membership = KnownMembership.Leave;
|
||||
member.events.member = utils.mkMembership({
|
||||
event: true,
|
||||
sender: userA,
|
||||
mship: "leave",
|
||||
mship: KnownMembership.Leave,
|
||||
skey: userA,
|
||||
});
|
||||
expect(member.isKicked()).toBeFalsy();
|
||||
@@ -313,11 +314,11 @@ describe("RoomMember", function () {
|
||||
|
||||
it("should return true if the member's leave was sent by another user", () => {
|
||||
const member = new RoomMember(roomId, userA);
|
||||
member.membership = "leave";
|
||||
member.membership = KnownMembership.Leave;
|
||||
member.events.member = utils.mkMembership({
|
||||
event: true,
|
||||
sender: userB,
|
||||
mship: "leave",
|
||||
mship: KnownMembership.Leave,
|
||||
skey: userA,
|
||||
});
|
||||
expect(member.isKicked()).toBeTruthy();
|
||||
@@ -327,11 +328,11 @@ describe("RoomMember", function () {
|
||||
describe("getDMInviter", () => {
|
||||
it("should return userId of the sender of the invite if is_direct=true", () => {
|
||||
const member = new RoomMember(roomId, userA);
|
||||
member.membership = "invite";
|
||||
member.membership = KnownMembership.Invite;
|
||||
member.events.member = utils.mkMembership({
|
||||
event: true,
|
||||
sender: userB,
|
||||
mship: "invite",
|
||||
mship: KnownMembership.Invite,
|
||||
skey: userA,
|
||||
});
|
||||
member.events.member.event.content!.is_direct = true;
|
||||
@@ -340,11 +341,11 @@ describe("RoomMember", function () {
|
||||
|
||||
it("should not return userId of the sender of the invite if is_direct=false", () => {
|
||||
const member = new RoomMember(roomId, userA);
|
||||
member.membership = "invite";
|
||||
member.membership = KnownMembership.Invite;
|
||||
member.events.member = utils.mkMembership({
|
||||
event: true,
|
||||
sender: userB,
|
||||
mship: "invite",
|
||||
mship: KnownMembership.Invite,
|
||||
skey: userA,
|
||||
});
|
||||
member.events.member.event.content!.is_direct = false;
|
||||
@@ -355,7 +356,7 @@ describe("RoomMember", function () {
|
||||
describe("setMembershipEvent", function () {
|
||||
const joinEvent = utils.mkMembership({
|
||||
event: true,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: userA,
|
||||
room: roomId,
|
||||
name: "Alice",
|
||||
@@ -363,7 +364,7 @@ describe("RoomMember", function () {
|
||||
|
||||
const inviteEvent = utils.mkMembership({
|
||||
event: true,
|
||||
mship: "invite",
|
||||
mship: KnownMembership.Invite,
|
||||
user: userB,
|
||||
skey: userA,
|
||||
room: roomId,
|
||||
@@ -371,10 +372,10 @@ describe("RoomMember", function () {
|
||||
|
||||
it("should set 'membership' and assign the event to 'events.member'.", function () {
|
||||
member.setMembershipEvent(inviteEvent);
|
||||
expect(member.membership).toEqual("invite");
|
||||
expect(member.membership).toEqual(KnownMembership.Invite);
|
||||
expect(member.events.member).toEqual(inviteEvent);
|
||||
member.setMembershipEvent(joinEvent);
|
||||
expect(member.membership).toEqual("join");
|
||||
expect(member.membership).toEqual(KnownMembership.Join);
|
||||
expect(member.events.member).toEqual(joinEvent);
|
||||
});
|
||||
|
||||
@@ -387,13 +388,13 @@ describe("RoomMember", function () {
|
||||
return [
|
||||
utils.mkMembership({
|
||||
event: true,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
user: userB,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
event: true,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
user: userC,
|
||||
name: "Alice",
|
||||
@@ -443,7 +444,7 @@ describe("RoomMember", function () {
|
||||
it("should set 'name' to user_id if it is just whitespace", function () {
|
||||
const joinEvent = utils.mkMembership({
|
||||
event: true,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: userA,
|
||||
room: roomId,
|
||||
name: " \u200b ",
|
||||
@@ -457,7 +458,7 @@ describe("RoomMember", function () {
|
||||
it("should disambiguate users on a fuzzy displayname match", function () {
|
||||
const joinEvent = utils.mkMembership({
|
||||
event: true,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: userA,
|
||||
room: roomId,
|
||||
name: "Alíce\u200b", // note diacritic and zero width char
|
||||
@@ -471,7 +472,7 @@ describe("RoomMember", function () {
|
||||
return [
|
||||
utils.mkMembership({
|
||||
event: true,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
user: userC,
|
||||
name: "Alice",
|
||||
|
||||
@@ -25,9 +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";
|
||||
@@ -44,14 +46,14 @@ describe("RoomState", function () {
|
||||
utils.mkMembership({
|
||||
// userA joined
|
||||
event: true,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: userA,
|
||||
room: roomId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
// userB joined
|
||||
event: true,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: userB,
|
||||
room: roomId,
|
||||
}),
|
||||
@@ -102,20 +104,20 @@ describe("RoomState", function () {
|
||||
|
||||
it("should return a member which changes as state changes", function () {
|
||||
const member = state.getMember(userB);
|
||||
expect(member?.membership).toEqual("join");
|
||||
expect(member?.membership).toEqual(KnownMembership.Join);
|
||||
expect(member?.name).toEqual(userB);
|
||||
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
user: userB,
|
||||
mship: "leave",
|
||||
mship: KnownMembership.Leave,
|
||||
event: true,
|
||||
name: "BobGone",
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(member?.membership).toEqual("leave");
|
||||
expect(member?.membership).toEqual(KnownMembership.Leave);
|
||||
expect(member?.name).toEqual("BobGone");
|
||||
});
|
||||
});
|
||||
@@ -131,17 +133,17 @@ describe("RoomState", function () {
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
user: userA,
|
||||
mship: "leave",
|
||||
mship: KnownMembership.Leave,
|
||||
event: true,
|
||||
name: "AliceIsGone",
|
||||
}),
|
||||
]);
|
||||
const postLeaveUser = state.getSentinelMember(userA);
|
||||
|
||||
expect(preLeaveUser?.membership).toEqual("join");
|
||||
expect(preLeaveUser?.membership).toEqual(KnownMembership.Join);
|
||||
expect(preLeaveUser?.name).toEqual(userA);
|
||||
|
||||
expect(postLeaveUser?.membership).toEqual("leave");
|
||||
expect(postLeaveUser?.membership).toEqual(KnownMembership.Leave);
|
||||
expect(postLeaveUser?.name).toEqual("AliceIsGone");
|
||||
});
|
||||
});
|
||||
@@ -166,7 +168,7 @@ describe("RoomState", function () {
|
||||
it("should return a single MatrixEvent if a state_key was specified", function () {
|
||||
const event = state.getStateEvents("m.room.member", userA);
|
||||
expect(event?.getContent()).toMatchObject({
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -176,13 +178,13 @@ describe("RoomState", function () {
|
||||
const memberEvents = [
|
||||
utils.mkMembership({
|
||||
user: "@cleo:bar",
|
||||
mship: "invite",
|
||||
mship: KnownMembership.Invite,
|
||||
room: roomId,
|
||||
event: true,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: "@daisy:bar",
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
event: true,
|
||||
}),
|
||||
@@ -202,13 +204,13 @@ describe("RoomState", function () {
|
||||
const memberEvents = [
|
||||
utils.mkMembership({
|
||||
user: "@cleo:bar",
|
||||
mship: "invite",
|
||||
mship: KnownMembership.Invite,
|
||||
room: roomId,
|
||||
event: true,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: "@daisy:bar",
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
event: true,
|
||||
}),
|
||||
@@ -228,7 +230,7 @@ describe("RoomState", function () {
|
||||
const events = [
|
||||
utils.mkMembership({
|
||||
user: "@cleo:bar",
|
||||
mship: "invite",
|
||||
mship: KnownMembership.Invite,
|
||||
room: roomId,
|
||||
event: true,
|
||||
}),
|
||||
@@ -282,7 +284,7 @@ describe("RoomState", function () {
|
||||
|
||||
it("should call setPowerLevelEvent on a new RoomMember if power levels exist", function () {
|
||||
const memberEvent = utils.mkMembership({
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
user: userC,
|
||||
room: roomId,
|
||||
event: true,
|
||||
@@ -312,7 +314,7 @@ describe("RoomState", function () {
|
||||
it("should call setMembershipEvent on the right RoomMember", function () {
|
||||
const memberEvent = utils.mkMembership({
|
||||
user: userB,
|
||||
mship: "leave",
|
||||
mship: KnownMembership.Leave,
|
||||
room: roomId,
|
||||
event: true,
|
||||
});
|
||||
@@ -447,7 +449,7 @@ describe("RoomState", function () {
|
||||
it("should add a new member", function () {
|
||||
const oobMemberEvent = utils.mkMembership({
|
||||
user: userLazy,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
event: true,
|
||||
});
|
||||
@@ -462,7 +464,7 @@ describe("RoomState", function () {
|
||||
state.setOutOfBandMembers([
|
||||
utils.mkMembership({
|
||||
user: userLazy,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
event: true,
|
||||
}),
|
||||
@@ -474,7 +476,7 @@ describe("RoomState", function () {
|
||||
const userLazy = "@oob:hs";
|
||||
const oobMemberEvent = utils.mkMembership({
|
||||
user: userLazy,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
event: true,
|
||||
});
|
||||
@@ -491,7 +493,7 @@ describe("RoomState", function () {
|
||||
it("should never overwrite existing members", function () {
|
||||
const oobMemberEvent = utils.mkMembership({
|
||||
user: userA,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
event: true,
|
||||
});
|
||||
@@ -506,7 +508,7 @@ describe("RoomState", function () {
|
||||
const doesntExistYetUserId = "@doesntexistyet:hs";
|
||||
const oobMemberEvent = utils.mkMembership({
|
||||
user: doesntExistYetUserId,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
event: true,
|
||||
});
|
||||
@@ -529,7 +531,7 @@ describe("RoomState", function () {
|
||||
state.setOutOfBandMembers([
|
||||
utils.mkMembership({
|
||||
user: userLazy,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
event: true,
|
||||
}),
|
||||
@@ -554,7 +556,7 @@ describe("RoomState", function () {
|
||||
copy.setOutOfBandMembers([
|
||||
utils.mkMembership({
|
||||
user: userA,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
event: true,
|
||||
}),
|
||||
@@ -563,7 +565,7 @@ describe("RoomState", function () {
|
||||
state.setOutOfBandMembers([
|
||||
utils.mkMembership({
|
||||
user: userLazy,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
event: true,
|
||||
}),
|
||||
@@ -576,7 +578,7 @@ describe("RoomState", function () {
|
||||
copy.setStateEvents([
|
||||
utils.mkMembership({
|
||||
user: userLazy,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
event: true,
|
||||
}),
|
||||
@@ -697,9 +699,13 @@ describe("RoomState", function () {
|
||||
});
|
||||
|
||||
it("should update after adding joined member", function () {
|
||||
state.setStateEvents([utils.mkMembership({ event: true, mship: "join", user: userA, room: roomId })]);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({ event: true, mship: KnownMembership.Join, user: userA, room: roomId }),
|
||||
]);
|
||||
expect(state.getJoinedMemberCount()).toEqual(1);
|
||||
state.setStateEvents([utils.mkMembership({ event: true, mship: "join", user: userC, room: roomId })]);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({ event: true, mship: KnownMembership.Join, user: userC, room: roomId }),
|
||||
]);
|
||||
expect(state.getJoinedMemberCount()).toEqual(2);
|
||||
});
|
||||
});
|
||||
@@ -710,9 +716,13 @@ describe("RoomState", function () {
|
||||
});
|
||||
|
||||
it("should update after adding invited member", function () {
|
||||
state.setStateEvents([utils.mkMembership({ event: true, mship: "invite", user: userA, room: roomId })]);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({ event: true, mship: KnownMembership.Invite, user: userA, room: roomId }),
|
||||
]);
|
||||
expect(state.getInvitedMemberCount()).toEqual(1);
|
||||
state.setStateEvents([utils.mkMembership({ event: true, mship: "invite", user: userC, room: roomId })]);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({ event: true, mship: KnownMembership.Invite, user: userC, room: roomId }),
|
||||
]);
|
||||
expect(state.getInvitedMemberCount()).toEqual(2);
|
||||
});
|
||||
});
|
||||
@@ -723,19 +733,27 @@ describe("RoomState", function () {
|
||||
});
|
||||
|
||||
it("should, once used, override counting members from state", function () {
|
||||
state.setStateEvents([utils.mkMembership({ event: true, mship: "join", user: userA, room: roomId })]);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({ event: true, mship: KnownMembership.Join, user: userA, room: roomId }),
|
||||
]);
|
||||
expect(state.getJoinedMemberCount()).toEqual(1);
|
||||
state.setJoinedMemberCount(100);
|
||||
expect(state.getJoinedMemberCount()).toEqual(100);
|
||||
state.setStateEvents([utils.mkMembership({ event: true, mship: "join", user: userC, room: roomId })]);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({ event: true, mship: KnownMembership.Join, user: userC, room: roomId }),
|
||||
]);
|
||||
expect(state.getJoinedMemberCount()).toEqual(100);
|
||||
});
|
||||
|
||||
it("should, once used, override counting members from state, " + "also after clone", function () {
|
||||
state.setStateEvents([utils.mkMembership({ event: true, mship: "join", user: userA, room: roomId })]);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({ event: true, mship: KnownMembership.Join, user: userA, room: roomId }),
|
||||
]);
|
||||
state.setJoinedMemberCount(100);
|
||||
const copy = state.clone();
|
||||
copy.setStateEvents([utils.mkMembership({ event: true, mship: "join", user: userC, room: roomId })]);
|
||||
copy.setStateEvents([
|
||||
utils.mkMembership({ event: true, mship: KnownMembership.Join, user: userC, room: roomId }),
|
||||
]);
|
||||
expect(state.getJoinedMemberCount()).toEqual(100);
|
||||
});
|
||||
});
|
||||
@@ -746,19 +764,27 @@ describe("RoomState", function () {
|
||||
});
|
||||
|
||||
it("should, once used, override counting members from state", function () {
|
||||
state.setStateEvents([utils.mkMembership({ event: true, mship: "invite", user: userB, room: roomId })]);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({ event: true, mship: KnownMembership.Invite, user: userB, room: roomId }),
|
||||
]);
|
||||
expect(state.getInvitedMemberCount()).toEqual(1);
|
||||
state.setInvitedMemberCount(100);
|
||||
expect(state.getInvitedMemberCount()).toEqual(100);
|
||||
state.setStateEvents([utils.mkMembership({ event: true, mship: "invite", user: userC, room: roomId })]);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({ event: true, mship: KnownMembership.Invite, user: userC, room: roomId }),
|
||||
]);
|
||||
expect(state.getInvitedMemberCount()).toEqual(100);
|
||||
});
|
||||
|
||||
it("should, once used, override counting members from state, " + "also after clone", function () {
|
||||
state.setStateEvents([utils.mkMembership({ event: true, mship: "invite", user: userB, room: roomId })]);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({ event: true, mship: KnownMembership.Invite, user: userB, room: roomId }),
|
||||
]);
|
||||
state.setInvitedMemberCount(100);
|
||||
const copy = state.clone();
|
||||
copy.setStateEvents([utils.mkMembership({ event: true, mship: "invite", user: userC, room: roomId })]);
|
||||
copy.setStateEvents([
|
||||
utils.mkMembership({ event: true, mship: KnownMembership.Invite, user: userC, room: roomId }),
|
||||
]);
|
||||
expect(state.getInvitedMemberCount()).toEqual(100);
|
||||
});
|
||||
});
|
||||
@@ -1015,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");
|
||||
|
||||
|
||||
+470
-347
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,7 @@ import { RoomEncryptor, toRustHistoryVisibility } from "../../../src/rust-crypto
|
||||
import { KeyClaimManager } from "../../../src/rust-crypto/KeyClaimManager";
|
||||
import { defer } from "../../../src/utils";
|
||||
import { OutgoingRequestsManager } from "../../../src/rust-crypto/OutgoingRequestsManager";
|
||||
import { KnownMembership } from "../../../src/@types/membership";
|
||||
|
||||
describe("RoomEncryptor", () => {
|
||||
describe("History Visibility", () => {
|
||||
@@ -54,7 +55,7 @@ describe("RoomEncryptor", () => {
|
||||
|
||||
const mockRoomMember = {
|
||||
userId: "@alice:example.org",
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
} as unknown as Mocked<RoomMember>;
|
||||
|
||||
function createMockEvent(text: string): Mocked<MatrixEvent> {
|
||||
|
||||
@@ -99,6 +99,8 @@ describe("initRustCrypto", () => {
|
||||
isBackupEnabled: jest.fn().mockResolvedValue(false),
|
||||
verifyBackup: jest.fn().mockResolvedValue({ trusted: jest.fn().mockReturnValue(false) }),
|
||||
getBackupKeys: jest.fn(),
|
||||
getIdentity: jest.fn().mockResolvedValue(null),
|
||||
trackedUsers: jest.fn(),
|
||||
} as unknown as Mocked<OlmMachine>;
|
||||
}
|
||||
|
||||
@@ -180,6 +182,7 @@ describe("initRustCrypto", () => {
|
||||
jest.spyOn(Migration, "migrateMegolmSessions").mockResolvedValue(undefined);
|
||||
|
||||
const testOlmMachine = makeTestOlmMachine();
|
||||
testOlmMachine.trackedUsers.mockResolvedValue([]);
|
||||
jest.spyOn(OlmMachine, "initFromStore").mockResolvedValue(testOlmMachine);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { MatrixError } from "../../src/http-api";
|
||||
import { MatrixScheduler } from "../../src/scheduler";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { MatrixEvent } from "../../src";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -336,7 +337,7 @@ describe("MatrixScheduler", function () {
|
||||
utils.mkMembership({
|
||||
user: "@alice:bar",
|
||||
room: roomId,
|
||||
mship: "join",
|
||||
mship: KnownMembership.Join,
|
||||
event: true,
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
} from "../../src/sync-accumulator";
|
||||
import { IRoomSummary } from "../../src";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { KnownMembership, Membership } from "../../src/@types/membership";
|
||||
|
||||
// The event body & unsigned object get frozen to assert that they don't get altered
|
||||
// by the impl
|
||||
@@ -95,7 +96,7 @@ describe("SyncAccumulator", function () {
|
||||
ephemeral: { events: [] },
|
||||
unread_notifications: {},
|
||||
state: {
|
||||
events: [member("alice", "join"), member("bob", "join")],
|
||||
events: [member("alice", KnownMembership.Join), member("bob", KnownMembership.Join)],
|
||||
},
|
||||
summary: {
|
||||
"m.heroes": undefined,
|
||||
@@ -111,7 +112,7 @@ describe("SyncAccumulator", function () {
|
||||
knock: {
|
||||
"!knock": {
|
||||
knock_state: {
|
||||
events: [member("alice", "knock")],
|
||||
events: [member("alice", KnownMembership.Knock)],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -128,7 +129,7 @@ describe("SyncAccumulator", function () {
|
||||
// 10 timeline messages with a prev_batch of "pinned_to_1"
|
||||
sa.accumulate(
|
||||
syncSkeleton({
|
||||
state: { events: [member("alice", "join")] },
|
||||
state: { events: [member("alice", KnownMembership.Join)] },
|
||||
timeline: {
|
||||
events: [
|
||||
msg("alice", "1"),
|
||||
@@ -204,7 +205,7 @@ describe("SyncAccumulator", function () {
|
||||
it("should remove the stored timeline on limited syncs", () => {
|
||||
sa.accumulate(
|
||||
syncSkeleton({
|
||||
state: { events: [member("alice", "join")] },
|
||||
state: { events: [member("alice", KnownMembership.Join)] },
|
||||
timeline: {
|
||||
events: [msg("alice", "1"), msg("alice", "2"), msg("alice", "3")],
|
||||
prev_batch: "pinned_to_1",
|
||||
@@ -312,7 +313,7 @@ describe("SyncAccumulator", function () {
|
||||
events: [
|
||||
{
|
||||
content: {
|
||||
membership: "invite",
|
||||
membership: KnownMembership.Invite,
|
||||
},
|
||||
state_key: "bob",
|
||||
sender: "alice",
|
||||
@@ -336,7 +337,7 @@ describe("SyncAccumulator", function () {
|
||||
const rejectMemberEvent: IStateEvent = {
|
||||
event_id: "$" + Math.random(),
|
||||
content: {
|
||||
membership: "leave",
|
||||
membership: KnownMembership.Leave,
|
||||
},
|
||||
origin_server_ts: 123456789,
|
||||
state_key: "bob",
|
||||
@@ -344,7 +345,7 @@ describe("SyncAccumulator", function () {
|
||||
type: "m.room.member",
|
||||
unsigned: {
|
||||
prev_content: {
|
||||
membership: "invite",
|
||||
membership: KnownMembership.Invite,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -366,7 +367,7 @@ describe("SyncAccumulator", function () {
|
||||
|
||||
it("should accumulate knock state", () => {
|
||||
const initKnockState = {
|
||||
events: [member("alice", "knock")],
|
||||
events: [member("alice", KnownMembership.Knock)],
|
||||
};
|
||||
sa.accumulate(
|
||||
syncSkeleton(
|
||||
@@ -453,7 +454,7 @@ describe("SyncAccumulator", function () {
|
||||
const inviteStateEvents = [
|
||||
{
|
||||
content: {
|
||||
membership: "invite",
|
||||
membership: KnownMembership.Invite,
|
||||
},
|
||||
state_key: "bob",
|
||||
sender: "alice",
|
||||
@@ -482,7 +483,7 @@ describe("SyncAccumulator", function () {
|
||||
const memberEvent: IStateEvent = {
|
||||
event_id: "$" + Math.random(),
|
||||
content: {
|
||||
membership: "leave",
|
||||
membership: KnownMembership.Leave,
|
||||
},
|
||||
origin_server_ts: 123456789,
|
||||
state_key: "bob",
|
||||
@@ -490,7 +491,7 @@ describe("SyncAccumulator", function () {
|
||||
type: "m.room.member",
|
||||
unsigned: {
|
||||
prev_content: {
|
||||
membership: "knock",
|
||||
membership: KnownMembership.Knock,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -528,7 +529,7 @@ describe("SyncAccumulator", function () {
|
||||
const memberEvent: IStateEvent = {
|
||||
event_id: "$" + Math.random(),
|
||||
content: {
|
||||
membership: "leave",
|
||||
membership: KnownMembership.Leave,
|
||||
},
|
||||
origin_server_ts: 123456789,
|
||||
state_key: "bob",
|
||||
@@ -536,7 +537,7 @@ describe("SyncAccumulator", function () {
|
||||
type: "m.room.member",
|
||||
unsigned: {
|
||||
prev_content: {
|
||||
membership: "knock",
|
||||
membership: KnownMembership.Knock,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -933,7 +934,7 @@ function makeKnockState(): IKnockState {
|
||||
name: "Room",
|
||||
},
|
||||
}) as IStrippedState,
|
||||
member("bob", "knock"),
|
||||
member("bob", KnownMembership.Knock),
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -950,7 +951,7 @@ function msg(localpart: string, text: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function member(localpart: string, membership: string) {
|
||||
function member(localpart: string, membership: Membership) {
|
||||
return {
|
||||
event_id: "$" + Math.random(),
|
||||
content: {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -47,6 +47,7 @@ import { flushPromises } from "../../test-utils/flushPromises";
|
||||
import { CallFeedReport } from "../../../src/webrtc/stats/statsReport";
|
||||
import { CallFeedStatsReporter } from "../../../src/webrtc/stats/callFeedStatsReporter";
|
||||
import { StatsReportEmitter } from "../../../src/webrtc/stats/statsReportEmitter";
|
||||
import { KnownMembership } from "../../../src/@types/membership";
|
||||
|
||||
const FAKE_STATE_EVENTS = [
|
||||
{
|
||||
@@ -143,7 +144,7 @@ describe("Group Call", function () {
|
||||
groupCall = new GroupCall(mockClient, room, GroupCallType.Video, false, GroupCallIntent.Prompt);
|
||||
room.currentState.members[FAKE_USER_ID_1] = {
|
||||
userId: FAKE_USER_ID_1,
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
} as unknown as RoomMember;
|
||||
});
|
||||
|
||||
@@ -651,11 +652,11 @@ describe("Group Call", function () {
|
||||
|
||||
client1Room.currentState.members[FAKE_USER_ID_1] = client2Room.currentState.members[FAKE_USER_ID_1] = {
|
||||
userId: FAKE_USER_ID_1,
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
} as unknown as RoomMember;
|
||||
client1Room.currentState.members[FAKE_USER_ID_2] = client2Room.currentState.members[FAKE_USER_ID_2] = {
|
||||
userId: FAKE_USER_ID_2,
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
} as unknown as RoomMember;
|
||||
|
||||
groupCall1 = new GroupCall(
|
||||
@@ -831,11 +832,11 @@ describe("Group Call", function () {
|
||||
room.currentState.getStateEvents = jest.fn().mockImplementation(mockGetStateEvents());
|
||||
room.currentState.members[FAKE_USER_ID_1] = {
|
||||
userId: FAKE_USER_ID_1,
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
} as unknown as RoomMember;
|
||||
room.currentState.members[FAKE_USER_ID_2] = {
|
||||
userId: FAKE_USER_ID_2,
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
} as unknown as RoomMember;
|
||||
});
|
||||
|
||||
@@ -1026,11 +1027,11 @@ describe("Group Call", function () {
|
||||
room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_2);
|
||||
room.currentState.members[FAKE_USER_ID_1] = {
|
||||
userId: FAKE_USER_ID_1,
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
} as unknown as RoomMember;
|
||||
room.currentState.members[FAKE_USER_ID_2] = {
|
||||
userId: FAKE_USER_ID_2,
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
} as unknown as RoomMember;
|
||||
|
||||
groupCall = await createAndEnterGroupCall(mockClient, room);
|
||||
@@ -1260,11 +1261,11 @@ describe("Group Call", function () {
|
||||
room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_1);
|
||||
room.currentState.members[FAKE_USER_ID_1] = {
|
||||
userId: FAKE_USER_ID_1,
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
} as unknown as RoomMember;
|
||||
room.currentState.members[FAKE_USER_ID_2] = {
|
||||
userId: FAKE_USER_ID_2,
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
} as unknown as RoomMember;
|
||||
room.currentState.getStateEvents = jest.fn().mockImplementation(mockGetStateEvents());
|
||||
|
||||
@@ -1741,11 +1742,11 @@ describe("Group Call", function () {
|
||||
const room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_1);
|
||||
room.currentState.members[FAKE_USER_ID_1] = {
|
||||
userId: FAKE_USER_ID_1,
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
} as unknown as RoomMember;
|
||||
room.currentState.members[FAKE_USER_ID_2] = {
|
||||
userId: FAKE_USER_ID_2,
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
} as unknown as RoomMember;
|
||||
room.currentState.getStateEvents = jest.fn().mockImplementation(mockGetStateEvents());
|
||||
groupCall = await createAndEnterGroupCall(mockClient, room);
|
||||
|
||||
@@ -31,6 +31,7 @@ import { RoomState } from "../../../src/models/room-state";
|
||||
import { GroupCallEventHandler, GroupCallEventHandlerEvent } from "../../../src/webrtc/groupCallEventHandler";
|
||||
import { flushPromises } from "../../test-utils/flushPromises";
|
||||
import { makeMockGroupCallStateEvent, MockCallMatrixClient } from "../../test-utils/webrtc";
|
||||
import { KnownMembership } from "../../../src/@types/membership";
|
||||
|
||||
const FAKE_USER_ID = "@alice:test.dummy";
|
||||
const FAKE_DEVICE_ID = "AAAAAAA";
|
||||
@@ -50,7 +51,7 @@ describe("Group Call Event Handler", function () {
|
||||
|
||||
mockMember = {
|
||||
userId: FAKE_USER_ID,
|
||||
membership: "join",
|
||||
membership: KnownMembership.Join,
|
||||
} as unknown as RoomMember;
|
||||
|
||||
const mockEvent = makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
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 } from "../@types/event";
|
||||
|
||||
/**
|
||||
* Information on encrypted media attachments.
|
||||
*
|
||||
* Used within `m.room.message` events that reference files, such as `m.file` and `m.image`.
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#extensions-to-mroommessage-msgtypes
|
||||
*/
|
||||
export interface EncryptedFile {
|
||||
/**
|
||||
* The URL to the file.
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* A JSON Web Key object.
|
||||
*/
|
||||
key: {
|
||||
alg: string;
|
||||
key_ops: string[]; // eslint-disable-line camelcase
|
||||
kty: string;
|
||||
k: string;
|
||||
ext: boolean;
|
||||
};
|
||||
/**
|
||||
* The 128-bit unique counter block used by AES-CTR, encoded as unpadded base64.
|
||||
*/
|
||||
iv: string;
|
||||
/**
|
||||
* A map from an algorithm name to a hash of the ciphertext, encoded as unpadded base64.
|
||||
* Clients should support the SHA-256 hash, which uses the key `sha256`.
|
||||
*/
|
||||
hashes: { [alg: string]: string };
|
||||
/**
|
||||
* Version of the encrypted attachment's protocol. Must be `v2`.
|
||||
*/
|
||||
v: string;
|
||||
}
|
||||
|
||||
interface ThumbnailInfo {
|
||||
/**
|
||||
* The mimetype of the image, e.g. image/jpeg.
|
||||
*/
|
||||
mimetype?: string;
|
||||
/**
|
||||
* The intended display width of the image in pixels.
|
||||
* This may differ from the intrinsic dimensions of the image file.
|
||||
*/
|
||||
w?: number;
|
||||
/**
|
||||
* The intended display height of the image in pixels.
|
||||
* This may differ from the intrinsic dimensions of the image file.
|
||||
*/
|
||||
h?: number;
|
||||
/**
|
||||
* Size of the image in bytes.
|
||||
*/
|
||||
size?: number;
|
||||
}
|
||||
|
||||
interface BaseInfo {
|
||||
mimetype?: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Information on media attachments of msgtype `m.file`
|
||||
*
|
||||
* Used within `m.room.message` events that reference files.
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#mfile
|
||||
*/
|
||||
export interface FileInfo extends BaseInfo {
|
||||
/**
|
||||
* Information on the encrypted thumbnail file, as specified in End-to-end encryption.
|
||||
* Only present if the thumbnail is encrypted.
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#sending-encrypted-attachments
|
||||
*/
|
||||
thumbnail_file?: EncryptedFile;
|
||||
/**
|
||||
* Metadata about the image referred to in thumbnail_url.
|
||||
*/
|
||||
thumbnail_info?: ThumbnailInfo;
|
||||
/**
|
||||
* The URL to the thumbnail of the file. Only present if the thumbnail is unencrypted.
|
||||
*/
|
||||
thumbnail_url?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Information on media attachments of msgtype `m.image`
|
||||
*
|
||||
* Used within `m.room.message` events that reference images.
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#mimage
|
||||
*/
|
||||
export interface ImageInfo extends FileInfo, ThumbnailInfo {}
|
||||
|
||||
/**
|
||||
* Information on media attachments of msgtype `m.audio`
|
||||
*
|
||||
* Used within `m.room.message` events that reference audio files.
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#maudio
|
||||
*/
|
||||
export interface AudioInfo extends BaseInfo {
|
||||
/**
|
||||
* The duration of the audio in milliseconds.
|
||||
*/
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Information on media attachments of msgtype `m.video`
|
||||
*
|
||||
* Used within `m.room.message` events that reference video files.
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#mvideo
|
||||
*/
|
||||
export interface VideoInfo extends AudioInfo, ImageInfo {
|
||||
/**
|
||||
* The duration of the video in milliseconds.
|
||||
*/
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Union type representing the `content.info` field of all specified media events.
|
||||
*/
|
||||
export type MediaEventInfo = FileInfo | ImageInfo | AudioInfo | VideoInfo;
|
||||
|
||||
interface BaseContent {
|
||||
/**
|
||||
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#sending-encrypted-attachments
|
||||
*/
|
||||
file?: EncryptedFile;
|
||||
/**
|
||||
* Required if the file is unencrypted. The URL (typically mxc:// URI) to the file.
|
||||
*/
|
||||
url?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content format of media events with msgtype `m.file`
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#mfile
|
||||
*/
|
||||
export interface FileContent extends BaseContent {
|
||||
/**
|
||||
* A human-readable description of the file.
|
||||
* This is recommended to be the filename of the original upload.
|
||||
*/
|
||||
body: string;
|
||||
/**
|
||||
* The original filename of the uploaded file.
|
||||
*/
|
||||
filename?: string;
|
||||
/**
|
||||
* Information about the file referred to in url.
|
||||
*/
|
||||
info?: FileInfo;
|
||||
/**
|
||||
* One of: [m.file].
|
||||
*/
|
||||
msgtype: MsgType.File;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content format of media events with msgtype `m.image`
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#mimage
|
||||
*/
|
||||
export interface ImageContent extends BaseContent {
|
||||
/**
|
||||
* A textual representation of the image.
|
||||
* This could be the alt text of the image, the filename of the image,
|
||||
* or some kind of content description for accessibility e.g. ‘image attachment’.
|
||||
*/
|
||||
body: string;
|
||||
/**
|
||||
* Metadata about the image referred to in url.
|
||||
*/
|
||||
info?: ImageInfo;
|
||||
/**
|
||||
* One of: [m.image].
|
||||
*/
|
||||
msgtype: MsgType.Image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content format of media events with msgtype `m.audio`
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#maudio
|
||||
*/
|
||||
export interface AudioContent extends BaseContent {
|
||||
/**
|
||||
* A description of the audio e.g. ‘Bee Gees - Stayin’ Alive’,
|
||||
* or some kind of content description for accessibility e.g. ‘audio attachment’.
|
||||
*/
|
||||
body: string;
|
||||
/**
|
||||
* Metadata for the audio clip referred to in url.
|
||||
*/
|
||||
info?: AudioInfo;
|
||||
/**
|
||||
* One of: [m.audio].
|
||||
*/
|
||||
msgtype: MsgType.Audio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content format of media events with msgtype `m.video`
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#mvideo
|
||||
*/
|
||||
export interface VideoContent extends BaseContent {
|
||||
/**
|
||||
* A description of the video e.g. ‘Gangnam style’,
|
||||
* or some kind of content description for accessibility e.g. ‘video attachment’.
|
||||
*/
|
||||
body: string;
|
||||
/**
|
||||
* Metadata about the video clip referred to in url.
|
||||
*/
|
||||
info?: VideoInfo;
|
||||
/**
|
||||
* One of: [m.video].
|
||||
*/
|
||||
msgtype: MsgType.Video;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type representing media event contents for `m.room.message` events listed in the Matrix specification
|
||||
*/
|
||||
export type MediaEventContent = FileContent | ImageContent | AudioContent | VideoContent;
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Well-known values (from the spec or MSCs) that are allowed in the
|
||||
* {@link Membership} type.
|
||||
*/
|
||||
export enum KnownMembership {
|
||||
/**
|
||||
* The user has been banned from the room, and is no longer allowed to join
|
||||
* it until they are un-banned from the room (by having their membership
|
||||
* state set to a value other than ban).
|
||||
*/
|
||||
Ban = "ban",
|
||||
/**
|
||||
* The user has been invited to join a room, but has not yet joined it.
|
||||
* They may not participate in the room until they join.
|
||||
* */
|
||||
Invite = "invite",
|
||||
/**
|
||||
* The user has joined the room (possibly after accepting an invite), and
|
||||
* may participate in it.
|
||||
*/
|
||||
Join = "join",
|
||||
/**
|
||||
* The user has knocked on the room, requesting permission to participate.
|
||||
* They may not participate in the room until they join.
|
||||
*/
|
||||
Knock = "knock",
|
||||
/**
|
||||
* The user was once joined to the room, but has since left (possibly by
|
||||
* choice, or possibly by being kicked).
|
||||
*/
|
||||
Leave = "leave",
|
||||
}
|
||||
|
||||
/**
|
||||
* The membership state for a user in a room [1]. A value from
|
||||
* {@link KnownMembership} should be used where available, but all string values
|
||||
* are allowed to provide flexibility for upcoming spec changes or proposals.
|
||||
*
|
||||
* [1] https://spec.matrix.org/latest/client-server-api/#mroommember
|
||||
*/
|
||||
export type Membership = KnownMembership | string;
|
||||
@@ -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",
|
||||
|
||||
+11
-1
@@ -162,11 +162,21 @@ export interface ICreateRoomOpts {
|
||||
}
|
||||
|
||||
export interface IRoomDirectoryOptions {
|
||||
/**
|
||||
* The remote server to query for the room list.
|
||||
* Optional. If unspecified, get the local homeserver's public room list.
|
||||
*/
|
||||
server?: string;
|
||||
/**
|
||||
* Maximum number of entries to return
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* Token to paginate from
|
||||
*/
|
||||
since?: string;
|
||||
|
||||
// Filter parameters
|
||||
/** Filter parameters */
|
||||
filter?: {
|
||||
// String to search for
|
||||
generic_search_term?: string;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+88
-164
@@ -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";
|
||||
@@ -222,6 +223,9 @@ import {
|
||||
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;
|
||||
|
||||
@@ -368,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.
|
||||
@@ -498,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
|
||||
@@ -721,7 +722,7 @@ export interface IOpenIDToken {
|
||||
|
||||
interface IRoomInitialSyncResponse {
|
||||
room_id: string;
|
||||
membership: "invite" | "join" | "leave" | "ban";
|
||||
membership: Membership;
|
||||
messages?: {
|
||||
start?: string;
|
||||
end?: string;
|
||||
@@ -866,7 +867,7 @@ interface IThirdPartyUser {
|
||||
|
||||
interface IRoomSummary extends Omit<IPublicRoomsChunkRoom, "canonical_alias" | "aliases"> {
|
||||
room_type?: RoomType;
|
||||
membership?: string;
|
||||
membership?: Membership;
|
||||
is_encrypted: boolean;
|
||||
}
|
||||
|
||||
@@ -1261,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"];
|
||||
@@ -1436,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 ?? {});
|
||||
|
||||
@@ -1574,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) {
|
||||
@@ -2816,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");
|
||||
}
|
||||
@@ -3286,7 +3221,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* Encrypts and sends a given object via Olm to-device messages to a given
|
||||
* set of devices.
|
||||
*
|
||||
* @param userDeviceMap - mapping from userId to deviceInfo
|
||||
* @param userDeviceInfoArr - list of deviceInfo objects representing the devices to send to
|
||||
*
|
||||
* @param payload - fields to include in the encrypted payload
|
||||
*
|
||||
@@ -4357,7 +4292,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
const room = this.getRoom(roomIdOrAlias);
|
||||
if (room?.hasMembershipState(this.credentials.userId!, "join")) return room;
|
||||
if (room?.hasMembershipState(this.credentials.userId!, KnownMembership.Join)) return room;
|
||||
|
||||
let signPromise: Promise<IThirdPartySigned | void> = Promise.resolve();
|
||||
|
||||
@@ -4386,7 +4321,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
// with the resolved ID - this method is supposed to no-op if we already
|
||||
// were in the room, after all.
|
||||
const resolvedRoom = this.getRoom(roomId);
|
||||
if (resolvedRoom?.hasMembershipState(this.credentials.userId!, "join")) return resolvedRoom;
|
||||
if (resolvedRoom?.hasMembershipState(this.credentials.userId!, KnownMembership.Join)) return resolvedRoom;
|
||||
|
||||
const syncApi = new SyncApi(this, this.clientOpts, this.buildSyncApiOptions());
|
||||
const syncRoom = syncApi.createRoom(roomId);
|
||||
@@ -4406,7 +4341,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*/
|
||||
public knockRoom(roomIdOrAlias: string, opts: KnockRoomOpts = {}): Promise<{ room_id: string }> {
|
||||
const room = this.getRoom(roomIdOrAlias);
|
||||
if (room?.hasMembershipState(this.credentials.userId!, "knock")) {
|
||||
if (room?.hasMembershipState(this.credentials.userId!, KnownMembership.Knock)) {
|
||||
return Promise.resolve({ room_id: room.roomId });
|
||||
}
|
||||
|
||||
@@ -4548,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.
|
||||
*/
|
||||
@@ -4556,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()) {
|
||||
@@ -4623,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(
|
||||
@@ -5015,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);
|
||||
}
|
||||
@@ -5125,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);
|
||||
}
|
||||
|
||||
@@ -5162,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);
|
||||
}
|
||||
@@ -5548,7 +5483,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public invite(roomId: string, userId: string, reason?: string): Promise<{}> {
|
||||
return this.membershipChange(roomId, userId, "invite", reason);
|
||||
return this.membershipChange(roomId, userId, KnownMembership.Invite, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -5603,7 +5538,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public leave(roomId: string): Promise<{}> {
|
||||
return this.membershipChange(roomId, undefined, "leave");
|
||||
return this.membershipChange(roomId, undefined, KnownMembership.Leave);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -5661,7 +5596,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public ban(roomId: string, userId: string, reason?: string): Promise<{}> {
|
||||
return this.membershipChange(roomId, userId, "ban", reason);
|
||||
return this.membershipChange(roomId, userId, KnownMembership.Ban, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -5720,7 +5655,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
private membershipChange(
|
||||
roomId: string,
|
||||
userId: string | undefined,
|
||||
membership: string,
|
||||
membership: Membership | "forget",
|
||||
reason?: string,
|
||||
): Promise<{}> {
|
||||
// API returns an empty object
|
||||
@@ -6565,8 +6500,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
const timelineSet = eventTimeline.getTimelineSet();
|
||||
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, newToken ?? null);
|
||||
if (!newToken && backwards) {
|
||||
const originalEvent = await this.fetchRoomEvent(eventTimeline.getRoomId() ?? "", thread.id);
|
||||
timelineSet.addEventsToTimeline([mapper(originalEvent)], true, eventTimeline, null);
|
||||
const originalEvent =
|
||||
thread.rootEvent ??
|
||||
mapper(await this.fetchRoomEvent(eventTimeline.getRoomId() ?? "", thread.id));
|
||||
timelineSet.addEventsToTimeline([originalEvent], true, eventTimeline, null);
|
||||
}
|
||||
this.processAggregatedTimelineEvents(timelineSet.room, matrixEvents);
|
||||
|
||||
@@ -6701,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,
|
||||
},
|
||||
"",
|
||||
);
|
||||
@@ -6712,7 +6649,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
roomId,
|
||||
EventType.RoomHistoryVisibility,
|
||||
{
|
||||
history_visibility: "world_readable",
|
||||
history_visibility: HistoryVisibility.WorldReadable,
|
||||
},
|
||||
"",
|
||||
);
|
||||
@@ -7498,7 +7435,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* store client options with boolean/string/numeric values
|
||||
* to know in the next session what flags the sync data was
|
||||
* created with (e.g. lazy loading)
|
||||
* @param opts - the complete set of client options
|
||||
* @returns for store operation
|
||||
*/
|
||||
public storeClientOptions(): Promise<void> {
|
||||
@@ -8384,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> {
|
||||
@@ -8404,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -8480,12 +8421,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
/**
|
||||
* @param options - Options for this request
|
||||
* @param server - The remote server to query for the room list.
|
||||
* Optional. If unspecified, get the local home
|
||||
* server's public room list.
|
||||
* @param limit - Maximum number of entries to return
|
||||
* @param since - Token to paginate from
|
||||
* @param params - Options for this request
|
||||
* @returns Promise which resolves: IPublicRoomsResponse
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
@@ -8495,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8561,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
|
||||
@@ -8585,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.
|
||||
@@ -8601,9 +8532,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
/**
|
||||
* Query the user directory with a term matching user IDs, display names and domains.
|
||||
* @param term - the term with which to search.
|
||||
* @param limit - the maximum number of results to return. The server will
|
||||
* apply a limit if unspecified.
|
||||
* @param options
|
||||
* @param options.term - the term with which to search.
|
||||
* @param options.limit - the maximum number of results to return. The server will apply a limit if unspecified.
|
||||
* @returns Promise which resolves: an array of results.
|
||||
*/
|
||||
public searchUserDirectory({ term, limit }: { term: string; limit?: number }): Promise<IUserDirectoryResponse> {
|
||||
@@ -9008,8 +8939,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
/**
|
||||
* Perform a server-side search.
|
||||
* @param next_batch - the batch token to pass in the query string
|
||||
* @param body - the JSON object to pass to the request body.
|
||||
* @param params
|
||||
* @param params.next_batch - the batch token to pass in the query string
|
||||
* @param params.body - the JSON object to pass to the request body.
|
||||
* @param abortSignal - optional signal used to cancel the http request.
|
||||
* @returns Promise which resolves to the search response object.
|
||||
* @returns Rejects: with an error response.
|
||||
@@ -9783,7 +9715,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*/
|
||||
public unstableGetFileTreeSpace(roomId: string): MSC3089TreeSpace | null {
|
||||
const room = this.getRoom(roomId);
|
||||
if (room?.getMyMembership() !== "join") return null;
|
||||
if (room?.getMyMembership() !== KnownMembership.Join) return null;
|
||||
|
||||
const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, "");
|
||||
const purposeEvent = room.currentState.getStateEvents(
|
||||
@@ -9831,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`.
|
||||
*/
|
||||
|
||||
+14
-10
@@ -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.
|
||||
@@ -55,7 +56,14 @@ function checkPayloadLength(payloadString: string): void {
|
||||
}
|
||||
|
||||
interface IInitOpts {
|
||||
/**
|
||||
* (Optional) data from exported device that must be re-created.
|
||||
* If present, opts.pickleKey is ignored (exported data already provides a pickle key)
|
||||
*/
|
||||
fromExportedDevice?: IExportedDevice;
|
||||
/**
|
||||
* (Optional) pickle key to set instead of default one
|
||||
*/
|
||||
pickleKey?: string;
|
||||
}
|
||||
|
||||
@@ -174,11 +182,7 @@ export class OlmDevice {
|
||||
*
|
||||
* Reads the device keys from the OlmAccount object.
|
||||
*
|
||||
* @param fromExportedDevice - (Optional) data from exported device
|
||||
* that must be re-created.
|
||||
* If present, opts.pickleKey is ignored
|
||||
* (exported data already provides a pickle key)
|
||||
* @param pickleKey - (Optional) pickle key to set instead of default one
|
||||
* @param IInitOpts - opts to initialise the OlmAccount with
|
||||
*/
|
||||
public async init({ pickleKey, fromExportedDevice }: IInitOpts = {}): Promise<void> {
|
||||
let e2eKeys;
|
||||
@@ -1217,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,
|
||||
@@ -1233,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,
|
||||
@@ -44,6 +43,9 @@ import { RoomKeyRequestState } from "../OutgoingRoomKeyRequestManager";
|
||||
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 {
|
||||
@@ -1312,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.
|
||||
@@ -1338,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", {
|
||||
@@ -1376,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,
|
||||
@@ -1404,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 {
|
||||
@@ -1680,7 +1685,7 @@ export class MegolmDecryption extends DecryptionAlgorithm {
|
||||
const fromInviter =
|
||||
memberEvent?.getSender() === senderKeyUser ||
|
||||
(memberEvent?.getUnsigned()?.prev_sender === senderKeyUser &&
|
||||
memberEvent?.getPrevContent()?.membership === "invite");
|
||||
memberEvent?.getPrevContent()?.membership === KnownMembership.Invite);
|
||||
|
||||
if (room && fromInviter) {
|
||||
return true;
|
||||
|
||||
@@ -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 || {};
|
||||
|
||||
+9
-32
@@ -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,13 +98,14 @@ 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";
|
||||
import { ClientPrefix, MatrixError, Method } from "../http-api";
|
||||
import { decodeBase64, encodeBase64 } from "../base64";
|
||||
import { KnownMembership } from "../@types/membership";
|
||||
|
||||
/* re-exports for backwards compatibility */
|
||||
export type {
|
||||
@@ -473,8 +475,6 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
*
|
||||
* @param cryptoStore - storage for the crypto layer.
|
||||
*
|
||||
* @param roomList - An initialised RoomList object
|
||||
*
|
||||
* @param verificationMethods - Array of verification methods to use.
|
||||
* Each element can either be a string from MatrixClient.verificationMethods
|
||||
* or a class that implements a verification method.
|
||||
@@ -842,15 +842,6 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
* secret storage (if it has been setup)
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @param authUploadDeviceSigningKeys - Function
|
||||
* called to await an interactive auth flow when uploading device signing keys.
|
||||
* @param setupNewCrossSigning - Optional. Reset even if keys
|
||||
* already exist.
|
||||
* Args:
|
||||
* A function that makes the request requiring auth. Receives the
|
||||
* auth data as an object. Can be called multiple times, first with an empty
|
||||
* authDict, to obtain the flows.
|
||||
*/
|
||||
public async bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys,
|
||||
@@ -964,20 +955,6 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @param createSecretStorageKey - Optional. Function
|
||||
* called to await a secret storage key creation flow.
|
||||
* Returns a Promise which resolves to an object with public key metadata, encoded private
|
||||
* recovery key which should be disposed of after displaying to the user,
|
||||
* and raw private key to avoid round tripping if needed.
|
||||
* @param keyBackupInfo - The current key backup object. If passed,
|
||||
* the passphrase and recovery key from this backup will be used.
|
||||
* @param setupNewKeyBackup - If true, a new key backup version will be
|
||||
* created and the private key stored in the new SSSS store. Ignored if keyBackupInfo
|
||||
* is supplied.
|
||||
* @param setupNewSecretStorage - Optional. Reset even if keys already exist.
|
||||
* @param getKeyBackupPassphrase - Optional. Function called to get the user's
|
||||
* current key backup passphrase. Should return a promise that resolves with a Buffer
|
||||
* containing the key, or rejects if the key cannot be obtained.
|
||||
* Returns:
|
||||
* A promise which resolves to key creation data for
|
||||
* SecretStorage#addKey: an object with `passphrase` etc fields.
|
||||
@@ -3529,7 +3506,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
|
||||
// ignore any rooms which we have left
|
||||
const myMembership = room.getMyMembership();
|
||||
return myMembership === "join" || myMembership === "invite";
|
||||
return myMembership === KnownMembership.Join || myMembership === KnownMembership.Invite;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4012,12 +3989,12 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
// the result of anyway, as we'll need to do a query again once all the members are fetched
|
||||
// by calling _trackRoomDevices
|
||||
if (roomId in this.roomDeviceTrackingState) {
|
||||
if (member.membership == "join") {
|
||||
if (member.membership == KnownMembership.Join) {
|
||||
logger.log("Join event for " + member.userId + " in " + roomId);
|
||||
// make sure we are tracking the deviceList for this user
|
||||
this.deviceList.startTrackingDeviceList(member.userId);
|
||||
} else if (
|
||||
member.membership == "invite" &&
|
||||
member.membership == KnownMembership.Invite &&
|
||||
this.clientStore.getRoom(roomId)?.shouldEncryptForInvitedMembers()
|
||||
) {
|
||||
logger.log("Invite event for " + member.userId + " in " + roomId);
|
||||
@@ -4233,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;
|
||||
}
|
||||
|
||||
@@ -495,8 +495,9 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
|
||||
/**
|
||||
* Cancels the request, sending a cancellation to the other party
|
||||
* @param reason - the error reason to send the cancellation with
|
||||
* @param code - the error code to send the cancellation with
|
||||
* @param params
|
||||
* @param params.reason - the error reason to send the cancellation with
|
||||
* @param params.code - the error code to send the cancellation with
|
||||
* @returns resolves when the event has been sent.
|
||||
*/
|
||||
public async cancel({ reason = "User declined", code = "m.user" } = {}): Promise<void> {
|
||||
|
||||
+12
-1
@@ -108,11 +108,22 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
private lifecycle?: AbortController;
|
||||
private syncState: SyncState | null = null;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param widgetApi - The widget api to use for communication.
|
||||
* @param capabilities - The capabilities the widget client will request.
|
||||
* @param roomId - The room id the widget is associated with.
|
||||
* @param opts - The configuration options for this client.
|
||||
* @param sendContentLoaded - Whether to send a content loaded widget action immediately after initial setup.
|
||||
* Set to `false` if the widget uses `waitForIFrameLoad=true` (in this case the client does not expect a content loaded action at all),
|
||||
* or if the the widget wants to send the `ContentLoaded` action at a later point in time after the initial setup.
|
||||
*/
|
||||
public constructor(
|
||||
private readonly widgetApi: WidgetApi,
|
||||
private readonly capabilities: ICapabilities,
|
||||
private readonly roomId: string,
|
||||
opts: IMatrixClientCreateOpts,
|
||||
sendContentLoaded: boolean,
|
||||
) {
|
||||
super(opts);
|
||||
|
||||
@@ -165,7 +176,7 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
// does *not* (yes, that is the right way around) wait for this event. Let's
|
||||
// start sending this, then once this has rolled out, we can change element-web to
|
||||
// use waitForIFrameLoad=false and have a widget API that's less racy.
|
||||
widgetApi.sendContentLoaded();
|
||||
if (sendContentLoaded) widgetApi.sendContentLoaded();
|
||||
}
|
||||
|
||||
public async startClient(opts: IStartClientOpts = {}): Promise<void> {
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -111,8 +111,8 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
*
|
||||
* @param body - The HTTP JSON body.
|
||||
*
|
||||
* @param opts - additional options.
|
||||
* When `opts.doNotAttemptTokenRefresh` is true, token refresh will not be attempted
|
||||
* @param paramOpts - additional options.
|
||||
* When `paramOpts.doNotAttemptTokenRefresh` is true, token refresh will not be attempted
|
||||
* when an expired token is encountered. Used to only attempt token refresh once.
|
||||
*
|
||||
* @returns Promise which resolves to
|
||||
|
||||
+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);
|
||||
|
||||
+16
-1
@@ -164,11 +164,26 @@ export function createClient(opts: ICreateClientOpts): MatrixClient {
|
||||
return new MatrixClient(amendClientOpts(opts));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a Matrix Client that works in a widget.
|
||||
* This client has a subset of features compared to a full client.
|
||||
* It uses the widget-api to communicate with matrix. (widget \<-\> client \<-\> homeserver)
|
||||
* @returns A new matrix client with a subset of features.
|
||||
* @param opts - The configuration options for this client. These configuration
|
||||
* options will be passed directly to {@link MatrixClient}.
|
||||
* @param widgetApi - The widget api to use for communication.
|
||||
* @param capabilities - The capabilities the widget client will request.
|
||||
* @param roomId - The room id the widget is associated with.
|
||||
* @param sendContentLoaded - Whether to send a content loaded widget action immediately after initial setup.
|
||||
* Set to `false` if the widget uses `waitForIFrameLoad=true` (in this case the client does not expect a content loaded action at all),
|
||||
* or if the the widget wants to send the `ContentLoaded` action at a later point in time after the initial setup.
|
||||
*/
|
||||
export function createRoomWidgetClient(
|
||||
widgetApi: WidgetApi,
|
||||
capabilities: ICapabilities,
|
||||
roomId: string,
|
||||
opts: ICreateClientOpts,
|
||||
sendContentLoaded = true,
|
||||
): MatrixClient {
|
||||
return new RoomWidgetClient(widgetApi, capabilities, roomId, amendClientOpts(opts));
|
||||
return new RoomWidgetClient(widgetApi, capabilities, roomId, amendClientOpts(opts), sendContentLoaded);
|
||||
}
|
||||
|
||||
@@ -23,10 +23,12 @@ import { EventType } from "../@types/event";
|
||||
import { CallMembership, CallMembershipData } from "./CallMembership";
|
||||
import { RoomStateEvent } from "../models/room-state";
|
||||
import { Focus } from "./focus";
|
||||
import { MatrixError, MatrixEvent } from "../matrix";
|
||||
import { randomString, secureRandomBase64Url } from "../randomstring";
|
||||
import { EncryptionKeysEventContent } from "./types";
|
||||
import { decodeBase64, encodeUnpaddedBase64 } from "../base64";
|
||||
import { KnownMembership } from "../@types/membership";
|
||||
import { MatrixError } from "../http-api/errors";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
|
||||
const MEMBERSHIP_EXPIRY_TIME = 60 * 60 * 1000;
|
||||
const MEMBER_EVENT_CHECK_PERIOD = 2 * 60 * 1000; // How often we check to see if we need to re-send our member event
|
||||
@@ -156,7 +158,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
logger.info(`Ignoring expired device membership ${membership.sender}/${membership.deviceId}`);
|
||||
continue;
|
||||
}
|
||||
if (!room.hasMembershipState(membership.sender ?? "", "join")) {
|
||||
if (!room.hasMembershipState(membership.sender ?? "", KnownMembership.Join)) {
|
||||
logger.info(`Ignoring membership of user ${membership.sender} who is not in the room.`);
|
||||
continue;
|
||||
}
|
||||
@@ -610,7 +612,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
|
||||
/**
|
||||
* Constructs our own membership
|
||||
* @param prevEvent - The previous version of our call membership, if any
|
||||
* @param prevMembership - The previous value of our call membership, if any
|
||||
*/
|
||||
private makeMyMembership(prevMembership?: CallMembership): CallMembershipData {
|
||||
if (this.relativeExpiry === undefined) {
|
||||
|
||||
@@ -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";
|
||||
@@ -33,6 +33,9 @@ import { MSC3089Branch } from "./MSC3089Branch";
|
||||
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
|
||||
@@ -77,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
|
||||
@@ -175,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;
|
||||
@@ -233,7 +242,7 @@ export class MSC3089TreeSpace {
|
||||
this.roomId,
|
||||
EventType.SpaceChild,
|
||||
{
|
||||
via: [this.client.getDomain()],
|
||||
via: [this.client.getDomain()!],
|
||||
},
|
||||
directory.roomId,
|
||||
);
|
||||
@@ -242,7 +251,7 @@ export class MSC3089TreeSpace {
|
||||
directory.roomId,
|
||||
EventType.SpaceParent,
|
||||
{
|
||||
via: [this.client.getDomain()],
|
||||
via: [this.client.getDomain()!],
|
||||
},
|
||||
this.roomId,
|
||||
);
|
||||
@@ -291,11 +300,11 @@ export class MSC3089TreeSpace {
|
||||
await dir.delete();
|
||||
}
|
||||
|
||||
const kickMemberships = ["invite", "knock", "join"];
|
||||
const kickMemberships = [KnownMembership.Invite, KnownMembership.Knock, KnownMembership.Join];
|
||||
const members = this.room.currentState.getStateEvents(EventType.RoomMember);
|
||||
for (const member of members) {
|
||||
const isNotUs = member.getStateKey() !== this.client.getUserId();
|
||||
if (isNotUs && kickMemberships.includes(member.getContent().membership!)) {
|
||||
if (isNotUs && kickMemberships.includes(member.getContent().membership! as KnownMembership)) {
|
||||
const stateKey = member.getStateKey();
|
||||
if (!stateKey) {
|
||||
throw new Error("State key not found for branch");
|
||||
@@ -449,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,
|
||||
@@ -472,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,
|
||||
@@ -498,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, {
|
||||
@@ -506,7 +517,7 @@ export class MSC3089TreeSpace {
|
||||
});
|
||||
info.url = mxc;
|
||||
|
||||
const fileContent = {
|
||||
const fileContent: FileContent = {
|
||||
msgtype: MsgType.File,
|
||||
body: name,
|
||||
url: mxc,
|
||||
@@ -525,7 +536,7 @@ export class MSC3089TreeSpace {
|
||||
...additionalContent,
|
||||
...fileContent,
|
||||
[UNSTABLE_MSC3089_LEAF.name]: {},
|
||||
});
|
||||
} as FileContent);
|
||||
|
||||
await this.client.sendStateEvent(
|
||||
this.roomId,
|
||||
|
||||
@@ -159,7 +159,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
* @param client - the Matrix client which owns this EventTimelineSet,
|
||||
* can be omitted if room is specified.
|
||||
* @param thread - the thread to which this timeline set relates.
|
||||
* @param isThreadTimeline - Whether this timeline set relates to a thread list timeline
|
||||
* @param threadListType - the type of thread list represented, if any
|
||||
* (e.g., All threads or My threads)
|
||||
*/
|
||||
public constructor(
|
||||
@@ -674,6 +674,8 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
*
|
||||
* Will fire "Room.timeline" for each event added.
|
||||
*
|
||||
* @param event - the event to add
|
||||
* @param timeline - the timeline onto which to add it
|
||||
* @param options - addEventToTimeline options
|
||||
*
|
||||
* @remarks
|
||||
@@ -771,8 +773,6 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @param options - addEventToTimeline options
|
||||
*
|
||||
* @remarks
|
||||
* Fires {@link RoomEvent.Timeline}
|
||||
*/
|
||||
|
||||
+58
-71
@@ -41,12 +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";
|
||||
|
||||
@@ -54,7 +55,7 @@ export { EventStatus } from "./event-status";
|
||||
export interface IContent {
|
||||
[key: string]: any;
|
||||
"msgtype"?: MsgType | string;
|
||||
"membership"?: string;
|
||||
"membership"?: Membership;
|
||||
"avatar_url"?: string;
|
||||
"displayname"?: string;
|
||||
"m.relates_to"?: IEventRelation;
|
||||
@@ -92,7 +93,7 @@ export interface IEvent {
|
||||
origin_server_ts: number;
|
||||
txn_id?: string;
|
||||
state_key?: string;
|
||||
membership?: string;
|
||||
membership?: Membership;
|
||||
unsigned: IUnsigned;
|
||||
redacts?: string;
|
||||
|
||||
@@ -226,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",
|
||||
@@ -238,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;
|
||||
@@ -273,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()
|
||||
*/
|
||||
@@ -770,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;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -883,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);
|
||||
|
||||
@@ -926,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.
|
||||
//
|
||||
@@ -941,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
|
||||
@@ -959,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.
|
||||
*
|
||||
@@ -980,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;
|
||||
@@ -990,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();
|
||||
}
|
||||
|
||||
@@ -1159,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
|
||||
@@ -1170,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");
|
||||
@@ -1217,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);
|
||||
}
|
||||
@@ -1354,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.
|
||||
*
|
||||
@@ -1567,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,
|
||||
@@ -140,8 +147,9 @@ export class IgnoredInvites {
|
||||
/**
|
||||
* Find out whether an invite should be ignored.
|
||||
*
|
||||
* @param sender - The user id for the user who issued the invite.
|
||||
* @param roomId - The room to which the user is invited.
|
||||
* @param params
|
||||
* @param params.sender - The user id for the user who issued the invite.
|
||||
* @param params.roomId - The room to which the user is invited.
|
||||
* @returns A rule matching the entity, if any was found, `null` otherwise.
|
||||
*/
|
||||
public async getRuleForInvite({
|
||||
@@ -172,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) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import { RoomState } from "./room-state";
|
||||
import { logger } from "../logger";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
import { EventType } from "../@types/event";
|
||||
import { KnownMembership, Membership } from "../@types/membership";
|
||||
|
||||
export enum RoomMemberEvent {
|
||||
Membership = "RoomMember.membership",
|
||||
@@ -119,7 +120,7 @@ export class RoomMember extends TypedEventEmitter<RoomMemberEvent, RoomMemberEve
|
||||
/**
|
||||
* The membership state for this room member e.g. 'join'.
|
||||
*/
|
||||
public membership?: string;
|
||||
public membership?: Membership;
|
||||
/**
|
||||
* True if the member's name is disambiguated.
|
||||
*/
|
||||
@@ -314,7 +315,7 @@ export class RoomMember extends TypedEventEmitter<RoomMemberEvent, RoomMemberEve
|
||||
|
||||
public isKicked(): boolean {
|
||||
return (
|
||||
this.membership === "leave" &&
|
||||
this.membership === KnownMembership.Leave &&
|
||||
this.events.member !== undefined &&
|
||||
this.events.member.getSender() !== this.events.member.getStateKey()
|
||||
);
|
||||
@@ -340,12 +341,12 @@ export class RoomMember extends TypedEventEmitter<RoomMemberEvent, RoomMemberEve
|
||||
let memberContent = memberEvent.getContent();
|
||||
let inviteSender: string | undefined = memberEvent.getSender();
|
||||
|
||||
if (memberContent.membership === "join") {
|
||||
if (memberContent.membership === KnownMembership.Join) {
|
||||
memberContent = memberEvent.getPrevContent();
|
||||
inviteSender = memberEvent.getUnsigned().prev_sender;
|
||||
}
|
||||
|
||||
if (memberContent.membership === "invite" && memberContent.is_direct) {
|
||||
if (memberContent.membership === KnownMembership.Invite && memberContent.is_direct) {
|
||||
return inviteSender;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ export class RoomReceipts {
|
||||
/**
|
||||
* Look for dangling receipts for the given event ID,
|
||||
* and add them to the thread of unthread receipts if found.
|
||||
* @param eventId - the event ID to look for
|
||||
* @param event - the event to look for
|
||||
*/
|
||||
private onTimelineEvent = (event: MatrixEvent): void => {
|
||||
const eventId = event.getId();
|
||||
|
||||
+23
-10
@@ -25,6 +25,7 @@ import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
import { Beacon, BeaconEvent, BeaconEventHandlerMap, getBeaconInfoIdentifier, BeaconIdentifier } from "./beacon";
|
||||
import { TypedReEmitter } from "../ReEmitter";
|
||||
import { M_BEACON, M_BEACON_INFO } from "../@types/beacon";
|
||||
import { KnownMembership } from "../@types/membership";
|
||||
|
||||
export interface IMarkerFoundOptions {
|
||||
/** Whether the timeline was empty before the marker event arrived in the
|
||||
@@ -52,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
|
||||
@@ -59,6 +61,7 @@ export interface IPowerLevelsContent {
|
||||
// eslint-disable-next-line camelcase
|
||||
state_default?: number;
|
||||
ban?: number;
|
||||
invite?: number;
|
||||
kick?: number;
|
||||
redact?: number;
|
||||
}
|
||||
@@ -129,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
|
||||
@@ -206,7 +211,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
}
|
||||
if (this.joinedMemberCount === null) {
|
||||
this.joinedMemberCount = this.getMembers().reduce((count, m) => {
|
||||
return m.membership === "join" ? count + 1 : count;
|
||||
return m.membership === KnownMembership.Join ? count + 1 : count;
|
||||
}, 0);
|
||||
}
|
||||
return this.joinedMemberCount;
|
||||
@@ -230,7 +235,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
}
|
||||
if (this.invitedMemberCount === null) {
|
||||
this.invitedMemberCount = this.getMembers().reduce((count, m) => {
|
||||
return m.membership === "invite" ? count + 1 : count;
|
||||
return m.membership === KnownMembership.Invite ? count + 1 : count;
|
||||
}, 0);
|
||||
}
|
||||
return this.invitedMemberCount;
|
||||
@@ -297,13 +302,15 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
/**
|
||||
* Get state events from the state of the room.
|
||||
* @param eventType - The event type of the state event.
|
||||
* @param stateKey - Optional. The state_key of the state event. If
|
||||
* this is `undefined` then all matching state events will be
|
||||
* returned.
|
||||
* @returns A list of events if state_key was
|
||||
* `undefined`, else a single event (or null if no match found).
|
||||
* @returns A list of events
|
||||
*/
|
||||
public getStateEvents(eventType: EventType | string): MatrixEvent[];
|
||||
/**
|
||||
* Get state events from the state of the room.
|
||||
* @param eventType - The event type of the state event.
|
||||
* @param stateKey - The state_key of the state event.
|
||||
* @returns A single event (or null if no match found).
|
||||
*/
|
||||
public getStateEvents(eventType: EventType | string, stateKey: string): MatrixEvent | null;
|
||||
public getStateEvents(eventType: EventType | string, stateKey?: string): MatrixEvent[] | MatrixEvent | null {
|
||||
if (!this.events.has(eventType)) {
|
||||
@@ -432,7 +439,10 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
// leave events apparently elide the displayname or avatar_url,
|
||||
// so let's fake one up so that we don't leak user ids
|
||||
// into the timeline
|
||||
if (event.getContent().membership === "leave" || event.getContent().membership === "ban") {
|
||||
if (
|
||||
event.getContent().membership === KnownMembership.Leave ||
|
||||
event.getContent().membership === KnownMembership.Ban
|
||||
) {
|
||||
event.getContent().avatar_url = event.getContent().avatar_url || event.getPrevContent().avatar_url;
|
||||
event.getContent().displayname =
|
||||
event.getContent().displayname || event.getPrevContent().displayname;
|
||||
@@ -774,7 +784,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
*/
|
||||
public maySendRedactionForEvent(mxEvent: MatrixEvent, userId: string): boolean {
|
||||
const member = this.getMember(userId);
|
||||
if (!member || member.membership === "leave") return false;
|
||||
if (!member || member.membership === KnownMembership.Leave) return false;
|
||||
|
||||
if (mxEvent.status || mxEvent.isRedacted()) return false;
|
||||
|
||||
@@ -794,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 = {};
|
||||
|
||||
+211
-58
@@ -68,6 +68,8 @@ 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
|
||||
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
|
||||
@@ -177,7 +179,7 @@ export type RoomEventHandlerMap = {
|
||||
* @param membership - The new membership value
|
||||
* @param prevMembership - The previous membership value
|
||||
*/
|
||||
[RoomEvent.MyMembership]: (room: Room, membership: string, prevMembership?: string) => void;
|
||||
[RoomEvent.MyMembership]: (room: Room, membership: Membership, prevMembership?: Membership) => void;
|
||||
/**
|
||||
* Fires whenever a room's tags are updated.
|
||||
* @param event - The tags event
|
||||
@@ -351,7 +353,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
private readonly pendingEventList?: MatrixEvent[];
|
||||
// read by megolm via getter; boolean value - null indicates "use global value"
|
||||
private blacklistUnverifiedDevices?: boolean;
|
||||
private selfMembership?: string;
|
||||
private selfMembership?: Membership;
|
||||
private summaryHeroes: string[] | null = null;
|
||||
// flags to stop logspam about missing m.room.create events
|
||||
private getTypeWarning = false;
|
||||
@@ -473,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)];
|
||||
@@ -863,8 +869,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
/**
|
||||
* @returns the membership type (join | leave | invite | knock) for the logged in user
|
||||
*/
|
||||
public getMyMembership(): string {
|
||||
return this.selfMembership ?? "leave";
|
||||
public getMyMembership(): Membership {
|
||||
return this.selfMembership ?? KnownMembership.Leave;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -878,7 +884,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
return me.getDMInviter();
|
||||
}
|
||||
|
||||
if (this.selfMembership === "invite") {
|
||||
if (this.selfMembership === KnownMembership.Invite) {
|
||||
// fall back to summary information
|
||||
const memberCount = this.getInvitedAndJoinedMemberCount();
|
||||
if (memberCount === 2) {
|
||||
@@ -914,37 +920,69 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
return this.myUserId;
|
||||
}
|
||||
|
||||
public getAvatarFallbackMember(): RoomMember | undefined {
|
||||
const memberCount = this.getInvitedAndJoinedMemberCount();
|
||||
if (memberCount > 2) {
|
||||
return;
|
||||
/**
|
||||
* Gets the "functional members" in this room.
|
||||
*
|
||||
* Returns the list of userIDs from the `io.element.functional_members` event. Does not consider the
|
||||
* current membership states of those users.
|
||||
*
|
||||
* @see https://github.com/element-hq/element-meta/blob/develop/spec/functional_members.md.
|
||||
*/
|
||||
private getFunctionalMembers(): string[] {
|
||||
const mFunctionalMembers = this.currentState.getStateEvents(UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, "");
|
||||
if (Array.isArray(mFunctionalMembers?.getContent().service_members)) {
|
||||
return mFunctionalMembers!.getContent().service_members;
|
||||
}
|
||||
const hasHeroes = Array.isArray(this.summaryHeroes) && this.summaryHeroes.length;
|
||||
return [];
|
||||
}
|
||||
|
||||
public getAvatarFallbackMember(): RoomMember | undefined {
|
||||
const functionalMembers = this.getFunctionalMembers();
|
||||
|
||||
// Only generate a fallback avatar if the conversation is with a single specific other user (a "DM").
|
||||
let nonFunctionalMemberCount = 0;
|
||||
this.getMembers()!.forEach((m) => {
|
||||
if (m.membership !== "join" && m.membership !== "invite") return;
|
||||
if (functionalMembers.includes(m.userId)) return;
|
||||
nonFunctionalMemberCount++;
|
||||
});
|
||||
if (nonFunctionalMemberCount > 2) return;
|
||||
|
||||
// Prefer the list of heroes, if present. It should only include the single other user in the DM.
|
||||
const nonFunctionalHeroes = this.summaryHeroes?.filter((h) => !functionalMembers.includes(h));
|
||||
const hasHeroes = Array.isArray(nonFunctionalHeroes) && nonFunctionalHeroes.length;
|
||||
if (hasHeroes) {
|
||||
const availableMember = this.summaryHeroes!.map((userId) => {
|
||||
return this.getMember(userId);
|
||||
}).find((member) => !!member);
|
||||
const availableMember = nonFunctionalHeroes
|
||||
.map((userId) => {
|
||||
return this.getMember(userId);
|
||||
})
|
||||
.find((member) => !!member);
|
||||
if (availableMember) {
|
||||
return availableMember;
|
||||
}
|
||||
}
|
||||
const members = this.currentState.getMembers();
|
||||
// could be different than memberCount
|
||||
// as this includes left members
|
||||
if (members.length <= 2) {
|
||||
const availableMember = members.find((m) => {
|
||||
|
||||
// Consider *all*, including previous, members, to generate the avatar for DMs where the other user left.
|
||||
// Needed to generate a matching avatar for rooms named "Empty Room (was Alice)".
|
||||
const members = this.getMembers();
|
||||
const nonFunctionalMembers = members?.filter((m) => !functionalMembers.includes(m.userId));
|
||||
if (nonFunctionalMembers.length <= 2) {
|
||||
const availableMember = nonFunctionalMembers.find((m) => {
|
||||
return m.userId !== this.myUserId;
|
||||
});
|
||||
if (availableMember) {
|
||||
return availableMember;
|
||||
}
|
||||
}
|
||||
// if all else fails, try falling back to a user,
|
||||
// and create a one-off member for it
|
||||
|
||||
// If all else failed, but the homeserver gave us heroes that previously could not be found in the room members,
|
||||
// trust and try falling back to a hero, creating a one-off member for it
|
||||
if (hasHeroes) {
|
||||
const availableUser = this.summaryHeroes!.map((userId) => {
|
||||
return this.client.getUser(userId);
|
||||
}).find((user) => !!user);
|
||||
const availableUser = nonFunctionalHeroes
|
||||
.map((userId) => {
|
||||
return this.client.getUser(userId);
|
||||
})
|
||||
.find((user) => !!user);
|
||||
if (availableUser) {
|
||||
const member = new RoomMember(this.roomId, availableUser.userId);
|
||||
member.user = availableUser;
|
||||
@@ -957,11 +995,11 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* Sets the membership this room was received as during sync
|
||||
* @param membership - join | leave | invite
|
||||
*/
|
||||
public updateMyMembership(membership: string): void {
|
||||
public updateMyMembership(membership: Membership): void {
|
||||
const prevMembership = this.selfMembership;
|
||||
this.selfMembership = membership;
|
||||
if (prevMembership !== membership) {
|
||||
if (membership === "leave") {
|
||||
if (membership === KnownMembership.Leave) {
|
||||
this.cleanupAfterLeaving();
|
||||
}
|
||||
this.emit(RoomEvent.MyMembership, this, membership, prevMembership);
|
||||
@@ -970,7 +1008,12 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
|
||||
private async loadMembersFromServer(): Promise<IStateEventWithRoomId[]> {
|
||||
const lastSyncToken = this.client.store.getSyncToken();
|
||||
const response = await this.client.members(this.roomId, undefined, "leave", lastSyncToken ?? undefined);
|
||||
const response = await this.client.members(
|
||||
this.roomId,
|
||||
undefined,
|
||||
KnownMembership.Leave,
|
||||
lastSyncToken ?? undefined,
|
||||
);
|
||||
return response.chunk;
|
||||
}
|
||||
|
||||
@@ -1268,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
|
||||
*
|
||||
@@ -1474,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);
|
||||
}
|
||||
|
||||
@@ -1677,7 +1825,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* @returns A list of currently joined members.
|
||||
*/
|
||||
public getJoinedMembers(): RoomMember[] {
|
||||
return this.getMembersWithMembership("join");
|
||||
return this.getMembersWithMembership(KnownMembership.Join);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1712,7 +1860,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* @param membership - The membership state.
|
||||
* @returns A list of members with the given membership state.
|
||||
*/
|
||||
public getMembersWithMembership(membership: string): RoomMember[] {
|
||||
public getMembersWithMembership(membership: Membership): RoomMember[] {
|
||||
return this.currentState.getMembers().filter(function (m) {
|
||||
return m.membership === membership;
|
||||
});
|
||||
@@ -1725,9 +1873,9 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
*/
|
||||
public async getEncryptionTargetMembers(): Promise<RoomMember[]> {
|
||||
await this.loadMembersIfNeeded();
|
||||
let members = this.getMembersWithMembership("join");
|
||||
let members = this.getMembersWithMembership(KnownMembership.Join);
|
||||
if (this.shouldEncryptForInvitedMembers()) {
|
||||
members = members.concat(this.getMembersWithMembership("invite"));
|
||||
members = members.concat(this.getMembersWithMembership(KnownMembership.Invite));
|
||||
}
|
||||
return members;
|
||||
}
|
||||
@@ -1758,7 +1906,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* @param membership - The membership e.g. `'join'`
|
||||
* @returns True if this user_id has the given membership state.
|
||||
*/
|
||||
public hasMembershipState(userId: string, membership: string): boolean {
|
||||
public hasMembershipState(userId: string, membership: Membership): boolean {
|
||||
const member = this.getMember(userId);
|
||||
if (!member) {
|
||||
return false;
|
||||
@@ -2307,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);
|
||||
@@ -2322,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 &&
|
||||
@@ -2928,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[]];
|
||||
}
|
||||
}
|
||||
@@ -3087,7 +3237,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
const membership = membershipEvent.getContent().membership;
|
||||
this.updateMyMembership(membership!);
|
||||
|
||||
if (membership === "invite") {
|
||||
if (membership === KnownMembership.Invite) {
|
||||
const strippedStateEvents = membershipEvent.getUnsigned().invite_room_state || [];
|
||||
strippedStateEvents.forEach((strippedEvent) => {
|
||||
const existingEvent = this.currentState.getStateEvents(strippedEvent.type, strippedEvent.state_key);
|
||||
@@ -3173,7 +3323,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
*/
|
||||
public maySendMessage(): boolean {
|
||||
return (
|
||||
this.getMyMembership() === "join" &&
|
||||
this.getMyMembership() === KnownMembership.Join &&
|
||||
(this.hasEncryptionStateEvent()
|
||||
? this.currentState.maySendEvent(EventType.RoomMessageEncrypted, this.myUserId)
|
||||
: this.currentState.maySendEvent(EventType.RoomMessage, this.myUserId))
|
||||
@@ -3186,7 +3336,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* @returns true if the user should be permitted to issue invites for this room.
|
||||
*/
|
||||
public canInvite(userId: string): boolean {
|
||||
let canInvite = this.getMyMembership() === "join";
|
||||
let canInvite = this.getMyMembership() === KnownMembership.Join;
|
||||
const powerLevelsEvent = this.currentState.getStateEvents(EventType.RoomPowerLevels, "");
|
||||
const powerLevels = powerLevelsEvent && powerLevelsEvent.getContent();
|
||||
const me = this.getMember(userId);
|
||||
@@ -3351,11 +3501,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
let inviteJoinCount = joinedMemberCount + invitedMemberCount - 1;
|
||||
|
||||
// get service members (e.g. helper bots) for exclusion
|
||||
let excludedUserIds: string[] = [];
|
||||
const mFunctionalMembers = this.currentState.getStateEvents(UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, "");
|
||||
if (Array.isArray(mFunctionalMembers?.getContent().service_members)) {
|
||||
excludedUserIds = mFunctionalMembers!.getContent().service_members;
|
||||
}
|
||||
const excludedUserIds = this.getFunctionalMembers();
|
||||
|
||||
// get members that are NOT ourselves and are actually in the room.
|
||||
let otherNames: string[] = [];
|
||||
@@ -3372,7 +3518,10 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
});
|
||||
} else {
|
||||
let otherMembers = this.currentState.getMembers().filter((m) => {
|
||||
return m.userId !== userId && (m.membership === "invite" || m.membership === "join");
|
||||
return (
|
||||
m.userId !== userId &&
|
||||
(m.membership === KnownMembership.Invite || m.membership === KnownMembership.Join)
|
||||
);
|
||||
});
|
||||
otherMembers = otherMembers.filter(({ userId }) => {
|
||||
// filter service members
|
||||
@@ -3400,7 +3549,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
const myMembership = this.getMyMembership();
|
||||
// if I have created a room and invited people through
|
||||
// 3rd party invites
|
||||
if (myMembership == "join") {
|
||||
if (myMembership == KnownMembership.Join) {
|
||||
const thirdPartyInvites = this.currentState.getStateEvents(EventType.RoomThirdPartyInvite);
|
||||
|
||||
if (thirdPartyInvites?.length) {
|
||||
@@ -3424,7 +3573,11 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
leftNames = this.currentState
|
||||
.getMembers()
|
||||
.filter((m) => {
|
||||
return m.userId !== userId && m.membership !== "invite" && m.membership !== "join";
|
||||
return (
|
||||
m.userId !== userId &&
|
||||
m.membership !== KnownMembership.Invite &&
|
||||
m.membership !== KnownMembership.Join
|
||||
);
|
||||
})
|
||||
.map((m) => m.name);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user