Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d018826f4 | |||
| df1a6a583a | |||
| a7496627fc | |||
| 8ef2ca681c | |||
| 0c7342cb20 | |||
| 429c05ba85 | |||
| af9993a710 | |||
| ff501834e6 | |||
| ef9157b37a | |||
| da0a55cea4 | |||
| d644f111ea | |||
| b2018ef81b | |||
| a4faab6155 | |||
| 1889f8dad5 | |||
| e98ce78027 | |||
| 83ba0fbb49 | |||
| 757c5e1d71 | |||
| eca651c0c2 | |||
| 2205445a50 | |||
| f168144c84 | |||
| eb288d125f | |||
| 4a72364fe3 | |||
| c2fa579fb2 | |||
| f71735d0c2 | |||
| e5ccfa86fe | |||
| 97c531aa42 | |||
| 44487078fb | |||
| e3c70a3ee4 | |||
| feb60a54b2 | |||
| c6e6248cd6 | |||
| 10cd84a653 | |||
| c36166d156 | |||
| 3a2cf14a68 | |||
| dd94f67a4f | |||
| 138281c620 | |||
| 378a91fe10 | |||
| 37ba169abf | |||
| 48fe267ea7 | |||
| a11fd8bc86 | |||
| eb9e557a64 | |||
| b9e684fdc3 | |||
| 9044145a7e | |||
| 939def2aa1 | |||
| c54f8f6106 | |||
| 25a777a0a6 | |||
| 0261868eb6 | |||
| 6ba4b35526 | |||
| f5ad4d0a73 | |||
| 582ea68c31 | |||
| 304c2b12bf | |||
| a3762c8e22 | |||
| 8b2a334ac4 |
@@ -19,6 +19,11 @@ version-resolver:
|
||||
labels:
|
||||
- "X-Breaking-Change"
|
||||
default: minor
|
||||
exclude-labels:
|
||||
- "T-Task"
|
||||
- "X-Reverted"
|
||||
exclude-contributors:
|
||||
- "RiotRobot"
|
||||
template: |
|
||||
$CHANGES
|
||||
prerelease: true
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
# from creeping in. They take a long time to run and consume 4 concurrent runners.
|
||||
if: github.event.workflow_run.event == 'merge_group'
|
||||
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/cypress.yaml@03b01b4a50d0f3fbbfa6c1a9314ef2d346d089d4
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/cypress.yaml@f6ef476f7905cc2b1f060f1a360b482e7546e682
|
||||
permissions:
|
||||
actions: read
|
||||
issues: read
|
||||
|
||||
@@ -32,3 +32,4 @@ jobs:
|
||||
site_id: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
desc: Documentation preview
|
||||
deployment_env: PR Documentation Preview
|
||||
environment: PR Documentation Preview
|
||||
|
||||
@@ -20,7 +20,7 @@ concurrency:
|
||||
jobs:
|
||||
build-element-web:
|
||||
name: Build element-web
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.84.0
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.84.1
|
||||
with:
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
react-sdk-repository: matrix-org/matrix-react-sdk
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Add notice
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
|
||||
with:
|
||||
script: |
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
|
||||
- name: Add label
|
||||
if: ${{ steps.teams.outputs.isTeamMember == 'false' }}
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.addLabels({
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
github.event.pull_request.head.repo.full_name != github.repository
|
||||
steps:
|
||||
- name: Close pull request
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
|
||||
@@ -2,13 +2,20 @@ name: Release Drafter
|
||||
on:
|
||||
push:
|
||||
branches: [staging]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
previous-version:
|
||||
description: What release to use as a base for release note purposes
|
||||
required: false
|
||||
type: string
|
||||
concurrency: ${{ github.workflow }}
|
||||
jobs:
|
||||
draft:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@dabcf3767562210392d862070ed2ef6434b9bc6f # v5
|
||||
- uses: release-drafter/release-drafter@e64b19c4c46173209ed9f2e5a2f4ca7de89a0e86 # v5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
disable-autolabeler: true
|
||||
previous-version: ${{ inputs.previous-version }}
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
sparse-checkout: |
|
||||
scripts/release
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Release Action
|
||||
name: Release Make
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
{
|
||||
echo "RELEASE_NOTES<<EOF"
|
||||
echo $BODY
|
||||
echo "$BODY"
|
||||
echo "EOF"
|
||||
} >> $GITHUB_ENV
|
||||
|
||||
@@ -102,11 +102,11 @@ jobs:
|
||||
VERSION: ${{ steps.release.outputs.tag_name }}
|
||||
|
||||
- name: Finalise version
|
||||
if: inputs.mode == 'final'
|
||||
if: inputs.final
|
||||
run: echo "VERSION=$(echo $VERSION | cut -d- -f1)" >> $GITHUB_ENV
|
||||
|
||||
- name: Check version number not in use
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { VERSION } = process.env;
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
git config --global user.email "releases@riot.im"
|
||||
git config --global user.name "RiotRobot"
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
@@ -179,16 +179,15 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Bump package.json version
|
||||
run: yarn version --no-git-tag-version --new-version "$VERSION"
|
||||
run: yarn version --no-git-tag-version --new-version "${VERSION#v}"
|
||||
|
||||
- name: Ingest upstream changes
|
||||
if: |
|
||||
inputs.dependencies &&
|
||||
inputs.include-changes &&
|
||||
contains(fromJSON(steps.update-dependencies.outputs.updated), inputs.include-changes)
|
||||
uses: actions/github-script@v6
|
||||
(!inputs.dependencies || contains(fromJSON(steps.update-dependencies.outputs.updated), inputs.include-changes))
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
RELEASE_ID: ${{ steps.upstream-release.outputs.body }}
|
||||
RELEASE_ID: ${{ steps.release.outputs.id }}
|
||||
DEPENDENCY: ${{ inputs.include-changes }}
|
||||
with:
|
||||
retries: 3
|
||||
@@ -204,7 +203,7 @@ jobs:
|
||||
core.exportVariable("RELEASE_NOTES", notes);
|
||||
|
||||
- name: Add to CHANGELOG.md
|
||||
if: inputs.mode == 'final'
|
||||
if: inputs.final
|
||||
run: |
|
||||
mv CHANGELOG.md CHANGELOG.md.old
|
||||
HEADER="Changes in [${VERSION#v}](https://github.com/${{ github.repository }}/releases/tag/$VERSION) ($(date '+%Y-%m-%d'))"
|
||||
@@ -230,7 +229,7 @@ jobs:
|
||||
run: git commit -m "$VERSION"
|
||||
|
||||
- name: Build assets
|
||||
if: steps.prepare.outputs.has-dist-script
|
||||
if: steps.prepare.outputs.has-dist-script == '1'
|
||||
run: DIST_VERSION="$VERSION" yarn dist
|
||||
|
||||
- name: Upload release assets & signatures
|
||||
@@ -272,7 +271,7 @@ jobs:
|
||||
|
||||
- name: Validate release has expected assets
|
||||
if: inputs.expected-asset-count
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
RELEASE_ID: ${{ steps.release.outputs.id }}
|
||||
EXPECTED_ASSET_COUNT: ${{ inputs.expected-asset-count }}
|
||||
@@ -300,12 +299,13 @@ jobs:
|
||||
git push origin master
|
||||
|
||||
- name: Publish release
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
RELEASE_ID: ${{ steps.release.outputs.id }}
|
||||
FINAL: ${{ inputs.final }}
|
||||
with:
|
||||
retries: 3
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
script: |
|
||||
const { RELEASE_ID: release_id, RELEASE_NOTES, VERSION, FINAL } = process.env;
|
||||
const { owner, repo } = context.repo;
|
||||
@@ -341,7 +341,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: repository
|
||||
run: echo "REPO=${GITHUB_REPOSITORY#*/}" >> $GITHUB_ENV
|
||||
run: echo "REPO=${GITHUB_REPOSITORY#*/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: garganshu/github-label-updater@3770d15ebfed2fe2cb06a241047bc340f774a7d1 # v1.0.0
|
||||
with:
|
||||
@@ -11,6 +11,8 @@ jobs:
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: staging
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
@@ -23,7 +23,7 @@ on:
|
||||
concurrency: ${{ github.workflow }}
|
||||
jobs:
|
||||
release:
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-action.yml@develop
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop
|
||||
secrets: inherit
|
||||
with:
|
||||
final: ${{ inputs.mode == 'final' }}
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
- name: "🩻 SonarCloud Scan"
|
||||
id: sonarcloud
|
||||
uses: matrix-org/sonarcloud-workflow-action@v2.6
|
||||
uses: matrix-org/sonarcloud-workflow-action@v2.7
|
||||
# workflow_run fails report against the develop commit always, we don't want that for PRs
|
||||
continue-on-error: ${{ github.event.workflow_run.head_branch != 'develop' }}
|
||||
with:
|
||||
|
||||
@@ -51,6 +51,22 @@ jobs:
|
||||
- name: Run Linter
|
||||
run: "yarn run lint:js"
|
||||
|
||||
workflow_lint:
|
||||
name: "Workflow Lint"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
|
||||
- name: Run Linter
|
||||
run: "yarn lint:workflows"
|
||||
|
||||
docs:
|
||||
name: "JSDoc Checker"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -7,4 +7,5 @@ on:
|
||||
jobs:
|
||||
call-triage-labelled:
|
||||
uses: vector-im/element-web/.github/workflows/triage-labelled.yml@develop
|
||||
secrets: inherit
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
Changes in [30.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v30.2.0) (2023-12-05)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Only await key query after lazy members resolved ([#3902](https://github.com/matrix-org/matrix-js-sdk/pull/3902)). Contributed by @BillCarsonFr.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Rewrite receipt-handling code ([#3901](https://github.com/matrix-org/matrix-js-sdk/pull/3901)). Contributed by @andybalaam.
|
||||
* Explicitly free some Rust-side objects ([#3911](https://github.com/matrix-org/matrix-js-sdk/pull/3911)). Contributed by @richvdh.
|
||||
* Fix type for TimestampToEventResponse.origin\_server\_ts ([#3906](https://github.com/matrix-org/matrix-js-sdk/pull/3906)). Contributed by @Half-Shot.
|
||||
|
||||
|
||||
Changes in [30.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v30.1.0) (2023-11-21)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
+7
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "30.1.0",
|
||||
"version": "30.2.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
@@ -14,10 +14,11 @@
|
||||
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
|
||||
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
|
||||
"gendoc": "typedoc",
|
||||
"lint": "yarn lint:types && yarn lint:js",
|
||||
"lint": "yarn lint:types && yarn lint:js && yarn lint:workflows",
|
||||
"lint:js": "eslint --max-warnings 0 src spec && prettier --check .",
|
||||
"lint:js-fix": "prettier --loglevel=warn --write . && eslint --fix src spec",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"coverage": "yarn test --coverage"
|
||||
@@ -51,7 +52,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^3.0.1",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^3.1.0",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
@@ -66,6 +67,8 @@
|
||||
"uuid": "9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@action-validator/cli": "^0.5.3",
|
||||
"@action-validator/core": "^0.5.3",
|
||||
"@babel/cli": "^7.12.10",
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/eslint-parser": "^7.12.10",
|
||||
@@ -92,7 +95,6 @@
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"allchange": "^1.0.6",
|
||||
"babel-jest": "^29.0.0",
|
||||
"babelify": "^10.0.0",
|
||||
"debug": "^4.3.4",
|
||||
"domexception": "^4.0.0",
|
||||
"eslint": "8.53.0",
|
||||
@@ -105,8 +107,8 @@
|
||||
"eslint-plugin-matrix-org": "^1.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.2.17",
|
||||
"eslint-plugin-unicorn": "^49.0.0",
|
||||
"exorcist": "^2.0.0",
|
||||
"fake-indexeddb": "^5.0.0",
|
||||
"fetch-mock": "9.11.0",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
"husky": "^8.0.3",
|
||||
"jest": "^29.0.0",
|
||||
@@ -117,9 +119,7 @@
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"prettier": "2.8.8",
|
||||
"rimraf": "^5.0.0",
|
||||
"terser": "^5.5.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsify": "^5.0.2",
|
||||
"typedoc": "^0.24.0",
|
||||
"typedoc-plugin-coverage": "^2.1.0",
|
||||
"typedoc-plugin-mdn-links": "^3.0.3",
|
||||
|
||||
@@ -24,6 +24,8 @@ async function getRelease(github, dependency) {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
const HEADING_PREFIX = "## ";
|
||||
|
||||
const main = async ({ github, releaseId, dependencies }) => {
|
||||
const { GITHUB_REPOSITORY } = process.env;
|
||||
const [owner, repo] = GITHUB_REPOSITORY.split("/");
|
||||
@@ -33,13 +35,13 @@ const main = async ({ github, releaseId, dependencies }) => {
|
||||
for (const dependency of dependencies) {
|
||||
const release = await getRelease(github, dependency);
|
||||
for (const line of release.body.split("\n")) {
|
||||
if (line.startsWith("#")) {
|
||||
heading = line;
|
||||
if (line.startsWith(HEADING_PREFIX)) {
|
||||
heading = line.trim();
|
||||
sections.set(heading, []);
|
||||
continue;
|
||||
}
|
||||
if (heading && line) {
|
||||
sections.get(heading).push(line);
|
||||
sections.get(heading).push(line.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,10 +52,24 @@ const main = async ({ github, releaseId, dependencies }) => {
|
||||
release_id: releaseId,
|
||||
});
|
||||
|
||||
const headings = ["🚨 BREAKING CHANGES", "🦖 Deprecations", "✨ Features", "🐛 Bug Fixes", "🧰 Maintenance"].map(
|
||||
(h) => HEADING_PREFIX + h,
|
||||
);
|
||||
|
||||
heading = null;
|
||||
const output = [];
|
||||
for (const line of [...release.body.split("\n"), null]) {
|
||||
if (line === null || line.startsWith("#")) {
|
||||
if (line === null || line.startsWith(HEADING_PREFIX)) {
|
||||
// If we have a heading, and it's not the first in the list of pending headings, output the section.
|
||||
// If we're processing the last line (null) then output all remaining sections.
|
||||
while (headings.length > 0 && (line === null || (heading && headings[0] !== heading))) {
|
||||
const heading = headings.shift();
|
||||
if (sections.has(heading)) {
|
||||
output.push(heading);
|
||||
output.push(...sections.get(heading));
|
||||
}
|
||||
}
|
||||
|
||||
if (heading && sections.has(heading)) {
|
||||
const lastIsBlank = !output.at(-1)?.trim();
|
||||
if (lastIsBlank) output.pop();
|
||||
|
||||
@@ -85,7 +85,7 @@ describe("CallMembership", () => {
|
||||
|
||||
it("considers memberships expired when local age large", () => {
|
||||
const fakeEvent = makeMockEvent(1000);
|
||||
fakeEvent.getLocalAge = jest.fn().mockReturnValue(6000);
|
||||
fakeEvent.localTimestamp = Date.now() - 6000;
|
||||
const membership = new CallMembership(fakeEvent, membershipTemplate);
|
||||
expect(membership.isExpired()).toEqual(true);
|
||||
});
|
||||
|
||||
@@ -60,13 +60,14 @@ describe("MatrixRTCSession", () => {
|
||||
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
|
||||
expect(sess?.memberships[0].membershipID).toEqual("bloop");
|
||||
expect(sess?.memberships[0].isExpired()).toEqual(false);
|
||||
expect(sess?.callId).toEqual("");
|
||||
});
|
||||
|
||||
it("ignores expired memberships events", () => {
|
||||
const expiredMembership = Object.assign({}, membershipTemplate);
|
||||
expiredMembership.expires = 1000;
|
||||
expiredMembership.device_id = "EXPIRED";
|
||||
const mockRoom = makeMockRoom([membershipTemplate, expiredMembership], () => 10000);
|
||||
const mockRoom = makeMockRoom([membershipTemplate, expiredMembership], 10000);
|
||||
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
expect(sess?.memberships.length).toEqual(1);
|
||||
@@ -265,7 +266,7 @@ describe("MatrixRTCSession", () => {
|
||||
const timeElapsed = 60 * 60 * 1000 - 1000;
|
||||
mockRoom.getLiveTimeline().getState(EventTimeline.FORWARDS)!.getStateEvents = jest
|
||||
.fn()
|
||||
.mockReturnValue(mockRTCEvent(eventContent.memberships, mockRoom.roomId, () => timeElapsed));
|
||||
.mockReturnValue(mockRTCEvent(eventContent.memberships, mockRoom.roomId, timeElapsed));
|
||||
|
||||
const eventReSentPromise = new Promise<Record<string, any>>((r) => {
|
||||
resolveFn = (_roomId: string, _type: string, val: Record<string, any>) => {
|
||||
@@ -538,10 +539,8 @@ describe("MatrixRTCSession", () => {
|
||||
it("emits an event at the time a membership event expires", () => {
|
||||
jest.useFakeTimers();
|
||||
try {
|
||||
let eventAge = 0;
|
||||
|
||||
const membership = Object.assign({}, membershipTemplate);
|
||||
const mockRoom = makeMockRoom([membership], () => eventAge);
|
||||
const mockRoom = makeMockRoom([membership], 0);
|
||||
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
const membershipObject = sess.memberships[0];
|
||||
@@ -549,7 +548,6 @@ describe("MatrixRTCSession", () => {
|
||||
const onMembershipsChanged = jest.fn();
|
||||
sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
|
||||
|
||||
eventAge = 61 * 1000 * 1000;
|
||||
jest.advanceTimersByTime(61 * 1000 * 1000);
|
||||
|
||||
expect(onMembershipsChanged).toHaveBeenCalledWith([membershipObject], []);
|
||||
@@ -560,47 +558,49 @@ describe("MatrixRTCSession", () => {
|
||||
});
|
||||
|
||||
it("prunes expired memberships on update", () => {
|
||||
client.sendStateEvent = jest.fn();
|
||||
jest.useFakeTimers();
|
||||
try {
|
||||
client.sendStateEvent = jest.fn();
|
||||
|
||||
let eventAge = 0;
|
||||
|
||||
const mockRoom = makeMockRoom(
|
||||
[
|
||||
const mockMemberships = [
|
||||
Object.assign({}, membershipTemplate, {
|
||||
device_id: "OTHERDEVICE",
|
||||
expires: 1000,
|
||||
}),
|
||||
],
|
||||
() => eventAge,
|
||||
);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
];
|
||||
const mockRoomNoExpired = makeMockRoom(mockMemberships, 0);
|
||||
|
||||
// sanity check
|
||||
expect(sess.memberships).toHaveLength(1);
|
||||
expect(sess.memberships[0].deviceId).toEqual("OTHERDEVICE");
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoomNoExpired);
|
||||
|
||||
eventAge = 10000;
|
||||
// sanity check
|
||||
expect(sess.memberships).toHaveLength(1);
|
||||
expect(sess.memberships[0].deviceId).toEqual("OTHERDEVICE");
|
||||
|
||||
sess.joinRoomSession([mockFocus]);
|
||||
jest.advanceTimersByTime(10000);
|
||||
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
mockRoom!.roomId,
|
||||
EventType.GroupCallMemberPrefix,
|
||||
{
|
||||
memberships: [
|
||||
{
|
||||
application: "m.call",
|
||||
scope: "m.room",
|
||||
call_id: "",
|
||||
device_id: "AAAAAAA",
|
||||
expires: 3600000,
|
||||
foci_active: [mockFocus],
|
||||
membershipID: expect.stringMatching(".*"),
|
||||
},
|
||||
],
|
||||
},
|
||||
"@alice:example.org",
|
||||
);
|
||||
sess.joinRoomSession([mockFocus]);
|
||||
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
mockRoomNoExpired!.roomId,
|
||||
EventType.GroupCallMemberPrefix,
|
||||
{
|
||||
memberships: [
|
||||
{
|
||||
application: "m.call",
|
||||
scope: "m.room",
|
||||
call_id: "",
|
||||
device_id: "AAAAAAA",
|
||||
expires: 3600000,
|
||||
foci_active: [mockFocus],
|
||||
membershipID: expect.stringMatching(".*"),
|
||||
},
|
||||
],
|
||||
},
|
||||
"@alice:example.org",
|
||||
);
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("fills in created_ts for other memberships on update", () => {
|
||||
|
||||
@@ -18,41 +18,29 @@ import { EventType, MatrixEvent, Room } from "../../../src";
|
||||
import { CallMembershipData } from "../../../src/matrixrtc/CallMembership";
|
||||
import { randomString } from "../../../src/randomstring";
|
||||
|
||||
export function makeMockRoom(
|
||||
memberships: CallMembershipData[],
|
||||
getLocalAge: (() => number) | undefined = undefined,
|
||||
): Room {
|
||||
export function makeMockRoom(memberships: CallMembershipData[], localAge: number | null = null): Room {
|
||||
const roomId = randomString(8);
|
||||
// Caching roomState here so it does not get recreated when calling `getLiveTimeline.getState()`
|
||||
const roomState = makeMockRoomState(memberships, roomId, localAge);
|
||||
return {
|
||||
roomId: roomId,
|
||||
getLiveTimeline: jest.fn().mockReturnValue({
|
||||
getState: jest.fn().mockReturnValue(makeMockRoomState(memberships, roomId, getLocalAge)),
|
||||
getState: jest.fn().mockReturnValue(roomState),
|
||||
}),
|
||||
} as unknown as Room;
|
||||
}
|
||||
|
||||
export function makeMockRoomState(
|
||||
memberships: CallMembershipData[],
|
||||
roomId: string,
|
||||
getLocalAge: (() => number) | undefined,
|
||||
) {
|
||||
export function makeMockRoomState(memberships: CallMembershipData[], roomId: string, localAge: number | null = null) {
|
||||
const event = mockRTCEvent(memberships, roomId, localAge);
|
||||
return {
|
||||
getStateEvents: (_: string, stateKey: string) => {
|
||||
const event = mockRTCEvent(memberships, roomId, getLocalAge);
|
||||
|
||||
if (stateKey !== undefined) return event;
|
||||
return [event];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function mockRTCEvent(
|
||||
memberships: CallMembershipData[],
|
||||
roomId: string,
|
||||
getLocalAge: (() => number) | undefined,
|
||||
): MatrixEvent {
|
||||
const getLocalAgeFn = getLocalAge ?? (() => 10);
|
||||
|
||||
export function mockRTCEvent(memberships: CallMembershipData[], roomId: string, localAge: number | null): MatrixEvent {
|
||||
return {
|
||||
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
||||
getContent: jest.fn().mockReturnValue({
|
||||
@@ -60,8 +48,7 @@ export function mockRTCEvent(
|
||||
}),
|
||||
getSender: jest.fn().mockReturnValue("@mock:user.example"),
|
||||
getTs: jest.fn().mockReturnValue(1000),
|
||||
getLocalAge: getLocalAgeFn,
|
||||
localTimestamp: Date.now(),
|
||||
localTimestamp: Date.now() - (localAge ?? 10),
|
||||
getRoomId: jest.fn().mockReturnValue(roomId),
|
||||
sender: {
|
||||
userId: "@mock:user.example",
|
||||
|
||||
+279
-21
@@ -14,10 +14,19 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MockedObject } from "jest-mock";
|
||||
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event";
|
||||
import { emitPromise } from "../../test-utils/test-utils";
|
||||
import { Crypto, IEventDecryptionResult } from "../../../src/crypto";
|
||||
import { IAnnotatedPushRule, PushRuleActionName, TweakName } from "../../../src";
|
||||
import {
|
||||
IAnnotatedPushRule,
|
||||
MatrixClient,
|
||||
PushRuleActionName,
|
||||
Room,
|
||||
THREAD_RELATION_TYPE,
|
||||
TweakName,
|
||||
} from "../../../src";
|
||||
|
||||
describe("MatrixEvent", () => {
|
||||
it("should create copies of itself", () => {
|
||||
@@ -61,31 +70,264 @@ describe("MatrixEvent", () => {
|
||||
expect(a.toSnapshot().isEquivalentTo(b)).toBe(false);
|
||||
});
|
||||
|
||||
it("should prune clearEvent when being redacted", () => {
|
||||
const ev = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
body: "Test",
|
||||
},
|
||||
event_id: "$event1:server",
|
||||
describe("redaction", () => {
|
||||
it("should prune clearEvent when being redacted", () => {
|
||||
const ev = createEvent("$event1:server", "Test");
|
||||
|
||||
expect(ev.getContent().body).toBe("Test");
|
||||
expect(ev.getWireContent().body).toBe("Test");
|
||||
ev.makeEncrypted("m.room.encrypted", { ciphertext: "xyz" }, "", "");
|
||||
expect(ev.getContent().body).toBe("Test");
|
||||
expect(ev.getWireContent().body).toBeUndefined();
|
||||
expect(ev.getWireContent().ciphertext).toBe("xyz");
|
||||
|
||||
const mockClient = {} as unknown as MockedObject<MatrixClient>;
|
||||
const room = new Room("!roomid:e.xyz", mockClient, "myname");
|
||||
const redaction = createRedaction(ev.getId()!);
|
||||
|
||||
ev.makeRedacted(redaction, room);
|
||||
expect(ev.getContent().body).toBeUndefined();
|
||||
expect(ev.getWireContent().body).toBeUndefined();
|
||||
expect(ev.getWireContent().ciphertext).toBeUndefined();
|
||||
});
|
||||
|
||||
expect(ev.getContent().body).toBe("Test");
|
||||
expect(ev.getWireContent().body).toBe("Test");
|
||||
ev.makeEncrypted("m.room.encrypted", { ciphertext: "xyz" }, "", "");
|
||||
expect(ev.getContent().body).toBe("Test");
|
||||
expect(ev.getWireContent().body).toBeUndefined();
|
||||
expect(ev.getWireContent().ciphertext).toBe("xyz");
|
||||
it("should remain in the main timeline when redacted", async () => {
|
||||
// Given an event in the main timeline
|
||||
const mockClient = createMockClient();
|
||||
const room = new Room("!roomid:e.xyz", mockClient, "myname");
|
||||
const ev = createEvent("$event1:server");
|
||||
|
||||
const redaction = new MatrixEvent({
|
||||
type: "m.room.redaction",
|
||||
redacts: ev.getId(),
|
||||
await room.addLiveEvents([ev]);
|
||||
await room.createThreadsTimelineSets();
|
||||
expect(ev.threadRootId).toBeUndefined();
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual([ev.getId()]);
|
||||
|
||||
// When I redact it
|
||||
const redaction = createRedaction(ev.getId()!);
|
||||
ev.makeRedacted(redaction, room);
|
||||
|
||||
// Then it remains in the main timeline
|
||||
expect(ev.threadRootId).toBeUndefined();
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual([ev.getId()]);
|
||||
});
|
||||
|
||||
ev.makeRedacted(redaction);
|
||||
expect(ev.getContent().body).toBeUndefined();
|
||||
expect(ev.getWireContent().body).toBeUndefined();
|
||||
expect(ev.getWireContent().ciphertext).toBeUndefined();
|
||||
it("should keep thread roots in both timelines when redacted", async () => {
|
||||
// Given a thread exists
|
||||
const mockClient = createMockClient();
|
||||
const room = new Room("!roomid:e.xyz", mockClient, "myname");
|
||||
const threadRoot = createEvent("$threadroot:server");
|
||||
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
|
||||
|
||||
await room.addLiveEvents([threadRoot, ev]);
|
||||
await room.createThreadsTimelineSets();
|
||||
expect(threadRoot.threadRootId).toEqual(threadRoot.getId());
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
|
||||
expect(threadLiveEventIds(room, 0)).toEqual([threadRoot.getId(), ev.getId()]);
|
||||
|
||||
// When I redact the thread root
|
||||
const redaction = createRedaction(ev.getId()!);
|
||||
threadRoot.makeRedacted(redaction, room);
|
||||
|
||||
// Then it remains in the main timeline and the thread
|
||||
expect(threadRoot.threadRootId).toEqual(threadRoot.getId());
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
|
||||
expect(threadLiveEventIds(room, 0)).toEqual([threadRoot.getId(), ev.getId()]);
|
||||
});
|
||||
|
||||
it("should move into the main timeline when redacted", async () => {
|
||||
// Given an event in a thread
|
||||
const mockClient = createMockClient();
|
||||
const room = new Room("!roomid:e.xyz", mockClient, "myname");
|
||||
const threadRoot = createEvent("$threadroot:server");
|
||||
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
|
||||
|
||||
await room.addLiveEvents([threadRoot, ev]);
|
||||
await room.createThreadsTimelineSets();
|
||||
expect(ev.threadRootId).toEqual(threadRoot.getId());
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
|
||||
expect(threadLiveEventIds(room, 0)).toEqual([threadRoot.getId(), ev.getId()]);
|
||||
|
||||
// When I redact it
|
||||
const redaction = createRedaction(ev.getId()!);
|
||||
ev.makeRedacted(redaction, room);
|
||||
|
||||
// Then it disappears from the thread and appears in the main timeline
|
||||
expect(ev.threadRootId).toBeUndefined();
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId(), ev.getId()]);
|
||||
expect(threadLiveEventIds(room, 0)).not.toContain(ev.getId());
|
||||
});
|
||||
|
||||
it("should move reactions to a redacted event into the main timeline", async () => {
|
||||
// Given an event in a thread with a reaction
|
||||
const mockClient = createMockClient();
|
||||
const room = new Room("!roomid:e.xyz", mockClient, "myname");
|
||||
const threadRoot = createEvent("$threadroot:server");
|
||||
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
|
||||
const reaction = createReactionEvent("$reaction:server", ev.getId()!);
|
||||
|
||||
await room.addLiveEvents([threadRoot, ev, reaction]);
|
||||
await room.createThreadsTimelineSets();
|
||||
expect(reaction.threadRootId).toEqual(threadRoot.getId());
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
|
||||
expect(threadLiveEventIds(room, 0)).toEqual([threadRoot.getId(), ev.getId(), reaction.getId()]);
|
||||
|
||||
// When I redact the event
|
||||
const redaction = createRedaction(ev.getId()!);
|
||||
ev.makeRedacted(redaction, room);
|
||||
|
||||
// Then the reaction moves into the main timeline
|
||||
expect(reaction.threadRootId).toBeUndefined();
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId(), ev.getId(), reaction.getId()]);
|
||||
expect(threadLiveEventIds(room, 0)).not.toContain(reaction.getId());
|
||||
});
|
||||
|
||||
it("should move edits of a redacted event into the main timeline", async () => {
|
||||
// Given an event in a thread with a reaction
|
||||
const mockClient = createMockClient();
|
||||
const room = new Room("!roomid:e.xyz", mockClient, "myname");
|
||||
const threadRoot = createEvent("$threadroot:server");
|
||||
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
|
||||
const edit = createEditEvent("$edit:server", ev.getId()!);
|
||||
|
||||
await room.addLiveEvents([threadRoot, ev, edit]);
|
||||
await room.createThreadsTimelineSets();
|
||||
expect(edit.threadRootId).toEqual(threadRoot.getId());
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
|
||||
expect(threadLiveEventIds(room, 0)).toEqual([threadRoot.getId(), ev.getId(), edit.getId()]);
|
||||
|
||||
// When I redact the event
|
||||
const redaction = createRedaction(ev.getId()!);
|
||||
ev.makeRedacted(redaction, room);
|
||||
|
||||
// Then the edit moves into the main timeline
|
||||
expect(edit.threadRootId).toBeUndefined();
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId(), ev.getId(), edit.getId()]);
|
||||
expect(threadLiveEventIds(room, 0)).not.toContain(edit.getId());
|
||||
});
|
||||
|
||||
it("should move reactions to replies to replies a redacted event into the main timeline", async () => {
|
||||
// Given an event in a thread with a reaction
|
||||
const mockClient = createMockClient();
|
||||
const room = new Room("!roomid:e.xyz", mockClient, "myname");
|
||||
const threadRoot = createEvent("$threadroot:server");
|
||||
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
|
||||
const reply1 = createReplyEvent("$reply1:server", ev.getId()!);
|
||||
const reply2 = createReplyEvent("$reply2:server", reply1.getId()!);
|
||||
const reaction = createReactionEvent("$reaction:server", reply2.getId()!);
|
||||
|
||||
await room.addLiveEvents([threadRoot, ev, reply1, reply2, reaction]);
|
||||
await room.createThreadsTimelineSets();
|
||||
expect(reaction.threadRootId).toEqual(threadRoot.getId());
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
|
||||
expect(threadLiveEventIds(room, 0)).toEqual([
|
||||
threadRoot.getId(),
|
||||
ev.getId(),
|
||||
reply1.getId(),
|
||||
reply2.getId(),
|
||||
reaction.getId(),
|
||||
]);
|
||||
|
||||
// When I redact the event
|
||||
const redaction = createRedaction(ev.getId()!);
|
||||
ev.makeRedacted(redaction, room);
|
||||
|
||||
// Then the replies move to the main thread and the reaction disappears
|
||||
expect(reaction.threadRootId).toBeUndefined();
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual([
|
||||
threadRoot.getId(),
|
||||
ev.getId(),
|
||||
reply1.getId(),
|
||||
reply2.getId(),
|
||||
reaction.getId(),
|
||||
]);
|
||||
expect(threadLiveEventIds(room, 0)).not.toContain(reply1.getId());
|
||||
expect(threadLiveEventIds(room, 0)).not.toContain(reply2.getId());
|
||||
expect(threadLiveEventIds(room, 0)).not.toContain(reaction.getId());
|
||||
});
|
||||
|
||||
function createMockClient(): MatrixClient {
|
||||
return {
|
||||
supportsThreads: jest.fn().mockReturnValue(true),
|
||||
decryptEventIfNeeded: jest.fn().mockReturnThis(),
|
||||
getUserId: jest.fn().mockReturnValue("@user:server"),
|
||||
} as unknown as MockedObject<MatrixClient>;
|
||||
}
|
||||
|
||||
function createEvent(eventId: string, body?: string): MatrixEvent {
|
||||
return new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
body: body ?? eventId,
|
||||
},
|
||||
event_id: eventId,
|
||||
});
|
||||
}
|
||||
|
||||
function createThreadedEvent(eventId: string, threadRootId: string): MatrixEvent {
|
||||
return new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"body": eventId,
|
||||
"m.relates_to": {
|
||||
rel_type: THREAD_RELATION_TYPE.name,
|
||||
event_id: threadRootId,
|
||||
},
|
||||
},
|
||||
event_id: eventId,
|
||||
});
|
||||
}
|
||||
|
||||
function createEditEvent(eventId: string, repliedToId: string): MatrixEvent {
|
||||
return new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"body": "Edited",
|
||||
"m.new_content": {
|
||||
body: "Edited",
|
||||
},
|
||||
"m.relates_to": {
|
||||
event_id: repliedToId,
|
||||
rel_type: "m.replace",
|
||||
},
|
||||
},
|
||||
event_id: eventId,
|
||||
});
|
||||
}
|
||||
|
||||
function createReplyEvent(eventId: string, repliedToId: string): MatrixEvent {
|
||||
return new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
event_id: repliedToId,
|
||||
key: "x",
|
||||
rel_type: "m.in_reply_to",
|
||||
},
|
||||
},
|
||||
event_id: eventId,
|
||||
});
|
||||
}
|
||||
|
||||
function createReactionEvent(eventId: string, reactedToId: string): MatrixEvent {
|
||||
return new MatrixEvent({
|
||||
type: "m.reaction",
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
event_id: reactedToId,
|
||||
key: "x",
|
||||
rel_type: "m.annotation",
|
||||
},
|
||||
},
|
||||
event_id: eventId,
|
||||
});
|
||||
}
|
||||
|
||||
function createRedaction(redactedEventid: string): MatrixEvent {
|
||||
return new MatrixEvent({
|
||||
type: "m.room.redaction",
|
||||
redacts: redactedEventid,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("applyVisibilityEvent", () => {
|
||||
@@ -330,3 +572,19 @@ describe("MatrixEvent", () => {
|
||||
expect(stateEvent.threadRootId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
function mainTimelineLiveEventIds(room: Room): Array<string> {
|
||||
return room
|
||||
.getLiveTimeline()
|
||||
.getEvents()
|
||||
.map((e) => e.getId()!);
|
||||
}
|
||||
|
||||
function threadLiveEventIds(room: Room, threadIndex: number): Array<string> {
|
||||
return room
|
||||
.getThreads()
|
||||
[threadIndex].getUnfilteredTimelineSet()
|
||||
.getLiveTimeline()
|
||||
.getEvents()
|
||||
.map((e) => e.getId()!);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ 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";
|
||||
|
||||
describe("RoomState", function () {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -362,9 +363,11 @@ describe("RoomState", function () {
|
||||
});
|
||||
|
||||
it("does not add redacted beacon info events to state", () => {
|
||||
const mockClient = {} as unknown as MockedObject<MatrixClient>;
|
||||
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId);
|
||||
const redactionEvent = new MatrixEvent({ type: "m.room.redaction" });
|
||||
redactedBeaconEvent.makeRedacted(redactionEvent);
|
||||
const room = new Room(roomId, mockClient, userA);
|
||||
redactedBeaconEvent.makeRedacted(redactionEvent, room);
|
||||
const emitSpy = jest.spyOn(state, "emit");
|
||||
|
||||
state.setStateEvents([redactedBeaconEvent]);
|
||||
@@ -394,11 +397,13 @@ describe("RoomState", function () {
|
||||
});
|
||||
|
||||
it("destroys and removes redacted beacon events", () => {
|
||||
const mockClient = {} as unknown as MockedObject<MatrixClient>;
|
||||
const beaconId = "$beacon1";
|
||||
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
|
||||
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
|
||||
const redactionEvent = new MatrixEvent({ type: "m.room.redaction", redacts: beaconEvent.getId() });
|
||||
redactedBeaconEvent.makeRedacted(redactionEvent);
|
||||
const room = new Room(roomId, mockClient, userA);
|
||||
redactedBeaconEvent.makeRedacted(redactionEvent, room);
|
||||
|
||||
state.setStateEvents([beaconEvent]);
|
||||
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
|
||||
|
||||
@@ -3165,7 +3165,6 @@ describe("Room", function () {
|
||||
// When we ask what they have read
|
||||
// Then we say "nothing"
|
||||
expect(room.getEventReadUpTo(userA)).toBeNull();
|
||||
expect(logger.warn).toHaveBeenCalledWith("Ignoring receipt for missing event with id missingEventId");
|
||||
});
|
||||
|
||||
it("ignores receipts pointing at the wrong thread", () => {
|
||||
@@ -3654,7 +3653,7 @@ describe("Room", function () {
|
||||
expect(room.polls.get(pollStartEvent.getId()!)).toBeTruthy();
|
||||
|
||||
const redactedEvent = new MatrixEvent({ type: "m.room.redaction" });
|
||||
pollStartEvent.makeRedacted(redactedEvent);
|
||||
pollStartEvent.makeRedacted(redactedEvent, room);
|
||||
|
||||
await flushPromises();
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingReque
|
||||
import { KeyClaimManager } from "../../../src/rust-crypto/KeyClaimManager";
|
||||
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
|
||||
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi } from "../../../src";
|
||||
import { logger, LogSpan } from "../../../src/logger";
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.mockReset();
|
||||
@@ -93,7 +94,7 @@ describe("KeyClaimManager", () => {
|
||||
olmMachine.markRequestAsSent.mockResolvedValueOnce(undefined);
|
||||
|
||||
// fire off the request
|
||||
await keyClaimManager.ensureSessionsForUsers([u1, u2]);
|
||||
await keyClaimManager.ensureSessionsForUsers(new LogSpan(logger, "test"), [u1, u2]);
|
||||
|
||||
// check that all the calls were made
|
||||
expect(olmMachine.getMissingSessions).toHaveBeenCalledWith([u1, u2]);
|
||||
@@ -119,12 +120,13 @@ describe("KeyClaimManager", () => {
|
||||
let markRequestAsSentPromise = awaitCallToMarkRequestAsSent();
|
||||
|
||||
// fire off two requests, and keep track of whether their promises resolve
|
||||
const span = new LogSpan(logger, "test");
|
||||
let req1Resolved = false;
|
||||
keyClaimManager.ensureSessionsForUsers([u1]).then(() => {
|
||||
keyClaimManager.ensureSessionsForUsers(span, [u1]).then(() => {
|
||||
req1Resolved = true;
|
||||
});
|
||||
let req2Resolved = false;
|
||||
const req2 = keyClaimManager.ensureSessionsForUsers([u2]).then(() => {
|
||||
const req2 = keyClaimManager.ensureSessionsForUsers(span, [u2]).then(() => {
|
||||
req2Resolved = true;
|
||||
});
|
||||
|
||||
|
||||
@@ -645,6 +645,7 @@ describe("RustCrypto", () => {
|
||||
|
||||
it("should call getDevice", async () => {
|
||||
olmMachine.getDevice.mockResolvedValue({
|
||||
free: jest.fn(),
|
||||
isCrossSigningTrusted: jest.fn().mockReturnValue(false),
|
||||
isLocallyTrusted: jest.fn().mockReturnValue(false),
|
||||
isCrossSignedByOwner: jest.fn().mockReturnValue(false),
|
||||
@@ -871,7 +872,7 @@ describe("RustCrypto", () => {
|
||||
});
|
||||
|
||||
it("returns a verified UserVerificationStatus when the UserIdentity is verified", async () => {
|
||||
olmMachine.getIdentity.mockResolvedValue({ isVerified: jest.fn().mockReturnValue(true) });
|
||||
olmMachine.getIdentity.mockResolvedValue({ free: jest.fn(), isVerified: jest.fn().mockReturnValue(true) });
|
||||
|
||||
const userVerificationStatus = await rustCrypto.getUserVerificationStatus(testData.TEST_USER_ID);
|
||||
expect(userVerificationStatus.isVerified()).toBeTruthy();
|
||||
|
||||
@@ -94,6 +94,9 @@ export enum EventType {
|
||||
// Group call events
|
||||
GroupCallPrefix = "org.matrix.msc3401.call",
|
||||
GroupCallMemberPrefix = "org.matrix.msc3401.call.member",
|
||||
|
||||
// MatrixRTC events
|
||||
CallNotify = "org.matrix.msc4075.call.notify",
|
||||
}
|
||||
|
||||
export enum RelationType {
|
||||
|
||||
+4
-1
@@ -895,7 +895,7 @@ interface IRoomHierarchy {
|
||||
|
||||
export interface TimestampToEventResponse {
|
||||
event_id: string;
|
||||
origin_server_ts: string;
|
||||
origin_server_ts: number;
|
||||
}
|
||||
|
||||
interface IWhoamiResponse {
|
||||
@@ -1494,6 +1494,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
this.ignoredInvites = new IgnoredInvites(this);
|
||||
this._secretStorage = new ServerSideSecretStorageImpl(this, opts.cryptoCallbacks ?? {});
|
||||
|
||||
// having lots of event listeners is not unusual. 0 means "unlimited".
|
||||
this.setMaxListeners(0);
|
||||
}
|
||||
|
||||
public set store(newStore: Store) {
|
||||
|
||||
+50
-9
@@ -18,7 +18,18 @@ limitations under the License.
|
||||
import loglevel from "loglevel";
|
||||
|
||||
/** Logger interface used within the js-sdk codebase */
|
||||
export interface Logger {
|
||||
export interface Logger extends BaseLogger {
|
||||
/**
|
||||
* Create a child logger.
|
||||
*
|
||||
* @param namespace - name to add to the current logger to generate the child. Some implementations of `Logger`
|
||||
* use this as a prefix; others use a different mechanism.
|
||||
*/
|
||||
getChild(namespace: string): Logger;
|
||||
}
|
||||
|
||||
/** The basic interface for a logger which doesn't support children */
|
||||
interface BaseLogger {
|
||||
/**
|
||||
* Output trace message to the logger, with stack trace.
|
||||
*
|
||||
@@ -53,14 +64,6 @@ export interface Logger {
|
||||
* @param msg - Data to log.
|
||||
*/
|
||||
error(...msg: any[]): void;
|
||||
|
||||
/**
|
||||
* Create a child logger.
|
||||
*
|
||||
* @param namespace - name to add to the current logger to generate the child. Some implementations of `Logger`
|
||||
* use this as a prefix; others use a different mechanism.
|
||||
*/
|
||||
getChild(namespace: string): Logger;
|
||||
}
|
||||
|
||||
// This is to demonstrate, that you can use any namespace you want.
|
||||
@@ -139,3 +142,41 @@ function getPrefixedLogger(prefix: string): PrefixedLogger {
|
||||
export const logger = loglevel.getLogger(DEFAULT_NAMESPACE) as PrefixedLogger;
|
||||
logger.setLevel(loglevel.levels.DEBUG, false);
|
||||
extendLogger(logger);
|
||||
|
||||
/**
|
||||
* A "span" for grouping related log lines together.
|
||||
*
|
||||
* The current implementation just adds the name at the start of each log line.
|
||||
*
|
||||
* This offers a lighter-weight alternative to 'child' loggers returned by {@link Logger#getChild}. In particular,
|
||||
* it's not possible to apply individual filters to the LogSpan such as setting the verbosity level. On the other hand,
|
||||
* no reference to the LogSpan is retained in the logging framework, so it is safe to make lots of them over the course
|
||||
* of an application's life and just drop references to them when the job is done.
|
||||
*/
|
||||
export class LogSpan implements BaseLogger {
|
||||
private readonly name;
|
||||
|
||||
public constructor(private readonly parent: BaseLogger, name: string) {
|
||||
this.name = name + ":";
|
||||
}
|
||||
|
||||
public trace(...msg: any[]): void {
|
||||
this.parent.trace(this.name, ...msg);
|
||||
}
|
||||
|
||||
public debug(...msg: any[]): void {
|
||||
this.parent.debug(this.name, ...msg);
|
||||
}
|
||||
|
||||
public info(...msg: any[]): void {
|
||||
this.parent.info(this.name, ...msg);
|
||||
}
|
||||
|
||||
public warn(...msg: any[]): void {
|
||||
this.parent.warn(this.name, ...msg);
|
||||
}
|
||||
|
||||
public error(...msg: any[]): void {
|
||||
this.parent.error(this.name, ...msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ export class CallMembership {
|
||||
}
|
||||
|
||||
public isExpired(): boolean {
|
||||
return this.getAbsoluteExpiry() < this.parentEvent.getTs() + this.parentEvent.getLocalAge();
|
||||
return this.getMsUntilExpiry() <= 0;
|
||||
}
|
||||
|
||||
public getActiveFoci(): Focus[] {
|
||||
|
||||
@@ -78,6 +78,9 @@ export type MatrixRTCSessionEventHandlerMap = {
|
||||
* This class doesn't deal with media at all, just membership & properties of a session.
|
||||
*/
|
||||
export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, MatrixRTCSessionEventHandlerMap> {
|
||||
// The session Id of the call, this is the call_id of the call Member event.
|
||||
private _callId: string | undefined;
|
||||
|
||||
// How many ms after we joined the call, that our membership should expire, or undefined
|
||||
// if we're not yet joined
|
||||
private relativeExpiry: number | undefined;
|
||||
@@ -106,6 +109,15 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
private encryptionKeys = new Map<string, Array<Uint8Array>>();
|
||||
private lastEncryptionKeyUpdateRequest?: number;
|
||||
|
||||
/**
|
||||
* The callId (sessionId) of the call.
|
||||
*
|
||||
* It can be undefined since the callId is only known once the first membership joins.
|
||||
* The callId is the property that, per definition, groups memberships into one call.
|
||||
*/
|
||||
public get callId(): string | undefined {
|
||||
return this._callId;
|
||||
}
|
||||
/**
|
||||
* Returns all the call memberships for a room, oldest first
|
||||
*/
|
||||
@@ -178,6 +190,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
public memberships: CallMembership[],
|
||||
) {
|
||||
super();
|
||||
this._callId = memberships[0]?.callId;
|
||||
this.setExpiryTimer();
|
||||
}
|
||||
|
||||
@@ -551,6 +564,8 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
const oldMemberships = this.memberships;
|
||||
this.memberships = MatrixRTCSession.callMembershipsForRoom(this.room);
|
||||
|
||||
this._callId = this._callId ?? this.memberships[0]?.callId;
|
||||
|
||||
const changed =
|
||||
oldMemberships.length != this.memberships.length ||
|
||||
oldMemberships.some((m, i) => !CallMembership.equal(m, this.memberships[i]));
|
||||
@@ -563,7 +578,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
const isMyMembership = (m: CallMembership): boolean =>
|
||||
m.sender === this.client.getUserId() && m.deviceId === this.client.getDeviceId();
|
||||
|
||||
if (this.isJoined() && this.makeNewKeyTimeout === undefined) {
|
||||
if (this.manageMediaKeys && this.isJoined() && this.makeNewKeyTimeout === undefined) {
|
||||
const oldMebershipIds = new Set(
|
||||
oldMemberships.filter((m) => !isMyMembership(m)).map(getParticipantIdFromMembership),
|
||||
);
|
||||
@@ -767,6 +782,8 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
}
|
||||
|
||||
private onRotateKeyTimeout = (): void => {
|
||||
if (!this.manageMediaKeys) return;
|
||||
|
||||
this.makeNewKeyTimeout = undefined;
|
||||
logger.info("Making new sender key for key rotation");
|
||||
this.makeNewSenderKey(true);
|
||||
|
||||
+10
-1
@@ -13,7 +13,7 @@ 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 { IMentions } from "../matrix";
|
||||
export interface EncryptionKeyEntry {
|
||||
index: number;
|
||||
key: string;
|
||||
@@ -24,3 +24,12 @@ export interface EncryptionKeysEventContent {
|
||||
device_id: string;
|
||||
call_id: string;
|
||||
}
|
||||
|
||||
export type CallNotifyType = "ring" | "notify";
|
||||
|
||||
export interface ICallNotifyContent {
|
||||
"application": string;
|
||||
"m.mentions": IMentions;
|
||||
"notify_type": CallNotifyType;
|
||||
"call_id": string;
|
||||
}
|
||||
|
||||
@@ -162,9 +162,8 @@ export class MSC3089Branch {
|
||||
|
||||
if (!event) throw new Error("Failed to find event");
|
||||
|
||||
// Sometimes the event isn't decrypted for us, so do that. We specifically set `emit: true`
|
||||
// to ensure that the relations system in the sdk will function.
|
||||
await this.client.decryptEventIfNeeded(event, { emit: true, isRetry: true });
|
||||
// Sometimes the event isn't decrypted for us, so do that.
|
||||
await this.client.decryptEventIfNeeded(event);
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
+65
-5
@@ -45,6 +45,8 @@ import { DecryptionError } from "../crypto/algorithms";
|
||||
import { CryptoBackend } from "../common-crypto/CryptoBackend";
|
||||
import { WITHHELD_MESSAGES } from "../crypto/OlmDevice";
|
||||
import { IAnnotatedPushRule } from "../@types/PushRules";
|
||||
import { Room } from "./room";
|
||||
import { EventTimeline } from "./event-timeline";
|
||||
|
||||
export { EventStatus } from "./event-status";
|
||||
|
||||
@@ -175,11 +177,23 @@ interface IKeyRequestRecipient {
|
||||
}
|
||||
|
||||
export interface IDecryptOptions {
|
||||
// Emits "event.decrypted" if set to true
|
||||
/** Whether to emit {@link MatrixEventEvent.Decrypted} events on successful decryption. Defaults to true.
|
||||
*/
|
||||
emit?: boolean;
|
||||
// True if this is a retry (enables more logging)
|
||||
|
||||
/**
|
||||
* True if this is a retry, after receiving an update to the session key. (Enables more logging.)
|
||||
*
|
||||
* This is only intended for use within the js-sdk.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
isRetry?: boolean;
|
||||
// whether the message should be re-decrypted if it was previously successfully decrypted with an untrusted key
|
||||
|
||||
/**
|
||||
* Whether the message should be re-decrypted if it was previously successfully decrypted with an untrusted key.
|
||||
* Defaults to `false`.
|
||||
*/
|
||||
forceRedecryptIfUntrusted?: boolean;
|
||||
}
|
||||
|
||||
@@ -390,7 +404,13 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
});
|
||||
|
||||
this.txnId = event.txn_id;
|
||||
this.localTimestamp = Date.now() - (this.getAge() ?? 0);
|
||||
// The localTimestamp is calculated using the age.
|
||||
// Some events lack an `age` property, either because they are EDUs such as typing events,
|
||||
// or due to server-side bugs such as https://github.com/matrix-org/synapse/issues/8429.
|
||||
// The fallback in these cases will be to use the origin_server_ts.
|
||||
// For EDUs, the origin_server_ts also is not defined so we use Date.now().
|
||||
const age = this.getAge();
|
||||
this.localTimestamp = age !== undefined ? Date.now() - age : this.getTs() ?? Date.now();
|
||||
this.reEmitter = new TypedReEmitter(this);
|
||||
}
|
||||
|
||||
@@ -1135,13 +1155,19 @@ 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
|
||||
*
|
||||
* @param redactionEvent - event causing the redaction
|
||||
* @param room - the room in which the event exists
|
||||
*/
|
||||
public makeRedacted(redactionEvent: MatrixEvent): 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");
|
||||
@@ -1185,9 +1211,43 @@ 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()) {
|
||||
this.moveAllRelatedToMainTimeline(room);
|
||||
redactionEvent.moveToMainTimeline(room);
|
||||
}
|
||||
|
||||
this.invalidateExtensibleEvent();
|
||||
}
|
||||
|
||||
private moveAllRelatedToMainTimeline(room: Room): void {
|
||||
const thread = this.thread;
|
||||
this.moveToMainTimeline(room);
|
||||
|
||||
// If we dont have access to the thread, we can only move this
|
||||
// event, not things related to it.
|
||||
if (thread) {
|
||||
for (const event of thread.events) {
|
||||
if (event.getRelation()?.event_id === this.getId()) {
|
||||
event.moveAllRelatedToMainTimeline(room);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private moveToMainTimeline(room: Room): void {
|
||||
// Remove it from its thread
|
||||
this.thread?.timelineSet.removeEvent(this.getId()!);
|
||||
this.setThread(undefined);
|
||||
|
||||
// And insert it into the main timeline
|
||||
const timeline = room.getLiveTimeline();
|
||||
// We use insertEventIntoTimeline to insert it in timestamp order,
|
||||
// because we don't know where it should go (until we have MSC4033).
|
||||
timeline.getTimelineSet().insertEventIntoTimeline(this, timeline, timeline.getState(EventTimeline.FORWARDS)!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this event has been redacted
|
||||
*
|
||||
|
||||
@@ -143,7 +143,6 @@ export abstract class ReadReceipt<
|
||||
//
|
||||
// 4. The receipt had the incorrect thread ID (due to a bug in a
|
||||
// client, or malicious behaviour).
|
||||
logger.warn(`Ignoring receipt for missing event with id ${receipt.eventId}`);
|
||||
|
||||
// This receipt is not "valid" because it doesn't point at an event
|
||||
// we have. We want to pretend it doesn't exist.
|
||||
|
||||
+14
-6
@@ -236,8 +236,9 @@ export type RoomEventHandlerMap = {
|
||||
*
|
||||
* @param event - The matrix redaction event
|
||||
* @param room - The room containing the redacted event
|
||||
* @param threadId - The thread containing the redacted event (before it was redacted)
|
||||
*/
|
||||
[RoomEvent.Redaction]: (event: MatrixEvent, room: Room) => void;
|
||||
[RoomEvent.Redaction]: (event: MatrixEvent, room: Room, threadId?: string) => void;
|
||||
/**
|
||||
* Fires when an event that was previously redacted isn't anymore.
|
||||
* This happens when the redaction couldn't be sent and
|
||||
@@ -549,7 +550,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
const decryptionPromises = events
|
||||
.slice(readReceiptTimelineIndex)
|
||||
.reverse()
|
||||
.map((event) => this.client.decryptEventIfNeeded(event, { isRetry: true }));
|
||||
.map((event) => this.client.decryptEventIfNeeded(event));
|
||||
|
||||
await Promise.allSettled(decryptionPromises);
|
||||
}
|
||||
@@ -567,7 +568,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
.getEvents()
|
||||
.slice(0) // copy before reversing
|
||||
.reverse()
|
||||
.map((event) => this.client.decryptEventIfNeeded(event, { isRetry: true }));
|
||||
.map((event) => this.client.decryptEventIfNeeded(event));
|
||||
|
||||
await Promise.allSettled(decryptionPromises);
|
||||
}
|
||||
@@ -2113,6 +2114,12 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* Relations (other than m.thread), redactions, replies to a thread root live only in the main timeline
|
||||
* Relations, redactions, replies where the parent cannot be found live in no timelines but should be aggregated regardless.
|
||||
* Otherwise, the event lives in the main timeline only.
|
||||
*
|
||||
* Note: when a redaction is applied, the redacted event, events relating
|
||||
* to it, and the redaction event itself, will all move to the main thread.
|
||||
* This method classifies them as inside the thread of the redacted event.
|
||||
* They are moved later as part of makeRedacted.
|
||||
* This will change if MSC3389 is merged.
|
||||
*/
|
||||
public eventShouldLiveIn(
|
||||
event: MatrixEvent,
|
||||
@@ -2329,7 +2336,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
// if we know about this event, redact its contents now.
|
||||
const redactedEvent = redactId ? this.findEventById(redactId) : undefined;
|
||||
if (redactedEvent) {
|
||||
redactedEvent.makeRedacted(event);
|
||||
const threadRootId = redactedEvent.threadRootId;
|
||||
redactedEvent.makeRedacted(event, this);
|
||||
|
||||
// If this is in the current state, replace it with the redacted version
|
||||
if (redactedEvent.isState()) {
|
||||
@@ -2342,7 +2350,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
}
|
||||
|
||||
this.emit(RoomEvent.Redaction, event, this);
|
||||
this.emit(RoomEvent.Redaction, event, this, threadRootId);
|
||||
|
||||
// TODO: we stash user displaynames (among other things) in
|
||||
// RoomMember objects which are then attached to other events
|
||||
@@ -2495,7 +2503,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
if (redactedEvent) {
|
||||
redactedEvent.markLocallyRedacted(event);
|
||||
this.emit(RoomEvent.Redaction, event, this);
|
||||
this.emit(RoomEvent.Redaction, event, this, redactedEvent.threadRootId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -143,6 +143,9 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
public constructor(public readonly id: string, public rootEvent: MatrixEvent | undefined, opts: IThreadOpts) {
|
||||
super();
|
||||
|
||||
// each Event in the thread adds a reemitter, so we could hit the listener limit.
|
||||
this.setMaxListeners(1000);
|
||||
|
||||
if (!opts?.room) {
|
||||
// Logging/debugging for https://github.com/vector-im/element-web/issues/22141
|
||||
// Hope is that we end up with a more obvious stack trace.
|
||||
@@ -228,8 +231,8 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
}
|
||||
};
|
||||
|
||||
private onRedaction = async (event: MatrixEvent): Promise<void> => {
|
||||
if (event.threadRootId !== this.id) return; // ignore redactions for other timelines
|
||||
private onRedaction = async (event: MatrixEvent, room: Room, threadRootId?: string): Promise<void> => {
|
||||
if (threadRootId !== this.id) return; // ignore redactions for other timelines
|
||||
if (this.replyCount <= 0) {
|
||||
for (const threadEvent of this.timeline) {
|
||||
this.clearEventMetadata(threadEvent);
|
||||
@@ -368,7 +371,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
if (!Thread.hasServerSideSupport) {
|
||||
// When there's no server-side support, just add it to the end of the timeline.
|
||||
this.addEventToTimeline(event, toStartOfTimeline);
|
||||
this.client.decryptEventIfNeeded(event, {});
|
||||
this.client.decryptEventIfNeeded(event);
|
||||
} else if (!toStartOfTimeline && this.initialEventsFetched && isNewestReply) {
|
||||
// When we've asked for the event to be added to the end, and we're
|
||||
// not in the initial state, and this event belongs at the end, add it.
|
||||
|
||||
@@ -91,10 +91,13 @@ export class CrossSigningIdentity {
|
||||
this.olmMachine.userId,
|
||||
this.olmMachine.deviceId,
|
||||
);
|
||||
|
||||
// Sign the device with our cross-signing key and upload the signature
|
||||
const request: RustSdkCryptoJs.SignatureUploadRequest = await device.verify();
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(request);
|
||||
try {
|
||||
// Sign the device with our cross-signing key and upload the signature
|
||||
const request: RustSdkCryptoJs.SignatureUploadRequest = await device.verify();
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(request);
|
||||
} finally {
|
||||
device.free();
|
||||
}
|
||||
} else {
|
||||
logger.log(
|
||||
"bootStrapCrossSigning: Cross-signing private keys not found locally or in secret storage, creating new keys",
|
||||
|
||||
@@ -17,6 +17,7 @@ limitations under the License.
|
||||
import { OlmMachine, UserId } from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
|
||||
import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
|
||||
import { LogSpan } from "../logger";
|
||||
|
||||
/**
|
||||
* KeyClaimManager: linearises calls to OlmMachine.getMissingSessions to avoid races
|
||||
@@ -52,7 +53,7 @@ export class KeyClaimManager {
|
||||
*
|
||||
* @param userList - list of userIDs to claim
|
||||
*/
|
||||
public ensureSessionsForUsers(userList: Array<UserId>): Promise<void> {
|
||||
public ensureSessionsForUsers(logger: LogSpan, userList: Array<UserId>): Promise<void> {
|
||||
// The Rust-SDK requires that we only have one getMissingSessions process in flight at once. This little dance
|
||||
// ensures that, by only having one call to ensureSessionsForUsersInner active at once (and making them
|
||||
// queue up in order).
|
||||
@@ -61,19 +62,22 @@ export class KeyClaimManager {
|
||||
// any errors in the previous claim will have been reported already, so there is nothing to do here.
|
||||
// we just throw away the error and start anew.
|
||||
})
|
||||
.then(() => this.ensureSessionsForUsersInner(userList));
|
||||
.then(() => this.ensureSessionsForUsersInner(logger, userList));
|
||||
this.currentClaimPromise = prom;
|
||||
return prom;
|
||||
}
|
||||
|
||||
private async ensureSessionsForUsersInner(userList: Array<UserId>): Promise<void> {
|
||||
private async ensureSessionsForUsersInner(logger: LogSpan, userList: Array<UserId>): Promise<void> {
|
||||
// bail out quickly if we've been stopped.
|
||||
if (this.stopped) {
|
||||
throw new Error(`Cannot ensure Olm sessions: shutting down`);
|
||||
}
|
||||
logger.info("Checking for missing Olm sessions");
|
||||
const claimRequest = await this.olmMachine.getMissingSessions(userList);
|
||||
if (claimRequest) {
|
||||
logger.info("Making /keys/claim request");
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(claimRequest);
|
||||
}
|
||||
logger.info("Olm sessions prepared");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
import { EventType } from "../@types/event";
|
||||
import { IContent, MatrixEvent } from "../models/event";
|
||||
import { Room } from "../models/room";
|
||||
import { Logger, logger } from "../logger";
|
||||
import { Logger, logger, LogSpan } from "../logger";
|
||||
import { KeyClaimManager } from "./KeyClaimManager";
|
||||
import { RoomMember } from "../models/room-member";
|
||||
import { HistoryVisibility } from "../@types/partials";
|
||||
@@ -64,12 +64,13 @@ export class RoomEncryptor {
|
||||
// start tracking devices for any users already known to be in this room.
|
||||
// Do not load members here, would defeat lazy loading.
|
||||
const members = room.getJoinedMembers();
|
||||
|
||||
// At this point just mark the known members as tracked, it might not be the full list of members
|
||||
// because of lazy loading. This is fine, because we will get a member list update when sending a message for
|
||||
// the first time, see `RoomEncryptor#ensureEncryptionSession`
|
||||
this.olmMachine.updateTrackedUsers(members.map((u) => new RustSdkCryptoJs.UserId(u.userId))).then(() => {
|
||||
this.prefixedLogger.debug(`Updated tracked users for room ${room.roomId}`);
|
||||
});
|
||||
this.olmMachine
|
||||
.updateTrackedUsers(members.map((u) => new RustSdkCryptoJs.UserId(u.userId)))
|
||||
.catch((e) => this.prefixedLogger.error("Error initializing tracked users", e));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,12 +111,27 @@ export class RoomEncryptor {
|
||||
*
|
||||
* @param globalBlacklistUnverifiedDevices - When `true`, it will not send encrypted messages to unverified devices
|
||||
*/
|
||||
public async ensureEncryptionSession(globalBlacklistUnverifiedDevices: boolean): Promise<void> {
|
||||
public async prepareForEncryption(globalBlacklistUnverifiedDevices: boolean): Promise<void> {
|
||||
const logger = new LogSpan(this.prefixedLogger, "prepareForEncryption");
|
||||
await this.ensureEncryptionSession(logger, globalBlacklistUnverifiedDevices);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare to encrypt events in this room.
|
||||
*
|
||||
* This ensures that we have a megolm session ready to use and that we have shared its key with all the devices
|
||||
* in the room.
|
||||
*
|
||||
* @param logger - a place to write diagnostics to
|
||||
* @param globalBlacklistUnverifiedDevices - When `true`, it will not send encrypted messages to unverified devices
|
||||
*/
|
||||
private async ensureEncryptionSession(logger: LogSpan, globalBlacklistUnverifiedDevices: boolean): Promise<void> {
|
||||
if (this.encryptionSettings.algorithm !== "m.megolm.v1.aes-sha2") {
|
||||
throw new Error(
|
||||
`Cannot encrypt in ${this.room.roomId} for unsupported algorithm '${this.encryptionSettings.algorithm}'`,
|
||||
);
|
||||
}
|
||||
logger.debug("Starting encryption");
|
||||
|
||||
const members = await this.room.getEncryptionTargetMembers();
|
||||
|
||||
@@ -127,28 +143,38 @@ export class RoomEncryptor {
|
||||
// not a particular problem, since `OlmMachine.updateTrackedUsers` just adds any users that weren't already tracked.
|
||||
if (!this.lazyLoadedMembersResolved) {
|
||||
await this.olmMachine.updateTrackedUsers(members.map((u) => new RustSdkCryptoJs.UserId(u.userId)));
|
||||
logger.debug(`Updated tracked users`);
|
||||
this.lazyLoadedMembersResolved = true;
|
||||
this.prefixedLogger.debug(`Updated tracked users for room ${this.room.roomId}`);
|
||||
|
||||
// Query keys in case we don't have them for newly tracked members.
|
||||
// It's important after loading members for the first time, as likely most of them won't be
|
||||
// known yet and will be unable to decrypt messages despite being in the room for long.
|
||||
// This must be done before ensuring sessions. If not the devices of these users are not
|
||||
// known yet and will not get the room key.
|
||||
// We don't have API to only get the keys queries related to this member list, so we just
|
||||
// process the pending requests from the olmMachine. (usually these are processed
|
||||
// at the end of the sync, but we can't wait for that).
|
||||
// XXX future improvement process only KeysQueryRequests for the users that have never been queried.
|
||||
logger.debug(`Processing outgoing requests`);
|
||||
await this.outgoingRequestManager.doProcessOutgoingRequests();
|
||||
} else {
|
||||
// If members are already loaded it's less critical to await on key queries.
|
||||
// We might still want to trigger a processOutgoingRequests here.
|
||||
// The call to `ensureSessionsForUsers` below will wait a bit on in-flight key queries we are
|
||||
// interested in. If a sync handling happens in the meantime, and some new members are added to the room
|
||||
// or have new devices it would give us a chance to query them before sending.
|
||||
// It's less critical due to the racy nature of this process.
|
||||
logger.debug(`Processing outgoing requests in background`);
|
||||
this.outgoingRequestManager.doProcessOutgoingRequests();
|
||||
}
|
||||
|
||||
// Query keys in case we don't have them for newly tracked members.
|
||||
// This must be done before ensuring sessions. If not the devices of these users are not
|
||||
// known yet and will not get the room key.
|
||||
// We don't have API to only get the keys queries related to this member list, so we just
|
||||
// process the pending requests from the olmMachine. (usually these are processed
|
||||
// at the end of the sync, but we can't wait for that).
|
||||
// XXX future improvement process only KeysQueryRequests for the tracked users.
|
||||
await this.outgoingRequestManager.doProcessOutgoingRequests();
|
||||
|
||||
this.prefixedLogger.debug(
|
||||
logger.debug(
|
||||
`Encrypting for users (shouldEncryptForInvitedMembers: ${this.room.shouldEncryptForInvitedMembers()}):`,
|
||||
members.map((u) => `${u.userId} (${u.membership})`),
|
||||
);
|
||||
|
||||
const userList = members.map((u) => new UserId(u.userId));
|
||||
await this.keyClaimManager.ensureSessionsForUsers(userList);
|
||||
|
||||
this.prefixedLogger.debug("Sessions for users are ready; now sharing room key");
|
||||
await this.keyClaimManager.ensureSessionsForUsers(logger, userList);
|
||||
|
||||
const rustEncryptionSettings = new EncryptionSettings();
|
||||
rustEncryptionSettings.historyVisibility = toRustHistoryVisibility(this.room.getHistoryVisibility());
|
||||
@@ -204,8 +230,10 @@ export class RoomEncryptor {
|
||||
* @param globalBlacklistUnverifiedDevices - When `true`, it will not send encrypted messages to unverified devices
|
||||
*/
|
||||
public async encryptEvent(event: MatrixEvent, globalBlacklistUnverifiedDevices: boolean): Promise<void> {
|
||||
await this.ensureEncryptionSession(globalBlacklistUnverifiedDevices);
|
||||
const logger = new LogSpan(this.prefixedLogger, event.getTxnId() ?? "");
|
||||
await this.ensureEncryptionSession(logger, globalBlacklistUnverifiedDevices);
|
||||
|
||||
logger.debug("Encrypting actual message content");
|
||||
const encryptedContent = await this.olmMachine.encryptRoomEvent(
|
||||
new RoomId(this.room.roomId),
|
||||
event.getType(),
|
||||
@@ -218,6 +246,8 @@ export class RoomEncryptor {
|
||||
this.olmMachine.identityKeys.curve25519.toBase64(),
|
||||
this.olmMachine.identityKeys.ed25519.toBase64(),
|
||||
);
|
||||
|
||||
logger.debug("Encrypted event successfully");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+112
-69
@@ -375,7 +375,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
const encryptor = this.roomEncryptors[room.roomId];
|
||||
|
||||
if (encryptor) {
|
||||
encryptor.ensureEncryptionSession(this.globalBlacklistUnverifiedDevices);
|
||||
encryptor.prepareForEncryption(this.globalBlacklistUnverifiedDevices);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,6 +425,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(request);
|
||||
}
|
||||
const userIdentity = await this.olmMachine.getIdentity(rustTrackedUser);
|
||||
userIdentity?.free();
|
||||
return userIdentity !== undefined;
|
||||
} else if (downloadUncached) {
|
||||
// Download the cross signing keys and check if the master key is available
|
||||
@@ -562,7 +563,13 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
if (!device) {
|
||||
throw new Error(`Unknown device ${userId}|${deviceId}`);
|
||||
}
|
||||
await device.setLocalTrust(verified ? RustSdkCryptoJs.LocalTrust.Verified : RustSdkCryptoJs.LocalTrust.Unset);
|
||||
try {
|
||||
await device.setLocalTrust(
|
||||
verified ? RustSdkCryptoJs.LocalTrust.Verified : RustSdkCryptoJs.LocalTrust.Unset,
|
||||
);
|
||||
} finally {
|
||||
device.free();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -578,13 +585,16 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
);
|
||||
|
||||
if (!device) return null;
|
||||
|
||||
return new DeviceVerificationStatus({
|
||||
signedByOwner: device.isCrossSignedByOwner(),
|
||||
crossSigningVerified: device.isCrossSigningTrusted(),
|
||||
localVerified: device.isLocallyTrusted(),
|
||||
trustCrossSignedDevices: this._trustCrossSignedDevices,
|
||||
});
|
||||
try {
|
||||
return new DeviceVerificationStatus({
|
||||
signedByOwner: device.isCrossSignedByOwner(),
|
||||
crossSigningVerified: device.isCrossSigningTrusted(),
|
||||
localVerified: device.isLocallyTrusted(),
|
||||
trustCrossSignedDevices: this._trustCrossSignedDevices,
|
||||
});
|
||||
} finally {
|
||||
device.free();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -596,7 +606,9 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
if (userIdentity === undefined) {
|
||||
return new UserVerificationStatus(false, false, false);
|
||||
}
|
||||
return new UserVerificationStatus(userIdentity.isVerified(), false, false);
|
||||
const verified = userIdentity.isVerified();
|
||||
userIdentity.free();
|
||||
return new UserVerificationStatus(verified, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -621,42 +633,51 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
const userIdentity: RustSdkCryptoJs.OwnUserIdentity | undefined = await this.olmMachine.getIdentity(
|
||||
new RustSdkCryptoJs.UserId(this.userId),
|
||||
);
|
||||
|
||||
const crossSigningStatus: RustSdkCryptoJs.CrossSigningStatus = await this.olmMachine.crossSigningStatus();
|
||||
const privateKeysOnDevice =
|
||||
crossSigningStatus.hasMaster && crossSigningStatus.hasUserSigning && crossSigningStatus.hasSelfSigning;
|
||||
|
||||
if (!userIdentity || !privateKeysOnDevice) {
|
||||
// The public or private keys are not available on this device
|
||||
if (!userIdentity) {
|
||||
// The public keys are not available on this device
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!userIdentity.isVerified()) {
|
||||
// We have both public and private keys, but they don't match!
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const crossSigningStatus: RustSdkCryptoJs.CrossSigningStatus = await this.olmMachine.crossSigningStatus();
|
||||
|
||||
let key: string;
|
||||
switch (type) {
|
||||
case CrossSigningKey.Master:
|
||||
key = userIdentity.masterKey;
|
||||
break;
|
||||
case CrossSigningKey.SelfSigning:
|
||||
key = userIdentity.selfSigningKey;
|
||||
break;
|
||||
case CrossSigningKey.UserSigning:
|
||||
key = userIdentity.userSigningKey;
|
||||
break;
|
||||
default:
|
||||
// Unknown type
|
||||
const privateKeysOnDevice =
|
||||
crossSigningStatus.hasMaster && crossSigningStatus.hasUserSigning && crossSigningStatus.hasSelfSigning;
|
||||
|
||||
if (!privateKeysOnDevice) {
|
||||
// The private keys are not available on this device
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const parsedKey: CrossSigningKeyInfo = JSON.parse(key);
|
||||
// `keys` is an object with { [`ed25519:${pubKey}`]: pubKey }
|
||||
// We assume only a single key, and we want the bare form without type
|
||||
// prefix, so we select the values.
|
||||
return Object.values(parsedKey.keys)[0];
|
||||
if (!userIdentity.isVerified()) {
|
||||
// We have both public and private keys, but they don't match!
|
||||
return null;
|
||||
}
|
||||
|
||||
let key: string;
|
||||
switch (type) {
|
||||
case CrossSigningKey.Master:
|
||||
key = userIdentity.masterKey;
|
||||
break;
|
||||
case CrossSigningKey.SelfSigning:
|
||||
key = userIdentity.selfSigningKey;
|
||||
break;
|
||||
case CrossSigningKey.UserSigning:
|
||||
key = userIdentity.userSigningKey;
|
||||
break;
|
||||
default:
|
||||
// Unknown type
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsedKey: CrossSigningKeyInfo = JSON.parse(key);
|
||||
// `keys` is an object with { [`ed25519:${pubKey}`]: pubKey }
|
||||
// We assume only a single key, and we want the bare form without type
|
||||
// prefix, so we select the values.
|
||||
return Object.values(parsedKey.keys)[0];
|
||||
} finally {
|
||||
userIdentity.free();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -800,6 +821,8 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
Boolean(userIdentity?.masterKey) &&
|
||||
Boolean(userIdentity?.selfSigningKey) &&
|
||||
Boolean(userIdentity?.userSigningKey);
|
||||
userIdentity?.free();
|
||||
|
||||
const privateKeysInSecretStorage = await secretStorageContainsCrossSigningKeys(this.secretStorage);
|
||||
const crossSigningStatus: RustSdkCryptoJs.CrossSigningStatus | null =
|
||||
await this.getOlmMachineOrThrow().crossSigningStatus();
|
||||
@@ -917,23 +940,31 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
|
||||
if (!userIdentity) throw new Error(`unknown userId ${userId}`);
|
||||
|
||||
// Transform the verification methods into rust objects
|
||||
const methods = this._supportedVerificationMethods.map((method) =>
|
||||
verificationMethodIdentifierToMethod(method),
|
||||
);
|
||||
// Get the request content to send to the DM room
|
||||
const verificationEventContent: string = await userIdentity.verificationRequestContent(methods);
|
||||
try {
|
||||
// Transform the verification methods into rust objects
|
||||
const methods = this._supportedVerificationMethods.map((method) =>
|
||||
verificationMethodIdentifierToMethod(method),
|
||||
);
|
||||
// Get the request content to send to the DM room
|
||||
const verificationEventContent: string = await userIdentity.verificationRequestContent(methods);
|
||||
|
||||
// Send the request content to send to the DM room
|
||||
const eventId = await this.sendVerificationRequestContent(roomId, verificationEventContent);
|
||||
// Send the request content to send to the DM room
|
||||
const eventId = await this.sendVerificationRequestContent(roomId, verificationEventContent);
|
||||
|
||||
// Get a verification request
|
||||
const request: RustSdkCryptoJs.VerificationRequest = await userIdentity.requestVerification(
|
||||
new RustSdkCryptoJs.RoomId(roomId),
|
||||
new RustSdkCryptoJs.EventId(eventId),
|
||||
methods,
|
||||
);
|
||||
return new RustVerificationRequest(request, this.outgoingRequestProcessor, this._supportedVerificationMethods);
|
||||
// Get a verification request
|
||||
const request: RustSdkCryptoJs.VerificationRequest = await userIdentity.requestVerification(
|
||||
new RustSdkCryptoJs.RoomId(roomId),
|
||||
new RustSdkCryptoJs.EventId(eventId),
|
||||
methods,
|
||||
);
|
||||
return new RustVerificationRequest(
|
||||
request,
|
||||
this.outgoingRequestProcessor,
|
||||
this._supportedVerificationMethods,
|
||||
);
|
||||
} finally {
|
||||
userIdentity.free();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -995,12 +1026,20 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
throw new Error("cannot request verification for this device when there is no existing cross-signing key");
|
||||
}
|
||||
|
||||
const [request, outgoingRequest]: [RustSdkCryptoJs.VerificationRequest, RustSdkCryptoJs.ToDeviceRequest] =
|
||||
await userIdentity.requestVerification(
|
||||
this._supportedVerificationMethods.map(verificationMethodIdentifierToMethod),
|
||||
try {
|
||||
const [request, outgoingRequest]: [RustSdkCryptoJs.VerificationRequest, RustSdkCryptoJs.ToDeviceRequest] =
|
||||
await userIdentity.requestVerification(
|
||||
this._supportedVerificationMethods.map(verificationMethodIdentifierToMethod),
|
||||
);
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(outgoingRequest);
|
||||
return new RustVerificationRequest(
|
||||
request,
|
||||
this.outgoingRequestProcessor,
|
||||
this._supportedVerificationMethods,
|
||||
);
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(outgoingRequest);
|
||||
return new RustVerificationRequest(request, this.outgoingRequestProcessor, this._supportedVerificationMethods);
|
||||
} finally {
|
||||
userIdentity.free();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1025,12 +1064,20 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
throw new Error("Not a known device");
|
||||
}
|
||||
|
||||
const [request, outgoingRequest]: [RustSdkCryptoJs.VerificationRequest, RustSdkCryptoJs.ToDeviceRequest] =
|
||||
await device.requestVerification(
|
||||
this._supportedVerificationMethods.map(verificationMethodIdentifierToMethod),
|
||||
try {
|
||||
const [request, outgoingRequest]: [RustSdkCryptoJs.VerificationRequest, RustSdkCryptoJs.ToDeviceRequest] =
|
||||
await device.requestVerification(
|
||||
this._supportedVerificationMethods.map(verificationMethodIdentifierToMethod),
|
||||
);
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(outgoingRequest);
|
||||
return new RustVerificationRequest(
|
||||
request,
|
||||
this.outgoingRequestProcessor,
|
||||
this._supportedVerificationMethods,
|
||||
);
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(outgoingRequest);
|
||||
return new RustVerificationRequest(request, this.outgoingRequestProcessor, this._supportedVerificationMethods);
|
||||
} finally {
|
||||
device.free();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1560,10 +1607,6 @@ class EventDecryptor {
|
||||
) {}
|
||||
|
||||
public async attemptEventDecryption(event: MatrixEvent): Promise<IEventDecryptionResult> {
|
||||
this.logger.info(
|
||||
`Attempting decryption of event ${event.getId()} in ${event.getRoomId()} from ${event.getSender()}`,
|
||||
);
|
||||
|
||||
// add the event to the pending list *before* attempting to decrypt.
|
||||
// then, if the key turns up while decryption is in progress (and
|
||||
// decryption fails), we will schedule a retry.
|
||||
|
||||
@@ -133,7 +133,6 @@ export class GroupCallEventHandler {
|
||||
break;
|
||||
}
|
||||
|
||||
logger.debug(`GroupCallEventHandler createGroupCallForRoom() processed room (roomId=${room.roomId})`);
|
||||
this.getRoomDeferred(room.roomId).resolve!();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user