Compare commits

..

103 Commits

Author SHA1 Message Date
RiotRobot b64a30f0ad v19.6.0 2022-09-27 16:46:53 +01:00
RiotRobot 5451f6139a Prepare changelog for v19.6.0 2022-09-27 16:46:52 +01:00
RiotRobot f34e568a98 v19.6.0-rc.1 2022-09-20 13:55:51 +01:00
RiotRobot 6fd80ed3ed Prepare changelog for v19.6.0-rc.1 2022-09-20 13:55:50 +01:00
Kerry 9ff11d1f32 test typescriptification - last few unit test files (#2675)
* renamed:    spec/unit/crypto/verification/sas.spec.js -> spec/unit/crypto/verification/sas.spec.ts

* ts issues in sas.spec

* renamed:    spec/unit/crypto/verification/secret_request.spec.js -> spec/unit/crypto/verification/secret_request.spec.ts

* ts issues in secret_request.spec

* renamed:    spec/unit/crypto/verification/verification_request.spec.js -> spec/unit/crypto/verification/verification_request.spec.ts

* ts fix verification_req.spec

* renamed:    spec/browserify/sync-browserify.spec.js -> spec/browserify/sync-browserify.spec.ts

* fix strict

* formatting
2022-09-16 16:00:40 +00:00
Kerry 41ab3337b5 test typescriptification - spec/unit/crypto/verification (#2673)
* renamed:    spec/unit/crypto/verification/request.spec.js -> spec/unit/crypto/verification/request.spec.ts

* renamed:    spec/unit/crypto/verification/qr_code.spec.js -> spec/unit/crypto/verification/qr_code.spec.ts

* renamed:    spec/unit/crypto/verification/InRoomChannel.spec.js -> spec/unit/crypto/verification/InRoomChannel.spec.ts

* fix ts issues in InRoomChannel.spec

* renamed:    spec/unit/crypto/verification/util.js -> spec/unit/crypto/verification/util.ts

* fix ts issues in util.t

* fix strict errors in util.ts

* js lint
2022-09-16 10:46:56 +02:00
Michael Telatynski 1432e094c2 Update sonarcloud.yml 2022-09-15 14:26:22 +01:00
Michael Telatynski e09936cc9b Update sonarcloud.yml (#2671) 2022-09-15 14:16:15 +01:00
Germain d32d190a8a Change return type to avoid null strict error (#2630) 2022-09-14 09:02:16 +01:00
Michael Telatynski 53de8d5690 Fix reset_dependency for if the dep is indirect (#2664)
Like for in element-desktop
2022-09-14 08:57:52 +01:00
Michael Telatynski 829110b580 Update release-npm.yml (#2665) 2022-09-14 08:57:36 +01:00
RiotRobot 59c82cb679 Resetting package fields for development 2022-09-13 12:32:41 +01:00
RiotRobot 6d3741d55c Merge branch 'master' into develop 2022-09-13 12:32:37 +01:00
RiotRobot 43213fec78 v19.5.0 2022-09-13 12:31:14 +01:00
RiotRobot c8438ff6da Prepare changelog for v19.5.0 2022-09-13 12:31:13 +01:00
RiotRobot a1d0f037e2 Fix release.sh check_dependency 2022-09-13 12:26:03 +01:00
Šimon Brandner fb565f301b Remove support for unstable private read receipts (#2624) 2022-09-12 18:04:14 +02:00
Michael Telatynski afa3b37ad5 Revert "Add login flow types from matrix-react-sdk (#2633)" (#2661)
* Revert "Add login flow types from matrix-react-sdk (#2633)"

This reverts commit 583e4808

* Leave login flow types, only revert method return type

Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
2022-09-12 11:25:39 +01:00
Hugh Nimmo-Smith a57c430b09 Implementation of MSC3824 to add action= param on SSO login (#2398)
Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-09-11 21:18:33 +00:00
Hugh Nimmo-Smith 583e48086b Add login flow types from matrix-react-sdk (#2633)
Co-authored-by: Faye Duxovni <fayed@element.io>
2022-09-11 21:50:10 +01:00
Robin b22c671fee Add a property aggregating all names of a NamespacedValue (#2656)
For convenience
2022-09-09 09:55:17 -04:00
David Baker 1b86acb2fb Fix import locations (#2655)
These were causing circular references in the group call branch
2022-09-08 20:51:42 +01:00
renovate[bot] d2f7a2575e Update all (major) (#2651)
* Update all

* Pin p-retry once more

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-09-08 09:52:50 +00:00
Michael Telatynski 8490f72488 Tweak backport labels (#2652) 2022-09-07 11:51:11 +00:00
kegsay d87e53858b Merge pull request #2628 from matrix-org/kegan/ss-member-counts
sliding sync: add invited|joined_count
2022-09-07 11:22:47 +02:00
David Teller 917e8c01d8 Base support for MSC3847: Ignore invites with policy rooms (#2626)
* Base support for MSC3847: Ignore invites with policy rooms

Type: enhancement

* Base support for MSC3847: Ignore invites with policy rooms

Type: enhancement

* WIP: Applying feedback

* WIP: Applying feedback

* WIP: CI linter gives me different errors, weird

* WIP: A little more linting
2022-09-06 22:17:42 -06:00
renovate[bot] eb3309db43 Update jest monorepo to v29 (#2649)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-06 22:02:40 -06:00
renovate[bot] 65741d7860 Update all (#2647)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-06 22:05:37 +01:00
renovate[bot] a7a264f4e7 Update typescript-eslint monorepo to v5.36.2 (#2645)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-06 17:53:05 +01:00
renovate[bot] 0fa125b60a Update babel monorepo to v7.19.0 (#2644)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-06 17:52:57 +01:00
Michael Telatynski 8aee884d03 Fix handling of remote echoes doubling up (#2639)
* Fix handling of remote echoes doubling up

* Simplify code

* Make TSC strict happier

* Update `timelineWasEmpty` to match type

* Add tests

* Add tests

* Add lowly `!`
2022-09-06 13:27:24 +01:00
RiotRobot a8fd0f3d13 Fix release.sh check_dependency order 2022-09-06 12:48:52 +01:00
Michael Telatynski 876491e38d Remove redundant concurrency 2022-09-06 12:36:54 +01:00
RiotRobot 71fcd9f35b v19.5.0-rc.4 2022-09-06 12:35:27 +01:00
RiotRobot 193ff0b4d1 Prepare changelog for v19.5.0-rc.4 2022-09-06 12:35:27 +01:00
RiotRobot f616022d07 Fix self-referencing gha concurrency leading to blocked jobs 2022-09-06 12:34:32 +01:00
RiotRobot 3c206c0b85 v19.5.0-rc.3 2022-09-06 12:25:58 +01:00
RiotRobot a8c4ff473a Prepare changelog for v19.5.0-rc.3 2022-09-06 12:25:57 +01:00
RiotRobot 289a930cda Fix release.sh 2022-09-06 12:25:07 +01:00
RiotRobot 8ba30bc4ef v19.5.0-rc.2 2022-09-06 12:21:02 +01:00
RiotRobot 6e28634819 Prepare changelog for v19.5.0-rc.2 2022-09-06 12:21:01 +01:00
RiotRobot b1e70c5404 Fix release.sh 2022-09-06 12:20:41 +01:00
RiotRobot b11c502a40 Merge branch 'develop' into staging 2022-09-06 12:18:10 +01:00
Michael Telatynski 167f51c8cd Fix release script for layers without a release_config.yaml file (#2642) 2022-09-06 12:17:40 +01:00
RiotRobot 5d2753241e Merge branch 'develop' into staging 2022-09-06 12:11:27 +01:00
Michael Telatynski 274fe447fd Simplify releases: move npm publishing to gha, consolidate scripts (#2616)
* Remove stale comment re dependency

* Move npm publishing from release.sh to GHA

* Extract js-sdk & react-sdk post release steps

* Consolidate release subproject upgrade management
2022-09-06 12:10:26 +01:00
RiotRobot c0f1849a83 v19.5.0-rc.1 2022-09-06 11:28:19 +01:00
RiotRobot 7b2b618d26 Prepare changelog for v19.5.0-rc.1 2022-09-06 11:28:19 +01:00
Kerry 37187ef347 Test typescriptification - room-member and room-state (#2601)
* renamed:    spec/MockStorageApi.js -> spec/MockStorageApi.ts

* renamed:    spec/olm-loader.js -> spec/olm-loader.t

* renamed:    spec/unit/room-state.spec.js -> spec/unit/room-state.spec.ts

* ts fixes in room-state.spec

* renamed:    spec/unit/room-member.spec.js -> spec/unit/room-member.spec.ts

* ts fixes in room-member.spec

* strict mode fixes for MockStorageApi

* strict ts fixes in room-state

* strict errors
2022-09-05 10:38:05 +02:00
3nprob e87ce873b0 utils: Fix bug in deepCompare which would incorrectly return objects with disjoint keys as equal (#2586)
* utils: Fix bug in deepCompare which would incorrectly return objects with disjoint keys as equal

* Fix bugs in sync test

This test wrongly asserted that `initialSyncLimit` would be used to make a filter
It is used only for the initial sync inline filter, and not in POST /filter

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-09-01 21:36:24 +00:00
Michael Weimann 207171efd6 Link contributing to Element Web (#2621) 2022-09-01 13:49:47 +02:00
Travis Ralston 8cc5efdf46 Update CHANGELOG.md 2022-08-31 11:28:56 -06:00
RiotRobot cbcf47d5c0 Resetting package fields for development 2022-08-31 16:26:39 +01:00
RiotRobot bbaa0e6536 Merge branch 'master' into develop 2022-08-31 16:26:39 +01:00
RiotRobot 1efeb1ec0e v19.4.0 2022-08-31 16:24:27 +01:00
RiotRobot 06e8d98911 Prepare changelog for v19.4.0 2022-08-31 16:24:26 +01:00
Travis Ralston 8716c1ab9b Convert several internal maps to real maps 2022-08-31 09:21:46 -06:00
Kegan Dougal ac7f505a2b Use the right nulls 2022-08-30 18:13:37 +01:00
Kegan Dougal b5576758e4 Even more strict TS type checking 2022-08-30 18:07:53 +01:00
Kegan Dougal c32a83fdac Linting 2022-08-30 18:02:56 +01:00
Kegan Dougal 2d9556c1bb More strict TS type checking 2022-08-30 18:00:03 +01:00
Kegan Dougal 818b70554a Strict TS checks 2022-08-30 17:50:48 +01:00
Kegan Dougal 725336ffc6 sliding sync: add invited|joined_count
This is critical for calculating client-side push rules correctly.
Without it, the push processor may think rooms have a different
number of members, resulting typically in annoying failure modes
where rooms constantly make noises because the code thinks they
are 1:1 rooms.
2022-08-30 17:41:54 +01:00
renovate[bot] 1fbd8983ed Lock file maintenance (#2625)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-29 00:00:48 -04:00
Faye Duxovni 1c77816dbd Use deep equality comparisons when searching for outgoing key requests by target (#2623) 2022-08-27 03:13:08 +00:00
Michael Telatynski c1160f40c2 Tweak tsc-strict config (#2620) 2022-08-25 09:49:58 +01:00
Michael Telatynski b789cc5933 Refactor Sync and fix initialSyncLimit (#2587)
* Small tidy-up to sync.ts

* Convert doSync into a while loop

* Apply `initialSyncLimit` only to initial syncs

* Convert matrix-client-syncing spec to TS

* Add tests around initial sync filtering

* Switch confusing filterId field for `filter`

* Tweak doSync error control flow

* Fix error control flow intricacies

* use includes

* Add tests

* Fix some strict mode errors

* Fix more strict mode errors

* Fix some strict mode errors
2022-08-23 16:25:54 +01:00
Jonathan Otto 5e4474b959 Fix room membership race with PREPARED event (#2613)
* Fix room membership race with PREPARED event

See the call site of the original triggering event of this function: https://github.com/matrix-org/matrix-js-sdk/blob/b265d795a427c6d30ccdf279a09f7836509df863/src/sliding-sync.ts#L789-L806

I think the bug is current code assumes downstream event listeners of `SlidingSyncEvent.RoomData` have synchronous execution so that by the time it emits `SlidingSyncState.Complete`, and eventually `SyncState.Prepared` the room state is correct. But since SlidingSyncSdk's `processRoomData` is async, and the membership field was being set after the async, it looks like `SlidingSyncState.Complete` was being fired before the membership field was set.

* Rm whitespace
2022-08-23 15:01:33 +00:00
Michael Telatynski 8e646ea584 Add static analysis for tsc --strict (#2615)
* Initial attempt at CI to annotate new TSC errors

* Make tsconfig file valid

* enable debug

* Specify commit

* Fix commit specification

* Switch back to main

* Tweak permissions

* Add strict mode failure

* Attempt number two

* Fix ts-extra-args

* Add static analysis for tsc --strict
2022-08-23 14:02:50 +01:00
RiotRobot 528e9343ae v19.4.0-rc.1 2022-08-23 10:53:02 +01:00
RiotRobot 6571b6a1ab Prepare changelog for v19.4.0-rc.1 2022-08-23 10:53:01 +01:00
kegsay 1df329df7c Merge pull request #2610 from matrix-org/kegan/ss-bugfix
sliding sync: handle lone DELETE and INSERT operations
2022-08-23 08:42:34 +01:00
kegsay 760eeaeed7 Merge pull request #2612 from matrix-org/kegan/ss-tags
Add tags and not_tags to the list of valid sliding sync filters
2022-08-23 08:42:14 +01:00
renovate[bot] 438fc70615 Update all (#2614)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-23 08:42:56 +02:00
Kegan Dougal de3b3960d2 Add tags and not_tags to the list of valid sliding sync filters 2022-08-22 18:20:51 +01:00
Robin b265d795a4 Re-emit room state events on rooms (#2607)
* Re-emit room state events on rooms

This also fixes some potential memory leaks and abuse of
removeAllListeners in sync.ts.

* Remove some stray whitespace

* Deduplicate some code to appease SonarCloud

* Name helper function more explicitly
2022-08-22 17:04:32 +02:00
Michael Telatynski eb79f6246d Add ability to override built in room name generator for an i18n'able one (#2609)
* Add ability to override built in room name generator for an i18n'able one

* Add tests
2022-08-22 14:39:04 +01:00
Kegan Dougal 37f8f736e0 sliding sync: handle lone DELETE and INSERT operations
If you leave a room you can get a lone DELETE op.
If you join a room you can get a lone INSERT op.

Up until now, we've assumed these operations happen at the ends
of the list (e.g [0] or [length-1]) which is not guaranteed as it
depends on the sort order (e.g sort alphabetically and join a room
called 'D'). In this scenario, the indexes would not be tracked
correctly. Fixed with integration tests.
2022-08-22 14:37:10 +01:00
renovate[bot] 4b1a443f90 Lock file maintenance (#2608)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-22 09:30:03 +01:00
Šimon Brandner 3ae974e23e Remove duplicate log of answering a call (#2595) 2022-08-17 10:23:06 +00:00
RiotRobot 566b4ba56c Resetting package fields for development 2022-08-16 15:25:58 +01:00
RiotRobot 13291f33d2 Merge branch 'master' into develop 2022-08-16 15:25:57 +01:00
RiotRobot 8502759e3e v19.3.0 2022-08-16 15:23:33 +01:00
RiotRobot 24f9075a84 Prepare changelog for v19.3.0 2022-08-16 15:23:33 +01:00
ElementRobot d18aae09c8 Fix: Handle parsing of a beacon info event without asset (#2591) (#2592)
* test case

* handle missing beacon info asset

* default beacon info asset type to self

* make BeaconLocationState.assetType optional

(cherry picked from commit be3e731499)

Co-authored-by: Kerry <kerrya@element.io>
2022-08-16 14:50:49 +01:00
Kerry be3e731499 Fix: Handle parsing of a beacon info event without asset (#2591)
* test case

* handle missing beacon info asset

* default beacon info asset type to self

* make BeaconLocationState.assetType optional
2022-08-16 15:33:19 +02:00
3nprob 3f6f5b69c7 Improve test coverage and modernize style for interactive-auth (#2574)
* style: address no-mixed-operators errors,minor style improvements

* test: Fix async interactive-auth tests, add test case

* tests: Fix incorrectly stringified mock response

* pushprocessor: style update

* use async primitives in interactive-auth-spec

* lint

* fixup: remove duplicate test

* add test case for no-flow-with-session for interactive-auth

* interactive-auth: handle non-existing error.data

* async test fix

* test: add dummyauth test

* add testing for errcode

* Revert "pushprocessor: style update"

This reverts commit 3ed0fdfb73ae55b725aa7c74d9cab35fb96c9178.

* add testcase for missing error data

* test: move sessionId assignment

* Add tests to improve coverage for interactive-auth

* pushprocessor: style update
2022-08-11 15:29:53 +01:00
Šimon Brandner 478270b225 Fix finding event read up to if stable private read receipts is missing (#2585)
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-08-11 13:30:51 +01:00
kegsay 9eb72908a7 Merge pull request #2583 from matrix-org/kegan/sync-v3
sliding sync bugfix: ensure history is treated as history and not live events
2022-08-10 20:00:53 +01:00
Kegan Dougal 2728d74771 Review comments 2022-08-10 19:53:36 +01:00
Kegan Dougal edcef9364c Only add events if there are some; set the pagination token for faster scrollback 2022-08-10 12:43:47 +01:00
Kegan Dougal 1635ac9971 sliding sync bugfix: ensure history is treated as history and not live events
with tests
2022-08-10 12:32:28 +01:00
kegsay 8f13df2dd9 Merge pull request #2567 from matrix-org/kegan/sync-v3
Add txn_id support to sliding sync
2022-08-10 12:16:47 +01:00
Kegan Dougal fa9f078a75 Review comments 2022-08-10 11:53:13 +01:00
Michael Telatynski 055af933cc Update backport.yml 2022-08-10 11:41:44 +01:00
Michael Telatynski 1ba2730e25 Update backport.yml (#2582) 2022-08-10 11:33:51 +01:00
Michael Telatynski e29e0d15a5 Set up basic Backporting action (#2580)
* Create backport.yml

* Update backport.yml
2022-08-10 09:21:39 +01:00
Michael Telatynski 9ee94c9902 Update jsdoc.yml (#2577) 2022-08-10 09:04:38 +01:00
renovate[bot] 1645867ea6 Update babel monorepo to v7.18.10 (#2578)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-10 07:27:31 +01:00
renovate[bot] 24d4181a08 Update typescript-eslint monorepo to v5.33.0 (#2579)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-09 15:42:40 -04:00
Kegan Dougal 2596999cb8 Review comments 2022-08-08 14:26:24 +01:00
Kegan Dougal a3248c0aa1 Merge branch 'develop' into kegan/sync-v3 2022-08-08 12:51:03 +01:00
Kegan Dougal e05f9b5815 Add txn_id support to sliding sync
This allows clients to know when a request has been applied
on the server. This allows us to change `resend(): void` to
`resend(): Promise<string>` which resolves/rejects with the
transaction ID when it has been applied/not.
2022-08-05 17:28:02 +01:00
74 changed files with 4668 additions and 2631 deletions
+4
View File
@@ -57,6 +57,10 @@ module.exports = {
// We're okay with assertion errors when we ask for them
"@typescript-eslint/no-non-null-assertion": "off",
// The non-TypeScript rule produces false positives
"func-call-spacing": "off",
"@typescript-eslint/func-call-spacing": ["error"],
"quotes": "off",
// We use a `logger` intermediary module
"no-console": "error",
+30
View File
@@ -0,0 +1,30 @@
name: Backport
on:
pull_request_target:
types:
- closed
- labeled
branches:
- develop
jobs:
backport:
name: Backport
runs-on: ubuntu-latest
# Only react to merged PRs for security reasons.
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
if: >
github.event.pull_request.merged
&& (
github.event.action == 'closed'
|| (
github.event.action == 'labeled'
&& contains(github.event.label.name, 'backport')
)
)
steps:
- uses: tibdex/backport@v2
with:
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
# We can't use GITHUB_TOKEN here or CI won't run on the new PR
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}
+40
View File
@@ -0,0 +1,40 @@
# Must only be called from `release#published` triggers
name: Publish to npm
on:
workflow_call:
secrets:
NPM_TOKEN:
required: true
jobs:
npm:
name: Publish to npm
runs-on: ubuntu-latest
steps:
- name: 🧮 Checkout code
uses: actions/checkout@v3
- name: 🔧 Yarn cache
uses: actions/setup-node@v3
with:
cache: "yarn"
registry-url: 'https://registry.npmjs.org'
- name: 🔨 Install dependencies
run: "yarn install --pure-lockfile"
- name: 🚀 Publish to npm
id: npm-publish
uses: JS-DevTools/npm-publish@v1
with:
token: ${{ secrets.NPM_TOKEN }}
access: public
tag: next
- name: 🎖️ Add `latest` dist-tag to final releases
if: github.event.release.prerelease == false
run: |
package=$(cat package.json | jq -er .name)
npm dist-tag add "$package@$release" latest
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
release: ${{ steps.npm-publish.outputs.version }}
@@ -24,7 +24,6 @@ jobs:
- name: 📋 Copy to temp
run: |
ls -lah
tag="${{ github.ref_name }}"
version="${tag#v}"
echo "VERSION=$version" >> $GITHUB_ENV
@@ -51,3 +50,9 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }}
keep_files: true
publish_dir: .
npm:
name: Publish
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+21
View File
@@ -10,7 +10,18 @@ jobs:
runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'success'
steps:
# We create the status here and then update it to success/failure in the `report` stage
# This provides an easy link to this workflow_run from the PR before Cypress is done.
- uses: Sibz/github-status-action@v1
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: pending
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
sha: ${{ github.event.workflow_run.head_sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: "🩻 SonarCloud Scan"
id: sonarcloud
uses: matrix-org/sonarcloud-workflow-action@v2.2
with:
repository: ${{ github.event.workflow_run.head_repository.full_name }}
@@ -22,3 +33,13 @@ jobs:
coverage_run_id: ${{ github.event.workflow_run.id }}
coverage_workflow_name: tests.yml
coverage_extract_path: coverage
- uses: Sibz/github-status-action@v1
if: always()
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: ${{ steps.sonarcloud.outcome == 'success' && 'success' || 'failure' }}
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
sha: ${{ github.event.workflow_run.head_sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
+35
View File
@@ -54,3 +54,38 @@ jobs:
- name: Generate Docs
run: "yarn run gendoc"
tsc-strict:
name: Typescript Strict Error Checker
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
pull-requests: read
checks: write
steps:
- uses: actions/checkout@v3
- name: Get diff lines
id: diff
uses: Equip-Collaboration/diff-line-numbers@v1.0.0
with:
include: '["\\.tsx?$"]'
- name: Detecting files changed
id: files
uses: futuratrepadeira/changed-files@v4.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pattern: '^.*\.tsx?$'
- uses: t3chguy/typescript-check-action@main
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
use-check: false
check-fail-mode: added
output-behaviour: annotate
ts-extra-args: '--strict'
files-changed: ${{ steps.files.outputs.files_updated }}
files-added: ${{ steps.files.outputs.files_created }}
files-deleted: ${{ steps.files.outputs.files_deleted }}
line-numbers: ${{ steps.diff.outputs.lineNumbers }}
+49 -9
View File
@@ -1,16 +1,53 @@
Changes in [19.3.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.3.0-rc.2) (2022-08-12)
============================================================================================================
## 🐛 Bug Fixes
* Fix finding event read up to if stable private read receipts is missing ([\#2585](https://github.com/matrix-org/matrix-js-sdk/pull/2585)). Fixes vector-im/element-web#23027.
Changes in [19.3.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.3.0-rc.1) (2022-08-09)
============================================================================================================
Changes in [19.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.6.0) (2022-09-27)
==================================================================================================
## ✨ Features
* Add a property aggregating all names of a NamespacedValue ([\#2656](https://github.com/matrix-org/matrix-js-sdk/pull/2656)).
* Implementation of MSC3824 to add action= param on SSO login ([\#2398](https://github.com/matrix-org/matrix-js-sdk/pull/2398)). Contributed by @hughns.
* Add invited_count and joined_count to sliding sync room responses. ([\#2628](https://github.com/matrix-org/matrix-js-sdk/pull/2628)).
* Base support for MSC3847: Ignore invites with policy rooms ([\#2626](https://github.com/matrix-org/matrix-js-sdk/pull/2626)). Contributed by @Yoric.
## 🐛 Bug Fixes
* Fix handling of remote echoes doubling up ([\#2639](https://github.com/matrix-org/matrix-js-sdk/pull/2639)). Fixes #2618.
Changes in [19.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.5.0) (2022-09-13)
==================================================================================================
## 🐛 Bug Fixes
* Fix bug in deepCompare which would incorrectly return objects with disjoint keys as equal ([\#2586](https://github.com/matrix-org/matrix-js-sdk/pull/2586)). Contributed by @3nprob.
* Refactor Sync and fix `initialSyncLimit` ([\#2587](https://github.com/matrix-org/matrix-js-sdk/pull/2587)).
* Use deep equality comparisons when searching for outgoing key requests by target ([\#2623](https://github.com/matrix-org/matrix-js-sdk/pull/2623)). Contributed by @duxovni.
* Fix room membership race with PREPARED event ([\#2613](https://github.com/matrix-org/matrix-js-sdk/pull/2613)). Contributed by @jotto.
Changes in [19.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.4.0) (2022-08-31)
==================================================================================================
## 🔒 Security
* Fix for [CVE-2022-36059](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D36059)
Find more details at https://matrix.org/blog/2022/08/31/security-releases-matrix-js-sdk-19-4-0-and-matrix-react-sdk-3-53-0
## ✨ Features
* Re-emit room state events on rooms ([\#2607](https://github.com/matrix-org/matrix-js-sdk/pull/2607)).
* Add ability to override built in room name generator for an i18n'able one ([\#2609](https://github.com/matrix-org/matrix-js-sdk/pull/2609)).
* Add txn_id support to sliding sync ([\#2567](https://github.com/matrix-org/matrix-js-sdk/pull/2567)).
## 🐛 Bug Fixes
* Refactor Sync and fix `initialSyncLimit` ([\#2587](https://github.com/matrix-org/matrix-js-sdk/pull/2587)).
* Use deep equality comparisons when searching for outgoing key requests by target ([\#2623](https://github.com/matrix-org/matrix-js-sdk/pull/2623)). Contributed by @duxovni.
* Fix room membership race with PREPARED event ([\#2613](https://github.com/matrix-org/matrix-js-sdk/pull/2613)). Contributed by @jotto.
* fixed a sliding sync bug which could cause the `roomIndexToRoomId` map to be incorrect when a new room is added in the middle of the list or when an existing room is deleted from the middle of the list. ([\#2610](https://github.com/matrix-org/matrix-js-sdk/pull/2610)).
* Fix: Handle parsing of a beacon info event without asset ([\#2591](https://github.com/matrix-org/matrix-js-sdk/pull/2591)). Fixes vector-im/element-web#23078. Contributed by @kerryarchibald.
* Fix finding event read up to if stable private read receipts is missing ([\#2585](https://github.com/matrix-org/matrix-js-sdk/pull/2585)). Fixes vector-im/element-web#23027.
* fixed a sliding sync issue where history could be interpreted as live events. ([\#2583](https://github.com/matrix-org/matrix-js-sdk/pull/2583)).
Changes in [19.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.3.0) (2022-08-16)
==================================================================================================
## ✨ Features
* Add txn_id support to sliding sync ([\#2567](https://github.com/matrix-org/matrix-js-sdk/pull/2567)).
* Emit an event when the client receives TURN servers ([\#2529](https://github.com/matrix-org/matrix-js-sdk/pull/2529)).
* Add support for stable prefixes for MSC2285 ([\#2524](https://github.com/matrix-org/matrix-js-sdk/pull/2524)).
* Always block sending keys to unverified devices of verified users ([\#2562](https://github.com/matrix-org/matrix-js-sdk/pull/2562)). Contributed by @duxovni.
* Remove stream-replacement ([\#2551](https://github.com/matrix-org/matrix-js-sdk/pull/2551)).
* Add support for sending user-defined encrypted to-device messages ([\#2528](https://github.com/matrix-org/matrix-js-sdk/pull/2528)).
* Retry to-device messages ([\#2549](https://github.com/matrix-org/matrix-js-sdk/pull/2549)). Fixes vector-im/element-web#12851.
@@ -18,6 +55,9 @@ Changes in [19.3.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/ta
* Use stable prefixes for MSC3827 ([\#2537](https://github.com/matrix-org/matrix-js-sdk/pull/2537)).
## 🐛 Bug Fixes
* Fix: Handle parsing of a beacon info event without asset ([\#2591](https://github.com/matrix-org/matrix-js-sdk/pull/2591)). Fixes vector-im/element-web#23078.
* Fix finding event read up to if stable private read receipts is missing ([\#2585](https://github.com/matrix-org/matrix-js-sdk/pull/2585)). Fixes vector-im/element-web#23027.
* Fixed a sliding sync issue where history could be interpreted as live events. ([\#2583](https://github.com/matrix-org/matrix-js-sdk/pull/2583)).
* Don't load the sync accumulator if there's already a sync persist in flight ([\#2569](https://github.com/matrix-org/matrix-js-sdk/pull/2569)).
Changes in [19.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.2.0) (2022-08-02)
+1 -280
View File
@@ -1,284 +1,5 @@
Contributing code to matrix-js-sdk
==================================
Everyone is welcome to contribute code to matrix-js-sdk, provided that they are
willing to license their contributions under the same license as the project
itself. We follow a simple 'inbound=outbound' model for contributions: the act
of submitting an 'inbound' contribution means that the contributor agrees to
license the code under the same terms as the project's overall 'outbound'
license - in this case, Apache Software License v2 (see
[LICENSE](LICENSE)).
matrix-js-sdk follows the same pattern as https://github.com/vector-im/element-web/blob/develop/CONTRIBUTING.md
How to contribute
-----------------
The preferred and easiest way to contribute changes to the project is to fork
it on github, and then create a pull request to ask us to pull your changes
into our repo (https://help.github.com/articles/using-pull-requests/)
We use GitHub's pull request workflow to review the contribution, and either
ask you to make any refinements needed or merge it and make them ourselves.
Things that should go into your PR description:
* A changelog entry in the `Notes` section (see below)
* References to any bugs fixed by the change (in GitHub's `Fixes` notation)
* Describe the why and what is changing in the PR description so it's easy for
onlookers and reviewers to onboard and context switch. This information is
also helpful when we come back to look at this in 6 months and ask "why did
we do it like that?" we have a chance of finding out.
* Why didn't it work before? Why does it work now? What use cases does it
unlock?
* If you find yourself adding information on how the code works or why you
chose to do it the way you did, make sure this information is instead
written as comments in the code itself.
* Sometimes a PR can change considerably as it is developed. In this case,
the description should be updated to reflect the most recent state of
the PR. (It can be helpful to retain the old content under a suitable
heading, for additional context.)
* Include both **before** and **after** screenshots to easily compare and discuss
what's changing.
* Include a step-by-step testing strategy so that a reviewer can check out the
code locally and easily get to the point of testing your change.
* Add comments to the diff for the reviewer that might help them to understand
why the change is necessary or how they might better understand and review it.
We rely on information in pull request to populate the information that goes
into the changelogs our users see, both for the JS SDK itself and also for some
projects based on it. This is picked up from both labels on the pull request and
the `Notes:` annotation in the description. By default, the PR title will be
used for the changelog entry, but you can specify more options, as follows.
To add a longer, more detailed description of the change for the changelog:
*Fix llama herding bug*
```
Notes: Fix a bug (https://github.com/matrix-org/notaproject/issues/123) where the 'Herd' button would not herd more than 8 Llamas if the moon was in the waxing gibbous phase
```
For some PRs, it's not useful to have an entry in the user-facing changelog (this is
the default for PRs labelled with `T-Task`):
*Remove outdated comment from `Ungulates.ts`*
```
Notes: none
```
Sometimes, you're fixing a bug in a downstream project, in which case you want
an entry in that project's changelog. You can do that too:
*Fix another herding bug*
```
Notes: Fix a bug where the `herd()` function would only work on Tuesdays
element-web notes: Fix a bug where the 'Herd' button only worked on Tuesdays
```
This example is for Element Web. You can specify:
* matrix-react-sdk
* element-web
* element-desktop
If your PR introduces a breaking change, use the `Notes` section in the same
way, additionally adding the `X-Breaking-Change` label (see below). There's no need
to specify in the notes that it's a breaking change - this will be added
automatically based on the label - but remember to tell the developer how to
migrate:
*Remove legacy class*
```
Notes: Remove legacy `Camelopard` class. `Giraffe` should be used instead.
```
Other metadata can be added using labels.
* `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a *major* version bump.
* `T-Enhancement`: A new feature - adding this label will mean the change causes a *minor* version bump.
* `T-Defect`: A bug fix (in either code or docs).
* `T-Task`: No user-facing changes, eg. code comments, CI fixes, refactors or tests. Won't have a changelog entry unless you specify one.
If you don't have permission to add labels, your PR reviewer(s) can work with you
to add them: ask in the PR description or comments.
We use continuous integration, and all pull requests get automatically tested:
if your change breaks the build, then the PR will show that there are failed
checks, so please check back after a few minutes.
Tests
-----
Your PR should include tests.
For new user facing features in `matrix-react-sdk` or `element-web`, you
must include:
1. Comprehensive unit tests written in Jest. These are located in `/test`.
2. "happy path" end-to-end tests.
These are located in `/test/end-to-end-tests` in `matrix-react-sdk`, and
are run using `element-web`. Ideally, you would also include tests for edge
and error cases.
Unit tests are expected even when the feature is in labs. It's good practice
to write tests alongside the code as it ensures the code is testable from
the start, and gives you a fast feedback loop while you're developing the
functionality. End-to-end tests should be added prior to the feature
leaving labs, but don't have to be present from the start (although it might
be beneficial to have some running early, so you can test things faster).
For bugs in those repos, your change must include at least one unit test or
end-to-end test; which is best depends on what sort of test most concisely
exercises the area.
Changes to `matrix-js-sdk` must be accompanied by unit tests written in Jest.
These are located in `/spec/`.
When writing unit tests, please aim for a high level of test coverage
for new code - 80% or greater. If you cannot achieve that, please document
why it's not possible in your PR.
Some sections of code are not sensible to add coverage for, such as those
which explicitly inhibit noisy logging for tests. Which can be hidden using
an istanbul magic comment as [documented here][1]. See example:
```javascript
/* istanbul ignore if */
if (process.env.NODE_ENV !== "test") {
logger.error("Log line that is noisy enough in tests to want to skip");
}
```
Tests validate that your change works as intended and also document
concisely what is being changed. Ideally, your new tests fail
prior to your change, and succeed once it has been applied. You may
find this simpler to achieve if you write the tests first.
If you're spiking some code that's experimental and not being used to support
production features, exceptions can be made to requirements for tests.
Note that tests will still be required in order to ship the feature, and it's
strongly encouraged to think about tests early in the process, as adding
tests later will become progressively more difficult.
If you're not sure how to approach writing tests for your change, ask for help
in [#element-dev](https://matrix.to/#/#element-dev:matrix.org).
Code style
----------
The js-sdk aims to target TypeScript/ES6. All new files should be written in
TypeScript and existing files should use ES6 principles where possible.
Members should not be exported as a default export in general - it causes problems
with the architecture of the SDK (index file becomes less clear) and could
introduce naming problems (as default exports get aliased upon import). In
general, avoid using `export default`.
The remaining code-style for matrix-js-sdk is not formally documented, but
contributors are encouraged to read the
[code style document for matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md)
and follow the principles set out there.
Please ensure your changes match the cosmetic style of the existing project,
and ***never*** mix cosmetic and functional changes in the same commit, as it
makes it horribly hard to review otherwise.
Attribution
-----------
Everyone who contributes anything to Matrix is welcome to be listed in the
AUTHORS.rst file for the project in question. Please feel free to include a
change to AUTHORS.rst in your pull request to list yourself and a short
description of the area(s) you've worked on. Also, we sometimes have swag to
give away to contributors - if you feel that Matrix-branded apparel is missing
from your life, please mail us your shipping address to matrix at matrix.org
and we'll try to fix it :)
Sign off
--------
In order to have a concrete record that your contribution is intentional
and you agree to license it under the same terms as the project's license, we've
adopted the same lightweight approach that the Linux Kernel
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
projects use: the DCO (Developer Certificate of Origin:
http://developercertificate.org/). This is a simple declaration that you wrote
the contribution or otherwise have the right to contribute it to Matrix:
```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
If you agree to this for your contribution, then all that's needed is to
include the line in your commit or pull request comment:
```
Signed-off-by: Your Name <your@email.example.org>
```
We accept contributions under a legally identifiable name, such as your name on
government documentation or common-law names (names claimed by legitimate usage
or repute). Unfortunately, we cannot accept anonymous contributions at this
time.
Git allows you to add this signoff automatically when using the `-s` flag to
`git commit`, which uses the name and email set in your `user.name` and
`user.email` git configs.
If you forgot to sign off your commits before making your pull request and are
on Git 2.17+ you can mass signoff using rebase:
```
git rebase --signoff origin/develop
```
Review expectations
===================
See https://github.com/vector-im/element-meta/wiki/Review-process
Merge Strategy
==============
The preferred method for merging pull requests is squash merging to keep the
commit history trim, but it is up to the discretion of the team member merging
the change. We do not support rebase merges due to `allchange` being unable to
handle them. When merging make sure to leave the default commit title, or
at least leave the PR number at the end in brackets like by default.
When stacking pull requests, you may wish to do the following:
1. Branch from develop to your branch (branch1), push commits onto it and open a pull request
2. Branch from your base branch (branch1) to your work branch (branch2), push commits and open a pull request configuring the base to be branch1, saying in the description that it is based on your other PR.
3. Merge the first PR using a merge commit otherwise your stacked PR will need a rebase. Github will automatically adjust the base branch of your other PR to be develop.
[1]: https://github.com/gotwarlost/istanbul/blob/master/ignoring-code-for-coverage.md
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "19.3.0-rc.2",
"version": "19.6.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=12.9.0"
@@ -81,24 +81,24 @@
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz",
"@types/bs58": "^4.0.1",
"@types/content-type": "^1.1.5",
"@types/jest": "^28.0.0",
"@types/jest": "^29.0.0",
"@types/node": "16",
"@types/request": "^2.48.5",
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",
"allchange": "^1.0.6",
"babel-jest": "^28.0.0",
"babel-jest": "^29.0.0",
"babelify": "^10.0.0",
"better-docs": "^2.4.0-beta.9",
"browserify": "^17.0.0",
"docdash": "^1.2.0",
"eslint": "8.20.0",
"eslint": "8.23.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-matrix-org": "^0.5.0",
"eslint-plugin-matrix-org": "^0.6.0",
"exorcist": "^2.0.0",
"fake-indexeddb": "^4.0.0",
"jest": "^28.0.0",
"jest": "^29.0.0",
"jest-localstorage-mock": "^2.4.6",
"jest-sonar-reporter": "^2.0.0",
"jsdoc": "^3.6.6",
+37
View File
@@ -0,0 +1,37 @@
#!/bin/bash
#
# Script to perform a post-release steps of matrix-js-sdk.
#
# Requires:
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
set -e
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
# When merging to develop, we need revert the `main` and `typings` fields if we adjusted them previously.
for i in main typings
do
# If a `lib` prefixed value is present, it means we adjusted the field
# earlier at publish time, so we should revert it now.
if [ "$(jq -r ".matrix_lib_$i" package.json)" != "null" ]; then
# If there's a `src` prefixed value, use that, otherwise delete.
# This is used to delete the `typings` field and reset `main` back
# to the TypeScript source.
src_value=$(jq -r ".matrix_src_$i" package.json)
if [ "$src_value" != "null" ]; then
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json
else
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json
fi
fi
done
if [ -n "$(git ls-files --modified package.json)" ]; then
echo "Committing develop package.json"
git commit package.json -m "Resetting package fields for development"
fi
git push origin develop
fi
+78 -81
View File
@@ -3,19 +3,16 @@
# Script to perform a release of matrix-js-sdk and downstream projects.
#
# Requires:
# github-changelog-generator; install via:
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
# hub; install via brew (macOS) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
# npm; typically installed by Node.js
# yarn; install via brew (macOS) or similar (https://yarnpkg.com/docs/install/)
#
# Note: this script is also used to release matrix-react-sdk and element-web.
# Note: this script is also used to release matrix-react-sdk, element-web, and element-desktop.
set -e
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
if [[ `command -v hub` ]] && [[ `hub --version` =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then
if [[ $(command -v hub) ]] && [[ $(hub --version) =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then
HUB_VERSION_MAJOR=${BASH_REMATCH[1]}
HUB_VERSION_MINOR=${BASH_REMATCH[2]}
if [[ $HUB_VERSION_MAJOR -lt 2 ]] || [[ $HUB_VERSION_MAJOR -eq 2 && $HUB_VERSION_MINOR -lt 5 ]]; then
@@ -26,7 +23,6 @@ else
echo "hub is required: please install it"
exit
fi
npm --version > /dev/null || (echo "npm is required: please install it"; kill $$)
yarn --version > /dev/null || (echo "yarn is required: please install it"; kill $$)
USAGE="$0 [-x] [-c changelog_file] vX.Y.Z"
@@ -37,17 +33,9 @@ $USAGE
-c changelog_file: specify name of file containing changelog
-x: skip updating the changelog
-n: skip publish to NPM
EOF
}
ret=0
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
if [ "$ret" -eq 0 ]; then
echo "package.json contains develop dependencies. Refusing to release."
exit
fi
if ! git diff-index --quiet --cached HEAD; then
echo "this git checkout has staged (uncommitted) changes. Refusing to release."
exit
@@ -59,10 +47,8 @@ if ! git diff-files --quiet; then
fi
skip_changelog=
skip_npm=
changelog_file="CHANGELOG.md"
expected_npm_user="matrixdotorg"
while getopts hc:u:xzn f; do
while getopts hc:x f; do
case $f in
h)
help
@@ -74,21 +60,70 @@ while getopts hc:u:xzn f; do
x)
skip_changelog=1
;;
n)
skip_npm=1
;;
u)
expected_npm_user="$OPTARG"
;;
esac
done
shift `expr $OPTIND - 1`
shift $(expr $OPTIND - 1)
if [ $# -ne 1 ]; then
echo "Usage: $USAGE" >&2
exit 1
fi
function check_dependency {
local depver=$(cat package.json | jq -r .dependencies[\"$1\"])
if [ "$depver" == "null" ]; then return 0; fi
echo "Checking version of $1..."
local latestver=$(yarn info -s "$1" dist-tags.next)
if [ "$depver" != "$latestver" ]
then
echo "The latest version of $1 is $latestver but package.json depends on $depver."
echo -n "Type 'u' to auto-upgrade, 'c' to continue anyway, or 'a' to abort:"
read resp
if [ "$resp" != "u" ] && [ "$resp" != "c" ]
then
echo "Aborting."
exit 1
fi
if [ "$resp" == "u" ]
then
echo "Upgrading $1 to $latestver..."
yarn add -E "$1@$latestver"
git add -u
git commit -m "Upgrade $1 to $latestver"
fi
fi
}
function reset_dependency {
local depver=$(cat package.json | jq -r .dependencies[\"$1\"])
if [ "$depver" == "null" ]; then return 0; fi
echo "Resetting $1 to develop branch..."
yarn add "github:matrix-org/$1#develop"
git add -u
git commit -m "Reset $1 back to develop branch"
}
has_subprojects=0
if [ -f release_config.yaml ]; then
subprojects=$(cat release_config.yaml | python -c "import yaml; import sys; print(' '.join(list(yaml.load(sys.stdin)['subprojects'].keys())))" 2> /dev/null)
if [ "$?" -eq 0 ]; then
has_subprojects=1
echo "Checking subprojects for upgrades"
for proj in $subprojects; do
check_dependency "$proj"
done
fi
fi
ret=0
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
if [ "$ret" -eq 0 ]; then
echo "package.json contains develop dependencies. Refusing to release."
exit
fi
# We use Git branch / commit dependencies for some packages, and Yarn seems
# to have a hard time getting that right. See also
# https://github.com/yarnpkg/yarn/issues/4734. As a workaround, we clean the
@@ -97,16 +132,6 @@ yarn cache clean
# Ensure all dependencies are updated
yarn install --ignore-scripts --pure-lockfile
# Login and publish continues to use `npm`, as it seems to have more clearly
# defined options and semantics than `yarn` for writing to the registry.
if [ -z "$skip_npm" ]; then
actual_npm_user=`npm whoami`;
if [ $expected_npm_user != $actual_npm_user ]; then
echo "you need to be logged into npm as $expected_npm_user, but you are logged in as $actual_npm_user" >&2
exit 1
fi
fi
# ignore leading v on release
release="${1#v}"
tag="v${release}"
@@ -148,8 +173,8 @@ if [ -z "$skip_changelog" ]; then
git commit "$changelog_file" -m "Prepare changelog for $tag"
fi
fi
latest_changes=`mktemp`
cat "${changelog_file}" | `dirname $0`/scripts/changelog_head.py > "${latest_changes}"
latest_changes=$(mktemp)
cat "${changelog_file}" | "$(dirname "$0")/scripts/changelog_head.py" > "${latest_changes}"
set -x
@@ -176,7 +201,7 @@ do
done
# commit yarn.lock if it exists, is versioned, and is modified
if [[ -f yarn.lock && `git status --porcelain yarn.lock | grep '^ M'` ]];
if [[ -f yarn.lock && $(git status --porcelain yarn.lock | grep '^ M') ]];
then
pkglock='yarn.lock'
else
@@ -188,7 +213,7 @@ git commit package.json $pkglock -m "$tag"
# figure out if we should be signing this release
signing_id=
if [ -f release_config.yaml ]; then
result=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']" 2> /dev/null || true`
result=$(cat release_config.yaml | python -c "import yaml; import sys; print(yaml.load(sys.stdin)['signing_id'])" 2> /dev/null || true)
if [ "$?" -eq 0 ]; then
signing_id=$result
fi
@@ -206,8 +231,8 @@ assets=''
dodist=0
jq -e .scripts.dist package.json 2> /dev/null || dodist=$?
if [ $dodist -eq 0 ]; then
projdir=`pwd`
builddir=`mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir'`
projdir=$(pwd)
builddir=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
echo "Building distribution copy in $builddir"
pushd "$builddir"
git clone "$projdir" .
@@ -232,7 +257,7 @@ fi
if [ -n "$signing_id" ]; then
# make a signed tag
# gnupg seems to fail to get the right tty device unless we set it here
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=`tty` git tag -u "$signing_id" -F "${latest_changes}" "$tag"
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=$(tty) git tag -u "$signing_id" -F "${latest_changes}" "$tag"
else
git tag -a -F "${latest_changes}" "$tag"
fi
@@ -298,7 +323,7 @@ if [ $prerelease -eq 1 ]; then
hubflags='-p'
fi
release_text=`mktemp`
release_text=$(mktemp)
echo "$tag" > "${release_text}"
echo >> "${release_text}"
cat "${latest_changes}" >> "${release_text}"
@@ -310,19 +335,6 @@ fi
rm "${release_text}"
rm "${latest_changes}"
# Login and publish continues to use `npm`, as it seems to have more clearly
# defined options and semantics than `yarn` for writing to the registry.
# Tag both releases and prereleases as `next` so the last stable release remains
# the default.
if [ -z "$skip_npm" ]; then
npm publish --tag next
if [ $prerelease -eq 0 ]; then
# For a release, also add the default `latest` tag.
package=$(cat package.json | jq -er .name)
npm dist-tag add "$package@$release" latest
fi
fi
# if it is a pre-release, leave it on the release branch for now.
if [ $prerelease -eq 1 ]; then
git checkout "$rel_branch"
@@ -339,34 +351,19 @@ git merge "$rel_branch" --no-edit
git push origin master
# finally, merge master back onto develop (if it exists)
if [ $(git branch -lr | grep origin/develop -c) -ge 1 ]; then
if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
git checkout develop
git pull
git merge master --no-edit
# When merging to develop, we need revert the `main` and `typings` fields if
# we adjusted them previously.
for i in main typings
do
# If a `lib` prefixed value is present, it means we adjusted the field
# earlier at publish time, so we should revert it now.
if [ "$(jq -r ".matrix_lib_$i" package.json)" != "null" ]; then
# If there's a `src` prefixed value, use that, otherwise delete.
# This is used to delete the `typings` field and reset `main` back
# to the TypeScript source.
src_value=$(jq -r ".matrix_src_$i" package.json)
if [ "$src_value" != "null" ]; then
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json
else
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json
fi
fi
done
if [ -n "$(git ls-files --modified package.json)" ]; then
echo "Committing develop package.json"
git commit package.json -m "Resetting package fields for development"
fi
git push origin develop
fi
[ -x ./post-release.sh ] && ./post-release.sh
if [ $has_subprojects -eq 1 ] && [ $prerelease -eq 0 ]; then
echo "Resetting subprojects to develop"
for proj in $subprojects; do
reset_dependency "$proj"
done
git push origin develop
fi
@@ -1,6 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -17,31 +17,32 @@ limitations under the License.
/**
* A mock implementation of the webstorage api
* @constructor
*/
export function MockStorageApi() {
this.data = {};
this.keys = [];
this.length = 0;
}
export class MockStorageApi {
public data: Record<string, string> = {};
public keys: string[] = [];
public length = 0;
MockStorageApi.prototype = {
setItem: function(k, v) {
public setItem(k: string, v: string): void {
this.data[k] = v;
this._recalc();
},
getItem: function(k) {
this.recalc();
}
public getItem(k: string): string | null {
return this.data[k] || null;
},
removeItem: function(k) {
}
public removeItem(k: string): void {
delete this.data[k];
this._recalc();
},
key: function(index) {
this.recalc();
}
public key(index: number): string {
return this.keys[index];
},
_recalc: function() {
const keys = [];
}
private recalc(): void {
const keys: string[] = [];
for (const k in this.data) {
if (!this.data.hasOwnProperty(k)) {
continue;
@@ -50,6 +51,5 @@ MockStorageApi.prototype = {
}
this.keys = keys;
this.length = keys.length;
},
};
}
}
+1 -1
View File
@@ -50,7 +50,7 @@ export class TestClient {
options?: Partial<ICreateClientOpts>,
) {
if (sessionStoreBackend === undefined) {
sessionStoreBackend = new MockStorageApi();
sessionStoreBackend = new MockStorageApi() as unknown as Storage;
}
this.httpBackend = new MockHttpBackend();
File diff suppressed because it is too large Load Diff
+79
View File
@@ -28,6 +28,7 @@ import {
import { SlidingSyncSdk } from "../../src/sliding-sync-sdk";
import { SyncState } from "../../src/sync";
import { IStoredClientOpts } from "../../src/client";
import { logger } from "../../src/logger";
describe("SlidingSyncSdk", () => {
let client: MatrixClient = null;
@@ -167,6 +168,7 @@ describe("SlidingSyncSdk", () => {
const roomD = "!d_with_notif_count:localhost";
const roomE = "!e_with_invite:localhost";
const roomF = "!f_calc_room_name:localhost";
const roomG = "!g_join_invite_counts:localhost";
const data: Record<string, MSC3575RoomData> = {
[roomA]: {
name: "A",
@@ -260,12 +262,25 @@ describe("SlidingSyncSdk", () => {
],
initial: true,
},
[roomG]: {
name: "G",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
],
joined_count: 5,
invited_count: 2,
initial: true,
},
};
it("can be created with required_state and timeline", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]);
const gotRoom = client.getRoom(roomA);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.name).toEqual(data[roomA].name);
expect(gotRoom.getMyMembership()).toEqual("join");
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline);
@@ -275,6 +290,7 @@ describe("SlidingSyncSdk", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]);
const gotRoom = client.getRoom(roomB);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.name).toEqual(data[roomB].name);
expect(gotRoom.getMyMembership()).toEqual("join");
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline);
@@ -284,6 +300,7 @@ describe("SlidingSyncSdk", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]);
const gotRoom = client.getRoom(roomC);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight),
).toEqual(data[roomC].highlight_count);
@@ -293,15 +310,26 @@ describe("SlidingSyncSdk", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]);
const gotRoom = client.getRoom(roomD);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
).toEqual(data[roomD].notification_count);
});
it("can be created with an invited/joined_count", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]);
const gotRoom = client.getRoom(roomG);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getInvitedMemberCount()).toEqual(data[roomG].invited_count);
expect(gotRoom.getJoinedMemberCount()).toEqual(data[roomG].joined_count);
});
it("can be created with invite_state", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
const gotRoom = client.getRoom(roomE);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getMyMembership()).toEqual("invite");
expect(gotRoom.currentState.getJoinRule()).toEqual(JoinRule.Invite);
});
@@ -310,6 +338,7 @@ describe("SlidingSyncSdk", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]);
const gotRoom = client.getRoom(roomF);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.name,
).toEqual(data[roomF].name);
@@ -325,6 +354,7 @@ describe("SlidingSyncSdk", () => {
});
const gotRoom = client.getRoom(roomA);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
const newTimeline = data[roomA].timeline;
newTimeline.push(newEvent);
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-3), newTimeline);
@@ -332,6 +362,8 @@ describe("SlidingSyncSdk", () => {
it("can update with a new required_state event", async () => {
let gotRoom = client.getRoom(roomB);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Invite); // default
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomB, {
required_state: [
@@ -342,6 +374,7 @@ describe("SlidingSyncSdk", () => {
});
gotRoom = client.getRoom(roomB);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted);
});
@@ -354,6 +387,7 @@ describe("SlidingSyncSdk", () => {
});
const gotRoom = client.getRoom(roomC);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight),
).toEqual(1);
@@ -368,10 +402,55 @@ describe("SlidingSyncSdk", () => {
});
const gotRoom = client.getRoom(roomD);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
).toEqual(1);
});
it("can update with a new joined_count", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomG, {
name: data[roomD].name,
required_state: [],
timeline: [],
joined_count: 1,
});
const gotRoom = client.getRoom(roomG);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getJoinedMemberCount()).toEqual(1);
});
// Regression test for a bug which caused the timeline entries to be out-of-order
// when the same room appears twice with different timeline limits. E.g appears in
// the list with timeline_limit:1 then appears again as a room subscription with
// timeline_limit:50
it("can return history with a larger timeline_limit", async () => {
const timeline = data[roomA].timeline;
const oldTimeline = [
mkOwnEvent(EventType.RoomMessage, { body: "old event A" }),
mkOwnEvent(EventType.RoomMessage, { body: "old event B" }),
mkOwnEvent(EventType.RoomMessage, { body: "old event C" }),
...timeline,
];
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, {
timeline: oldTimeline,
required_state: [],
name: data[roomA].name,
initial: true, // e.g requested via room subscription
});
const gotRoom = client.getRoom(roomA);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
logger.log("want:", oldTimeline.map((e) => (e.type + " : " + (e.content || {}).body)));
logger.log("got:", gotRoom.getLiveTimeline().getEvents().map(
(e) => (e.getType() + " : " + e.getContent().body)),
);
// we expect the timeline now to be oldTimeline (so the old events are in fact old)
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents(), oldTimeline);
});
});
});
});
+366
View File
@@ -558,6 +558,372 @@ describe("SlidingSync", () => {
await httpBackend.flushAllExpected();
await responseProcessed;
await listPromise;
});
// this refers to a set of operations where the end result is no change.
it("should handle net zero operations correctly", async () => {
const indexToRoomId = {
0: roomB,
1: roomC,
};
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual(indexToRoomId);
httpBackend.when("POST", syncUrl).respond(200, {
pos: "f",
// currently the list is [B,C] so we will insert D then immediately delete it
lists: [{
count: 500,
ops: [
{
op: "DELETE", index: 2,
},
{
op: "INSERT", index: 0, room_id: roomA,
},
{
op: "DELETE", index: 0,
},
],
},
{
count: 50,
}],
});
const listPromise = listenUntil(slidingSync, "SlidingSync.List",
(listIndex, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0);
expect(joinedCount).toEqual(500);
expect(roomIndexToRoomId).toEqual(indexToRoomId);
return true;
});
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend.flushAllExpected();
await responseProcessed;
await listPromise;
});
it("should handle deletions correctly", async () => {
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual({
0: roomB,
1: roomC,
});
httpBackend.when("POST", syncUrl).respond(200, {
pos: "g",
lists: [{
count: 499,
ops: [
{
op: "DELETE", index: 0,
},
],
},
{
count: 50,
}],
});
const listPromise = listenUntil(slidingSync, "SlidingSync.List",
(listIndex, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0);
expect(joinedCount).toEqual(499);
expect(roomIndexToRoomId).toEqual({
0: roomC,
});
return true;
});
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend.flushAllExpected();
await responseProcessed;
await listPromise;
});
it("should handle insertions correctly", async () => {
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual({
0: roomC,
});
httpBackend.when("POST", syncUrl).respond(200, {
pos: "h",
lists: [{
count: 500,
ops: [
{
op: "INSERT", index: 1, room_id: roomA,
},
],
},
{
count: 50,
}],
});
let listPromise = listenUntil(slidingSync, "SlidingSync.List",
(listIndex, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0);
expect(joinedCount).toEqual(500);
expect(roomIndexToRoomId).toEqual({
0: roomC,
1: roomA,
});
return true;
});
let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend.flushAllExpected();
await responseProcessed;
await listPromise;
httpBackend.when("POST", syncUrl).respond(200, {
pos: "h",
lists: [{
count: 501,
ops: [
{
op: "INSERT", index: 1, room_id: roomB,
},
],
},
{
count: 50,
}],
});
listPromise = listenUntil(slidingSync, "SlidingSync.List",
(listIndex, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0);
expect(joinedCount).toEqual(501);
expect(roomIndexToRoomId).toEqual({
0: roomC,
1: roomB,
2: roomA,
});
return true;
});
responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend.flushAllExpected();
await responseProcessed;
await listPromise;
slidingSync.stop();
});
});
describe("transaction IDs", () => {
beforeAll(setupClient);
afterAll(teardownClient);
const roomId = "!foo:bar";
let slidingSync: SlidingSync;
// really this applies to them all but it's easier to just test one
it("should resolve modifyRoomSubscriptions after SlidingSync.start() is called", async () => {
const roomSubInfo = {
timeline_limit: 1,
required_state: [
["m.room.name", ""],
],
};
// add the subscription
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client, 1);
// modification before SlidingSync.start()
const subscribePromise = slidingSync.modifyRoomSubscriptions(new Set([roomId]));
let txnId;
httpBackend.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toBeTruthy();
expect(body.room_subscriptions[roomId]).toEqual(roomSubInfo);
expect(body.txn_id).toBeTruthy();
txnId = body.txn_id;
}).respond(200, function() {
return {
pos: "aaa",
txn_id: txnId,
lists: [],
extensions: {},
rooms: {
[roomId]: {
name: "foo bar",
required_state: [],
timeline: [],
},
},
};
});
slidingSync.start();
await httpBackend.flushAllExpected();
await subscribePromise;
});
it("should resolve setList during a connection", async () => {
const newList = {
ranges: [[0, 20]],
};
const promise = slidingSync.setList(0, newList);
let txnId;
httpBackend.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toBeFalsy();
expect(body.lists[0]).toEqual(newList);
expect(body.txn_id).toBeTruthy();
txnId = body.txn_id;
}).respond(200, function() {
return {
pos: "bbb",
txn_id: txnId,
lists: [{ count: 5 }],
extensions: {},
};
});
await httpBackend.flushAllExpected();
await promise;
expect(txnId).toBeDefined();
});
it("should resolve setListRanges during a connection", async () => {
const promise = slidingSync.setListRanges(0, [[20, 40]]);
let txnId;
httpBackend.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toBeFalsy();
expect(body.lists[0]).toEqual({
ranges: [[20, 40]],
});
expect(body.txn_id).toBeTruthy();
txnId = body.txn_id;
}).respond(200, function() {
return {
pos: "ccc",
txn_id: txnId,
lists: [{ count: 5 }],
extensions: {},
};
});
await httpBackend.flushAllExpected();
await promise;
expect(txnId).toBeDefined();
});
it("should resolve modifyRoomSubscriptionInfo during a connection", async () => {
const promise = slidingSync.modifyRoomSubscriptionInfo({
timeline_limit: 99,
});
let txnId;
httpBackend.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toBeTruthy();
expect(body.room_subscriptions[roomId]).toEqual({
timeline_limit: 99,
});
expect(body.txn_id).toBeTruthy();
txnId = body.txn_id;
}).respond(200, function() {
return {
pos: "ddd",
txn_id: txnId,
extensions: {},
};
});
await httpBackend.flushAllExpected();
await promise;
expect(txnId).toBeDefined();
});
it("should reject earlier pending promises if a later transaction is acknowledged", async () => {
// i.e if we have [A,B,C] and see txn_id=C then A,B should be rejected.
const gotTxnIds = [];
const pushTxn = function(req) {
gotTxnIds.push(req.data.txn_id);
};
const failPromise = slidingSync.setListRanges(0, [[20, 40]]);
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "e" }); // missing txn_id
await httpBackend.flushAllExpected();
const failPromise2 = slidingSync.setListRanges(0, [[60, 70]]);
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "f" }); // missing txn_id
await httpBackend.flushAllExpected();
// attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection
// which is a fail.
expect(failPromise).rejects.toEqual(gotTxnIds[0]);
expect(failPromise2).rejects.toEqual(gotTxnIds[1]);
const okPromise = slidingSync.setListRanges(0, [[0, 20]]);
let txnId;
httpBackend.when("POST", syncUrl).check((req) => {
txnId = req.data.txn_id;
}).respond(200, () => {
// include the txn_id, earlier requests should now be reject()ed.
return {
pos: "g",
txn_id: txnId,
};
});
await httpBackend.flushAllExpected();
await okPromise;
expect(txnId).toBeDefined();
});
it("should not reject later pending promises if an earlier transaction is acknowledged", async () => {
// i.e if we have [A,B,C] and see txn_id=B then C should not be rejected but A should.
const gotTxnIds = [];
const pushTxn = function(req) {
gotTxnIds.push(req.data.txn_id);
};
const A = slidingSync.setListRanges(0, [[20, 40]]);
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "A" });
await httpBackend.flushAllExpected();
const B = slidingSync.setListRanges(0, [[60, 70]]);
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "B" }); // missing txn_id
await httpBackend.flushAllExpected();
// attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection
// which is a fail.
expect(A).rejects.toEqual(gotTxnIds[0]);
const C = slidingSync.setListRanges(0, [[0, 20]]);
let pendingC = true;
C.finally(() => {
pendingC = false;
});
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, () => {
// include the txn_id for B, so C's promise is outstanding
return {
pos: "C",
txn_id: gotTxnIds[1],
};
});
await httpBackend.flushAllExpected();
// A is rejected, see above
expect(B).resolves.toEqual(gotTxnIds[1]); // B is resolved
expect(pendingC).toBe(true); // C is pending still
});
it("should do nothing for unknown txn_ids", async () => {
const promise = slidingSync.setListRanges(0, [[20, 40]]);
let pending = true;
promise.finally(() => {
pending = false;
});
let txnId;
httpBackend.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toBeFalsy();
expect(body.lists[0]).toEqual({
ranges: [[20, 40]],
});
expect(body.txn_id).toBeTruthy();
txnId = body.txn_id;
}).respond(200, function() {
return {
pos: "ccc",
txn_id: "bogus transaction id",
lists: [{ count: 5 }],
extensions: {},
};
});
await httpBackend.flushAllExpected();
expect(txnId).toBeDefined();
expect(pending).toBe(true);
slidingSync.stop();
});
});
@@ -20,6 +20,7 @@ import * as utils from "../src/utils";
// try to load the olm library.
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
global.Olm = require('@matrix-org/olm');
logger.log('loaded libolm');
} catch (e) {
@@ -28,6 +29,7 @@ try {
// also try to set node crypto
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const crypto = require('crypto');
utils.setCrypto(crypto);
} catch (err) {
+1 -1
View File
@@ -24,5 +24,5 @@ limitations under the License.
* expect(beaconLivenessEmits.length).toBe(1);
* ```
*/
export const filterEmitCallsByEventType = (eventType: string, spy: jest.SpyInstance<any, unknown[]>) =>
export const filterEmitCallsByEventType = (eventType: string, spy: jest.SpyInstance<any, any[]>) =>
spy.mock.calls.filter((args) => args[0] === eventType);
+3 -3
View File
@@ -147,9 +147,9 @@ export function mkEventCustom<T>(base: T): T & GeneratedMetadata {
interface IPresenceOpts {
user?: string;
sender?: string;
url: string;
name: string;
ago: number;
url?: string;
name?: string;
ago?: number;
presence?: string;
event?: boolean;
}
+5
View File
@@ -21,18 +21,21 @@ describe("NamespacedValue", () => {
const ns = new NamespacedValue("stable", "unstable");
expect(ns.name).toBe(ns.stable);
expect(ns.altName).toBe(ns.unstable);
expect(ns.names).toEqual([ns.stable, ns.unstable]);
});
it("should return unstable if there is no stable", () => {
const ns = new NamespacedValue(null, "unstable");
expect(ns.name).toBe(ns.unstable);
expect(ns.altName).toBeFalsy();
expect(ns.names).toEqual([ns.unstable]);
});
it("should have a falsey unstable if needed", () => {
const ns = new NamespacedValue("stable", null);
expect(ns.name).toBe(ns.stable);
expect(ns.altName).toBeFalsy();
expect(ns.names).toEqual([ns.stable]);
});
it("should match against either stable or unstable", () => {
@@ -58,12 +61,14 @@ describe("UnstableValue", () => {
const ns = new UnstableValue("stable", "unstable");
expect(ns.name).toBe(ns.unstable);
expect(ns.altName).toBe(ns.stable);
expect(ns.names).toEqual([ns.unstable, ns.stable]);
});
it("should return unstable if there is no stable", () => {
const ns = new UnstableValue(null, "unstable");
expect(ns.name).toBe(ns.unstable);
expect(ns.altName).toBeFalsy();
expect(ns.names).toEqual([ns.unstable]);
});
it("should not permit falsey unstable values", () => {
+17 -18
View File
@@ -153,27 +153,26 @@ describe("AutoDiscovery", function() {
]);
});
it("should return FAIL_PROMPT when .well-known returns not-JSON", function() {
it("should return FAIL_PROMPT when .well-known returns not-JSON", async () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "abc");
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "abc", true);
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
AutoDiscovery.findClientConfig("example.org").then(
expect(expected).toEqual,
),
]);
});
+7 -6
View File
@@ -15,6 +15,7 @@ import { CRYPTO_ENABLED } from "../../src/client";
import { DeviceInfo } from "../../src/crypto/deviceinfo";
import { logger } from '../../src/logger';
import { MemoryStore } from "../../src";
import { IStore } from '../../src/store';
const Olm = global.Olm;
@@ -158,8 +159,8 @@ describe("Crypto", function() {
let fakeEmitter;
beforeEach(async function() {
const mockStorage = new MockStorageApi();
const clientStore = new MemoryStore({ localStorage: mockStorage });
const mockStorage = new MockStorageApi() as unknown as Storage;
const clientStore = new MemoryStore({ localStorage: mockStorage }) as unknown as IStore;
const cryptoStore = new MemoryCryptoStore();
cryptoStore.storeEndToEndDeviceData({
@@ -469,12 +470,12 @@ describe("Crypto", function() {
jest.setTimeout(10000);
const client = (new TestClient("@a:example.com", "dev")).client;
await client.initCrypto();
client.crypto.getSecretStorageKey = async () => null;
client.crypto.getSecretStorageKey = jest.fn().mockResolvedValue(null);
client.crypto.isCrossSigningReady = async () => false;
client.crypto.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null);
client.crypto.baseApis.setAccountData = () => null;
client.crypto.baseApis.uploadKeySignatures = () => null;
client.crypto.baseApis.http.authedRequest = () => null;
client.crypto.baseApis.setAccountData = jest.fn().mockResolvedValue(null);
client.crypto.baseApis.uploadKeySignatures = jest.fn();
client.crypto.baseApis.http.authedRequest = jest.fn();
const createSecretStorageKey = async () => {
return {
keyInfo: undefined, // Returning undefined here used to cause a crash
+2 -2
View File
@@ -32,8 +32,8 @@ import { ClientEvent, MatrixClient, RoomMember } from '../../../../src';
import { DeviceInfo, IDevice } from '../../../../src/crypto/deviceinfo';
import { DeviceTrustLevel } from '../../../../src/crypto/CrossSigning';
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
const ROOM_ID = '!ROOM:ID';
+1 -1
View File
@@ -34,7 +34,7 @@ import { IAbortablePromise, MatrixScheduler } from '../../../src';
const Olm = global.Olm;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
const ROOM_ID = '!ROOM:ID';
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {
IndexedDBCryptoStore,
} from '../../../src/crypto/store/indexeddb-crypto-store';
import { CryptoStore } from '../../../src/crypto/store/base';
import { IndexedDBCryptoStore } from '../../../src/crypto/store/indexeddb-crypto-store';
import { LocalStorageCryptoStore } from '../../../src/crypto/store/localStorage-crypto-store';
import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store';
import { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager';
@@ -26,36 +26,39 @@ import 'jest-localstorage-mock';
const requests = [
{
requestId: "A",
requestBody: { session_id: "A", room_id: "A" },
requestBody: { session_id: "A", room_id: "A", sender_key: "A", algorithm: "m.megolm.v1.aes-sha2" },
state: RoomKeyRequestState.Sent,
recipients: [
{ userId: "@alice:example.com", deviceId: "*" },
{ userId: "@becca:example.com", deviceId: "foobarbaz" },
],
},
{
requestId: "B",
requestBody: { session_id: "B", room_id: "B" },
requestBody: { session_id: "B", room_id: "B", sender_key: "B", algorithm: "m.megolm.v1.aes-sha2" },
state: RoomKeyRequestState.Sent,
recipients: [
{ userId: "@alice:example.com", deviceId: "*" },
{ userId: "@carrie:example.com", deviceId: "barbazquux" },
],
},
{
requestId: "C",
requestBody: { session_id: "C", room_id: "C" },
requestBody: { session_id: "C", room_id: "C", sender_key: "B", algorithm: "m.megolm.v1.aes-sha2" },
state: RoomKeyRequestState.Unsent,
recipients: [
{ userId: "@becca:example.com", deviceId: "foobarbaz" },
],
},
];
describe.each([
["IndexedDBCryptoStore",
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
["LocalStorageCryptoStore",
() => new IndexedDBCryptoStore(undefined, "tests")],
["MemoryCryptoStore", () => {
const store = new IndexedDBCryptoStore(undefined, "tests");
// @ts-ignore set private properties
store.backend = new MemoryCryptoStore();
// @ts-ignore
store.backendPromise = Promise.resolve(store.backend);
return store;
}],
["LocalStorageCryptoStore", () => new LocalStorageCryptoStore(localStorage)],
["MemoryCryptoStore", () => new MemoryCryptoStore()],
])("Outgoing room key requests [%s]", function(name, dbFactory) {
let store;
let store: CryptoStore;
beforeAll(async () => {
store = dbFactory();
@@ -75,6 +78,15 @@ describe.each([
});
});
it("getOutgoingRoomKeyRequestsByTarget retrieves all entries with a given target",
async () => {
const r = await store.getOutgoingRoomKeyRequestsByTarget(
"@becca:example.com", "foobarbaz", [RoomKeyRequestState.Sent],
);
expect(r).toHaveLength(1);
expect(r[0]).toEqual(requests[0]);
});
test("getOutgoingRoomKeyRequestByState retrieves any entry in a given state",
async () => {
const r =
+4 -4
View File
@@ -250,8 +250,8 @@ describe("Secrets", function() {
osborne2.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
"VAX": {
user_id: "@alice:example.com",
device_id: "VAX",
verified: 0,
known: false,
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:VAX": vaxDevice.deviceEd25519Key,
@@ -261,9 +261,9 @@ describe("Secrets", function() {
});
vax.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
"Osborne2": {
user_id: "@alice:example.com",
device_id: "Osborne2",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
verified: 0,
known: false,
keys: {
"ed25519:Osborne2": osborne2Device.deviceEd25519Key,
"curve25519:Osborne2": osborne2Device.deviceCurve25519Key,
@@ -13,9 +13,9 @@ 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 { MatrixClient } from "../../../../src/client";
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
import { MatrixEvent } from "../../../../src/models/event";
"../../../../src/crypto/verification/request/ToDeviceChannel";
describe("InRoomChannel tests", function() {
const ALICE = "@alice:hs.tld";
@@ -23,7 +23,7 @@ describe("InRoomChannel tests", function() {
const MALORY = "@malory:hs.tld";
const client = {
getUserId() { return ALICE; },
};
} as unknown as MatrixClient;
it("getEventType only returns .request for a message with a msgtype", function() {
const invalidEvent = new MatrixEvent({
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import "../../../olm-loader";
import { verificationMethods } from "../../../../src/crypto";
import { CryptoEvent, verificationMethods } from "../../../../src/crypto";
import { logger } from "../../../../src/logger";
import { SAS } from "../../../../src/crypto/verification/SAS";
import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
@@ -52,23 +52,22 @@ describe("verification request integration tests with crypto layer", function()
alice.client.crypto.deviceList.getRawStoredDevicesForUser = function() {
return {
Dynabook: {
algorithms: [],
verified: 0,
known: false,
keys: {
"ed25519:Dynabook": "bob+base64+ed25519+key",
},
},
};
};
alice.client.downloadKeys = () => {
return Promise.resolve();
};
bob.client.downloadKeys = () => {
return Promise.resolve();
};
bob.client.on("crypto.verification.request", (request) => {
alice.client.downloadKeys = jest.fn().mockResolvedValue({});
bob.client.downloadKeys = jest.fn().mockResolvedValue({});
bob.client.on(CryptoEvent.VerificationRequest, (request) => {
const bobVerifier = request.beginKeyVerification(verificationMethods.SAS);
bobVerifier.verify();
// XXX: Private function access (but it's a test, so we're okay)
// @ts-ignore Private function access (but it's a test, so we're okay)
bobVerifier.endTimer();
});
const aliceRequest = await alice.client.requestVerification("@bob:example.com");
@@ -76,7 +75,7 @@ describe("verification request integration tests with crypto layer", function()
const aliceVerifier = aliceRequest.verifier;
expect(aliceVerifier).toBeInstanceOf(SAS);
// XXX: Private function access (but it's a test, so we're okay)
// @ts-ignore Private function access (but it's a test, so we're okay)
aliceVerifier.endTimer();
alice.stop();
@@ -19,10 +19,14 @@ import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
import { MatrixEvent } from "../../../../src/models/event";
import { SAS } from "../../../../src/crypto/verification/SAS";
import { DeviceInfo } from "../../../../src/crypto/deviceinfo";
import { verificationMethods } from "../../../../src/crypto";
import { CryptoEvent, verificationMethods } from "../../../../src/crypto";
import * as olmlib from "../../../../src/crypto/olmlib";
import { logger } from "../../../../src/logger";
import { resetCrossSigningKeys } from "../crypto-utils";
import { VerificationBase } from "../../../../src/crypto/verification/Base";
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
import { MatrixClient } from "../../../../src";
import { VerificationRequest } from "../../../../src/crypto/verification/request/VerificationRequest";
const Olm = global.Olm;
@@ -48,13 +52,15 @@ describe("SAS verification", function() {
//channel, baseApis, userId, deviceId, startEvent, request
const request = {
onVerifierCancelled: function() {},
};
} as VerificationRequest;
const channel = {
send: function() {
return Promise.resolve();
},
};
const sas = new SAS(channel, {}, "@alice:example.com", "ABCDEFG", null, request);
} as unknown as IVerificationChannel;
const mockClient = {} as unknown as MatrixClient;
const event = new MatrixEvent({ type: 'test' });
const sas = new SAS(channel, mockClient, "@alice:example.com", "ABCDEFG", event, request);
sas.handleEvent(new MatrixEvent({
sender: "@alice:example.com",
type: "es.inquisition",
@@ -65,7 +71,7 @@ describe("SAS verification", function() {
expect(spy).toHaveBeenCalled();
// Cancel the SAS for cleanup (we started a verification, so abort)
sas.cancel();
sas.cancel(new Error('error'));
});
describe("verification", () => {
@@ -403,16 +409,12 @@ describe("SAS verification", function() {
},
);
alice.client.setDeviceVerified = jest.fn();
alice.client.downloadKeys = () => {
return Promise.resolve();
};
alice.client.downloadKeys = jest.fn().mockResolvedValue({});
bob.client.setDeviceVerified = jest.fn();
bob.client.downloadKeys = () => {
return Promise.resolve();
};
bob.client.downloadKeys = jest.fn().mockResolvedValue({});
const bobPromise = new Promise((resolve, reject) => {
bob.client.on("crypto.verification.request", request => {
const bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
bob.client.on(CryptoEvent.VerificationRequest, request => {
request.verifier.on("show_sas", (e) => {
e.mismatch();
});
@@ -421,7 +423,7 @@ describe("SAS verification", function() {
});
const aliceVerifier = alice.client.beginKeyVerification(
verificationMethods.SAS, bob.client.getUserId(), bob.client.deviceId,
verificationMethods.SAS, bob.client.getUserId()!, bob.client.deviceId!,
);
const aliceSpy = jest.fn();
@@ -501,7 +503,7 @@ describe("SAS verification", function() {
aliceSasEvent = null;
bobSasEvent = null;
bobPromise = new Promise((resolve, reject) => {
bobPromise = new Promise<void>((resolve, reject) => {
bob.client.on("crypto.verification.request", async (request) => {
const verifier = request.beginKeyVerification(SAS.NAME);
verifier.on("show_sas", (e) => {
@@ -18,6 +18,9 @@ import { CrossSigningInfo } from '../../../../src/crypto/CrossSigning';
import { encodeBase64 } from "../../../../src/crypto/olmlib";
import { setupWebcrypto, teardownWebcrypto } from './util';
import { VerificationBase } from '../../../../src/crypto/verification/Base';
import { MatrixClient, MatrixEvent } from '../../../../src';
import { VerificationRequest } from '../../../../src/crypto/verification/request/VerificationRequest';
import { IVerificationChannel } from '../../../../src/crypto/verification/request/Channel';
jest.useFakeTimers();
@@ -54,9 +57,21 @@ describe("self-verifications", () => {
cacheCallbacks,
);
crossSigningInfo.keys = {
master: { keys: { X: testKeyPub } },
self_signing: { keys: { X: testKeyPub } },
user_signing: { keys: { X: testKeyPub } },
master: {
keys: { X: testKeyPub },
usage: [],
user_id: 'user-id',
},
self_signing: {
keys: { X: testKeyPub },
usage: [],
user_id: 'user-id',
},
user_signing: {
keys: { X: testKeyPub },
usage: [],
user_id: 'user-id',
},
};
const secretStorage = {
@@ -79,20 +94,22 @@ describe("self-verifications", () => {
getUserId: () => userId,
getKeyBackupVersion: () => Promise.resolve({}),
restoreKeyBackupWithCache,
};
} as unknown as MatrixClient;
const request = {
onVerifierFinished: () => undefined,
};
} as unknown as VerificationRequest;
const verification = new VerificationBase(
undefined, // channel
undefined as unknown as IVerificationChannel, // channel
client, // baseApis
userId,
"ABC", // deviceId
undefined, // startEvent
undefined as unknown as MatrixEvent, // startEvent
request,
);
// @ts-ignore set private property
verification.resolve = () => undefined;
const result = await verification.done();
@@ -102,12 +119,12 @@ describe("self-verifications", () => {
expect(secretStorage.request.mock.calls.length).toBe(4);
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[0][1])
.toEqual(testKey);
.toEqual(testKey);
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[1][1])
.toEqual(testKey);
.toEqual(testKey);
expect(storeSessionBackupPrivateKey.mock.calls[0][0])
.toEqual(testKey);
.toEqual(testKey);
expect(restoreKeyBackupWithCache).toHaveBeenCalled();
@@ -19,20 +19,23 @@ import nodeCrypto from "crypto";
import { TestClient } from '../../../TestClient';
import { MatrixEvent } from "../../../../src/models/event";
import { IRoomTimelineData } from "../../../../src/models/event-timeline-set";
import { Room, RoomEvent } from "../../../../src/models/room";
import { logger } from '../../../../src/logger';
import { MatrixClient, ClientEvent } from '../../../../src/client';
export async function makeTestClients(userInfos, options) {
const clients = [];
const timeouts = [];
const clientMap = {};
const sendToDevice = function(type, map) {
export async function makeTestClients(userInfos, options): Promise<[TestClient[], () => void]> {
const clients: TestClient[] = [];
const timeouts: ReturnType<typeof setTimeout>[] = [];
const clientMap: Record<string, Record<string, MatrixClient>> = {};
const makeSendToDevice = (matrixClient: MatrixClient): MatrixClient['sendToDevice'] => async (type, map) => {
// logger.log(this.getUserId(), "sends", type, map);
for (const [userId, devMap] of Object.entries(map)) {
if (userId in clientMap) {
for (const [deviceId, msg] of Object.entries(devMap)) {
if (deviceId in clientMap[userId]) {
const event = new MatrixEvent({
sender: this.getUserId(), // eslint-disable-line @babel/no-invalid-this
sender: matrixClient.getUserId()!,
type: type,
content: msg,
});
@@ -42,18 +45,19 @@ export async function makeTestClients(userInfos, options) {
Promise.resolve();
decryptionPromise.then(
() => client.emit("toDeviceEvent", event),
() => client.emit(ClientEvent.ToDeviceEvent, event),
);
}
}
}
}
return {};
};
const sendEvent = function(room, type, content) {
const makeSendEvent = (matrixClient: MatrixClient) => (room, type, content) => {
// make up a unique ID as the event ID
const eventId = "$" + this.makeTxnId(); // eslint-disable-line @babel/no-invalid-this
const eventId = "$" + matrixClient.makeTxnId();
const rawEvent = {
sender: this.getUserId(), // eslint-disable-line @babel/no-invalid-this
sender: matrixClient.getUserId()!,
type: type,
content: content,
room_id: room,
@@ -63,22 +67,24 @@ export async function makeTestClients(userInfos, options) {
const event = new MatrixEvent(rawEvent);
const remoteEcho = new MatrixEvent(Object.assign({}, rawEvent, {
unsigned: {
transaction_id: this.makeTxnId(), // eslint-disable-line @babel/no-invalid-this
transaction_id: matrixClient.makeTxnId(),
},
}));
const timeout = setTimeout(() => {
for (const tc of clients) {
if (tc.client === this) { // eslint-disable-line @babel/no-invalid-this
const room = new Room('test', tc.client, tc.client.getUserId()!);
const roomTimelineData = {} as unknown as IRoomTimelineData;
if (tc.client === matrixClient) {
logger.log("sending remote echo!!");
tc.client.emit("Room.timeline", remoteEcho);
tc.client.emit(RoomEvent.Timeline, remoteEcho, room, false, false, roomTimelineData);
} else {
tc.client.emit("Room.timeline", event);
tc.client.emit(RoomEvent.Timeline, event, room, false, false, roomTimelineData);
}
}
});
timeouts.push(timeout);
timeouts.push(timeout as unknown as ReturnType<typeof setTimeout>);
return Promise.resolve({ event_id: eventId });
};
@@ -99,8 +105,8 @@ export async function makeTestClients(userInfos, options) {
clientMap[userInfo.userId] = {};
}
clientMap[userInfo.userId][userInfo.deviceId] = testClient.client;
testClient.client.sendToDevice = sendToDevice;
testClient.client.sendEvent = sendEvent;
testClient.client.sendToDevice = makeSendToDevice(testClient.client);
testClient.client.sendEvent = makeSendEvent(testClient.client);
clients.push(testClient);
}
@@ -116,11 +122,12 @@ export async function makeTestClients(userInfos, options) {
export function setupWebcrypto() {
global.crypto = {
getRandomValues: (buf) => {
return nodeCrypto.randomFillSync(buf);
return nodeCrypto.randomFillSync(buf as any);
},
};
} as unknown as Crypto;
}
export function teardownWebcrypto() {
// @ts-ignore undefined != Crypto
global.crypto = undefined;
}
@@ -19,11 +19,18 @@ import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoo
import { ToDeviceChannel } from
"../../../../src/crypto/verification/request/ToDeviceChannel";
import { MatrixEvent } from "../../../../src/models/event";
import { MatrixClient } from "../../../../src/client";
import { setupWebcrypto, teardownWebcrypto } from "./util";
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
import { VerificationBase } from "../../../../src/crypto/verification/Base";
function makeMockClient(userId, deviceId) {
type MockClient = MatrixClient & {
popEvents: () => MatrixEvent[];
popDeviceEvents: (userId: string, deviceId: string) => MatrixEvent[];
};
function makeMockClient(userId: string, deviceId: string): MockClient {
let counter = 1;
let events = [];
let events: MatrixEvent[] = [];
const deviceEvents = {};
return {
getUserId() { return userId; },
@@ -54,16 +61,18 @@ function makeMockClient(userId, deviceId) {
deviceEvents[userId][deviceId].push(event);
}
}
return Promise.resolve();
return Promise.resolve({});
},
popEvents() {
// @ts-ignore special testing fn
popEvents(): MatrixEvent[] {
const e = events;
events = [];
return e;
},
popDeviceEvents(userId, deviceId) {
// @ts-ignore special testing fn
popDeviceEvents(userId: string, deviceId: string): MatrixEvent[] {
const forDevice = deviceEvents[userId];
const events = forDevice && forDevice[deviceId];
const result = events || [];
@@ -72,12 +81,21 @@ function makeMockClient(userId, deviceId) {
}
return result;
},
};
} as unknown as MockClient;
}
const MOCK_METHOD = "mock-verify";
class MockVerifier {
constructor(channel, client, userId, deviceId, startEvent) {
class MockVerifier extends VerificationBase<'', any> {
public _channel;
public _startEvent;
constructor(
channel: IVerificationChannel,
client: MatrixClient,
userId: string,
deviceId: string,
startEvent: MatrixEvent,
) {
super(channel, client, userId, deviceId, startEvent, {} as unknown as VerificationRequest);
this._channel = channel;
this._startEvent = startEvent;
}
@@ -115,7 +133,10 @@ function makeRemoteEcho(event) {
async function distributeEvent(ownRequest, theirRequest, event) {
await ownRequest.channel.handleEvent(
makeRemoteEcho(event), ownRequest, true);
makeRemoteEcho(event),
ownRequest,
true,
);
await theirRequest.channel.handleEvent(event, theirRequest, true);
}
@@ -133,12 +154,19 @@ describe("verification request unit tests", function() {
it("transition from UNSENT to DONE through happy path", async function() {
const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1");
const verificationMethods = new Map(
[[MOCK_METHOD, MockVerifier]],
) as unknown as Map<string, typeof VerificationBase>;
const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob.getUserId()),
new Map([[MOCK_METHOD, MockVerifier]]), alice);
new InRoomChannel(alice, "!room", bob.getUserId()!),
verificationMethods,
alice,
);
const bobRequest = new VerificationRequest(
new InRoomChannel(bob, "!room"),
new Map([[MOCK_METHOD, MockVerifier]]), bob);
verificationMethods,
bob,
);
expect(aliceRequest.invalid).toBe(true);
expect(bobRequest.invalid).toBe(true);
@@ -157,7 +185,7 @@ describe("verification request unit tests", function() {
expect(aliceRequest.ready).toBe(true);
const verifier = aliceRequest.beginKeyVerification(MOCK_METHOD);
await verifier.start();
await (verifier as MockVerifier).start();
const [startEvent] = alice.popEvents();
expect(startEvent.getType()).toBe(START_TYPE);
await distributeEvent(aliceRequest, bobRequest, startEvent);
@@ -165,8 +193,7 @@ describe("verification request unit tests", function() {
expect(aliceRequest.verifier).toBeInstanceOf(MockVerifier);
expect(bobRequest.started).toBe(true);
expect(bobRequest.verifier).toBeInstanceOf(MockVerifier);
await bobRequest.verifier.start();
await (bobRequest.verifier as MockVerifier).start();
const [bobDoneEvent] = bob.popEvents();
expect(bobDoneEvent.getType()).toBe(DONE_TYPE);
await distributeEvent(bobRequest, aliceRequest, bobDoneEvent);
@@ -180,12 +207,20 @@ describe("verification request unit tests", function() {
it("methods only contains common methods", async function() {
const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1");
const aliceVerificationMethods = new Map(
[["c", function() {}], ["a", function() {}]],
) as unknown as Map<string, typeof VerificationBase>;
const bobVerificationMethods = new Map(
[["c", function() {}], ["b", function() {}]],
) as unknown as Map<string, typeof VerificationBase>;
const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob.getUserId()),
new Map([["c", function() {}], ["a", function() {}]]), alice);
new InRoomChannel(alice, "!room", bob.getUserId()!),
aliceVerificationMethods, alice);
const bobRequest = new VerificationRequest(
new InRoomChannel(bob, "!room"),
new Map([["c", function() {}], ["b", function() {}]]), bob);
bobVerificationMethods,
bob,
);
await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents();
await distributeEvent(aliceRequest, bobRequest, requestEvent);
@@ -201,13 +236,22 @@ describe("verification request unit tests", function() {
const bob1 = makeMockClient("@bob:matrix.tld", "device1");
const bob2 = makeMockClient("@bob:matrix.tld", "device2");
const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob1.getUserId()), new Map(), alice);
new InRoomChannel(alice, "!room", bob1.getUserId()!),
new Map(),
alice,
);
await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents();
const bob1Request = new VerificationRequest(
new InRoomChannel(bob1, "!room"), new Map(), bob1);
new InRoomChannel(bob1, "!room"),
new Map(),
bob1,
);
const bob2Request = new VerificationRequest(
new InRoomChannel(bob2, "!room"), new Map(), bob2);
new InRoomChannel(bob2, "!room"),
new Map(),
bob2,
);
await bob1Request.channel.handleEvent(requestEvent, bob1Request, true);
await bob2Request.channel.handleEvent(requestEvent, bob2Request, true);
@@ -222,22 +266,34 @@ describe("verification request unit tests", function() {
it("verify own device with to_device messages", async function() {
const bob1 = makeMockClient("@bob:matrix.tld", "device1");
const bob2 = makeMockClient("@bob:matrix.tld", "device2");
const verificationMethods = new Map(
[[MOCK_METHOD, MockVerifier]],
) as unknown as Map<string, typeof VerificationBase>;
const bob1Request = new VerificationRequest(
new ToDeviceChannel(bob1, bob1.getUserId(), ["device1", "device2"],
ToDeviceChannel.makeTransactionId(), "device2"),
new Map([[MOCK_METHOD, MockVerifier]]), bob1);
new ToDeviceChannel(
bob1,
bob1.getUserId()!,
["device1", "device2"],
ToDeviceChannel.makeTransactionId(),
"device2",
),
verificationMethods,
bob1,
);
const to = { userId: "@bob:matrix.tld", deviceId: "device2" };
const verifier = bob1Request.beginKeyVerification(MOCK_METHOD, to);
expect(verifier).toBeInstanceOf(MockVerifier);
await verifier.start();
await (verifier as MockVerifier).start();
const [startEvent] = bob1.popDeviceEvents(to.userId, to.deviceId);
expect(startEvent.getType()).toBe(START_TYPE);
const bob2Request = new VerificationRequest(
new ToDeviceChannel(bob2, bob2.getUserId(), ["device1"]),
new Map([[MOCK_METHOD, MockVerifier]]), bob2);
new ToDeviceChannel(bob2, bob2.getUserId()!, ["device1"]),
verificationMethods,
bob2,
);
await bob2Request.channel.handleEvent(startEvent, bob2Request, true);
await bob2Request.verifier.start();
await (bob2Request.verifier as MockVerifier).start();
const [doneEvent1] = bob2.popDeviceEvents("@bob:matrix.tld", "device1");
expect(doneEvent1.getType()).toBe(DONE_TYPE);
await bob1Request.channel.handleEvent(doneEvent1, bob1Request, true);
@@ -253,11 +309,13 @@ describe("verification request unit tests", function() {
const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1");
const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob.getUserId()), new Map(), alice);
new InRoomChannel(alice, "!room", bob.getUserId()!),
new Map(),
alice,
);
await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents();
await aliceRequest.channel.handleEvent(requestEvent, aliceRequest, true,
true, true);
await aliceRequest.channel.handleEvent(requestEvent, aliceRequest, true);
expect(aliceRequest.cancelled).toBe(false);
expect(aliceRequest._cancellingUserId).toBe(undefined);
@@ -269,11 +327,17 @@ describe("verification request unit tests", function() {
const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1");
const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob.getUserId()), new Map(), alice);
new InRoomChannel(alice, "!room", bob.getUserId()!),
new Map(),
alice,
);
await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents();
const bobRequest = new VerificationRequest(
new InRoomChannel(bob, "!room"), new Map(), bob);
new InRoomChannel(bob, "!room"),
new Map(),
bob,
);
await bobRequest.channel.handleEvent(requestEvent, bobRequest, true);
+32 -1
View File
@@ -16,14 +16,15 @@ limitations under the License.
import * as utils from "../test-utils/test-utils";
import {
DuplicateStrategy,
EventTimeline,
EventTimelineSet,
EventType,
Filter,
MatrixClient,
MatrixEvent,
MatrixEventEvent,
Room,
DuplicateStrategy,
} from '../../src';
import { Thread } from "../../src/models/thread";
import { ReEmitter } from "../../src/ReEmitter";
@@ -291,4 +292,34 @@ describe('EventTimelineSet', () => {
expect(eventTimelineSet.canContain(event)).toBeTruthy();
});
});
describe("handleRemoteEcho", () => {
it("should add to liveTimeline only if the event matches the filter", () => {
const filter = new Filter(client.getUserId()!, "test_filter");
filter.setDefinition({
room: {
timeline: {
types: [EventType.RoomMessage],
},
},
});
const eventTimelineSet = new EventTimelineSet(room, { filter }, client);
const roomMessageEvent = new MatrixEvent({
type: EventType.RoomMessage,
content: { body: "test" },
event_id: "!test1:server",
});
eventTimelineSet.handleRemoteEcho(roomMessageEvent, "~!local-event-id:server", roomMessageEvent.getId());
expect(eventTimelineSet.getLiveTimeline().getEvents()).toContain(roomMessageEvent);
const roomFilteredEvent = new MatrixEvent({
type: "other_event_type",
content: { body: "test" },
event_id: "!test2:server",
});
eventTimelineSet.handleRemoteEcho(roomFilteredEvent, "~!local-event-id:server", roomFilteredEvent.getId());
expect(eventTimelineSet.getLiveTimeline().getEvents()).not.toContain(roomFilteredEvent);
});
});
});
+293 -25
View File
@@ -32,8 +32,8 @@ class FakeClient {
const getFakeClient = (): MatrixClient => new FakeClient() as unknown as MatrixClient;
describe("InteractiveAuth", function() {
it("should start an auth stage and complete it", function() {
describe("InteractiveAuth", () => {
it("should start an auth stage and complete it", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
@@ -59,7 +59,7 @@ describe("InteractiveAuth", function() {
});
// first we expect a call here
stateUpdated.mockImplementation(function(stage) {
stateUpdated.mockImplementation((stage) => {
logger.log('aaaa');
expect(stage).toEqual(AuthType.Password);
ia.submitAuthDict({
@@ -69,23 +69,130 @@ describe("InteractiveAuth", function() {
// .. which should trigger a call here
const requestRes = { "a": "b" };
doRequest.mockImplementation(function(authData) {
doRequest.mockImplementation(async (authData) => {
logger.log('cccc');
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Password,
});
return Promise.resolve(requestRes);
return requestRes;
});
return ia.attemptAuth().then(function(res) {
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(1);
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(1);
});
it("should make a request if no authdata is provided", function() {
it("should handle auth errcode presence ", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest: doRequest,
stateUpdated: stateUpdated,
requestEmailToken: jest.fn(),
authData: {
session: "sessionId",
flows: [
{ stages: [AuthType.Password] },
],
errcode: "MockError0",
params: {
[AuthType.Password]: { param: "aa" },
},
},
});
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams(AuthType.Password)).toEqual({
param: "aa",
});
// first we expect a call here
stateUpdated.mockImplementation((stage) => {
logger.log('aaaa');
expect(stage).toEqual(AuthType.Password);
ia.submitAuthDict({
type: AuthType.Password,
});
});
// .. which should trigger a call here
const requestRes = { "a": "b" };
doRequest.mockImplementation(async (authData) => {
logger.log('cccc');
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Password,
});
return requestRes;
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(1);
});
it("should handle set emailSid for email flow", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
doRequest,
stateUpdated,
requestEmailToken,
matrixClient: getFakeClient(),
emailSid: 'myEmailSid',
authData: {
session: "sessionId",
flows: [
{ stages: [AuthType.Email, AuthType.Password] },
],
params: {
[AuthType.Email]: { param: "aa" },
[AuthType.Password]: { param: "bb" },
},
},
});
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams(AuthType.Email)).toEqual({
param: "aa",
});
// first we expect a call here
stateUpdated.mockImplementation((stage) => {
logger.log('husky');
expect(stage).toEqual(AuthType.Email);
ia.submitAuthDict({
type: AuthType.Email,
});
});
// .. which should trigger a call here
const requestRes = { "a": "b" };
doRequest.mockImplementation(async (authData) => {
logger.log('barfoo');
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Email,
});
return requestRes;
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(1);
expect(requestEmailToken).toBeCalledTimes(0);
expect(ia.getEmailSid()).toBe("myEmailSid");
});
it("should make a request if no authdata is provided", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
@@ -101,7 +208,7 @@ describe("InteractiveAuth", function() {
expect(ia.getStageParams(AuthType.Password)).toBe(undefined);
// first we expect a call to doRequest
doRequest.mockImplementation(function(authData) {
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual(null); // first request should be null
const err = new MatrixError({
@@ -119,7 +226,7 @@ describe("InteractiveAuth", function() {
// .. which should be followed by a call to stateUpdated
const requestRes = { "a": "b" };
stateUpdated.mockImplementation(function(stage) {
stateUpdated.mockImplementation((stage) => {
expect(stage).toEqual(AuthType.Password);
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams(AuthType.Password)).toEqual({
@@ -127,13 +234,13 @@ describe("InteractiveAuth", function() {
});
// submitAuthDict should trigger another call to doRequest
doRequest.mockImplementation(function(authData) {
doRequest.mockImplementation(async (authData) => {
logger.log("request2", authData);
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Password,
});
return Promise.resolve(requestRes);
return requestRes;
});
ia.submitAuthDict({
@@ -141,14 +248,76 @@ describe("InteractiveAuth", function() {
});
});
return ia.attemptAuth().then(function(res) {
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(2);
expect(stateUpdated).toBeCalledTimes(1);
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(2);
expect(stateUpdated).toBeCalledTimes(1);
});
it("should start an auth stage and reject if no auth flow", function() {
it("should make a request if authdata is null", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
authData: null,
matrixClient: getFakeClient(),
stateUpdated,
doRequest,
requestEmailToken,
});
expect(ia.getSessionId()).toBe(undefined);
expect(ia.getStageParams(AuthType.Password)).toBe(undefined);
// first we expect a call to doRequest
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual(null); // first request should be null
const err = new MatrixError({
session: "sessionId",
flows: [
{ stages: [AuthType.Password] },
],
params: {
[AuthType.Password]: { param: "aa" },
},
});
err.httpStatus = 401;
throw err;
});
// .. which should be followed by a call to stateUpdated
const requestRes = { "a": "b" };
stateUpdated.mockImplementation((stage) => {
expect(stage).toEqual(AuthType.Password);
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams(AuthType.Password)).toEqual({
param: "aa",
});
// submitAuthDict should trigger another call to doRequest
doRequest.mockImplementation(async (authData) => {
logger.log("request2", authData);
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Password,
});
return requestRes;
});
ia.submitAuthDict({
type: AuthType.Password,
});
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(2);
expect(stateUpdated).toBeCalledTimes(1);
});
it("should start an auth stage and reject if no auth flow", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
@@ -160,7 +329,7 @@ describe("InteractiveAuth", function() {
requestEmailToken,
});
doRequest.mockImplementation(function(authData) {
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual(null); // first request should be null
const err = new MatrixError({
@@ -174,9 +343,108 @@ describe("InteractiveAuth", function() {
throw err;
});
return ia.attemptAuth().catch(function(error) {
expect(error.message).toBe('No appropriate authentication flow found');
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(
new Error('No appropriate authentication flow found'),
);
});
it("should start an auth stage and reject if no auth flow but has session", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest,
stateUpdated,
requestEmailToken,
authData: {
},
sessionId: "sessionId",
});
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual({ "session": "sessionId" }); // has existing sessionId
const err = new MatrixError({
session: "sessionId",
flows: [],
params: {
[AuthType.Password]: { param: "aa" },
},
error: "Mock Error 1",
errcode: "MOCKERR1",
});
err.httpStatus = 401;
throw err;
});
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(
new Error('No appropriate authentication flow found'),
);
});
it("should handle unexpected error types without data propery set", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest,
stateUpdated,
requestEmailToken,
authData: {
session: "sessionId",
},
});
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual({ "session": "sessionId" }); // has existing sessionId
const err = new Error('myerror');
(err as any).httpStatus = 401;
throw err;
});
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(
new Error("myerror"),
);
});
it("should allow dummy auth", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest,
stateUpdated,
requestEmailToken,
authData: {
session: 'sessionId',
flows: [
{ stages: [AuthType.Dummy] },
],
params: {},
},
});
const requestRes = { "a": "b" };
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Dummy,
});
return requestRes;
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(0);
});
describe("requestEmailToken", () => {
@@ -247,7 +515,7 @@ describe("InteractiveAuth", function() {
doRequest, stateUpdated, requestEmailToken,
});
expect(async () => await ia.requestEmailToken()).rejects.toThrowError("unspecific network error");
await expect(ia.requestEmailToken.bind(ia)).rejects.toThrowError("unspecific network error");
});
it("only starts one request at a time", async () => {
+35
View File
@@ -1,3 +1,4 @@
import { SSOAction } from '../../src/@types/auth';
import { TestClient } from '../TestClient';
describe('Login request', function() {
@@ -22,3 +23,37 @@ describe('Login request', function() {
expect(client.client.getUserId()).toBe(response.user_id);
});
});
describe('SSO login URL', function() {
let client: TestClient;
beforeEach(function() {
client = new TestClient();
});
afterEach(function() {
client.stop();
});
describe('SSOAction', function() {
const redirectUri = "https://test.com/foo";
it('No action', function() {
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, undefined);
const url = new URL(urlString);
expect(url.searchParams.has('org.matrix.msc3824.action')).toBe(false);
});
it('register', function() {
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.REGISTER);
const url = new URL(urlString);
expect(url.searchParams.get('org.matrix.msc3824.action')).toEqual('register');
});
it('login', function() {
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.LOGIN);
const url = new URL(urlString);
expect(url.searchParams.get('org.matrix.msc3824.action')).toEqual('login');
});
});
});
+304 -2
View File
@@ -36,9 +36,14 @@ import { ReceiptType } from "../../src/@types/read_receipts";
import * as testUtils from "../test-utils/test-utils";
import { makeBeaconInfoContent } from "../../src/content-helpers";
import { M_BEACON_INFO } from "../../src/@types/beacon";
import { ContentHelpers, Room } from "../../src";
import { ContentHelpers, EventTimeline, Room } from "../../src";
import { supportsMatrixCall } from "../../src/webrtc/call";
import { makeBeaconEvent } from "../test-utils/beacon";
import {
IGNORE_INVITES_ACCOUNT_EVENT_KEY,
POLICIES_ACCOUNT_EVENT_TYPE,
PolicyScope,
} from "../../src/models/invites-ignorer";
jest.useFakeTimers();
@@ -427,7 +432,7 @@ describe("MatrixClient", function() {
}
});
});
await client.startClient();
await client.startClient({ filter });
await syncPromise;
});
@@ -1412,4 +1417,301 @@ describe("MatrixClient", function() {
expect(client.crypto.encryptAndSendToDevices).toHaveBeenLastCalledWith(deviceInfos, payload);
});
});
describe("support for ignoring invites", () => {
beforeEach(() => {
// Mockup `getAccountData`/`setAccountData`.
const dataStore = new Map();
client.setAccountData = function(eventType, content) {
dataStore.set(eventType, content);
return Promise.resolve();
};
client.getAccountData = function(eventType) {
const data = dataStore.get(eventType);
return new MatrixEvent({
content: data,
});
};
// Mockup `createRoom`/`getRoom`/`joinRoom`, including state.
const rooms = new Map();
client.createRoom = function(options = {}) {
const roomId = options["_roomId"] || `!room-${rooms.size}:example.org`;
const state = new Map();
const room = {
roomId,
_options: options,
_state: state,
getUnfilteredTimelineSet: function() {
return {
getLiveTimeline: function() {
return {
getState: function(direction) {
expect(direction).toBe(EventTimeline.FORWARDS);
return {
getStateEvents: function(type) {
const store = state.get(type) || {};
return Object.keys(store).map(key => store[key]);
},
};
},
};
},
};
},
};
rooms.set(roomId, room);
return Promise.resolve({ room_id: roomId });
};
client.getRoom = function(roomId) {
return rooms.get(roomId);
};
client.joinRoom = function(roomId) {
return this.getRoom(roomId) || this.createRoom({ _roomId: roomId });
};
// Mockup state events
client.sendStateEvent = function(roomId, type, content) {
const room = this.getRoom(roomId);
const state: Map<string, any> = room._state;
let store = state.get(type);
if (!store) {
store = {};
state.set(type, store);
}
const eventId = `$event-${Math.random()}:example.org`;
store[eventId] = {
getId: function() {
return eventId;
},
getRoomId: function() {
return roomId;
},
getContent: function() {
return content;
},
};
return { event_id: eventId };
};
client.redactEvent = function(roomId, eventId) {
const room = this.getRoom(roomId);
const state: Map<string, any> = room._state;
for (const store of state.values()) {
delete store[eventId];
}
};
});
it("should initialize and return the same `target` consistently", async () => {
const target1 = await client.ignoredInvites.getOrCreateTargetRoom();
const target2 = await client.ignoredInvites.getOrCreateTargetRoom();
expect(target1).toBeTruthy();
expect(target1).toBe(target2);
});
it("should initialize and return the same `sources` consistently", async () => {
const sources1 = await client.ignoredInvites.getOrCreateSourceRooms();
const sources2 = await client.ignoredInvites.getOrCreateSourceRooms();
expect(sources1).toBeTruthy();
expect(sources1).toHaveLength(1);
expect(sources1).toEqual(sources2);
});
it("should initially not reject any invite", async () => {
const rule = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: "!snafu:somewhere.org",
});
expect(rule).toBeFalsy();
});
it("should reject invites once we have added a matching rule in the target room (scope: user)", async () => {
await client.ignoredInvites.addRule(PolicyScope.User, "*:example.org", "just a test");
// We should reject this invite.
const ruleMatch = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleMatch).toBeTruthy();
expect(ruleMatch.getContent()).toMatchObject({
recommendation: "m.ban",
reason: "just a test",
});
// We should let these invites go through.
const ruleWrongServer = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:somewhere.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleWrongServer).toBeFalsy();
const ruleWrongServerRoom = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:somewhere.org",
roomId: "!snafu:example.org",
});
expect(ruleWrongServerRoom).toBeFalsy();
});
it("should reject invites once we have added a matching rule in the target room (scope: server)", async () => {
const REASON = `Just a test ${Math.random()}`;
await client.ignoredInvites.addRule(PolicyScope.Server, "example.org", REASON);
// We should reject these invites.
const ruleSenderMatch = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleSenderMatch).toBeTruthy();
expect(ruleSenderMatch.getContent()).toMatchObject({
recommendation: "m.ban",
reason: REASON,
});
const ruleRoomMatch = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:somewhere.org",
roomId: "!snafu:example.org",
});
expect(ruleRoomMatch).toBeTruthy();
expect(ruleRoomMatch.getContent()).toMatchObject({
recommendation: "m.ban",
reason: REASON,
});
// We should let these invites go through.
const ruleWrongServer = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:somewhere.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleWrongServer).toBeFalsy();
});
it("should reject invites once we have added a matching rule in the target room (scope: room)", async () => {
const REASON = `Just a test ${Math.random()}`;
const BAD_ROOM_ID = "!bad:example.org";
const GOOD_ROOM_ID = "!good:example.org";
await client.ignoredInvites.addRule(PolicyScope.Room, BAD_ROOM_ID, REASON);
// We should reject this invite.
const ruleSenderMatch = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: BAD_ROOM_ID,
});
expect(ruleSenderMatch).toBeTruthy();
expect(ruleSenderMatch.getContent()).toMatchObject({
recommendation: "m.ban",
reason: REASON,
});
// We should let these invites go through.
const ruleWrongRoom = await client.ignoredInvites.getRuleForInvite({
sender: BAD_ROOM_ID,
roomId: GOOD_ROOM_ID,
});
expect(ruleWrongRoom).toBeFalsy();
});
it("should reject invites once we have added a matching rule in a non-target source room", async () => {
const NEW_SOURCE_ROOM_ID = "!another-source:example.org";
// Make sure that everything is initialized.
await client.ignoredInvites.getOrCreateSourceRooms();
await client.joinRoom(NEW_SOURCE_ROOM_ID);
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, {
entity: "*:example.org",
reason: "just a test",
recommendation: "m.ban",
});
// We should reject this invite.
const ruleMatch = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleMatch).toBeTruthy();
expect(ruleMatch.getContent()).toMatchObject({
recommendation: "m.ban",
reason: "just a test",
});
// We should let these invites go through.
const ruleWrongServer = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:somewhere.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleWrongServer).toBeFalsy();
const ruleWrongServerRoom = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:somewhere.org",
roomId: "!snafu:example.org",
});
expect(ruleWrongServerRoom).toBeFalsy();
});
it("should not reject invites anymore once we have removed a rule", async () => {
await client.ignoredInvites.addRule(PolicyScope.User, "*:example.org", "just a test");
// We should reject this invite.
const ruleMatch = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleMatch).toBeTruthy();
expect(ruleMatch.getContent()).toMatchObject({
recommendation: "m.ban",
reason: "just a test",
});
// After removing the invite, we shouldn't reject it anymore.
await client.ignoredInvites.removeRule(ruleMatch);
const ruleMatch2 = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleMatch2).toBeFalsy();
});
it("should add new rules in the target room, rather than any other source room", async () => {
const NEW_SOURCE_ROOM_ID = "!another-source:example.org";
// Make sure that everything is initialized.
await client.ignoredInvites.getOrCreateSourceRooms();
await client.joinRoom(NEW_SOURCE_ROOM_ID);
const newSourceRoom = client.getRoom(NEW_SOURCE_ROOM_ID);
// Fetch the list of sources and check that we do not have the new room yet.
const policies = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name).getContent();
expect(policies).toBeTruthy();
const ignoreInvites = policies[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name];
expect(ignoreInvites).toBeTruthy();
expect(ignoreInvites.sources).toBeTruthy();
expect(ignoreInvites.sources).not.toContain(NEW_SOURCE_ROOM_ID);
// Add a source.
const added = await client.ignoredInvites.addSource(NEW_SOURCE_ROOM_ID);
expect(added).toBe(true);
const added2 = await client.ignoredInvites.addSource(NEW_SOURCE_ROOM_ID);
expect(added2).toBe(false);
// Fetch the list of sources and check that we have added the new room.
const policies2 = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name).getContent();
expect(policies2).toBeTruthy();
const ignoreInvites2 = policies2[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name];
expect(ignoreInvites2).toBeTruthy();
expect(ignoreInvites2.sources).toBeTruthy();
expect(ignoreInvites2.sources).toContain(NEW_SOURCE_ROOM_ID);
// Add a rule.
const eventId = await client.ignoredInvites.addRule(PolicyScope.User, "*:example.org", "just a test");
// Check where it shows up.
const targetRoomId = ignoreInvites2.target;
const targetRoom = client.getRoom(targetRoomId);
expect(targetRoom._state.get(PolicyScope.User)[eventId]).toBeTruthy();
expect(newSourceRoom._state.get(PolicyScope.User)?.[eventId]).toBeFalsy();
});
});
});
+19
View File
@@ -15,6 +15,7 @@ limitations under the License.
*/
import { MatrixEvent } from "../../../src";
import { M_BEACON_INFO } from "../../../src/@types/beacon";
import {
isTimestampInDuration,
Beacon,
@@ -129,6 +130,24 @@ describe('Beacon', () => {
expect(beacon.beaconInfo).toBeTruthy();
});
it('creates beacon without error from a malformed event', () => {
const event = new MatrixEvent({
type: M_BEACON_INFO.name,
room_id: roomId,
state_key: userId,
content: {},
});
const beacon = new Beacon(event);
expect(beacon.beaconInfoId).toEqual(event.getId());
expect(beacon.roomId).toEqual(roomId);
expect(beacon.isLive).toEqual(false);
expect(beacon.beaconInfoOwner).toEqual(userId);
expect(beacon.beaconInfoEventType).toEqual(liveBeaconEvent.getType());
expect(beacon.identifier).toEqual(`${roomId}_${userId}`);
expect(beacon.beaconInfo).toBeTruthy();
});
describe('isLive()', () => {
it('returns false when beacon is explicitly set to not live', () => {
const beacon = new Beacon(notLiveBeaconEvent);
@@ -1,12 +1,29 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as utils from "../test-utils/test-utils";
import { RoomMember } from "../../src/models/room-member";
import { RoomMember, RoomMemberEvent } from "../../src/models/room-member";
import { RoomState } from "../../src";
describe("RoomMember", function() {
const roomId = "!foo:bar";
const userA = "@alice:bar";
const userB = "@bertha:bar";
const userC = "@clarissa:bar";
let member;
let member = new RoomMember(roomId, userA);
beforeEach(function() {
member = new RoomMember(roomId, userA);
@@ -27,17 +44,17 @@ describe("RoomMember", function() {
avatar_url: "mxc://flibble/wibble",
},
});
const url = member.getAvatarUrl(hsUrl);
const url = member.getAvatarUrl(hsUrl, 1, 1, '', false, false);
// we don't care about how the mxc->http conversion is done, other
// than it contains the mxc body.
expect(url.indexOf("flibble/wibble")).not.toEqual(-1);
expect(url?.indexOf("flibble/wibble")).not.toEqual(-1);
});
it("should return nothing if there is no m.room.member and allowDefault=false",
function() {
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false);
expect(url).toEqual(null);
});
function() {
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false, false);
expect(url).toEqual(null);
});
});
describe("setPowerLevelEvent", function() {
@@ -66,92 +83,92 @@ describe("RoomMember", function() {
});
it("should emit 'RoomMember.powerLevel' if the power level changes.",
function() {
const event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
content: {
users_default: 20,
users: {
"@bertha:bar": 200,
"@invalid:user": 10, // shouldn't barf on this.
function() {
const event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
content: {
users_default: 20,
users: {
"@bertha:bar": 200,
"@invalid:user": 10, // shouldn't barf on this.
},
},
},
event: true,
});
let emitCount = 0;
event: true,
});
let emitCount = 0;
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) {
emitCount += 1;
expect(emitMember).toEqual(member);
expect(emitEvent).toEqual(event);
});
member.on(RoomMemberEvent.PowerLevel, function(emitEvent, emitMember) {
emitCount += 1;
expect(emitMember).toEqual(member);
expect(emitEvent).toEqual(event);
});
member.setPowerLevelEvent(event);
expect(emitCount).toEqual(1);
member.setPowerLevelEvent(event); // no-op
expect(emitCount).toEqual(1);
});
member.setPowerLevelEvent(event);
expect(emitCount).toEqual(1);
member.setPowerLevelEvent(event); // no-op
expect(emitCount).toEqual(1);
});
it("should honour power levels of zero.",
function() {
const event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
content: {
users_default: 20,
users: {
"@alice:bar": 0,
function() {
const event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
content: {
users_default: 20,
users: {
"@alice:bar": 0,
},
},
},
event: true,
});
let emitCount = 0;
event: true,
});
let emitCount = 0;
// set the power level to something other than zero or we
// won't get an event
member.powerLevel = 1;
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) {
emitCount += 1;
expect(emitMember.userId).toEqual('@alice:bar');
expect(emitMember.powerLevel).toEqual(0);
expect(emitEvent).toEqual(event);
});
// set the power level to something other than zero or we
// won't get an event
member.powerLevel = 1;
member.on(RoomMemberEvent.PowerLevel, function(emitEvent, emitMember) {
emitCount += 1;
expect(emitMember.userId).toEqual('@alice:bar');
expect(emitMember.powerLevel).toEqual(0);
expect(emitEvent).toEqual(event);
});
member.setPowerLevelEvent(event);
expect(member.powerLevel).toEqual(0);
expect(emitCount).toEqual(1);
});
member.setPowerLevelEvent(event);
expect(member.powerLevel).toEqual(0);
expect(emitCount).toEqual(1);
});
it("should not honor string power levels.",
function() {
const event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
content: {
users_default: 20,
users: {
"@alice:bar": "5",
function() {
const event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
content: {
users_default: 20,
users: {
"@alice:bar": "5",
},
},
},
event: true,
});
let emitCount = 0;
event: true,
});
let emitCount = 0;
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) {
emitCount += 1;
expect(emitMember.userId).toEqual('@alice:bar');
expect(emitMember.powerLevel).toEqual(20);
expect(emitEvent).toEqual(event);
});
member.on(RoomMemberEvent.PowerLevel, function(emitEvent, emitMember) {
emitCount += 1;
expect(emitMember.userId).toEqual('@alice:bar');
expect(emitMember.powerLevel).toEqual(20);
expect(emitEvent).toEqual(event);
});
member.setPowerLevelEvent(event);
expect(member.powerLevel).toEqual(20);
expect(emitCount).toEqual(1);
});
member.setPowerLevelEvent(event);
expect(member.powerLevel).toEqual(20);
expect(emitCount).toEqual(1);
});
});
describe("setTypingEvent", function() {
@@ -183,34 +200,34 @@ describe("RoomMember", function() {
});
it("should emit 'RoomMember.typing' if the typing state changes",
function() {
const event = utils.mkEvent({
type: "m.typing",
room: roomId,
content: {
user_ids: [
userA, userC,
],
},
event: true,
function() {
const event = utils.mkEvent({
type: "m.typing",
room: roomId,
content: {
user_ids: [
userA, userC,
],
},
event: true,
});
let emitCount = 0;
member.on(RoomMemberEvent.Typing, function(ev, mem) {
expect(mem).toEqual(member);
expect(ev).toEqual(event);
emitCount += 1;
});
member.typing = false;
member.setTypingEvent(event);
expect(emitCount).toEqual(1);
member.setTypingEvent(event); // no-op
expect(emitCount).toEqual(1);
});
let emitCount = 0;
member.on("RoomMember.typing", function(ev, mem) {
expect(mem).toEqual(member);
expect(ev).toEqual(event);
emitCount += 1;
});
member.typing = false;
member.setTypingEvent(event);
expect(emitCount).toEqual(1);
member.setTypingEvent(event); // no-op
expect(emitCount).toEqual(1);
});
});
describe("isOutOfBand", function() {
it("should be set by markOutOfBand", function() {
const member = new RoomMember();
const member = new RoomMember(roomId, userA);
expect(member.isOutOfBand()).toEqual(false);
member.markOutOfBand();
expect(member.isOutOfBand()).toEqual(true);
@@ -235,50 +252,50 @@ 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.events.member).toEqual(inviteEvent);
member.setMembershipEvent(joinEvent);
expect(member.membership).toEqual("join");
expect(member.events.member).toEqual(joinEvent);
});
function() {
member.setMembershipEvent(inviteEvent);
expect(member.membership).toEqual("invite");
expect(member.events.member).toEqual(inviteEvent);
member.setMembershipEvent(joinEvent);
expect(member.membership).toEqual("join");
expect(member.events.member).toEqual(joinEvent);
});
it("should set 'name' based on user_id, displayname and room state",
function() {
const roomState = {
getStateEvents: function(type) {
if (type !== "m.room.member") {
return [];
}
return [
utils.mkMembership({
event: true, mship: "join", room: roomId,
user: userB,
}),
utils.mkMembership({
event: true, mship: "join", room: roomId,
user: userC, name: "Alice",
}),
joinEvent,
];
},
getUserIdsWithDisplayName: function(displayName) {
return [userA, userC];
},
};
expect(member.name).toEqual(userA); // default = user_id
member.setMembershipEvent(joinEvent);
expect(member.name).toEqual("Alice"); // prefer displayname
member.setMembershipEvent(joinEvent, roomState);
expect(member.name).not.toEqual("Alice"); // it should disambig.
// user_id should be there somewhere
expect(member.name.indexOf(userA)).not.toEqual(-1);
});
function() {
const roomState = {
getStateEvents: function(type) {
if (type !== "m.room.member") {
return [];
}
return [
utils.mkMembership({
event: true, mship: "join", room: roomId,
user: userB,
}),
utils.mkMembership({
event: true, mship: "join", room: roomId,
user: userC, name: "Alice",
}),
joinEvent,
];
},
getUserIdsWithDisplayName: function(displayName) {
return [userA, userC];
},
} as unknown as RoomState;
expect(member.name).toEqual(userA); // default = user_id
member.setMembershipEvent(joinEvent);
expect(member.name).toEqual("Alice"); // prefer displayname
member.setMembershipEvent(joinEvent, roomState);
expect(member.name).not.toEqual("Alice"); // it should disambig.
// user_id should be there somewhere
expect(member.name.indexOf(userA)).not.toEqual(-1);
});
it("should emit 'RoomMember.membership' if the membership changes", function() {
let emitCount = 0;
member.on("RoomMember.membership", function(ev, mem) {
member.on(RoomMemberEvent.Membership, function(ev, mem) {
emitCount += 1;
expect(mem).toEqual(member);
expect(ev).toEqual(inviteEvent);
@@ -291,7 +308,7 @@ describe("RoomMember", function() {
it("should emit 'RoomMember.name' if the name changes", function() {
let emitCount = 0;
member.on("RoomMember.name", function(ev, mem) {
member.on(RoomMemberEvent.Name, function(ev, mem) {
emitCount += 1;
expect(mem).toEqual(member);
expect(ev).toEqual(joinEvent);
@@ -341,7 +358,7 @@ describe("RoomMember", function() {
getUserIdsWithDisplayName: function(displayName) {
return [userA, userC];
},
};
} as unknown as RoomState;
expect(member.name).toEqual(userA); // default = user_id
member.setMembershipEvent(joinEvent, roomState);
expect(member.name).not.toEqual("Alíce"); // it should disambig.
@@ -1,14 +1,37 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { MockedObject } from 'jest-mock';
import * as utils from "../test-utils/test-utils";
import { makeBeaconEvent, makeBeaconInfoEvent } from "../test-utils/beacon";
import { filterEmitCallsByEventType } from "../test-utils/emitter";
import { RoomState, RoomStateEvent } from "../../src/models/room-state";
import { BeaconEvent, getBeaconInfoIdentifier } from "../../src/models/beacon";
import {
Beacon,
BeaconEvent,
getBeaconInfoIdentifier,
} from "../../src/models/beacon";
import { EventType, RelationType, UNSTABLE_MSC2716_MARKER } from "../../src/@types/event";
import {
MatrixEvent,
MatrixEventEvent,
} from "../../src/models/event";
import { M_BEACON } from "../../src/@types/beacon";
import { MatrixClient } from "../../src/client";
describe("RoomState", function() {
const roomId = "!foo:bar";
@@ -17,7 +40,7 @@ describe("RoomState", function() {
const userC = "@cleo:bar";
const userLazy = "@lazy:bar";
let state;
let state = new RoomState(roomId);
beforeEach(function() {
state = new RoomState(roomId);
@@ -67,8 +90,8 @@ 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.name).toEqual(userB);
expect(member?.membership).toEqual("join");
expect(member?.name).toEqual(userB);
state.setStateEvents([
utils.mkMembership({
@@ -77,40 +100,40 @@ describe("RoomState", function() {
}),
]);
expect(member.membership).toEqual("leave");
expect(member.name).toEqual("BobGone");
expect(member?.membership).toEqual("leave");
expect(member?.name).toEqual("BobGone");
});
});
describe("getSentinelMember", function() {
it("should return a member with the user id as name", function() {
expect(state.getSentinelMember("@no-one:here").name).toEqual("@no-one:here");
expect(state.getSentinelMember("@no-one:here")?.name).toEqual("@no-one:here");
});
it("should return a member which doesn't change when the state is updated",
function() {
const preLeaveUser = state.getSentinelMember(userA);
state.setStateEvents([
utils.mkMembership({
room: roomId, user: userA, mship: "leave", event: true,
name: "AliceIsGone",
}),
]);
const postLeaveUser = state.getSentinelMember(userA);
function() {
const preLeaveUser = state.getSentinelMember(userA);
state.setStateEvents([
utils.mkMembership({
room: roomId, user: userA, mship: "leave", event: true,
name: "AliceIsGone",
}),
]);
const postLeaveUser = state.getSentinelMember(userA);
expect(preLeaveUser.membership).toEqual("join");
expect(preLeaveUser.name).toEqual(userA);
expect(preLeaveUser?.membership).toEqual("join");
expect(preLeaveUser?.name).toEqual(userA);
expect(postLeaveUser.membership).toEqual("leave");
expect(postLeaveUser.name).toEqual("AliceIsGone");
});
expect(postLeaveUser?.membership).toEqual("leave");
expect(postLeaveUser?.name).toEqual("AliceIsGone");
});
});
describe("getStateEvents", function() {
it("should return null if a state_key was specified and there was no match",
function() {
expect(state.getStateEvents("foo.bar.baz", "keyname")).toEqual(null);
});
function() {
expect(state.getStateEvents("foo.bar.baz", "keyname")).toEqual(null);
});
it("should return an empty list if a state_key was not specified and there" +
" was no match", function() {
@@ -118,21 +141,21 @@ describe("RoomState", function() {
});
it("should return a list of matching events if no state_key was specified",
function() {
const events = state.getStateEvents("m.room.member");
expect(events.length).toEqual(2);
// ordering unimportant
expect([userA, userB].indexOf(events[0].getStateKey())).not.toEqual(-1);
expect([userA, userB].indexOf(events[1].getStateKey())).not.toEqual(-1);
});
function() {
const events = state.getStateEvents("m.room.member");
expect(events.length).toEqual(2);
// ordering unimportant
expect([userA, userB].indexOf(events[0].getStateKey() as string)).not.toEqual(-1);
expect([userA, userB].indexOf(events[1].getStateKey() as string)).not.toEqual(-1);
});
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",
function() {
const event = state.getStateEvents("m.room.member", userA);
expect(event.getContent()).toMatchObject({
membership: "join",
});
});
});
});
describe("setStateEvents", function() {
@@ -146,7 +169,7 @@ describe("RoomState", function() {
}),
];
let emitCount = 0;
state.on("RoomState.members", function(ev, st, mem) {
state.on(RoomStateEvent.Members, function(ev, st, mem) {
expect(ev).toEqual(memberEvents[emitCount]);
expect(st).toEqual(state);
expect(mem).toEqual(state.getMember(ev.getSender()));
@@ -166,7 +189,7 @@ describe("RoomState", function() {
}),
];
let emitCount = 0;
state.on("RoomState.newMember", function(ev, st, mem) {
state.on(RoomStateEvent.NewMember, function(ev, st, mem) {
expect(state.getMember(mem.userId)).toEqual(mem);
expect(mem.userId).toEqual(memberEvents[emitCount].getSender());
expect(mem.membership).toBeFalsy(); // not defined yet
@@ -192,7 +215,7 @@ describe("RoomState", function() {
}),
];
let emitCount = 0;
state.on("RoomState.events", function(ev, st) {
state.on(RoomStateEvent.Events, function(ev, st) {
expect(ev).toEqual(events[emitCount]);
expect(st).toEqual(state);
emitCount += 1;
@@ -272,7 +295,7 @@ describe("RoomState", function() {
}),
];
let emitCount = 0;
state.on("RoomState.Marker", function(markerEvent, markerFoundOptions) {
state.on(RoomStateEvent.Marker, function(markerEvent, markerFoundOptions) {
expect(markerEvent).toEqual(events[emitCount]);
expect(markerFoundOptions).toEqual({ timelineWasEmpty: true });
emitCount += 1;
@@ -296,7 +319,7 @@ describe("RoomState", function() {
it('does not add redacted beacon info events to state', () => {
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId);
const redactionEvent = { event: { type: 'm.room.redaction' } };
const redactionEvent = new MatrixEvent({ type: 'm.room.redaction' });
redactedBeaconEvent.makeRedacted(redactionEvent);
const emitSpy = jest.spyOn(state, 'emit');
@@ -316,27 +339,27 @@ describe("RoomState", function() {
state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
expect(beaconInstance.isLive).toEqual(true);
expect(beaconInstance?.isLive).toEqual(true);
state.setStateEvents([updatedBeaconEvent]);
// same Beacon
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(beaconInstance);
// updated liveness
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent)).isLive).toEqual(false);
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))?.isLive).toEqual(false);
});
it('destroys and removes redacted beacon events', () => {
const beaconId = '$beacon1';
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
const redactionEvent = { event: { type: 'm.room.redaction', redacts: beaconEvent.getId() } };
const redactionEvent = new MatrixEvent({ type: 'm.room.redaction', redacts: beaconEvent.getId() });
redactedBeaconEvent.makeRedacted(redactionEvent);
state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
const destroySpy = jest.spyOn(beaconInstance, 'destroy');
expect(beaconInstance.isLive).toEqual(true);
const destroySpy = jest.spyOn(beaconInstance as Beacon, 'destroy');
expect(beaconInstance?.isLive).toEqual(true);
state.setStateEvents([redactedBeaconEvent]);
@@ -357,7 +380,7 @@ describe("RoomState", function() {
// live beacon is now not live
const updatedLiveBeaconEvent = makeBeaconInfoEvent(
userA, roomId, { isLive: false }, liveBeaconEvent.getId(), '$beacon1',
userA, roomId, { isLive: false }, liveBeaconEvent.getId(),
);
state.setStateEvents([updatedLiveBeaconEvent]);
@@ -377,8 +400,8 @@ describe("RoomState", function() {
state.markOutOfBandMembersStarted();
state.setOutOfBandMembers([oobMemberEvent]);
const member = state.getMember(userLazy);
expect(member.userId).toEqual(userLazy);
expect(member.isOutOfBand()).toEqual(true);
expect(member?.userId).toEqual(userLazy);
expect(member?.isOutOfBand()).toEqual(true);
});
it("should have no effect when not in correct status", function() {
@@ -394,7 +417,7 @@ describe("RoomState", function() {
user: userLazy, mship: "join", room: roomId, event: true,
});
let eventReceived = false;
state.once('RoomState.newMember', (_, __, member) => {
state.once(RoomStateEvent.NewMember, (_event, _state, member) => {
expect(member.userId).toEqual(userLazy);
eventReceived = true;
});
@@ -410,8 +433,8 @@ describe("RoomState", function() {
state.markOutOfBandMembersStarted();
state.setOutOfBandMembers([oobMemberEvent]);
const memberA = state.getMember(userA);
expect(memberA.events.member.getId()).not.toEqual(oobMemberEvent.getId());
expect(memberA.isOutOfBand()).toEqual(false);
expect(memberA?.events?.member?.getId()).not.toEqual(oobMemberEvent.getId());
expect(memberA?.isOutOfBand()).toEqual(false);
});
it("should emit members when updating a member", function() {
@@ -420,7 +443,7 @@ describe("RoomState", function() {
user: doesntExistYetUserId, mship: "join", room: roomId, event: true,
});
let eventReceived = false;
state.once('RoomState.members', (_, __, member) => {
state.once(RoomStateEvent.Members, (_event, _state, member) => {
expect(member.userId).toEqual(doesntExistYetUserId);
eventReceived = true;
});
@@ -443,8 +466,8 @@ describe("RoomState", function() {
[userA, userB, userLazy].forEach((userId) => {
const member = state.getMember(userId);
const memberCopy = copy.getMember(userId);
expect(member.name).toEqual(memberCopy.name);
expect(member.isOutOfBand()).toEqual(memberCopy.isOutOfBand());
expect(member?.name).toEqual(memberCopy?.name);
expect(member?.isOutOfBand()).toEqual(memberCopy?.isOutOfBand());
});
// check member keys
expect(Object.keys(state.members)).toEqual(Object.keys(copy.members));
@@ -496,78 +519,80 @@ describe("RoomState", function() {
describe("maySendStateEvent", function() {
it("should say any member may send state with no power level event",
function() {
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
});
function() {
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
});
it("should say members with power >=50 may send state with power level event " +
"but no state default",
function() {
const powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
const powerLevelEvent = new MatrixEvent({
type: "m.room.power_levels", room_id: roomId, sender: userA,
state_key: "",
content: {
users_default: 10,
// state_default: 50, "intentionally left blank"
events_default: 25,
users: {
[userA]: 50,
},
},
};
powerLevelEvent.content.users[userA] = 50;
});
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
state.setStateEvents([powerLevelEvent]);
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(false);
});
it("should obey state_default",
function() {
const powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
users_default: 10,
state_default: 30,
events_default: 25,
users: {
function() {
const powerLevelEvent = new MatrixEvent({
type: "m.room.power_levels", room_id: roomId, sender: userA,
state_key: "",
content: {
users_default: 10,
state_default: 30,
events_default: 25,
users: {
[userA]: 30,
[userB]: 29,
},
},
},
};
powerLevelEvent.content.users[userA] = 30;
powerLevelEvent.content.users[userB] = 29;
});
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
state.setStateEvents([powerLevelEvent]);
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(false);
});
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(false);
});
it("should honour explicit event power levels in the power_levels event",
function() {
const powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
events: {
"m.room.other_thing": 76,
function() {
const powerLevelEvent = new MatrixEvent({
type: "m.room.power_levels", room_id: roomId, sender: userA,
state_key: "", content: {
events: {
"m.room.other_thing": 76,
},
users_default: 10,
state_default: 50,
events_default: 25,
users: {
[userA]: 80,
[userB]: 50,
},
},
users_default: 10,
state_default: 50,
events_default: 25,
users: {
},
},
};
powerLevelEvent.content.users[userA] = 80;
powerLevelEvent.content.users[userB] = 50;
});
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
state.setStateEvents([powerLevelEvent]);
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(true);
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(true);
expect(state.maySendStateEvent('m.room.other_thing', userA)).toEqual(true);
expect(state.maySendStateEvent('m.room.other_thing', userB)).toEqual(false);
});
expect(state.maySendStateEvent('m.room.other_thing', userA)).toEqual(true);
expect(state.maySendStateEvent('m.room.other_thing', userB)).toEqual(false);
});
});
describe("getJoinedMemberCount", function() {
@@ -682,71 +707,73 @@ describe("RoomState", function() {
describe("maySendEvent", function() {
it("should say any member may send events with no power level event",
function() {
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
expect(state.maySendMessage(userA)).toEqual(true);
});
function() {
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
expect(state.maySendMessage(userA)).toEqual(true);
});
it("should obey events_default",
function() {
const powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
users_default: 10,
state_default: 30,
events_default: 25,
users: {
function() {
const powerLevelEvent = new MatrixEvent({
type: "m.room.power_levels", room_id: roomId, sender: userA,
state_key: "",
content: {
users_default: 10,
state_default: 30,
events_default: 25,
users: {
[userA]: 26,
[userB]: 24,
},
},
},
};
powerLevelEvent.content.users[userA] = 26;
powerLevelEvent.content.users[userB] = 24;
});
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
state.setStateEvents([powerLevelEvent]);
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
expect(state.maySendEvent('m.room.message', userB)).toEqual(false);
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
expect(state.maySendEvent('m.room.message', userB)).toEqual(false);
expect(state.maySendMessage(userA)).toEqual(true);
expect(state.maySendMessage(userB)).toEqual(false);
});
expect(state.maySendMessage(userA)).toEqual(true);
expect(state.maySendMessage(userB)).toEqual(false);
});
it("should honour explicit event power levels in the power_levels event",
function() {
const powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
events: {
"m.room.other_thing": 33,
function() {
const powerLevelEvent = new MatrixEvent({
type: "m.room.power_levels", room_id: roomId, sender: userA,
state_key: "",
content: {
events: {
"m.room.other_thing": 33,
},
users_default: 10,
state_default: 50,
events_default: 25,
users: {
[userA]: 40,
[userB]: 30,
},
},
users_default: 10,
state_default: 50,
events_default: 25,
users: {
},
},
};
powerLevelEvent.content.users[userA] = 40;
powerLevelEvent.content.users[userB] = 30;
});
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
state.setStateEvents([powerLevelEvent]);
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
expect(state.maySendEvent('m.room.message', userB)).toEqual(true);
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
expect(state.maySendEvent('m.room.message', userB)).toEqual(true);
expect(state.maySendMessage(userA)).toEqual(true);
expect(state.maySendMessage(userB)).toEqual(true);
expect(state.maySendMessage(userA)).toEqual(true);
expect(state.maySendMessage(userB)).toEqual(true);
expect(state.maySendEvent('m.room.other_thing', userA)).toEqual(true);
expect(state.maySendEvent('m.room.other_thing', userB)).toEqual(false);
});
expect(state.maySendEvent('m.room.other_thing', userA)).toEqual(true);
expect(state.maySendEvent('m.room.other_thing', userB)).toEqual(false);
});
});
describe('processBeaconEvents', () => {
const beacon1 = makeBeaconInfoEvent(userA, roomId, {}, '$beacon1', '$beacon1');
const beacon2 = makeBeaconInfoEvent(userB, roomId, {}, '$beacon2', '$beacon2');
const beacon1 = makeBeaconInfoEvent(userA, roomId, {}, '$beacon1');
const beacon2 = makeBeaconInfoEvent(userB, roomId, {}, '$beacon2');
const mockClient = { decryptEventIfNeeded: jest.fn() };
const mockClient = { decryptEventIfNeeded: jest.fn() } as unknown as MockedObject<MatrixClient>;
beforeEach(() => {
mockClient.decryptEventIfNeeded.mockClear();
@@ -816,11 +843,11 @@ describe("RoomState", function() {
beaconInfoId: 'some-other-beacon',
});
state.setStateEvents([beacon1, beacon2], mockClient);
state.setStateEvents([beacon1, beacon2]);
expect(state.beacons.size).toEqual(2);
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beacon1));
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
const addLocationsSpy = jest.spyOn(beaconInstance, 'addLocations');
state.processBeaconEvents([location1, location2, location3], mockClient);
@@ -885,7 +912,7 @@ describe("RoomState", function() {
});
state.setStateEvents([beacon1, beacon2]);
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
state.processBeaconEvents([location, otherRelatedEvent], mockClient);
expect(addLocationsSpy).not.toHaveBeenCalled();
@@ -945,13 +972,13 @@ describe("RoomState", function() {
});
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
state.setStateEvents([beacon1, beacon2]);
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
// this event is a message after decryption
decryptingRelatedEvent.type = EventType.RoomMessage;
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted);
decryptingRelatedEvent.event.type = EventType.RoomMessage;
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted, decryptingRelatedEvent);
expect(addLocationsSpy).not.toHaveBeenCalled();
});
@@ -967,14 +994,14 @@ describe("RoomState", function() {
});
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
state.setStateEvents([beacon1, beacon2]);
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
// update type after '''decryption'''
decryptingRelatedEvent.event.type = M_BEACON.name;
decryptingRelatedEvent.event.content = locationEvent.content;
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted);
decryptingRelatedEvent.event.content = locationEvent.event.content;
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted, decryptingRelatedEvent);
expect(addLocationsSpy).toHaveBeenCalledWith([decryptingRelatedEvent]);
});
+36 -33
View File
@@ -288,11 +288,11 @@ describe("Room", function() {
room.addLiveEvents(events);
expect(room.currentState.setStateEvents).toHaveBeenCalledWith(
[events[0]],
{ timelineWasEmpty: undefined },
{ timelineWasEmpty: false },
);
expect(room.currentState.setStateEvents).toHaveBeenCalledWith(
[events[1]],
{ timelineWasEmpty: undefined },
{ timelineWasEmpty: false },
);
expect(events[0].forwardLooking).toBe(true);
expect(events[1].forwardLooking).toBe(true);
@@ -426,6 +426,17 @@ describe("Room", function() {
// but without the event ID matching we will still have the local event in pending events
expect(room.getEventForTxnId(txnId)).toBeUndefined();
});
it("should correctly handle remote echoes from other devices", () => {
const remoteEvent = utils.mkMessage({
room: roomId, user: userA, event: true,
});
remoteEvent.event.unsigned = { transaction_id: "TXN_ID" };
// add the remoteEvent
room.addLiveEvents([remoteEvent]);
expect(room.timeline.length).toEqual(1);
});
});
describe('addEphemeralEvents', () => {
@@ -2441,15 +2452,13 @@ describe("Room", function() {
if (receiptType === ReceiptType.ReadPrivate) {
return { eventId: "eventId1" } as IWrappedReceipt;
}
if (receiptType === ReceiptType.UnstableReadPrivate) {
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId2" } as IWrappedReceipt;
}
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId3" } as IWrappedReceipt;
}
return null;
};
for (let i = 1; i <= 3; i++) {
for (let i = 1; i <= 2; i++) {
room.getUnfilteredTimelineSet = () => ({ compareEventOrdering: (event1, event2) => {
return (event1 === `eventId${i}`) ? 1 : -1;
} } as EventTimelineSet);
@@ -2460,20 +2469,18 @@ describe("Room", function() {
describe("correctly compares by timestamp", () => {
it("should correctly compare, if we have all receipts", () => {
for (let i = 1; i <= 3; i++) {
for (let i = 1; i <= 2; i++) {
room.getUnfilteredTimelineSet = () => ({
compareEventOrdering: (_1, _2) => null,
} as EventTimelineSet);
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
if (receiptType === ReceiptType.ReadPrivate) {
return { eventId: "eventId1", data: { ts: i === 1 ? 1 : 0 } } as IWrappedReceipt;
}
if (receiptType === ReceiptType.UnstableReadPrivate) {
return { eventId: "eventId2", data: { ts: i === 2 ? 1 : 0 } } as IWrappedReceipt;
return { eventId: "eventId1", data: { ts: i === 1 ? 2 : 1 } } as IWrappedReceipt;
}
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId3", data: { ts: i === 3 ? 1 : 0 } } as IWrappedReceipt;
return { eventId: "eventId2", data: { ts: i === 2 ? 2 : 1 } } as IWrappedReceipt;
}
return null;
};
expect(room.getEventReadUpTo(userA)).toEqual(`eventId${i}`);
@@ -2485,12 +2492,10 @@ describe("Room", function() {
compareEventOrdering: (_1, _2) => null,
} as EventTimelineSet);
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
if (receiptType === ReceiptType.UnstableReadPrivate) {
return { eventId: "eventId1", data: { ts: 0 } } as IWrappedReceipt;
}
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId2", data: { ts: 1 } } as IWrappedReceipt;
}
return null;
};
expect(room.getEventReadUpTo(userA)).toEqual(`eventId2`);
@@ -2509,35 +2514,21 @@ describe("Room", function() {
if (receiptType === ReceiptType.ReadPrivate) {
return { eventId: "eventId1" } as IWrappedReceipt;
}
if (receiptType === ReceiptType.UnstableReadPrivate) {
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId2" } as IWrappedReceipt;
}
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId3" } as IWrappedReceipt;
}
return null;
};
expect(room.getEventReadUpTo(userA)).toEqual(`eventId1`);
});
it("should give precedence to org.matrix.msc2285.read.private", () => {
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
if (receiptType === ReceiptType.UnstableReadPrivate) {
return { eventId: "eventId2" } as IWrappedReceipt;
}
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId2" } as IWrappedReceipt;
}
};
expect(room.getEventReadUpTo(userA)).toEqual(`eventId2`);
});
it("should give precedence to m.read", () => {
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId3" } as IWrappedReceipt;
}
return null;
};
expect(room.getEventReadUpTo(userA)).toEqual(`eventId3`);
@@ -2545,4 +2536,16 @@ describe("Room", function() {
});
});
});
describe("roomNameGenerator", () => {
const client = new TestClient(userA).client;
client.roomNameGenerator = jest.fn().mockReturnValue(null);
const room = new Room(roomId, client, userA);
it("should call fn when recalculating room name", () => {
(client.roomNameGenerator as jest.Mock).mockClear();
room.recalculate();
expect(client.roomNameGenerator).toHaveBeenCalled();
});
});
});
-6
View File
@@ -302,9 +302,6 @@ describe("SyncAccumulator", function() {
[ReceiptType.ReadPrivate]: {
"@dan:localhost": { ts: 4 },
},
[ReceiptType.UnstableReadPrivate]: {
"@matthew:localhost": { ts: 5 },
},
"some.other.receipt.type": {
"@should_be_ignored:localhost": { key: "val" },
},
@@ -350,9 +347,6 @@ describe("SyncAccumulator", function() {
[ReceiptType.ReadPrivate]: {
"@dan:localhost": { ts: 4 },
},
[ReceiptType.UnstableReadPrivate]: {
"@matthew:localhost": { ts: 5 },
},
},
"$event2:localhost": {
[ReceiptType.Read]: {
+3 -36
View File
@@ -152,6 +152,9 @@ describe("utils", function() {
assert.isTrue(utils.deepCompare({ a: 1, b: 2 }, { a: 1, b: 2 }));
assert.isTrue(utils.deepCompare({ a: 1, b: 2 }, { b: 2, a: 1 }));
assert.isFalse(utils.deepCompare({ a: 1, b: 2 }, { a: 1, b: 3 }));
assert.isFalse(utils.deepCompare({ a: 1, b: 2 }, { a: 1 }));
assert.isFalse(utils.deepCompare({ a: 1 }, { a: 1, b: 2 }));
assert.isFalse(utils.deepCompare({ a: 1 }, { b: 1 }));
assert.isTrue(utils.deepCompare({
1: { name: "mhc", age: 28 },
@@ -525,38 +528,6 @@ describe("utils", function() {
});
});
describe('getPrivateReadReceiptField', () => {
it('should return m.read.private if server supports stable', async () => {
expect(await utils.getPrivateReadReceiptField({
doesServerSupportUnstableFeature: jest.fn().mockImplementation((feature) => {
return feature === "org.matrix.msc2285.stable";
}),
} as any)).toBe(ReceiptType.ReadPrivate);
});
it('should return m.read.private if server supports stable and unstable', async () => {
expect(await utils.getPrivateReadReceiptField({
doesServerSupportUnstableFeature: jest.fn().mockImplementation((feature) => {
return ["org.matrix.msc2285.stable", "org.matrix.msc2285"].includes(feature);
}),
} as any)).toBe(ReceiptType.ReadPrivate);
});
it('should return org.matrix.msc2285.read.private if server supports unstable', async () => {
expect(await utils.getPrivateReadReceiptField({
doesServerSupportUnstableFeature: jest.fn().mockImplementation((feature) => {
return feature === "org.matrix.msc2285";
}),
} as any)).toBe(ReceiptType.UnstableReadPrivate);
});
it('should return none if server does not support either', async () => {
expect(await utils.getPrivateReadReceiptField({
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
} as any)).toBeFalsy();
});
});
describe('isSupportedReceiptType', () => {
it('should support m.read', () => {
expect(utils.isSupportedReceiptType(ReceiptType.Read)).toBeTruthy();
@@ -566,10 +537,6 @@ describe("utils", function() {
expect(utils.isSupportedReceiptType(ReceiptType.ReadPrivate)).toBeTruthy();
});
it('should support org.matrix.msc2285.read.private', () => {
expect(utils.isSupportedReceiptType(ReceiptType.UnstableReadPrivate)).toBeTruthy();
});
it('should not support other receipt types', () => {
expect(utils.isSupportedReceiptType("this is a receipt type")).toBeFalsy();
});
+63
View File
@@ -27,3 +27,66 @@ export interface IRefreshTokenResponse {
}
/* eslint-enable camelcase */
/**
* Response to GET login flows as per https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3login
*/
export interface ILoginFlowsResponse {
flows: LoginFlow[];
}
export type LoginFlow = ISSOFlow | IPasswordFlow | ILoginFlow;
export interface ILoginFlow {
type: string;
}
export interface IPasswordFlow extends ILoginFlow {
type: "m.login.password";
}
/**
* Representation of SSO flow as per https://spec.matrix.org/latest/client-server-api/#client-login-via-sso
*/
export interface ISSOFlow extends ILoginFlow {
type: "m.login.sso" | "m.login.cas";
// eslint-disable-next-line camelcase
identity_providers?: IIdentityProvider[];
}
export enum IdentityProviderBrand {
Gitlab = "gitlab",
Github = "github",
Apple = "apple",
Google = "google",
Facebook = "facebook",
Twitter = "twitter",
}
export interface IIdentityProvider {
id: string;
name: string;
icon?: string;
brand?: IdentityProviderBrand | string;
}
/**
* Parameters to login request as per https://spec.matrix.org/latest/client-server-api/#login
*/
/* eslint-disable camelcase */
export interface ILoginParams {
identifier?: object;
password?: string;
token?: string;
device_id?: string;
initial_device_display_name?: string;
}
/* eslint-enable camelcase */
export enum SSOAction {
/** The user intends to login to an existing account */
LOGIN = "login",
/** The user intends to register for a new account */
REGISTER = "register",
}
-4
View File
@@ -18,8 +18,4 @@ export enum ReceiptType {
Read = "m.read",
FullyRead = "m.fully_read",
ReadPrivate = "m.read.private",
/**
* @deprecated Please use the ReadPrivate type when possible. This value may be removed at any time without notice.
*/
UnstableReadPrivate = "org.matrix.msc2285.read.private",
}
+7
View File
@@ -41,6 +41,13 @@ export class NamespacedValue<S extends string, U extends string> {
return this.unstable;
}
public get names(): (U | S)[] {
const names = [this.name];
const altName = this.altName;
if (altName) names.push(altName);
return names;
}
public matches(val: string): boolean {
return this.name === val || this.altName === val;
}
+29
View File
@@ -24,7 +24,16 @@ import { ListenerMap, TypedEventEmitter } from "./models/typed-event-emitter";
export class ReEmitter {
constructor(private readonly target: EventEmitter) {}
// Map from emitter to event name to re-emitter
private reEmitters = new Map<EventEmitter, Map<string, (...args: any[]) => void>>();
public reEmit(source: EventEmitter, eventNames: string[]): void {
let reEmittersByEvent = this.reEmitters.get(source);
if (!reEmittersByEvent) {
reEmittersByEvent = new Map();
this.reEmitters.set(source, reEmittersByEvent);
}
for (const eventName of eventNames) {
// We include the source as the last argument for event handlers which may need it,
// such as read receipt listeners on the client class which won't have the context
@@ -44,8 +53,21 @@ export class ReEmitter {
this.target.emit(eventName, ...args, source);
};
source.on(eventName, forSource);
reEmittersByEvent.set(eventName, forSource);
}
}
public stopReEmitting(source: EventEmitter, eventNames: string[]): void {
const reEmittersByEvent = this.reEmitters.get(source);
if (!reEmittersByEvent) return; // We were never re-emitting these events in the first place
for (const eventName of eventNames) {
source.off(eventName, reEmittersByEvent.get(eventName));
reEmittersByEvent.delete(eventName);
}
if (reEmittersByEvent.size === 0) this.reEmitters.delete(source);
}
}
export class TypedReEmitter<
@@ -62,4 +84,11 @@ export class TypedReEmitter<
): void {
super.reEmit(source, eventNames);
}
public stopReEmitting<ReEmittedEvents extends string, T extends Events & ReEmittedEvents>(
source: TypedEventEmitter<ReEmittedEvents, any>,
eventNames: T[],
): void {
super.stopReEmitting(source, eventNames);
}
}
+28 -24
View File
@@ -17,6 +17,8 @@ limitations under the License.
/** @module auto-discovery */
import { ServerResponse } from "http";
import { IClientWellKnown, IWellKnownConfig } from "./client";
import { logger } from './logger';
@@ -409,39 +411,41 @@ export class AutoDiscovery {
* @return {Promise<object>} Resolves to the returned state.
* @private
*/
private static fetchWellKnownObject(url: string): Promise<IWellKnownConfig> {
return new Promise(function(resolve) {
private static fetchWellKnownObject(uri: string): Promise<IWellKnownConfig> {
return new Promise((resolve) => {
// eslint-disable-next-line
const request = require("./matrix").getRequest();
if (!request) throw new Error("No request library available");
request(
{ method: "GET", uri: url, timeout: 5000 },
(err, response, body) => {
if (err || response &&
(response.statusCode < 200 || response.statusCode >= 300)
) {
let action = AutoDiscoveryAction.FAIL_PROMPT;
let reason = (err ? err.message : null) || "General failure";
if (response && response.statusCode === 404) {
action = AutoDiscoveryAction.IGNORE;
reason = AutoDiscovery.ERROR_MISSING_WELLKNOWN;
}
resolve({ raw: {}, action: action, reason: reason, error: err });
return;
{ method: "GET", uri, timeout: 5000 },
(error: Error, response: ServerResponse, body: string) => {
if (error || response?.statusCode < 200 || response?.statusCode >= 300) {
const result = { error, raw: {} };
return resolve(response?.statusCode === 404
? {
...result,
action: AutoDiscoveryAction.IGNORE,
reason: AutoDiscovery.ERROR_MISSING_WELLKNOWN,
} : {
...result,
action: AutoDiscoveryAction.FAIL_PROMPT,
reason: error?.message || "General failure",
});
}
try {
resolve({ raw: JSON.parse(body), action: AutoDiscoveryAction.SUCCESS });
} catch (e) {
let reason = AutoDiscovery.ERROR_INVALID;
if (e.name === "SyntaxError") {
reason = AutoDiscovery.ERROR_INVALID_JSON;
}
resolve({
return resolve({
raw: JSON.parse(body),
action: AutoDiscoveryAction.SUCCESS,
});
} catch (err) {
return resolve({
error: err,
raw: {},
action: AutoDiscoveryAction.FAIL_PROMPT,
reason: reason,
error: e,
reason: err?.name === "SyntaxError"
? AutoDiscovery.ERROR_INVALID_JSON
: AutoDiscovery.ERROR_INVALID,
});
}
},
+36 -10
View File
@@ -137,7 +137,7 @@ import { VerificationRequest } from "./crypto/verification/request/VerificationR
import { VerificationBase as Verification } from "./crypto/verification/Base";
import * as ContentHelpers from "./content-helpers";
import { CrossSigningInfo, DeviceTrustLevel, ICacheCallbacks, UserTrustLevel } from "./crypto/CrossSigning";
import { Room } from "./models/room";
import { Room, RoomNameState } from "./models/room";
import {
IAddThreePidOnlyBody,
IBindThreePidBody,
@@ -188,15 +188,17 @@ import { IPusher, IPusherRequest, IPushRules, PushRuleAction, PushRuleKind, Rule
import { IThreepid } from "./@types/threepids";
import { CryptoStore } from "./crypto/store/base";
import { MediaHandler } from "./webrtc/mediaHandler";
import { IRefreshTokenResponse } from "./@types/auth";
import { IRefreshTokenResponse, SSOAction } from "./@types/auth";
import { TypedEventEmitter } from "./models/typed-event-emitter";
import { ReceiptType } from "./@types/read_receipts";
import { MSC3575SlidingSyncRequest, MSC3575SlidingSyncResponse, SlidingSync } from "./sliding-sync";
import { SlidingSyncSdk } from "./sliding-sync-sdk";
import { Thread, THREAD_RELATION_TYPE } from "./models/thread";
import { MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
import { UnstableValue } from "./NamespacedValue";
import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue";
import { ToDeviceBatch } from "./models/ToDeviceMessage";
import { IgnoredInvites } from "./models/invites-ignorer";
export type Store = IStore;
@@ -344,6 +346,12 @@ export interface ICreateClientOpts {
fallbackICEServerAllowed?: boolean;
cryptoCallbacks?: ICryptoCallbacks;
/**
* Method to generate room names for empty rooms and rooms names based on membership.
* Defaults to a built-in English handler with basic pluralisation.
*/
roomNameGenerator?: (roomId: string, state: RoomNameState) => string | null;
}
export interface IMatrixClientCreateOpts extends ICreateClientOpts {
@@ -390,8 +398,7 @@ export interface IStartClientOpts {
pollTimeout?: number;
/**
* The filter to apply to /sync calls. This will override the opts.initialSyncLimit, which would
* normally result in a timeline limit filter.
* The filter to apply to /sync calls.
*/
filter?: Filter;
@@ -875,6 +882,8 @@ export type ClientEventHandlerMap = {
& HttpApiEventHandlerMap
& BeaconEventHandlerMap;
const SSO_ACTION_PARAM = new UnstableValue("action", "org.matrix.msc3824.action");
/**
* Represents a Matrix Client. Only directly construct this if you want to use
* custom modules. Normally, {@link createClient} should be used
@@ -918,6 +927,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
protected fallbackICEServerAllowed = false;
protected roomList: RoomList;
protected syncApi: SlidingSyncSdk | SyncApi;
public roomNameGenerator?: ICreateClientOpts["roomNameGenerator"];
public pushRules: IPushRules;
protected syncLeftRoomsPromise: Promise<Room[]>;
protected syncedLeftRooms = false;
@@ -948,6 +958,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
private toDeviceMessageQueue: ToDeviceMessageQueue;
// A manager for determining which invites should be ignored.
public readonly ignoredInvites: IgnoredInvites;
constructor(opts: IMatrixClientCreateOpts) {
super();
@@ -1041,6 +1054,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// we still want to know which rooms are encrypted even if crypto is disabled:
// we don't want to start sending unencrypted events to them.
this.roomList = new RoomList(this.cryptoStore);
this.roomNameGenerator = opts.roomNameGenerator;
this.toDeviceMessageQueue = new ToDeviceMessageQueue(this);
@@ -1128,6 +1142,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
room.setUnreadNotificationCount(NotificationCountType.Highlight, highlightCount);
}
});
this.ignoredInvites = new IgnoredInvites(this);
}
/**
@@ -5352,7 +5368,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
};
await thread.fetchInitialEvents();
let nextBatch = thread.liveTimeline.getPaginationToken(Direction.Backward);
let nextBatch: string | null | undefined = thread.liveTimeline.getPaginationToken(Direction.Backward);
// Fetch events until we find the one we were asked for, or we run out of pages
while (!thread.findEventById(eventId)) {
@@ -7143,15 +7159,26 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {string} loginType The type of SSO login we are doing (sso or cas).
* Defaults to 'sso'.
* @param {string} idpId The ID of the Identity Provider being targeted, optional.
* @param {SSOAction} action the SSO flow to indicate to the IdP, optional.
* @return {string} The HS URL to hit to begin the SSO login process.
*/
public getSsoLoginUrl(redirectUrl: string, loginType = "sso", idpId?: string): string {
public getSsoLoginUrl(
redirectUrl: string,
loginType = "sso",
idpId?: string,
action?: SSOAction,
): string {
let url = "/login/" + loginType + "/redirect";
if (idpId) {
url += "/" + idpId;
}
return this.http.getUrl(url, { redirectUrl }, PREFIX_R0);
const params = {
redirectUrl,
[SSO_ACTION_PARAM.unstable!]: action,
};
return this.http.getUrl(url, params, PREFIX_R0);
}
/**
@@ -7516,9 +7543,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
[ReceiptType.Read]: rrEventId,
};
const privateField = await utils.getPrivateReadReceiptField(this);
if (privateField) {
content[privateField] = rpEventId;
if (await this.doesServerSupportUnstableFeature("org.matrix.msc2285.stable")) {
content[ReceiptType.ReadPrivate] = rpEventId;
}
return this.http.authedRequest(undefined, Method.Post, path, undefined, content);
+3 -3
View File
@@ -247,7 +247,7 @@ export const makeBeaconInfoContent: MakeBeaconInfoContent = (
});
export type BeaconInfoState = MBeaconInfoContent & {
assetType: LocationAssetType;
assetType?: LocationAssetType;
timestamp: number;
};
/**
@@ -255,14 +255,14 @@ export type BeaconInfoState = MBeaconInfoContent & {
*/
export const parseBeaconInfoContent = (content: MBeaconInfoEventContent): BeaconInfoState => {
const { description, timeout, live } = content;
const { type: assetType } = M_ASSET.findIn<MAssetContent>(content);
const timestamp = M_TIMESTAMP.findIn<number>(content);
const asset = M_ASSET.findIn<MAssetContent>(content);
return {
description,
timeout,
live,
assetType,
assetType: asset?.type,
timestamp,
};
};
+4 -4
View File
@@ -34,7 +34,7 @@ import { IRoomEncryption } from "../RoomList";
*
* @type {Object.<string, function(new: module:crypto/algorithms/base.EncryptionAlgorithm)>}
*/
export const ENCRYPTION_CLASSES: Record<string, new (params: IParams) => EncryptionAlgorithm> = {};
export const ENCRYPTION_CLASSES = new Map<string, new (params: IParams) => EncryptionAlgorithm>();
type DecryptionClassParams = Omit<IParams, "deviceId" | "config">;
@@ -44,7 +44,7 @@ type DecryptionClassParams = Omit<IParams, "deviceId" | "config">;
*
* @type {Object.<string, function(new: module:crypto/algorithms/base.DecryptionAlgorithm)>}
*/
export const DECRYPTION_CLASSES: Record<string, new (params: DecryptionClassParams) => DecryptionAlgorithm> = {};
export const DECRYPTION_CLASSES = new Map<string, new (params: DecryptionClassParams) => DecryptionAlgorithm>();
export interface IParams {
userId: string;
@@ -297,6 +297,6 @@ export function registerAlgorithm(
encryptor: new (params: IParams) => EncryptionAlgorithm,
decryptor: new (params: Omit<IParams, "deviceId">) => DecryptionAlgorithm,
): void {
ENCRYPTION_CLASSES[algorithm] = encryptor;
DECRYPTION_CLASSES[algorithm] = decryptor;
ENCRYPTION_CLASSES.set(algorithm, encryptor);
DECRYPTION_CLASSES.set(algorithm, decryptor);
}
+11 -11
View File
@@ -1191,7 +1191,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
class MegolmDecryption extends DecryptionAlgorithm {
// events which we couldn't decrypt due to unknown sessions / indexes: map from
// senderKey|sessionId to Set of MatrixEvents
private pendingEvents: Record<string, Map<string, Set<MatrixEvent>>> = {};
private pendingEvents = new Map<string, Map<string, Set<MatrixEvent>>>();
// this gets stubbed out by the unit tests.
private olmlib = olmlib;
@@ -1343,10 +1343,10 @@ class MegolmDecryption extends DecryptionAlgorithm {
const content = event.getWireContent();
const senderKey = content.sender_key;
const sessionId = content.session_id;
if (!this.pendingEvents[senderKey]) {
this.pendingEvents[senderKey] = new Map();
if (!this.pendingEvents.has(senderKey)) {
this.pendingEvents.set(senderKey, new Map<string, Set<MatrixEvent>>());
}
const senderPendingEvents = this.pendingEvents[senderKey];
const senderPendingEvents = this.pendingEvents.get(senderKey);
if (!senderPendingEvents.has(sessionId)) {
senderPendingEvents.set(sessionId, new Set());
}
@@ -1364,7 +1364,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
const content = event.getWireContent();
const senderKey = content.sender_key;
const sessionId = content.session_id;
const senderPendingEvents = this.pendingEvents[senderKey];
const senderPendingEvents = this.pendingEvents.get(senderKey);
const pendingEvents = senderPendingEvents?.get(sessionId);
if (!pendingEvents) {
return;
@@ -1375,7 +1375,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
senderPendingEvents.delete(sessionId);
}
if (senderPendingEvents.size === 0) {
delete this.pendingEvents[senderKey];
this.pendingEvents.delete(senderKey);
}
}
@@ -1711,7 +1711,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
* @return {Boolean} whether all messages were successfully decrypted
*/
private async retryDecryption(senderKey: string, sessionId: string): Promise<boolean> {
const senderPendingEvents = this.pendingEvents[senderKey];
const senderPendingEvents = this.pendingEvents.get(senderKey);
if (!senderPendingEvents) {
return true;
}
@@ -1732,16 +1732,16 @@ class MegolmDecryption extends DecryptionAlgorithm {
}));
// If decrypted successfully, they'll have been removed from pendingEvents
return !this.pendingEvents[senderKey]?.has(sessionId);
return !this.pendingEvents.get(senderKey)?.has(sessionId);
}
public async retryDecryptionFromSender(senderKey: string): Promise<boolean> {
const senderPendingEvents = this.pendingEvents[senderKey];
const senderPendingEvents = this.pendingEvents.get(senderKey);
if (!senderPendingEvents) {
return true;
}
delete this.pendingEvents[senderKey];
this.pendingEvents.delete(senderKey);
await Promise.all([...senderPendingEvents].map(async ([_sessionId, pending]) => {
await Promise.all([...pending].map(async (ev) => {
@@ -1753,7 +1753,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
}));
}));
return !this.pendingEvents[senderKey];
return !this.pendingEvents.has(senderKey);
}
public async sendSharedHistoryInboundSessions(devicesByUser: Record<string, DeviceInfo[]>): Promise<void> {
+26 -25
View File
@@ -278,9 +278,9 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
private oneTimeKeyCheckInProgress = false;
// EncryptionAlgorithm instance for each room
private roomEncryptors: Record<string, EncryptionAlgorithm> = {};
private roomEncryptors = new Map<string, EncryptionAlgorithm>();
// map from algorithm to DecryptionAlgorithm instance, for each room
private roomDecryptors: Record<string, Record<string, DecryptionAlgorithm>> = {};
private roomDecryptors = new Map<string, Map<string, DecryptionAlgorithm>>();
private deviceKeys: Record<string, string> = {}; // type: key
@@ -422,7 +422,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
this.deviceList.on(CryptoEvent.UserCrossSigningUpdated, this.onDeviceListUserCrossSigningUpdated);
this.reEmitter.reEmit(this.deviceList, [CryptoEvent.DevicesUpdated, CryptoEvent.WillUpdateDevices]);
this.supportedAlgorithms = Object.keys(algorithms.DECRYPTION_CLASSES);
this.supportedAlgorithms = Array.from(algorithms.DECRYPTION_CLASSES.keys());
this.outgoingRoomKeyRequestManager = new OutgoingRoomKeyRequestManager(
baseApis, this.deviceId, this.cryptoStore,
@@ -2527,7 +2527,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* This should not normally be necessary.
*/
public forceDiscardSession(roomId: string): void {
const alg = this.roomEncryptors[roomId];
const alg = this.roomEncryptors.get(roomId);
if (alg === undefined) throw new Error("Room not encrypted");
if (alg.forceDiscardSession === undefined) {
throw new Error("Room encryption algorithm doesn't support session discarding");
@@ -2580,7 +2580,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// the encryption event would appear in both.
// If it's called more than twice though,
// it signals a bug on client or server.
const existingAlg = this.roomEncryptors[roomId];
const existingAlg = this.roomEncryptors.get(roomId);
if (existingAlg) {
return;
}
@@ -2594,7 +2594,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
storeConfigPromise = this.roomList.setRoomEncryption(roomId, config);
}
const AlgClass = algorithms.ENCRYPTION_CLASSES[config.algorithm];
const AlgClass = algorithms.ENCRYPTION_CLASSES.get(config.algorithm);
if (!AlgClass) {
throw new Error("Unable to encrypt with " + config.algorithm);
}
@@ -2608,7 +2608,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
roomId,
config,
});
this.roomEncryptors[roomId] = alg;
this.roomEncryptors.set(roomId, alg);
if (storeConfigPromise) {
await storeConfigPromise;
@@ -2640,7 +2640,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
public trackRoomDevices(roomId: string): Promise<void> {
const trackMembers = async () => {
// not an encrypted room
if (!this.roomEncryptors[roomId]) {
if (!this.roomEncryptors.has(roomId)) {
return;
}
const room = this.clientStore.getRoom(roomId);
@@ -2785,7 +2785,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* @param {module:models/room} room the room the event is in
*/
public prepareToEncrypt(room: Room): void {
const alg = this.roomEncryptors[room.roomId];
const alg = this.roomEncryptors.get(room.roomId);
if (alg) {
alg.prepareToEncrypt(room);
}
@@ -2808,7 +2808,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
const roomId = event.getRoomId();
const alg = this.roomEncryptors[roomId];
const alg = this.roomEncryptors.get(roomId);
if (!alg) {
// MatrixClient has already checked that this room should be encrypted,
// so this is an unexpected situation.
@@ -3097,7 +3097,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
private getTrackedE2eRooms(): Room[] {
return this.clientStore.getRooms().filter((room) => {
// check for rooms with encryption enabled
const alg = this.roomEncryptors[room.roomId];
const alg = this.roomEncryptors.get(room.roomId);
if (!alg) {
return false;
}
@@ -3533,7 +3533,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
const roomId = member.roomId;
const alg = this.roomEncryptors[roomId];
const alg = this.roomEncryptors.get(roomId);
if (!alg) {
// not encrypting in this room
return;
@@ -3634,11 +3634,11 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
` for ${roomId} / ${body.session_id} (id ${req.requestId})`);
if (userId !== this.userId) {
if (!this.roomEncryptors[roomId]) {
if (!this.roomEncryptors.get(roomId)) {
logger.debug(`room key request for unencrypted room ${roomId}`);
return;
}
const encryptor = this.roomEncryptors[roomId];
const encryptor = this.roomEncryptors.get(roomId);
const device = this.deviceList.getStoredDevice(userId, deviceId);
if (!device) {
logger.debug(`Ignoring keyshare for unknown device ${userId}:${deviceId}`);
@@ -3674,12 +3674,12 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// if we don't have a decryptor for this room/alg, we don't have
// the keys for the requested events, and can drop the requests.
if (!this.roomDecryptors[roomId]) {
if (!this.roomDecryptors.has(roomId)) {
logger.log(`room key request for unencrypted room ${roomId}`);
return;
}
const decryptor = this.roomDecryptors[roomId][alg];
const decryptor = this.roomDecryptors.get(roomId).get(alg);
if (!decryptor) {
logger.log(`room key request for unknown alg ${alg} in room ${roomId}`);
return;
@@ -3745,23 +3745,24 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* unknown
*/
public getRoomDecryptor(roomId: string, algorithm: string): DecryptionAlgorithm {
let decryptors: Record<string, DecryptionAlgorithm>;
let decryptors: Map<string, DecryptionAlgorithm>;
let alg: DecryptionAlgorithm;
roomId = roomId || null;
if (roomId) {
decryptors = this.roomDecryptors[roomId];
decryptors = this.roomDecryptors.get(roomId);
if (!decryptors) {
this.roomDecryptors[roomId] = decryptors = {};
decryptors = new Map<string, DecryptionAlgorithm>();
this.roomDecryptors.set(roomId, decryptors);
}
alg = decryptors[algorithm];
alg = decryptors.get(algorithm);
if (alg) {
return alg;
}
}
const AlgClass = algorithms.DECRYPTION_CLASSES[algorithm];
const AlgClass = algorithms.DECRYPTION_CLASSES.get(algorithm);
if (!AlgClass) {
throw new algorithms.DecryptionError(
'UNKNOWN_ENCRYPTION_ALGORITHM',
@@ -3777,7 +3778,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
});
if (decryptors) {
decryptors[algorithm] = alg;
decryptors.set(algorithm, alg);
}
return alg;
}
@@ -3791,9 +3792,9 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
*/
private getRoomDecryptors(algorithm: string): DecryptionAlgorithm[] {
const decryptors = [];
for (const d of Object.values(this.roomDecryptors)) {
if (algorithm in d) {
decryptors.push(d[algorithm]);
for (const d of this.roomDecryptors.values()) {
if (d.has(algorithm)) {
decryptors.push(d.get(algorithm));
}
}
return decryptors;
@@ -26,7 +26,7 @@ import {
Mode,
OutgoingRoomKeyRequest,
} from "./base";
import { IRoomKeyRequestBody } from "../index";
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
@@ -261,7 +261,9 @@ export class Backend implements CryptoStore {
const cursor = this.result;
if (cursor) {
const keyReq = cursor.value;
if (keyReq.recipients.includes({ userId, deviceId })) {
if (keyReq.recipients.some((recipient: IRoomKeyRequestRecipient) =>
recipient.userId === userId && recipient.deviceId === deviceId,
)) {
results.push(keyReq);
}
cursor.continue();
+4 -2
View File
@@ -191,11 +191,13 @@ export class MemoryCryptoStore implements CryptoStore {
deviceId: string,
wantedStates: number[],
): Promise<OutgoingRoomKeyRequest[]> {
const results = [];
const results: OutgoingRoomKeyRequest[] = [];
for (const req of this.outgoingRoomKeyRequests) {
for (const state of wantedStates) {
if (req.state === state && req.recipients.includes({ userId, deviceId })) {
if (req.state === state && req.recipients.some(
(recipient) => recipient.userId === userId && recipient.deviceId === deviceId,
)) {
results.push(req);
}
}
+13 -15
View File
@@ -250,12 +250,9 @@ export class InteractiveAuth {
if (!this.data?.flows) {
this.busyChangedCallback?.(true);
// use the existing sessionId, if one is present.
let auth = null;
if (this.data.session) {
auth = {
session: this.data.session,
};
}
const auth = this.data.session
? { session: this.data.session }
: null;
this.doRequest(auth).finally(() => {
this.busyChangedCallback?.(false);
});
@@ -312,7 +309,7 @@ export class InteractiveAuth {
* @return {string} session id
*/
public getSessionId(): string {
return this.data ? this.data.session : undefined;
return this.data?.session;
}
/**
@@ -477,6 +474,9 @@ export class InteractiveAuth {
logger.log("Background poll request failed doing UI auth: ignoring", error);
}
}
if (!error.data) {
error.data = {};
}
// if the error didn't come with flows, completed flows or session ID,
// copy over the ones we have. Synapse sometimes sends responses without
// any UI auth data (eg. when polling for email validation, if the email
@@ -539,19 +539,17 @@ export class InteractiveAuth {
return;
}
if (this.data && this.data.errcode || this.data.error) {
if (this.data?.errcode || this.data?.error) {
this.stateUpdatedCallback(nextStage, {
errcode: this.data.errcode || "",
error: this.data.error || "",
errcode: this.data?.errcode || "",
error: this.data?.error || "",
});
return;
}
const stageStatus: IStageStatus = {};
if (nextStage == EMAIL_STAGE_TYPE) {
stageStatus.emailSid = this.emailSid;
}
this.stateUpdatedCallback(nextStage, stageStatus);
this.stateUpdatedCallback(nextStage, nextStage === EMAIL_STAGE_TYPE
? { emailSid: this.emailSid }
: {});
}
/**
+1 -1
View File
@@ -197,7 +197,7 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
const startTimestamp = this._beaconInfo?.timestamp > Date.now() ?
this._beaconInfo?.timestamp - 360000 /* 6min */ :
this._beaconInfo?.timestamp;
this._isLive = this._beaconInfo?.live &&
this._isLive = !!this._beaconInfo?.live &&
isTimestampInDuration(startTimestamp, this._beaconInfo?.timeout, Date.now());
if (prevLiveness !== this.isLive) {
+22 -30
View File
@@ -86,7 +86,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
private readonly displayPendingEvents: boolean;
private liveTimeline: EventTimeline;
private timelines: EventTimeline[];
private _eventIdToTimeline: Record<string, EventTimeline>;
private _eventIdToTimeline = new Map<string, EventTimeline>();
private filter?: Filter;
/**
@@ -138,7 +138,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
// just a list - *not* ordered.
this.timelines = [this.liveTimeline];
this._eventIdToTimeline = {};
this._eventIdToTimeline = new Map<string, EventTimeline>();
this.filter = opts.filter;
@@ -210,7 +210,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
* @return {module:models/event-timeline~EventTimeline} timeline
*/
public eventIdToTimeline(eventId: string): EventTimeline {
return this._eventIdToTimeline[eventId];
return this._eventIdToTimeline.get(eventId);
}
/**
@@ -220,10 +220,10 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
* @param {String} newEventId event ID of the replacement event
*/
public replaceEventId(oldEventId: string, newEventId: string): void {
const existingTimeline = this._eventIdToTimeline[oldEventId];
const existingTimeline = this._eventIdToTimeline.get(oldEventId);
if (existingTimeline) {
delete this._eventIdToTimeline[oldEventId];
this._eventIdToTimeline[newEventId] = existingTimeline;
this._eventIdToTimeline.delete(oldEventId);
this._eventIdToTimeline.set(newEventId, existingTimeline);
}
}
@@ -257,7 +257,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
if (resetAllTimelines) {
this.timelines = [newTimeline];
this._eventIdToTimeline = {};
this._eventIdToTimeline = new Map<string, EventTimeline>();
} else {
this.timelines.push(newTimeline);
}
@@ -288,7 +288,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
* the given event, or null if unknown
*/
public getTimelineForEvent(eventId: string): EventTimeline | null {
const res = this._eventIdToTimeline[eventId];
const res = this._eventIdToTimeline.get(eventId);
return (res === undefined) ? null : res;
}
@@ -450,7 +450,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
const event = events[i];
const eventId = event.getId();
const existingTimeline = this._eventIdToTimeline[eventId];
const existingTimeline = this._eventIdToTimeline.get(eventId);
if (!existingTimeline) {
// we don't know about this event yet. Just add it to the timeline.
@@ -601,7 +601,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
}
}
const timeline = this._eventIdToTimeline[event.getId()];
const timeline = this._eventIdToTimeline.get(event.getId());
if (timeline) {
if (duplicateStrategy === DuplicateStrategy.Replace) {
debuglog("EventTimelineSet.addLiveEvent: replacing duplicate event " + event.getId());
@@ -697,7 +697,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
roomState,
timelineWasEmpty,
});
this._eventIdToTimeline[eventId] = timeline;
this._eventIdToTimeline.set(eventId, timeline);
this.relations.aggregateParentEvent(event);
this.relations.aggregateChildEvent(event, this);
@@ -725,22 +725,14 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
newEventId: string,
): void {
// XXX: why don't we infer newEventId from localEvent?
const existingTimeline = this._eventIdToTimeline[oldEventId];
const existingTimeline = this._eventIdToTimeline.get(oldEventId);
if (existingTimeline) {
delete this._eventIdToTimeline[oldEventId];
this._eventIdToTimeline[newEventId] = existingTimeline;
} else {
if (this.filter) {
if (this.filter.filterRoomTimeline([localEvent]).length) {
this.addEventToTimeline(localEvent, this.liveTimeline, {
toStartOfTimeline: false,
});
}
} else {
this.addEventToTimeline(localEvent, this.liveTimeline, {
toStartOfTimeline: false,
});
}
this._eventIdToTimeline.delete(oldEventId);
this._eventIdToTimeline.set(newEventId, existingTimeline);
} else if (!this.filter || this.filter.filterRoomTimeline([localEvent]).length) {
this.addEventToTimeline(localEvent, this.liveTimeline, {
toStartOfTimeline: false,
});
}
}
@@ -753,14 +745,14 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
* in this room.
*/
public removeEvent(eventId: string): MatrixEvent | null {
const timeline = this._eventIdToTimeline[eventId];
const timeline = this._eventIdToTimeline.get(eventId);
if (!timeline) {
return null;
}
const removed = timeline.removeEvent(eventId);
if (removed) {
delete this._eventIdToTimeline[eventId];
this._eventIdToTimeline.delete(eventId);
const data = {
timeline: timeline,
};
@@ -787,8 +779,8 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
return 0;
}
const timeline1 = this._eventIdToTimeline[eventId1];
const timeline2 = this._eventIdToTimeline[eventId2];
const timeline1 = this._eventIdToTimeline.get(eventId1);
const timeline2 = this._eventIdToTimeline.get(eventId2);
if (timeline1 === undefined) {
return null;
+1 -1
View File
@@ -307,7 +307,7 @@ export class EventTimeline {
*
* @param {?string} token pagination token
*
* @param {string} direction EventTimeline.BACKWARDS to set the pagination
* @param {string} direction EventTimeline.BACKWARDS to set the paginatio
* token for going backwards in time; EventTimeline.FORWARDS to set the
* pagination token for going forwards in time.
*/
+6 -12
View File
@@ -26,7 +26,7 @@ import { logger } from '../logger';
import { VerificationRequest } from "../crypto/verification/request/VerificationRequest";
import { EVENT_VISIBILITY_CHANGE_TYPE, EventType, MsgType, RelationType } from "../@types/event";
import { Crypto, IEventDecryptionResult } from "../crypto";
import { deepSortedObjectEntries } from "../utils";
import { deepSortedObjectEntries, internaliseString } from "../utils";
import { RoomMember } from "./room-member";
import { Thread, ThreadEvent, EventHandlerMap as ThreadEventHandlerMap, THREAD_RELATION_TYPE } from "./thread";
import { IActionsObject } from '../pushprocessor';
@@ -37,14 +37,6 @@ import { EventStatus } from "./event-status";
export { EventStatus } from "./event-status";
const interns: Record<string, string> = {};
function intern(str: string): string {
if (!interns[str]) {
interns[str] = str;
}
return interns[str];
}
/* eslint-disable camelcase */
export interface IContent {
[key: string]: any;
@@ -326,17 +318,17 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
// of space if we don't intern it.
["state_key", "type", "sender", "room_id", "membership"].forEach((prop) => {
if (typeof event[prop] !== "string") return;
event[prop] = intern(event[prop]);
event[prop] = internaliseString(event[prop]);
});
["membership", "avatar_url", "displayname"].forEach((prop) => {
if (typeof event.content?.[prop] !== "string") return;
event.content[prop] = intern(event.content[prop]);
event.content[prop] = internaliseString(event.content[prop]);
});
["rel_type"].forEach((prop) => {
if (typeof event.content?.["m.relates_to"]?.[prop] !== "string") return;
event.content["m.relates_to"][prop] = intern(event.content["m.relates_to"][prop]);
event.content["m.relates_to"][prop] = internaliseString(event.content["m.relates_to"][prop]);
});
this.txnId = event.txn_id || null;
@@ -796,6 +788,8 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
// not a decryption error: log the whole exception as an error
// (and don't bother with a retry)
const re = options.isRetry ? 're' : '';
// For find results: this can produce "Error decrypting event (id=$ev)" and
// "Error redecrypting event (id=$ev)".
logger.error(
`Error ${re}decrypting event ` +
`(id=${this.getId()}): ${e.stack || e}`,
+361
View File
@@ -0,0 +1,361 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { UnstableValue } from "matrix-events-sdk";
import { MatrixClient } from "../client";
import { MatrixEvent } from "./event";
import { EventTimeline } from "./event-timeline";
import { Preset } from "../@types/partials";
import { globToRegexp } from "../utils";
import { Room } from "./room";
/// The event type storing the user's individual policies.
///
/// Exported for testing purposes.
export const POLICIES_ACCOUNT_EVENT_TYPE = new UnstableValue("m.policies", "org.matrix.msc3847.policies");
/// The key within the user's individual policies storing the user's ignored invites.
///
/// Exported for testing purposes.
export const IGNORE_INVITES_ACCOUNT_EVENT_KEY = new UnstableValue("m.ignore.invites",
"org.matrix.msc3847.ignore.invites");
/// The types of recommendations understood.
enum PolicyRecommendation {
Ban = "m.ban",
}
/**
* The various scopes for policies.
*/
export enum PolicyScope {
/**
* The policy deals with an individual user, e.g. reject invites
* from this user.
*/
User = "m.policy.user",
/**
* The policy deals with a room, e.g. reject invites towards
* a specific room.
*/
Room = "m.policy.room",
/**
* The policy deals with a server, e.g. reject invites from
* this server.
*/
Server = "m.policy.server",
}
/**
* A container for ignored invites.
*
* # Performance
*
* This implementation is extremely naive. It expects that we are dealing
* with a very short list of sources (e.g. only one). If real-world
* applications turn out to require longer lists, we may need to rework
* our data structures.
*/
export class IgnoredInvites {
constructor(
private readonly client: MatrixClient,
) {
}
/**
* Add a new rule.
*
* @param scope The scope for this rule.
* @param entity The entity covered by this rule. Globs are supported.
* @param reason A human-readable reason for introducing this new rule.
* @return The event id for the new rule.
*/
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, {
entity,
reason,
recommendation: PolicyRecommendation.Ban,
});
return response.event_id;
}
/**
* Remove a rule.
*/
public async removeRule(event: MatrixEvent) {
await this.client.redactEvent(event.getRoomId()!, event.getId()!);
}
/**
* Add a new room to the list of sources. If the user isn't a member of the
* room, attempt to join it.
*
* @param roomId A valid room id. If this room is already in the list
* of sources, it will not be duplicated.
* @return `true` if the source was added, `false` if it was already present.
* @throws If `roomId` isn't the id of a room that the current user is already
* member of or can join.
*
* # Safety
*
* This method will rewrite the `Policies` object in the user's account data.
* This rewrite is inherently racy and could overwrite or be overwritten by
* other concurrent rewrites of the same object.
*/
public async addSource(roomId: string): Promise<boolean> {
// We attempt to join the room *before* calling
// `await this.getOrCreateSourceRooms()` to decrease the duration
// of the racy section.
await this.client.joinRoom(roomId);
// Race starts.
const sources = (await this.getOrCreateSourceRooms())
.map(room => room.roomId);
if (sources.includes(roomId)) {
return false;
}
sources.push(roomId);
await this.withIgnoreInvitesPolicies(ignoreInvitesPolicies => {
ignoreInvitesPolicies.sources = sources;
});
// Race ends.
return true;
}
/**
* 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.
* @returns A rule matching the entity, if any was found, `null` otherwise.
*/
public async getRuleForInvite({ sender, roomId }: {
sender: string;
roomId: string;
}): Promise<Readonly<MatrixEvent | null>> {
// In this implementation, we perform a very naive lookup:
// - search in each policy room;
// - turn each (potentially glob) rule entity into a regexp.
//
// Real-world testing will tell us whether this is performant enough.
// In the (unfortunately likely) case it isn't, there are several manners
// in which we could optimize this:
// - match several entities per go;
// - pre-compile each rule entity into a regexp;
// - pre-compile entire rooms into a single regexp.
const policyRooms = await this.getOrCreateSourceRooms();
const senderServer = sender.split(":")[1];
const roomServer = roomId.split(":")[1];
for (const room of policyRooms) {
const state = room.getUnfilteredTimelineSet().getLiveTimeline().getState(EventTimeline.FORWARDS);
for (const { scope, entities } of [
{ scope: PolicyScope.Room, entities: [roomId] },
{ scope: PolicyScope.User, entities: [sender] },
{ scope: PolicyScope.Server, entities: [senderServer, roomServer] },
]) {
const events = state.getStateEvents(scope);
for (const event of events) {
const content = event.getContent();
if (content?.recommendation != PolicyRecommendation.Ban) {
// Ignoring invites only looks at `m.ban` recommendations.
continue;
}
const glob = content?.entity;
if (!glob) {
// Invalid event.
continue;
}
let regexp: RegExp;
try {
regexp = new RegExp(globToRegexp(glob, false));
} catch (ex) {
// Assume invalid event.
continue;
}
for (const entity of entities) {
if (entity && regexp.test(entity)) {
return event;
}
}
// No match.
}
}
}
return null;
}
/**
* Get the target room, i.e. the room in which any new rule should be written.
*
* If there is no target room setup, a target room is created.
*
* Note: This method is public for testing reasons. Most clients should not need
* to call it directly.
*
* # Safety
*
* This method will rewrite the `Policies` object in the user's account data.
* This rewrite is inherently racy and could overwrite or be overwritten by
* other concurrent rewrites of the same object.
*/
public async getOrCreateTargetRoom(): Promise<Room> {
const ignoreInvitesPolicies = this.getIgnoreInvitesPolicies();
let target = ignoreInvitesPolicies.target;
// Validate `target`. If it is invalid, trash out the current `target`
// and create a new room.
if (typeof target !== "string") {
target = null;
}
if (target) {
// Check that the room exists and is valid.
const room = this.client.getRoom(target);
if (room) {
return room;
} else {
target = null;
}
}
// We need to create our own policy room for ignoring invites.
target = (await this.client.createRoom({
name: "Individual Policy Room",
preset: Preset.PrivateChat,
})).room_id;
await this.withIgnoreInvitesPolicies(ignoreInvitesPolicies => {
ignoreInvitesPolicies.target = target;
});
// Since we have just called `createRoom`, `getRoom` should not be `null`.
return this.client.getRoom(target)!;
}
/**
* Get the list of source rooms, i.e. the rooms from which rules need to be read.
*
* If no source rooms are setup, the target room is used as sole source room.
*
* Note: This method is public for testing reasons. Most clients should not need
* to call it directly.
*
* # Safety
*
* This method will rewrite the `Policies` object in the user's account data.
* This rewrite is inherently racy and could overwrite or be overwritten by
* other concurrent rewrites of the same object.
*/
public async getOrCreateSourceRooms(): Promise<Room[]> {
const ignoreInvitesPolicies = this.getIgnoreInvitesPolicies();
let sources = ignoreInvitesPolicies.sources;
// Validate `sources`. If it is invalid, trash out the current `sources`
// and create a new list of sources from `target`.
let hasChanges = false;
if (!Array.isArray(sources)) {
// `sources` could not be an array.
hasChanges = true;
sources = [];
}
let sourceRooms: Room[] = sources
// `sources` could contain non-string / invalid room ids
.filter(roomId => typeof roomId === "string")
.map(roomId => this.client.getRoom(roomId))
.filter(room => !!room);
if (sourceRooms.length != sources.length) {
hasChanges = true;
}
if (sourceRooms.length == 0) {
// `sources` could be empty (possibly because we've removed
// invalid content)
const target = await this.getOrCreateTargetRoom();
hasChanges = true;
sourceRooms = [target];
}
if (hasChanges) {
// Reload `policies`/`ignoreInvitesPolicies` in case it has been changed
// during or by our call to `this.getTargetRoom()`.
await this.withIgnoreInvitesPolicies(ignoreInvitesPolicies => {
ignoreInvitesPolicies.sources = sources;
});
}
return sourceRooms;
}
/**
* Fetch the `IGNORE_INVITES_POLICIES` object from account data.
*
* If both an unstable prefix version and a stable prefix version are available,
* it will return the stable prefix version preferentially.
*
* The result is *not* validated but is guaranteed to be a non-null object.
*
* @returns A non-null object.
*/
private getIgnoreInvitesPolicies(): {[key: string]: any} {
return this.getPoliciesAndIgnoreInvitesPolicies().ignoreInvitesPolicies;
}
/**
* Modify in place the `IGNORE_INVITES_POLICIES` object from account data.
*/
private async withIgnoreInvitesPolicies(cb: (ignoreInvitesPolicies: {[key: string]: any}) => void) {
const { policies, ignoreInvitesPolicies } = this.getPoliciesAndIgnoreInvitesPolicies();
cb(ignoreInvitesPolicies);
policies[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name] = ignoreInvitesPolicies;
await this.client.setAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name, policies);
}
/**
* As `getIgnoreInvitesPolicies` but also return the `POLICIES_ACCOUNT_EVENT_TYPE`
* object.
*/
private getPoliciesAndIgnoreInvitesPolicies():
{policies: {[key: string]: any}, ignoreInvitesPolicies: {[key: string]: any}} {
let policies = {};
for (const key of [POLICIES_ACCOUNT_EVENT_TYPE.name, POLICIES_ACCOUNT_EVENT_TYPE.altName]) {
if (!key) {
continue;
}
const value = this.client.getAccountData(key)?.getContent();
if (value) {
policies = value;
break;
}
}
let ignoreInvitesPolicies = {};
let hasIgnoreInvitesPolicies = false;
for (const key of [IGNORE_INVITES_ACCOUNT_EVENT_KEY.name, IGNORE_INVITES_ACCOUNT_EVENT_KEY.altName]) {
if (!key) {
continue;
}
const value = policies[key];
if (value && typeof value == "object") {
ignoreInvitesPolicies = value;
hasIgnoreInvitesPolicies = true;
break;
}
}
if (!hasIgnoreInvitesPolicies) {
policies[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name] = ignoreInvitesPolicies;
}
return { policies, ignoreInvitesPolicies };
}
}
+19 -21
View File
@@ -23,14 +23,8 @@ import { Room } from "./room";
export class RelationsContainer {
// A tree of objects to access a set of related children for an event, as in:
// this.relations[parentEventId][relationType][relationEventType]
private relations: {
[parentEventId: string]: {
[relationType: RelationType | string]: {
[eventType: EventType | string]: Relations;
};
};
} = {};
// this.relations.get(parentEventId).get(relationType).get(relationEventType)
private relations = new Map<string, Map<RelationType | string, Map<EventType | string, Relations>>>();
constructor(private readonly client: MatrixClient, private readonly room?: Room) {
}
@@ -57,14 +51,15 @@ export class RelationsContainer {
relationType: RelationType | string,
eventType: EventType | string,
): Relations | undefined {
return this.relations[eventId]?.[relationType]?.[eventType];
return this.relations.get(eventId)?.get(relationType)?.get(eventType);
}
public getAllChildEventsForEvent(parentEventId: string): MatrixEvent[] {
const relationsForEvent = this.relations[parentEventId] ?? {};
const relationsForEvent = this.relations.get(parentEventId)
?? new Map<RelationType | string, Map<EventType | string, Relations>>();
const events: MatrixEvent[] = [];
for (const relationsRecord of Object.values(relationsForEvent)) {
for (const relations of Object.values(relationsRecord)) {
for (const relationsRecord of relationsForEvent.values()) {
for (const relations of relationsRecord.values()) {
events.push(...relations.getRelations());
}
}
@@ -79,11 +74,11 @@ export class RelationsContainer {
* @param {MatrixEvent} event The event to check as relation target.
*/
public aggregateParentEvent(event: MatrixEvent): void {
const relationsForEvent = this.relations[event.getId()];
const relationsForEvent = this.relations.get(event.getId());
if (!relationsForEvent) return;
for (const relationsWithRelType of Object.values(relationsForEvent)) {
for (const relationsWithEventType of Object.values(relationsWithRelType)) {
for (const relationsWithRelType of relationsForEvent.values()) {
for (const relationsWithEventType of relationsWithRelType.values()) {
relationsWithEventType.setTargetEvent(event);
}
}
@@ -123,23 +118,26 @@ export class RelationsContainer {
const { event_id: relatesToEventId, rel_type: relationType } = relation;
const eventType = event.getType();
let relationsForEvent = this.relations[relatesToEventId];
let relationsForEvent = this.relations.get(relatesToEventId);
if (!relationsForEvent) {
relationsForEvent = this.relations[relatesToEventId] = {};
relationsForEvent = new Map<RelationType | string, Map<EventType | string, Relations>>();
this.relations.set(relatesToEventId, relationsForEvent);
}
let relationsWithRelType = relationsForEvent[relationType];
let relationsWithRelType = relationsForEvent.get(relationType);
if (!relationsWithRelType) {
relationsWithRelType = relationsForEvent[relationType] = {};
relationsWithRelType = new Map<EventType | string, Relations>();
relationsForEvent.set(relationType, relationsWithRelType);
}
let relationsWithEventType = relationsWithRelType[eventType];
let relationsWithEventType = relationsWithRelType.get(eventType);
if (!relationsWithEventType) {
relationsWithEventType = relationsWithRelType[eventType] = new Relations(
relationsWithEventType = new Relations(
relationType,
eventType,
this.client,
);
relationsWithRelType.set(eventType, relationsWithEventType);
const room = this.room ?? timelineSet?.room;
const relatesToEvent = timelineSet?.findEventById(relatesToEventId)
+7 -8
View File
@@ -79,7 +79,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
public readonly reEmitter = new TypedReEmitter<EmittedEvents, EventHandlerMap>(this);
private sentinels: Record<string, RoomMember> = {}; // userId: RoomMember
// stores fuzzy matches to a list of userIDs (applies utils.removeHiddenChars to keys)
private displayNameToUserIds: Record<string, string[]> = {};
private displayNameToUserIds = new Map<string, string[]>();
private userIdsToDisplayNames: Record<string, string> = {};
private tokenToInvite: Record<string, MatrixEvent> = {}; // 3pid invite state_key to m.room.member invite
private joinedMemberCount: number = null; // cache of the number of joined members
@@ -709,7 +709,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
* @return {string[]} An array of user IDs or an empty array.
*/
public getUserIdsWithDisplayName(displayName: string): string[] {
return this.displayNameToUserIds[utils.removeHiddenChars(displayName)] || [];
return this.displayNameToUserIds.get(utils.removeHiddenChars(displayName)) ?? [];
}
/**
@@ -941,11 +941,11 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
// the lot.
const strippedOldName = utils.removeHiddenChars(oldName);
const existingUserIds = this.displayNameToUserIds[strippedOldName];
const existingUserIds = this.displayNameToUserIds.get(strippedOldName);
if (existingUserIds) {
// remove this user ID from this array
const filteredUserIDs = existingUserIds.filter((id) => id !== userId);
this.displayNameToUserIds[strippedOldName] = filteredUserIDs;
this.displayNameToUserIds.set(strippedOldName, filteredUserIDs);
}
}
@@ -954,10 +954,9 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
const strippedDisplayname = displayName && utils.removeHiddenChars(displayName);
// an empty stripped displayname (undefined/'') will be set to MXID in room-member.js
if (strippedDisplayname) {
if (!this.displayNameToUserIds[strippedDisplayname]) {
this.displayNameToUserIds[strippedDisplayname] = [];
}
this.displayNameToUserIds[strippedDisplayname].push(userId);
const arr = this.displayNameToUserIds.get(strippedDisplayname) ?? [];
arr.push(userId);
this.displayNameToUserIds.set(strippedDisplayname, arr);
}
}
}
+178 -89
View File
@@ -1,5 +1,5 @@
/*
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2015 - 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -37,7 +37,8 @@ import {
import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials";
import { Filter, IFilterDefinition } from "../filter";
import { RoomState } from "./room-state";
import { RoomState, RoomStateEvent, RoomStateEventHandlerMap } from "./room-state";
import { BeaconEvent, BeaconEventHandlerMap } from "./beacon";
import {
Thread,
ThreadEvent,
@@ -172,16 +173,19 @@ export enum RoomEvent {
}
type EmittedEvents = RoomEvent
| RoomStateEvent.Events
| RoomStateEvent.Members
| RoomStateEvent.NewMember
| RoomStateEvent.Update
| RoomStateEvent.Marker
| ThreadEvent.New
| ThreadEvent.Update
| ThreadEvent.NewReply
| RoomEvent.Timeline
| RoomEvent.TimelineReset
| RoomEvent.TimelineRefresh
| RoomEvent.HistoryImportedWithinTimeline
| RoomEvent.OldStateUpdated
| RoomEvent.CurrentStateUpdated
| MatrixEventEvent.BeforeRedaction;
| MatrixEventEvent.BeforeRedaction
| BeaconEvent.New
| BeaconEvent.Update
| BeaconEvent.Destroy
| BeaconEvent.LivenessChange;
export type RoomEventHandlerMap = {
[RoomEvent.MyMembership]: (room: Room, membership: string, prevMembership?: string) => void;
@@ -205,7 +209,21 @@ export type RoomEventHandlerMap = {
) => void;
[RoomEvent.TimelineRefresh]: (room: Room, eventTimelineSet: EventTimelineSet) => void;
[ThreadEvent.New]: (thread: Thread, toStartOfTimeline: boolean) => void;
} & ThreadHandlerMap & MatrixEventHandlerMap;
} & ThreadHandlerMap
& MatrixEventHandlerMap
& Pick<
RoomStateEventHandlerMap,
RoomStateEvent.Events
| RoomStateEvent.Members
| RoomStateEvent.NewMember
| RoomStateEvent.Update
| RoomStateEvent.Marker
| BeaconEvent.New
>
& Pick<
BeaconEventHandlerMap,
BeaconEvent.Update | BeaconEvent.Destroy | BeaconEvent.LivenessChange
>;
export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap> {
public readonly reEmitter: TypedReEmitter<EmittedEvents, RoomEventHandlerMap>;
@@ -1068,6 +1086,32 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
if (previousCurrentState !== this.currentState) {
this.emit(RoomEvent.CurrentStateUpdated, this, previousCurrentState, this.currentState);
// Re-emit various events on the current room state
// TODO: If currentState really only exists for backwards
// compatibility, shouldn't we be doing this some other way?
this.reEmitter.stopReEmitting(previousCurrentState, [
RoomStateEvent.Events,
RoomStateEvent.Members,
RoomStateEvent.NewMember,
RoomStateEvent.Update,
RoomStateEvent.Marker,
BeaconEvent.New,
BeaconEvent.Update,
BeaconEvent.Destroy,
BeaconEvent.LivenessChange,
]);
this.reEmitter.reEmit(this.currentState, [
RoomStateEvent.Events,
RoomStateEvent.Members,
RoomStateEvent.NewMember,
RoomStateEvent.Update,
RoomStateEvent.Marker,
BeaconEvent.New,
BeaconEvent.Update,
BeaconEvent.Destroy,
BeaconEvent.LivenessChange,
]);
}
}
@@ -1937,14 +1981,6 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
}
}
}
if (event.getUnsigned().transaction_id) {
const existingEvent = this.txnToEvent[event.getUnsigned().transaction_id];
if (existingEvent) {
// remote echo of an event we sent earlier
this.handleRemoteEcho(event, existingEvent);
}
}
}
/**
@@ -1952,7 +1988,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
* "Room.timeline".
*
* @param {MatrixEvent} event Event to be added
* @param {IAddLiveEventOptions} options addLiveEvent options
* @param {IAddLiveEventOptions} addLiveEventOptions addLiveEvent options
* @fires module:client~MatrixClient#event:"Room.timeline"
* @private
*/
@@ -2300,7 +2336,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
fromCache = false,
): void {
let duplicateStrategy = duplicateStrategyOrOpts as DuplicateStrategy;
let timelineWasEmpty: boolean;
let timelineWasEmpty = false;
if (typeof (duplicateStrategyOrOpts) === 'object') {
({
duplicateStrategy,
@@ -2339,10 +2375,25 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
const threadRoots = this.findThreadRoots(events);
const eventsByThread: { [threadId: string]: MatrixEvent[] } = {};
const options: IAddLiveEventOptions = {
duplicateStrategy,
fromCache,
timelineWasEmpty,
};
for (const event of events) {
// TODO: We should have a filter to say "only add state event types X Y Z to the timeline".
this.processLiveEvent(event);
if (event.getUnsigned().transaction_id) {
const existingEvent = this.txnToEvent[event.getUnsigned().transaction_id!];
if (existingEvent) {
// remote echo of an event we sent earlier
this.handleRemoteEcho(event, existingEvent);
continue; // we can skip adding the event to the timeline sets, it is already there
}
}
const {
shouldLiveInRoom,
shouldLiveInThread,
@@ -2355,11 +2406,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
eventsByThread[threadId]?.push(event);
if (shouldLiveInRoom) {
this.addLiveEvent(event, {
duplicateStrategy,
fromCache,
timelineWasEmpty,
});
this.addLiveEvent(event, options);
}
}
@@ -2553,59 +2600,26 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
// some more intelligent manner or the client should just use timestamps
const timelineSet = this.getUnfilteredTimelineSet();
const publicReadReceipt = this.getReadReceiptForUserId(
userId,
ignoreSynthesized,
ReceiptType.Read,
);
const privateReadReceipt = this.getReadReceiptForUserId(
userId,
ignoreSynthesized,
ReceiptType.ReadPrivate,
);
const unstablePrivateReadReceipt = this.getReadReceiptForUserId(
userId,
ignoreSynthesized,
ReceiptType.UnstableReadPrivate,
);
const publicReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.Read);
const privateReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.ReadPrivate);
// If we have all, compare them
if (publicReadReceipt?.eventId && privateReadReceipt?.eventId && unstablePrivateReadReceipt?.eventId) {
const comparison1 = timelineSet.compareEventOrdering(
publicReadReceipt.eventId,
privateReadReceipt.eventId,
);
const comparison2 = timelineSet.compareEventOrdering(
publicReadReceipt.eventId,
unstablePrivateReadReceipt.eventId,
);
const comparison3 = timelineSet.compareEventOrdering(
privateReadReceipt.eventId,
unstablePrivateReadReceipt.eventId,
);
if (comparison1 && comparison2 && comparison3) {
return (comparison1 > 0)
? ((comparison2 > 0) ? publicReadReceipt.eventId : unstablePrivateReadReceipt.eventId)
: ((comparison3 > 0) ? privateReadReceipt.eventId : unstablePrivateReadReceipt.eventId);
}
// If we have both, compare them
let comparison: number | null | undefined;
if (publicReadReceipt?.eventId && privateReadReceipt?.eventId) {
comparison = timelineSet.compareEventOrdering(publicReadReceipt?.eventId, privateReadReceipt?.eventId);
}
let latest = privateReadReceipt;
[unstablePrivateReadReceipt, publicReadReceipt].forEach((receipt) => {
if (receipt?.data?.ts > latest?.data?.ts || !latest) {
latest = receipt;
}
});
if (latest?.eventId) return latest?.eventId;
// If we didn't get a comparison try to compare the ts of the receipts
if (!comparison && publicReadReceipt?.data?.ts && privateReadReceipt?.data?.ts) {
comparison = publicReadReceipt?.data?.ts - privateReadReceipt?.data?.ts;
}
// The more less likely it is for a read receipt to drift out of date
// the bigger is its precedence
return (
privateReadReceipt?.eventId ??
unstablePrivateReadReceipt?.eventId ??
publicReadReceipt?.eventId ??
null
);
// The public receipt is more likely to drift out of date so the private
// one has precedence
if (!comparison) return privateReadReceipt?.eventId ?? publicReadReceipt?.eventId ?? null;
// If public read receipt is older, return the private one
return ((comparison < 0) ? privateReadReceipt?.eventId : publicReadReceipt?.eventId) ?? null;
}
/**
@@ -2914,6 +2928,33 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
return this.getType() === RoomType.ElementVideo;
}
private roomNameGenerator(state: RoomNameState): string {
if (this.client.roomNameGenerator) {
const name = this.client.roomNameGenerator(this.roomId, state);
if (name !== null) {
return name;
}
}
switch (state.type) {
case RoomNameType.Actual:
return state.name;
case RoomNameType.Generated:
switch (state.subtype) {
case "Inviting":
return `Inviting ${memberNamesToRoomName(state.names, state.count)}`;
default:
return memberNamesToRoomName(state.names, state.count);
}
case RoomNameType.EmptyRoom:
if (state.oldName) {
return `Empty room (was ${state.oldName})`;
} else {
return "Empty room";
}
}
}
/**
* This is an internal method. Calculates the name of the room from the current
* room state.
@@ -2928,14 +2969,20 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
// check for an alias, if any. for now, assume first alias is the
// official one.
const mRoomName = this.currentState.getStateEvents(EventType.RoomName, "");
if (mRoomName && mRoomName.getContent() && mRoomName.getContent().name) {
return mRoomName.getContent().name;
if (mRoomName?.getContent().name) {
return this.roomNameGenerator({
type: RoomNameType.Actual,
name: mRoomName.getContent().name,
});
}
}
const alias = this.getCanonicalAlias();
if (alias) {
return alias;
return this.roomNameGenerator({
type: RoomNameType.Actual,
name: alias,
});
}
const joinedMemberCount = this.currentState.getJoinedMemberCount();
@@ -2967,8 +3014,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, 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 === "invite" || m.membership === "join");
});
otherMembers = otherMembers.filter(({ userId }) => {
// filter service members
@@ -2986,24 +3032,33 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
}
if (inviteJoinCount) {
return memberNamesToRoomName(otherNames, inviteJoinCount);
return this.roomNameGenerator({
type: RoomNameType.Generated,
names: otherNames,
count: inviteJoinCount,
});
}
const myMembership = this.getMyMembership();
// if I have created a room and invited people through
// 3rd party invites
if (myMembership == 'join') {
const thirdPartyInvites =
this.currentState.getStateEvents(EventType.RoomThirdPartyInvite);
const thirdPartyInvites = this.currentState.getStateEvents(EventType.RoomThirdPartyInvite);
if (thirdPartyInvites && thirdPartyInvites.length) {
if (thirdPartyInvites?.length) {
const thirdPartyNames = thirdPartyInvites.map((i) => {
return i.getContent().display_name;
});
return `Inviting ${memberNamesToRoomName(thirdPartyNames)}`;
return this.roomNameGenerator({
type: RoomNameType.Generated,
subtype: "Inviting",
names: thirdPartyNames,
count: thirdPartyNames.length + 1,
});
}
}
// let's try to figure out who was here before
let leftNames = otherNames;
// if we didn't have heroes, try finding them in the room state
@@ -3014,11 +3069,20 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
m.membership !== "join";
}).map((m) => m.name);
}
let oldName: string;
if (leftNames.length) {
return `Empty room (was ${memberNamesToRoomName(leftNames)})`;
} else {
return "Empty room";
oldName = this.roomNameGenerator({
type: RoomNameType.Generated,
names: leftNames,
count: leftNames.length + 1,
});
}
return this.roomNameGenerator({
type: RoomNameType.EmptyRoom,
oldName,
});
}
/**
@@ -3203,8 +3267,33 @@ const ALLOWED_TRANSITIONS: Record<EventStatus, EventStatus[]> = {
[EventStatus.CANCELLED]: [],
};
// TODO i18n
function memberNamesToRoomName(names: string[], count = (names.length + 1)) {
export enum RoomNameType {
EmptyRoom,
Generated,
Actual,
}
export interface EmptyRoomNameState {
type: RoomNameType.EmptyRoom;
oldName?: string;
}
export interface GeneratedRoomNameState {
type: RoomNameType.Generated;
subtype?: "Inviting";
names: string[];
count: number;
}
export interface ActualRoomNameState {
type: RoomNameType.Actual;
name: string;
}
export type RoomNameState = EmptyRoomNameState | GeneratedRoomNameState | ActualRoomNameState;
// Can be overriden by IMatrixClientCreateOpts::memberNamesToRoomNameFn
function memberNamesToRoomName(names: string[], count: number): string {
const countWithoutMe = count - 1;
if (!names.length) {
return "Empty room";
+2 -2
View File
@@ -335,7 +335,7 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
/**
* Return last reply to the thread, if known.
*/
public lastReply(matches: (ev: MatrixEvent) => boolean = () => true): Optional<MatrixEvent> {
public lastReply(matches: (ev: MatrixEvent) => boolean = () => true): MatrixEvent | null {
for (let i = this.events.length - 1; i >= 0; i--) {
const event = this.events[i];
if (matches(event)) {
@@ -384,7 +384,7 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
public async fetchEvents(opts: IRelationsRequestOpts = { limit: 20, direction: Direction.Backward }): Promise<{
originalEvent: MatrixEvent;
events: MatrixEvent[];
nextBatch?: string;
nextBatch?: string | null;
prevBatch?: string;
}> {
let {
+3 -7
View File
@@ -362,13 +362,9 @@ export class PushProcessor {
return false;
}
let regex;
if (cond.key == 'content.body') {
regex = this.createCachedRegex('(^|\\W)', cond.pattern, '(\\W|$)');
} else {
regex = this.createCachedRegex('^', cond.pattern, '$');
}
const regex = cond.key === 'content.body'
? this.createCachedRegex('(^|\\W)', cond.pattern, '(\\W|$)')
: this.createCachedRegex('^', cond.pattern, '$');
return !!val.match(regex);
}
+60 -62
View File
@@ -19,13 +19,11 @@ import { logger } from './logger';
import * as utils from "./utils";
import { EventTimeline } from "./models/event-timeline";
import { ClientEvent, IStoredClientOpts, MatrixClient, PendingEventOrdering } from "./client";
import { ISyncStateData, SyncState } from "./sync";
import { ISyncStateData, SyncState, _createAndReEmitRoom } from "./sync";
import { MatrixEvent } from "./models/event";
import { Crypto } from "./crypto";
import { IMinimalEvent, IRoomEvent, IStateEvent, IStrippedState } from "./sync-accumulator";
import { MatrixError } from "./http-api";
import { RoomStateEvent } from "./models/room-state";
import { RoomMemberEvent } from "./models/room-member";
import {
Extension,
ExtensionState,
@@ -290,18 +288,21 @@ export class SlidingSyncSdk {
logger.debug("initial flag not set but no stored room exists for room ", roomId, roomData);
return;
}
room = createRoom(this.client, roomId, this.opts);
room = _createAndReEmitRoom(this.client, roomId, this.opts);
}
this.processRoomData(this.client, room, roomData);
}
private onLifecycle(state: SlidingSyncState, resp: MSC3575SlidingSyncResponse, err?: Error): void {
private onLifecycle(state: SlidingSyncState, resp: MSC3575SlidingSyncResponse | null, err: Error | null): void {
if (err) {
logger.debug("onLifecycle", state, err);
}
switch (state) {
case SlidingSyncState.Complete:
this.purgeNotifications();
if (!resp) {
break;
}
// Element won't stop showing the initial loading spinner unless we fire SyncState.Prepared
if (!this.lastPos) {
this.updateSyncState(SyncState.Prepared, {
@@ -406,9 +407,51 @@ export class SlidingSyncSdk {
// this helps large account to speed up faster
// room::decryptCriticalEvent is in charge of decrypting all the events
// required for a client to function properly
const timelineEvents = mapEvents(this.client, room.roomId, roomData.timeline, false);
let timelineEvents = mapEvents(this.client, room.roomId, roomData.timeline, false);
const ephemeralEvents = []; // TODO this.mapSyncEventsFormat(joinObj.ephemeral);
// TODO: handle threaded / beacon events
if (roomData.initial) {
// we should not know about any of these timeline entries if this is a genuinely new room.
// If we do, then we've effectively done scrollback (e.g requesting timeline_limit: 1 for
// this room, then timeline_limit: 50).
const knownEvents = new Set<string>();
room.getLiveTimeline().getEvents().forEach((e) => {
knownEvents.add(e.getId());
});
// all unknown events BEFORE a known event must be scrollback e.g:
// D E <-- what we know
// A B C D E F <-- what we just received
// means:
// A B C <-- scrollback
// D E <-- dupes
// F <-- new event
// We bucket events based on if we have seen a known event yet.
const oldEvents: MatrixEvent[] = [];
const newEvents: MatrixEvent[] = [];
let seenKnownEvent = false;
for (let i = timelineEvents.length-1; i >= 0; i--) {
const recvEvent = timelineEvents[i];
if (knownEvents.has(recvEvent.getId())) {
seenKnownEvent = true;
continue; // don't include this event, it's a dupe
}
if (seenKnownEvent) {
// old -> new
oldEvents.push(recvEvent);
} else {
// old -> new
newEvents.unshift(recvEvent);
}
}
timelineEvents = newEvents;
if (oldEvents.length > 0) {
// old events are scrollback, insert them now
room.addEventsToTimeline(oldEvents, true, room.getLiveTimeline(), roomData.prev_batch);
}
}
const encrypted = this.client.isRoomEncrypted(room.roomId);
// we do this first so it's correct when any of the events fire
if (roomData.notification_count != null) {
@@ -432,6 +475,13 @@ export class SlidingSyncSdk {
}
}
if (Number.isInteger(roomData.invited_count)) {
room.currentState.setInvitedMemberCount(roomData.invited_count!);
}
if (Number.isInteger(roomData.joined_count)) {
room.currentState.setJoinedMemberCount(roomData.joined_count!);
}
if (roomData.invite_state) {
const inviteStateEvents = mapEvents(this.client, room.roomId, roomData.invite_state);
this.processRoomEvents(room, inviteStateEvents);
@@ -494,7 +544,6 @@ export class SlidingSyncSdk {
}
if (limited) {
deregisterStateListeners(room);
room.resetLiveTimeline(
roomData.prev_batch,
null, // TODO this.opts.canResetEntireTimeline(room.roomId) ? null : syncEventData.oldSyncToken,
@@ -504,7 +553,6 @@ export class SlidingSyncSdk {
// reason to stop incrementally tracking notifications and
// reset the timeline.
this.client.resetNotifTimelineSet();
registerStateListeners(this.client, room);
}
} */
@@ -513,6 +561,10 @@ export class SlidingSyncSdk {
// we deliberately don't add ephemeral events to the timeline
room.addEphemeralEvents(ephemeralEvents);
// local fields must be set before any async calls because call site assumes
// synchronous execution prior to emitting SlidingSyncState.Complete
room.updateMyMembership("join");
room.recalculate();
if (roomData.initial) {
client.store.storeRoom(room);
@@ -536,8 +588,6 @@ export class SlidingSyncSdk {
client.emit(ClientEvent.Event, e);
});
room.updateMyMembership("join");
// Decrypt only the last message in all rooms to make sure we can generate a preview
// And decrypt all events after the recorded read receipt to ensure an accurate
// notification count
@@ -774,58 +824,6 @@ function ensureNameEvent(client: MatrixClient, roomId: string, roomData: MSC3575
// Helper functions which set up JS SDK structs are below and are identical to the sync v2 counterparts,
// just outside the class.
function createRoom(client: MatrixClient, roomId: string, opts: Partial<IStoredClientOpts>): Room { // XXX cargoculted from sync.ts
const { timelineSupport } = client;
const room = new Room(roomId, client, client.getUserId(), {
lazyLoadMembers: opts.lazyLoadMembers,
pendingEventOrdering: opts.pendingEventOrdering,
timelineSupport,
});
client.reEmitter.reEmit(room, [
RoomEvent.Name,
RoomEvent.Redaction,
RoomEvent.RedactionCancelled,
RoomEvent.Receipt,
RoomEvent.Tags,
RoomEvent.LocalEchoUpdated,
RoomEvent.AccountData,
RoomEvent.MyMembership,
RoomEvent.Timeline,
RoomEvent.TimelineReset,
]);
registerStateListeners(client, room);
return room;
}
function registerStateListeners(client: MatrixClient, room: Room): void { // XXX cargoculted from sync.ts
// we need to also re-emit room state and room member events, so hook it up
// to the client now. We need to add a listener for RoomState.members in
// order to hook them correctly.
client.reEmitter.reEmit(room.currentState, [
RoomStateEvent.Events,
RoomStateEvent.Members,
RoomStateEvent.NewMember,
RoomStateEvent.Update,
]);
room.currentState.on(RoomStateEvent.NewMember, function(event, state, member) {
member.user = client.getUser(member.userId);
client.reEmitter.reEmit(member, [
RoomMemberEvent.Name,
RoomMemberEvent.Typing,
RoomMemberEvent.PowerLevel,
RoomMemberEvent.Membership,
]);
});
}
/*
function deregisterStateListeners(room: Room): void { // XXX cargoculted from sync.ts
// could do with a better way of achieving this.
room.currentState.removeAllListeners(RoomStateEvent.Events);
room.currentState.removeAllListeners(RoomStateEvent.Members);
room.currentState.removeAllListeners(RoomStateEvent.NewMember);
} */
function mapEvents(client: MatrixClient, roomId: string, events: object[], decrypt = true): MatrixEvent[] {
const mapper = client.getEventMapper({ decrypt });
return (events as Array<IStrippedState | IRoomEvent | IStateEvent | IMinimalEvent>).map(function(e) {
+158 -42
View File
@@ -19,7 +19,7 @@ import { IAbortablePromise } from "./@types/partials";
import { MatrixClient } from "./client";
import { IRoomEvent, IStateEvent } from "./sync-accumulator";
import { TypedEventEmitter } from "./models//typed-event-emitter";
import { sleep } from "./utils";
import { sleep, IDeferred, defer } from "./utils";
// /sync requests allow you to set a timeout= but the request may continue
// beyond that and wedge forever, so we need to track how long we are willing
@@ -47,6 +47,8 @@ export interface MSC3575Filter {
room_types?: string[];
not_room_types?: string[];
spaces?: string[];
tags?: string[];
not_tags?: string[];
}
/**
@@ -68,6 +70,7 @@ export interface MSC3575SlidingSyncRequest {
unsubscribe_rooms?: string[];
room_subscriptions?: Record<string, MSC3575RoomSubscription>;
extensions?: object;
txn_id?: string;
// query params
pos?: string;
@@ -81,6 +84,8 @@ export interface MSC3575RoomData {
timeline: (IRoomEvent | IStateEvent)[];
notification_count?: number;
highlight_count?: number;
joined_count?: number;
invited_count?: number;
invite_state?: IStateEvent[];
initial?: boolean;
limited?: boolean;
@@ -126,6 +131,7 @@ type Operation = DeleteOperation | InsertOperation | InvalidateOperation | SyncO
*/
export interface MSC3575SlidingSyncResponse {
pos: string;
txn_id?: string;
lists: ListResponse[];
rooms: Record<string, MSC3575RoomData>;
extensions: object;
@@ -316,7 +322,9 @@ export enum SlidingSyncEvent {
export type SlidingSyncEventHandlerMap = {
[SlidingSyncEvent.RoomData]: (roomId: string, roomData: MSC3575RoomData) => void;
[SlidingSyncEvent.Lifecycle]: (state: SlidingSyncState, resp: MSC3575SlidingSyncResponse, err: Error) => void;
[SlidingSyncEvent.Lifecycle]: (
state: SlidingSyncState, resp: MSC3575SlidingSyncResponse | null, err: Error | null,
) => void;
[SlidingSyncEvent.List]: (
listIndex: number, joinedCount: number, roomIndexToRoomId: Record<number, string>,
) => void;
@@ -334,6 +342,11 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
private terminated = false;
// flag set when resend() is called because we cannot rely on detecting AbortError in JS SDK :(
private needsResend = false;
// the txn_id to send with the next request.
private txnId?: string = null;
// a list (in chronological order of when they were sent) of objects containing the txn ID and
// a defer to resolve/reject depending on whether they were successfully sent or not.
private txnIdDefers: (IDeferred<string> & { txnId: string})[] = [];
// map of extension name to req/resp handler
private extensions: Record<string, Extension> = {};
@@ -403,10 +416,13 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
* whereas setList always will.
* @param index The list index to modify
* @param ranges The new ranges to apply.
* @return A promise which resolves to the transaction ID when it has been received down sync
* (or rejects with the transaction ID if the action was not applied e.g the request was cancelled
* immediately after sending, in which case the action will be applied in the subsequent request)
*/
public setListRanges(index: number, ranges: number[][]): void {
public setListRanges(index: number, ranges: number[][]): Promise<string> {
this.lists[index].updateListRange(ranges);
this.resend();
return this.resend();
}
/**
@@ -414,15 +430,18 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
* lists.
* @param index The index to modify
* @param list The new list parameters.
* @return A promise which resolves to the transaction ID when it has been received down sync
* (or rejects with the transaction ID if the action was not applied e.g the request was cancelled
* immediately after sending, in which case the action will be applied in the subsequent request)
*/
public setList(index: number, list: MSC3575List): void {
public setList(index: number, list: MSC3575List): Promise<string> {
if (this.lists[index]) {
this.lists[index].replaceList(list);
} else {
this.lists[index] = new SlidingList(list);
}
this.listModifiedCount += 1;
this.resend();
return this.resend();
}
/**
@@ -438,21 +457,27 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
* /sync request to resend new subscriptions. If the /sync stream has not started, this will
* prepare the room subscriptions for when start() is called.
* @param s The new desired room subscriptions.
* @return A promise which resolves to the transaction ID when it has been received down sync
* (or rejects with the transaction ID if the action was not applied e.g the request was cancelled
* immediately after sending, in which case the action will be applied in the subsequent request)
*/
public modifyRoomSubscriptions(s: Set<string>) {
public modifyRoomSubscriptions(s: Set<string>): Promise<string> {
this.desiredRoomSubscriptions = s;
this.resend();
return this.resend();
}
/**
* Modify which events to retrieve for room subscriptions. Invalidates all room subscriptions
* such that they will be sent up afresh.
* @param rs The new room subscription fields to fetch.
* @return A promise which resolves to the transaction ID when it has been received down sync
* (or rejects with the transaction ID if the action was not applied e.g the request was cancelled
* immediately after sending, in which case the action will be applied in the subsequent request)
*/
public modifyRoomSubscriptionInfo(rs: MSC3575RoomSubscription): void {
public modifyRoomSubscriptionInfo(rs: MSC3575RoomSubscription): Promise<string> {
this.roomSubscriptionInfo = rs;
this.confirmedRoomSubscriptions = new Set<string>();
this.resend();
return this.resend();
}
/**
@@ -511,6 +536,65 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
this.emit(SlidingSyncEvent.Lifecycle, state, resp, err);
}
private shiftRight(listIndex: number, hi: number, low: number) {
// l h
// 0,1,2,3,4 <- before
// 0,1,2,2,3 <- after, hi is deleted and low is duplicated
for (let i = hi; i > low; i--) {
if (this.lists[listIndex].isIndexInRange(i)) {
this.lists[listIndex].roomIndexToRoomId[i] =
this.lists[listIndex].roomIndexToRoomId[
i - 1
];
}
}
}
private shiftLeft(listIndex: number, hi: number, low: number) {
// l h
// 0,1,2,3,4 <- before
// 0,1,3,4,4 <- after, low is deleted and hi is duplicated
for (let i = low; i < hi; i++) {
if (this.lists[listIndex].isIndexInRange(i)) {
this.lists[listIndex].roomIndexToRoomId[i] =
this.lists[listIndex].roomIndexToRoomId[
i + 1
];
}
}
}
private removeEntry(listIndex: number, index: number) {
// work out the max index
let max = -1;
for (const n in this.lists[listIndex].roomIndexToRoomId) {
if (Number(n) > max) {
max = Number(n);
}
}
if (max < 0 || index > max) {
return;
}
// Everything higher than the gap needs to be shifted left.
this.shiftLeft(listIndex, max, index);
delete this.lists[listIndex].roomIndexToRoomId[max];
}
private addEntry(listIndex: number, index: number) {
// work out the max index
let max = -1;
for (const n in this.lists[listIndex].roomIndexToRoomId) {
if (Number(n) > max) {
max = Number(n);
}
}
if (max < 0 || index > max) {
return;
}
// Everything higher than the gap needs to be shifted right, +1 so we don't delete the highest element
this.shiftRight(listIndex, max+1, index);
}
private processListOps(list: ListResponse, listIndex: number): void {
let gapIndex = -1;
list.ops.forEach((op: Operation) => {
@@ -518,6 +602,10 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
case "DELETE": {
logger.debug("DELETE", listIndex, op.index, ";");
delete this.lists[listIndex].roomIndexToRoomId[op.index];
if (gapIndex !== -1) {
// we already have a DELETE operation to process, so process it.
this.removeEntry(listIndex, gapIndex);
}
gapIndex = op.index;
break;
}
@@ -532,20 +620,9 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
if (this.lists[listIndex].roomIndexToRoomId[op.index]) {
// something is in this space, shift items out of the way
if (gapIndex < 0) {
logger.debug(
"cannot work out where gap is, INSERT without previous DELETE! List: ",
listIndex,
);
return;
}
// 0,1,2,3 index
// [A,B,C,D]
// DEL 3
// [A,B,C,_]
// INSERT E 0
// [E,A,B,C]
// gapIndex=3, op.index=0
if (gapIndex > op.index) {
// we haven't been told where to shift from, so make way for a new room entry.
this.addEntry(listIndex, op.index);
} else if (gapIndex > op.index) {
// the gap is further down the list, shift every element to the right
// starting at the gap so we can just shift each element in turn:
// [A,B,C,_] gapIndex=3, op.index=0
@@ -553,26 +630,13 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
// [A,B,B,C] i=2
// [A,A,B,C] i=1
// Terminate. We'll assign into op.index next.
for (let i = gapIndex; i > op.index; i--) {
if (this.lists[listIndex].isIndexInRange(i)) {
this.lists[listIndex].roomIndexToRoomId[i] =
this.lists[listIndex].roomIndexToRoomId[
i - 1
];
}
}
this.shiftRight(listIndex, gapIndex, op.index);
} else if (gapIndex < op.index) {
// the gap is further up the list, shift every element to the left
// starting at the gap so we can just shift each element in turn
for (let i = gapIndex; i < op.index; i++) {
if (this.lists[listIndex].isIndexInRange(i)) {
this.lists[listIndex].roomIndexToRoomId[i] =
this.lists[listIndex].roomIndexToRoomId[
i + 1
];
}
}
this.shiftLeft(listIndex, op.index, gapIndex);
}
gapIndex = -1; // forget the gap, we don't need it anymore.
}
this.lists[listIndex].roomIndexToRoomId[op.index] = op.room_id;
break;
@@ -612,14 +676,60 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
}
}
});
if (gapIndex !== -1) {
// we already have a DELETE operation to process, so process it
// Everything higher than the gap needs to be shifted left.
this.removeEntry(listIndex, gapIndex);
}
}
/**
* Resend a Sliding Sync request. Used when something has changed in the request.
* Resend a Sliding Sync request. Used when something has changed in the request. Resolves with
* the transaction ID of this request on success. Rejects with the transaction ID of this request
* on failure.
*/
public resend(): void {
public resend(): Promise<string> {
if (this.needsResend && this.txnIdDefers.length > 0) {
// we already have a resend queued, so just return the same promise
return this.txnIdDefers[this.txnIdDefers.length-1].promise;
}
this.needsResend = true;
this.txnId = this.client.makeTxnId();
const d = defer<string>();
this.txnIdDefers.push({
...d,
txnId: this.txnId,
});
this.pendingReq?.abort();
return d.promise;
}
private resolveTransactionDefers(txnId?: string) {
if (!txnId) {
return;
}
// find the matching index
let txnIndex = -1;
for (let i = 0; i < this.txnIdDefers.length; i++) {
if (this.txnIdDefers[i].txnId === txnId) {
txnIndex = i;
break;
}
}
if (txnIndex === -1) {
// this shouldn't happen; we shouldn't be seeing txn_ids for things we don't know about,
// whine about it.
logger.warn(`resolveTransactionDefers: seen ${txnId} but it isn't a pending txn, ignoring.`);
return;
}
// This list is sorted in time, so if the input txnId ACKs in the middle of this array,
// then everything before it that hasn't been ACKed yet never will and we should reject them.
for (let i = 0; i < txnIndex; i++) {
this.txnIdDefers[i].reject(this.txnIdDefers[i].txnId);
}
this.txnIdDefers[txnIndex].resolve(txnId);
// clear out settled promises, incuding the one we resolved.
this.txnIdDefers = this.txnIdDefers.slice(txnIndex+1);
}
/**
@@ -666,6 +776,10 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
reqBody.room_subscriptions[roomId] = this.roomSubscriptionInfo;
}
}
if (this.txnId) {
reqBody.txn_id = this.txnId;
this.txnId = null;
}
this.pendingReq = this.client.slidingSync(reqBody, this.proxyBaseUrl);
resp = await this.pendingReq;
logger.debug(resp);
@@ -747,6 +861,8 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
i, this.lists[i].joinedCount, Object.assign({}, this.lists[i].roomIndexToRoomId),
);
});
this.resolveTransactionDefers(resp.txn_id);
}
}
}
+332 -356
View File
@@ -1,5 +1,5 @@
/*
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2015 - 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -23,6 +23,8 @@ limitations under the License.
* for HTTP and WS at some point.
*/
import { Optional } from "matrix-events-sdk";
import { User, UserEvent } from "./models/user";
import { NotificationCountType, Room, RoomEvent } from "./models/room";
import * as utils from "./utils";
@@ -51,7 +53,7 @@ import { MatrixError, Method } from "./http-api";
import { ISavedSync } from "./store";
import { EventType } from "./@types/event";
import { IPushRules } from "./@types/PushRules";
import { RoomState, RoomStateEvent, IMarkerFoundOptions } from "./models/room-state";
import { RoomStateEvent, IMarkerFoundOptions } from "./models/room-state";
import { RoomMemberEvent } from "./models/room-member";
import { BeaconEvent } from "./models/beacon";
import { IEventsResponse } from "./@types/requests";
@@ -100,18 +102,16 @@ const MSC2716_ROOM_VERSIONS = [
function getFilterName(userId: string, suffix?: string): string {
// scope this on the user ID because people may login on many accounts
// and they all need to be stored!
return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : "");
return `FILTER_SYNC_${userId}` + (suffix ? "_" + suffix : "");
}
function debuglog(...params) {
if (!DEBUG) {
return;
}
if (!DEBUG) return;
logger.log(...params);
}
interface ISyncOptions {
filterId?: string;
filter?: string;
hasSyncedBefore?: boolean;
}
@@ -161,14 +161,14 @@ type WrappedRoom<T> = T & {
* updating presence.
*/
export class SyncApi {
private _peekRoom: Room = null;
private currentSyncRequest: IAbortablePromise<ISyncResponse> = null;
private syncState: SyncState = null;
private syncStateData: ISyncStateData = null; // additional data (eg. error object for failed sync)
private _peekRoom: Optional<Room> = null;
private currentSyncRequest: Optional<IAbortablePromise<ISyncResponse>> = null;
private syncState: Optional<SyncState> = null;
private syncStateData: Optional<ISyncStateData> = null; // additional data (eg. error object for failed sync)
private catchingUp = false;
private running = false;
private keepAliveTimer: ReturnType<typeof setTimeout> = null;
private connectionReturnedDefer: IDeferred<boolean> = null;
private keepAliveTimer: Optional<ReturnType<typeof setTimeout>> = null;
private connectionReturnedDefer: Optional<IDeferred<boolean>> = null;
private notifEvents: MatrixEvent[] = []; // accumulator of sync events in the current sync response
private failedSyncCount = 0; // Number of consecutive failed /sync requests
private storeIsInvalid = false; // flag set if the store needs to be cleared before we can start
@@ -199,85 +199,13 @@ export class SyncApi {
* @return {Room}
*/
public createRoom(roomId: string): Room {
const client = this.client;
const {
timelineSupport,
} = client;
const room = new Room(roomId, client, client.getUserId(), {
lazyLoadMembers: this.opts.lazyLoadMembers,
pendingEventOrdering: this.opts.pendingEventOrdering,
timelineSupport,
});
client.reEmitter.reEmit(room, [
RoomEvent.Name,
RoomEvent.Redaction,
RoomEvent.RedactionCancelled,
RoomEvent.Receipt,
RoomEvent.Tags,
RoomEvent.LocalEchoUpdated,
RoomEvent.AccountData,
RoomEvent.MyMembership,
RoomEvent.Timeline,
RoomEvent.TimelineReset,
]);
this.registerStateListeners(room);
// Register listeners again after the state reference changes
room.on(RoomEvent.CurrentStateUpdated, (targetRoom, previousCurrentState) => {
if (targetRoom !== room) {
return;
}
const room = _createAndReEmitRoom(this.client, roomId, this.opts);
this.deregisterStateListeners(previousCurrentState);
this.registerStateListeners(room);
});
return room;
}
/**
* @param {Room} room
* @private
*/
private registerStateListeners(room: Room): void {
const client = this.client;
// we need to also re-emit room state and room member events, so hook it up
// to the client now. We need to add a listener for RoomState.members in
// order to hook them correctly. (TODO: find a better way?)
client.reEmitter.reEmit(room.currentState, [
RoomStateEvent.Events,
RoomStateEvent.Members,
RoomStateEvent.NewMember,
RoomStateEvent.Update,
BeaconEvent.New,
BeaconEvent.Update,
BeaconEvent.Destroy,
BeaconEvent.LivenessChange,
]);
room.currentState.on(RoomStateEvent.NewMember, function(event, state, member) {
member.user = client.getUser(member.userId);
client.reEmitter.reEmit(member, [
RoomMemberEvent.Name,
RoomMemberEvent.Typing,
RoomMemberEvent.PowerLevel,
RoomMemberEvent.Membership,
]);
});
room.currentState.on(RoomStateEvent.Marker, (markerEvent, markerFoundOptions) => {
room.on(RoomStateEvent.Marker, (markerEvent, markerFoundOptions) => {
this.onMarkerStateEvent(room, markerEvent, markerFoundOptions);
});
}
/**
* @param {RoomState} roomState The roomState to clear listeners from
* @private
*/
private deregisterStateListeners(roomState: RoomState): void {
// could do with a better way of achieving this.
roomState.removeAllListeners(RoomStateEvent.Events);
roomState.removeAllListeners(RoomStateEvent.Members);
roomState.removeAllListeners(RoomStateEvent.NewMember);
roomState.removeAllListeners(RoomStateEvent.Marker);
return room;
}
/** When we see the marker state change in the room, we know there is some
@@ -286,7 +214,7 @@ export class SyncApi {
* historical messages are shown when we paginate `/messages` again.
* @param {Room} room The room where the marker event was sent
* @param {MatrixEvent} markerEvent The new marker event
* @param {ISetStateOptions} setStateOptions When `timelineWasEmpty` is set
* @param {IMarkerFoundOptions} setStateOptions When `timelineWasEmpty` is set
* as `true`, the given marker event will be ignored
*/
private onMarkerStateEvent(
@@ -439,7 +367,7 @@ export class SyncApi {
// XXX: copypasted from /sync until we kill off this minging v1 API stuff)
// handle presence events (User objects)
if (response.presence && Array.isArray(response.presence)) {
if (Array.isArray(response.presence)) {
response.presence.map(client.getEventMapper()).forEach(
function(presenceEvent) {
let user = client.store.getUser(presenceEvent.getContent().user_id);
@@ -614,20 +542,135 @@ export class SyncApi {
return false;
}
private getPushRules = async () => {
try {
debuglog("Getting push rules...");
const result = await this.client.getPushRules();
debuglog("Got push rules");
this.client.pushRules = result;
} catch (err) {
logger.error("Getting push rules failed", err);
if (this.shouldAbortSync(err)) return;
// wait for saved sync to complete before doing anything else,
// otherwise the sync state will end up being incorrect
debuglog("Waiting for saved sync before retrying push rules...");
await this.recoverFromSyncStartupError(this.savedSyncPromise, err);
return this.getPushRules(); // try again
}
};
private buildDefaultFilter = () => {
return new Filter(this.client.credentials.userId);
};
private checkLazyLoadStatus = async () => {
debuglog("Checking lazy load status...");
if (this.opts.lazyLoadMembers && this.client.isGuest()) {
this.opts.lazyLoadMembers = false;
}
if (this.opts.lazyLoadMembers) {
debuglog("Checking server lazy load support...");
const supported = await this.client.doesServerSupportLazyLoading();
if (supported) {
debuglog("Enabling lazy load on sync filter...");
if (!this.opts.filter) {
this.opts.filter = this.buildDefaultFilter();
}
this.opts.filter.setLazyLoadMembers(true);
} else {
debuglog("LL: lazy loading requested but not supported " +
"by server, so disabling");
this.opts.lazyLoadMembers = false;
}
}
// need to vape the store when enabling LL and wasn't enabled before
debuglog("Checking whether lazy loading has changed in store...");
const shouldClear = await this.wasLazyLoadingToggled(this.opts.lazyLoadMembers);
if (shouldClear) {
this.storeIsInvalid = true;
const reason = InvalidStoreError.TOGGLED_LAZY_LOADING;
const error = new InvalidStoreError(reason, !!this.opts.lazyLoadMembers);
this.updateSyncState(SyncState.Error, { error });
// bail out of the sync loop now: the app needs to respond to this error.
// we leave the state as 'ERROR' which isn't great since this normally means
// we're retrying. The client must be stopped before clearing the stores anyway
// so the app should stop the client, clear the store and start it again.
logger.warn("InvalidStoreError: store is not usable: stopping sync.");
return;
}
if (this.opts.lazyLoadMembers) {
this.opts.crypto?.enableLazyLoading();
}
try {
debuglog("Storing client options...");
await this.client.storeClientOptions();
debuglog("Stored client options");
} catch (err) {
logger.error("Storing client options failed", err);
throw err;
}
};
private getFilter = async (): Promise<{
filterId?: string;
filter?: Filter;
}> => {
debuglog("Getting filter...");
let filter: Filter;
if (this.opts.filter) {
filter = this.opts.filter;
} else {
filter = this.buildDefaultFilter();
}
let filterId: string;
try {
filterId = await this.client.getOrCreateFilter(getFilterName(this.client.credentials.userId), filter);
} catch (err) {
logger.error("Getting filter failed", err);
if (this.shouldAbortSync(err)) return {};
// wait for saved sync to complete before doing anything else,
// otherwise the sync state will end up being incorrect
debuglog("Waiting for saved sync before retrying filter...");
await this.recoverFromSyncStartupError(this.savedSyncPromise, err);
return this.getFilter(); // try again
}
return { filter, filterId };
};
private savedSyncPromise: Promise<void>;
/**
* Main entry point
*/
public sync(): void {
const client = this.client;
public async sync(): Promise<void> {
this.running = true;
if (global.window && global.window.addEventListener) {
global.window.addEventListener("online", this.onOnline, false);
global.window?.addEventListener?.("online", this.onOnline, false);
if (this.client.isGuest()) {
// no push rules for guests, no access to POST filter for guests.
return this.doSync({});
}
let savedSyncPromise = Promise.resolve();
let savedSyncToken = null;
// Pull the saved sync token out first, before the worker starts sending
// all the sync data which could take a while. This will let us send our
// first incremental sync request before we've processed our saved data.
debuglog("Getting saved sync token...");
const savedSyncTokenPromise = this.client.store.getSavedSyncToken().then(tok => {
debuglog("Got saved sync token");
return tok;
});
this.savedSyncPromise = this.client.store.getSavedSync().then((savedSync) => {
debuglog(`Got reply from saved sync, exists? ${!!savedSync}`);
if (savedSync) {
return this.syncFromCache(savedSync);
}
}).catch(err => {
logger.error("Getting saved sync failed", err);
});
// We need to do one-off checks before we can begin the /sync loop.
// These are:
@@ -637,149 +680,45 @@ export class SyncApi {
// 3) We need to check the lazy loading option matches what was used in the
// stored sync. If it doesn't, we can't use the stored sync.
const getPushRules = async () => {
try {
debuglog("Getting push rules...");
const result = await client.getPushRules();
debuglog("Got push rules");
// Now start the first incremental sync request: this can also
// take a while so if we set it going now, we can wait for it
// to finish while we process our saved sync data.
await this.getPushRules();
await this.checkLazyLoadStatus();
const { filterId, filter } = await this.getFilter();
if (!filter) return; // bail, getFilter failed
client.pushRules = result;
} catch (err) {
logger.error("Getting push rules failed", err);
if (this.shouldAbortSync(err)) return;
// wait for saved sync to complete before doing anything else,
// otherwise the sync state will end up being incorrect
debuglog("Waiting for saved sync before retrying push rules...");
await this.recoverFromSyncStartupError(savedSyncPromise, err);
getPushRules();
return;
}
checkLazyLoadStatus(); // advance to the next stage
};
// reset the notifications timeline to prepare it to paginate from
// the current point in time.
// The right solution would be to tie /sync pagination tokens into
// /notifications API somehow.
this.client.resetNotifTimelineSet();
const buildDefaultFilter = () => {
const filter = new Filter(client.credentials.userId);
filter.setTimelineLimit(this.opts.initialSyncLimit);
return filter;
};
if (this.currentSyncRequest === null) {
let firstSyncFilter = filterId;
const savedSyncToken = await savedSyncTokenPromise;
const checkLazyLoadStatus = async () => {
debuglog("Checking lazy load status...");
if (this.opts.lazyLoadMembers && client.isGuest()) {
this.opts.lazyLoadMembers = false;
}
if (this.opts.lazyLoadMembers) {
debuglog("Checking server lazy load support...");
const supported = await client.doesServerSupportLazyLoading();
if (supported) {
debuglog("Enabling lazy load on sync filter...");
if (!this.opts.filter) {
this.opts.filter = buildDefaultFilter();
}
this.opts.filter.setLazyLoadMembers(true);
} else {
debuglog("LL: lazy loading requested but not supported " +
"by server, so disabling");
this.opts.lazyLoadMembers = false;
}
}
// need to vape the store when enabling LL and wasn't enabled before
debuglog("Checking whether lazy loading has changed in store...");
const shouldClear = await this.wasLazyLoadingToggled(this.opts.lazyLoadMembers);
if (shouldClear) {
this.storeIsInvalid = true;
const reason = InvalidStoreError.TOGGLED_LAZY_LOADING;
const error = new InvalidStoreError(reason, !!this.opts.lazyLoadMembers);
this.updateSyncState(SyncState.Error, { error });
// bail out of the sync loop now: the app needs to respond to this error.
// we leave the state as 'ERROR' which isn't great since this normally means
// we're retrying. The client must be stopped before clearing the stores anyway
// so the app should stop the client, clear the store and start it again.
logger.warn("InvalidStoreError: store is not usable: stopping sync.");
return;
}
if (this.opts.lazyLoadMembers && this.opts.crypto) {
this.opts.crypto.enableLazyLoading();
}
try {
debuglog("Storing client options...");
await this.client.storeClientOptions();
debuglog("Stored client options");
} catch (err) {
logger.error("Storing client options failed", err);
throw err;
}
getFilter(); // Now get the filter and start syncing
};
const getFilter = async () => {
debuglog("Getting filter...");
let filter;
if (this.opts.filter) {
filter = this.opts.filter;
} else {
filter = buildDefaultFilter();
}
let filterId;
try {
filterId = await client.getOrCreateFilter(getFilterName(client.credentials.userId), filter);
} catch (err) {
logger.error("Getting filter failed", err);
if (this.shouldAbortSync(err)) return;
// wait for saved sync to complete before doing anything else,
// otherwise the sync state will end up being incorrect
debuglog("Waiting for saved sync before retrying filter...");
await this.recoverFromSyncStartupError(savedSyncPromise, err);
getFilter();
return;
}
// reset the notifications timeline to prepare it to paginate from
// the current point in time.
// The right solution would be to tie /sync pagination tokens into
// /notifications API somehow.
client.resetNotifTimelineSet();
if (this.currentSyncRequest === null) {
// Send this first sync request here so we can then wait for the saved
// sync data to finish processing before we process the results of this one.
if (savedSyncToken) {
debuglog("Sending first sync request...");
this.currentSyncRequest = this.doSyncRequest({ filterId }, savedSyncToken);
} else {
debuglog("Sending initial sync request...");
const initialFilter = this.buildDefaultFilter();
initialFilter.setDefinition(filter.getDefinition());
initialFilter.setTimelineLimit(this.opts.initialSyncLimit);
// Use an inline filter, no point uploading it for a single usage
firstSyncFilter = JSON.stringify(initialFilter.getDefinition());
}
// Now wait for the saved sync to finish...
debuglog("Waiting for saved sync before starting sync processing...");
await savedSyncPromise;
this.doSync({ filterId });
};
if (client.isGuest()) {
// no push rules for guests, no access to POST filter for guests.
this.doSync({});
} else {
// Pull the saved sync token out first, before the worker starts sending
// all the sync data which could take a while. This will let us send our
// first incremental sync request before we've processed our saved data.
debuglog("Getting saved sync token...");
savedSyncPromise = client.store.getSavedSyncToken().then((tok) => {
debuglog("Got saved sync token");
savedSyncToken = tok;
debuglog("Getting saved sync...");
return client.store.getSavedSync();
}).then((savedSync) => {
debuglog(`Got reply from saved sync, exists? ${!!savedSync}`);
if (savedSync) {
return this.syncFromCache(savedSync);
}
}).catch(err => {
logger.error("Getting saved sync failed", err);
});
// Now start the first incremental sync request: this can also
// take a while so if we set it going now, we can wait for it
// to finish while we process our saved sync data.
getPushRules();
// Send this first sync request here so we can then wait for the saved
// sync data to finish processing before we process the results of this one.
this.currentSyncRequest = this.doSyncRequest({ filter: firstSyncFilter }, savedSyncToken);
}
// Now wait for the saved sync to finish...
debuglog("Waiting for saved sync before starting sync processing...");
await this.savedSyncPromise;
// process the first sync request and continue syncing with the normal filterId
return this.doSync({ filter: filterId });
}
/**
@@ -791,9 +730,7 @@ export class SyncApi {
// global.window AND global.window.removeEventListener.
// Some platforms (e.g. React Native) register global.window,
// but do not have global.window.removeEventListener.
if (global.window && global.window.removeEventListener) {
global.window.removeEventListener("online", this.onOnline, false);
}
global.window?.removeEventListener?.("online", this.onOnline, false);
this.running = false;
this.currentSyncRequest?.abort();
if (this.keepAliveTimer) {
@@ -828,8 +765,7 @@ export class SyncApi {
this.client.store.setSyncToken(nextSyncToken);
// No previous sync, set old token to null
const syncEventData = {
oldSyncToken: null,
const syncEventData: ISyncStateData = {
nextSyncToken,
catchingUp: false,
fromCache: true,
@@ -864,7 +800,91 @@ export class SyncApi {
* @param {boolean} syncOptions.hasSyncedBefore
*/
private async doSync(syncOptions: ISyncOptions): Promise<void> {
const client = this.client;
while (this.running) {
const syncToken = this.client.store.getSyncToken();
let data: ISyncResponse;
try {
//debuglog('Starting sync since=' + syncToken);
if (this.currentSyncRequest === null) {
this.currentSyncRequest = this.doSyncRequest(syncOptions, syncToken);
}
data = await this.currentSyncRequest;
} catch (e) {
const abort = await this.onSyncError(e);
if (abort) return;
continue;
} finally {
this.currentSyncRequest = null;
}
//debuglog('Completed sync, next_batch=' + data.next_batch);
// set the sync token NOW *before* processing the events. We do this so
// if something barfs on an event we can skip it rather than constantly
// polling with the same token.
this.client.store.setSyncToken(data.next_batch);
// Reset after a successful sync
this.failedSyncCount = 0;
await this.client.store.setSyncData(data);
const syncEventData = {
oldSyncToken: syncToken,
nextSyncToken: data.next_batch,
catchingUp: this.catchingUp,
};
if (this.opts.crypto) {
// tell the crypto module we're about to process a sync
// response
await this.opts.crypto.onSyncWillProcess(syncEventData);
}
try {
await this.processSyncResponse(syncEventData, data);
} catch (e) {
// log the exception with stack if we have it, else fall back
// to the plain description
logger.error("Caught /sync error", e);
// Emit the exception for client handling
this.client.emit(ClientEvent.SyncUnexpectedError, e);
}
// update this as it may have changed
syncEventData.catchingUp = this.catchingUp;
// emit synced events
if (!syncOptions.hasSyncedBefore) {
this.updateSyncState(SyncState.Prepared, syncEventData);
syncOptions.hasSyncedBefore = true;
}
// tell the crypto module to do its processing. It may block (to do a
// /keys/changes request).
if (this.opts.crypto) {
await this.opts.crypto.onSyncCompleted(syncEventData);
}
// keep emitting SYNCING -> SYNCING for clients who want to do bulk updates
this.updateSyncState(SyncState.Syncing, syncEventData);
if (this.client.store.wantsSave()) {
// We always save the device list (if it's dirty) before saving the sync data:
// this means we know the saved device list data is at least as fresh as the
// stored sync data which means we don't have to worry that we may have missed
// device changes. We can also skip the delay since we're not calling this very
// frequently (and we don't really want to delay the sync for it).
if (this.opts.crypto) {
await this.opts.crypto.saveDeviceList(0);
}
// tell databases that everything is now in a consistent state and can be saved.
this.client.store.save();
}
}
if (!this.running) {
debuglog("Sync no longer running: exiting.");
@@ -873,94 +893,7 @@ export class SyncApi {
this.connectionReturnedDefer = null;
}
this.updateSyncState(SyncState.Stopped);
return;
}
const syncToken = client.store.getSyncToken();
let data;
try {
//debuglog('Starting sync since=' + syncToken);
if (this.currentSyncRequest === null) {
this.currentSyncRequest = this.doSyncRequest(syncOptions, syncToken);
}
data = await this.currentSyncRequest;
} catch (e) {
this.onSyncError(e, syncOptions);
return;
} finally {
this.currentSyncRequest = null;
}
//debuglog('Completed sync, next_batch=' + data.next_batch);
// set the sync token NOW *before* processing the events. We do this so
// if something barfs on an event we can skip it rather than constantly
// polling with the same token.
client.store.setSyncToken(data.next_batch);
// Reset after a successful sync
this.failedSyncCount = 0;
await client.store.setSyncData(data);
const syncEventData = {
oldSyncToken: syncToken,
nextSyncToken: data.next_batch,
catchingUp: this.catchingUp,
};
if (this.opts.crypto) {
// tell the crypto module we're about to process a sync
// response
await this.opts.crypto.onSyncWillProcess(syncEventData);
}
try {
await this.processSyncResponse(syncEventData, data);
} catch (e) {
// log the exception with stack if we have it, else fall back
// to the plain description
logger.error("Caught /sync error", e);
// Emit the exception for client handling
this.client.emit(ClientEvent.SyncUnexpectedError, e);
}
// update this as it may have changed
syncEventData.catchingUp = this.catchingUp;
// emit synced events
if (!syncOptions.hasSyncedBefore) {
this.updateSyncState(SyncState.Prepared, syncEventData);
syncOptions.hasSyncedBefore = true;
}
// tell the crypto module to do its processing. It may block (to do a
// /keys/changes request).
if (this.opts.crypto) {
await this.opts.crypto.onSyncCompleted(syncEventData);
}
// keep emitting SYNCING -> SYNCING for clients who want to do bulk updates
this.updateSyncState(SyncState.Syncing, syncEventData);
if (client.store.wantsSave()) {
// We always save the device list (if it's dirty) before saving the sync data:
// this means we know the saved device list data is at least as fresh as the
// stored sync data which means we don't have to worry that we may have missed
// device changes. We can also skip the delay since we're not calling this very
// frequently (and we don't really want to delay the sync for it).
if (this.opts.crypto) {
await this.opts.crypto.saveDeviceList(0);
}
// tell databases that everything is now in a consistent state and can be saved.
client.store.save();
}
// Begin next sync
this.doSync(syncOptions);
}
private doSyncRequest(syncOptions: ISyncOptions, syncToken: string): IAbortablePromise<ISyncResponse> {
@@ -974,7 +907,7 @@ export class SyncApi {
private getSyncParams(syncOptions: ISyncOptions, syncToken: string): ISyncParams {
let pollTimeout = this.opts.pollTimeout;
if (this.getSyncState() !== 'SYNCING' || this.catchingUp) {
if (this.getSyncState() !== SyncState.Syncing || this.catchingUp) {
// unless we are happily syncing already, we want the server to return
// as quickly as possible, even if there are no events queued. This
// serves two purposes:
@@ -990,13 +923,13 @@ export class SyncApi {
pollTimeout = 0;
}
let filterId = syncOptions.filterId;
if (this.client.isGuest() && !filterId) {
filterId = this.getGuestFilter();
let filter = syncOptions.filter;
if (this.client.isGuest() && !filter) {
filter = this.getGuestFilter();
}
const qps: ISyncParams = {
filter: filterId,
filter,
timeout: pollTimeout,
};
@@ -1013,7 +946,7 @@ export class SyncApi {
qps._cacheBuster = Date.now();
}
if (this.getSyncState() == 'ERROR' || this.getSyncState() == 'RECONNECTING') {
if ([SyncState.Reconnecting, SyncState.Error].includes(this.getSyncState())) {
// we think the connection is dead. If it comes back up, we won't know
// about it till /sync returns. If the timeout= is high, this could
// be a long time. Set it to 0 when doing retries so we don't have to wait
@@ -1024,7 +957,7 @@ export class SyncApi {
return qps;
}
private onSyncError(err: MatrixError, syncOptions: ISyncOptions): void {
private async onSyncError(err: MatrixError): Promise<boolean> {
if (!this.running) {
debuglog("Sync no longer running: exiting");
if (this.connectionReturnedDefer) {
@@ -1032,14 +965,13 @@ export class SyncApi {
this.connectionReturnedDefer = null;
}
this.updateSyncState(SyncState.Stopped);
return;
return true; // abort
}
logger.error("/sync error %s", err);
logger.error(err);
if (this.shouldAbortSync(err)) {
return;
return true; // abort
}
this.failedSyncCount++;
@@ -1053,20 +985,7 @@ export class SyncApi {
// erroneous. We set the state to 'reconnecting'
// instead, so that clients can observe this state
// if they wish.
this.startKeepAlives().then((connDidFail) => {
// Only emit CATCHUP if we detected a connectivity error: if we didn't,
// it's quite likely the sync will fail again for the same reason and we
// want to stay in ERROR rather than keep flip-flopping between ERROR
// and CATCHUP.
if (connDidFail && this.getSyncState() === SyncState.Error) {
this.updateSyncState(SyncState.Catchup, {
oldSyncToken: null,
nextSyncToken: null,
catchingUp: true,
});
}
this.doSync(syncOptions);
});
const keepAlivePromise = this.startKeepAlives();
this.currentSyncRequest = null;
// Transition from RECONNECTING to ERROR after a given number of failed syncs
@@ -1075,6 +994,19 @@ export class SyncApi {
SyncState.Error : SyncState.Reconnecting,
{ error: err },
);
const connDidFail = await keepAlivePromise;
// Only emit CATCHUP if we detected a connectivity error: if we didn't,
// it's quite likely the sync will fail again for the same reason and we
// want to stay in ERROR rather than keep flip-flopping between ERROR
// and CATCHUP.
if (connDidFail && this.getSyncState() === SyncState.Error) {
this.updateSyncState(SyncState.Catchup, {
catchingUp: true,
});
}
return false;
}
/**
@@ -1133,7 +1065,7 @@ export class SyncApi {
// - The isBrandNewRoom boilerplate is boilerplatey.
// handle presence events (User objects)
if (data.presence && Array.isArray(data.presence.events)) {
if (Array.isArray(data.presence?.events)) {
data.presence.events.map(client.getEventMapper()).forEach(
function(presenceEvent) {
let user = client.store.getUser(presenceEvent.getSender());
@@ -1149,7 +1081,7 @@ export class SyncApi {
}
// handle non-room account_data
if (data.account_data && Array.isArray(data.account_data.events)) {
if (Array.isArray(data.account_data?.events)) {
const events = data.account_data.events.map(client.getEventMapper());
const prevEventsMap = events.reduce((m, c) => {
m[c.getId()] = client.store.getAccountData(c.getType());
@@ -1290,8 +1222,7 @@ export class SyncApi {
// bother setting it here. We trust our calculations better than the
// server's for this case, and therefore will assume that our non-zero
// count is accurate.
if (!encrypted
|| (encrypted && room.getUnreadNotificationCount(NotificationCountType.Highlight) <= 0)) {
if (!encrypted || room.getUnreadNotificationCount(NotificationCountType.Highlight) <= 0) {
room.setUnreadNotificationCount(
NotificationCountType.Highlight,
joinObj.unread_notifications.highlight_count,
@@ -1304,8 +1235,7 @@ export class SyncApi {
if (joinObj.isBrandNewRoom) {
// set the back-pagination token. Do this *before* adding any
// events so that clients can start back-paginating.
room.getLiveTimeline().setPaginationToken(
joinObj.timeline.prev_batch, EventTimeline.BACKWARDS);
room.getLiveTimeline().setPaginationToken(joinObj.timeline.prev_batch, EventTimeline.BACKWARDS);
} else if (joinObj.timeline.limited) {
let limited = true;
@@ -1792,3 +1722,49 @@ function createNewUser(client: MatrixClient, userId: string): User {
return user;
}
// /!\ This function is not intended for public use! It's only exported from
// here in order to share some common logic with sliding-sync-sdk.ts.
export function _createAndReEmitRoom(client: MatrixClient, roomId: string, opts: Partial<IStoredClientOpts>): Room {
const { timelineSupport } = client;
const room = new Room(roomId, client, client.getUserId(), {
lazyLoadMembers: opts.lazyLoadMembers,
pendingEventOrdering: opts.pendingEventOrdering,
timelineSupport,
});
client.reEmitter.reEmit(room, [
RoomEvent.Name,
RoomEvent.Redaction,
RoomEvent.RedactionCancelled,
RoomEvent.Receipt,
RoomEvent.Tags,
RoomEvent.LocalEchoUpdated,
RoomEvent.AccountData,
RoomEvent.MyMembership,
RoomEvent.Timeline,
RoomEvent.TimelineReset,
RoomStateEvent.Events,
RoomStateEvent.Members,
RoomStateEvent.NewMember,
RoomStateEvent.Update,
BeaconEvent.New,
BeaconEvent.Update,
BeaconEvent.Destroy,
BeaconEvent.LivenessChange,
]);
// We need to add a listener for RoomState.members in order to hook them
// correctly.
room.on(RoomStateEvent.NewMember, (event, state, member) => {
member.user = client.getUser(member.userId);
client.reEmitter.reEmit(member, [
RoomMemberEvent.Name,
RoomMemberEvent.Typing,
RoomMemberEvent.PowerLevel,
RoomMemberEvent.Membership,
]);
});
return room;
}
+50 -40
View File
@@ -24,10 +24,34 @@ import unhomoglyph from "unhomoglyph";
import promiseRetry from "p-retry";
import type * as NodeCrypto from "crypto";
import { MatrixClient, MatrixEvent } from ".";
import { MatrixEvent } from ".";
import { M_TIMESTAMP } from "./@types/location";
import { ReceiptType } from "./@types/read_receipts";
const interns = new Map<string, string>();
/**
* Internalises a string, reusing a known pointer or storing the pointer
* if needed for future strings.
* @param str The string to internalise.
* @returns The internalised string.
*/
export function internaliseString(str: string): string {
// Unwrap strings before entering the map, if we somehow got a wrapped
// string as our input. This should only happen from tests.
if ((str as unknown) instanceof String) {
str = str.toString();
}
// Check the map to see if we can store the value
if (!interns.has(str)) {
interns.set(str, str);
}
// Return any cached string reference
return interns.get(str);
}
/**
* Encode a dictionary of query parameters.
* Omits any undefined/null values.
@@ -74,8 +98,7 @@ export function decodeParams(query: string): QueryDict {
* variables with. E.g. { "$bar": "baz" }.
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
*/
export function encodeUri(pathTemplate: string,
variables: Record<string, string>): string {
export function encodeUri(pathTemplate: string, variables: Record<string, string>): string {
for (const key in variables) {
if (!variables.hasOwnProperty(key)) {
continue;
@@ -215,33 +238,24 @@ export function deepCompare(x: any, y: any): boolean {
}
}
} else {
// disable jshint "The body of a for in should be wrapped in an if
// statement"
/* jshint -W089 */
// check that all of y's direct keys are in x
let p;
for (p in y) {
for (const p in y) {
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
return false;
}
}
// finally, compare each of x's keys with y
for (p in y) { // eslint-disable-line guard-for-in
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
return false;
}
if (!deepCompare(x[p], y[p])) {
for (const p in x) {
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p) || !deepCompare(x[p], y[p])) {
return false;
}
}
}
/* jshint +W089 */
return true;
}
// Dev note: This returns a tuple, but jsdoc doesn't like that. https://github.com/jsdoc/jsdoc/issues/1703
// Dev note: This returns an array of tuples, but jsdoc doesn't like that. https://github.com/jsdoc/jsdoc/issues/1703
/**
* Creates an array of object properties/values (entries) then
* sorts the result by key, recursively. The input object must
@@ -327,23 +341,29 @@ export function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export function globToRegexp(glob: string, extended?: any): string {
extended = typeof(extended) === 'boolean' ? extended : true;
export function globToRegexp(glob: string, extended = false): string {
// From
// https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
// Because micromatch is about 130KB with dependencies,
// and minimatch is not much better.
let pat = escapeRegExp(glob);
pat = pat.replace(/\\\*/g, '.*');
pat = pat.replace(/\?/g, '.');
if (extended) {
pat = pat.replace(/\\\[(!|)(.*)\\]/g, function(match, p1, p2, offset, string) {
const first = p1 && '^' || '';
const second = p2.replace(/\\-/, '-');
return '[' + first + second + ']';
});
}
return pat;
const replacements: ([RegExp, string | ((substring: string, ...args: any[]) => string) ])[] = [
[/\\\*/g, '.*'],
[/\?/g, '.'],
!extended && [
/\\\[(!|)(.*)\\]/g,
(_match: string, neg: string, pat: string) => [
'[',
neg ? '^' : '',
pat.replace(/\\-/, '-'),
']',
].join(''),
],
];
return replacements.reduce(
// https://github.com/microsoft/TypeScript/issues/30134
(pat, args) => args ? pat.replace(args[0], args[1] as any) : pat,
escapeRegExp(glob),
);
}
export function ensureNoTrailingSlash(url: string): string {
@@ -650,17 +670,7 @@ export function sortEventsByLatestContentTimestamp(left: MatrixEvent, right: Mat
return getContentTimestampWithFallback(right) - getContentTimestampWithFallback(left);
}
export async function getPrivateReadReceiptField(client: MatrixClient): Promise<ReceiptType | null> {
if (await client.doesServerSupportUnstableFeature("org.matrix.msc2285.stable")) return ReceiptType.ReadPrivate;
if (await client.doesServerSupportUnstableFeature("org.matrix.msc2285")) return ReceiptType.UnstableReadPrivate;
return null;
}
export function isSupportedReceiptType(receiptType: string): boolean {
return [
ReceiptType.Read,
ReceiptType.ReadPrivate,
ReceiptType.UnstableReadPrivate,
].includes(receiptType as ReceiptType);
return [ReceiptType.Read, ReceiptType.ReadPrivate].includes(receiptType as ReceiptType);
}
+2 -4
View File
@@ -866,8 +866,6 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
public answerWithCallFeeds(callFeeds: CallFeed[]): void {
if (this.inviteOrAnswerSent) return;
logger.debug(`Answering call ${this.callId}`);
this.gotCallFeedsForAnswer(callFeeds);
}
@@ -1865,10 +1863,10 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
const shouldTerminate = (
// reject events also end the call if it's ringing: it's another of
// our devices rejecting the call.
([CallState.InviteSent, CallState.Ringing].includes(this.state)) ||
[CallState.InviteSent, CallState.Ringing].includes(this.state) ||
// also if we're in the init state and it's an inbound call, since
// this means we just haven't entered the ringing state yet
this.state === CallState.Fledgling && this.direction === CallDirection.Inbound
(this.state === CallState.Fledgling && this.direction === CallDirection.Inbound)
);
if (shouldTerminate) {
+1 -1
View File
@@ -12,6 +12,6 @@
},
"include": [
"./src/**/*.ts",
"./spec/**/*.ts",
"./spec/**/*.ts"
]
}
+854 -698
View File
File diff suppressed because it is too large Load Diff