Compare commits
44 Commits
v18.0.0-rc.2
...
v18.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| bf30c15d77 | |||
| 8acc92770d | |||
| c707c8632b | |||
| 2700f1d053 | |||
| 64c6c477dc | |||
| c28939c250 | |||
| 05dd3c4967 | |||
| 8c72c5d0e6 | |||
| 93293750ce | |||
| 220f709b3a | |||
| 6c336ae470 | |||
| a4a50a4a5c | |||
| 169e865bb6 | |||
| 8803d2b7e2 | |||
| ab16adfb7d | |||
| f90adcab89 | |||
| 6090c7bbfc | |||
| 12253064d1 | |||
| b2120a0a13 | |||
| ad030bfc1f | |||
| 60d665e866 | |||
| 2fd48a607d | |||
| 2f540e31a5 | |||
| 7bd9626496 | |||
| 58a5742bd3 | |||
| d90f983438 | |||
| 81a6e48fc0 | |||
| db4a6fa9e1 | |||
| 457f063c67 | |||
| 01eee96b29 | |||
| 25afb7cb3c | |||
| c12932b2a6 | |||
| 7ac1d07cc4 | |||
| 66adc2d597 | |||
| a9516d047f | |||
| e81d84502b | |||
| 81d884f899 | |||
| 2dccefb33a | |||
| af6915b4fc | |||
| 3786ea4ca2 | |||
| c7f6777e48 | |||
| f78fed5ede | |||
| add3732450 | |||
| d9f0704048 |
@@ -52,6 +52,8 @@ module.exports = {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
// We'd rather not do this but we do
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
// We're okay with assertion errors when we ask for them
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
|
||||
"quotes": "off",
|
||||
// We use a `logger` intermediary module
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
name: Release Process
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
jsdoc:
|
||||
name: Publish Documentation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --pure-lockfile"
|
||||
|
||||
- name: 📖 Generate JSDoc
|
||||
run: "yarn gendoc"
|
||||
|
||||
- name: 📋 Copy to temp
|
||||
run: |
|
||||
ls -lah
|
||||
tag="${{ github.ref_name }}"
|
||||
version="${tag#v}"
|
||||
echo "VERSION=$version" >> $GITHUB_ENV
|
||||
cp -a "./.jsdoc/matrix-js-sdk/$version" $RUNNER_TEMP
|
||||
|
||||
- name: 🧮 Checkout gh-pages
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: gh-pages
|
||||
|
||||
- name: 🔪 Prepare
|
||||
run: |
|
||||
cp -a "$RUNNER_TEMP/$VERSION" .
|
||||
|
||||
# Add the new directory to the index if it isn't there already
|
||||
if ! grep -q "Version $VERSION" index.html; then
|
||||
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
|
||||
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' "$VERSION" index.html
|
||||
fi
|
||||
|
||||
- name: 🚀 Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
keep_files: true
|
||||
publish_dir: .
|
||||
@@ -1,8 +1,4 @@
|
||||
# Find details about the PR associated with this ref
|
||||
# Outputs:
|
||||
# prnumber: the ID number of the associated PR
|
||||
# headref: the name of the head branch of the PR
|
||||
# baseref: the name of the base branch of the PR
|
||||
name: PR Details
|
||||
on:
|
||||
workflow_call:
|
||||
@@ -18,13 +14,16 @@ on:
|
||||
outputs:
|
||||
pr_id:
|
||||
description: The ID of the PR found
|
||||
value: ${{ jobs.prdetails.outputs.pr_id }}
|
||||
value: ${{ fromJSON(jobs.prdetails.outputs.result).number }}
|
||||
head_branch:
|
||||
description: The head branch of the PR found
|
||||
value: ${{ jobs.prdetails.outputs.head_branch }}
|
||||
value: ${{ fromJSON(jobs.prdetails.outputs.result).head.ref }}
|
||||
base_branch:
|
||||
description: The base branch of the PR found
|
||||
value: ${{ jobs.prdetails.outputs.base_branch }}
|
||||
value: ${{ fromJSON(jobs.prdetails.outputs.result).base.ref }}
|
||||
data:
|
||||
description: The JSON data of the pull request API object
|
||||
value: ${{ jobs.prdetails.outputs.result }})
|
||||
|
||||
jobs:
|
||||
prdetails:
|
||||
@@ -33,27 +32,18 @@ jobs:
|
||||
steps:
|
||||
- name: "🔍 Read PR details"
|
||||
id: details
|
||||
# We need to find the PR number that corresponds to the branch, which we do by searching the GH API
|
||||
# The workflow_run event includes a list of pull requests, but it doesn't get populated for
|
||||
# forked PRs: https://docs.github.com/en/rest/reference/checks#create-a-check-run
|
||||
run: |
|
||||
head_branch='${{ inputs.owner }}:${{ inputs.branch }}'
|
||||
echo "Head branch: $head_branch"
|
||||
pulls_uri="https://api.github.com/repos/${{ github.repository }}/pulls?head=$(jq -Rr '@uri' <<<$head_branch)"
|
||||
pr_data=$(curl -s -H 'Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' "$pulls_uri")
|
||||
|
||||
pr_number=$(jq -r '.[] | .number' <<< "$pr_data")
|
||||
echo "PR number: $pr_number"
|
||||
echo "::set-output name=prnumber::$pr_number"
|
||||
|
||||
head_ref=$(jq -r '.[] | .head.ref' <<< "$pr_data")
|
||||
echo "Head ref: $head_ref"
|
||||
echo "::set-output name=headref::$head_ref"
|
||||
|
||||
base_ref=$(jq -r '.[] | .base.ref' <<< "$pr_data")
|
||||
echo "Base ref: $base_ref"
|
||||
echo "::set-output name=baseref::$base_ref"
|
||||
uses: actions/github-script@v5
|
||||
with:
|
||||
# We need to find the PR number that corresponds to the branch, which we do by searching the GH API
|
||||
# The workflow_run event includes a list of pull requests, but it doesn't get populated for
|
||||
# forked PRs: https://docs.github.com/en/rest/reference/checks#create-a-check-run
|
||||
script: |
|
||||
const [owner, repo] = "${{ github.repository }}".split("/");
|
||||
const response = await github.rest.pulls.list({
|
||||
head: "${{ inputs.owner }}:${{ inputs.branch }}",
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
return response.data[0];
|
||||
outputs:
|
||||
pr_id: ${{ steps.details.outputs.prnumber }}
|
||||
head_branch: ${{ steps.details.outputs.headref }}
|
||||
base_branch: ${{ steps.details.outputs.baseref }}
|
||||
result: ${{ steps.details.outputs.result }}
|
||||
|
||||
@@ -2,9 +2,7 @@ name: Pull Request
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [ opened, edited, labeled, unlabeled, synchronize ]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }}
|
||||
jobs:
|
||||
changelog:
|
||||
name: Preview Changelog
|
||||
@@ -21,7 +19,7 @@ jobs:
|
||||
permissions:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- uses: yogevbd/enforce-label-action@2.1.0
|
||||
- uses: yogevbd/enforce-label-action@2.2.2
|
||||
with:
|
||||
REQUIRED_LABELS_ANY: "T-Defect,T-Deprecation,T-Enhancement,T-Task"
|
||||
BANNED_LABELS: "X-Blocked"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Must only be called from a workflow_run in the context of the upstream repo
|
||||
name: SonarCloud
|
||||
on:
|
||||
workflow_call:
|
||||
@@ -35,13 +36,6 @@ on:
|
||||
type: string
|
||||
required: false
|
||||
description: The base branch of the PR if this workflow is being triggered due to one
|
||||
|
||||
# Org specific parameters
|
||||
main_branch:
|
||||
type: string
|
||||
required: false
|
||||
description: The default branch of the repository
|
||||
default: "develop"
|
||||
secrets:
|
||||
SONAR_TOKEN:
|
||||
required: true
|
||||
@@ -57,13 +51,20 @@ jobs:
|
||||
ref: ${{ inputs.head_branch }} # checkout commit that triggered this workflow
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
|
||||
# Fetch develop so that Sonar can identify new issues in PR builds
|
||||
- name: "📕 Fetch ${{ inputs.main_branch }}"
|
||||
if: inputs.head_branch != inputs.main_branch
|
||||
run: git rev-parse HEAD && git fetch origin ${{ inputs.main_branch }}:${{ inputs.main_branch }} && git status && git rev-parse HEAD
|
||||
# Fetch base branch from the upstream repo so that Sonar can identify new code in PR builds
|
||||
- name: "📕 Fetch upstream base branch"
|
||||
# workflow_call retains the github context of the caller, so `repository` will be upstream always due
|
||||
# to it running on `workflow_run` which is called from the context of the target repo and not the fork.
|
||||
if: inputs.base_branch
|
||||
run: |
|
||||
git remote add upstream https://github.com/${{ github.repository }}
|
||||
git rev-parse HEAD
|
||||
git fetch upstream ${{ inputs.base_branch }}:${{ inputs.base_branch }}
|
||||
git status
|
||||
git rev-parse HEAD
|
||||
|
||||
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
|
||||
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
|
||||
# (https://github.com/actions/download-artifact/issues/60) so instead we get this alternative:
|
||||
- name: "📥 Download Coverage Report"
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
if: inputs.coverage_workflow_name
|
||||
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
types:
|
||||
- completed
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
prdetails:
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
run: "yarn build"
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: "yarn coverage --ci"
|
||||
run: "yarn coverage --ci --reporters github-actions"
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
instrumentation:
|
||||
compact: false
|
||||
+30
-5
@@ -1,11 +1,36 @@
|
||||
Changes in [18.0.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v18.0.0-rc.2) (2022-05-20)
|
||||
============================================================================================================
|
||||
Changes in [18.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v18.1.0) (2022-06-07)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Convert `getLocalAliases` to a stable API call ([\#2402](https://github.com/matrix-org/matrix-js-sdk/pull/2402)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Catch promise errors in degradable ([\#2388](https://github.com/matrix-org/matrix-js-sdk/pull/2388)). Fixes #2384.
|
||||
* Fix request, crypto, and bs58 imports ([\#2414](https://github.com/matrix-org/matrix-js-sdk/pull/2414)). Fixes #2415.
|
||||
* Update relations after every decryption attempt ([\#2387](https://github.com/matrix-org/matrix-js-sdk/pull/2387)). Fixes vector-im/element-web#22258. Contributed by @weeman1337.
|
||||
* Fix degraded mode for the IDBStore and test it ([\#2400](https://github.com/matrix-org/matrix-js-sdk/pull/2400)). Fixes matrix-org/element-web-rageshakes#13170.
|
||||
* Don't cancel SAS verifications if `ready` is received after `start` ([\#2250](https://github.com/matrix-org/matrix-js-sdk/pull/2250)).
|
||||
* Prevent overlapping sync accumulator persists ([\#2392](https://github.com/matrix-org/matrix-js-sdk/pull/2392)). Fixes vector-im/element-web#21541.
|
||||
* Fix behaviour of isRelation with relation m.replace for state events ([\#2389](https://github.com/matrix-org/matrix-js-sdk/pull/2389)). Fixes vector-im/element-web#22280.
|
||||
* Fixes #2384 ([\#2385](https://github.com/matrix-org/matrix-js-sdk/pull/2385)). Fixes undefined/matrix-js-sdk#2384. Contributed by @schmop.
|
||||
* Ensure rooms are recalculated on re-invites ([\#2374](https://github.com/matrix-org/matrix-js-sdk/pull/2374)). Fixes vector-im/element-web#22106.
|
||||
|
||||
Changes in [18.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v18.0.0-rc.1) (2022-05-17)
|
||||
============================================================================================================
|
||||
Changes in [18.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v18.0.0) (2022-05-24)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES (to experimental methods)
|
||||
* Implement changes to MSC2285 (private read receipts) ([\#2221](https://github.com/matrix-org/matrix-js-sdk/pull/2221)).
|
||||
|
||||
## ✨ Features
|
||||
* Add support for HTML renderings of room topics ([\#2272](https://github.com/matrix-org/matrix-js-sdk/pull/2272)).
|
||||
* Add stopClient parameter to MatrixClient::logout ([\#2367](https://github.com/matrix-org/matrix-js-sdk/pull/2367)).
|
||||
* registration: add function to re-request email token ([\#2357](https://github.com/matrix-org/matrix-js-sdk/pull/2357)).
|
||||
* Remove hacky custom status feature ([\#2350](https://github.com/matrix-org/matrix-js-sdk/pull/2350)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Remove default push rule override for MSC1930 ([\#2376](https://github.com/matrix-org/matrix-js-sdk/pull/2376)). Fixes vector-im/element-web#15439.
|
||||
* Tweak thread creation & event adding to fix bugs around relations ([\#2369](https://github.com/matrix-org/matrix-js-sdk/pull/2369)). Fixes vector-im/element-web#22162 and vector-im/element-web#22180.
|
||||
* Prune both clear & wire content on redaction ([\#2346](https://github.com/matrix-org/matrix-js-sdk/pull/2346)). Fixes vector-im/element-web#21929.
|
||||
* MSC3786: Add a default push rule to ignore `m.room.server_acl` events ([\#2333](https://github.com/matrix-org/matrix-js-sdk/pull/2333)). Fixes vector-im/element-web#20788.
|
||||
|
||||
Changes in [17.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v17.2.0) (2022-05-10)
|
||||
==================================================================================================
|
||||
|
||||
+4
-1
@@ -249,7 +249,10 @@ 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. When stacking pull requests, you may wish to do the following:
|
||||
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.
|
||||
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "18.0.0-rc.2",
|
||||
"version": "18.1.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.8.tgz",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/jest": "^27.0.0",
|
||||
"@types/node": "12",
|
||||
"@types/request": "^2.48.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.6.0",
|
||||
"@typescript-eslint/parser": "^5.6.0",
|
||||
"allchange": "^1.0.6",
|
||||
"babel-jest": "^26.6.3",
|
||||
"babel-jest": "^28.0.0",
|
||||
"babelify": "^10.0.0",
|
||||
"better-docs": "^2.4.0-beta.9",
|
||||
"browserify": "^17.0.0",
|
||||
"docdash": "^1.2.0",
|
||||
"eslint": "8.9.0",
|
||||
"eslint": "8.16.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-matrix-org": "^0.4.0",
|
||||
"eslint-plugin-matrix-org": "^0.5.0",
|
||||
"exorcist": "^1.0.1",
|
||||
"fake-indexeddb": "^3.1.2",
|
||||
"jest": "^26.6.3",
|
||||
"jest": "^28.0.0",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jest-sonar-reporter": "^2.0.0",
|
||||
"jsdoc": "^3.6.6",
|
||||
|
||||
+1
-22
@@ -29,7 +29,7 @@ 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 [-xz] [-c changelog_file] vX.Y.Z"
|
||||
USAGE="$0 [-x] [-c changelog_file] vX.Y.Z"
|
||||
|
||||
help() {
|
||||
cat <<EOF
|
||||
@@ -37,7 +37,6 @@ $USAGE
|
||||
|
||||
-c changelog_file: specify name of file containing changelog
|
||||
-x: skip updating the changelog
|
||||
-z: skip generating the jsdoc
|
||||
-n: skip publish to NPM
|
||||
EOF
|
||||
}
|
||||
@@ -60,7 +59,6 @@ if ! git diff-files --quiet; then
|
||||
fi
|
||||
|
||||
skip_changelog=
|
||||
skip_jsdoc=
|
||||
skip_npm=
|
||||
changelog_file="CHANGELOG.md"
|
||||
expected_npm_user="matrixdotorg"
|
||||
@@ -76,9 +74,6 @@ while getopts hc:u:xzn f; do
|
||||
x)
|
||||
skip_changelog=1
|
||||
;;
|
||||
z)
|
||||
skip_jsdoc=1
|
||||
;;
|
||||
n)
|
||||
skip_npm=1
|
||||
;;
|
||||
@@ -326,22 +321,6 @@ if [ -z "$skip_npm" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$skip_jsdoc" ]; then
|
||||
echo "generating jsdocs"
|
||||
yarn gendoc
|
||||
|
||||
echo "copying jsdocs to gh-pages branch"
|
||||
git checkout gh-pages
|
||||
git pull
|
||||
cp -a ".jsdoc/matrix-js-sdk/$release" .
|
||||
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
|
||||
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' \
|
||||
$release index.html
|
||||
git add "$release"
|
||||
git commit --no-verify -m "Add jsdoc for $release" index.html "$release"
|
||||
git push origin gh-pages
|
||||
fi
|
||||
|
||||
# if it is a pre-release, leave it on the release branch for now.
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
git checkout "$rel_branch"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base",
|
||||
":dependencyDashboardApproval"
|
||||
],
|
||||
"labels": ["T-Task", "Dependencies"],
|
||||
"lockFileMaintenance": { "enabled": true },
|
||||
"groupName": "all",
|
||||
"packageRules": [{
|
||||
"matchFiles": ["package.json"],
|
||||
"rangeStrategy": "update-lockfile"
|
||||
}]
|
||||
}
|
||||
@@ -136,138 +136,137 @@ describe("DeviceList management:", function() {
|
||||
});
|
||||
});
|
||||
|
||||
it("We should not get confused by out-of-order device query responses",
|
||||
() => {
|
||||
// https://github.com/vector-im/element-web/issues/3126
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
|
||||
return aliceTestClient.start().then(() => {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse(['@bob:xyz', '@chris:abc']));
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(() => {
|
||||
// to make sure the initial device queries are flushed out, we
|
||||
// attempt to send a message.
|
||||
it.skip("We should not get confused by out-of-order device query responses", () => {
|
||||
// https://github.com/vector-im/element-web/issues/3126
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
|
||||
return aliceTestClient.start().then(() => {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse(['@bob:xyz', '@chris:abc']));
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(() => {
|
||||
// to make sure the initial device queries are flushed out, we
|
||||
// attempt to send a message.
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
'@chris:abc': {},
|
||||
},
|
||||
},
|
||||
);
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
'@chris:abc': {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
aliceTestClient.httpBackend.when('PUT', '/send/').respond(
|
||||
200, { event_id: '$event1' });
|
||||
aliceTestClient.httpBackend.when('PUT', '/send/').respond(
|
||||
200, { event_id: '$event1' });
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1).then(
|
||||
() => aliceTestClient.httpBackend.flush('/send/', 1),
|
||||
),
|
||||
aliceTestClient.client.crypto.deviceList.saveIfDirty(),
|
||||
]);
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
expect(data.syncToken).toEqual(1);
|
||||
});
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1).then(
|
||||
() => aliceTestClient.httpBackend.flush('/send/', 1),
|
||||
),
|
||||
aliceTestClient.client.crypto.deviceList.saveIfDirty(),
|
||||
]);
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
expect(data.syncToken).toEqual(1);
|
||||
});
|
||||
|
||||
// invalidate bob's and chris's device lists in separate syncs
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
|
||||
next_batch: '2',
|
||||
device_lists: {
|
||||
changed: ['@bob:xyz'],
|
||||
},
|
||||
});
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
|
||||
next_batch: '3',
|
||||
device_lists: {
|
||||
changed: ['@chris:abc'],
|
||||
},
|
||||
});
|
||||
// flush both syncs
|
||||
return aliceTestClient.flushSync().then(() => {
|
||||
return aliceTestClient.flushSync();
|
||||
});
|
||||
}).then(() => {
|
||||
// check that we don't yet have a request for chris's devices.
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query', {
|
||||
device_keys: {
|
||||
'@chris:abc': {},
|
||||
},
|
||||
token: '3',
|
||||
}).respond(200, {
|
||||
device_keys: { '@chris:abc': {} },
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(0);
|
||||
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
if (bobStat != 1 && bobStat != 2) {
|
||||
throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
|
||||
bobStat);
|
||||
}
|
||||
const chrisStat = data.trackingStatus['@chris:abc'];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error(
|
||||
'Unexpected status for chris: wanted 1 or 2, got ' + chrisStat,
|
||||
);
|
||||
}
|
||||
});
|
||||
// invalidate bob's and chris's device lists in separate syncs
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
|
||||
next_batch: '2',
|
||||
device_lists: {
|
||||
changed: ['@bob:xyz'],
|
||||
},
|
||||
});
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
|
||||
next_batch: '3',
|
||||
device_lists: {
|
||||
changed: ['@chris:abc'],
|
||||
},
|
||||
});
|
||||
// flush both syncs
|
||||
return aliceTestClient.flushSync().then(() => {
|
||||
return aliceTestClient.flushSync();
|
||||
});
|
||||
}).then(() => {
|
||||
// check that we don't yet have a request for chris's devices.
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query', {
|
||||
device_keys: {
|
||||
'@chris:abc': {},
|
||||
},
|
||||
token: '3',
|
||||
}).respond(200, {
|
||||
device_keys: { '@chris:abc': {} },
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(0);
|
||||
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
if (bobStat != 1 && bobStat != 2) {
|
||||
throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
|
||||
bobStat);
|
||||
}
|
||||
const chrisStat = data.trackingStatus['@chris:abc'];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error(
|
||||
'Unexpected status for chris: wanted 1 or 2, got ' + chrisStat,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// now add an expectation for a query for bob's devices, and let
|
||||
// it complete.
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query', {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
},
|
||||
token: '2',
|
||||
}).respond(200, {
|
||||
device_keys: { '@bob:xyz': {} },
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(1);
|
||||
// now add an expectation for a query for bob's devices, and let
|
||||
// it complete.
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query', {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
},
|
||||
token: '2',
|
||||
}).respond(200, {
|
||||
device_keys: { '@bob:xyz': {} },
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(1);
|
||||
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
||||
}).then(() => {
|
||||
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
expect(bobStat).toEqual(3);
|
||||
const chrisStat = data.trackingStatus['@chris:abc'];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error(
|
||||
'Unexpected status for chris: wanted 1 or 2, got ' + bobStat,
|
||||
);
|
||||
}
|
||||
});
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
||||
}).then(() => {
|
||||
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
expect(bobStat).toEqual(3);
|
||||
const chrisStat = data.trackingStatus['@chris:abc'];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error(
|
||||
'Unexpected status for chris: wanted 1 or 2, got ' + bobStat,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// now let the query for chris's devices complete.
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(1);
|
||||
// now let the query for chris's devices complete.
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(1);
|
||||
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(['@chris:abc']);
|
||||
}).then(() => {
|
||||
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
const chrisStat = data.trackingStatus['@bob:xyz'];
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(['@chris:abc']);
|
||||
}).then(() => {
|
||||
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
const chrisStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
expect(bobStat).toEqual(3);
|
||||
expect(chrisStat).toEqual(3);
|
||||
expect(data.syncToken).toEqual(3);
|
||||
});
|
||||
});
|
||||
}).timeout(3000);
|
||||
expect(bobStat).toEqual(3);
|
||||
expect(chrisStat).toEqual(3);
|
||||
expect(data.syncToken).toEqual(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// https://github.com/vector-im/element-web/issues/4983
|
||||
describe("Alice should know she has stale device lists", () => {
|
||||
@@ -42,7 +42,7 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
describe("uploadContent", function() {
|
||||
const buf = new Buffer('hello world');
|
||||
const buf = Buffer.from('hello world');
|
||||
it("should upload the file", function() {
|
||||
httpBackend.when(
|
||||
"POST", "/_matrix/media/r0/upload",
|
||||
@@ -474,6 +474,10 @@ describe("MatrixClient", function() {
|
||||
return client.initCrypto();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it("should do an HTTP request and then store the keys", function() {
|
||||
const ed25519key = "7wG2lzAqbjcyEkOP7O4gU7ItYcn+chKzh5sT/5r2l78";
|
||||
// ed25519key = client.getDeviceEd25519Key();
|
||||
|
||||
@@ -8,7 +8,6 @@ import { MatrixError } from "../../src/http-api";
|
||||
|
||||
describe("MatrixClient opts", function() {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
const userId = "@alice:localhost";
|
||||
const userB = "@bob:localhost";
|
||||
@@ -65,6 +64,7 @@ describe("MatrixClient opts", function() {
|
||||
});
|
||||
|
||||
describe("without opts.store", function() {
|
||||
let client;
|
||||
beforeEach(function() {
|
||||
client = new MatrixClient({
|
||||
request: httpBackend.requestFn,
|
||||
@@ -124,6 +124,7 @@ describe("MatrixClient opts", function() {
|
||||
});
|
||||
|
||||
describe("without opts.scheduler", function() {
|
||||
let client;
|
||||
beforeEach(function() {
|
||||
client = new MatrixClient({
|
||||
request: httpBackend.requestFn,
|
||||
@@ -135,6 +136,10 @@ describe("MatrixClient opts", function() {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it("shouldn't retry sending events", function(done) {
|
||||
httpBackend.when("PUT", "/txn1").fail(500, new MatrixError({
|
||||
errcode: "M_SOMETHING",
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { EventTimeline } from "../../src/models/event-timeline";
|
||||
/*
|
||||
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 { EventTimeline, MatrixEvent, RoomEvent } from "../../src";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
@@ -60,6 +75,112 @@ describe("MatrixClient syncing", function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Room.myMembership for invite->leave->invite cycles", async () => {
|
||||
const roomId = "!cycles:example.org";
|
||||
|
||||
// First sync: an invite
|
||||
const inviteSyncRoomSection = {
|
||||
invite: {
|
||||
[roomId]: {
|
||||
invite_state: {
|
||||
events: [{
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "invite",
|
||||
},
|
||||
}],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
...syncData,
|
||||
rooms: inviteSyncRoomSection,
|
||||
});
|
||||
|
||||
// Second sync: a leave (reject of some kind)
|
||||
httpBackend.when("POST", "/leave").respond(200, {});
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
...syncData,
|
||||
rooms: {
|
||||
leave: {
|
||||
[roomId]: {
|
||||
account_data: { events: [] },
|
||||
ephemeral: { events: [] },
|
||||
state: {
|
||||
events: [{
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "leave",
|
||||
},
|
||||
prev_content: {
|
||||
membership: "invite",
|
||||
},
|
||||
// XXX: And other fields required on an event
|
||||
}],
|
||||
},
|
||||
timeline: {
|
||||
limited: false,
|
||||
events: [{
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "leave",
|
||||
},
|
||||
prev_content: {
|
||||
membership: "invite",
|
||||
},
|
||||
// XXX: And other fields required on an event
|
||||
}],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Third sync: another invite
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
...syncData,
|
||||
rooms: inviteSyncRoomSection,
|
||||
});
|
||||
|
||||
// First fire: an initial invite
|
||||
let fires = 0;
|
||||
client.once(RoomEvent.MyMembership, (room, membership, oldMembership) => { // Room, string, string
|
||||
fires++;
|
||||
expect(room.roomId).toBe(roomId);
|
||||
expect(membership).toBe("invite");
|
||||
expect(oldMembership).toBeFalsy();
|
||||
|
||||
// Second fire: a leave
|
||||
client.once(RoomEvent.MyMembership, (room, membership, oldMembership) => {
|
||||
fires++;
|
||||
expect(room.roomId).toBe(roomId);
|
||||
expect(membership).toBe("leave");
|
||||
expect(oldMembership).toBe("invite");
|
||||
|
||||
// Third/final fire: a second invite
|
||||
client.once(RoomEvent.MyMembership, (room, membership, oldMembership) => {
|
||||
fires++;
|
||||
expect(room.roomId).toBe(roomId);
|
||||
expect(membership).toBe("invite");
|
||||
expect(oldMembership).toBe("leave");
|
||||
});
|
||||
});
|
||||
|
||||
// For maximum safety, "leave" the room after we register the handler
|
||||
client.leave(roomId);
|
||||
});
|
||||
|
||||
// noinspection ES6MissingAwait
|
||||
client.startClient();
|
||||
await httpBackend.flushAllExpected();
|
||||
|
||||
expect(fires).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolving invites to profile info", function() {
|
||||
|
||||
@@ -1029,6 +1029,7 @@ describe("megolm", function() {
|
||||
});
|
||||
return event.attemptDecryption(testClient.client.crypto, true).then(() => {
|
||||
expect(event.isKeySourceUntrusted()).toBeFalsy();
|
||||
testClient.stop();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import '../olm-loader';
|
||||
|
||||
import { logger } from '../../src/logger';
|
||||
import { IContent, IEvent, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { ClientEvent, EventType, MatrixClient } from "../../src";
|
||||
import { ClientEvent, EventType, MatrixClient, MsgType } from "../../src";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { eventMapperFor } from "../../src/event-mapper";
|
||||
|
||||
@@ -225,7 +225,7 @@ export function mkMessage(opts: IMessageOpts, client?: MatrixClient): object | M
|
||||
...opts,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
msgtype: MsgType.Text,
|
||||
body: opts.msg,
|
||||
},
|
||||
};
|
||||
@@ -236,6 +236,45 @@ export function mkMessage(opts: IMessageOpts, client?: MatrixClient): object | M
|
||||
return mkEvent(eventOpts, client);
|
||||
}
|
||||
|
||||
interface IReplyMessageOpts extends IMessageOpts {
|
||||
replyToMessage: MatrixEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a reply message.
|
||||
*
|
||||
* @param {Object} opts Values for the message
|
||||
* @param {string} opts.room The room ID for the event.
|
||||
* @param {string} opts.user The user ID for the event.
|
||||
* @param {string} opts.msg Optional. The content.body for the event.
|
||||
* @param {MatrixEvent} opts.replyToMessage The replied message
|
||||
* @param {boolean} opts.event True to make a MatrixEvent.
|
||||
* @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters.
|
||||
* @return {Object|MatrixEvent} The event
|
||||
*/
|
||||
export function mkReplyMessage(opts: IReplyMessageOpts, client?: MatrixClient): object | MatrixEvent {
|
||||
const eventOpts: IEventOpts = {
|
||||
...opts,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
"msgtype": MsgType.Text,
|
||||
"body": opts.msg,
|
||||
"m.relates_to": {
|
||||
"rel_type": "m.in_reply_to",
|
||||
"event_id": opts.replyToMessage.getId(),
|
||||
"m.in_reply_to": {
|
||||
"event_id": opts.replyToMessage.getId(),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (!eventOpts.content.body) {
|
||||
eventOpts.content.body = "Random->" + Math.random();
|
||||
}
|
||||
return mkEvent(eventOpts, client);
|
||||
}
|
||||
|
||||
/**
|
||||
* A mock implementation of webstorage
|
||||
*
|
||||
|
||||
@@ -423,6 +423,7 @@ describe("Crypto", function() {
|
||||
await client.crypto.bootstrapSecretStorage({
|
||||
createSecretStorageKey,
|
||||
});
|
||||
client.stopClient();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -128,6 +128,7 @@ describe('DeviceList', function() {
|
||||
return prom1.then(() => {
|
||||
const storedKeys = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
|
||||
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
|
||||
dl.stop();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -170,6 +171,7 @@ describe('DeviceList', function() {
|
||||
|
||||
const prom3 = dl2.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
|
||||
dl2.stop();
|
||||
|
||||
queryDefer3.resolve(utils.deepCopy(signedDeviceList));
|
||||
|
||||
@@ -178,6 +180,7 @@ describe('DeviceList', function() {
|
||||
}).then(() => {
|
||||
const storedKeys = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
|
||||
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
|
||||
dl.stop();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -204,6 +207,7 @@ describe('DeviceList', function() {
|
||||
expect(Object.keys(storedKeys1)).toEqual(['HGKAWHRVJQ']);
|
||||
const storedKeys2 = dl.getRawStoredDevicesForUser('@test2:sw1v.org');
|
||||
expect(Object.keys(storedKeys2)).toEqual(['QJVRHWAKGH']);
|
||||
dl.stop();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -596,6 +596,8 @@ describe("MegolmDecryption", function() {
|
||||
});
|
||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||
await sendPromise;
|
||||
aliceClient.stopClient();
|
||||
bobClient.stopClient();
|
||||
});
|
||||
|
||||
it("throws an error describing why it doesn't have a key", async function() {
|
||||
@@ -666,6 +668,8 @@ describe("MegolmDecryption", function() {
|
||||
session_id: "session_id2",
|
||||
},
|
||||
}))).rejects.toThrow("The sender has blocked you.");
|
||||
aliceClient.stopClient();
|
||||
bobClient.stopClient();
|
||||
});
|
||||
|
||||
it("throws an error describing the lack of an olm session", async function() {
|
||||
@@ -749,6 +753,8 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
origin_server_ts: now,
|
||||
}))).rejects.toThrow("The sender was unable to establish a secure channel.");
|
||||
aliceClient.stopClient();
|
||||
bobClient.stopClient();
|
||||
});
|
||||
|
||||
it("throws an error to indicate a wedged olm session", async function() {
|
||||
@@ -799,5 +805,7 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
origin_server_ts: now,
|
||||
}))).rejects.toThrow("The secure channel with the sender was corrupted.");
|
||||
aliceClient.stopClient();
|
||||
bobClient.stopClient();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -329,6 +329,7 @@ describe("MegolmBackup", function() {
|
||||
);
|
||||
}).then(() => {
|
||||
expect(numCalls).toBe(1);
|
||||
client.stopClient();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -411,6 +412,7 @@ describe("MegolmBackup", function() {
|
||||
);
|
||||
}).then(() => {
|
||||
expect(numCalls).toBe(1);
|
||||
client.stopClient();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -487,6 +489,7 @@ describe("MegolmBackup", function() {
|
||||
}),
|
||||
]);
|
||||
expect(numCalls).toBe(2);
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it('retries when a backup fails', function() {
|
||||
@@ -593,6 +596,7 @@ describe("MegolmBackup", function() {
|
||||
);
|
||||
}).then(() => {
|
||||
expect(numCalls).toBe(2);
|
||||
client.stopClient();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -100,6 +100,7 @@ describe("Cross Signing", function() {
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
expect(alice.uploadDeviceSigningKeys).toHaveBeenCalled();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should abort bootstrap if device signing auth fails", async function() {
|
||||
@@ -151,6 +152,7 @@ describe("Cross Signing", function() {
|
||||
}
|
||||
}
|
||||
expect(bootstrapDidThrow).toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should upload a signature when a user is verified", async function() {
|
||||
@@ -182,6 +184,7 @@ describe("Cross Signing", function() {
|
||||
await alice.setDeviceVerified("@bob:example.com", "bobs+master+pubkey", true);
|
||||
// Alice should send a signature of Bob's key to the server
|
||||
await promise;
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it.skip("should get cross-signing keys from sync", async function() {
|
||||
@@ -353,6 +356,7 @@ describe("Cross Signing", function() {
|
||||
expect(aliceDeviceTrust.isLocallyVerified()).toBeTruthy();
|
||||
expect(aliceDeviceTrust.isTofu()).toBeTruthy();
|
||||
expect(aliceDeviceTrust.isVerified()).toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should use trust chain to determine device verification", async function() {
|
||||
@@ -437,6 +441,7 @@ describe("Cross Signing", function() {
|
||||
expect(bobDeviceTrust2.isCrossSigningVerified()).toBeTruthy();
|
||||
expect(bobDeviceTrust2.isLocallyVerified()).toBeFalsy();
|
||||
expect(bobDeviceTrust2.isTofu()).toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it.skip("should trust signatures received from other devices", async function() {
|
||||
@@ -600,6 +605,7 @@ describe("Cross Signing", function() {
|
||||
expect(bobDeviceTrust.isCrossSigningVerified()).toBeTruthy();
|
||||
expect(bobDeviceTrust.isLocallyVerified()).toBeFalsy();
|
||||
expect(bobDeviceTrust.isTofu()).toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should dis-trust an unsigned device", async function() {
|
||||
@@ -669,6 +675,7 @@ describe("Cross Signing", function() {
|
||||
const bobDeviceTrust2 = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
|
||||
expect(bobDeviceTrust2.isVerified()).toBeFalsy();
|
||||
expect(bobDeviceTrust2.isTofu()).toBeFalsy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should dis-trust a user when their ssk changes", async function() {
|
||||
@@ -805,6 +812,7 @@ describe("Cross Signing", function() {
|
||||
|
||||
const bobDeviceTrust4 = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
|
||||
expect(bobDeviceTrust4.isCrossSigningVerified()).toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should offer to upgrade device verifications to cross-signing", async function() {
|
||||
@@ -882,6 +890,8 @@ describe("Cross Signing", function() {
|
||||
const bobTrust3 = alice.checkUserTrust("@bob:example.com");
|
||||
expect(bobTrust3.isCrossSigningVerified()).toBeTruthy();
|
||||
expect(bobTrust3.isTofu()).toBeTruthy();
|
||||
alice.stopClient();
|
||||
bob.stopClient();
|
||||
});
|
||||
|
||||
it(
|
||||
@@ -954,6 +964,7 @@ describe("Cross Signing", function() {
|
||||
expect(alice.checkDeviceTrust(aliceCrossSignedDevice.device_id).isCrossSigningVerified()).toBeFalsy();
|
||||
// ... but we do acknowledge that the device is signed by them
|
||||
expect(alice.checkIfOwnDeviceCrossSigned(aliceCrossSignedDevice.device_id)).toBeTruthy();
|
||||
alice.stopClient();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1016,5 +1027,6 @@ describe("Cross Signing", function() {
|
||||
});
|
||||
|
||||
expect(alice.checkIfOwnDeviceCrossSigned(aliceNotCrossSignedDevice.device_id)).toBeFalsy();
|
||||
alice.stopClient();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -136,6 +136,7 @@ describe("Secrets", function() {
|
||||
expect(await secretStorage.get("foo")).toBe("bar");
|
||||
|
||||
expect(getKey).toHaveBeenCalled();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should throw if given a key that doesn't exist", async function() {
|
||||
@@ -150,6 +151,7 @@ describe("Secrets", function() {
|
||||
expect(true).toBeFalsy();
|
||||
} catch (e) {
|
||||
}
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should refuse to encrypt with zero keys", async function() {
|
||||
@@ -162,6 +164,7 @@ describe("Secrets", function() {
|
||||
expect(true).toBeFalsy();
|
||||
} catch (e) {
|
||||
}
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should encrypt with default key if keys is null", async function() {
|
||||
@@ -203,6 +206,7 @@ describe("Secrets", function() {
|
||||
|
||||
const accountData = alice.getAccountData('foo');
|
||||
expect(accountData.getContent().encrypted).toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should refuse to encrypt if no keys given and no default key", async function() {
|
||||
@@ -215,6 +219,7 @@ describe("Secrets", function() {
|
||||
expect(true).toBeFalsy();
|
||||
} catch (e) {
|
||||
}
|
||||
alice.stopClient();
|
||||
});
|
||||
|
||||
it("should request secrets from other clients", async function() {
|
||||
@@ -273,6 +278,8 @@ describe("Secrets", function() {
|
||||
const secret = await request.promise;
|
||||
|
||||
expect(secret).toBe("bar");
|
||||
osborne2.stop();
|
||||
vax.stop();
|
||||
});
|
||||
|
||||
describe("bootstrap", function() {
|
||||
@@ -340,6 +347,7 @@ describe("Secrets", function() {
|
||||
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
|
||||
.toBeTruthy();
|
||||
expect(await secretStorage.hasKey()).toBeTruthy();
|
||||
bob.stopClient();
|
||||
});
|
||||
|
||||
it("bootstraps when cross-signing keys in secret storage", async function() {
|
||||
@@ -406,6 +414,7 @@ describe("Secrets", function() {
|
||||
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
|
||||
.toBeTruthy();
|
||||
expect(await secretStorage.hasKey()).toBeTruthy();
|
||||
bob.stopClient();
|
||||
});
|
||||
|
||||
it("adds passphrase checking if it's lacking", async function() {
|
||||
@@ -538,6 +547,7 @@ describe("Secrets", function() {
|
||||
expect(keyInfo).toHaveProperty("mac");
|
||||
expect(alice.checkSecretStorageKey(secretStorageKeys.key_id, keyInfo))
|
||||
.toBeTruthy();
|
||||
alice.stopClient();
|
||||
});
|
||||
it("fixes backup keys in the wrong format", async function() {
|
||||
let crossSigningKeys = {
|
||||
@@ -668,6 +678,7 @@ describe("Secrets", function() {
|
||||
expect(backupKey.encrypted).toHaveProperty("key_id");
|
||||
expect(await alice.getSecret("m.megolm_backup.v1"))
|
||||
.toEqual("ey0GB1kB6jhOWgwiBUMIWg==");
|
||||
alice.stopClient();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,5 +78,8 @@ describe("verification request integration tests with crypto layer", function()
|
||||
|
||||
// XXX: Private function access (but it's a test, so we're okay)
|
||||
aliceVerifier.endTimer();
|
||||
|
||||
alice.stop();
|
||||
bob.stop();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -377,6 +377,9 @@ describe("SAS verification", function() {
|
||||
.not.toHaveBeenCalled();
|
||||
expect(bob.client.setDeviceVerified)
|
||||
.not.toHaveBeenCalled();
|
||||
|
||||
alice.stop();
|
||||
bob.stop();
|
||||
});
|
||||
|
||||
describe("verification in DM", function() {
|
||||
|
||||
@@ -44,6 +44,10 @@ describe("eventMapperFor", function() {
|
||||
rooms = [];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it("should de-duplicate MatrixEvent instances by means of findEventById on the room object", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const room = new Room(roomId, client, userId);
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
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 {
|
||||
EventTimeline,
|
||||
EventTimelineSet,
|
||||
EventType,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
Room,
|
||||
} from '../../src';
|
||||
|
||||
describe('EventTimelineSet', () => {
|
||||
const roomId = '!foo:bar';
|
||||
const userA = "@alice:bar";
|
||||
|
||||
let room: Room;
|
||||
let eventTimeline: EventTimeline;
|
||||
let eventTimelineSet: EventTimelineSet;
|
||||
let client: MatrixClient;
|
||||
|
||||
let messageEvent: MatrixEvent;
|
||||
let replyEvent: MatrixEvent;
|
||||
|
||||
const itShouldReturnTheRelatedEvents = () => {
|
||||
it('should return the related events', () => {
|
||||
eventTimelineSet.aggregateRelations(messageEvent);
|
||||
const relations = eventTimelineSet.getRelationsForEvent(
|
||||
messageEvent.getId(),
|
||||
"m.in_reply_to",
|
||||
EventType.RoomMessage,
|
||||
);
|
||||
expect(relations).toBeDefined();
|
||||
expect(relations.getRelations().length).toBe(1);
|
||||
expect(relations.getRelations()[0].getId()).toBe(replyEvent.getId());
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
client = utils.mock(MatrixClient, 'MatrixClient');
|
||||
room = new Room(roomId, client, userA);
|
||||
eventTimelineSet = new EventTimelineSet(room, {
|
||||
unstableClientRelationAggregation: true,
|
||||
});
|
||||
eventTimeline = new EventTimeline(eventTimelineSet);
|
||||
messageEvent = utils.mkMessage({
|
||||
room: roomId,
|
||||
user: userA,
|
||||
msg: 'Hi!',
|
||||
event: true,
|
||||
}) as MatrixEvent;
|
||||
replyEvent = utils.mkReplyMessage({
|
||||
room: roomId,
|
||||
user: userA,
|
||||
msg: 'Hoo!',
|
||||
event: true,
|
||||
replyToMessage: messageEvent,
|
||||
}) as MatrixEvent;
|
||||
});
|
||||
|
||||
describe('aggregateRelations', () => {
|
||||
describe('with unencrypted events', () => {
|
||||
beforeEach(() => {
|
||||
eventTimelineSet.addEventsToTimeline(
|
||||
[
|
||||
messageEvent,
|
||||
replyEvent,
|
||||
],
|
||||
true,
|
||||
eventTimeline,
|
||||
'foo',
|
||||
);
|
||||
});
|
||||
|
||||
itShouldReturnTheRelatedEvents();
|
||||
});
|
||||
|
||||
describe('with events to be decrypted', () => {
|
||||
let messageEventShouldAttemptDecryptionSpy: jest.SpyInstance;
|
||||
let messageEventIsDecryptionFailureSpy: jest.SpyInstance;
|
||||
|
||||
let replyEventShouldAttemptDecryptionSpy: jest.SpyInstance;
|
||||
let replyEventIsDecryptionFailureSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
messageEventShouldAttemptDecryptionSpy = jest.spyOn(messageEvent, 'shouldAttemptDecryption');
|
||||
messageEventShouldAttemptDecryptionSpy.mockReturnValue(true);
|
||||
messageEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, 'isDecryptionFailure');
|
||||
|
||||
replyEventShouldAttemptDecryptionSpy = jest.spyOn(replyEvent, 'shouldAttemptDecryption');
|
||||
replyEventShouldAttemptDecryptionSpy.mockReturnValue(true);
|
||||
replyEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, 'isDecryptionFailure');
|
||||
|
||||
eventTimelineSet.addEventsToTimeline(
|
||||
[
|
||||
messageEvent,
|
||||
replyEvent,
|
||||
],
|
||||
true,
|
||||
eventTimeline,
|
||||
'foo',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not return the related events', () => {
|
||||
eventTimelineSet.aggregateRelations(messageEvent);
|
||||
const relations = eventTimelineSet.getRelationsForEvent(
|
||||
messageEvent.getId(),
|
||||
"m.in_reply_to",
|
||||
EventType.RoomMessage,
|
||||
);
|
||||
expect(relations).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('after decryption', () => {
|
||||
beforeEach(() => {
|
||||
// simulate decryption failure once
|
||||
messageEventIsDecryptionFailureSpy.mockReturnValue(true);
|
||||
replyEventIsDecryptionFailureSpy.mockReturnValue(true);
|
||||
|
||||
messageEvent.emit(MatrixEventEvent.Decrypted, messageEvent);
|
||||
replyEvent.emit(MatrixEventEvent.Decrypted, replyEvent);
|
||||
|
||||
// simulate decryption
|
||||
messageEventIsDecryptionFailureSpy.mockReturnValue(false);
|
||||
replyEventIsDecryptionFailureSpy.mockReturnValue(false);
|
||||
|
||||
messageEventShouldAttemptDecryptionSpy.mockReturnValue(false);
|
||||
replyEventShouldAttemptDecryptionSpy.mockReturnValue(false);
|
||||
|
||||
messageEvent.emit(MatrixEventEvent.Decrypted, messageEvent);
|
||||
replyEvent.emit(MatrixEventEvent.Decrypted, replyEvent);
|
||||
});
|
||||
|
||||
itShouldReturnTheRelatedEvents();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -118,6 +118,7 @@ describe("MatrixClient", function() {
|
||||
method: method,
|
||||
path: path,
|
||||
};
|
||||
pendingLookup.promise.abort = () => {}; // to make it a valid IAbortablePromise
|
||||
return pendingLookup.promise;
|
||||
}
|
||||
if (next.path === path && next.method === method) {
|
||||
@@ -150,6 +151,10 @@ describe("MatrixClient", function() {
|
||||
}
|
||||
return Promise.resolve(next.data);
|
||||
}
|
||||
// Jest doesn't let us have custom expectation errors, so if you're seeing this then
|
||||
// you forgot to handle at least 1 pending request. Check your tests to ensure your
|
||||
// number of expectations lines up with your number of requests made, and that those
|
||||
// requests match your expectations.
|
||||
expect(true).toBe(false);
|
||||
return new Promise(() => {});
|
||||
}
|
||||
@@ -205,6 +210,7 @@ describe("MatrixClient", function() {
|
||||
client.http.authedRequest.mockImplementation(function() {
|
||||
return new Promise(() => {});
|
||||
});
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it("should create (unstable) file trees", async () => {
|
||||
@@ -725,18 +731,16 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
describe("guest rooms", function() {
|
||||
it("should only do /sync calls (without filter/pushrules)", function(done) {
|
||||
httpLookups = []; // no /pushrules or /filterw
|
||||
it("should only do /sync calls (without filter/pushrules)", async function() {
|
||||
httpLookups = []; // no /pushrules or /filter
|
||||
httpLookups.push({
|
||||
method: "GET",
|
||||
path: "/sync",
|
||||
data: SYNC_DATA,
|
||||
thenCall: function() {
|
||||
done();
|
||||
},
|
||||
});
|
||||
client.setGuest(true);
|
||||
client.startClient();
|
||||
await client.startClient();
|
||||
expect(httpLookups.length).toBe(0);
|
||||
});
|
||||
|
||||
xit("should be able to peek into a room using peekInRoom", function(done) {
|
||||
@@ -926,6 +930,7 @@ describe("MatrixClient", function() {
|
||||
};
|
||||
client.crypto = { // mock crypto
|
||||
encryptEvent: (event, room) => new Promise(() => {}),
|
||||
stop: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1191,4 +1196,26 @@ describe("MatrixClient", function() {
|
||||
passwordTest({ auth, new_password: newPassword, logout_devices: false }, callback);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getLocalAliases", () => {
|
||||
it("should call the right endpoint", async () => {
|
||||
const response = {
|
||||
aliases: ["#woop:example.org", "#another:example.org"],
|
||||
};
|
||||
client.http.authedRequest.mockClear().mockResolvedValue(response);
|
||||
|
||||
const roomId = "!whatever:example.org";
|
||||
const result = await client.getLocalAliases(roomId);
|
||||
|
||||
// Current version of the endpoint we support is v3
|
||||
const [callback, method, path, queryParams, data, opts] = client.http.authedRequest.mock.calls[0];
|
||||
expect(callback).toBeFalsy();
|
||||
expect(data).toBeFalsy();
|
||||
expect(method).toBe('GET');
|
||||
expect(path).toEqual(`/rooms/${encodeURIComponent(roomId)}/aliases`);
|
||||
expect(opts).toMatchObject({ prefix: "/_matrix/client/v3" });
|
||||
expect(queryParams).toBeFalsy();
|
||||
expect(result!.aliases).toEqual(response.aliases);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -168,6 +168,8 @@ describe("Relations", function() {
|
||||
await relations.setTargetEvent(originalTopic);
|
||||
expect(originalTopic.replacingEvent()).toBe(null);
|
||||
expect(originalTopic.getContent().topic).toBe("orig");
|
||||
expect(badlyEditedTopic.isRelation()).toBe(false);
|
||||
expect(badlyEditedTopic.isRelation("m.replace")).toBe(false);
|
||||
|
||||
await relations.addEvent(badlyEditedTopic);
|
||||
expect(originalTopic.replacingEvent()).toBe(null);
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
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 'fake-indexeddb/auto';
|
||||
import 'jest-localstorage-mock';
|
||||
|
||||
import { IndexedDBStore, IStateEventWithRoomId } from "../../../src";
|
||||
import { emitPromise } from "../../test-utils/test-utils";
|
||||
import { LocalIndexedDBStoreBackend } from "../../../src/store/indexeddb-local-backend";
|
||||
|
||||
describe("IndexedDBStore", () => {
|
||||
it("should degrade to MemoryStore on IDB errors", async () => {
|
||||
const roomId = "!room:id";
|
||||
const store = new IndexedDBStore({
|
||||
indexedDB: indexedDB,
|
||||
dbName: "database",
|
||||
localStorage,
|
||||
});
|
||||
await store.startup();
|
||||
|
||||
const member1: IStateEventWithRoomId = {
|
||||
room_id: roomId,
|
||||
event_id: "!ev1:id",
|
||||
sender: "@user1:id",
|
||||
state_key: "@user1:id",
|
||||
type: "m.room.member",
|
||||
origin_server_ts: 123,
|
||||
content: {},
|
||||
};
|
||||
const member2: IStateEventWithRoomId = {
|
||||
room_id: roomId,
|
||||
event_id: "!ev2:id",
|
||||
sender: "@user2:id",
|
||||
state_key: "@user2:id",
|
||||
type: "m.room.member",
|
||||
origin_server_ts: 123,
|
||||
content: {},
|
||||
};
|
||||
|
||||
expect(await store.getOutOfBandMembers(roomId)).toBe(null);
|
||||
await store.setOutOfBandMembers(roomId, [member1]);
|
||||
expect(await store.getOutOfBandMembers(roomId)).toHaveLength(1);
|
||||
|
||||
// Simulate a broken IDB
|
||||
(store.backend as LocalIndexedDBStoreBackend)["db"].transaction = (): IDBTransaction => {
|
||||
const err = new Error("Failed to execute 'transaction' on 'IDBDatabase': " +
|
||||
"The database connection is closing.");
|
||||
err.name = "InvalidStateError";
|
||||
throw err;
|
||||
};
|
||||
|
||||
expect(await store.getOutOfBandMembers(roomId)).toHaveLength(1);
|
||||
await Promise.all([
|
||||
emitPromise(store["emitter"], "degraded"),
|
||||
store.setOutOfBandMembers(roomId, [member1, member2]),
|
||||
]);
|
||||
expect(await store.getOutOfBandMembers(roomId)).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
+27
-12
@@ -61,6 +61,7 @@ import {
|
||||
PREFIX_R0,
|
||||
PREFIX_UNSTABLE,
|
||||
PREFIX_V1,
|
||||
PREFIX_V3,
|
||||
retryNetworkOperation,
|
||||
UploadContentResponseType,
|
||||
} from "./http-api";
|
||||
@@ -633,6 +634,19 @@ interface IJoinedMembersResponse {
|
||||
};
|
||||
}
|
||||
|
||||
export interface IRegisterRequestParams {
|
||||
auth?: IAuthData;
|
||||
username?: string;
|
||||
password?: string;
|
||||
refresh_token?: boolean;
|
||||
guest_access_token?: string;
|
||||
x_show_msisdn?: boolean;
|
||||
bind_msisdn?: boolean;
|
||||
bind_email?: boolean;
|
||||
inhibit_login?: boolean;
|
||||
initial_device_display_name?: string;
|
||||
}
|
||||
|
||||
export interface IPublicRoomsChunkRoom {
|
||||
room_id: string;
|
||||
name?: string;
|
||||
@@ -1206,6 +1220,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* clean shutdown.
|
||||
*/
|
||||
public stopClient() {
|
||||
this.crypto?.stop(); // crypto might have been initialised even if the client wasn't fully started
|
||||
|
||||
if (!this.clientRunning) return; // already stopped
|
||||
|
||||
logger.log('stopping MatrixClient');
|
||||
@@ -1215,7 +1231,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
this.syncApi?.stop();
|
||||
this.syncApi = null;
|
||||
|
||||
this.crypto?.stop();
|
||||
this.peekSync?.stopPeeking();
|
||||
|
||||
this.callEventHandler?.stop();
|
||||
@@ -6846,7 +6861,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
guestAccessToken?: string,
|
||||
inhibitLogin?: boolean,
|
||||
callback?: Callback,
|
||||
): Promise<any> { // TODO: Types (many)
|
||||
): Promise<IAuthData> {
|
||||
// backwards compat
|
||||
if (bindThreepids === true) {
|
||||
bindThreepids = { email: true };
|
||||
@@ -6862,7 +6877,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
auth.session = sessionId;
|
||||
}
|
||||
|
||||
const params: any = {
|
||||
const params: IRegisterRequestParams = {
|
||||
auth: auth,
|
||||
refresh_token: true, // always ask for a refresh token - does nothing if unsupported
|
||||
};
|
||||
@@ -6933,8 +6948,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @return {Promise} Resolves: to the /register response
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
public registerRequest(data: any, kind?: string, callback?: Callback): Promise<any> { // TODO: Types
|
||||
const params: any = {};
|
||||
public registerRequest(data: IRegisterRequestParams, kind?: string, callback?: Callback): Promise<IAuthData> {
|
||||
const params: { kind?: string } = {};
|
||||
if (kind) {
|
||||
params.kind = kind;
|
||||
}
|
||||
@@ -7518,16 +7533,16 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} roomId
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* Gets the local aliases for the room. Note: this includes all local aliases, unlike the
|
||||
* curated list from the m.room.canonical_alias state event.
|
||||
* @param {string} roomId The room ID to get local aliases for.
|
||||
* @return {Promise} Resolves: an object with an `aliases` property, containing an array of local aliases
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
public unstableGetLocalAliases(roomId: string, callback?: Callback): Promise<{ aliases: string[] }> {
|
||||
const path = utils.encodeUri("/rooms/$roomId/aliases",
|
||||
{ $roomId: roomId });
|
||||
const prefix = PREFIX_UNSTABLE + "/org.matrix.msc2432";
|
||||
return this.http.authedRequest(callback, Method.Get, path, null, null, { prefix });
|
||||
public getLocalAliases(roomId: string): Promise<{ aliases: string[] }> {
|
||||
const path = utils.encodeUri("/rooms/$roomId/aliases", { $roomId: roomId });
|
||||
const prefix = PREFIX_V3;
|
||||
return this.http.authedRequest(undefined, Method.Get, path, null, null, { prefix });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,7 @@ import * as bs58 from 'bs58';
|
||||
const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01];
|
||||
|
||||
export function encodeRecoveryKey(key: ArrayLike<number>): string {
|
||||
const buf = new Buffer(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
|
||||
const buf = Buffer.alloc(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
|
||||
buf.set(OLM_RECOVERY_KEY_PREFIX, 0);
|
||||
buf.set(key, OLM_RECOVERY_KEY_PREFIX.length);
|
||||
|
||||
|
||||
@@ -841,11 +841,11 @@ export class VerificationRequest<
|
||||
}
|
||||
|
||||
const isUnexpectedRequest = type === REQUEST_TYPE && this.phase !== PHASE_UNSENT;
|
||||
const isUnexpectedReady = type === READY_TYPE && this.phase !== PHASE_REQUESTED;
|
||||
const isUnexpectedReady = type === READY_TYPE && this.phase !== PHASE_REQUESTED && this.phase !== PHASE_STARTED;
|
||||
// only if phase has passed from PHASE_UNSENT should we cancel, because events
|
||||
// are allowed to come in in any order (at least with InRoomChannel). So we only know
|
||||
// we're dealing with a valid request we should participate in once we've moved to PHASE_REQUESTED
|
||||
// before that, we could be looking at somebody elses verification request and we just
|
||||
// we're dealing with a valid request we should participate in once we've moved to PHASE_REQUESTED.
|
||||
// Before that, we could be looking at somebody else's verification request and we just
|
||||
// happen to be in the room
|
||||
if (this.phase !== PHASE_UNSENT && (isUnexpectedRequest || isUnexpectedReady)) {
|
||||
logger.warn(`Cancelling, unexpected ${type} verification ` +
|
||||
|
||||
+7
-2
@@ -48,10 +48,15 @@ TODO:
|
||||
export const PREFIX_R0 = "/_matrix/client/r0";
|
||||
|
||||
/**
|
||||
* A constant representing the URI path for release v1 of the Client-Server HTTP API.
|
||||
* A constant representing the URI path for the legacy release v1 of the Client-Server HTTP API.
|
||||
*/
|
||||
export const PREFIX_V1 = "/_matrix/client/v1";
|
||||
|
||||
/**
|
||||
* A constant representing the URI path for Client-Server API endpoints versioned at v3.
|
||||
*/
|
||||
export const PREFIX_V3 = "/_matrix/client/v3";
|
||||
|
||||
/**
|
||||
* A constant representing the URI path for as-yet unspecified Client-Server HTTP APIs.
|
||||
*/
|
||||
@@ -403,7 +408,7 @@ export class MatrixHttpApi {
|
||||
resp = bodyParser(resp);
|
||||
}
|
||||
} catch (err) {
|
||||
err.http_status = xhr.status;
|
||||
err.httpStatus = xhr.status;
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -46,9 +46,16 @@ export interface IAuthData {
|
||||
session?: string;
|
||||
completed?: string[];
|
||||
flows?: IFlow[];
|
||||
available_flows?: IFlow[];
|
||||
stages?: string[];
|
||||
required_stages?: AuthType[];
|
||||
params?: Record<string, Record<string, any>>;
|
||||
data?: Record<string, string>;
|
||||
errcode?: string;
|
||||
error?: string;
|
||||
user_id?: string;
|
||||
device_id?: string;
|
||||
access_token?: string;
|
||||
}
|
||||
|
||||
export enum AuthType {
|
||||
|
||||
@@ -822,11 +822,20 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
return;
|
||||
}
|
||||
|
||||
const onEventDecrypted = (event: MatrixEvent) => {
|
||||
if (event.isDecryptionFailure()) {
|
||||
// This could for example happen if the encryption keys are not yet available.
|
||||
// The event may still be decrypted later. Register the listener again.
|
||||
event.once(MatrixEventEvent.Decrypted, onEventDecrypted);
|
||||
return;
|
||||
}
|
||||
|
||||
this.aggregateRelations(event);
|
||||
};
|
||||
|
||||
// If the event is currently encrypted, wait until it has been decrypted.
|
||||
if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) {
|
||||
event.once(MatrixEventEvent.Decrypted, () => {
|
||||
this.aggregateRelations(event);
|
||||
});
|
||||
event.once(MatrixEventEvent.Decrypted, onEventDecrypted);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+6
-3
@@ -1290,7 +1290,7 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
|
||||
|
||||
/**
|
||||
* Get whether the event is a relation event, and of a given type if
|
||||
* `relType` is passed in.
|
||||
* `relType` is passed in. State events cannot be relation events
|
||||
*
|
||||
* @param {string?} relType if given, checks that the relation is of the
|
||||
* given type
|
||||
@@ -1300,8 +1300,11 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
|
||||
// Relation info is lifted out of the encrypted content when sent to
|
||||
// encrypted rooms, so we have to check `getWireContent` for this.
|
||||
const relation = this.getWireContent()?.["m.relates_to"];
|
||||
return relation && relation.rel_type && relation.event_id &&
|
||||
((relType && relation.rel_type === relType) || !relType);
|
||||
if (this.isState() && relation?.rel_type === RelationType.Replace) {
|
||||
// State events cannot be m.replace relations
|
||||
return false;
|
||||
}
|
||||
return relation?.rel_type && relation.event_id && (relType ? relation.rel_type === relType : true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -103,7 +103,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
|
||||
|
||||
if (this.relationType === RelationType.Annotation) {
|
||||
this.addAnnotationToAggregation(event);
|
||||
} else if (this.relationType === RelationType.Replace && this.targetEvent) {
|
||||
} else if (this.relationType === RelationType.Replace && this.targetEvent && !this.targetEvent.isState()) {
|
||||
const lastReplacement = await this.getLastReplacement();
|
||||
this.targetEvent.makeReplaced(lastReplacement);
|
||||
}
|
||||
@@ -144,7 +144,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
|
||||
|
||||
if (this.relationType === RelationType.Annotation) {
|
||||
this.removeAnnotationFromAggregation(event);
|
||||
} else if (this.relationType === RelationType.Replace && this.targetEvent) {
|
||||
} else if (this.relationType === RelationType.Replace && this.targetEvent && !this.targetEvent.isState()) {
|
||||
const lastReplacement = await this.getLastReplacement();
|
||||
this.targetEvent.makeReplaced(lastReplacement);
|
||||
}
|
||||
@@ -261,7 +261,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
|
||||
if (this.relationType === RelationType.Annotation) {
|
||||
// Remove the redacted annotation from aggregation by key
|
||||
this.removeAnnotationFromAggregation(redactedEvent);
|
||||
} else if (this.relationType === RelationType.Replace && this.targetEvent) {
|
||||
} else if (this.relationType === RelationType.Replace && this.targetEvent && !this.targetEvent.isState()) {
|
||||
const lastReplacement = await this.getLastReplacement();
|
||||
this.targetEvent.makeReplaced(lastReplacement);
|
||||
}
|
||||
@@ -364,7 +364,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
|
||||
}
|
||||
this.targetEvent = event;
|
||||
|
||||
if (this.relationType === RelationType.Replace) {
|
||||
if (this.relationType === RelationType.Replace && !this.targetEvent.isState()) {
|
||||
const replacement = await this.getLastReplacement();
|
||||
// this is the initial update, so only call it if we already have something
|
||||
// to not emit Event.replaced needlessly
|
||||
|
||||
@@ -84,9 +84,7 @@ const DEFAULT_OVERRIDE_RULES: IPushRule[] = [
|
||||
pattern: EventType.RoomServerAcl,
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
PushRuleActionName.DontNotify,
|
||||
],
|
||||
actions: [],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -127,6 +127,8 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||
private db: IDBDatabase = null;
|
||||
private disconnected = true;
|
||||
private _isNewlyCreated = false;
|
||||
private isPersisting = false;
|
||||
private pendingUserPresenceData: UserTuple[] = [];
|
||||
|
||||
/**
|
||||
* Does the actual reading from and writing to the indexeddb
|
||||
@@ -266,7 +268,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||
reject(err);
|
||||
};
|
||||
}).then((events) => {
|
||||
logger.log(`LL: got ${events && events.length} membershipEvents from storage for room ${roomId} ...`);
|
||||
logger.log(`LL: got ${events?.length} membershipEvents from storage for room ${roomId} ...`);
|
||||
return events;
|
||||
});
|
||||
}
|
||||
@@ -401,11 +403,24 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||
public async syncToDatabase(userTuples: UserTuple[]): Promise<void> {
|
||||
const syncData = this.syncAccumulator.getJSON(true);
|
||||
|
||||
await Promise.all([
|
||||
this.persistUserPresenceEvents(userTuples),
|
||||
this.persistAccountData(syncData.accountData),
|
||||
this.persistSyncData(syncData.nextBatch, syncData.roomsData),
|
||||
]);
|
||||
if (this.isPersisting) {
|
||||
logger.warn("Skipping syncToDatabase() as persist already in flight");
|
||||
this.pendingUserPresenceData.push(...userTuples);
|
||||
return;
|
||||
} else {
|
||||
userTuples.unshift(...this.pendingUserPresenceData);
|
||||
this.isPersisting = true;
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
this.persistUserPresenceEvents(userTuples),
|
||||
this.persistAccountData(syncData.accountData),
|
||||
this.persistSyncData(syncData.nextBatch, syncData.roomsData),
|
||||
]);
|
||||
} finally {
|
||||
this.isPersisting = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -427,7 +442,9 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||
nextBatch,
|
||||
roomsData,
|
||||
}); // put == UPSERT
|
||||
return txnAsPromise(txn).then();
|
||||
return txnAsPromise(txn).then(() => {
|
||||
logger.log("Persisted sync data up to", nextBatch);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -243,7 +243,7 @@ export class IndexedDBStore extends MemoryStore {
|
||||
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
|
||||
* @returns {null} in case the members for this room haven't been stored yet
|
||||
*/
|
||||
public getOutOfBandMembers = this.degradable((roomId: string): Promise<IStateEventWithRoomId[]> => {
|
||||
public getOutOfBandMembers = this.degradable((roomId: string): Promise<IStateEventWithRoomId[] | null> => {
|
||||
return this.backend.getOutOfBandMembers(roomId);
|
||||
}, "getOutOfBandMembers");
|
||||
|
||||
@@ -320,7 +320,7 @@ export class IndexedDBStore extends MemoryStore {
|
||||
// `IndexedDBStore` also maintain the state that `MemoryStore` uses (many are
|
||||
// not overridden at all).
|
||||
if (fallbackFn) {
|
||||
return fallbackFn(...args);
|
||||
return fallbackFn.call(this, ...args);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+7
-10
@@ -55,6 +55,7 @@ import { RoomStateEvent } from "./models/room-state";
|
||||
import { RoomMemberEvent } from "./models/room-member";
|
||||
import { BeaconEvent } from "./models/beacon";
|
||||
import { IEventsResponse } from "./@types/requests";
|
||||
import { IAbortablePromise } from "./@types/partials";
|
||||
|
||||
const DEBUG = true;
|
||||
|
||||
@@ -121,11 +122,6 @@ interface ISyncParams {
|
||||
_cacheBuster?: string | number; // not part of the API itself
|
||||
}
|
||||
|
||||
// http-api mangles an abort method onto its promises
|
||||
interface IRequestPromise<T> extends Promise<T> {
|
||||
abort(): void;
|
||||
}
|
||||
|
||||
type WrappedRoom<T> = T & {
|
||||
room: Room;
|
||||
isBrandNewRoom: boolean;
|
||||
@@ -148,7 +144,7 @@ type WrappedRoom<T> = T & {
|
||||
*/
|
||||
export class SyncApi {
|
||||
private _peekRoom: Room = null;
|
||||
private currentSyncRequest: IRequestPromise<ISyncResponse> = null;
|
||||
private currentSyncRequest: IAbortablePromise<ISyncResponse> = null;
|
||||
private syncState: SyncState = null;
|
||||
private syncStateData: ISyncStateData = null; // additional data (eg. error object for failed sync)
|
||||
private catchingUp = false;
|
||||
@@ -702,9 +698,7 @@ export class SyncApi {
|
||||
global.window.removeEventListener("online", this.onOnline, false);
|
||||
}
|
||||
this.running = false;
|
||||
if (this.currentSyncRequest) {
|
||||
this.currentSyncRequest.abort();
|
||||
}
|
||||
this.currentSyncRequest?.abort();
|
||||
if (this.keepAliveTimer) {
|
||||
clearTimeout(this.keepAliveTimer);
|
||||
this.keepAliveTimer = null;
|
||||
@@ -872,7 +866,7 @@ export class SyncApi {
|
||||
this.doSync(syncOptions);
|
||||
}
|
||||
|
||||
private doSyncRequest(syncOptions: ISyncOptions, syncToken: string): IRequestPromise<ISyncResponse> {
|
||||
private doSyncRequest(syncOptions: ISyncOptions, syncToken: string): IAbortablePromise<ISyncResponse> {
|
||||
const qps = this.getSyncParams(syncOptions, syncToken);
|
||||
return this.client.http.authedRequest<ISyncResponse>(
|
||||
undefined, Method.Get, "/sync", qps as any, undefined,
|
||||
@@ -1166,6 +1160,9 @@ export class SyncApi {
|
||||
room.recalculate();
|
||||
client.store.storeRoom(room);
|
||||
client.emit(ClientEvent.Room, room);
|
||||
} else {
|
||||
// Update room state for invite->reject->invite cycles
|
||||
room.recalculate();
|
||||
}
|
||||
stateEvents.forEach(function(e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
|
||||
@@ -275,7 +275,7 @@ export class CallEventHandler {
|
||||
|
||||
// The following events need a call and a peer connection
|
||||
if (!call || !call.hasPeerConnection) {
|
||||
logger.warn("Discarding an event, we don't have a call/peerConn", type);
|
||||
logger.info(`Discarding possible call event ${event.getId()} as we don't have a call/peerConn`, type);
|
||||
return;
|
||||
}
|
||||
// Ignore remote echo
|
||||
|
||||
Reference in New Issue
Block a user