You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-26 17:03:12 +03:00
Merge branch 'develop' into matthew/fix-flaky-verif-test
This commit is contained in:
@@ -21,3 +21,6 @@ insert_final_newline = true
|
|||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
|||||||
41
.git-blame-ignore-revs
Normal file
41
.git-blame-ignore-revs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Minor white-space adjustments
|
||||||
|
1d1d59c75744e1f6a2be1cb3e0d1bd9ded5f8025
|
||||||
|
# Import ordering and spacing: eslint-plugin-import
|
||||||
|
80aaa6c32b50601f82e0c991c24e5a4590f39463
|
||||||
|
# Minor white-space adjustment
|
||||||
|
8fb036ba2d01fab66dc4373802ccf19b5cac8541
|
||||||
|
# Minor white-space adjustment
|
||||||
|
b63de6a902a9e1f8ffd7697dea33820fc04f028e
|
||||||
|
3ca84cfc491b0987eec1f13f13cae58d2032bf54
|
||||||
|
# Conform to new typescript eslint rules
|
||||||
|
a87858840b57514603f63e2abbbda4f107f05a77
|
||||||
|
5cf6684129a921295f5593173f16f192336fe0a2
|
||||||
|
# Comply with new member-delimiter-style rule
|
||||||
|
b2ad957d298720d3e026b6bd91be0c403338361a
|
||||||
|
# Fix semicolons in TS files
|
||||||
|
e2ec8952e38b8fea3f0ccaa09ecb42feeba0d923
|
||||||
|
# Migrate to `eslint-plugin-matrix-org`
|
||||||
|
# and `babel/...` to `@babel/...` migration
|
||||||
|
09fac77ce0d9bcf6637088c29afab84084f0e739
|
||||||
|
102704e91a70643bcc09721e14b0d909f0ef55c6
|
||||||
|
# Eslint formatting
|
||||||
|
cec00cd303787fa9008b6c48826e75ed438036fa
|
||||||
|
# Minor eslint changes
|
||||||
|
68bb8182e4e62d8f450f80c408c4b231b8725f1b
|
||||||
|
c979ff6696e30ab8983ac416a3590996d84d3560
|
||||||
|
f4a7395e3a3751a1a8e92dd302c49175a3296ad2
|
||||||
|
# eslint --fix for dangley commas on function calls
|
||||||
|
423175f5397910b0afe3112d6fb18283fc7d27d4
|
||||||
|
# eslint ---fix for prefer-const
|
||||||
|
7bca05af644e8b997dae81e568a3913d8f18d7ca
|
||||||
|
# Fix linting on tests
|
||||||
|
cee7f7a280a8c20bafc21c0a2911f60851f7a7ca
|
||||||
|
# eslint --fix
|
||||||
|
0fa9f7c6098822db1ae214f352fd1fe5c248b02c
|
||||||
|
# eslint --fix for lots of white-space
|
||||||
|
5abf6b9f208801c5022a47023150b5846cb0b309
|
||||||
|
# eslint --fix
|
||||||
|
7ed65407e6cdf292ce3cf659310c68d19dcd52b2
|
||||||
|
# Switch to ESLint from JSHint (Google eslint rules as a base)
|
||||||
|
e057956ede9ad1a931ff8050c411aca7907e0394
|
||||||
|
|
||||||
6
.github/codecov.yml
vendored
6
.github/codecov.yml
vendored
@@ -1,6 +0,0 @@
|
|||||||
comment:
|
|
||||||
layout: "diff, files"
|
|
||||||
behavior: default
|
|
||||||
require_changes: false
|
|
||||||
require_base: no
|
|
||||||
require_head: no
|
|
||||||
25
.github/workflows/notify-downstream.yaml
vendored
Normal file
25
.github/workflows/notify-downstream.yaml
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
name: Notify Downstream Projects
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ develop ]
|
||||||
|
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
jobs:
|
||||||
|
notify-downstream:
|
||||||
|
continue-on-error: true
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- repo: vector-im/element-web
|
||||||
|
event: element-web-notify
|
||||||
|
- repo: matrix-org/matrix-react-sdk
|
||||||
|
event: upstream-sdk-notify
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
|
||||||
|
uses: peter-evans/repository-dispatch@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
repository: ${{ matrix.repo }}
|
||||||
|
event-type: ${{ matrix.event }}
|
||||||
59
.github/workflows/pr_details.yml
vendored
Normal file
59
.github/workflows/pr_details.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# 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:
|
||||||
|
inputs:
|
||||||
|
owner:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: The github username of the owner of the head branch
|
||||||
|
branch:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: The name of the head branch
|
||||||
|
outputs:
|
||||||
|
pr_id:
|
||||||
|
description: The ID of the PR found
|
||||||
|
value: ${{ jobs.prdetails.outputs.pr_id }}
|
||||||
|
head_branch:
|
||||||
|
description: The head branch of the PR found
|
||||||
|
value: ${{ jobs.prdetails.outputs.head_branch }}
|
||||||
|
base_branch:
|
||||||
|
description: The base branch of the PR found
|
||||||
|
value: ${{ jobs.prdetails.outputs.base_branch }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
prdetails:
|
||||||
|
name: Find PR Details
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
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"
|
||||||
|
outputs:
|
||||||
|
pr_id: ${{ steps.details.outputs.prnumber }}
|
||||||
|
head_branch: ${{ steps.details.outputs.headref }}
|
||||||
|
base_branch: ${{ steps.details.outputs.baseref }}
|
||||||
12
.github/workflows/preview_changelog.yaml
vendored
12
.github/workflows/preview_changelog.yaml
vendored
@@ -1,12 +0,0 @@
|
|||||||
name: Preview Changelog
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [ opened, edited, labeled ]
|
|
||||||
jobs:
|
|
||||||
changelog:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Preview Changelog
|
|
||||||
uses: matrix-org/allchange@main
|
|
||||||
with:
|
|
||||||
ghToken: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
28
.github/workflows/pull_request.yaml
vendored
Normal file
28
.github/workflows/pull_request.yaml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: Pull Request
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [ opened, edited, labeled, unlabeled, synchronize ]
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
jobs:
|
||||||
|
changelog:
|
||||||
|
name: Preview Changelog
|
||||||
|
if: github.event.action != 'synchronize'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: matrix-org/allchange@main
|
||||||
|
with:
|
||||||
|
ghToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
enforce-label:
|
||||||
|
name: Enforce Labels
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: read
|
||||||
|
steps:
|
||||||
|
- uses: yogevbd/enforce-label-action@2.1.0
|
||||||
|
with:
|
||||||
|
REQUIRED_LABELS_ANY: "T-Defect,T-Deprecation,T-Enhancement,T-Task"
|
||||||
|
BANNED_LABELS: "X-Blocked"
|
||||||
|
BANNED_LABELS_DESCRIPTION: "Preventing merge whilst PR is marked blocked!"
|
||||||
91
.github/workflows/sonarcloud.yml
vendored
Normal file
91
.github/workflows/sonarcloud.yml
vendored
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
name: SonarCloud
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
repo:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: The full name of the repo in org/repo format
|
||||||
|
head_branch:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: The name of the head branch
|
||||||
|
# We cannot use ${{ github.sha }} here as for pull requests it'll be a simulated merge commit instead
|
||||||
|
revision:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: The git revision with which this sonar run should be associated
|
||||||
|
|
||||||
|
# Coverage specific parameters, assumes coverage reports live in a /coverage/ directory
|
||||||
|
coverage_workflow_name:
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
description: The name of the workflow which uploaded the `coverage` artifact, if any
|
||||||
|
coverage_run_id:
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
description: The run_id of the workflow which upload the coverage relevant to this run
|
||||||
|
|
||||||
|
# PR specific parameters
|
||||||
|
pr_id:
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
description: The ID number of the PR if this workflow is being triggered due to one
|
||||||
|
base_branch:
|
||||||
|
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
|
||||||
|
jobs:
|
||||||
|
analysis:
|
||||||
|
name: Analysis
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: "🧮 Checkout code"
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
repository: ${{ inputs.repo }}
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
- name: "📥 Download Coverage Report"
|
||||||
|
uses: dawidd6/action-download-artifact@v2
|
||||||
|
if: inputs.coverage_workflow_name
|
||||||
|
with:
|
||||||
|
workflow: ${{ inputs.coverage_workflow_name }}
|
||||||
|
run_id: ${{ inputs.coverage_run_id }}
|
||||||
|
name: coverage
|
||||||
|
path: coverage
|
||||||
|
|
||||||
|
- name: "🔍 Read package.json version"
|
||||||
|
id: version
|
||||||
|
uses: martinbeentjes/npm-get-version-action@main
|
||||||
|
|
||||||
|
- name: "🩻 SonarCloud Scan"
|
||||||
|
uses: SonarSource/sonarcloud-github-action@master
|
||||||
|
with:
|
||||||
|
args: >
|
||||||
|
-Dsonar.projectVersion=${{ steps.version.outputs.current-version }}
|
||||||
|
-Dsonar.scm.revision=${{ inputs.revision }}
|
||||||
|
-Dsonar.pullrequest.key=${{ inputs.pr_id }}
|
||||||
|
-Dsonar.pullrequest.branch=${{ inputs.pr_id && inputs.head_branch }}
|
||||||
|
-Dsonar.pullrequest.base=${{ inputs.pr_id && inputs.base_branch }}
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
|
||||||
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
37
.github/workflows/sonarqube.yml
vendored
Normal file
37
.github/workflows/sonarqube.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
name: SonarQube
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: [ "Tests" ]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
jobs:
|
||||||
|
prdetails:
|
||||||
|
name: ℹ️ PR Details
|
||||||
|
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
|
||||||
|
uses: matrix-org/matrix-js-sdk/.github/workflows/pr_details.yml@develop
|
||||||
|
with:
|
||||||
|
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||||
|
branch: ${{ github.event.workflow_run.head_branch }}
|
||||||
|
|
||||||
|
sonarqube:
|
||||||
|
name: 🩻 SonarQube
|
||||||
|
needs: prdetails
|
||||||
|
# Only wait for prdetails if it isn't skipped
|
||||||
|
if: |
|
||||||
|
always() &&
|
||||||
|
(needs.prdetails.result == 'success' || needs.prdetails.result == 'skipped') &&
|
||||||
|
github.event.workflow_run.conclusion == 'success'
|
||||||
|
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
|
||||||
|
with:
|
||||||
|
repo: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||||
|
pr_id: ${{ needs.prdetails.outputs.pr_id }}
|
||||||
|
head_branch: ${{ needs.prdetails.outputs.head_branch || github.event.workflow_run.head_branch }}
|
||||||
|
base_branch: ${{ needs.prdetails.outputs.base_branch }}
|
||||||
|
revision: ${{ github.event.workflow_run.head_sha }}
|
||||||
|
coverage_workflow_name: tests.yml
|
||||||
|
coverage_run_id: ${{ github.event.workflow_run.id }}
|
||||||
|
secrets:
|
||||||
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
56
.github/workflows/static_analysis.yml
vendored
Normal file
56
.github/workflows/static_analysis.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
name: Static Analysis
|
||||||
|
on:
|
||||||
|
pull_request: { }
|
||||||
|
push:
|
||||||
|
branches: [ develop, master ]
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
jobs:
|
||||||
|
ts_lint:
|
||||||
|
name: "Typescript Syntax Check"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
|
- name: Install Deps
|
||||||
|
run: "yarn install"
|
||||||
|
|
||||||
|
- name: Typecheck
|
||||||
|
run: "yarn run lint:types"
|
||||||
|
|
||||||
|
js_lint:
|
||||||
|
name: "ESLint"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
|
- name: Install Deps
|
||||||
|
run: "yarn install"
|
||||||
|
|
||||||
|
- name: Run Linter
|
||||||
|
run: "yarn run lint:js"
|
||||||
|
|
||||||
|
docs:
|
||||||
|
name: "JSDoc Checker"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
|
- name: Install Deps
|
||||||
|
run: "yarn install"
|
||||||
|
|
||||||
|
- name: Generate Docs
|
||||||
|
run: "yarn run gendoc"
|
||||||
19
.github/workflows/test_coverage.yml
vendored
19
.github/workflows/test_coverage.yml
vendored
@@ -1,19 +0,0 @@
|
|||||||
name: Test coverage
|
|
||||||
on:
|
|
||||||
pull_request: {}
|
|
||||||
push:
|
|
||||||
branches: [develop, main, master]
|
|
||||||
jobs:
|
|
||||||
test-coverage:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Run tests with coverage
|
|
||||||
run: "yarn install && yarn build && yarn coverage"
|
|
||||||
|
|
||||||
- name: Upload coverage
|
|
||||||
uses: codecov/codecov-action@v2
|
|
||||||
with:
|
|
||||||
verbose: true
|
|
||||||
37
.github/workflows/tests.yml
vendored
Normal file
37
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
name: Tests
|
||||||
|
on:
|
||||||
|
pull_request: { }
|
||||||
|
push:
|
||||||
|
branches: [ develop, master ]
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
jobs:
|
||||||
|
jest:
|
||||||
|
name: Jest
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Yarn cache
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: "yarn install"
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: "yarn build"
|
||||||
|
|
||||||
|
- name: Run tests with coverage
|
||||||
|
run: "yarn coverage --ci"
|
||||||
|
|
||||||
|
- name: Upload Artifact
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: coverage
|
||||||
|
path: |
|
||||||
|
coverage
|
||||||
|
!coverage/lcov-report
|
||||||
38
.github/workflows/upgrade_dependencies.yml
vendored
Normal file
38
.github/workflows/upgrade_dependencies.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: Upgrade Dependencies
|
||||||
|
on:
|
||||||
|
workflow_dispatch: { }
|
||||||
|
workflow_call:
|
||||||
|
secrets:
|
||||||
|
ELEMENT_BOT_TOKEN:
|
||||||
|
required: true
|
||||||
|
jobs:
|
||||||
|
upgrade:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
|
- name: Upgrade
|
||||||
|
run: yarn upgrade && yarn install
|
||||||
|
|
||||||
|
- name: Create Pull Request
|
||||||
|
id: cpr
|
||||||
|
uses: peter-evans/create-pull-request@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
branch: actions/upgrade-deps
|
||||||
|
delete-branch: true
|
||||||
|
title: Upgrade dependencies
|
||||||
|
labels: |
|
||||||
|
Dependencies
|
||||||
|
T-Task
|
||||||
|
|
||||||
|
- name: Enable automerge
|
||||||
|
uses: peter-evans/enable-pull-request-automerge@v2
|
||||||
|
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||||
78
CHANGELOG.md
78
CHANGELOG.md
@@ -1,3 +1,75 @@
|
|||||||
|
Changes in [17.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v17.2.0) (2022-05-10)
|
||||||
|
==================================================================================================
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
* Live location sharing: handle encrypted messages in processBeaconEvents ([\#2327](https://github.com/matrix-org/matrix-js-sdk/pull/2327)).
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
* Fix race conditions around threads ([\#2331](https://github.com/matrix-org/matrix-js-sdk/pull/2331)). Fixes vector-im/element-web#21627.
|
||||||
|
* Ignore m.replace relations on state events, they're invalid ([\#2306](https://github.com/matrix-org/matrix-js-sdk/pull/2306)). Fixes vector-im/element-web#21851.
|
||||||
|
* fix example in readme ([\#2315](https://github.com/matrix-org/matrix-js-sdk/pull/2315)).
|
||||||
|
* Don't decrement the length count of a thread when root redacted ([\#2314](https://github.com/matrix-org/matrix-js-sdk/pull/2314)).
|
||||||
|
* Prevent attempt to create thread with id "undefined" ([\#2308](https://github.com/matrix-org/matrix-js-sdk/pull/2308)).
|
||||||
|
* Update threads handling for replies-to-thread-responses as per MSC update ([\#2305](https://github.com/matrix-org/matrix-js-sdk/pull/2305)). Fixes vector-im/element-web#19678.
|
||||||
|
|
||||||
|
Changes in [17.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v17.1.0) (2022-04-26)
|
||||||
|
==================================================================================================
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
* Add MatrixClient.doesServerSupportLogoutDevices() for MSC2457 ([\#2297](https://github.com/matrix-org/matrix-js-sdk/pull/2297)).
|
||||||
|
* Live location sharing - expose room liveBeaconIds ([\#2296](https://github.com/matrix-org/matrix-js-sdk/pull/2296)).
|
||||||
|
* Support for MSC2457 logout_devices param for setPassword() ([\#2285](https://github.com/matrix-org/matrix-js-sdk/pull/2285)).
|
||||||
|
* Stabilise token authenticated registration support ([\#2181](https://github.com/matrix-org/matrix-js-sdk/pull/2181)). Contributed by @govynnus.
|
||||||
|
* Live location sharing - Aggregate beacon locations on beacons ([\#2268](https://github.com/matrix-org/matrix-js-sdk/pull/2268)).
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
* Prevent duplicated re-emitter setups in event-mapper ([\#2293](https://github.com/matrix-org/matrix-js-sdk/pull/2293)).
|
||||||
|
* Make self membership less prone to races ([\#2277](https://github.com/matrix-org/matrix-js-sdk/pull/2277)). Fixes vector-im/element-web#21661.
|
||||||
|
|
||||||
|
Changes in [17.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v17.0.0) (2022-04-11)
|
||||||
|
==================================================================================================
|
||||||
|
|
||||||
|
## 🚨 BREAKING CHANGES
|
||||||
|
* Remove groups and groups-related APIs ([\#2234](https://github.com/matrix-org/matrix-js-sdk/pull/2234)).
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
* Add Element video room type ([\#2273](https://github.com/matrix-org/matrix-js-sdk/pull/2273)).
|
||||||
|
* Live location sharing - handle redacted beacons ([\#2269](https://github.com/matrix-org/matrix-js-sdk/pull/2269)).
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
* Fix getSessionsNeedingBackup() limit support ([\#2270](https://github.com/matrix-org/matrix-js-sdk/pull/2270)). Contributed by @adamvy.
|
||||||
|
* Fix issues with /search and /context API handling for threads ([\#2261](https://github.com/matrix-org/matrix-js-sdk/pull/2261)). Fixes vector-im/element-web#21543.
|
||||||
|
* Prevent exception 'Unable to set up secret storage' ([\#2260](https://github.com/matrix-org/matrix-js-sdk/pull/2260)).
|
||||||
|
|
||||||
|
Changes in [16.0.2-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.2-rc.1) (2022-04-05)
|
||||||
|
============================================================================================================
|
||||||
|
|
||||||
|
## 🚨 BREAKING CHANGES
|
||||||
|
* Remove groups and groups-related APIs ([\#2234](https://github.com/matrix-org/matrix-js-sdk/pull/2234)).
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
* Add Element video room type ([\#2273](https://github.com/matrix-org/matrix-js-sdk/pull/2273)).
|
||||||
|
* Live location sharing - handle redacted beacons ([\#2269](https://github.com/matrix-org/matrix-js-sdk/pull/2269)).
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
* Fix getSessionsNeedingBackup() limit support ([\#2270](https://github.com/matrix-org/matrix-js-sdk/pull/2270)). Contributed by @adamvy.
|
||||||
|
* Fix issues with /search and /context API handling for threads ([\#2261](https://github.com/matrix-org/matrix-js-sdk/pull/2261)). Fixes vector-im/element-web#21543.
|
||||||
|
* Prevent exception 'Unable to set up secret storage' ([\#2260](https://github.com/matrix-org/matrix-js-sdk/pull/2260)).
|
||||||
|
|
||||||
|
Changes in [16.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.1) (2022-03-28)
|
||||||
|
==================================================================================================
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
* emit aggregate room beacon liveness ([\#2241](https://github.com/matrix-org/matrix-js-sdk/pull/2241)).
|
||||||
|
* Live location sharing - create m.beacon_info events ([\#2238](https://github.com/matrix-org/matrix-js-sdk/pull/2238)).
|
||||||
|
* Beacon event types from MSC3489 ([\#2230](https://github.com/matrix-org/matrix-js-sdk/pull/2230)).
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
* Fix incorrect usage of unstable variant of `is_falling_back` ([\#2227](https://github.com/matrix-org/matrix-js-sdk/pull/2227)).
|
||||||
|
|
||||||
|
Changes in [16.0.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.1-rc.1) (2022-03-22)
|
||||||
|
============================================================================================================
|
||||||
|
|
||||||
Changes in [16.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.0) (2022-03-15)
|
Changes in [16.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.0) (2022-03-15)
|
||||||
==================================================================================================
|
==================================================================================================
|
||||||
|
|
||||||
@@ -1974,6 +2046,12 @@ All Changes
|
|||||||
* [BREAKING] Refactor the entire build process
|
* [BREAKING] Refactor the entire build process
|
||||||
[\#1113](https://github.com/matrix-org/matrix-js-sdk/pull/1113)
|
[\#1113](https://github.com/matrix-org/matrix-js-sdk/pull/1113)
|
||||||
|
|
||||||
|
Changes in [3.42.2-rc.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v3.42.2-rc.3) (2022-04-08)
|
||||||
|
============================================================================================================
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
* Make self membership less prone to races ([\#2277](https://github.com/matrix-org/matrix-js-sdk/pull/2277)). Fixes vector-im/element-web#21661.
|
||||||
|
|
||||||
Changes in [3.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v3.0.0) (2020-01-13)
|
Changes in [3.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v3.0.0) (2020-01-13)
|
||||||
================================================================================================
|
================================================================================================
|
||||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v3.0.0-rc.1...v3.0.0)
|
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v3.0.0-rc.1...v3.0.0)
|
||||||
|
|||||||
@@ -243,3 +243,18 @@ on Git 2.17+ you can mass signoff using rebase:
|
|||||||
```
|
```
|
||||||
git rebase --signoff origin/develop
|
git rebase --signoff origin/develop
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -1,3 +1,11 @@
|
|||||||
|
[](https://www.npmjs.com/package/matrix-js-sdk)
|
||||||
|

|
||||||
|

|
||||||
|
[](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
|
||||||
|
[](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
|
||||||
|
[](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
|
||||||
|
[](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
|
||||||
|
|
||||||
Matrix Javascript SDK
|
Matrix Javascript SDK
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
@@ -26,7 +34,7 @@ In Node.js
|
|||||||
|
|
||||||
Ensure you have the latest LTS version of Node.js installed.
|
Ensure you have the latest LTS version of Node.js installed.
|
||||||
|
|
||||||
This SDK targets Node 10 for compatibility, which translates to ES6. If you're using
|
This SDK targets Node 12 for compatibility, which translates to ES6. If you're using
|
||||||
a bundler like webpack you'll likely have to transpile dependencies, including this
|
a bundler like webpack you'll likely have to transpile dependencies, including this
|
||||||
SDK, to match your target browsers.
|
SDK, to match your target browsers.
|
||||||
|
|
||||||
@@ -307,7 +315,7 @@ The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
|
|||||||
[libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the
|
[libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the
|
||||||
application to make libolm available, via the ``Olm`` global.
|
application to make libolm available, via the ``Olm`` global.
|
||||||
|
|
||||||
It is also necessary to call ``matrixClient.initCrypto()`` after creating a new
|
It is also necessary to call ``await matrixClient.initCrypto()`` after creating a new
|
||||||
``MatrixClient`` (but **before** calling ``matrixClient.startClient()``) to
|
``MatrixClient`` (but **before** calling ``matrixClient.startClient()``) to
|
||||||
initialise the crypto layer.
|
initialise the crypto layer.
|
||||||
|
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ function printLine(event) {
|
|||||||
|
|
||||||
var maxNameWidth = 15;
|
var maxNameWidth = 15;
|
||||||
if (name.length > maxNameWidth) {
|
if (name.length > maxNameWidth) {
|
||||||
name = name.substr(0, maxNameWidth-1) + "\u2026";
|
name = name.slice(0, maxNameWidth-1) + "\u2026";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.getType() === "m.room.message") {
|
if (event.getType() === "m.room.message") {
|
||||||
@@ -398,7 +398,7 @@ function print(str, formatter) {
|
|||||||
|
|
||||||
function fixWidth(str, len) {
|
function fixWidth(str, len) {
|
||||||
if (str.length > len) {
|
if (str.length > len) {
|
||||||
return str.substr(0, len-2) + "\u2026";
|
return str.substring(0, len-2) + "\u2026";
|
||||||
}
|
}
|
||||||
else if (str.length < len) {
|
else if (str.length < len) {
|
||||||
return str + new Array(len - str.length).join(" ");
|
return str + new Array(len - str.length).join(" ");
|
||||||
|
|||||||
17
package.json
17
package.json
@@ -1,7 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "matrix-js-sdk",
|
"name": "matrix-js-sdk",
|
||||||
"version": "16.0.0",
|
"version": "17.2.0",
|
||||||
"description": "Matrix Client-Server SDK for Javascript",
|
"description": "Matrix Client-Server SDK for Javascript",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.9.0"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepublishOnly": "yarn build",
|
"prepublishOnly": "yarn build",
|
||||||
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||||
@@ -97,6 +100,7 @@
|
|||||||
"fake-indexeddb": "^3.1.2",
|
"fake-indexeddb": "^3.1.2",
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"jest-localstorage-mock": "^2.4.6",
|
"jest-localstorage-mock": "^2.4.6",
|
||||||
|
"jest-sonar-reporter": "^2.0.0",
|
||||||
"jsdoc": "^3.6.6",
|
"jsdoc": "^3.6.6",
|
||||||
"matrix-mock-request": "^1.2.3",
|
"matrix-mock-request": "^1.2.3",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
@@ -113,8 +117,13 @@
|
|||||||
"<rootDir>/src/**/*.{js,ts}"
|
"<rootDir>/src/**/*.{js,ts}"
|
||||||
],
|
],
|
||||||
"coverageReporters": [
|
"coverageReporters": [
|
||||||
"text",
|
"text-summary",
|
||||||
"json"
|
"lcov"
|
||||||
]
|
],
|
||||||
|
"testResultsProcessor": "jest-sonar-reporter"
|
||||||
|
},
|
||||||
|
"jestSonar": {
|
||||||
|
"reportPath": "coverage",
|
||||||
|
"sonar56x": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
sonar-project.properties
Normal file
16
sonar-project.properties
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
sonar.projectKey=matrix-js-sdk
|
||||||
|
sonar.organization=matrix-org
|
||||||
|
|
||||||
|
# Encoding of the source code. Default is default system encoding
|
||||||
|
#sonar.sourceEncoding=UTF-8
|
||||||
|
|
||||||
|
sonar.sources=src
|
||||||
|
sonar.tests=spec
|
||||||
|
sonar.exclusions=docs,examples,git-hooks
|
||||||
|
|
||||||
|
sonar.typescript.tsconfigPath=./tsconfig.json
|
||||||
|
sonar.javascript.lcov.reportPaths=coverage/lcov.info
|
||||||
|
sonar.coverage.exclusions=spec/**/*
|
||||||
|
sonar.testExecutionReportPaths=coverage/test-report.xml
|
||||||
|
|
||||||
|
sonar.lang.patterns.ts=**/*.ts,**/*.tsx
|
||||||
@@ -47,7 +47,7 @@ describe("Browserify Test", function() {
|
|||||||
httpBackend.stop();
|
httpBackend.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Sync", async function() {
|
it("Sync", function() {
|
||||||
const event = utils.mkMembership({
|
const event = utils.mkMembership({
|
||||||
room: ROOM_ID,
|
room: ROOM_ID,
|
||||||
mship: "join",
|
mship: "join",
|
||||||
@@ -71,7 +71,7 @@ describe("Browserify Test", function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||||
return await Promise.race([
|
return Promise.race([
|
||||||
httpBackend.flushAllExpected(),
|
httpBackend.flushAllExpected(),
|
||||||
new Promise((_, reject) => {
|
new Promise((_, reject) => {
|
||||||
client.once("sync.unexpectedError", reject);
|
client.once("sync.unexpectedError", reject);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import * as utils from "../test-utils/test-utils";
|
|||||||
import { EventTimeline } from "../../src/matrix";
|
import { EventTimeline } from "../../src/matrix";
|
||||||
import { logger } from "../../src/logger";
|
import { logger } from "../../src/logger";
|
||||||
import { TestClient } from "../TestClient";
|
import { TestClient } from "../TestClient";
|
||||||
|
import { Thread, THREAD_RELATION_TYPE } from "../../src/models/thread";
|
||||||
|
|
||||||
const userId = "@alice:localhost";
|
const userId = "@alice:localhost";
|
||||||
const userName = "Alice";
|
const userName = "Alice";
|
||||||
@@ -69,6 +70,27 @@ const EVENTS = [
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const THREAD_ROOT = utils.mkMessage({
|
||||||
|
room: roomId,
|
||||||
|
user: userId,
|
||||||
|
msg: "thread root",
|
||||||
|
});
|
||||||
|
|
||||||
|
const THREAD_REPLY = utils.mkEvent({
|
||||||
|
room: roomId,
|
||||||
|
user: userId,
|
||||||
|
type: "m.room.message",
|
||||||
|
content: {
|
||||||
|
"body": "thread reply",
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"m.relates_to": {
|
||||||
|
// We can't use the const here because we change server support mode for test
|
||||||
|
rel_type: "io.element.thread",
|
||||||
|
event_id: THREAD_ROOT.event_id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// start the client, and wait for it to initialise
|
// start the client, and wait for it to initialise
|
||||||
function startClient(httpBackend, client) {
|
function startClient(httpBackend, client) {
|
||||||
httpBackend.when("GET", "/versions").respond(200, {});
|
httpBackend.when("GET", "/versions").respond(200, {});
|
||||||
@@ -116,9 +138,7 @@ describe("getEventTimeline support", function() {
|
|||||||
return startClient(httpBackend, client).then(function() {
|
return startClient(httpBackend, client).then(function() {
|
||||||
const room = client.getRoom(roomId);
|
const room = client.getRoom(roomId);
|
||||||
const timelineSet = room.getTimelineSets()[0];
|
const timelineSet = room.getTimelineSets()[0];
|
||||||
expect(function() {
|
expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeTruthy();
|
||||||
client.getEventTimeline(timelineSet, "event");
|
|
||||||
}).toThrow();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -136,16 +156,12 @@ describe("getEventTimeline support", function() {
|
|||||||
return startClient(httpBackend, client).then(() => {
|
return startClient(httpBackend, client).then(() => {
|
||||||
const room = client.getRoom(roomId);
|
const room = client.getRoom(roomId);
|
||||||
const timelineSet = room.getTimelineSets()[0];
|
const timelineSet = room.getTimelineSets()[0];
|
||||||
expect(function() {
|
expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeFalsy();
|
||||||
client.getEventTimeline(timelineSet, "event");
|
|
||||||
}).not.toThrow();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("scrollback should be able to scroll back to before a gappy /sync",
|
it("scrollback should be able to scroll back to before a gappy /sync", function() {
|
||||||
function() {
|
|
||||||
// need a client with timelineSupport disabled to make this work
|
// need a client with timelineSupport disabled to make this work
|
||||||
|
|
||||||
let room;
|
let room;
|
||||||
|
|
||||||
return startClient(httpBackend, client).then(function() {
|
return startClient(httpBackend, client).then(function() {
|
||||||
@@ -229,6 +245,7 @@ describe("MatrixClient event timelines", function() {
|
|||||||
afterEach(function() {
|
afterEach(function() {
|
||||||
httpBackend.verifyNoOutstandingExpectation();
|
httpBackend.verifyNoOutstandingExpectation();
|
||||||
client.stopClient();
|
client.stopClient();
|
||||||
|
Thread.setServerSideSupport(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getEventTimeline", function() {
|
describe("getEventTimeline", function() {
|
||||||
@@ -355,8 +372,7 @@ describe("MatrixClient event timelines", function() {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should join timelines where they overlap a previous /context",
|
it("should join timelines where they overlap a previous /context", function() {
|
||||||
function() {
|
|
||||||
const room = client.getRoom(roomId);
|
const room = client.getRoom(roomId);
|
||||||
const timelineSet = room.getTimelineSets()[0];
|
const timelineSet = room.getTimelineSets()[0];
|
||||||
|
|
||||||
@@ -478,6 +494,50 @@ describe("MatrixClient event timelines", function() {
|
|||||||
httpBackend.flushAllExpected(),
|
httpBackend.flushAllExpected(),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should handle thread replies with server support by fetching a contiguous thread timeline", async () => {
|
||||||
|
client.clientOpts.experimentalThreadSupport = true;
|
||||||
|
Thread.setServerSideSupport(true);
|
||||||
|
client.stopClient(); // we don't need the client to be syncing at this time
|
||||||
|
const room = client.getRoom(roomId);
|
||||||
|
const timelineSet = room.getTimelineSets()[0];
|
||||||
|
|
||||||
|
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_REPLY.event_id))
|
||||||
|
.respond(200, function() {
|
||||||
|
return {
|
||||||
|
start: "start_token0",
|
||||||
|
events_before: [],
|
||||||
|
event: THREAD_REPLY,
|
||||||
|
events_after: [],
|
||||||
|
end: "end_token0",
|
||||||
|
state: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id))
|
||||||
|
.respond(200, function() {
|
||||||
|
return THREAD_ROOT;
|
||||||
|
});
|
||||||
|
|
||||||
|
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
|
||||||
|
encodeURIComponent(THREAD_ROOT.event_id) + "/" +
|
||||||
|
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?limit=20")
|
||||||
|
.respond(200, function() {
|
||||||
|
return {
|
||||||
|
original_event: THREAD_ROOT,
|
||||||
|
chunk: [THREAD_REPLY],
|
||||||
|
// no next batch as this is the oldest end of the timeline
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id);
|
||||||
|
await httpBackend.flushAllExpected();
|
||||||
|
|
||||||
|
const timeline = await timelinePromise;
|
||||||
|
|
||||||
|
expect(timeline.getEvents().find(e => e.getId() === THREAD_ROOT.event_id)).toBeTruthy();
|
||||||
|
expect(timeline.getEvents().find(e => e.getId() === THREAD_REPLY.event_id)).toBeTruthy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("paginateEventTimeline", function() {
|
describe("paginateEventTimeline", function() {
|
||||||
|
|||||||
@@ -1,8 +1,25 @@
|
|||||||
|
/*
|
||||||
|
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 * as utils from "../test-utils/test-utils";
|
||||||
import { CRYPTO_ENABLED } from "../../src/client";
|
import { CRYPTO_ENABLED } from "../../src/client";
|
||||||
import { MatrixEvent } from "../../src/models/event";
|
import { MatrixEvent } from "../../src/models/event";
|
||||||
import { Filter, MemoryStore, Room } from "../../src/matrix";
|
import { Filter, MemoryStore, Room } from "../../src/matrix";
|
||||||
import { TestClient } from "../TestClient";
|
import { TestClient } from "../TestClient";
|
||||||
|
import { THREAD_RELATION_TYPE } from "../../src/models/thread";
|
||||||
|
|
||||||
describe("MatrixClient", function() {
|
describe("MatrixClient", function() {
|
||||||
let client = null;
|
let client = null;
|
||||||
@@ -14,9 +31,7 @@ describe("MatrixClient", function() {
|
|||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
store = new MemoryStore();
|
store = new MemoryStore();
|
||||||
|
|
||||||
const testClient = new TestClient(userId, "aliceDevice", accessToken, undefined, {
|
const testClient = new TestClient(userId, "aliceDevice", accessToken, undefined, { store });
|
||||||
store: store,
|
|
||||||
});
|
|
||||||
httpBackend = testClient.httpBackend;
|
httpBackend = testClient.httpBackend;
|
||||||
client = testClient.client;
|
client = testClient.client;
|
||||||
});
|
});
|
||||||
@@ -146,12 +161,14 @@ describe("MatrixClient", function() {
|
|||||||
describe("joinRoom", function() {
|
describe("joinRoom", function() {
|
||||||
it("should no-op if you've already joined a room", function() {
|
it("should no-op if you've already joined a room", function() {
|
||||||
const roomId = "!foo:bar";
|
const roomId = "!foo:bar";
|
||||||
const room = new Room(roomId, userId);
|
const room = new Room(roomId, client, userId);
|
||||||
|
client.fetchRoomEvent = () => Promise.resolve({});
|
||||||
room.addLiveEvents([
|
room.addLiveEvents([
|
||||||
utils.mkMembership({
|
utils.mkMembership({
|
||||||
user: userId, room: roomId, mship: "join", event: true,
|
user: userId, room: roomId, mship: "join", event: true,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
httpBackend.verifyNoOutstandingRequests();
|
||||||
store.storeRoom(room);
|
store.storeRoom(room);
|
||||||
client.joinRoom(roomId);
|
client.joinRoom(roomId);
|
||||||
httpBackend.verifyNoOutstandingRequests();
|
httpBackend.verifyNoOutstandingRequests();
|
||||||
@@ -244,14 +261,15 @@ describe("MatrixClient", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("searching", function() {
|
describe("searching", function() {
|
||||||
|
it("searchMessageText should perform a /search for room_events", function() {
|
||||||
const response = {
|
const response = {
|
||||||
search_categories: {
|
search_categories: {
|
||||||
room_events: {
|
room_events: {
|
||||||
count: 24,
|
count: 24,
|
||||||
results: {
|
results: [{
|
||||||
"$flibble:localhost": {
|
|
||||||
rank: 0.1,
|
rank: 0.1,
|
||||||
result: {
|
result: {
|
||||||
|
event_id: "$flibble:localhost",
|
||||||
type: "m.room.message",
|
type: "m.room.message",
|
||||||
user_id: "@alice:localhost",
|
user_id: "@alice:localhost",
|
||||||
room_id: "!feuiwhf:localhost",
|
room_id: "!feuiwhf:localhost",
|
||||||
@@ -260,13 +278,11 @@ describe("MatrixClient", function() {
|
|||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}],
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
it("searchMessageText should perform a /search for room_events", function(done) {
|
|
||||||
client.searchMessageText({
|
client.searchMessageText({
|
||||||
query: "monkeys",
|
query: "monkeys",
|
||||||
});
|
});
|
||||||
@@ -280,8 +296,171 @@ describe("MatrixClient", function() {
|
|||||||
});
|
});
|
||||||
}).respond(200, response);
|
}).respond(200, response);
|
||||||
|
|
||||||
httpBackend.flush().then(function() {
|
return httpBackend.flush();
|
||||||
done();
|
});
|
||||||
|
|
||||||
|
describe("should filter out context from different timelines (threads)", () => {
|
||||||
|
it("filters out thread replies when result is in the main timeline", async () => {
|
||||||
|
const response = {
|
||||||
|
search_categories: {
|
||||||
|
room_events: {
|
||||||
|
count: 24,
|
||||||
|
results: [{
|
||||||
|
rank: 0.1,
|
||||||
|
result: {
|
||||||
|
event_id: "$flibble:localhost",
|
||||||
|
type: "m.room.message",
|
||||||
|
user_id: "@alice:localhost",
|
||||||
|
room_id: "!feuiwhf:localhost",
|
||||||
|
content: {
|
||||||
|
body: "main timeline",
|
||||||
|
msgtype: "m.text",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
events_after: [{
|
||||||
|
event_id: "$ev-after:server",
|
||||||
|
type: "m.room.message",
|
||||||
|
user_id: "@alice:localhost",
|
||||||
|
room_id: "!feuiwhf:localhost",
|
||||||
|
content: {
|
||||||
|
"body": "thread reply",
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"m.relates_to": {
|
||||||
|
"event_id": "$some-thread:server",
|
||||||
|
"rel_type": THREAD_RELATION_TYPE.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
events_before: [{
|
||||||
|
event_id: "$ev-before:server",
|
||||||
|
type: "m.room.message",
|
||||||
|
user_id: "@alice:localhost",
|
||||||
|
room_id: "!feuiwhf:localhost",
|
||||||
|
content: {
|
||||||
|
body: "main timeline again",
|
||||||
|
msgtype: "m.text",
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
results: [],
|
||||||
|
highlights: [],
|
||||||
|
};
|
||||||
|
client.processRoomEventsSearch(data, response);
|
||||||
|
|
||||||
|
expect(data.results).toHaveLength(1);
|
||||||
|
expect(data.results[0].context.timeline).toHaveLength(2);
|
||||||
|
expect(data.results[0].context.timeline.find(e => e.getId() === "$ev-after:server")).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters out thread replies from threads other than the thread the result replied to", () => {
|
||||||
|
const response = {
|
||||||
|
search_categories: {
|
||||||
|
room_events: {
|
||||||
|
count: 24,
|
||||||
|
results: [{
|
||||||
|
rank: 0.1,
|
||||||
|
result: {
|
||||||
|
event_id: "$flibble:localhost",
|
||||||
|
type: "m.room.message",
|
||||||
|
user_id: "@alice:localhost",
|
||||||
|
room_id: "!feuiwhf:localhost",
|
||||||
|
content: {
|
||||||
|
"body": "thread 1 reply 1",
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"m.relates_to": {
|
||||||
|
"event_id": "$thread1:server",
|
||||||
|
"rel_type": THREAD_RELATION_TYPE.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
events_after: [{
|
||||||
|
event_id: "$ev-after:server",
|
||||||
|
type: "m.room.message",
|
||||||
|
user_id: "@alice:localhost",
|
||||||
|
room_id: "!feuiwhf:localhost",
|
||||||
|
content: {
|
||||||
|
"body": "thread 2 reply 2",
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"m.relates_to": {
|
||||||
|
"event_id": "$thread2:server",
|
||||||
|
"rel_type": THREAD_RELATION_TYPE.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
events_before: [],
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
results: [],
|
||||||
|
highlights: [],
|
||||||
|
};
|
||||||
|
client.processRoomEventsSearch(data, response);
|
||||||
|
|
||||||
|
expect(data.results).toHaveLength(1);
|
||||||
|
expect(data.results[0].context.timeline).toHaveLength(1);
|
||||||
|
expect(data.results[0].context.timeline.find(e => e.getId() === "$flibble:localhost")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters out main timeline events when result is a thread reply", () => {
|
||||||
|
const response = {
|
||||||
|
search_categories: {
|
||||||
|
room_events: {
|
||||||
|
count: 24,
|
||||||
|
results: [{
|
||||||
|
rank: 0.1,
|
||||||
|
result: {
|
||||||
|
event_id: "$flibble:localhost",
|
||||||
|
type: "m.room.message",
|
||||||
|
user_id: "@alice:localhost",
|
||||||
|
room_id: "!feuiwhf:localhost",
|
||||||
|
content: {
|
||||||
|
"body": "thread 1 reply 1",
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"m.relates_to": {
|
||||||
|
"event_id": "$thread1:server",
|
||||||
|
"rel_type": THREAD_RELATION_TYPE.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
events_after: [{
|
||||||
|
event_id: "$ev-after:server",
|
||||||
|
type: "m.room.message",
|
||||||
|
user_id: "@alice:localhost",
|
||||||
|
room_id: "!feuiwhf:localhost",
|
||||||
|
content: {
|
||||||
|
"body": "main timeline",
|
||||||
|
"msgtype": "m.text",
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
events_before: [],
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
results: [],
|
||||||
|
highlights: [],
|
||||||
|
};
|
||||||
|
client.processRoomEventsSearch(data, response);
|
||||||
|
|
||||||
|
expect(data.results).toHaveLength(1);
|
||||||
|
expect(data.results[0].context.timeline).toHaveLength(1);
|
||||||
|
expect(data.results[0].context.timeline.find(e => e.getId() === "$flibble:localhost")).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -395,9 +574,14 @@ describe("MatrixClient", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("partitionThreadedEvents", function() {
|
describe("partitionThreadedEvents", function() {
|
||||||
|
let room;
|
||||||
|
beforeEach(() => {
|
||||||
|
room = new Room("!STrMRsukXHtqQdSeHa:matrix.org", client, userId);
|
||||||
|
});
|
||||||
|
|
||||||
it("returns empty arrays when given an empty arrays", function() {
|
it("returns empty arrays when given an empty arrays", function() {
|
||||||
const events = [];
|
const events = [];
|
||||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
const [timeline, threaded] = room.partitionThreadedEvents(events);
|
||||||
expect(timeline).toEqual([]);
|
expect(timeline).toEqual([]);
|
||||||
expect(threaded).toEqual([]);
|
expect(threaded).toEqual([]);
|
||||||
});
|
});
|
||||||
@@ -405,24 +589,24 @@ describe("MatrixClient", function() {
|
|||||||
it("copies pre-thread in-timeline vote events onto both timelines", function() {
|
it("copies pre-thread in-timeline vote events onto both timelines", function() {
|
||||||
client.clientOpts = { experimentalThreadSupport: true };
|
client.clientOpts = { experimentalThreadSupport: true };
|
||||||
|
|
||||||
const eventMessageInThread = buildEventMessageInThread();
|
|
||||||
const eventPollResponseReference = buildEventPollResponseReference();
|
const eventPollResponseReference = buildEventPollResponseReference();
|
||||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||||
|
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
|
||||||
|
|
||||||
const events = [
|
const events = [
|
||||||
|
eventPollStartThreadRoot,
|
||||||
eventMessageInThread,
|
eventMessageInThread,
|
||||||
eventPollResponseReference,
|
eventPollResponseReference,
|
||||||
eventPollStartThreadRoot,
|
|
||||||
];
|
];
|
||||||
// Vote has no threadId yet
|
// Vote has no threadId yet
|
||||||
expect(eventPollResponseReference.threadId).toBeFalsy();
|
expect(eventPollResponseReference.threadId).toBeFalsy();
|
||||||
|
|
||||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
const [timeline, threaded] = room.partitionThreadedEvents(events);
|
||||||
|
|
||||||
expect(timeline).toEqual([
|
expect(timeline).toEqual([
|
||||||
// The message that was sent in a thread is missing
|
// The message that was sent in a thread is missing
|
||||||
eventPollResponseReference,
|
|
||||||
eventPollStartThreadRoot,
|
eventPollStartThreadRoot,
|
||||||
|
eventPollResponseReference,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// The vote event has been copied into the thread
|
// The vote event has been copied into the thread
|
||||||
@@ -431,33 +615,34 @@ describe("MatrixClient", function() {
|
|||||||
expect(eventRefWithThreadId.threadId).toBeTruthy();
|
expect(eventRefWithThreadId.threadId).toBeTruthy();
|
||||||
|
|
||||||
expect(threaded).toEqual([
|
expect(threaded).toEqual([
|
||||||
|
eventPollStartThreadRoot,
|
||||||
eventMessageInThread,
|
eventMessageInThread,
|
||||||
eventRefWithThreadId,
|
eventRefWithThreadId,
|
||||||
// Thread does not see thread root
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("copies pre-thread in-timeline reactions onto both timelines", function() {
|
it("copies pre-thread in-timeline reactions onto both timelines", function() {
|
||||||
client.clientOpts = { experimentalThreadSupport: true };
|
client.clientOpts = { experimentalThreadSupport: true };
|
||||||
|
|
||||||
const eventMessageInThread = buildEventMessageInThread();
|
|
||||||
const eventReaction = buildEventReaction();
|
|
||||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||||
|
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
|
||||||
|
const eventReaction = buildEventReaction(eventPollStartThreadRoot);
|
||||||
|
|
||||||
const events = [
|
const events = [
|
||||||
|
eventPollStartThreadRoot,
|
||||||
eventMessageInThread,
|
eventMessageInThread,
|
||||||
eventReaction,
|
eventReaction,
|
||||||
eventPollStartThreadRoot,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
const [timeline, threaded] = room.partitionThreadedEvents(events);
|
||||||
|
|
||||||
expect(timeline).toEqual([
|
expect(timeline).toEqual([
|
||||||
eventReaction,
|
|
||||||
eventPollStartThreadRoot,
|
eventPollStartThreadRoot,
|
||||||
|
eventReaction,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(threaded).toEqual([
|
expect(threaded).toEqual([
|
||||||
|
eventPollStartThreadRoot,
|
||||||
eventMessageInThread,
|
eventMessageInThread,
|
||||||
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
|
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
|
||||||
]);
|
]);
|
||||||
@@ -467,23 +652,24 @@ describe("MatrixClient", function() {
|
|||||||
client.clientOpts = { experimentalThreadSupport: true };
|
client.clientOpts = { experimentalThreadSupport: true };
|
||||||
|
|
||||||
const eventPollResponseReference = buildEventPollResponseReference();
|
const eventPollResponseReference = buildEventPollResponseReference();
|
||||||
const eventMessageInThread = buildEventMessageInThread();
|
|
||||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||||
|
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
|
||||||
|
|
||||||
const events = [
|
const events = [
|
||||||
|
eventPollStartThreadRoot,
|
||||||
eventPollResponseReference,
|
eventPollResponseReference,
|
||||||
eventMessageInThread,
|
eventMessageInThread,
|
||||||
eventPollStartThreadRoot,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
const [timeline, threaded] = room.partitionThreadedEvents(events);
|
||||||
|
|
||||||
expect(timeline).toEqual([
|
expect(timeline).toEqual([
|
||||||
eventPollResponseReference,
|
|
||||||
eventPollStartThreadRoot,
|
eventPollStartThreadRoot,
|
||||||
|
eventPollResponseReference,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(threaded).toEqual([
|
expect(threaded).toEqual([
|
||||||
|
eventPollStartThreadRoot,
|
||||||
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
|
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
|
||||||
eventMessageInThread,
|
eventMessageInThread,
|
||||||
]);
|
]);
|
||||||
@@ -492,26 +678,27 @@ describe("MatrixClient", function() {
|
|||||||
it("copies post-thread in-timeline reactions onto both timelines", function() {
|
it("copies post-thread in-timeline reactions onto both timelines", function() {
|
||||||
client.clientOpts = { experimentalThreadSupport: true };
|
client.clientOpts = { experimentalThreadSupport: true };
|
||||||
|
|
||||||
const eventReaction = buildEventReaction();
|
|
||||||
const eventMessageInThread = buildEventMessageInThread();
|
|
||||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||||
|
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
|
||||||
|
const eventReaction = buildEventReaction(eventPollStartThreadRoot);
|
||||||
|
|
||||||
const events = [
|
const events = [
|
||||||
eventReaction,
|
|
||||||
eventMessageInThread,
|
|
||||||
eventPollStartThreadRoot,
|
eventPollStartThreadRoot,
|
||||||
|
eventMessageInThread,
|
||||||
|
eventReaction,
|
||||||
];
|
];
|
||||||
|
|
||||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
const [timeline, threaded] = room.partitionThreadedEvents(events);
|
||||||
|
|
||||||
expect(timeline).toEqual([
|
expect(timeline).toEqual([
|
||||||
eventReaction,
|
|
||||||
eventPollStartThreadRoot,
|
eventPollStartThreadRoot,
|
||||||
|
eventReaction,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(threaded).toEqual([
|
expect(threaded).toEqual([
|
||||||
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
|
eventPollStartThreadRoot,
|
||||||
eventMessageInThread,
|
eventMessageInThread,
|
||||||
|
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -519,9 +706,9 @@ describe("MatrixClient", function() {
|
|||||||
client.clientOpts = { experimentalThreadSupport: true };
|
client.clientOpts = { experimentalThreadSupport: true };
|
||||||
// This is based on recording the events in a real room:
|
// This is based on recording the events in a real room:
|
||||||
|
|
||||||
const eventMessageInThread = buildEventMessageInThread();
|
|
||||||
const eventPollResponseReference = buildEventPollResponseReference();
|
|
||||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||||
|
const eventPollResponseReference = buildEventPollResponseReference();
|
||||||
|
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
|
||||||
const eventRoomName = buildEventRoomName();
|
const eventRoomName = buildEventRoomName();
|
||||||
const eventEncryption = buildEventEncryption();
|
const eventEncryption = buildEventEncryption();
|
||||||
const eventGuestAccess = buildEventGuestAccess();
|
const eventGuestAccess = buildEventGuestAccess();
|
||||||
@@ -532,9 +719,9 @@ describe("MatrixClient", function() {
|
|||||||
const eventCreate = buildEventCreate();
|
const eventCreate = buildEventCreate();
|
||||||
|
|
||||||
const events = [
|
const events = [
|
||||||
eventMessageInThread,
|
|
||||||
eventPollResponseReference,
|
|
||||||
eventPollStartThreadRoot,
|
eventPollStartThreadRoot,
|
||||||
|
eventPollResponseReference,
|
||||||
|
eventMessageInThread,
|
||||||
eventRoomName,
|
eventRoomName,
|
||||||
eventEncryption,
|
eventEncryption,
|
||||||
eventGuestAccess,
|
eventGuestAccess,
|
||||||
@@ -544,12 +731,12 @@ describe("MatrixClient", function() {
|
|||||||
eventMember,
|
eventMember,
|
||||||
eventCreate,
|
eventCreate,
|
||||||
];
|
];
|
||||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
const [timeline, threaded] = room.partitionThreadedEvents(events);
|
||||||
|
|
||||||
expect(timeline).toEqual([
|
expect(timeline).toEqual([
|
||||||
// The message that was sent in a thread is missing
|
// The message that was sent in a thread is missing
|
||||||
eventPollResponseReference,
|
|
||||||
eventPollStartThreadRoot,
|
eventPollStartThreadRoot,
|
||||||
|
eventPollResponseReference,
|
||||||
eventRoomName,
|
eventRoomName,
|
||||||
eventEncryption,
|
eventEncryption,
|
||||||
eventGuestAccess,
|
eventGuestAccess,
|
||||||
@@ -560,13 +747,262 @@ describe("MatrixClient", function() {
|
|||||||
eventCreate,
|
eventCreate,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Thread should contain only stuff that happened in the thread -
|
// Thread should contain only stuff that happened in the thread - no room state events
|
||||||
// no thread root, and no room state events
|
|
||||||
expect(threaded).toEqual([
|
expect(threaded).toEqual([
|
||||||
eventMessageInThread,
|
eventPollStartThreadRoot,
|
||||||
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
|
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
|
||||||
|
eventMessageInThread,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sends redactions of reactions to thread responses to thread timeline only", () => {
|
||||||
|
client.clientOpts = { experimentalThreadSupport: true };
|
||||||
|
|
||||||
|
const threadRootEvent = buildEventPollStartThreadRoot();
|
||||||
|
const eventMessageInThread = buildEventMessageInThread(threadRootEvent);
|
||||||
|
const threadedReaction = buildEventReaction(eventMessageInThread);
|
||||||
|
const threadedReactionRedaction = buildEventRedaction(threadedReaction);
|
||||||
|
|
||||||
|
const events = [
|
||||||
|
threadRootEvent,
|
||||||
|
eventMessageInThread,
|
||||||
|
threadedReaction,
|
||||||
|
threadedReactionRedaction,
|
||||||
|
];
|
||||||
|
|
||||||
|
const [timeline, threaded] = room.partitionThreadedEvents(events);
|
||||||
|
|
||||||
|
expect(timeline).toEqual([
|
||||||
|
threadRootEvent,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(threaded).toEqual([
|
||||||
|
threadRootEvent,
|
||||||
|
eventMessageInThread,
|
||||||
|
threadedReaction,
|
||||||
|
threadedReactionRedaction,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends reply to reply to thread root outside of thread to main timeline only", () => {
|
||||||
|
client.clientOpts = { experimentalThreadSupport: true };
|
||||||
|
|
||||||
|
const threadRootEvent = buildEventPollStartThreadRoot();
|
||||||
|
const eventMessageInThread = buildEventMessageInThread(threadRootEvent);
|
||||||
|
const directReplyToThreadRoot = buildEventReply(threadRootEvent);
|
||||||
|
const replyToReply = buildEventReply(directReplyToThreadRoot);
|
||||||
|
|
||||||
|
const events = [
|
||||||
|
threadRootEvent,
|
||||||
|
eventMessageInThread,
|
||||||
|
directReplyToThreadRoot,
|
||||||
|
replyToReply,
|
||||||
|
];
|
||||||
|
|
||||||
|
const [timeline, threaded] = room.partitionThreadedEvents(events);
|
||||||
|
|
||||||
|
expect(timeline).toEqual([
|
||||||
|
threadRootEvent,
|
||||||
|
directReplyToThreadRoot,
|
||||||
|
replyToReply,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(threaded).toEqual([
|
||||||
|
threadRootEvent,
|
||||||
|
eventMessageInThread,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends reply to thread responses to main timeline only", () => {
|
||||||
|
client.clientOpts = { experimentalThreadSupport: true };
|
||||||
|
|
||||||
|
const threadRootEvent = buildEventPollStartThreadRoot();
|
||||||
|
const eventMessageInThread = buildEventMessageInThread(threadRootEvent);
|
||||||
|
const replyToThreadResponse = buildEventReply(eventMessageInThread);
|
||||||
|
|
||||||
|
const events = [
|
||||||
|
threadRootEvent,
|
||||||
|
eventMessageInThread,
|
||||||
|
replyToThreadResponse,
|
||||||
|
];
|
||||||
|
|
||||||
|
const [timeline, threaded] = room.partitionThreadedEvents(events);
|
||||||
|
|
||||||
|
expect(timeline).toEqual([
|
||||||
|
threadRootEvent,
|
||||||
|
replyToThreadResponse,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(threaded).toEqual([
|
||||||
|
threadRootEvent,
|
||||||
|
eventMessageInThread,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getThirdpartyUser", () => {
|
||||||
|
it("should hit the expected API endpoint", async () => {
|
||||||
|
const response = [{
|
||||||
|
userid: "@Bob",
|
||||||
|
protocol: "irc",
|
||||||
|
fields: {},
|
||||||
|
}];
|
||||||
|
|
||||||
|
const prom = client.getThirdpartyUser("irc", {});
|
||||||
|
httpBackend.when("GET", "/thirdparty/user/irc").respond(200, response);
|
||||||
|
await httpBackend.flush();
|
||||||
|
expect(await prom).toStrictEqual(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getThirdpartyLocation", () => {
|
||||||
|
it("should hit the expected API endpoint", async () => {
|
||||||
|
const response = [{
|
||||||
|
alias: "#alias",
|
||||||
|
protocol: "irc",
|
||||||
|
fields: {},
|
||||||
|
}];
|
||||||
|
|
||||||
|
const prom = client.getThirdpartyLocation("irc", {});
|
||||||
|
httpBackend.when("GET", "/thirdparty/location/irc").respond(200, response);
|
||||||
|
await httpBackend.flush();
|
||||||
|
expect(await prom).toStrictEqual(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getPushers", () => {
|
||||||
|
it("should hit the expected API endpoint", async () => {
|
||||||
|
const response = {
|
||||||
|
pushers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const prom = client.getPushers();
|
||||||
|
httpBackend.when("GET", "/pushers").respond(200, response);
|
||||||
|
await httpBackend.flush();
|
||||||
|
expect(await prom).toStrictEqual(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getKeyChanges", () => {
|
||||||
|
it("should hit the expected API endpoint", async () => {
|
||||||
|
const response = {
|
||||||
|
changed: [],
|
||||||
|
left: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const prom = client.getKeyChanges("old", "new");
|
||||||
|
httpBackend.when("GET", "/keys/changes").check((req) => {
|
||||||
|
expect(req.queryParams.from).toEqual("old");
|
||||||
|
expect(req.queryParams.to).toEqual("new");
|
||||||
|
}).respond(200, response);
|
||||||
|
await httpBackend.flush();
|
||||||
|
expect(await prom).toStrictEqual(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getDevices", () => {
|
||||||
|
it("should hit the expected API endpoint", async () => {
|
||||||
|
const response = {
|
||||||
|
devices: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const prom = client.getDevices();
|
||||||
|
httpBackend.when("GET", "/devices").respond(200, response);
|
||||||
|
await httpBackend.flush();
|
||||||
|
expect(await prom).toStrictEqual(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getDevice", () => {
|
||||||
|
it("should hit the expected API endpoint", async () => {
|
||||||
|
const response = {
|
||||||
|
device_id: "DEADBEEF",
|
||||||
|
display_name: "NotAPhone",
|
||||||
|
last_seen_ip: "127.0.0.1",
|
||||||
|
last_seen_ts: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const prom = client.getDevice("DEADBEEF");
|
||||||
|
httpBackend.when("GET", "/devices/DEADBEEF").respond(200, response);
|
||||||
|
await httpBackend.flush();
|
||||||
|
expect(await prom).toStrictEqual(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getThreePids", () => {
|
||||||
|
it("should hit the expected API endpoint", async () => {
|
||||||
|
const response = {
|
||||||
|
threepids: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const prom = client.getThreePids();
|
||||||
|
httpBackend.when("GET", "/account/3pid").respond(200, response);
|
||||||
|
await httpBackend.flush();
|
||||||
|
expect(await prom).toStrictEqual(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("deleteAlias", () => {
|
||||||
|
it("should hit the expected API endpoint", async () => {
|
||||||
|
const response = {};
|
||||||
|
const prom = client.deleteAlias("#foo:bar");
|
||||||
|
httpBackend.when("DELETE", "/directory/room/" + encodeURIComponent("#foo:bar")).respond(200, response);
|
||||||
|
await httpBackend.flush();
|
||||||
|
expect(await prom).toStrictEqual(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("deleteRoomTag", () => {
|
||||||
|
it("should hit the expected API endpoint", async () => {
|
||||||
|
const response = {};
|
||||||
|
const prom = client.deleteRoomTag("!roomId:server", "u.tag");
|
||||||
|
const url = `/user/${encodeURIComponent(userId)}/rooms/${encodeURIComponent("!roomId:server")}/tags/u.tag`;
|
||||||
|
httpBackend.when("DELETE", url).respond(200, response);
|
||||||
|
await httpBackend.flush();
|
||||||
|
expect(await prom).toStrictEqual(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getRoomTags", () => {
|
||||||
|
it("should hit the expected API endpoint", async () => {
|
||||||
|
const response = {
|
||||||
|
tags: {
|
||||||
|
"u.tag": {
|
||||||
|
order: 0.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const prom = client.getRoomTags("!roomId:server");
|
||||||
|
const url = `/user/${encodeURIComponent(userId)}/rooms/${encodeURIComponent("!roomId:server")}/tags`;
|
||||||
|
httpBackend.when("GET", url).respond(200, response);
|
||||||
|
await httpBackend.flush();
|
||||||
|
expect(await prom).toStrictEqual(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("requestRegisterEmailToken", () => {
|
||||||
|
it("should hit the expected API endpoint", async () => {
|
||||||
|
const response = {
|
||||||
|
sid: "random_sid",
|
||||||
|
submit_url: "https://foobar.matrix/_matrix/matrix",
|
||||||
|
};
|
||||||
|
|
||||||
|
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||||
|
versions: ["r0.5.0"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const prom = client.requestRegisterEmailToken("bob@email", "secret", 1);
|
||||||
|
httpBackend.when("POST", "/register/email/requestToken").check(req => {
|
||||||
|
expect(req.data).toStrictEqual({
|
||||||
|
email: "bob@email",
|
||||||
|
client_secret: "secret",
|
||||||
|
send_attempt: 1,
|
||||||
|
});
|
||||||
|
}).respond(200, response);
|
||||||
|
await httpBackend.flush();
|
||||||
|
expect(await prom).toStrictEqual(response);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -576,16 +1012,16 @@ function withThreadId(event, newThreadId) {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildEventMessageInThread = () => new MatrixEvent({
|
const buildEventMessageInThread = (root) => new MatrixEvent({
|
||||||
"age": 80098509,
|
"age": 80098509,
|
||||||
"content": {
|
"content": {
|
||||||
"algorithm": "m.megolm.v1.aes-sha2",
|
"algorithm": "m.megolm.v1.aes-sha2",
|
||||||
"ciphertext": "ENCRYPTEDSTUFF",
|
"ciphertext": "ENCRYPTEDSTUFF",
|
||||||
"device_id": "XISFUZSKHH",
|
"device_id": "XISFUZSKHH",
|
||||||
"m.relates_to": {
|
"m.relates_to": {
|
||||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
"event_id": root.getId(),
|
||||||
"m.in_reply_to": {
|
"m.in_reply_to": {
|
||||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
"event_id": root.getId(),
|
||||||
},
|
},
|
||||||
"rel_type": "m.thread",
|
"rel_type": "m.thread",
|
||||||
},
|
},
|
||||||
@@ -623,10 +1059,10 @@ const buildEventPollResponseReference = () => new MatrixEvent({
|
|||||||
"user_id": "@andybalaam-test1:matrix.org",
|
"user_id": "@andybalaam-test1:matrix.org",
|
||||||
});
|
});
|
||||||
|
|
||||||
const buildEventReaction = () => new MatrixEvent({
|
const buildEventReaction = (event) => new MatrixEvent({
|
||||||
"content": {
|
"content": {
|
||||||
"m.relates_to": {
|
"m.relates_to": {
|
||||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
"event_id": event.getId(),
|
||||||
"key": "🤗",
|
"key": "🤗",
|
||||||
"rel_type": "m.annotation",
|
"rel_type": "m.annotation",
|
||||||
},
|
},
|
||||||
@@ -642,6 +1078,22 @@ const buildEventReaction = () => new MatrixEvent({
|
|||||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const buildEventRedaction = (event) => new MatrixEvent({
|
||||||
|
"content": {
|
||||||
|
|
||||||
|
},
|
||||||
|
"origin_server_ts": 1643977249239,
|
||||||
|
"sender": "@andybalaam-test1:matrix.org",
|
||||||
|
"redacts": event.getId(),
|
||||||
|
"type": "m.room.redaction",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 22597,
|
||||||
|
"transaction_id": "m1643977249073.17",
|
||||||
|
},
|
||||||
|
"event_id": "$86B2b-x3LgE4DlV4y24b7UHnt72LIA3rzjvMysTtAfB",
|
||||||
|
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||||
|
});
|
||||||
|
|
||||||
const buildEventPollStartThreadRoot = () => new MatrixEvent({
|
const buildEventPollStartThreadRoot = () => new MatrixEvent({
|
||||||
"age": 80108647,
|
"age": 80108647,
|
||||||
"content": {
|
"content": {
|
||||||
@@ -660,6 +1112,29 @@ const buildEventPollStartThreadRoot = () => new MatrixEvent({
|
|||||||
"user_id": "@andybalaam-test1:matrix.org",
|
"user_id": "@andybalaam-test1:matrix.org",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const buildEventReply = (target) => new MatrixEvent({
|
||||||
|
"age": 80098509,
|
||||||
|
"content": {
|
||||||
|
"algorithm": "m.megolm.v1.aes-sha2",
|
||||||
|
"ciphertext": "ENCRYPTEDSTUFF",
|
||||||
|
"device_id": "XISFUZSKHH",
|
||||||
|
"m.relates_to": {
|
||||||
|
"m.in_reply_to": {
|
||||||
|
"event_id": target.getId(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
|
||||||
|
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
|
||||||
|
},
|
||||||
|
"event_id": target.getId() + Math.random(),
|
||||||
|
"origin_server_ts": 1643815466378,
|
||||||
|
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||||
|
"sender": "@andybalaam-test1:matrix.org",
|
||||||
|
"type": "m.room.encrypted",
|
||||||
|
"unsigned": { "age": 80098509 },
|
||||||
|
"user_id": "@andybalaam-test1:matrix.org",
|
||||||
|
});
|
||||||
|
|
||||||
const buildEventRoomName = () => new MatrixEvent({
|
const buildEventRoomName = () => new MatrixEvent({
|
||||||
"age": 80123249,
|
"age": 80123249,
|
||||||
"content": {
|
"content": {
|
||||||
|
|||||||
@@ -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 * as utils from "../test-utils/test-utils";
|
||||||
import { TestClient } from "../TestClient";
|
import { TestClient } from "../TestClient";
|
||||||
|
|
||||||
@@ -60,6 +75,112 @@ describe("MatrixClient syncing", function() {
|
|||||||
done();
|
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() {
|
describe("resolving invites to profile info", function() {
|
||||||
@@ -735,8 +856,7 @@ describe("MatrixClient syncing", function() {
|
|||||||
expect(tok).toEqual("pagTok");
|
expect(tok).toEqual("pagTok");
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// first flush the filter request; this will make syncLeftRooms
|
// first flush the filter request; this will make syncLeftRooms make its /sync call
|
||||||
// make its /sync call
|
|
||||||
httpBackend.flush("/filter").then(function() {
|
httpBackend.flush("/filter").then(function() {
|
||||||
return httpBackend.flushAllExpected();
|
return httpBackend.flushAllExpected();
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -50,12 +50,14 @@ export const makeBeaconInfoEvent = (
|
|||||||
...contentProps,
|
...contentProps,
|
||||||
};
|
};
|
||||||
const event = new MatrixEvent({
|
const event = new MatrixEvent({
|
||||||
type: `${M_BEACON_INFO.name}.${sender}`,
|
type: M_BEACON_INFO.name,
|
||||||
room_id: roomId,
|
room_id: roomId,
|
||||||
state_key: sender,
|
state_key: sender,
|
||||||
content: makeBeaconInfoContent(timeout, isLive, description, assetType),
|
content: makeBeaconInfoContent(timeout, isLive, description, assetType),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
event.event.origin_server_ts = Date.now();
|
||||||
|
|
||||||
// live beacons use the beacon_info event id
|
// live beacons use the beacon_info event id
|
||||||
// set or default this
|
// set or default this
|
||||||
event.replaceLocalEventId(eventId || `$${Math.random()}-${Math.random()}`);
|
event.replaceLocalEventId(eventId || `$${Math.random()}-${Math.random()}`);
|
||||||
|
|||||||
@@ -1,369 +0,0 @@
|
|||||||
// load olm before the sdk if possible
|
|
||||||
import '../olm-loader';
|
|
||||||
|
|
||||||
import { logger } from '../../src/logger';
|
|
||||||
import { MatrixEvent } from "../../src/models/event";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a promise that is resolved when the client next emits a
|
|
||||||
* SYNCING event.
|
|
||||||
* @param {Object} client The client
|
|
||||||
* @param {Number=} count Number of syncs to wait for (default 1)
|
|
||||||
* @return {Promise} Resolves once the client has emitted a SYNCING event
|
|
||||||
*/
|
|
||||||
export function syncPromise(client, count) {
|
|
||||||
if (count === undefined) {
|
|
||||||
count = 1;
|
|
||||||
}
|
|
||||||
if (count <= 0) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
const p = new Promise((resolve, reject) => {
|
|
||||||
const cb = (state) => {
|
|
||||||
logger.log(`${Date.now()} syncPromise(${count}): ${state}`);
|
|
||||||
if (state === 'SYNCING') {
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
client.once('sync', cb);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
client.once('sync', cb);
|
|
||||||
});
|
|
||||||
|
|
||||||
return p.then(() => {
|
|
||||||
return syncPromise(client, count-1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a spy for an object and automatically spy its methods.
|
|
||||||
* @param {*} constr The class constructor (used with 'new')
|
|
||||||
* @param {string} name The name of the class
|
|
||||||
* @return {Object} An instantiated object with spied methods/properties.
|
|
||||||
*/
|
|
||||||
export function mock(constr, name) {
|
|
||||||
// Based on
|
|
||||||
// http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/
|
|
||||||
const HelperConstr = new Function(); // jshint ignore:line
|
|
||||||
HelperConstr.prototype = constr.prototype;
|
|
||||||
const result = new HelperConstr();
|
|
||||||
result.toString = function() {
|
|
||||||
return "mock" + (name ? " of " + name : "");
|
|
||||||
};
|
|
||||||
for (const key of Object.getOwnPropertyNames(constr.prototype)) { // eslint-disable-line guard-for-in
|
|
||||||
try {
|
|
||||||
if (constr.prototype[key] instanceof Function) {
|
|
||||||
result[key] = jest.fn();
|
|
||||||
}
|
|
||||||
} catch (ex) {
|
|
||||||
// Direct access to some non-function fields of DOM prototypes may
|
|
||||||
// cause exceptions.
|
|
||||||
// Overwriting will not work either in that case.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an Event.
|
|
||||||
* @param {Object} opts Values for the event.
|
|
||||||
* @param {string} opts.type The event.type
|
|
||||||
* @param {string} opts.room The event.room_id
|
|
||||||
* @param {string} opts.sender The event.sender
|
|
||||||
* @param {string} opts.skey Optional. The state key (auto inserts empty string)
|
|
||||||
* @param {Object} opts.content The event.content
|
|
||||||
* @param {boolean} opts.event True to make a MatrixEvent.
|
|
||||||
* @return {Object} a JSON object representing this event.
|
|
||||||
*/
|
|
||||||
export function mkEvent(opts) {
|
|
||||||
if (!opts.type || !opts.content) {
|
|
||||||
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
|
|
||||||
}
|
|
||||||
const event = {
|
|
||||||
type: opts.type,
|
|
||||||
room_id: opts.room,
|
|
||||||
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
|
||||||
content: opts.content,
|
|
||||||
unsigned: opts.unsigned,
|
|
||||||
event_id: "$" + Math.random() + "-" + Math.random(),
|
|
||||||
};
|
|
||||||
if (opts.skey !== undefined) {
|
|
||||||
event.state_key = opts.skey;
|
|
||||||
} else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
|
|
||||||
"m.room.power_levels", "m.room.topic",
|
|
||||||
"com.example.state"].includes(opts.type)) {
|
|
||||||
event.state_key = "";
|
|
||||||
}
|
|
||||||
return opts.event ? new MatrixEvent(event) : event;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an m.presence event.
|
|
||||||
* @param {Object} opts Values for the presence.
|
|
||||||
* @return {Object|MatrixEvent} The event
|
|
||||||
*/
|
|
||||||
export function mkPresence(opts) {
|
|
||||||
if (!opts.user) {
|
|
||||||
throw new Error("Missing user");
|
|
||||||
}
|
|
||||||
const event = {
|
|
||||||
event_id: "$" + Math.random() + "-" + Math.random(),
|
|
||||||
type: "m.presence",
|
|
||||||
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
|
||||||
content: {
|
|
||||||
avatar_url: opts.url,
|
|
||||||
displayname: opts.name,
|
|
||||||
last_active_ago: opts.ago,
|
|
||||||
presence: opts.presence || "offline",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return opts.event ? new MatrixEvent(event) : event;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an m.room.member event.
|
|
||||||
* @param {Object} opts Values for the membership.
|
|
||||||
* @param {string} opts.room The room ID for the event.
|
|
||||||
* @param {string} opts.mship The content.membership for the event.
|
|
||||||
* @param {string} opts.sender The sender user ID for the event.
|
|
||||||
* @param {string} opts.skey The target user ID for the event if applicable
|
|
||||||
* e.g. for invites/bans.
|
|
||||||
* @param {string} opts.name The content.displayname for the event.
|
|
||||||
* @param {string} opts.url The content.avatar_url for the event.
|
|
||||||
* @param {boolean} opts.event True to make a MatrixEvent.
|
|
||||||
* @return {Object|MatrixEvent} The event
|
|
||||||
*/
|
|
||||||
export function mkMembership(opts) {
|
|
||||||
opts.type = "m.room.member";
|
|
||||||
if (!opts.skey) {
|
|
||||||
opts.skey = opts.sender || opts.user;
|
|
||||||
}
|
|
||||||
if (!opts.mship) {
|
|
||||||
throw new Error("Missing .mship => " + JSON.stringify(opts));
|
|
||||||
}
|
|
||||||
opts.content = {
|
|
||||||
membership: opts.mship,
|
|
||||||
};
|
|
||||||
if (opts.name) {
|
|
||||||
opts.content.displayname = opts.name;
|
|
||||||
}
|
|
||||||
if (opts.url) {
|
|
||||||
opts.content.avatar_url = opts.url;
|
|
||||||
}
|
|
||||||
return mkEvent(opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an m.room.message event.
|
|
||||||
* @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 {boolean} opts.event True to make a MatrixEvent.
|
|
||||||
* @return {Object|MatrixEvent} The event
|
|
||||||
*/
|
|
||||||
export function mkMessage(opts) {
|
|
||||||
opts.type = "m.room.message";
|
|
||||||
if (!opts.msg) {
|
|
||||||
opts.msg = "Random->" + Math.random();
|
|
||||||
}
|
|
||||||
if (!opts.room || !opts.user) {
|
|
||||||
throw new Error("Missing .room or .user from %s", opts);
|
|
||||||
}
|
|
||||||
opts.content = {
|
|
||||||
msgtype: "m.text",
|
|
||||||
body: opts.msg,
|
|
||||||
};
|
|
||||||
return mkEvent(opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A mock implementation of webstorage
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
export function MockStorageApi() {
|
|
||||||
this.data = {};
|
|
||||||
}
|
|
||||||
MockStorageApi.prototype = {
|
|
||||||
get length() {
|
|
||||||
return Object.keys(this.data).length;
|
|
||||||
},
|
|
||||||
key: function(i) {
|
|
||||||
return Object.keys(this.data)[i];
|
|
||||||
},
|
|
||||||
setItem: function(k, v) {
|
|
||||||
this.data[k] = v;
|
|
||||||
},
|
|
||||||
getItem: function(k) {
|
|
||||||
return this.data[k] || null;
|
|
||||||
},
|
|
||||||
removeItem: function(k) {
|
|
||||||
delete this.data[k];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If an event is being decrypted, wait for it to finish being decrypted.
|
|
||||||
*
|
|
||||||
* @param {MatrixEvent} event
|
|
||||||
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
|
|
||||||
*/
|
|
||||||
export function awaitDecryption(event) {
|
|
||||||
// An event is not always decrypted ahead of time
|
|
||||||
// getClearContent is a good signal to know whether an event has been decrypted
|
|
||||||
// already
|
|
||||||
if (event.getClearContent() !== null) {
|
|
||||||
return event;
|
|
||||||
} else {
|
|
||||||
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
event.once('Event.decrypted', (ev) => {
|
|
||||||
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
|
||||||
resolve(ev);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HttpResponse(
|
|
||||||
httpLookups, acceptKeepalives, ignoreUnhandledSync,
|
|
||||||
) {
|
|
||||||
this.httpLookups = httpLookups;
|
|
||||||
this.acceptKeepalives = acceptKeepalives === undefined ? true : acceptKeepalives;
|
|
||||||
this.ignoreUnhandledSync = ignoreUnhandledSync;
|
|
||||||
this.pendingLookup = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpResponse.prototype.request = function(
|
|
||||||
cb, method, path, qp, data, prefix,
|
|
||||||
) {
|
|
||||||
if (path === HttpResponse.KEEP_ALIVE_PATH && this.acceptKeepalives) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
const next = this.httpLookups.shift();
|
|
||||||
const logLine = (
|
|
||||||
"MatrixClient[UT] RECV " + method + " " + path + " " +
|
|
||||||
"EXPECT " + (next ? next.method : next) + " " + (next ? next.path : next)
|
|
||||||
);
|
|
||||||
logger.log(logLine);
|
|
||||||
|
|
||||||
if (!next) { // no more things to return
|
|
||||||
if (method === "GET" && path === "/sync" && this.ignoreUnhandledSync) {
|
|
||||||
logger.log("MatrixClient[UT] Ignoring.");
|
|
||||||
return new Promise(() => {});
|
|
||||||
}
|
|
||||||
if (this.pendingLookup) {
|
|
||||||
if (this.pendingLookup.method === method
|
|
||||||
&& this.pendingLookup.path === path) {
|
|
||||||
return this.pendingLookup.promise;
|
|
||||||
}
|
|
||||||
// >1 pending thing, and they are different, whine.
|
|
||||||
expect(false).toBe(
|
|
||||||
true, ">1 pending request. You should probably handle them. " +
|
|
||||||
"PENDING: " + JSON.stringify(this.pendingLookup) + " JUST GOT: " +
|
|
||||||
method + " " + path,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.pendingLookup = {
|
|
||||||
promise: new Promise(() => {}),
|
|
||||||
method: method,
|
|
||||||
path: path,
|
|
||||||
};
|
|
||||||
return this.pendingLookup.promise;
|
|
||||||
}
|
|
||||||
if (next.path === path && next.method === method) {
|
|
||||||
logger.log(
|
|
||||||
"MatrixClient[UT] Matched. Returning " +
|
|
||||||
(next.error ? "BAD" : "GOOD") + " response",
|
|
||||||
);
|
|
||||||
if (next.expectBody) {
|
|
||||||
expect(next.expectBody).toEqual(data);
|
|
||||||
}
|
|
||||||
if (next.expectQueryParams) {
|
|
||||||
Object.keys(next.expectQueryParams).forEach(function(k) {
|
|
||||||
expect(qp[k]).toEqual(next.expectQueryParams[k]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (next.thenCall) {
|
|
||||||
process.nextTick(next.thenCall, 0); // next tick so we return first.
|
|
||||||
}
|
|
||||||
|
|
||||||
if (next.error) {
|
|
||||||
return Promise.reject({
|
|
||||||
errcode: next.error.errcode,
|
|
||||||
httpStatus: next.error.httpStatus,
|
|
||||||
name: next.error.errcode,
|
|
||||||
message: "Expected testing error",
|
|
||||||
data: next.error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Promise.resolve(next.data);
|
|
||||||
} else if (method === "GET" && path === "/sync" && this.ignoreUnhandledSync) {
|
|
||||||
logger.log("MatrixClient[UT] Ignoring.");
|
|
||||||
this.httpLookups.unshift(next);
|
|
||||||
return new Promise(() => {});
|
|
||||||
}
|
|
||||||
expect(true).toBe(false, "Expected different request. " + logLine);
|
|
||||||
return new Promise(() => {});
|
|
||||||
};
|
|
||||||
|
|
||||||
HttpResponse.KEEP_ALIVE_PATH = "/_matrix/client/versions";
|
|
||||||
|
|
||||||
HttpResponse.PUSH_RULES_RESPONSE = {
|
|
||||||
method: "GET",
|
|
||||||
path: "/pushrules/",
|
|
||||||
data: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
HttpResponse.PUSH_RULES_RESPONSE = {
|
|
||||||
method: "GET",
|
|
||||||
path: "/pushrules/",
|
|
||||||
data: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
HttpResponse.USER_ID = "@alice:bar";
|
|
||||||
|
|
||||||
HttpResponse.filterResponse = function(userId) {
|
|
||||||
const filterPath = "/user/" + encodeURIComponent(userId) + "/filter";
|
|
||||||
return {
|
|
||||||
method: "POST",
|
|
||||||
path: filterPath,
|
|
||||||
data: { filter_id: "f1lt3r" },
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
HttpResponse.SYNC_DATA = {
|
|
||||||
next_batch: "s_5_3",
|
|
||||||
presence: { events: [] },
|
|
||||||
rooms: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
HttpResponse.SYNC_RESPONSE = {
|
|
||||||
method: "GET",
|
|
||||||
path: "/sync",
|
|
||||||
data: HttpResponse.SYNC_DATA,
|
|
||||||
};
|
|
||||||
|
|
||||||
HttpResponse.defaultResponses = function(userId) {
|
|
||||||
return [
|
|
||||||
HttpResponse.PUSH_RULES_RESPONSE,
|
|
||||||
HttpResponse.filterResponse(userId),
|
|
||||||
HttpResponse.SYNC_RESPONSE,
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
export function setHttpResponses(
|
|
||||||
httpBackend, responses,
|
|
||||||
) {
|
|
||||||
responses.forEach(response => {
|
|
||||||
httpBackend
|
|
||||||
.when(response.method, response.path)
|
|
||||||
.respond(200, response.data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const emitPromise = (e, k) => new Promise(r => e.once(k, r));
|
|
||||||
292
spec/test-utils/test-utils.ts
Normal file
292
spec/test-utils/test-utils.ts
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import EventEmitter from "events";
|
||||||
|
|
||||||
|
// load olm before the sdk if possible
|
||||||
|
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 { SyncState } from "../../src/sync";
|
||||||
|
import { eventMapperFor } from "../../src/event-mapper";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a promise that is resolved when the client next emits a
|
||||||
|
* SYNCING event.
|
||||||
|
* @param {Object} client The client
|
||||||
|
* @param {Number=} count Number of syncs to wait for (default 1)
|
||||||
|
* @return {Promise} Resolves once the client has emitted a SYNCING event
|
||||||
|
*/
|
||||||
|
export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
|
||||||
|
if (count <= 0) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
const p = new Promise<void>((resolve) => {
|
||||||
|
const cb = (state: SyncState) => {
|
||||||
|
logger.log(`${Date.now()} syncPromise(${count}): ${state}`);
|
||||||
|
if (state === SyncState.Syncing) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
client.once(ClientEvent.Sync, cb);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
client.once(ClientEvent.Sync, cb);
|
||||||
|
});
|
||||||
|
|
||||||
|
return p.then(() => {
|
||||||
|
return syncPromise(client, count - 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a spy for an object and automatically spy its methods.
|
||||||
|
* @param {*} constr The class constructor (used with 'new')
|
||||||
|
* @param {string} name The name of the class
|
||||||
|
* @return {Object} An instantiated object with spied methods/properties.
|
||||||
|
*/
|
||||||
|
export function mock<T>(constr: { new(...args: any[]): T }, name: string): T {
|
||||||
|
// Based on http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/
|
||||||
|
const HelperConstr = new Function(); // jshint ignore:line
|
||||||
|
HelperConstr.prototype = constr.prototype;
|
||||||
|
// @ts-ignore
|
||||||
|
const result = new HelperConstr();
|
||||||
|
result.toString = function() {
|
||||||
|
return "mock" + (name ? " of " + name : "");
|
||||||
|
};
|
||||||
|
for (const key of Object.getOwnPropertyNames(constr.prototype)) { // eslint-disable-line guard-for-in
|
||||||
|
try {
|
||||||
|
if (constr.prototype[key] instanceof Function) {
|
||||||
|
result[key] = jest.fn();
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
// Direct access to some non-function fields of DOM prototypes may
|
||||||
|
// cause exceptions.
|
||||||
|
// Overwriting will not work either in that case.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IEventOpts {
|
||||||
|
type: EventType | string;
|
||||||
|
room: string;
|
||||||
|
sender?: string;
|
||||||
|
skey?: string;
|
||||||
|
content: IContent;
|
||||||
|
event?: boolean;
|
||||||
|
user?: string;
|
||||||
|
unsigned?: IUnsigned;
|
||||||
|
redacts?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let testEventIndex = 1; // counter for events, easier for comparison of randomly generated events
|
||||||
|
/**
|
||||||
|
* Create an Event.
|
||||||
|
* @param {Object} opts Values for the event.
|
||||||
|
* @param {string} opts.type The event.type
|
||||||
|
* @param {string} opts.room The event.room_id
|
||||||
|
* @param {string} opts.sender The event.sender
|
||||||
|
* @param {string} opts.skey Optional. The state key (auto inserts empty string)
|
||||||
|
* @param {Object} opts.content The event.content
|
||||||
|
* @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} a JSON object representing this event.
|
||||||
|
*/
|
||||||
|
export function mkEvent(opts: IEventOpts, client?: MatrixClient): object | MatrixEvent {
|
||||||
|
if (!opts.type || !opts.content) {
|
||||||
|
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
|
||||||
|
}
|
||||||
|
const event: Partial<IEvent> = {
|
||||||
|
type: opts.type as string,
|
||||||
|
room_id: opts.room,
|
||||||
|
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
||||||
|
content: opts.content,
|
||||||
|
unsigned: opts.unsigned || {},
|
||||||
|
event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(),
|
||||||
|
txn_id: "~" + Math.random(),
|
||||||
|
redacts: opts.redacts,
|
||||||
|
};
|
||||||
|
if (opts.skey !== undefined) {
|
||||||
|
event.state_key = opts.skey;
|
||||||
|
} else if ([
|
||||||
|
EventType.RoomName,
|
||||||
|
EventType.RoomTopic,
|
||||||
|
EventType.RoomCreate,
|
||||||
|
EventType.RoomJoinRules,
|
||||||
|
EventType.RoomPowerLevels,
|
||||||
|
EventType.RoomTopic,
|
||||||
|
"com.example.state",
|
||||||
|
].includes(opts.type)) {
|
||||||
|
event.state_key = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.event && client) {
|
||||||
|
return eventMapperFor(client, {})(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts.event ? new MatrixEvent(event) : event;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPresenceOpts {
|
||||||
|
user?: string;
|
||||||
|
sender?: string;
|
||||||
|
url: string;
|
||||||
|
name: string;
|
||||||
|
ago: number;
|
||||||
|
presence?: string;
|
||||||
|
event?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an m.presence event.
|
||||||
|
* @param {Object} opts Values for the presence.
|
||||||
|
* @return {Object|MatrixEvent} The event
|
||||||
|
*/
|
||||||
|
export function mkPresence(opts: IPresenceOpts): object | MatrixEvent {
|
||||||
|
const event = {
|
||||||
|
event_id: "$" + Math.random() + "-" + Math.random(),
|
||||||
|
type: "m.presence",
|
||||||
|
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
||||||
|
content: {
|
||||||
|
avatar_url: opts.url,
|
||||||
|
displayname: opts.name,
|
||||||
|
last_active_ago: opts.ago,
|
||||||
|
presence: opts.presence || "offline",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return opts.event ? new MatrixEvent(event) : event;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IMembershipOpts {
|
||||||
|
room: string;
|
||||||
|
mship: string;
|
||||||
|
sender?: string;
|
||||||
|
user?: string;
|
||||||
|
skey?: string;
|
||||||
|
name?: string;
|
||||||
|
url?: string;
|
||||||
|
event?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an m.room.member event.
|
||||||
|
* @param {Object} opts Values for the membership.
|
||||||
|
* @param {string} opts.room The room ID for the event.
|
||||||
|
* @param {string} opts.mship The content.membership for the event.
|
||||||
|
* @param {string} opts.sender The sender user ID for the event.
|
||||||
|
* @param {string} opts.skey The target user ID for the event if applicable
|
||||||
|
* e.g. for invites/bans.
|
||||||
|
* @param {string} opts.name The content.displayname for the event.
|
||||||
|
* @param {string} opts.url The content.avatar_url for the event.
|
||||||
|
* @param {boolean} opts.event True to make a MatrixEvent.
|
||||||
|
* @return {Object|MatrixEvent} The event
|
||||||
|
*/
|
||||||
|
export function mkMembership(opts: IMembershipOpts): object | MatrixEvent {
|
||||||
|
const eventOpts: IEventOpts = {
|
||||||
|
...opts,
|
||||||
|
type: EventType.RoomMember,
|
||||||
|
content: {
|
||||||
|
membership: opts.mship,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!opts.skey) {
|
||||||
|
eventOpts.skey = opts.sender || opts.user;
|
||||||
|
}
|
||||||
|
if (opts.name) {
|
||||||
|
eventOpts.content.displayname = opts.name;
|
||||||
|
}
|
||||||
|
if (opts.url) {
|
||||||
|
eventOpts.content.avatar_url = opts.url;
|
||||||
|
}
|
||||||
|
return mkEvent(eventOpts);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IMessageOpts {
|
||||||
|
room: string;
|
||||||
|
user: string;
|
||||||
|
msg?: string;
|
||||||
|
event?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an m.room.message event.
|
||||||
|
* @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 {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 mkMessage(opts: IMessageOpts, client?: MatrixClient): object | MatrixEvent {
|
||||||
|
const eventOpts: IEventOpts = {
|
||||||
|
...opts,
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
content: {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: opts.msg,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!eventOpts.content.body) {
|
||||||
|
eventOpts.content.body = "Random->" + Math.random();
|
||||||
|
}
|
||||||
|
return mkEvent(eventOpts, client);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mock implementation of webstorage
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export class MockStorageApi {
|
||||||
|
private data: Record<string, any> = {};
|
||||||
|
|
||||||
|
public get length() {
|
||||||
|
return Object.keys(this.data).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public key(i: number): any {
|
||||||
|
return Object.keys(this.data)[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
public setItem(k: string, v: any): void {
|
||||||
|
this.data[k] = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getItem(k: string): any {
|
||||||
|
return this.data[k] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeItem(k: string): void {
|
||||||
|
delete this.data[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If an event is being decrypted, wait for it to finish being decrypted.
|
||||||
|
*
|
||||||
|
* @param {MatrixEvent} event
|
||||||
|
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
|
||||||
|
*/
|
||||||
|
export async function awaitDecryption(event: MatrixEvent): Promise<MatrixEvent> {
|
||||||
|
// An event is not always decrypted ahead of time
|
||||||
|
// getClearContent is a good signal to know whether an event has been decrypted
|
||||||
|
// already
|
||||||
|
if (event.getClearContent() !== null) {
|
||||||
|
return event;
|
||||||
|
} else {
|
||||||
|
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
event.once(MatrixEventEvent.Decrypted, (ev) => {
|
||||||
|
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||||
|
resolve(ev);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise(r => e.once(k, r));
|
||||||
@@ -16,9 +16,14 @@ limitations under the License.
|
|||||||
|
|
||||||
import { REFERENCE_RELATION } from "matrix-events-sdk";
|
import { REFERENCE_RELATION } from "matrix-events-sdk";
|
||||||
|
|
||||||
import { M_BEACON_INFO } from "../../src/@types/beacon";
|
|
||||||
import { LocationAssetType, M_ASSET, M_LOCATION, M_TIMESTAMP } from "../../src/@types/location";
|
import { LocationAssetType, M_ASSET, M_LOCATION, M_TIMESTAMP } from "../../src/@types/location";
|
||||||
import { makeBeaconContent, makeBeaconInfoContent } from "../../src/content-helpers";
|
import { M_TOPIC } from "../../src/@types/topic";
|
||||||
|
import {
|
||||||
|
makeBeaconContent,
|
||||||
|
makeBeaconInfoContent,
|
||||||
|
makeTopicContent,
|
||||||
|
parseTopicContent,
|
||||||
|
} from "../../src/content-helpers";
|
||||||
|
|
||||||
describe('Beacon content helpers', () => {
|
describe('Beacon content helpers', () => {
|
||||||
describe('makeBeaconInfoContent()', () => {
|
describe('makeBeaconInfoContent()', () => {
|
||||||
@@ -36,11 +41,9 @@ describe('Beacon content helpers', () => {
|
|||||||
'nice beacon_info',
|
'nice beacon_info',
|
||||||
LocationAssetType.Pin,
|
LocationAssetType.Pin,
|
||||||
)).toEqual({
|
)).toEqual({
|
||||||
[M_BEACON_INFO.name]: {
|
|
||||||
description: 'nice beacon_info',
|
description: 'nice beacon_info',
|
||||||
timeout: 1234,
|
timeout: 1234,
|
||||||
live: true,
|
live: true,
|
||||||
},
|
|
||||||
[M_TIMESTAMP.name]: mockDateNow,
|
[M_TIMESTAMP.name]: mockDateNow,
|
||||||
[M_ASSET.name]: {
|
[M_ASSET.name]: {
|
||||||
type: LocationAssetType.Pin,
|
type: LocationAssetType.Pin,
|
||||||
@@ -125,3 +128,68 @@ describe('Beacon content helpers', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Topic content helpers', () => {
|
||||||
|
describe('makeTopicContent()', () => {
|
||||||
|
it('creates fully defined event content without html', () => {
|
||||||
|
expect(makeTopicContent("pizza")).toEqual({
|
||||||
|
topic: "pizza",
|
||||||
|
[M_TOPIC.name]: [{
|
||||||
|
body: "pizza",
|
||||||
|
mimetype: "text/plain",
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates fully defined event content with html', () => {
|
||||||
|
expect(makeTopicContent("pizza", "<b>pizza</b>")).toEqual({
|
||||||
|
topic: "pizza",
|
||||||
|
[M_TOPIC.name]: [{
|
||||||
|
body: "pizza",
|
||||||
|
mimetype: "text/plain",
|
||||||
|
}, {
|
||||||
|
body: "<b>pizza</b>",
|
||||||
|
mimetype: "text/html",
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseTopicContent()', () => {
|
||||||
|
it('parses event content with plain text topic without mimetype', () => {
|
||||||
|
expect(parseTopicContent({
|
||||||
|
topic: "pizza",
|
||||||
|
[M_TOPIC.name]: [{
|
||||||
|
body: "pizza",
|
||||||
|
}],
|
||||||
|
})).toEqual({
|
||||||
|
text: "pizza",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses event content with plain text topic', () => {
|
||||||
|
expect(parseTopicContent({
|
||||||
|
topic: "pizza",
|
||||||
|
[M_TOPIC.name]: [{
|
||||||
|
body: "pizza",
|
||||||
|
mimetype: "text/plain",
|
||||||
|
}],
|
||||||
|
})).toEqual({
|
||||||
|
text: "pizza",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses event content with html topic', () => {
|
||||||
|
expect(parseTopicContent({
|
||||||
|
topic: "pizza",
|
||||||
|
[M_TOPIC.name]: [{
|
||||||
|
body: "<b>pizza</b>",
|
||||||
|
mimetype: "text/html",
|
||||||
|
}],
|
||||||
|
})).toEqual({
|
||||||
|
text: "pizza",
|
||||||
|
html: "<b>pizza</b>",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import * as olmlib from "../../src/crypto/olmlib";
|
|||||||
import { sleep } from "../../src/utils";
|
import { sleep } from "../../src/utils";
|
||||||
import { CRYPTO_ENABLED } from "../../src/client";
|
import { CRYPTO_ENABLED } from "../../src/client";
|
||||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||||
|
import { logger } from '../../src/logger';
|
||||||
|
|
||||||
const Olm = global.Olm;
|
const Olm = global.Olm;
|
||||||
|
|
||||||
@@ -400,4 +401,28 @@ describe("Crypto", function() {
|
|||||||
expect(aliceClient.sendToDevice.mock.calls[2][2]).not.toBe(txnId);
|
expect(aliceClient.sendToDevice.mock.calls[2][2]).not.toBe(txnId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Secret storage', function() {
|
||||||
|
it("creates secret storage even if there is no keyInfo", async function() {
|
||||||
|
jest.spyOn(logger, 'log').mockImplementation(() => {});
|
||||||
|
jest.setTimeout(10000);
|
||||||
|
const client = (new TestClient("@a:example.com", "dev")).client;
|
||||||
|
await client.initCrypto();
|
||||||
|
client.crypto.getSecretStorageKey = async () => null;
|
||||||
|
client.crypto.isCrossSigningReady = async () => false;
|
||||||
|
client.crypto.baseApis.uploadDeviceSigningKeys = () => null;
|
||||||
|
client.crypto.baseApis.setAccountData = () => null;
|
||||||
|
client.crypto.baseApis.uploadKeySignatures = () => null;
|
||||||
|
client.crypto.baseApis.http.authedRequest = () => null;
|
||||||
|
const createSecretStorageKey = async () => {
|
||||||
|
return {
|
||||||
|
keyInfo: undefined, // Returning undefined here used to cause a crash
|
||||||
|
privateKey: Uint8Array.of(32, 33),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
await client.crypto.bootstrapSecretStorage({
|
||||||
|
createSecretStorageKey,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,11 +20,33 @@ import anotherjson from 'another-json';
|
|||||||
|
|
||||||
import * as olmlib from "../../../src/crypto/olmlib";
|
import * as olmlib from "../../../src/crypto/olmlib";
|
||||||
import { TestClient } from '../../TestClient';
|
import { TestClient } from '../../TestClient';
|
||||||
import { HttpResponse, setHttpResponses } from '../../test-utils/test-utils';
|
|
||||||
import { resetCrossSigningKeys } from "./crypto-utils";
|
import { resetCrossSigningKeys } from "./crypto-utils";
|
||||||
import { MatrixError } from '../../../src/http-api';
|
import { MatrixError } from '../../../src/http-api';
|
||||||
import { logger } from '../../../src/logger';
|
import { logger } from '../../../src/logger';
|
||||||
|
|
||||||
|
const PUSH_RULES_RESPONSE = {
|
||||||
|
method: "GET",
|
||||||
|
path: "/pushrules/",
|
||||||
|
data: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterResponse = function(userId) {
|
||||||
|
const filterPath = "/user/" + encodeURIComponent(userId) + "/filter";
|
||||||
|
return {
|
||||||
|
method: "POST",
|
||||||
|
path: filterPath,
|
||||||
|
data: { filter_id: "f1lt3r" },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function setHttpResponses(httpBackend, responses) {
|
||||||
|
responses.forEach(response => {
|
||||||
|
httpBackend
|
||||||
|
.when(response.method, response.path)
|
||||||
|
.respond(200, response.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function makeTestClient(userInfo, options, keys) {
|
async function makeTestClient(userInfo, options, keys) {
|
||||||
if (!keys) keys = {};
|
if (!keys) keys = {};
|
||||||
|
|
||||||
@@ -237,7 +259,7 @@ describe("Cross Signing", function() {
|
|||||||
|
|
||||||
// feed sync result that includes master key, ssk, device key
|
// feed sync result that includes master key, ssk, device key
|
||||||
const responses = [
|
const responses = [
|
||||||
HttpResponse.PUSH_RULES_RESPONSE,
|
PUSH_RULES_RESPONSE,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: "/keys/upload",
|
path: "/keys/upload",
|
||||||
@@ -248,7 +270,7 @@ describe("Cross Signing", function() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
HttpResponse.filterResponse("@alice:example.com"),
|
filterResponse("@alice:example.com"),
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
path: "/sync",
|
path: "/sync",
|
||||||
@@ -493,7 +515,7 @@ describe("Cross Signing", function() {
|
|||||||
// - master key signed by her usk (pretend that it was signed by another
|
// - master key signed by her usk (pretend that it was signed by another
|
||||||
// of Alice's devices)
|
// of Alice's devices)
|
||||||
const responses = [
|
const responses = [
|
||||||
HttpResponse.PUSH_RULES_RESPONSE,
|
PUSH_RULES_RESPONSE,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: "/keys/upload",
|
path: "/keys/upload",
|
||||||
@@ -504,7 +526,7 @@ describe("Cross Signing", function() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
HttpResponse.filterResponse("@alice:example.com"),
|
filterResponse("@alice:example.com"),
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
path: "/sync",
|
path: "/sync",
|
||||||
@@ -861,4 +883,138 @@ describe("Cross Signing", function() {
|
|||||||
expect(bobTrust3.isCrossSigningVerified()).toBeTruthy();
|
expect(bobTrust3.isCrossSigningVerified()).toBeTruthy();
|
||||||
expect(bobTrust3.isTofu()).toBeTruthy();
|
expect(bobTrust3.isTofu()).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it(
|
||||||
|
"should observe that our own device is cross-signed, even if this device doesn't trust the key",
|
||||||
|
async function() {
|
||||||
|
const { client: alice } = await makeTestClient(
|
||||||
|
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||||
|
);
|
||||||
|
alice.uploadDeviceSigningKeys = async () => {};
|
||||||
|
alice.uploadKeySignatures = async () => {};
|
||||||
|
|
||||||
|
// Generate Alice's SSK etc
|
||||||
|
const aliceMasterSigning = new global.Olm.PkSigning();
|
||||||
|
const aliceMasterPrivkey = aliceMasterSigning.generate_seed();
|
||||||
|
const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey);
|
||||||
|
const aliceSigning = new global.Olm.PkSigning();
|
||||||
|
const alicePrivkey = aliceSigning.generate_seed();
|
||||||
|
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
|
||||||
|
const aliceSSK = {
|
||||||
|
user_id: "@alice:example.com",
|
||||||
|
usage: ["self_signing"],
|
||||||
|
keys: {
|
||||||
|
["ed25519:" + alicePubkey]: alicePubkey,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const sskSig = aliceMasterSigning.sign(anotherjson.stringify(aliceSSK));
|
||||||
|
aliceSSK.signatures = {
|
||||||
|
"@alice:example.com": {
|
||||||
|
["ed25519:" + aliceMasterPubkey]: sskSig,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Alice's device downloads the keys, but doesn't trust them yet
|
||||||
|
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||||
|
keys: {
|
||||||
|
master: {
|
||||||
|
user_id: "@alice:example.com",
|
||||||
|
usage: ["master"],
|
||||||
|
keys: {
|
||||||
|
["ed25519:" + aliceMasterPubkey]: aliceMasterPubkey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
self_signing: aliceSSK,
|
||||||
|
},
|
||||||
|
firstUse: 1,
|
||||||
|
unsigned: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Alice has a second device that's cross-signed
|
||||||
|
const aliceCrossSignedDevice = {
|
||||||
|
user_id: "@alice:example.com",
|
||||||
|
device_id: "Dynabook",
|
||||||
|
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
|
||||||
|
keys: {
|
||||||
|
"curve25519:Dynabook": "somePubkey",
|
||||||
|
"ed25519:Dynabook": "someOtherPubkey",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const sig = aliceSigning.sign(anotherjson.stringify(aliceCrossSignedDevice));
|
||||||
|
aliceCrossSignedDevice.signatures = {
|
||||||
|
"@alice:example.com": {
|
||||||
|
["ed25519:" + alicePubkey]: sig,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
alice.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||||
|
Dynabook: aliceCrossSignedDevice,
|
||||||
|
});
|
||||||
|
|
||||||
|
// We don't trust the cross-signing keys yet...
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("should observe that our own device isn't cross-signed", async function() {
|
||||||
|
const { client: alice } = await makeTestClient(
|
||||||
|
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||||
|
);
|
||||||
|
alice.uploadDeviceSigningKeys = async () => {};
|
||||||
|
alice.uploadKeySignatures = async () => {};
|
||||||
|
|
||||||
|
// Generate Alice's SSK etc
|
||||||
|
const aliceMasterSigning = new global.Olm.PkSigning();
|
||||||
|
const aliceMasterPrivkey = aliceMasterSigning.generate_seed();
|
||||||
|
const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey);
|
||||||
|
const aliceSigning = new global.Olm.PkSigning();
|
||||||
|
const alicePrivkey = aliceSigning.generate_seed();
|
||||||
|
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
|
||||||
|
const aliceSSK = {
|
||||||
|
user_id: "@alice:example.com",
|
||||||
|
usage: ["self_signing"],
|
||||||
|
keys: {
|
||||||
|
["ed25519:" + alicePubkey]: alicePubkey,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const sskSig = aliceMasterSigning.sign(anotherjson.stringify(aliceSSK));
|
||||||
|
aliceSSK.signatures = {
|
||||||
|
"@alice:example.com": {
|
||||||
|
["ed25519:" + aliceMasterPubkey]: sskSig,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Alice's device downloads the keys
|
||||||
|
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||||
|
keys: {
|
||||||
|
master: {
|
||||||
|
user_id: "@alice:example.com",
|
||||||
|
usage: ["master"],
|
||||||
|
keys: {
|
||||||
|
["ed25519:" + aliceMasterPubkey]: aliceMasterPubkey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
self_signing: aliceSSK,
|
||||||
|
},
|
||||||
|
firstUse: 1,
|
||||||
|
unsigned: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Alice has a second device that's also not cross-signed
|
||||||
|
const aliceNotCrossSignedDevice = {
|
||||||
|
user_id: "@alice:example.com",
|
||||||
|
device_id: "Dynabook",
|
||||||
|
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
|
||||||
|
keys: {
|
||||||
|
"curve25519:Dynabook": "somePubkey",
|
||||||
|
"ed25519:Dynabook": "someOtherPubkey",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
alice.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||||
|
Dynabook: aliceNotCrossSignedDevice,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(alice.checkIfOwnDeviceCrossSigned(aliceNotCrossSignedDevice.device_id)).toBeFalsy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
180
spec/unit/event-mapper.spec.ts
Normal file
180
spec/unit/event-mapper.spec.ts
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
/*
|
||||||
|
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 { MatrixClient, MatrixEvent, MatrixEventEvent, MatrixScheduler, Room } from "../../src";
|
||||||
|
import { eventMapperFor } from "../../src/event-mapper";
|
||||||
|
import { IStore } from "../../src/store";
|
||||||
|
|
||||||
|
describe("eventMapperFor", function() {
|
||||||
|
let rooms: Room[] = [];
|
||||||
|
|
||||||
|
const userId = "@test:example.org";
|
||||||
|
|
||||||
|
let client: MatrixClient;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
client = new MatrixClient({
|
||||||
|
baseUrl: "https://my.home.server",
|
||||||
|
accessToken: "my.access.token",
|
||||||
|
request: function() {} as any, // NOP
|
||||||
|
store: {
|
||||||
|
getRoom(roomId: string): Room | null {
|
||||||
|
return rooms.find(r => r.roomId === roomId);
|
||||||
|
},
|
||||||
|
} as IStore,
|
||||||
|
scheduler: {
|
||||||
|
setProcessFunction: jest.fn(),
|
||||||
|
} as unknown as MatrixScheduler,
|
||||||
|
userId: userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
rooms = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
rooms.push(room);
|
||||||
|
|
||||||
|
const mapper = eventMapperFor(client, {
|
||||||
|
preventReEmit: true,
|
||||||
|
decrypt: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventId = "$event1:server";
|
||||||
|
const eventDefinition = {
|
||||||
|
type: "m.room.message",
|
||||||
|
room_id: roomId,
|
||||||
|
sender: userId,
|
||||||
|
content: {
|
||||||
|
body: "body",
|
||||||
|
},
|
||||||
|
unsigned: {},
|
||||||
|
event_id: eventId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const event = mapper(eventDefinition);
|
||||||
|
expect(event).toBeInstanceOf(MatrixEvent);
|
||||||
|
|
||||||
|
room.addLiveEvents([event]);
|
||||||
|
expect(room.findEventById(eventId)).toBe(event);
|
||||||
|
|
||||||
|
const event2 = mapper(eventDefinition);
|
||||||
|
expect(event).toBe(event2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not de-duplicate state events due to directionality of sentinel members", async () => {
|
||||||
|
const roomId = "!room:example.org";
|
||||||
|
const room = new Room(roomId, client, userId);
|
||||||
|
rooms.push(room);
|
||||||
|
|
||||||
|
const mapper = eventMapperFor(client, {
|
||||||
|
preventReEmit: true,
|
||||||
|
decrypt: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventId = "$event1:server";
|
||||||
|
const eventDefinition = {
|
||||||
|
type: "m.room.name",
|
||||||
|
room_id: roomId,
|
||||||
|
sender: userId,
|
||||||
|
content: {
|
||||||
|
name: "Room name",
|
||||||
|
},
|
||||||
|
unsigned: {},
|
||||||
|
event_id: eventId,
|
||||||
|
state_key: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const event = mapper(eventDefinition);
|
||||||
|
expect(event).toBeInstanceOf(MatrixEvent);
|
||||||
|
|
||||||
|
room.oldState.setStateEvents([event]);
|
||||||
|
room.currentState.setStateEvents([event]);
|
||||||
|
room.addLiveEvents([event]);
|
||||||
|
expect(room.findEventById(eventId)).toBe(event);
|
||||||
|
|
||||||
|
const event2 = mapper(eventDefinition);
|
||||||
|
expect(event).not.toBe(event2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should decrypt appropriately", async () => {
|
||||||
|
const roomId = "!room:example.org";
|
||||||
|
const room = new Room(roomId, client, userId);
|
||||||
|
rooms.push(room);
|
||||||
|
|
||||||
|
const eventId = "$event1:server";
|
||||||
|
const eventDefinition = {
|
||||||
|
type: "m.room.encrypted",
|
||||||
|
room_id: roomId,
|
||||||
|
sender: userId,
|
||||||
|
content: {
|
||||||
|
ciphertext: "",
|
||||||
|
},
|
||||||
|
unsigned: {},
|
||||||
|
event_id: eventId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const decryptEventIfNeededSpy = jest.spyOn(client, "decryptEventIfNeeded");
|
||||||
|
decryptEventIfNeededSpy.mockResolvedValue(); // stub it out
|
||||||
|
|
||||||
|
const mapper = eventMapperFor(client, {
|
||||||
|
decrypt: true,
|
||||||
|
});
|
||||||
|
const event = mapper(eventDefinition);
|
||||||
|
expect(event).toBeInstanceOf(MatrixEvent);
|
||||||
|
expect(decryptEventIfNeededSpy).toHaveBeenCalledWith(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should configure re-emitter appropriately", async () => {
|
||||||
|
const roomId = "!room:example.org";
|
||||||
|
const room = new Room(roomId, client, userId);
|
||||||
|
rooms.push(room);
|
||||||
|
|
||||||
|
const eventId = "$event1:server";
|
||||||
|
const eventDefinition = {
|
||||||
|
type: "m.room.message",
|
||||||
|
room_id: roomId,
|
||||||
|
sender: userId,
|
||||||
|
content: {
|
||||||
|
body: "body",
|
||||||
|
},
|
||||||
|
unsigned: {},
|
||||||
|
event_id: eventId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const evListener = jest.fn();
|
||||||
|
client.on(MatrixEventEvent.Replaced, evListener);
|
||||||
|
|
||||||
|
const noReEmitMapper = eventMapperFor(client, {
|
||||||
|
preventReEmit: true,
|
||||||
|
});
|
||||||
|
const event1 = noReEmitMapper(eventDefinition);
|
||||||
|
expect(event1).toBeInstanceOf(MatrixEvent);
|
||||||
|
event1.emit(MatrixEventEvent.Replaced, event1);
|
||||||
|
expect(evListener).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
const reEmitMapper = eventMapperFor(client, {
|
||||||
|
preventReEmit: false,
|
||||||
|
});
|
||||||
|
const event2 = reEmitMapper(eventDefinition);
|
||||||
|
expect(event2).toBeInstanceOf(MatrixEvent);
|
||||||
|
event2.emit(MatrixEventEvent.Replaced, event2);
|
||||||
|
expect(evListener.mock.calls[0][0]).toEqual(event2);
|
||||||
|
|
||||||
|
expect(event1).not.toBe(event2); // the event wasn't added to a room so de-duplication wouldn't occur
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017 New Vector Ltd
|
||||||
Copyright 2019 The Matrix.org Foundaction C.I.C.
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
MatrixEvent,
|
||||||
RelationType,
|
RelationType,
|
||||||
} from "../../src";
|
} from "../../src";
|
||||||
import { FilterComponent } from "../../src/filter-component";
|
import { FilterComponent } from "../../src/filter-component";
|
||||||
@@ -13,7 +14,7 @@ describe("Filter Component", function() {
|
|||||||
content: { },
|
content: { },
|
||||||
room: 'roomId',
|
room: 'roomId',
|
||||||
event: true,
|
event: true,
|
||||||
});
|
}) as MatrixEvent;
|
||||||
|
|
||||||
const checkResult = filter.check(event);
|
const checkResult = filter.check(event);
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ describe("Filter Component", function() {
|
|||||||
content: { },
|
content: { },
|
||||||
room: 'roomId',
|
room: 'roomId',
|
||||||
event: true,
|
event: true,
|
||||||
});
|
}) as MatrixEvent;
|
||||||
|
|
||||||
const checkResult = filter.check(event);
|
const checkResult = filter.check(event);
|
||||||
|
|
||||||
@@ -54,7 +55,7 @@ describe("Filter Component", function() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
}) as MatrixEvent;
|
||||||
|
|
||||||
expect(filter.check(threadRootNotParticipated)).toBe(false);
|
expect(filter.check(threadRootNotParticipated)).toBe(false);
|
||||||
});
|
});
|
||||||
@@ -79,7 +80,7 @@ describe("Filter Component", function() {
|
|||||||
user: '@someone-else:server.org',
|
user: '@someone-else:server.org',
|
||||||
room: 'roomId',
|
room: 'roomId',
|
||||||
event: true,
|
event: true,
|
||||||
});
|
}) as MatrixEvent;
|
||||||
|
|
||||||
expect(filter.check(threadRootParticipated)).toBe(true);
|
expect(filter.check(threadRootParticipated)).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -99,7 +100,7 @@ describe("Filter Component", function() {
|
|||||||
[RelationType.Reference]: {},
|
[RelationType.Reference]: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
}) as MatrixEvent;
|
||||||
|
|
||||||
expect(filter.check(referenceRelationEvent)).toBe(false);
|
expect(filter.check(referenceRelationEvent)).toBe(false);
|
||||||
});
|
});
|
||||||
@@ -122,7 +123,7 @@ describe("Filter Component", function() {
|
|||||||
},
|
},
|
||||||
room: 'roomId',
|
room: 'roomId',
|
||||||
event: true,
|
event: true,
|
||||||
});
|
}) as MatrixEvent;
|
||||||
|
|
||||||
const eventWithMultipleRelations = mkEvent({
|
const eventWithMultipleRelations = mkEvent({
|
||||||
"type": "m.room.message",
|
"type": "m.room.message",
|
||||||
@@ -147,7 +148,7 @@ describe("Filter Component", function() {
|
|||||||
},
|
},
|
||||||
"room": 'roomId',
|
"room": 'roomId',
|
||||||
"event": true,
|
"event": true,
|
||||||
});
|
}) as MatrixEvent;
|
||||||
|
|
||||||
const noMatchEvent = mkEvent({
|
const noMatchEvent = mkEvent({
|
||||||
"type": "m.room.message",
|
"type": "m.room.message",
|
||||||
@@ -159,7 +160,7 @@ describe("Filter Component", function() {
|
|||||||
},
|
},
|
||||||
"room": 'roomId',
|
"room": 'roomId',
|
||||||
"event": true,
|
"event": true,
|
||||||
});
|
}) as MatrixEvent;
|
||||||
|
|
||||||
expect(filter.check(threadRootEvent)).toBe(true);
|
expect(filter.check(threadRootEvent)).toBe(true);
|
||||||
expect(filter.check(eventWithMultipleRelations)).toBe(true);
|
expect(filter.check(eventWithMultipleRelations)).toBe(true);
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ limitations under the License.
|
|||||||
import { logger } from "../../src/logger";
|
import { logger } from "../../src/logger";
|
||||||
import { InteractiveAuth } from "../../src/interactive-auth";
|
import { InteractiveAuth } from "../../src/interactive-auth";
|
||||||
import { MatrixError } from "../../src/http-api";
|
import { MatrixError } from "../../src/http-api";
|
||||||
|
import { sleep } from "../../src/utils";
|
||||||
|
import { randomString } from "../../src/randomstring";
|
||||||
|
|
||||||
// Trivial client object to test interactive auth
|
// Trivial client object to test interactive auth
|
||||||
// (we do not need TestClient here)
|
// (we do not need TestClient here)
|
||||||
@@ -172,4 +174,107 @@ describe("InteractiveAuth", function() {
|
|||||||
expect(error.message).toBe('No appropriate authentication flow found');
|
expect(error.message).toBe('No appropriate authentication flow found');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("requestEmailToken", () => {
|
||||||
|
it("increases auth attempts", async () => {
|
||||||
|
const doRequest = jest.fn();
|
||||||
|
const stateUpdated = jest.fn();
|
||||||
|
const requestEmailToken = jest.fn();
|
||||||
|
requestEmailToken.mockImplementation(async () => ({ sid: "" }));
|
||||||
|
|
||||||
|
const ia = new InteractiveAuth({
|
||||||
|
matrixClient: new FakeClient(),
|
||||||
|
doRequest, stateUpdated, requestEmailToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
await ia.requestEmailToken();
|
||||||
|
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 1, undefined);
|
||||||
|
requestEmailToken.mockClear();
|
||||||
|
await ia.requestEmailToken();
|
||||||
|
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 2, undefined);
|
||||||
|
requestEmailToken.mockClear();
|
||||||
|
await ia.requestEmailToken();
|
||||||
|
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 3, undefined);
|
||||||
|
requestEmailToken.mockClear();
|
||||||
|
await ia.requestEmailToken();
|
||||||
|
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 4, undefined);
|
||||||
|
requestEmailToken.mockClear();
|
||||||
|
await ia.requestEmailToken();
|
||||||
|
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 5, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("increases auth attempts", async () => {
|
||||||
|
const doRequest = jest.fn();
|
||||||
|
const stateUpdated = jest.fn();
|
||||||
|
const requestEmailToken = jest.fn();
|
||||||
|
requestEmailToken.mockImplementation(async () => ({ sid: "" }));
|
||||||
|
|
||||||
|
const ia = new InteractiveAuth({
|
||||||
|
matrixClient: new FakeClient(),
|
||||||
|
doRequest, stateUpdated, requestEmailToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
await ia.requestEmailToken();
|
||||||
|
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 1, undefined);
|
||||||
|
requestEmailToken.mockClear();
|
||||||
|
await ia.requestEmailToken();
|
||||||
|
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 2, undefined);
|
||||||
|
requestEmailToken.mockClear();
|
||||||
|
await ia.requestEmailToken();
|
||||||
|
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 3, undefined);
|
||||||
|
requestEmailToken.mockClear();
|
||||||
|
await ia.requestEmailToken();
|
||||||
|
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 4, undefined);
|
||||||
|
requestEmailToken.mockClear();
|
||||||
|
await ia.requestEmailToken();
|
||||||
|
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 5, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes errors through", async () => {
|
||||||
|
const doRequest = jest.fn();
|
||||||
|
const stateUpdated = jest.fn();
|
||||||
|
const requestEmailToken = jest.fn();
|
||||||
|
requestEmailToken.mockImplementation(async () => {
|
||||||
|
throw new Error("unspecific network error");
|
||||||
|
});
|
||||||
|
|
||||||
|
const ia = new InteractiveAuth({
|
||||||
|
matrixClient: new FakeClient(),
|
||||||
|
doRequest, stateUpdated, requestEmailToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(async () => await ia.requestEmailToken()).rejects.toThrowError("unspecific network error");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("only starts one request at a time", async () => {
|
||||||
|
const doRequest = jest.fn();
|
||||||
|
const stateUpdated = jest.fn();
|
||||||
|
const requestEmailToken = jest.fn();
|
||||||
|
requestEmailToken.mockImplementation(() => sleep(500, { sid: "" }));
|
||||||
|
|
||||||
|
const ia = new InteractiveAuth({
|
||||||
|
matrixClient: new FakeClient(),
|
||||||
|
doRequest, stateUpdated, requestEmailToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all([ia.requestEmailToken(), ia.requestEmailToken(), ia.requestEmailToken()]);
|
||||||
|
expect(requestEmailToken).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores result in email sid", async () => {
|
||||||
|
const doRequest = jest.fn();
|
||||||
|
const stateUpdated = jest.fn();
|
||||||
|
const requestEmailToken = jest.fn();
|
||||||
|
const sid = randomString(24);
|
||||||
|
requestEmailToken.mockImplementation(() => sleep(500, { sid }));
|
||||||
|
|
||||||
|
const ia = new InteractiveAuth({
|
||||||
|
matrixClient: new FakeClient(),
|
||||||
|
doRequest, stateUpdated, requestEmailToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
await ia.requestEmailToken();
|
||||||
|
expect(ia.getEmailSid()).toEqual(sid);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
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 { logger } from "../../src/logger";
|
import { logger } from "../../src/logger";
|
||||||
import { MatrixClient } from "../../src/client";
|
import { MatrixClient } from "../../src/client";
|
||||||
import { Filter } from "../../src/filter";
|
import { Filter } from "../../src/filter";
|
||||||
@@ -13,9 +29,12 @@ import {
|
|||||||
import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib";
|
import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib";
|
||||||
import { EventStatus, MatrixEvent } from "../../src/models/event";
|
import { EventStatus, MatrixEvent } from "../../src/models/event";
|
||||||
import { Preset } from "../../src/@types/partials";
|
import { Preset } from "../../src/@types/partials";
|
||||||
|
import { ReceiptType } from "../../src/@types/read_receipts";
|
||||||
import * as testUtils from "../test-utils/test-utils";
|
import * as testUtils from "../test-utils/test-utils";
|
||||||
import { makeBeaconInfoContent } from "../../src/content-helpers";
|
import { makeBeaconInfoContent } from "../../src/content-helpers";
|
||||||
import { M_BEACON_INFO } from "../../src/@types/beacon";
|
import { M_BEACON_INFO } from "../../src/@types/beacon";
|
||||||
|
import { ContentHelpers, Room } from "../../src";
|
||||||
|
import { makeBeaconEvent } from "../test-utils/beacon";
|
||||||
|
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
|
||||||
@@ -72,7 +91,12 @@ describe("MatrixClient", function() {
|
|||||||
let pendingLookup = null;
|
let pendingLookup = null;
|
||||||
function httpReq(cb, method, path, qp, data, prefix) {
|
function httpReq(cb, method, path, qp, data, prefix) {
|
||||||
if (path === KEEP_ALIVE_PATH && acceptKeepalives) {
|
if (path === KEEP_ALIVE_PATH && acceptKeepalives) {
|
||||||
return Promise.resolve();
|
return Promise.resolve({
|
||||||
|
unstable_features: {
|
||||||
|
"org.matrix.msc3440.stable": true,
|
||||||
|
},
|
||||||
|
versions: ["r0.6.0", "r0.6.1"],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const next = httpLookups.shift();
|
const next = httpLookups.shift();
|
||||||
const logLine = (
|
const logLine = (
|
||||||
@@ -793,11 +817,12 @@ describe("MatrixClient", function() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
threads: {
|
getThread: jest.fn(),
|
||||||
get: jest.fn(),
|
|
||||||
},
|
|
||||||
addPendingEvent: jest.fn(),
|
addPendingEvent: jest.fn(),
|
||||||
updatePendingEvent: jest.fn(),
|
updatePendingEvent: jest.fn(),
|
||||||
|
reEmitter: {
|
||||||
|
reEmit: jest.fn(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -941,6 +966,7 @@ describe("MatrixClient", function() {
|
|||||||
it("partitions root events to room timeline and thread timeline", () => {
|
it("partitions root events to room timeline and thread timeline", () => {
|
||||||
const supportsExperimentalThreads = client.supportsExperimentalThreads;
|
const supportsExperimentalThreads = client.supportsExperimentalThreads;
|
||||||
client.supportsExperimentalThreads = () => true;
|
client.supportsExperimentalThreads = () => true;
|
||||||
|
const room = new Room("!room1:matrix.org", client, userId);
|
||||||
|
|
||||||
const rootEvent = new MatrixEvent({
|
const rootEvent = new MatrixEvent({
|
||||||
"content": {},
|
"content": {},
|
||||||
@@ -963,15 +989,55 @@ describe("MatrixClient", function() {
|
|||||||
|
|
||||||
expect(rootEvent.isThreadRoot).toBe(true);
|
expect(rootEvent.isThreadRoot).toBe(true);
|
||||||
|
|
||||||
const [room, threads] = client.partitionThreadedEvents([rootEvent]);
|
const [roomEvents, threadEvents] = room.partitionThreadedEvents([rootEvent]);
|
||||||
expect(room).toHaveLength(1);
|
expect(roomEvents).toHaveLength(1);
|
||||||
expect(threads).toHaveLength(1);
|
expect(threadEvents).toHaveLength(1);
|
||||||
|
|
||||||
// Restore method
|
// Restore method
|
||||||
client.supportsExperimentalThreads = supportsExperimentalThreads;
|
client.supportsExperimentalThreads = supportsExperimentalThreads;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("read-markers and read-receipts", () => {
|
||||||
|
it("setRoomReadMarkers", () => {
|
||||||
|
client.setRoomReadMarkersHttpRequest = jest.fn();
|
||||||
|
const room = {
|
||||||
|
hasPendingEvent: jest.fn().mockReturnValue(false),
|
||||||
|
addLocalEchoReceipt: jest.fn(),
|
||||||
|
};
|
||||||
|
const rrEvent = new MatrixEvent({ event_id: "read_event_id" });
|
||||||
|
const rpEvent = new MatrixEvent({ event_id: "read_private_event_id" });
|
||||||
|
client.getRoom = () => room;
|
||||||
|
|
||||||
|
client.setRoomReadMarkers(
|
||||||
|
"room_id",
|
||||||
|
"read_marker_event_id",
|
||||||
|
rrEvent,
|
||||||
|
rpEvent,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(client.setRoomReadMarkersHttpRequest).toHaveBeenCalledWith(
|
||||||
|
"room_id",
|
||||||
|
"read_marker_event_id",
|
||||||
|
"read_event_id",
|
||||||
|
"read_private_event_id",
|
||||||
|
);
|
||||||
|
expect(room.addLocalEchoReceipt).toHaveBeenCalledTimes(2);
|
||||||
|
expect(room.addLocalEchoReceipt).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
client.credentials.userId,
|
||||||
|
rrEvent,
|
||||||
|
ReceiptType.Read,
|
||||||
|
);
|
||||||
|
expect(room.addLocalEchoReceipt).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
client.credentials.userId,
|
||||||
|
rpEvent,
|
||||||
|
ReceiptType.ReadPrivate,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("beacons", () => {
|
describe("beacons", () => {
|
||||||
const roomId = '!room:server.org';
|
const roomId = '!room:server.org';
|
||||||
const content = makeBeaconInfoContent(100, true);
|
const content = makeBeaconInfoContent(100, true);
|
||||||
@@ -981,10 +1047,10 @@ describe("MatrixClient", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("creates new beacon info", async () => {
|
it("creates new beacon info", async () => {
|
||||||
await client.unstable_createLiveBeacon(roomId, content, '123');
|
await client.unstable_createLiveBeacon(roomId, content);
|
||||||
|
|
||||||
// event type combined
|
// event type combined
|
||||||
const expectedEventType = `${M_BEACON_INFO.name}.${userId}.123`;
|
const expectedEventType = M_BEACON_INFO.name;
|
||||||
const [callback, method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
|
const [callback, method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
|
||||||
expect(callback).toBeFalsy();
|
expect(callback).toBeFalsy();
|
||||||
expect(method).toBe('PUT');
|
expect(method).toBe('PUT');
|
||||||
@@ -997,17 +1063,132 @@ describe("MatrixClient", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("updates beacon info with specific event type", async () => {
|
it("updates beacon info with specific event type", async () => {
|
||||||
const eventType = `${M_BEACON_INFO.name}.${userId}.456`;
|
await client.unstable_setLiveBeacon(roomId, content);
|
||||||
|
|
||||||
await client.unstable_setLiveBeacon(roomId, eventType, content);
|
|
||||||
|
|
||||||
// event type combined
|
// event type combined
|
||||||
const [, , path, , requestContent] = client.http.authedRequest.mock.calls[0];
|
const [, , path, , requestContent] = client.http.authedRequest.mock.calls[0];
|
||||||
expect(path).toEqual(
|
expect(path).toEqual(
|
||||||
`/rooms/${encodeURIComponent(roomId)}/state/` +
|
`/rooms/${encodeURIComponent(roomId)}/state/` +
|
||||||
`${encodeURIComponent(eventType)}/${encodeURIComponent(userId)}`,
|
`${encodeURIComponent(M_BEACON_INFO.name)}/${encodeURIComponent(userId)}`,
|
||||||
);
|
);
|
||||||
expect(requestContent).toEqual(content);
|
expect(requestContent).toEqual(content);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('processBeaconEvents()', () => {
|
||||||
|
it('does nothing when events is falsy', () => {
|
||||||
|
const room = new Room(roomId, client, userId);
|
||||||
|
const roomStateProcessSpy = jest.spyOn(room.currentState, 'processBeaconEvents');
|
||||||
|
|
||||||
|
client.processBeaconEvents(room, undefined);
|
||||||
|
expect(roomStateProcessSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing when events is of length 0', () => {
|
||||||
|
const room = new Room(roomId, client, userId);
|
||||||
|
const roomStateProcessSpy = jest.spyOn(room.currentState, 'processBeaconEvents');
|
||||||
|
|
||||||
|
client.processBeaconEvents(room, []);
|
||||||
|
expect(roomStateProcessSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls room states processBeaconEvents with events', () => {
|
||||||
|
const room = new Room(roomId, client, userId);
|
||||||
|
const roomStateProcessSpy = jest.spyOn(room.currentState, 'processBeaconEvents');
|
||||||
|
|
||||||
|
const messageEvent = testUtils.mkMessage({ room: roomId, user: userId, event: true });
|
||||||
|
const beaconEvent = makeBeaconEvent(userId);
|
||||||
|
|
||||||
|
client.processBeaconEvents(room, [messageEvent, beaconEvent]);
|
||||||
|
expect(roomStateProcessSpy).toHaveBeenCalledWith([messageEvent, beaconEvent], client);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("setRoomTopic", () => {
|
||||||
|
const roomId = "!foofoofoofoofoofoo:matrix.org";
|
||||||
|
const createSendStateEventMock = (topic: string, htmlTopic?: string) => {
|
||||||
|
return jest.fn()
|
||||||
|
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
|
||||||
|
expect(roomId).toEqual(roomId);
|
||||||
|
expect(eventType).toEqual(EventType.RoomTopic);
|
||||||
|
expect(content).toMatchObject(ContentHelpers.makeTopicContent(topic, htmlTopic));
|
||||||
|
expect(stateKey).toBeUndefined();
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
it("is called with plain text topic and sends state event", async () => {
|
||||||
|
const sendStateEvent = createSendStateEventMock("pizza");
|
||||||
|
client.sendStateEvent = sendStateEvent;
|
||||||
|
await client.setRoomTopic(roomId, "pizza");
|
||||||
|
expect(sendStateEvent).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is called with plain text topic and callback and sends state event", async () => {
|
||||||
|
const sendStateEvent = createSendStateEventMock("pizza");
|
||||||
|
client.sendStateEvent = sendStateEvent;
|
||||||
|
await client.setRoomTopic(roomId, "pizza", () => {});
|
||||||
|
expect(sendStateEvent).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is called with plain text and HTML topic and sends state event", async () => {
|
||||||
|
const sendStateEvent = createSendStateEventMock("pizza", "<b>pizza</b>");
|
||||||
|
client.sendStateEvent = sendStateEvent;
|
||||||
|
await client.setRoomTopic(roomId, "pizza", "<b>pizza</b>");
|
||||||
|
expect(sendStateEvent).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("setPassword", () => {
|
||||||
|
const auth = { session: 'abcdef', type: 'foo' };
|
||||||
|
const newPassword = 'newpassword';
|
||||||
|
const callback = () => {};
|
||||||
|
|
||||||
|
const passwordTest = (expectedRequestContent: any, expectedCallback?: Function) => {
|
||||||
|
const [callback, method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
|
||||||
|
if (expectedCallback) {
|
||||||
|
expect(callback).toBe(expectedCallback);
|
||||||
|
} else {
|
||||||
|
expect(callback).toBeFalsy();
|
||||||
|
}
|
||||||
|
expect(method).toBe('POST');
|
||||||
|
expect(path).toEqual('/account/password');
|
||||||
|
expect(queryParams).toBeFalsy();
|
||||||
|
expect(requestContent).toEqual(expectedRequestContent);
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
client.http.authedRequest.mockClear().mockResolvedValue({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("no logout_devices specified", async () => {
|
||||||
|
await client.setPassword(auth, newPassword);
|
||||||
|
passwordTest({ auth, new_password: newPassword });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("no logout_devices specified + callback", async () => {
|
||||||
|
await client.setPassword(auth, newPassword, callback);
|
||||||
|
passwordTest({ auth, new_password: newPassword }, callback);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("overload logoutDevices=true", async () => {
|
||||||
|
await client.setPassword(auth, newPassword, true);
|
||||||
|
passwordTest({ auth, new_password: newPassword, logout_devices: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("overload logoutDevices=true + callback", async () => {
|
||||||
|
await client.setPassword(auth, newPassword, true, callback);
|
||||||
|
passwordTest({ auth, new_password: newPassword, logout_devices: true }, callback);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("overload logoutDevices=false", async () => {
|
||||||
|
await client.setPassword(auth, newPassword, false);
|
||||||
|
passwordTest({ auth, new_password: newPassword, logout_devices: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("overload logoutDevices=false + callback", async () => {
|
||||||
|
await client.setPassword(auth, newPassword, false, callback);
|
||||||
|
passwordTest({ auth, new_password: newPassword, logout_devices: false }, callback);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,15 +14,12 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventType } from "../../../src";
|
|
||||||
import { M_BEACON_INFO } from "../../../src/@types/beacon";
|
|
||||||
import {
|
import {
|
||||||
isTimestampInDuration,
|
isTimestampInDuration,
|
||||||
isBeaconInfoEventType,
|
|
||||||
Beacon,
|
Beacon,
|
||||||
BeaconEvent,
|
BeaconEvent,
|
||||||
} from "../../../src/models/beacon";
|
} from "../../../src/models/beacon";
|
||||||
import { makeBeaconInfoEvent } from "../../test-utils/beacon";
|
import { makeBeaconEvent, makeBeaconInfoEvent } from "../../test-utils/beacon";
|
||||||
|
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
|
||||||
@@ -57,27 +54,9 @@ describe('Beacon', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isBeaconInfoEventType', () => {
|
|
||||||
it.each([
|
|
||||||
EventType.CallAnswer,
|
|
||||||
`prefix.${M_BEACON_INFO.name}`,
|
|
||||||
`prefix.${M_BEACON_INFO.altName}`,
|
|
||||||
])('returns false for %s', (type) => {
|
|
||||||
expect(isBeaconInfoEventType(type)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each([
|
|
||||||
M_BEACON_INFO.name,
|
|
||||||
M_BEACON_INFO.altName,
|
|
||||||
`${M_BEACON_INFO.name}.@test:server.org.12345`,
|
|
||||||
`${M_BEACON_INFO.altName}.@test:server.org.12345`,
|
|
||||||
])('returns true for %s', (type) => {
|
|
||||||
expect(isBeaconInfoEventType(type)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Beacon', () => {
|
describe('Beacon', () => {
|
||||||
const userId = '@user:server.org';
|
const userId = '@user:server.org';
|
||||||
|
const userId2 = '@user2:server.org';
|
||||||
const roomId = '$room:server.org';
|
const roomId = '$room:server.org';
|
||||||
// 14.03.2022 16:15
|
// 14.03.2022 16:15
|
||||||
const now = 1647270879403;
|
const now = 1647270879403;
|
||||||
@@ -88,6 +67,7 @@ describe('Beacon', () => {
|
|||||||
// without timeout of 3 hours
|
// without timeout of 3 hours
|
||||||
let liveBeaconEvent;
|
let liveBeaconEvent;
|
||||||
let notLiveBeaconEvent;
|
let notLiveBeaconEvent;
|
||||||
|
let user2BeaconEvent;
|
||||||
|
|
||||||
const advanceDateAndTime = (ms: number) => {
|
const advanceDateAndTime = (ms: number) => {
|
||||||
// bc liveness check uses Date.now we have to advance this mock
|
// bc liveness check uses Date.now we have to advance this mock
|
||||||
@@ -99,13 +79,30 @@ describe('Beacon', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// go back in time to create the beacon
|
// go back in time to create the beacon
|
||||||
jest.spyOn(global.Date, 'now').mockReturnValue(now - HOUR_MS);
|
jest.spyOn(global.Date, 'now').mockReturnValue(now - HOUR_MS);
|
||||||
liveBeaconEvent = makeBeaconInfoEvent(userId, roomId, { timeout: HOUR_MS * 3, isLive: true }, '$live123');
|
liveBeaconEvent = makeBeaconInfoEvent(
|
||||||
|
userId,
|
||||||
|
roomId,
|
||||||
|
{
|
||||||
|
timeout: HOUR_MS * 3,
|
||||||
|
isLive: true,
|
||||||
|
},
|
||||||
|
'$live123',
|
||||||
|
);
|
||||||
notLiveBeaconEvent = makeBeaconInfoEvent(
|
notLiveBeaconEvent = makeBeaconInfoEvent(
|
||||||
userId,
|
userId,
|
||||||
roomId,
|
roomId,
|
||||||
{ timeout: HOUR_MS * 3, isLive: false },
|
{ timeout: HOUR_MS * 3, isLive: false },
|
||||||
'$dead123',
|
'$dead123',
|
||||||
);
|
);
|
||||||
|
user2BeaconEvent = makeBeaconInfoEvent(
|
||||||
|
userId2,
|
||||||
|
roomId,
|
||||||
|
{
|
||||||
|
timeout: HOUR_MS * 3,
|
||||||
|
isLive: true,
|
||||||
|
},
|
||||||
|
'$user2live123',
|
||||||
|
);
|
||||||
|
|
||||||
// back to now
|
// back to now
|
||||||
jest.spyOn(global.Date, 'now').mockReturnValue(now);
|
jest.spyOn(global.Date, 'now').mockReturnValue(now);
|
||||||
@@ -123,6 +120,8 @@ describe('Beacon', () => {
|
|||||||
expect(beacon.isLive).toEqual(true);
|
expect(beacon.isLive).toEqual(true);
|
||||||
expect(beacon.beaconInfoOwner).toEqual(userId);
|
expect(beacon.beaconInfoOwner).toEqual(userId);
|
||||||
expect(beacon.beaconInfoEventType).toEqual(liveBeaconEvent.getType());
|
expect(beacon.beaconInfoEventType).toEqual(liveBeaconEvent.getType());
|
||||||
|
expect(beacon.identifier).toEqual(`${roomId}_${userId}`);
|
||||||
|
expect(beacon.beaconInfo).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isLive()', () => {
|
describe('isLive()', () => {
|
||||||
@@ -159,8 +158,27 @@ describe('Beacon', () => {
|
|||||||
|
|
||||||
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
|
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
|
||||||
|
|
||||||
expect(() => beacon.update(notLiveBeaconEvent)).toThrow();
|
expect(() => beacon.update(user2BeaconEvent)).toThrow();
|
||||||
expect(beacon.isLive).toEqual(true);
|
// didnt update
|
||||||
|
expect(beacon.identifier).toEqual(`${roomId}_${userId}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not update with an older event', () => {
|
||||||
|
const beacon = new Beacon(liveBeaconEvent);
|
||||||
|
const emitSpy = jest.spyOn(beacon, 'emit').mockClear();
|
||||||
|
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
|
||||||
|
|
||||||
|
const oldUpdateEvent = makeBeaconInfoEvent(
|
||||||
|
userId,
|
||||||
|
roomId,
|
||||||
|
);
|
||||||
|
// less than the original event
|
||||||
|
oldUpdateEvent.event.origin_server_ts = liveBeaconEvent.event.origin_server_ts - 1000;
|
||||||
|
|
||||||
|
beacon.update(oldUpdateEvent);
|
||||||
|
// didnt update
|
||||||
|
expect(emitSpy).not.toHaveBeenCalled();
|
||||||
|
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates event', () => {
|
it('updates event', () => {
|
||||||
@@ -184,7 +202,11 @@ describe('Beacon', () => {
|
|||||||
expect(beacon.isLive).toEqual(true);
|
expect(beacon.isLive).toEqual(true);
|
||||||
|
|
||||||
const updatedBeaconEvent = makeBeaconInfoEvent(
|
const updatedBeaconEvent = makeBeaconInfoEvent(
|
||||||
userId, roomId, { timeout: HOUR_MS * 3, isLive: false }, beacon.beaconInfoId);
|
userId,
|
||||||
|
roomId,
|
||||||
|
{ timeout: HOUR_MS * 3, isLive: false },
|
||||||
|
beacon.beaconInfoId,
|
||||||
|
);
|
||||||
|
|
||||||
beacon.update(updatedBeaconEvent);
|
beacon.update(updatedBeaconEvent);
|
||||||
expect(beacon.isLive).toEqual(false);
|
expect(beacon.isLive).toEqual(false);
|
||||||
@@ -223,7 +245,23 @@ describe('Beacon', () => {
|
|||||||
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LivenessChange, false, beacon);
|
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LivenessChange, false, beacon);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('destroy kills liveness monitor', () => {
|
it('clears monitor interval when re-monitoring liveness', () => {
|
||||||
|
// live beacon was created an hour ago
|
||||||
|
// and has a 3hr duration
|
||||||
|
const beacon = new Beacon(liveBeaconEvent);
|
||||||
|
expect(beacon.isLive).toBeTruthy();
|
||||||
|
|
||||||
|
beacon.monitorLiveness();
|
||||||
|
// @ts-ignore
|
||||||
|
const oldMonitor = beacon.livenessWatchInterval;
|
||||||
|
|
||||||
|
beacon.monitorLiveness();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
expect(beacon.livenessWatchInterval).not.toEqual(oldMonitor);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('destroy kills liveness monitor and emits', () => {
|
||||||
// live beacon was created an hour ago
|
// live beacon was created an hour ago
|
||||||
// and has a 3hr duration
|
// and has a 3hr duration
|
||||||
const beacon = new Beacon(liveBeaconEvent);
|
const beacon = new Beacon(liveBeaconEvent);
|
||||||
@@ -234,9 +272,101 @@ describe('Beacon', () => {
|
|||||||
|
|
||||||
// destroy the beacon
|
// destroy the beacon
|
||||||
beacon.destroy();
|
beacon.destroy();
|
||||||
|
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.Destroy, beacon.identifier);
|
||||||
|
// live forced to false
|
||||||
|
expect(beacon.isLive).toBe(false);
|
||||||
|
|
||||||
advanceDateAndTime(HOUR_MS * 2 + 1);
|
advanceDateAndTime(HOUR_MS * 2 + 1);
|
||||||
|
|
||||||
|
// no additional calls
|
||||||
|
expect(emitSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addLocations', () => {
|
||||||
|
it('ignores locations when beacon is not live', () => {
|
||||||
|
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: false }));
|
||||||
|
const emitSpy = jest.spyOn(beacon, 'emit');
|
||||||
|
|
||||||
|
beacon.addLocations([
|
||||||
|
makeBeaconEvent(userId, { beaconInfoId: beacon.beaconInfoId, timestamp: now + 1 }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(beacon.latestLocationState).toBeFalsy();
|
||||||
|
expect(emitSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores locations outside the beacon live duration', () => {
|
||||||
|
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
|
||||||
|
const emitSpy = jest.spyOn(beacon, 'emit');
|
||||||
|
|
||||||
|
beacon.addLocations([
|
||||||
|
// beacon has now + 60000 live period
|
||||||
|
makeBeaconEvent(userId, { beaconInfoId: beacon.beaconInfoId, timestamp: now + 100000 }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(beacon.latestLocationState).toBeFalsy();
|
||||||
|
expect(emitSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets latest location state to most recent location', () => {
|
||||||
|
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
|
||||||
|
const emitSpy = jest.spyOn(beacon, 'emit');
|
||||||
|
|
||||||
|
const locations = [
|
||||||
|
// older
|
||||||
|
makeBeaconEvent(
|
||||||
|
userId, { beaconInfoId: beacon.beaconInfoId, uri: 'geo:foo', timestamp: now + 1 },
|
||||||
|
),
|
||||||
|
// newer
|
||||||
|
makeBeaconEvent(
|
||||||
|
userId, { beaconInfoId: beacon.beaconInfoId, uri: 'geo:bar', timestamp: now + 10000 },
|
||||||
|
),
|
||||||
|
// not valid
|
||||||
|
makeBeaconEvent(
|
||||||
|
userId, { beaconInfoId: beacon.beaconInfoId, uri: 'geo:baz', timestamp: now - 5 },
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
beacon.addLocations(locations);
|
||||||
|
|
||||||
|
const expectedLatestLocation = {
|
||||||
|
description: undefined,
|
||||||
|
timestamp: now + 10000,
|
||||||
|
uri: 'geo:bar',
|
||||||
|
};
|
||||||
|
|
||||||
|
// the newest valid location
|
||||||
|
expect(beacon.latestLocationState).toEqual(expectedLatestLocation);
|
||||||
|
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LocationUpdate, expectedLatestLocation);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores locations that are less recent that the current latest location', () => {
|
||||||
|
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
|
||||||
|
|
||||||
|
const olderLocation = makeBeaconEvent(
|
||||||
|
userId, { beaconInfoId: beacon.beaconInfoId, uri: 'geo:foo', timestamp: now + 1 },
|
||||||
|
);
|
||||||
|
const newerLocation = makeBeaconEvent(
|
||||||
|
userId, { beaconInfoId: beacon.beaconInfoId, uri: 'geo:bar', timestamp: now + 10000 },
|
||||||
|
);
|
||||||
|
|
||||||
|
beacon.addLocations([newerLocation]);
|
||||||
|
// latest location set to newerLocation
|
||||||
|
expect(beacon.latestLocationState).toEqual(expect.objectContaining({
|
||||||
|
uri: 'geo:bar',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const emitSpy = jest.spyOn(beacon, 'emit').mockClear();
|
||||||
|
|
||||||
|
// add older location
|
||||||
|
beacon.addLocations([olderLocation]);
|
||||||
|
|
||||||
|
// no change
|
||||||
|
expect(beacon.latestLocationState).toEqual(expect.objectContaining({
|
||||||
|
uri: 'geo:bar',
|
||||||
|
}));
|
||||||
|
// no emit
|
||||||
expect(emitSpy).not.toHaveBeenCalled();
|
expect(emitSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -57,4 +57,31 @@ describe('MatrixEvent', () => {
|
|||||||
expect(a.toSnapshot().isEquivalentTo(a)).toBe(true);
|
expect(a.toSnapshot().isEquivalentTo(a)).toBe(true);
|
||||||
expect(a.toSnapshot().isEquivalentTo(b)).toBe(false);
|
expect(a.toSnapshot().isEquivalentTo(b)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should prune clearEvent when being redacted", () => {
|
||||||
|
const ev = new MatrixEvent({
|
||||||
|
type: "m.room.message",
|
||||||
|
content: {
|
||||||
|
body: "Test",
|
||||||
|
},
|
||||||
|
event_id: "$event1:server",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ev.getContent().body).toBe("Test");
|
||||||
|
expect(ev.getWireContent().body).toBe("Test");
|
||||||
|
ev.makeEncrypted("m.room.encrypted", { ciphertext: "xyz" }, "", "");
|
||||||
|
expect(ev.getContent().body).toBe("Test");
|
||||||
|
expect(ev.getWireContent().body).toBeUndefined();
|
||||||
|
expect(ev.getWireContent().ciphertext).toBe("xyz");
|
||||||
|
|
||||||
|
const redaction = new MatrixEvent({
|
||||||
|
type: "m.room.redaction",
|
||||||
|
redacts: ev.getId(),
|
||||||
|
});
|
||||||
|
|
||||||
|
ev.makeRedacted(redaction);
|
||||||
|
expect(ev.getContent().body).toBeUndefined();
|
||||||
|
expect(ev.getWireContent().body).toBeUndefined();
|
||||||
|
expect(ev.getWireContent().ciphertext).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
28
spec/unit/models/thread.spec.ts
Normal file
28
spec/unit/models/thread.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
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 { Thread } from "../../../src/models/thread";
|
||||||
|
|
||||||
|
describe('Thread', () => {
|
||||||
|
describe("constructor", () => {
|
||||||
|
it("should explode for element-web#22141 logging", () => {
|
||||||
|
// Logging/debugging for https://github.com/vector-im/element-web/issues/22141
|
||||||
|
expect(() => {
|
||||||
|
new Thread("$event", undefined, {} as any); // deliberate cast to test error case
|
||||||
|
}).toThrow("element-web#22141: A thread requires a room in order to function");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as utils from "../test-utils/test-utils";
|
import * as utils from "../test-utils/test-utils";
|
||||||
import { PushProcessor } from "../../src/pushprocessor";
|
import { PushProcessor } from "../../src/pushprocessor";
|
||||||
|
import { EventType } from "../../src";
|
||||||
|
|
||||||
describe('NotificationService', function() {
|
describe('NotificationService', function() {
|
||||||
const testUserId = "@ali:matrix.org";
|
const testUserId = "@ali:matrix.org";
|
||||||
@@ -208,6 +209,7 @@ describe('NotificationService', function() {
|
|||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
matrixClient.pushRules = PushProcessor.rewriteDefaultRules(matrixClient.pushRules);
|
||||||
pushProcessor = new PushProcessor(matrixClient);
|
pushProcessor = new PushProcessor(matrixClient);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -295,6 +297,21 @@ describe('NotificationService', function() {
|
|||||||
expect(actions.tweaks.highlight).toEqual(false);
|
expect(actions.tweaks.highlight).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not bing on room server ACL changes', function() {
|
||||||
|
testEvent = utils.mkEvent({
|
||||||
|
type: EventType.RoomServerAcl,
|
||||||
|
room: testRoomId,
|
||||||
|
user: "@alfred:localhost",
|
||||||
|
event: true,
|
||||||
|
content: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||||
|
expect(actions.tweaks.highlight).toBeFalsy();
|
||||||
|
expect(actions.tweaks.sound).toBeFalsy();
|
||||||
|
expect(actions.notify).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
// invalid
|
// invalid
|
||||||
|
|
||||||
it('should gracefully handle bad input.', function() {
|
it('should gracefully handle bad input.', function() {
|
||||||
|
|||||||
@@ -130,4 +130,51 @@ describe("Relations", function() {
|
|||||||
await relationsCreated;
|
await relationsCreated;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should ignore m.replace for state events", async () => {
|
||||||
|
const userId = "@bob:example.com";
|
||||||
|
const room = new Room("room123", null, userId);
|
||||||
|
const relations = new Relations("m.replace", "m.room.topic", room);
|
||||||
|
|
||||||
|
// Create an instance of a state event with rel_type m.replace
|
||||||
|
const originalTopic = new MatrixEvent({
|
||||||
|
"sender": userId,
|
||||||
|
"type": "m.room.topic",
|
||||||
|
"event_id": "$orig",
|
||||||
|
"room_id": room.roomId,
|
||||||
|
"content": {
|
||||||
|
"topic": "orig",
|
||||||
|
},
|
||||||
|
"state_key": "",
|
||||||
|
});
|
||||||
|
const badlyEditedTopic = new MatrixEvent({
|
||||||
|
"sender": userId,
|
||||||
|
"type": "m.room.topic",
|
||||||
|
"event_id": "$orig",
|
||||||
|
"room_id": room.roomId,
|
||||||
|
"content": {
|
||||||
|
"topic": "topic",
|
||||||
|
"m.new_content": {
|
||||||
|
"topic": "edit",
|
||||||
|
},
|
||||||
|
"m.relates_to": {
|
||||||
|
"event_id": "$orig",
|
||||||
|
"rel_type": "m.replace",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"state_key": "",
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
expect(originalTopic.getContent().topic).toBe("orig");
|
||||||
|
expect(badlyEditedTopic.replacingEvent()).toBe(null);
|
||||||
|
expect(badlyEditedTopic.getContent().topic).toBe("topic");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import * as utils from "../test-utils/test-utils";
|
import * as utils from "../test-utils/test-utils";
|
||||||
import { makeBeaconInfoEvent } from "../test-utils/beacon";
|
import { makeBeaconEvent, makeBeaconInfoEvent } from "../test-utils/beacon";
|
||||||
import { filterEmitCallsByEventType } from "../test-utils/emitter";
|
import { filterEmitCallsByEventType } from "../test-utils/emitter";
|
||||||
import { RoomState, RoomStateEvent } from "../../src/models/room-state";
|
import { RoomState, RoomStateEvent } from "../../src/models/room-state";
|
||||||
import { BeaconEvent } from "../../src/models/beacon";
|
import { BeaconEvent, getBeaconInfoIdentifier } from "../../src/models/beacon";
|
||||||
|
import { EventType, RelationType } from "../../src/@types/event";
|
||||||
|
import {
|
||||||
|
MatrixEvent,
|
||||||
|
MatrixEventEvent,
|
||||||
|
} from "../../src/models/event";
|
||||||
|
import { M_BEACON } from "../../src/@types/beacon";
|
||||||
|
|
||||||
describe("RoomState", function() {
|
describe("RoomState", function() {
|
||||||
const roomId = "!foo:bar";
|
const roomId = "!foo:bar";
|
||||||
@@ -252,6 +258,7 @@ describe("RoomState", function() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('beacon events', () => {
|
||||||
it('adds new beacon info events to state and emits', () => {
|
it('adds new beacon info events to state and emits', () => {
|
||||||
const beaconEvent = makeBeaconInfoEvent(userA, roomId);
|
const beaconEvent = makeBeaconInfoEvent(userA, roomId);
|
||||||
const emitSpy = jest.spyOn(state, 'emit');
|
const emitSpy = jest.spyOn(state, 'emit');
|
||||||
@@ -259,31 +266,64 @@ describe("RoomState", function() {
|
|||||||
state.setStateEvents([beaconEvent]);
|
state.setStateEvents([beaconEvent]);
|
||||||
|
|
||||||
expect(state.beacons.size).toEqual(1);
|
expect(state.beacons.size).toEqual(1);
|
||||||
const beaconInstance = state.beacons.get(beaconEvent.getId());
|
const beaconInstance = state.beacons.get(`${roomId}_${userA}`);
|
||||||
expect(beaconInstance).toBeTruthy();
|
expect(beaconInstance).toBeTruthy();
|
||||||
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.New, beaconEvent, beaconInstance);
|
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.New, beaconEvent, beaconInstance);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not add redacted beacon info events to state', () => {
|
||||||
|
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId);
|
||||||
|
const redactionEvent = { event: { type: 'm.room.redaction' } };
|
||||||
|
redactedBeaconEvent.makeRedacted(redactionEvent);
|
||||||
|
const emitSpy = jest.spyOn(state, 'emit');
|
||||||
|
|
||||||
|
state.setStateEvents([redactedBeaconEvent]);
|
||||||
|
|
||||||
|
// no beacon added
|
||||||
|
expect(state.beacons.size).toEqual(0);
|
||||||
|
expect(state.beacons.get(getBeaconInfoIdentifier(redactedBeaconEvent))).toBeFalsy();
|
||||||
|
// no new beacon emit
|
||||||
|
expect(filterEmitCallsByEventType(BeaconEvent.New, emitSpy).length).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
it('updates existing beacon info events in state', () => {
|
it('updates existing beacon info events in state', () => {
|
||||||
const beaconId = '$beacon1';
|
const beaconId = '$beacon1';
|
||||||
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
|
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
|
||||||
const updatedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, beaconId);
|
const updatedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, beaconId);
|
||||||
|
|
||||||
state.setStateEvents([beaconEvent]);
|
state.setStateEvents([beaconEvent]);
|
||||||
const beaconInstance = state.beacons.get(beaconId);
|
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
|
||||||
expect(beaconInstance.isLive).toEqual(true);
|
expect(beaconInstance.isLive).toEqual(true);
|
||||||
|
|
||||||
state.setStateEvents([updatedBeaconEvent]);
|
state.setStateEvents([updatedBeaconEvent]);
|
||||||
|
|
||||||
// same Beacon
|
// same Beacon
|
||||||
expect(state.beacons.get(beaconId)).toBe(beaconInstance);
|
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(beaconInstance);
|
||||||
// updated liveness
|
// updated liveness
|
||||||
expect(state.beacons.get(beaconId).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() } };
|
||||||
|
redactedBeaconEvent.makeRedacted(redactionEvent);
|
||||||
|
|
||||||
|
state.setStateEvents([beaconEvent]);
|
||||||
|
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
|
||||||
|
const destroySpy = jest.spyOn(beaconInstance, 'destroy');
|
||||||
|
expect(beaconInstance.isLive).toEqual(true);
|
||||||
|
|
||||||
|
state.setStateEvents([redactedBeaconEvent]);
|
||||||
|
|
||||||
|
expect(destroySpy).toHaveBeenCalled();
|
||||||
|
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates live beacon ids once after setting state events', () => {
|
it('updates live beacon ids once after setting state events', () => {
|
||||||
const liveBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, '$beacon1');
|
const liveBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, '$beacon1');
|
||||||
const deadBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, '$beacon2');
|
const deadBeaconEvent = makeBeaconInfoEvent(userB, roomId, { isLive: false }, '$beacon2');
|
||||||
|
|
||||||
const emitSpy = jest.spyOn(state, 'emit');
|
const emitSpy = jest.spyOn(state, 'emit');
|
||||||
|
|
||||||
@@ -294,16 +334,17 @@ describe("RoomState", function() {
|
|||||||
|
|
||||||
// live beacon is now not live
|
// live beacon is now not live
|
||||||
const updatedLiveBeaconEvent = makeBeaconInfoEvent(
|
const updatedLiveBeaconEvent = makeBeaconInfoEvent(
|
||||||
userA, roomId, { isLive: false }, liveBeaconEvent.getId(),
|
userA, roomId, { isLive: false }, liveBeaconEvent.getId(), '$beacon1',
|
||||||
);
|
);
|
||||||
|
|
||||||
state.setStateEvents([updatedLiveBeaconEvent]);
|
state.setStateEvents([updatedLiveBeaconEvent]);
|
||||||
|
|
||||||
expect(state.hasLiveBeacons).toBe(false);
|
expect(state.hasLiveBeacons).toBe(false);
|
||||||
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(2);
|
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(3);
|
||||||
expect(emitSpy).toHaveBeenCalledWith(RoomStateEvent.BeaconLiveness, state, false);
|
expect(emitSpy).toHaveBeenCalledWith(RoomStateEvent.BeaconLiveness, state, false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("setOutOfBandMembers", function() {
|
describe("setOutOfBandMembers", function() {
|
||||||
it("should add a new member", function() {
|
it("should add a new member", function() {
|
||||||
@@ -677,4 +718,243 @@ describe("RoomState", function() {
|
|||||||
expect(state.maySendEvent('m.room.other_thing', userB)).toEqual(false);
|
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 mockClient = { decryptEventIfNeeded: jest.fn() };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockClient.decryptEventIfNeeded.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing when state has no beacons', () => {
|
||||||
|
const emitSpy = jest.spyOn(state, 'emit');
|
||||||
|
state.processBeaconEvents([makeBeaconEvent(userA, { beaconInfoId: '$beacon1' })], mockClient);
|
||||||
|
expect(emitSpy).not.toHaveBeenCalled();
|
||||||
|
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing when there are no events', () => {
|
||||||
|
state.setStateEvents([beacon1, beacon2]);
|
||||||
|
const emitSpy = jest.spyOn(state, 'emit').mockClear();
|
||||||
|
state.processBeaconEvents([], mockClient);
|
||||||
|
expect(emitSpy).not.toHaveBeenCalled();
|
||||||
|
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('without encryption', () => {
|
||||||
|
it('discards events for beacons that are not in state', () => {
|
||||||
|
const location = makeBeaconEvent(userA, {
|
||||||
|
beaconInfoId: 'some-other-beacon',
|
||||||
|
});
|
||||||
|
const otherRelatedEvent = new MatrixEvent({
|
||||||
|
sender: userA,
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
content: {
|
||||||
|
['m.relates_to']: {
|
||||||
|
event_id: 'whatever',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
state.setStateEvents([beacon1, beacon2]);
|
||||||
|
const emitSpy = jest.spyOn(state, 'emit').mockClear();
|
||||||
|
state.processBeaconEvents([location, otherRelatedEvent], mockClient);
|
||||||
|
expect(emitSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('discards events that are not beacon type', () => {
|
||||||
|
// related to beacon1
|
||||||
|
const otherRelatedEvent = new MatrixEvent({
|
||||||
|
sender: userA,
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
content: {
|
||||||
|
['m.relates_to']: {
|
||||||
|
rel_type: RelationType.Reference,
|
||||||
|
event_id: beacon1.getId(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
state.setStateEvents([beacon1, beacon2]);
|
||||||
|
const emitSpy = jest.spyOn(state, 'emit').mockClear();
|
||||||
|
state.processBeaconEvents([otherRelatedEvent], mockClient);
|
||||||
|
expect(emitSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds locations to beacons', () => {
|
||||||
|
const location1 = makeBeaconEvent(userA, {
|
||||||
|
beaconInfoId: '$beacon1', timestamp: Date.now() + 1,
|
||||||
|
});
|
||||||
|
const location2 = makeBeaconEvent(userA, {
|
||||||
|
beaconInfoId: '$beacon1', timestamp: Date.now() + 2,
|
||||||
|
});
|
||||||
|
const location3 = makeBeaconEvent(userB, {
|
||||||
|
beaconInfoId: 'some-other-beacon',
|
||||||
|
});
|
||||||
|
|
||||||
|
state.setStateEvents([beacon1, beacon2], mockClient);
|
||||||
|
|
||||||
|
expect(state.beacons.size).toEqual(2);
|
||||||
|
|
||||||
|
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beacon1));
|
||||||
|
const addLocationsSpy = jest.spyOn(beaconInstance, 'addLocations');
|
||||||
|
|
||||||
|
state.processBeaconEvents([location1, location2, location3], mockClient);
|
||||||
|
|
||||||
|
expect(addLocationsSpy).toHaveBeenCalledTimes(2);
|
||||||
|
// only called with locations for beacon1
|
||||||
|
expect(addLocationsSpy).toHaveBeenCalledWith([location1]);
|
||||||
|
expect(addLocationsSpy).toHaveBeenCalledWith([location2]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with encryption', () => {
|
||||||
|
const beacon1RelationContent = { ['m.relates_to']: {
|
||||||
|
rel_type: RelationType.Reference,
|
||||||
|
event_id: beacon1.getId(),
|
||||||
|
} };
|
||||||
|
const relatedEncryptedEvent = new MatrixEvent({
|
||||||
|
sender: userA,
|
||||||
|
type: EventType.RoomMessageEncrypted,
|
||||||
|
content: beacon1RelationContent,
|
||||||
|
});
|
||||||
|
const decryptingRelatedEvent = new MatrixEvent({
|
||||||
|
sender: userA,
|
||||||
|
type: EventType.RoomMessageEncrypted,
|
||||||
|
content: beacon1RelationContent,
|
||||||
|
});
|
||||||
|
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
|
||||||
|
|
||||||
|
const failedDecryptionRelatedEvent = new MatrixEvent({
|
||||||
|
sender: userA,
|
||||||
|
type: EventType.RoomMessageEncrypted,
|
||||||
|
content: beacon1RelationContent,
|
||||||
|
});
|
||||||
|
jest.spyOn(failedDecryptionRelatedEvent, 'isDecryptionFailure').mockReturnValue(true);
|
||||||
|
|
||||||
|
it('discards events without relations', () => {
|
||||||
|
const unrelatedEvent = new MatrixEvent({
|
||||||
|
sender: userA,
|
||||||
|
type: EventType.RoomMessageEncrypted,
|
||||||
|
});
|
||||||
|
state.setStateEvents([beacon1, beacon2]);
|
||||||
|
const emitSpy = jest.spyOn(state, 'emit').mockClear();
|
||||||
|
state.processBeaconEvents([unrelatedEvent], mockClient);
|
||||||
|
expect(emitSpy).not.toHaveBeenCalled();
|
||||||
|
// discard unrelated events early
|
||||||
|
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('discards events for beacons that are not in state', () => {
|
||||||
|
const location = makeBeaconEvent(userA, {
|
||||||
|
beaconInfoId: 'some-other-beacon',
|
||||||
|
});
|
||||||
|
const otherRelatedEvent = new MatrixEvent({
|
||||||
|
sender: userA,
|
||||||
|
type: EventType.RoomMessageEncrypted,
|
||||||
|
content: {
|
||||||
|
['m.relates_to']: {
|
||||||
|
rel_type: RelationType.Reference,
|
||||||
|
event_id: 'whatever',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
state.setStateEvents([beacon1, beacon2]);
|
||||||
|
|
||||||
|
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
|
||||||
|
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
|
||||||
|
state.processBeaconEvents([location, otherRelatedEvent], mockClient);
|
||||||
|
expect(addLocationsSpy).not.toHaveBeenCalled();
|
||||||
|
// discard unrelated events early
|
||||||
|
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('decrypts related events if needed', () => {
|
||||||
|
const location = makeBeaconEvent(userA, {
|
||||||
|
beaconInfoId: beacon1.getId(),
|
||||||
|
});
|
||||||
|
state.setStateEvents([beacon1, beacon2]);
|
||||||
|
state.processBeaconEvents([location, relatedEncryptedEvent], mockClient);
|
||||||
|
// discard unrelated events early
|
||||||
|
expect(mockClient.decryptEventIfNeeded).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('listens for decryption on events that are being decrypted', () => {
|
||||||
|
const decryptingRelatedEvent = new MatrixEvent({
|
||||||
|
sender: userA,
|
||||||
|
type: EventType.RoomMessageEncrypted,
|
||||||
|
content: beacon1RelationContent,
|
||||||
|
});
|
||||||
|
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
|
||||||
|
// spy on event.once
|
||||||
|
const eventOnceSpy = jest.spyOn(decryptingRelatedEvent, 'once');
|
||||||
|
|
||||||
|
state.setStateEvents([beacon1, beacon2]);
|
||||||
|
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||||
|
|
||||||
|
// listener was added
|
||||||
|
expect(eventOnceSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('listens for decryption on events that have decryption failure', () => {
|
||||||
|
const failedDecryptionRelatedEvent = new MatrixEvent({
|
||||||
|
sender: userA,
|
||||||
|
type: EventType.RoomMessageEncrypted,
|
||||||
|
content: beacon1RelationContent,
|
||||||
|
});
|
||||||
|
jest.spyOn(failedDecryptionRelatedEvent, 'isDecryptionFailure').mockReturnValue(true);
|
||||||
|
// spy on event.once
|
||||||
|
const eventOnceSpy = jest.spyOn(decryptingRelatedEvent, 'once');
|
||||||
|
|
||||||
|
state.setStateEvents([beacon1, beacon2]);
|
||||||
|
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||||
|
|
||||||
|
// listener was added
|
||||||
|
expect(eventOnceSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('discard events that are not m.beacon type after decryption', () => {
|
||||||
|
const decryptingRelatedEvent = new MatrixEvent({
|
||||||
|
sender: userA,
|
||||||
|
type: EventType.RoomMessageEncrypted,
|
||||||
|
content: beacon1RelationContent,
|
||||||
|
});
|
||||||
|
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
|
||||||
|
state.setStateEvents([beacon1, beacon2]);
|
||||||
|
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
|
||||||
|
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);
|
||||||
|
|
||||||
|
expect(addLocationsSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds locations to beacons after decryption', () => {
|
||||||
|
const decryptingRelatedEvent = new MatrixEvent({
|
||||||
|
sender: userA,
|
||||||
|
type: EventType.RoomMessageEncrypted,
|
||||||
|
content: beacon1RelationContent,
|
||||||
|
});
|
||||||
|
const locationEvent = makeBeaconEvent(userA, {
|
||||||
|
beaconInfoId: '$beacon1', timestamp: Date.now() + 1,
|
||||||
|
});
|
||||||
|
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
|
||||||
|
state.setStateEvents([beacon1, beacon2]);
|
||||||
|
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
|
||||||
|
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);
|
||||||
|
|
||||||
|
expect(addLocationsSpy).toHaveBeenCalledWith([decryptingRelatedEvent]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { ReceiptType } from "../../src/@types/read_receipts";
|
||||||
import { SyncAccumulator } from "../../src/sync-accumulator";
|
import { SyncAccumulator } from "../../src/sync-accumulator";
|
||||||
|
|
||||||
// The event body & unsigned object get frozen to assert that they don't get altered
|
// The event body & unsigned object get frozen to assert that they don't get altered
|
||||||
@@ -294,10 +295,13 @@ describe("SyncAccumulator", function() {
|
|||||||
room_id: "!foo:bar",
|
room_id: "!foo:bar",
|
||||||
content: {
|
content: {
|
||||||
"$event1:localhost": {
|
"$event1:localhost": {
|
||||||
"m.read": {
|
[ReceiptType.Read]: {
|
||||||
"@alice:localhost": { ts: 1 },
|
"@alice:localhost": { ts: 1 },
|
||||||
"@bob:localhost": { ts: 2 },
|
"@bob:localhost": { ts: 2 },
|
||||||
},
|
},
|
||||||
|
[ReceiptType.ReadPrivate]: {
|
||||||
|
"@dan:localhost": { ts: 4 },
|
||||||
|
},
|
||||||
"some.other.receipt.type": {
|
"some.other.receipt.type": {
|
||||||
"@should_be_ignored:localhost": { key: "val" },
|
"@should_be_ignored:localhost": { key: "val" },
|
||||||
},
|
},
|
||||||
@@ -309,7 +313,7 @@ describe("SyncAccumulator", function() {
|
|||||||
room_id: "!foo:bar",
|
room_id: "!foo:bar",
|
||||||
content: {
|
content: {
|
||||||
"$event2:localhost": {
|
"$event2:localhost": {
|
||||||
"m.read": {
|
[ReceiptType.Read]: {
|
||||||
"@bob:localhost": { ts: 2 }, // clobbers event1 receipt
|
"@bob:localhost": { ts: 2 }, // clobbers event1 receipt
|
||||||
"@charlie:localhost": { ts: 3 },
|
"@charlie:localhost": { ts: 3 },
|
||||||
},
|
},
|
||||||
@@ -337,12 +341,15 @@ describe("SyncAccumulator", function() {
|
|||||||
room_id: "!foo:bar",
|
room_id: "!foo:bar",
|
||||||
content: {
|
content: {
|
||||||
"$event1:localhost": {
|
"$event1:localhost": {
|
||||||
"m.read": {
|
[ReceiptType.Read]: {
|
||||||
"@alice:localhost": { ts: 1 },
|
"@alice:localhost": { ts: 1 },
|
||||||
},
|
},
|
||||||
|
[ReceiptType.ReadPrivate]: {
|
||||||
|
"@dan:localhost": { ts: 4 },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"$event2:localhost": {
|
"$event2:localhost": {
|
||||||
"m.read": {
|
[ReceiptType.Read]: {
|
||||||
"@bob:localhost": { ts: 2 },
|
"@bob:localhost": { ts: 2 },
|
||||||
"@charlie:localhost": { ts: 3 },
|
"@charlie:localhost": { ts: 3 },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,8 +10,11 @@ import {
|
|||||||
prevString,
|
prevString,
|
||||||
simpleRetryOperation,
|
simpleRetryOperation,
|
||||||
stringToBase,
|
stringToBase,
|
||||||
|
sortEventsByLatestContentTimestamp,
|
||||||
} from "../../src/utils";
|
} from "../../src/utils";
|
||||||
import { logger } from "../../src/logger";
|
import { logger } from "../../src/logger";
|
||||||
|
import { mkMessage } from "../test-utils/test-utils";
|
||||||
|
import { makeBeaconEvent } from "../test-utils/beacon";
|
||||||
|
|
||||||
// TODO: Fix types throughout
|
// TODO: Fix types throughout
|
||||||
|
|
||||||
@@ -506,4 +509,30 @@ describe("utils", function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('sortEventsByLatestContentTimestamp', () => {
|
||||||
|
const roomId = '!room:server';
|
||||||
|
const userId = '@user:server';
|
||||||
|
const eventWithoutContentTimestamp = mkMessage({ room: roomId, user: userId, event: true });
|
||||||
|
// m.beacon events have timestamp in content
|
||||||
|
const beaconEvent1 = makeBeaconEvent(userId, { timestamp: 1648804528557 });
|
||||||
|
const beaconEvent2 = makeBeaconEvent(userId, { timestamp: 1648804528558 });
|
||||||
|
const beaconEvent3 = makeBeaconEvent(userId, { timestamp: 1648804528000 });
|
||||||
|
const beaconEvent4 = makeBeaconEvent(userId, { timestamp: 0 });
|
||||||
|
|
||||||
|
it('sorts events with timestamps as later than events without', () => {
|
||||||
|
expect(
|
||||||
|
[beaconEvent4, eventWithoutContentTimestamp, beaconEvent1]
|
||||||
|
.sort(utils.sortEventsByLatestContentTimestamp),
|
||||||
|
).toEqual([
|
||||||
|
beaconEvent1, beaconEvent4, eventWithoutContentTimestamp,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sorts by content timestamps correctly', () => {
|
||||||
|
expect(
|
||||||
|
[beaconEvent1, beaconEvent2, beaconEvent3].sort(sortEventsByLatestContentTimestamp),
|
||||||
|
).toEqual([beaconEvent2, beaconEvent1, beaconEvent3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { TestClient } from '../../TestClient';
|
import { TestClient } from '../../TestClient';
|
||||||
import { MatrixCall, CallErrorCode, CallEvent } from '../../../src/webrtc/call';
|
import { MatrixCall, CallErrorCode, CallEvent, supportsMatrixCall } from '../../../src/webrtc/call';
|
||||||
import { SDPStreamMetadataKey, SDPStreamMetadataPurpose } from '../../../src/webrtc/callEventTypes';
|
import { SDPStreamMetadataKey, SDPStreamMetadataPurpose } from '../../../src/webrtc/callEventTypes';
|
||||||
import { RoomMember } from "../../../src";
|
import { RoomMember } from "../../../src";
|
||||||
|
|
||||||
@@ -505,4 +505,40 @@ describe('Call', function() {
|
|||||||
return sender?.track?.kind === "video";
|
return sender?.track?.kind === "video";
|
||||||
}).track.id).toBe("video_track");
|
}).track.id).toBe("video_track");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("supportsMatrixCall", () => {
|
||||||
|
it("should return true when the environment is right", () => {
|
||||||
|
expect(supportsMatrixCall()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if window or document are undefined", () => {
|
||||||
|
global.window = undefined;
|
||||||
|
expect(supportsMatrixCall()).toBe(false);
|
||||||
|
global.window = prevWindow;
|
||||||
|
global.document = undefined;
|
||||||
|
expect(supportsMatrixCall()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if RTCPeerConnection throws", () => {
|
||||||
|
// @ts-ignore - writing to window as we are simulating browser edge-cases
|
||||||
|
global.window = {};
|
||||||
|
Object.defineProperty(global.window, "RTCPeerConnection", {
|
||||||
|
get: () => {
|
||||||
|
throw Error("Secure mode, naaah!");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(supportsMatrixCall()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if RTCPeerConnection & RTCSessionDescription " +
|
||||||
|
"& RTCIceCandidate & mediaDevices are unavailable",
|
||||||
|
() => {
|
||||||
|
global.window.RTCPeerConnection = undefined;
|
||||||
|
global.window.RTCSessionDescription = undefined;
|
||||||
|
global.window.RTCIceCandidate = undefined;
|
||||||
|
// @ts-ignore - writing to a read-only property as we are simulating faulty browsers
|
||||||
|
global.navigator.mediaDevices = undefined;
|
||||||
|
expect(supportsMatrixCall()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
54
spec/unit/webrtc/callEventHandler.spec.ts
Normal file
54
spec/unit/webrtc/callEventHandler.spec.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
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 { TestClient } from '../../TestClient';
|
||||||
|
import { ClientEvent, EventType, MatrixEvent, RoomEvent } from "../../../src";
|
||||||
|
import { CallEventHandler, CallEventHandlerEvent } from "../../../src/webrtc/callEventHandler";
|
||||||
|
import { SyncState } from "../../../src/sync";
|
||||||
|
|
||||||
|
describe("callEventHandler", () => {
|
||||||
|
it("should ignore a call if invite & hangup come within a single sync", () => {
|
||||||
|
const testClient = new TestClient();
|
||||||
|
const client = testClient.client;
|
||||||
|
client.callEventHandler = new CallEventHandler(client);
|
||||||
|
client.callEventHandler.start();
|
||||||
|
|
||||||
|
// Fire off call invite then hangup within a single sync
|
||||||
|
const callInvite = new MatrixEvent({
|
||||||
|
type: EventType.CallInvite,
|
||||||
|
content: {
|
||||||
|
call_id: "123",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
client.emit(RoomEvent.Timeline, callInvite);
|
||||||
|
|
||||||
|
const callHangup = new MatrixEvent({
|
||||||
|
type: EventType.CallHangup,
|
||||||
|
content: {
|
||||||
|
call_id: "123",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
client.emit(RoomEvent.Timeline, callHangup);
|
||||||
|
|
||||||
|
const incomingCallEmitted = jest.fn();
|
||||||
|
client.on(CallEventHandlerEvent.Incoming, incomingCallEmitted);
|
||||||
|
|
||||||
|
client.getSyncState = jest.fn().mockReturnValue(SyncState.Syncing);
|
||||||
|
client.emit(ClientEvent.Sync);
|
||||||
|
|
||||||
|
expect(incomingCallEmitted).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EitherAnd, RELATES_TO_RELATIONSHIP, REFERENCE_RELATION } from "matrix-events-sdk";
|
import { RELATES_TO_RELATIONSHIP, REFERENCE_RELATION } from "matrix-events-sdk";
|
||||||
|
|
||||||
import { UnstableValue } from "../NamespacedValue";
|
import { UnstableValue } from "../NamespacedValue";
|
||||||
import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location";
|
import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Beacon info and beacon event types as described in MSC3489
|
* Beacon info and beacon event types as described in MSC3672
|
||||||
* https://github.com/matrix-org/matrix-spec-proposals/pull/3489
|
* https://github.com/matrix-org/matrix-spec-proposals/pull/3672
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,16 +60,11 @@ import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location";
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* Variable event type for m.beacon_info
|
|
||||||
*/
|
|
||||||
export const M_BEACON_INFO_VARIABLE = new UnstableValue("m.beacon_info.*", "org.matrix.msc3489.beacon_info.*");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Non-variable type for m.beacon_info event content
|
* Non-variable type for m.beacon_info event content
|
||||||
*/
|
*/
|
||||||
export const M_BEACON_INFO = new UnstableValue("m.beacon_info", "org.matrix.msc3489.beacon_info");
|
export const M_BEACON_INFO = new UnstableValue("m.beacon_info", "org.matrix.msc3672.beacon_info");
|
||||||
export const M_BEACON = new UnstableValue("m.beacon", "org.matrix.msc3489.beacon");
|
export const M_BEACON = new UnstableValue("m.beacon", "org.matrix.msc3672.beacon");
|
||||||
|
|
||||||
export type MBeaconInfoContent = {
|
export type MBeaconInfoContent = {
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -80,16 +75,11 @@ export type MBeaconInfoContent = {
|
|||||||
live?: boolean;
|
live?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MBeaconInfoEvent = EitherAnd<
|
|
||||||
{ [M_BEACON_INFO.name]: MBeaconInfoContent },
|
|
||||||
{ [M_BEACON_INFO.altName]: MBeaconInfoContent }
|
|
||||||
>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* m.beacon_info Event example from the spec
|
* m.beacon_info Event example from the spec
|
||||||
* https://github.com/matrix-org/matrix-spec-proposals/pull/3489
|
* https://github.com/matrix-org/matrix-spec-proposals/pull/3672
|
||||||
* {
|
* {
|
||||||
"type": "m.beacon_info.@matthew:matrix.org.1",
|
"type": "m.beacon_info",
|
||||||
"state_key": "@matthew:matrix.org",
|
"state_key": "@matthew:matrix.org",
|
||||||
"content": {
|
"content": {
|
||||||
"m.beacon_info": {
|
"m.beacon_info": {
|
||||||
@@ -108,7 +98,7 @@ export type MBeaconInfoEvent = EitherAnd<
|
|||||||
* m.beacon_info.* event content
|
* m.beacon_info.* event content
|
||||||
*/
|
*/
|
||||||
export type MBeaconInfoEventContent = &
|
export type MBeaconInfoEventContent = &
|
||||||
MBeaconInfoEvent &
|
MBeaconInfoContent &
|
||||||
// creation timestamp of the beacon on the client
|
// creation timestamp of the beacon on the client
|
||||||
MTimestampEvent &
|
MTimestampEvent &
|
||||||
// the type of asset being tracked as per MSC3488
|
// the type of asset being tracked as per MSC3488
|
||||||
@@ -116,7 +106,7 @@ export type MBeaconInfoEventContent = &
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* m.beacon event example
|
* m.beacon event example
|
||||||
* https://github.com/matrix-org/matrix-spec-proposals/pull/3489
|
* https://github.com/matrix-org/matrix-spec-proposals/pull/3672
|
||||||
*
|
*
|
||||||
* {
|
* {
|
||||||
"type": "m.beacon",
|
"type": "m.beacon",
|
||||||
|
|||||||
@@ -93,14 +93,7 @@ export enum RelationType {
|
|||||||
Annotation = "m.annotation",
|
Annotation = "m.annotation",
|
||||||
Replace = "m.replace",
|
Replace = "m.replace",
|
||||||
Reference = "m.reference",
|
Reference = "m.reference",
|
||||||
/**
|
Thread = "m.thread",
|
||||||
* Note, "io.element.thread" is hardcoded
|
|
||||||
* Should be replaced with "m.thread" once MSC3440 lands
|
|
||||||
* Can not use `UnstableValue` as TypeScript does not
|
|
||||||
* allow computed values in enums
|
|
||||||
* https://github.com/microsoft/TypeScript/issues/27976
|
|
||||||
*/
|
|
||||||
Thread = "io.element.thread",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MsgType {
|
export enum MsgType {
|
||||||
@@ -119,6 +112,8 @@ export const RoomCreateTypeField = "type";
|
|||||||
|
|
||||||
export enum RoomType {
|
export enum RoomType {
|
||||||
Space = "m.space",
|
Space = "m.space",
|
||||||
|
UnstableCall = "org.matrix.msc3417.call",
|
||||||
|
ElementVideo = "io.element.video",
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
1
src/@types/global.d.ts
vendored
1
src/@types/global.d.ts
vendored
@@ -23,6 +23,7 @@ declare global {
|
|||||||
// use `number` as the return type in all cases for global.set{Interval,Timeout},
|
// use `number` as the return type in all cases for global.set{Interval,Timeout},
|
||||||
// so we don't accidentally use the methods on NodeJS.Timeout - they only exist in a subset of environments.
|
// so we don't accidentally use the methods on NodeJS.Timeout - they only exist in a subset of environments.
|
||||||
// The overload for clear{Interval,Timeout} is resolved as expected.
|
// The overload for clear{Interval,Timeout} is resolved as expected.
|
||||||
|
// We use `ReturnType<typeof setTimeout>` in the code to be agnostic of if this definition gets loaded.
|
||||||
function setInterval(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
|
function setInterval(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
|
||||||
function setTimeout(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
|
function setTimeout(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
|
||||||
|
|
||||||
|
|||||||
21
src/@types/read_receipts.ts
Normal file
21
src/@types/read_receipts.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum ReceiptType {
|
||||||
|
Read = "m.read",
|
||||||
|
FullyRead = "m.fully_read",
|
||||||
|
ReadPrivate = "org.matrix.msc2285.read.private"
|
||||||
|
}
|
||||||
@@ -17,9 +17,11 @@ limitations under the License.
|
|||||||
import { Callback } from "../client";
|
import { Callback } from "../client";
|
||||||
import { IContent, IEvent } from "../models/event";
|
import { IContent, IEvent } from "../models/event";
|
||||||
import { Preset, Visibility } from "./partials";
|
import { Preset, Visibility } from "./partials";
|
||||||
import { SearchKey } from "./search";
|
import { IEventWithRoomId, SearchKey } from "./search";
|
||||||
import { IRoomEventFilter } from "../filter";
|
import { IRoomEventFilter } from "../filter";
|
||||||
import { Direction } from "../models/event-timeline";
|
import { Direction } from "../models/event-timeline";
|
||||||
|
import { PushRuleAction } from "./PushRules";
|
||||||
|
import { IRoomEvent } from "../sync-accumulator";
|
||||||
|
|
||||||
// allow camelcase as these are things that go onto the wire
|
// allow camelcase as these are things that go onto the wire
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
@@ -155,4 +157,50 @@ export interface IRelationsResponse {
|
|||||||
prev_batch?: string;
|
prev_batch?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IContextResponse {
|
||||||
|
end: string;
|
||||||
|
start: string;
|
||||||
|
state: IEventWithRoomId[];
|
||||||
|
events_before: IEventWithRoomId[];
|
||||||
|
events_after: IEventWithRoomId[];
|
||||||
|
event: IEventWithRoomId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEventsResponse {
|
||||||
|
chunk: IEventWithRoomId[];
|
||||||
|
end: string;
|
||||||
|
start: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INotification {
|
||||||
|
actions: PushRuleAction[];
|
||||||
|
event: IRoomEvent;
|
||||||
|
profile_tag?: string;
|
||||||
|
read: boolean;
|
||||||
|
room_id: string;
|
||||||
|
ts: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INotificationsResponse {
|
||||||
|
next_token: string;
|
||||||
|
notifications: INotification[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFilterResponse {
|
||||||
|
filter_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITagsResponse {
|
||||||
|
tags: {
|
||||||
|
[tagId: string]: {
|
||||||
|
order: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IStatusResponse extends IPresenceOpts {
|
||||||
|
currently_active?: boolean;
|
||||||
|
last_active_ago?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
|
|||||||
62
src/@types/topic.ts
Normal file
62
src/@types/topic.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
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 { EitherAnd, IMessageRendering } from "matrix-events-sdk";
|
||||||
|
|
||||||
|
import { UnstableValue } from "../NamespacedValue";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extensible topic event type based on MSC3765
|
||||||
|
* https://github.com/matrix-org/matrix-spec-proposals/pull/3765
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eg
|
||||||
|
* {
|
||||||
|
* "type": "m.room.topic,
|
||||||
|
* "state_key": "",
|
||||||
|
* "content": {
|
||||||
|
* "topic": "All about **pizza**",
|
||||||
|
* "m.topic": [{
|
||||||
|
* "body": "All about **pizza**",
|
||||||
|
* "mimetype": "text/plain",
|
||||||
|
* }, {
|
||||||
|
* "body": "All about <b>pizza</b>",
|
||||||
|
* "mimetype": "text/html",
|
||||||
|
* }],
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The event type for an m.topic event (in content)
|
||||||
|
*/
|
||||||
|
export const M_TOPIC = new UnstableValue("m.topic", "org.matrix.msc3765.topic");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The event content for an m.topic event (in content)
|
||||||
|
*/
|
||||||
|
export type MTopicContent = IMessageRendering[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The event definition for an m.topic event (in content)
|
||||||
|
*/
|
||||||
|
export type MTopicEvent = EitherAnd<{ [M_TOPIC.name]: MTopicContent }, { [M_TOPIC.altName]: MTopicContent }>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The event content for an m.room.topic event
|
||||||
|
*/
|
||||||
|
export type MRoomTopicEventContent = { topic: string } & MTopicEvent;
|
||||||
@@ -17,8 +17,6 @@ limitations under the License.
|
|||||||
|
|
||||||
/** @module auto-discovery */
|
/** @module auto-discovery */
|
||||||
|
|
||||||
import { URL as NodeURL } from "url";
|
|
||||||
|
|
||||||
import { IClientWellKnown, IWellKnownConfig } from "./client";
|
import { IClientWellKnown, IWellKnownConfig } from "./client";
|
||||||
import { logger } from './logger';
|
import { logger } from './logger';
|
||||||
|
|
||||||
@@ -249,8 +247,7 @@ export class AutoDiscovery {
|
|||||||
|
|
||||||
// Step 7: Copy any other keys directly into the clientConfig. This is for
|
// Step 7: Copy any other keys directly into the clientConfig. This is for
|
||||||
// things like custom configuration of services.
|
// things like custom configuration of services.
|
||||||
Object.keys(wellknown)
|
Object.keys(wellknown).forEach((k) => {
|
||||||
.map((k) => {
|
|
||||||
if (k === "m.homeserver" || k === "m.identity_server") {
|
if (k === "m.homeserver" || k === "m.identity_server") {
|
||||||
// Only copy selected parts of the config to avoid overwriting
|
// Only copy selected parts of the config to avoid overwriting
|
||||||
// properties computed by the validation logic above.
|
// properties computed by the validation logic above.
|
||||||
@@ -373,16 +370,11 @@ export class AutoDiscovery {
|
|||||||
if (!url) return false;
|
if (!url) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// We have to try and parse the URL using the NodeJS URL
|
|
||||||
// library if we're on NodeJS and use the browser's URL
|
|
||||||
// library when we're in a browser. To accomplish this, we
|
|
||||||
// try the NodeJS version first and fall back to the browser.
|
|
||||||
let parsed = null;
|
let parsed = null;
|
||||||
try {
|
try {
|
||||||
if (NodeURL) parsed = new NodeURL(url);
|
|
||||||
else parsed = new URL(url);
|
|
||||||
} catch (e) {
|
|
||||||
parsed = new URL(url);
|
parsed = new URL(url);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("Could not parse url", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parsed || !parsed.hostname) return false;
|
if (!parsed || !parsed.hostname) return false;
|
||||||
@@ -411,14 +403,14 @@ export class AutoDiscovery {
|
|||||||
* the following properties:
|
* the following properties:
|
||||||
* raw: The JSON object returned by the server.
|
* raw: The JSON object returned by the server.
|
||||||
* action: One of SUCCESS, IGNORE, or FAIL_PROMPT.
|
* action: One of SUCCESS, IGNORE, or FAIL_PROMPT.
|
||||||
* reason: Relatively human readable description of what went wrong.
|
* reason: Relatively human-readable description of what went wrong.
|
||||||
* error: The actual Error, if one exists.
|
* error: The actual Error, if one exists.
|
||||||
* @param {string} url The URL to fetch a JSON object from.
|
* @param {string} url The URL to fetch a JSON object from.
|
||||||
* @return {Promise<object>} Resolves to the returned state.
|
* @return {Promise<object>} Resolves to the returned state.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private static async fetchWellKnownObject(url: string): Promise<IWellKnownConfig> {
|
private static fetchWellKnownObject(url: string): Promise<IWellKnownConfig> {
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve) {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const request = require("./matrix").getRequest();
|
const request = require("./matrix").getRequest();
|
||||||
if (!request) throw new Error("No request library available");
|
if (!request) throw new Error("No request library available");
|
||||||
|
|||||||
1166
src/client.ts
1166
src/client.ts
File diff suppressed because it is too large
Load Diff
@@ -16,9 +16,9 @@ limitations under the License.
|
|||||||
|
|
||||||
/** @module ContentHelpers */
|
/** @module ContentHelpers */
|
||||||
|
|
||||||
import { REFERENCE_RELATION } from "matrix-events-sdk";
|
import { isProvided, REFERENCE_RELATION } from "matrix-events-sdk";
|
||||||
|
|
||||||
import { MBeaconEventContent, MBeaconInfoContent, MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
|
import { MBeaconEventContent, MBeaconInfoContent, MBeaconInfoEventContent } from "./@types/beacon";
|
||||||
import { MsgType } from "./@types/event";
|
import { MsgType } from "./@types/event";
|
||||||
import { TEXT_NODE_TYPE } from "./@types/extensible_events";
|
import { TEXT_NODE_TYPE } from "./@types/extensible_events";
|
||||||
import {
|
import {
|
||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
MAssetContent,
|
MAssetContent,
|
||||||
LegacyLocationEventContent,
|
LegacyLocationEventContent,
|
||||||
} from "./@types/location";
|
} from "./@types/location";
|
||||||
|
import { MRoomTopicEventContent, MTopicContent, M_TOPIC } from "./@types/topic";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the content for a HTML Message event
|
* Generates the content for a HTML Message event
|
||||||
@@ -190,6 +191,34 @@ export const parseLocationEvent = (wireEventContent: LocationEventWireContent):
|
|||||||
return makeLocationContent(fallbackText, geoUri, timestamp, description, assetType);
|
return makeLocationContent(fallbackText, geoUri, timestamp, description, assetType);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Topic event helpers
|
||||||
|
*/
|
||||||
|
export type MakeTopicContent = (
|
||||||
|
topic: string,
|
||||||
|
htmlTopic?: string,
|
||||||
|
) => MRoomTopicEventContent;
|
||||||
|
|
||||||
|
export const makeTopicContent: MakeTopicContent = (topic, htmlTopic) => {
|
||||||
|
const renderings = [{ body: topic, mimetype: "text/plain" }];
|
||||||
|
if (isProvided(htmlTopic)) {
|
||||||
|
renderings.push({ body: htmlTopic, mimetype: "text/html" });
|
||||||
|
}
|
||||||
|
return { topic, [M_TOPIC.name]: renderings };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TopicState = {
|
||||||
|
text: string;
|
||||||
|
html?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseTopicContent = (content: MRoomTopicEventContent): TopicState => {
|
||||||
|
const mtopic = M_TOPIC.findIn<MTopicContent>(content);
|
||||||
|
const text = mtopic?.find(r => !isProvided(r.mimetype) || r.mimetype === "text/plain")?.body ?? content.topic;
|
||||||
|
const html = mtopic?.find(r => r.mimetype === "text/html")?.body;
|
||||||
|
return { text, html };
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Beacon event helpers
|
* Beacon event helpers
|
||||||
*/
|
*/
|
||||||
@@ -208,11 +237,9 @@ export const makeBeaconInfoContent: MakeBeaconInfoContent = (
|
|||||||
assetType,
|
assetType,
|
||||||
timestamp,
|
timestamp,
|
||||||
) => ({
|
) => ({
|
||||||
[M_BEACON_INFO.name]: {
|
|
||||||
description,
|
description,
|
||||||
timeout,
|
timeout,
|
||||||
live: isLive,
|
live: isLive,
|
||||||
},
|
|
||||||
[M_TIMESTAMP.name]: timestamp || Date.now(),
|
[M_TIMESTAMP.name]: timestamp || Date.now(),
|
||||||
[M_ASSET.name]: {
|
[M_ASSET.name]: {
|
||||||
type: assetType ?? LocationAssetType.Self,
|
type: assetType ?? LocationAssetType.Self,
|
||||||
@@ -227,7 +254,7 @@ export type BeaconInfoState = MBeaconInfoContent & {
|
|||||||
* Flatten beacon info event content
|
* Flatten beacon info event content
|
||||||
*/
|
*/
|
||||||
export const parseBeaconInfoContent = (content: MBeaconInfoEventContent): BeaconInfoState => {
|
export const parseBeaconInfoContent = (content: MBeaconInfoEventContent): BeaconInfoState => {
|
||||||
const { description, timeout, live } = M_BEACON_INFO.findIn<MBeaconInfoContent>(content);
|
const { description, timeout, live } = content;
|
||||||
const { type: assetType } = M_ASSET.findIn<MAssetContent>(content);
|
const { type: assetType } = M_ASSET.findIn<MAssetContent>(content);
|
||||||
const timestamp = M_TIMESTAMP.findIn<number>(content);
|
const timestamp = M_TIMESTAMP.findIn<number>(content);
|
||||||
|
|
||||||
@@ -243,14 +270,14 @@ export const parseBeaconInfoContent = (content: MBeaconInfoEventContent): Beacon
|
|||||||
export type MakeBeaconContent = (
|
export type MakeBeaconContent = (
|
||||||
uri: string,
|
uri: string,
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
beaconInfoId: string,
|
beaconInfoEventId: string,
|
||||||
description?: string,
|
description?: string,
|
||||||
) => MBeaconEventContent;
|
) => MBeaconEventContent;
|
||||||
|
|
||||||
export const makeBeaconContent: MakeBeaconContent = (
|
export const makeBeaconContent: MakeBeaconContent = (
|
||||||
uri,
|
uri,
|
||||||
timestamp,
|
timestamp,
|
||||||
beaconInfoId,
|
beaconInfoEventId,
|
||||||
description,
|
description,
|
||||||
) => ({
|
) => ({
|
||||||
[M_LOCATION.name]: {
|
[M_LOCATION.name]: {
|
||||||
@@ -260,6 +287,21 @@ export const makeBeaconContent: MakeBeaconContent = (
|
|||||||
[M_TIMESTAMP.name]: timestamp,
|
[M_TIMESTAMP.name]: timestamp,
|
||||||
"m.relates_to": {
|
"m.relates_to": {
|
||||||
rel_type: REFERENCE_RELATION.name,
|
rel_type: REFERENCE_RELATION.name,
|
||||||
event_id: beaconInfoId,
|
event_id: beaconInfoEventId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type BeaconLocationState = MLocationContent & {
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseBeaconContent = (content: MBeaconEventContent): BeaconLocationState => {
|
||||||
|
const { description, uri } = M_LOCATION.findIn<MLocationContent>(content);
|
||||||
|
const timestamp = M_TIMESTAMP.findIn<number>(content);
|
||||||
|
|
||||||
|
return {
|
||||||
|
description,
|
||||||
|
uri,
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -73,8 +73,8 @@ export function getHttpUriForMxc(
|
|||||||
const fragmentOffset = serverAndMediaId.indexOf("#");
|
const fragmentOffset = serverAndMediaId.indexOf("#");
|
||||||
let fragment = "";
|
let fragment = "";
|
||||||
if (fragmentOffset >= 0) {
|
if (fragmentOffset >= 0) {
|
||||||
fragment = serverAndMediaId.substr(fragmentOffset);
|
fragment = serverAndMediaId.slice(fragmentOffset);
|
||||||
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
|
serverAndMediaId = serverAndMediaId.slice(0, fragmentOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlParams = (Object.keys(params).length === 0 ? "" : ("?" + utils.encodeParams(params)));
|
const urlParams = (Object.keys(params).length === 0 ? "" : ("?" + utils.encodeParams(params)));
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ export class CrossSigningInfo {
|
|||||||
*/
|
*/
|
||||||
public async isStoredInSecretStorage(secretStorage: SecretStorage): Promise<Record<string, object>> {
|
public async isStoredInSecretStorage(secretStorage: SecretStorage): Promise<Record<string, object>> {
|
||||||
// check what SSSS keys have encrypted the master key (if any)
|
// check what SSSS keys have encrypted the master key (if any)
|
||||||
const stored = await secretStorage.isStored("m.cross_signing.master", false) || {};
|
const stored = await secretStorage.isStored("m.cross_signing.master") || {};
|
||||||
// then check which of those SSSS keys have also encrypted the SSK and USK
|
// then check which of those SSSS keys have also encrypted the SSK and USK
|
||||||
function intersect(s: Record<string, ISecretStorageKeyInfo>) {
|
function intersect(s: Record<string, ISecretStorageKeyInfo>) {
|
||||||
for (const k of Object.keys(stored)) {
|
for (const k of Object.keys(stored)) {
|
||||||
@@ -181,7 +181,7 @@ export class CrossSigningInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const type of ["self_signing", "user_signing"]) {
|
for (const type of ["self_signing", "user_signing"]) {
|
||||||
intersect(await secretStorage.isStored(`m.cross_signing.${type}`, false) || {});
|
intersect(await secretStorage.isStored(`m.cross_signing.${type}`) || {});
|
||||||
}
|
}
|
||||||
return Object.keys(stored).length ? stored : null;
|
return Object.keys(stored).length ? stored : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
|
|||||||
// The time the save is scheduled for
|
// The time the save is scheduled for
|
||||||
private savePromiseTime: number = null;
|
private savePromiseTime: number = null;
|
||||||
// The timer used to delay the save
|
// The timer used to delay the save
|
||||||
private saveTimer: number = null;
|
private saveTimer: ReturnType<typeof setTimeout> = null;
|
||||||
// True if we have fetched data from the server or loaded a non-empty
|
// True if we have fetched data from the server or loaded a non-empty
|
||||||
// set of device data from the store
|
// set of device data from the store
|
||||||
private hasFetched: boolean = null;
|
private hasFetched: boolean = null;
|
||||||
@@ -122,7 +122,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
|
|||||||
'readonly', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
|
'readonly', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
|
||||||
this.cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
|
this.cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
|
||||||
this.hasFetched = Boolean(deviceData && deviceData.devices);
|
this.hasFetched = Boolean(deviceData && deviceData.devices);
|
||||||
this.devices = deviceData ? deviceData.devices : {},
|
this.devices = deviceData ? deviceData.devices : {};
|
||||||
this.crossSigningInfo = deviceData ?
|
this.crossSigningInfo = deviceData ?
|
||||||
deviceData.crossSigningInfo || {} : {};
|
deviceData.crossSigningInfo || {} : {};
|
||||||
this.deviceTrackingStatus = deviceData ?
|
this.deviceTrackingStatus = deviceData ?
|
||||||
@@ -190,7 +190,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
|
|||||||
|
|
||||||
let savePromise = this.savePromise;
|
let savePromise = this.savePromise;
|
||||||
if (savePromise === null) {
|
if (savePromise === null) {
|
||||||
savePromise = new Promise((resolve, reject) => {
|
savePromise = new Promise((resolve) => {
|
||||||
this.resolveSavePromise = resolve;
|
this.resolveSavePromise = resolve;
|
||||||
});
|
});
|
||||||
this.savePromise = savePromise;
|
this.savePromise = savePromise;
|
||||||
@@ -309,10 +309,10 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
|
|||||||
*/
|
*/
|
||||||
private getDevicesFromStore(userIds: string[]): DeviceInfoMap {
|
private getDevicesFromStore(userIds: string[]): DeviceInfoMap {
|
||||||
const stored: DeviceInfoMap = {};
|
const stored: DeviceInfoMap = {};
|
||||||
userIds.map((u) => {
|
userIds.forEach((u) => {
|
||||||
stored[u] = {};
|
stored[u] = {};
|
||||||
const devices = this.getStoredDevicesForUser(u) || [];
|
const devices = this.getStoredDevicesForUser(u) || [];
|
||||||
devices.map(function(dev) {
|
devices.forEach(function(dev) {
|
||||||
stored[u][dev.deviceId] = dev;
|
stored[u][dev.deviceId] = dev;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -942,7 +942,7 @@ async function updateStoredDeviceKeysForUser(
|
|||||||
async function storeDeviceKeys(
|
async function storeDeviceKeys(
|
||||||
olmDevice: OlmDevice,
|
olmDevice: OlmDevice,
|
||||||
userStore: Record<string, DeviceInfo>,
|
userStore: Record<string, DeviceInfo>,
|
||||||
deviceResult: any, // TODO types
|
deviceResult: IDownloadKeyResult["device_keys"]["user_id"]["device_id"],
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (!deviceResult.keys) {
|
if (!deviceResult.keys) {
|
||||||
// no keys?
|
// no keys?
|
||||||
|
|||||||
@@ -909,12 +909,12 @@ export class OlmDevice {
|
|||||||
await this.cryptoStore.storeEndToEndSessionProblem(deviceKey, type, fixed);
|
await this.cryptoStore.storeEndToEndSessionProblem(deviceKey, type, fixed);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async sessionMayHaveProblems(deviceKey: string, timestamp: number): Promise<IProblem> {
|
public sessionMayHaveProblems(deviceKey: string, timestamp: number): Promise<IProblem> {
|
||||||
return await this.cryptoStore.getEndToEndSessionProblem(deviceKey, timestamp);
|
return this.cryptoStore.getEndToEndSessionProblem(deviceKey, timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
|
public filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
|
||||||
return await this.cryptoStore.filterOutNotifiedErrorDevices(devices);
|
return this.cryptoStore.filterOutNotifiedErrorDevices(devices);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Outbound group session
|
// Outbound group session
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export enum RoomKeyRequestState {
|
|||||||
export class OutgoingRoomKeyRequestManager {
|
export class OutgoingRoomKeyRequestManager {
|
||||||
// handle for the delayed call to sendOutgoingRoomKeyRequests. Non-null
|
// handle for the delayed call to sendOutgoingRoomKeyRequests. Non-null
|
||||||
// if the callback has been set, or if it is still running.
|
// if the callback has been set, or if it is still running.
|
||||||
private sendOutgoingRoomKeyRequestsTimer: number = null;
|
private sendOutgoingRoomKeyRequestsTimer: ReturnType<typeof setTimeout> = null;
|
||||||
|
|
||||||
// sanity check to ensure that we don't end up with two concurrent runs
|
// sanity check to ensure that we don't end up with two concurrent runs
|
||||||
// of sendOutgoingRoomKeyRequests
|
// of sendOutgoingRoomKeyRequests
|
||||||
@@ -189,9 +189,7 @@ export class OutgoingRoomKeyRequestManager {
|
|||||||
// in state ROOM_KEY_REQUEST_STATES.SENT, so we must have
|
// in state ROOM_KEY_REQUEST_STATES.SENT, so we must have
|
||||||
// raced with another tab to mark the request cancelled.
|
// raced with another tab to mark the request cancelled.
|
||||||
// Try again, to make sure the request is resent.
|
// Try again, to make sure the request is resent.
|
||||||
return await this.queueRoomKeyRequest(
|
return this.queueRoomKeyRequest(requestBody, recipients, resend);
|
||||||
requestBody, recipients, resend,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't want to wait for the timer, so we send it
|
// We don't want to wait for the timer, so we send it
|
||||||
|
|||||||
@@ -329,7 +329,7 @@ export class SecretStorage {
|
|||||||
// encoded, since this is how a key would normally be stored.
|
// encoded, since this is how a key would normally be stored.
|
||||||
if (encInfo.passthrough) return encodeBase64(decryption.get_private_key());
|
if (encInfo.passthrough) return encodeBase64(decryption.get_private_key());
|
||||||
|
|
||||||
return await decryption.decrypt(encInfo);
|
return decryption.decrypt(encInfo);
|
||||||
} finally {
|
} finally {
|
||||||
if (decryption && decryption.free) decryption.free();
|
if (decryption && decryption.free) decryption.free();
|
||||||
}
|
}
|
||||||
@@ -339,21 +339,15 @@ export class SecretStorage {
|
|||||||
* Check if a secret is stored on the server.
|
* Check if a secret is stored on the server.
|
||||||
*
|
*
|
||||||
* @param {string} name the name of the secret
|
* @param {string} name the name of the secret
|
||||||
* @param {boolean} checkKey check if the secret is encrypted by a trusted key
|
|
||||||
*
|
*
|
||||||
* @return {object?} map of key name to key info the secret is encrypted
|
* @return {object?} map of key name to key info the secret is encrypted
|
||||||
* with, or null if it is not present or not encrypted with a trusted
|
* with, or null if it is not present or not encrypted with a trusted
|
||||||
* key
|
* key
|
||||||
*/
|
*/
|
||||||
public async isStored(name: string, checkKey: boolean): Promise<Record<string, ISecretStorageKeyInfo> | null> {
|
public async isStored(name: string): Promise<Record<string, ISecretStorageKeyInfo> | null> {
|
||||||
// check if secret exists
|
// check if secret exists
|
||||||
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name);
|
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name);
|
||||||
if (!secretInfo) return null;
|
if (!secretInfo?.encrypted) return null;
|
||||||
if (!secretInfo.encrypted) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkKey === undefined) checkKey = true;
|
|
||||||
|
|
||||||
const ret = {};
|
const ret = {};
|
||||||
|
|
||||||
@@ -598,11 +592,11 @@ export class SecretStorage {
|
|||||||
|
|
||||||
if (keys[keyId].algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
if (keys[keyId].algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||||
const decryption = {
|
const decryption = {
|
||||||
encrypt: async function(secret: string): Promise<IEncryptedPayload> {
|
encrypt: function(secret: string): Promise<IEncryptedPayload> {
|
||||||
return await encryptAES(secret, privateKey, name);
|
return encryptAES(secret, privateKey, name);
|
||||||
},
|
},
|
||||||
decrypt: async function(encInfo: IEncryptedPayload): Promise<string> {
|
decrypt: function(encInfo: IEncryptedPayload): Promise<string> {
|
||||||
return await decryptAES(encInfo, privateKey, name);
|
return decryptAES(encInfo, privateKey, name);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return [keyId, decryption];
|
return [keyId, decryption];
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ async function deriveKeysBrowser(key: Uint8Array, name: string): Promise<[Crypto
|
|||||||
['sign', 'verify'],
|
['sign', 'verify'],
|
||||||
);
|
);
|
||||||
|
|
||||||
return await Promise.all([aesProm, hmacProm]);
|
return Promise.all([aesProm, hmacProm]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function encryptAES(data: string, key: Uint8Array, name: string, ivStr?: string): Promise<IEncryptedPayload> {
|
export function encryptAES(data: string, key: Uint8Array, name: string, ivStr?: string): Promise<IEncryptedPayload> {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class OlmEncryption extends EncryptionAlgorithm {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.prepPromise = this.crypto.downloadKeys(roomMembers).then((res) => {
|
this.prepPromise = this.crypto.downloadKeys(roomMembers).then(() => {
|
||||||
return this.crypto.ensureOlmSessionsForUsers(roomMembers);
|
return this.crypto.ensureOlmSessionsForUsers(roomMembers);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.sessionPrepared = true;
|
this.sessionPrepared = true;
|
||||||
@@ -144,7 +144,7 @@ class OlmEncryption extends EncryptionAlgorithm {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await Promise.all(promises).then(() => encryptedContent);
|
return Promise.all(promises).then(() => encryptedContent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,7 +261,7 @@ class OlmDecryption extends DecryptionAlgorithm {
|
|||||||
*
|
*
|
||||||
* @return {string} payload, if decrypted successfully.
|
* @return {string} payload, if decrypted successfully.
|
||||||
*/
|
*/
|
||||||
private async decryptMessage(theirDeviceIdentityKey: string, message: IMessage): Promise<string> {
|
private decryptMessage(theirDeviceIdentityKey: string, message: IMessage): Promise<string> {
|
||||||
// This is a wrapper that serialises decryptions of prekey messages, because
|
// This is a wrapper that serialises decryptions of prekey messages, because
|
||||||
// otherwise we race between deciding we have no active sessions for the message
|
// otherwise we race between deciding we have no active sessions for the message
|
||||||
// and creating a new one, which we can only do once because it removes the OTK.
|
// and creating a new one, which we can only do once because it removes the OTK.
|
||||||
@@ -274,7 +274,7 @@ class OlmDecryption extends DecryptionAlgorithm {
|
|||||||
});
|
});
|
||||||
// we want the error, but don't propagate it to the next decryption
|
// we want the error, but don't propagate it to the next decryption
|
||||||
this.olmDevice.olmPrekeyPromise = myPromise.catch(() => {});
|
this.olmDevice.olmPrekeyPromise = myPromise.catch(() => {});
|
||||||
return await myPromise;
|
return myPromise;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,18 +132,18 @@ export class BackupManager {
|
|||||||
if (!Algorithm) {
|
if (!Algorithm) {
|
||||||
throw new Error("Unknown backup algorithm: " + info.algorithm);
|
throw new Error("Unknown backup algorithm: " + info.algorithm);
|
||||||
}
|
}
|
||||||
if (!(typeof info.auth_data === "object")) {
|
if (typeof info.auth_data !== "object") {
|
||||||
throw new Error("Invalid backup data returned");
|
throw new Error("Invalid backup data returned");
|
||||||
}
|
}
|
||||||
return Algorithm.checkBackupVersion(info);
|
return Algorithm.checkBackupVersion(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async makeAlgorithm(info: IKeyBackupInfo, getKey: GetKey): Promise<BackupAlgorithm> {
|
public static makeAlgorithm(info: IKeyBackupInfo, getKey: GetKey): Promise<BackupAlgorithm> {
|
||||||
const Algorithm = algorithmsByName[info.algorithm];
|
const Algorithm = algorithmsByName[info.algorithm];
|
||||||
if (!Algorithm) {
|
if (!Algorithm) {
|
||||||
throw new Error("Unknown backup algorithm");
|
throw new Error("Unknown backup algorithm");
|
||||||
}
|
}
|
||||||
return await Algorithm.init(info.auth_data, getKey);
|
return Algorithm.init(info.auth_data, getKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async enableKeyBackup(info: IKeyBackupInfo): Promise<void> {
|
public async enableKeyBackup(info: IKeyBackupInfo): Promise<void> {
|
||||||
@@ -375,9 +375,7 @@ export class BackupManager {
|
|||||||
);
|
);
|
||||||
if (device) {
|
if (device) {
|
||||||
sigInfo.device = device;
|
sigInfo.device = device;
|
||||||
sigInfo.deviceTrust = await this.baseApis.checkDeviceTrust(
|
sigInfo.deviceTrust = this.baseApis.checkDeviceTrust(this.baseApis.getUserId(), sigInfo.deviceId);
|
||||||
this.baseApis.getUserId(), sigInfo.deviceId,
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
await verifySignature(
|
await verifySignature(
|
||||||
this.baseApis.crypto.olmDevice,
|
this.baseApis.crypto.olmDevice,
|
||||||
@@ -430,7 +428,7 @@ export class BackupManager {
|
|||||||
// requests from different clients hitting the server all at
|
// requests from different clients hitting the server all at
|
||||||
// the same time when a new key is sent
|
// the same time when a new key is sent
|
||||||
const delay = Math.random() * maxDelay;
|
const delay = Math.random() * maxDelay;
|
||||||
await sleep(delay, undefined);
|
await sleep(delay);
|
||||||
let numFailures = 0; // number of consecutive failures
|
let numFailures = 0; // number of consecutive failures
|
||||||
for (;;) {
|
for (;;) {
|
||||||
if (!this.algorithm) {
|
if (!this.algorithm) {
|
||||||
@@ -464,7 +462,7 @@ export class BackupManager {
|
|||||||
}
|
}
|
||||||
if (numFailures) {
|
if (numFailures) {
|
||||||
// exponential backoff if we have failures
|
// exponential backoff if we have failures
|
||||||
await sleep(1000 * Math.pow(2, Math.min(numFailures - 1, 4)), undefined);
|
await sleep(1000 * Math.pow(2, Math.min(numFailures - 1, 4)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -476,8 +474,8 @@ export class BackupManager {
|
|||||||
* Take some e2e keys waiting to be backed up and send them
|
* Take some e2e keys waiting to be backed up and send them
|
||||||
* to the backup.
|
* to the backup.
|
||||||
*
|
*
|
||||||
* @param {integer} limit Maximum number of keys to back up
|
* @param {number} limit Maximum number of keys to back up
|
||||||
* @returns {integer} Number of sessions backed up
|
* @returns {number} Number of sessions backed up
|
||||||
*/
|
*/
|
||||||
public async backupPendingKeys(limit: number): Promise<number> {
|
public async backupPendingKeys(limit: number): Promise<number> {
|
||||||
const sessions = await this.baseApis.crypto.cryptoStore.getSessionsNeedingBackup(limit);
|
const sessions = await this.baseApis.crypto.cryptoStore.getSessionsNeedingBackup(limit);
|
||||||
@@ -495,7 +493,7 @@ export class BackupManager {
|
|||||||
rooms[roomId] = { sessions: {} };
|
rooms[roomId] = { sessions: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionData = await this.baseApis.crypto.olmDevice.exportInboundGroupSession(
|
const sessionData = this.baseApis.crypto.olmDevice.exportInboundGroupSession(
|
||||||
session.senderKey, session.sessionId, session.sessionData,
|
session.senderKey, session.sessionId, session.sessionData,
|
||||||
);
|
);
|
||||||
sessionData.algorithm = MEGOLM_ALGORITHM;
|
sessionData.algorithm = MEGOLM_ALGORITHM;
|
||||||
@@ -779,15 +777,15 @@ export class Aes256 implements BackupAlgorithm {
|
|||||||
|
|
||||||
public get untrusted() { return false; }
|
public get untrusted() { return false; }
|
||||||
|
|
||||||
async encryptSession(data: Record<string, any>): Promise<any> {
|
public encryptSession(data: Record<string, any>): Promise<any> {
|
||||||
const plainText: Record<string, any> = Object.assign({}, data);
|
const plainText: Record<string, any> = Object.assign({}, data);
|
||||||
delete plainText.session_id;
|
delete plainText.session_id;
|
||||||
delete plainText.room_id;
|
delete plainText.room_id;
|
||||||
delete plainText.first_known_index;
|
delete plainText.first_known_index;
|
||||||
return await encryptAES(JSON.stringify(plainText), this.key, data.session_id);
|
return encryptAES(JSON.stringify(plainText), this.key, data.session_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async decryptSessions(sessions: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]> {
|
public async decryptSessions(sessions: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]> {
|
||||||
const keys: IMegolmSessionData[] = [];
|
const keys: IMegolmSessionData[] = [];
|
||||||
|
|
||||||
for (const [sessionId, sessionData] of Object.entries(sessions)) {
|
for (const [sessionId, sessionData] of Object.entries(sessions)) {
|
||||||
@@ -802,7 +800,7 @@ export class Aes256 implements BackupAlgorithm {
|
|||||||
return keys;
|
return keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
async keyMatches(key: Uint8Array): Promise<boolean> {
|
public async keyMatches(key: Uint8Array): Promise<boolean> {
|
||||||
if (this.authData.mac) {
|
if (this.authData.mac) {
|
||||||
const { mac } = await calculateKeyCheck(key, this.authData.iv);
|
const { mac } = await calculateKeyCheck(key, this.authData.iv);
|
||||||
return this.authData.mac.replace(/=+$/g, '') === mac.replace(/=+/g, '');
|
return this.authData.mac.replace(/=+$/g, '') === mac.replace(/=+/g, '');
|
||||||
|
|||||||
@@ -61,11 +61,13 @@ export class DehydrationManager {
|
|||||||
private key: Uint8Array;
|
private key: Uint8Array;
|
||||||
private keyInfo: {[props: string]: any};
|
private keyInfo: {[props: string]: any};
|
||||||
private deviceDisplayName: string;
|
private deviceDisplayName: string;
|
||||||
|
|
||||||
constructor(private readonly crypto: Crypto) {
|
constructor(private readonly crypto: Crypto) {
|
||||||
this.getDehydrationKeyFromCache();
|
this.getDehydrationKeyFromCache();
|
||||||
}
|
}
|
||||||
async getDehydrationKeyFromCache(): Promise<void> {
|
|
||||||
return await this.crypto.cryptoStore.doTxn(
|
public getDehydrationKeyFromCache(): Promise<void> {
|
||||||
|
return this.crypto.cryptoStore.doTxn(
|
||||||
'readonly',
|
'readonly',
|
||||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||||
(txn) => {
|
(txn) => {
|
||||||
@@ -93,7 +95,7 @@ export class DehydrationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** set the key, and queue periodic dehydration to the server in the background */
|
/** set the key, and queue periodic dehydration to the server in the background */
|
||||||
async setKeyAndQueueDehydration(
|
public async setKeyAndQueueDehydration(
|
||||||
key: Uint8Array, keyInfo: {[props: string]: any} = {},
|
key: Uint8Array, keyInfo: {[props: string]: any} = {},
|
||||||
deviceDisplayName: string = undefined,
|
deviceDisplayName: string = undefined,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -104,7 +106,7 @@ export class DehydrationManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setKey(
|
public async setKey(
|
||||||
key: Uint8Array, keyInfo: {[props: string]: any} = {},
|
key: Uint8Array, keyInfo: {[props: string]: any} = {},
|
||||||
deviceDisplayName: string = undefined,
|
deviceDisplayName: string = undefined,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
@@ -148,7 +150,7 @@ export class DehydrationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** returns the device id of the newly created dehydrated device */
|
/** returns the device id of the newly created dehydrated device */
|
||||||
async dehydrateDevice(): Promise<string> {
|
public async dehydrateDevice(): Promise<string> {
|
||||||
if (this.inProgress) {
|
if (this.inProgress) {
|
||||||
logger.log("Dehydration already in progress -- not starting new dehydration");
|
logger.log("Dehydration already in progress -- not starting new dehydration");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ import { keyFromPassphrase } from './key_passphrase';
|
|||||||
import { decodeRecoveryKey, encodeRecoveryKey } from './recoverykey';
|
import { decodeRecoveryKey, encodeRecoveryKey } from './recoverykey';
|
||||||
import { VerificationRequest } from "./verification/request/VerificationRequest";
|
import { VerificationRequest } from "./verification/request/VerificationRequest";
|
||||||
import { InRoomChannel, InRoomRequests } from "./verification/request/InRoomChannel";
|
import { InRoomChannel, InRoomRequests } from "./verification/request/InRoomChannel";
|
||||||
import { ToDeviceChannel, ToDeviceRequests } from "./verification/request/ToDeviceChannel";
|
import { ToDeviceChannel, ToDeviceRequests, Request } from "./verification/request/ToDeviceChannel";
|
||||||
import { IllegalMethod } from "./verification/IllegalMethod";
|
import { IllegalMethod } from "./verification/IllegalMethod";
|
||||||
import { KeySignatureUploadError } from "../errors";
|
import { KeySignatureUploadError } from "../errors";
|
||||||
import { calculateKeyCheck, decryptAES, encryptAES } from './aes';
|
import { calculateKeyCheck, decryptAES, encryptAES } from './aes';
|
||||||
@@ -309,7 +309,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
|
|
||||||
private oneTimeKeyCount: number;
|
private oneTimeKeyCount: number;
|
||||||
private needsNewFallback: boolean;
|
private needsNewFallback: boolean;
|
||||||
private fallbackCleanup?: number; // setTimeout ID
|
private fallbackCleanup?: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cryptography bits
|
* Cryptography bits
|
||||||
@@ -402,7 +402,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
|
|
||||||
// try to get key from app
|
// try to get key from app
|
||||||
if (this.baseApis.cryptoCallbacks && this.baseApis.cryptoCallbacks.getBackupKey) {
|
if (this.baseApis.cryptoCallbacks && this.baseApis.cryptoCallbacks.getBackupKey) {
|
||||||
return await this.baseApis.cryptoCallbacks.getBackupKey();
|
return this.baseApis.cryptoCallbacks.getBackupKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error("Unable to get private key");
|
throw new Error("Unable to get private key");
|
||||||
@@ -690,7 +690,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
|
|
||||||
// Cross-sign own device
|
// Cross-sign own device
|
||||||
const device = this.deviceList.getStoredDevice(this.userId, this.deviceId);
|
const device = this.deviceList.getStoredDevice(this.userId, this.deviceId);
|
||||||
const deviceSignature = await crossSigningInfo.signDevice(this.userId, device) as ISignedKey;
|
const deviceSignature = await crossSigningInfo.signDevice(this.userId, device);
|
||||||
builder.addKeySignature(this.userId, this.deviceId, deviceSignature);
|
builder.addKeySignature(this.userId, this.deviceId, deviceSignature);
|
||||||
|
|
||||||
// Sign message key backup with cross-signing master key
|
// Sign message key backup with cross-signing master key
|
||||||
@@ -920,7 +920,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
// secrets using it, in theory. We could move them to the new key but a)
|
// secrets using it, in theory. We could move them to the new key but a)
|
||||||
// that would mean we'd need to prompt for the old passphrase, and b)
|
// that would mean we'd need to prompt for the old passphrase, and b)
|
||||||
// it's not clear that would be the right thing to do anyway.
|
// it's not clear that would be the right thing to do anyway.
|
||||||
const { keyInfo, privateKey } = await createSecretStorageKey();
|
const { keyInfo = {} as IAddSecretStorageKeyOpts, privateKey } = await createSecretStorageKey();
|
||||||
newKeyId = await createSSSS(keyInfo, privateKey);
|
newKeyId = await createSSSS(keyInfo, privateKey);
|
||||||
} else if (!storageExists && keyBackupInfo) {
|
} else if (!storageExists && keyBackupInfo) {
|
||||||
// we have an existing backup, but no SSSS
|
// we have an existing backup, but no SSSS
|
||||||
@@ -1026,7 +1026,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
const decodedBackupKey = new Uint8Array(olmlib.decodeBase64(
|
const decodedBackupKey = new Uint8Array(olmlib.decodeBase64(
|
||||||
fixedBackupKey || sessionBackupKey,
|
fixedBackupKey || sessionBackupKey,
|
||||||
));
|
));
|
||||||
await builder.addSessionBackupPrivateKeyToCache(decodedBackupKey);
|
builder.addSessionBackupPrivateKeyToCache(decodedBackupKey);
|
||||||
} else if (this.backupManager.getKeyBackupEnabled()) {
|
} else if (this.backupManager.getKeyBackupEnabled()) {
|
||||||
// key backup is enabled but we don't have a session backup key in SSSS: see if we have one in
|
// key backup is enabled but we don't have a session backup key in SSSS: see if we have one in
|
||||||
// the cache or the user can provide one, and if so, write it to SSSS
|
// the cache or the user can provide one, and if so, write it to SSSS
|
||||||
@@ -1076,11 +1076,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
return this.secretStorage.get(name);
|
return this.secretStorage.get(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
public isSecretStored(
|
public isSecretStored(name: string): Promise<Record<string, ISecretStorageKeyInfo> | null> {
|
||||||
name: string,
|
return this.secretStorage.isStored(name);
|
||||||
checkKey?: boolean,
|
|
||||||
): Promise<Record<string, ISecretStorageKeyInfo> | null> {
|
|
||||||
return this.secretStorage.isStored(name, checkKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public requestSecret(name: string, devices: string[]): ISecretRequest {
|
public requestSecret(name: string, devices: string[]): ISecretRequest {
|
||||||
@@ -1423,6 +1420,25 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether one of our own devices is cross-signed by our
|
||||||
|
* user's stored keys, regardless of whether we trust those keys yet.
|
||||||
|
*
|
||||||
|
* @param {string} deviceId The ID of the device to check
|
||||||
|
*
|
||||||
|
* @returns {boolean} true if the device is cross-signed
|
||||||
|
*/
|
||||||
|
public checkIfOwnDeviceCrossSigned(deviceId: string): boolean {
|
||||||
|
const device = this.deviceList.getStoredDevice(this.userId, deviceId);
|
||||||
|
const userCrossSigning = this.deviceList.getStoredCrossSigningForUser(this.userId);
|
||||||
|
return userCrossSigning.checkDeviceTrust(
|
||||||
|
userCrossSigning,
|
||||||
|
device,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
).isCrossSigningVerified();
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Event handler for DeviceList's userNewDevices event
|
* Event handler for DeviceList's userNewDevices event
|
||||||
*/
|
*/
|
||||||
@@ -2302,8 +2318,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
userId: string,
|
userId: string,
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
transactionId: string = null,
|
transactionId: string = null,
|
||||||
): any { // TODO types
|
): VerificationBase<any, any> {
|
||||||
let request;
|
let request: Request;
|
||||||
if (transactionId) {
|
if (transactionId) {
|
||||||
request = this.toDeviceVerificationRequests.getRequestBySenderAndTxnId(userId, transactionId);
|
request = this.toDeviceVerificationRequests.getRequestBySenderAndTxnId(userId, transactionId);
|
||||||
if (!request) {
|
if (!request) {
|
||||||
@@ -2871,7 +2887,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
} else {
|
} else {
|
||||||
const content = event.getWireContent();
|
const content = event.getWireContent();
|
||||||
const alg = this.getRoomDecryptor(event.getRoomId(), content.algorithm);
|
const alg = this.getRoomDecryptor(event.getRoomId(), content.algorithm);
|
||||||
return await alg.decryptEvent(event);
|
return alg.decryptEvent(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ interface IKey {
|
|||||||
iterations: number;
|
iterations: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function keyFromAuthData(authData: IAuthData, password: string): Promise<Uint8Array> {
|
export function keyFromAuthData(authData: IAuthData, password: string): Promise<Uint8Array> {
|
||||||
if (!global.Olm) {
|
if (!global.Olm) {
|
||||||
throw new Error("Olm is not available");
|
throw new Error("Olm is not available");
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,7 @@ export async function keyFromAuthData(authData: IAuthData, password: string): Pr
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await deriveKey(
|
return deriveKey(
|
||||||
password, authData.private_key_salt,
|
password, authData.private_key_salt,
|
||||||
authData.private_key_iterations,
|
authData.private_key_iterations,
|
||||||
authData.private_key_bits || DEFAULT_BITSIZE,
|
authData.private_key_bits || DEFAULT_BITSIZE,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import bs58 from 'bs58';
|
import * as bs58 from 'bs58';
|
||||||
|
|
||||||
// picked arbitrarily but to try & avoid clashing with any bitcoin ones
|
// picked arbitrarily but to try & avoid clashing with any bitcoin ones
|
||||||
// (which are also base58 encoded, but bitcoin's involve a lot more hashing)
|
// (which are also base58 encoded, but bitcoin's involve a lot more hashing)
|
||||||
|
|||||||
@@ -873,7 +873,7 @@ export class Backend implements CryptoStore {
|
|||||||
|
|
||||||
public doTxn<T>(
|
public doTxn<T>(
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
stores: Iterable<string>,
|
stores: string | string[],
|
||||||
func: (txn: IDBTransaction) => T,
|
func: (txn: IDBTransaction) => T,
|
||||||
log: PrefixedLogger = logger,
|
log: PrefixedLogger = logger,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
|
|||||||
@@ -228,8 +228,8 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
|
|||||||
// (hence 43 characters long).
|
// (hence 43 characters long).
|
||||||
|
|
||||||
func({
|
func({
|
||||||
senderKey: key.substr(KEY_INBOUND_SESSION_PREFIX.length, 43),
|
senderKey: key.slice(KEY_INBOUND_SESSION_PREFIX.length, KEY_INBOUND_SESSION_PREFIX.length + 43),
|
||||||
sessionId: key.substr(KEY_INBOUND_SESSION_PREFIX.length + 44),
|
sessionId: key.slice(KEY_INBOUND_SESSION_PREFIX.length + 44),
|
||||||
sessionData: getJsonItem(this.store, key),
|
sessionData: getJsonItem(this.store, key),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -299,7 +299,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
|
|||||||
for (let i = 0; i < this.store.length; ++i) {
|
for (let i = 0; i < this.store.length; ++i) {
|
||||||
const key = this.store.key(i);
|
const key = this.store.key(i);
|
||||||
if (key.startsWith(prefix)) {
|
if (key.startsWith(prefix)) {
|
||||||
const roomId = key.substr(prefix.length);
|
const roomId = key.slice(prefix.length);
|
||||||
result[roomId] = getJsonItem(this.store, key);
|
result[roomId] = getJsonItem(this.store, key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -313,8 +313,8 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
|
|||||||
for (const session in sessionsNeedingBackup) {
|
for (const session in sessionsNeedingBackup) {
|
||||||
if (Object.prototype.hasOwnProperty.call(sessionsNeedingBackup, session)) {
|
if (Object.prototype.hasOwnProperty.call(sessionsNeedingBackup, session)) {
|
||||||
// see getAllEndToEndInboundGroupSessions for the magic number explanations
|
// see getAllEndToEndInboundGroupSessions for the magic number explanations
|
||||||
const senderKey = session.substr(0, 43);
|
const senderKey = session.slice(0, 43);
|
||||||
const sessionId = session.substr(44);
|
const sessionId = session.slice(44);
|
||||||
this.getEndToEndInboundGroupSession(
|
this.getEndToEndInboundGroupSession(
|
||||||
senderKey, sessionId, null,
|
senderKey, sessionId, null,
|
||||||
(sessionData) => {
|
(sessionData) => {
|
||||||
@@ -325,7 +325,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (limit && session.length >= limit) {
|
if (limit && sessions.length >= limit) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -418,8 +418,8 @@ export class MemoryCryptoStore implements CryptoStore {
|
|||||||
// (hence 43 characters long).
|
// (hence 43 characters long).
|
||||||
|
|
||||||
func({
|
func({
|
||||||
senderKey: key.substr(0, 43),
|
senderKey: key.slice(0, 43),
|
||||||
sessionId: key.substr(44),
|
sessionId: key.slice(44),
|
||||||
sessionData: this.inboundGroupSessions[key],
|
sessionData: this.inboundGroupSessions[key],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -482,8 +482,8 @@ export class MemoryCryptoStore implements CryptoStore {
|
|||||||
for (const session in this.sessionsNeedingBackup) {
|
for (const session in this.sessionsNeedingBackup) {
|
||||||
if (this.inboundGroupSessions[session]) {
|
if (this.inboundGroupSessions[session]) {
|
||||||
sessions.push({
|
sessions.push({
|
||||||
senderKey: session.substr(0, 43),
|
senderKey: session.slice(0, 43),
|
||||||
sessionId: session.substr(44),
|
sessionId: session.slice(44),
|
||||||
sessionData: this.inboundGroupSessions[session],
|
sessionData: this.inboundGroupSessions[session],
|
||||||
});
|
});
|
||||||
if (limit && session.length >= limit) {
|
if (limit && session.length >= limit) {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export class VerificationBase<
|
|||||||
private cancelled = false;
|
private cancelled = false;
|
||||||
private _done = false;
|
private _done = false;
|
||||||
private promise: Promise<void> = null;
|
private promise: Promise<void> = null;
|
||||||
private transactionTimeoutTimer: number = null;
|
private transactionTimeoutTimer: ReturnType<typeof setTimeout> = null;
|
||||||
protected expectedEvent: string;
|
protected expectedEvent: string;
|
||||||
private resolve: () => void;
|
private resolve: () => void;
|
||||||
private reject: (e: Error | MatrixEvent) => void;
|
private reject: (e: Error | MatrixEvent) => void;
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ function calculateMAC(olmSAS: OlmSAS, method: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const calculateKeyAgreement = {
|
const calculateKeyAgreement = {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
"curve25519-hkdf-sha256": function(sas: SAS, olmSAS: OlmSAS, bytes: number): Uint8Array {
|
"curve25519-hkdf-sha256": function(sas: SAS, olmSAS: OlmSAS, bytes: number): Uint8Array {
|
||||||
const ourInfo = `${sas.baseApis.getUserId()}|${sas.baseApis.deviceId}|`
|
const ourInfo = `${sas.baseApis.getUserId()}|${sas.baseApis.deviceId}|`
|
||||||
+ `${sas.ourSASPubKey}|`;
|
+ `${sas.ourSASPubKey}|`;
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ export class InRoomChannel implements IVerificationChannel {
|
|||||||
* @param {boolean} isLiveEvent whether this is an even received through sync or not
|
* @param {boolean} isLiveEvent whether this is an even received through sync or not
|
||||||
* @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent.
|
* @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent.
|
||||||
*/
|
*/
|
||||||
public async handleEvent(event: MatrixEvent, request: VerificationRequest, isLiveEvent = false): Promise<void> {
|
public handleEvent(event: MatrixEvent, request: VerificationRequest, isLiveEvent = false): Promise<void> {
|
||||||
// prevent processing the same event multiple times, as under
|
// prevent processing the same event multiple times, as under
|
||||||
// some circumstances Room.timeline can get emitted twice for the same event
|
// some circumstances Room.timeline can get emitted twice for the same event
|
||||||
if (request.hasEventId(event.getId())) {
|
if (request.hasEventId(event.getId())) {
|
||||||
@@ -221,8 +221,7 @@ export class InRoomChannel implements IVerificationChannel {
|
|||||||
const isRemoteEcho = !!event.getUnsigned().transaction_id;
|
const isRemoteEcho = !!event.getUnsigned().transaction_id;
|
||||||
const isSentByUs = event.getSender() === this.client.getUserId();
|
const isSentByUs = event.getSender() === this.client.getUserId();
|
||||||
|
|
||||||
return await request.handleEvent(
|
return request.handleEvent(type, event, isLiveEvent, isRemoteEcho, isSentByUs);
|
||||||
type, event, isLiveEvent, isRemoteEcho, isSentByUs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export class VerificationRequest<
|
|||||||
private eventsByUs = new Map<string, MatrixEvent>();
|
private eventsByUs = new Map<string, MatrixEvent>();
|
||||||
private eventsByThem = new Map<string, MatrixEvent>();
|
private eventsByThem = new Map<string, MatrixEvent>();
|
||||||
private _observeOnly = false;
|
private _observeOnly = false;
|
||||||
private timeoutTimer: number = null;
|
private timeoutTimer: ReturnType<typeof setTimeout> = null;
|
||||||
private _accepting = false;
|
private _accepting = false;
|
||||||
private _declining = false;
|
private _declining = false;
|
||||||
private verifierHasFinished = false;
|
private verifierHasFinished = false;
|
||||||
@@ -796,8 +796,7 @@ export class VerificationRequest<
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setupTimeout(phase: Phase): void {
|
private setupTimeout(phase: Phase): void {
|
||||||
const shouldTimeout = !this.timeoutTimer && !this.observeOnly &&
|
const shouldTimeout = !this.timeoutTimer && !this.observeOnly && phase === PHASE_REQUESTED;
|
||||||
phase === PHASE_REQUESTED;
|
|
||||||
|
|
||||||
if (shouldTimeout) {
|
if (shouldTimeout) {
|
||||||
this.timeoutTimer = setTimeout(this.cancelOnTimeout, this.timeout);
|
this.timeoutTimer = setTimeout(this.cancelOnTimeout, this.timeout);
|
||||||
@@ -814,15 +813,15 @@ export class VerificationRequest<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private cancelOnTimeout = () => {
|
private cancelOnTimeout = async () => {
|
||||||
try {
|
try {
|
||||||
if (this.initiatedByMe) {
|
if (this.initiatedByMe) {
|
||||||
this.cancel({
|
await this.cancel({
|
||||||
reason: "Other party didn't accept in time",
|
reason: "Other party didn't accept in time",
|
||||||
code: "m.timeout",
|
code: "m.timeout",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.cancel({
|
await this.cancel({
|
||||||
reason: "User didn't accept in time",
|
reason: "User didn't accept in time",
|
||||||
code: "m.timeout",
|
code: "m.timeout",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,15 +25,31 @@ export interface MapperOpts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function eventMapperFor(client: MatrixClient, options: MapperOpts): EventMapper {
|
export function eventMapperFor(client: MatrixClient, options: MapperOpts): EventMapper {
|
||||||
const preventReEmit = Boolean(options.preventReEmit);
|
let preventReEmit = Boolean(options.preventReEmit);
|
||||||
const decrypt = options.decrypt !== false;
|
const decrypt = options.decrypt !== false;
|
||||||
|
|
||||||
function mapper(plainOldJsObject: Partial<IEvent>) {
|
function mapper(plainOldJsObject: Partial<IEvent>) {
|
||||||
const event = new MatrixEvent(plainOldJsObject);
|
const room = client.getRoom(plainOldJsObject.room_id);
|
||||||
|
|
||||||
const room = client.getRoom(event.getRoomId());
|
let event: MatrixEvent;
|
||||||
if (room?.threads.has(event.getId())) {
|
// If the event is already known to the room, let's re-use the model rather than duplicating.
|
||||||
event.setThread(room.threads.get(event.getId()));
|
// We avoid doing this to state events as they may be forward or backwards looking which tweaks behaviour.
|
||||||
|
if (room && plainOldJsObject.state_key === undefined) {
|
||||||
|
event = room.findEventById(plainOldJsObject.event_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!event || event.status) {
|
||||||
|
event = new MatrixEvent(plainOldJsObject);
|
||||||
|
} else {
|
||||||
|
// merge the latest unsigned data from the server
|
||||||
|
event.setUnsigned({ ...event.getUnsigned(), ...plainOldJsObject.unsigned });
|
||||||
|
// prevent doubling up re-emitters
|
||||||
|
preventReEmit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const thread = room?.findThreadForEvent(event);
|
||||||
|
if (thread) {
|
||||||
|
event.setThread(thread);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.isEncrypted()) {
|
if (event.isEncrypted()) {
|
||||||
@@ -46,11 +62,15 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
|
|||||||
client.decryptEventIfNeeded(event);
|
client.decryptEventIfNeeded(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!preventReEmit) {
|
if (!preventReEmit) {
|
||||||
client.reEmitter.reEmit(event, [
|
client.reEmitter.reEmit(event, [
|
||||||
MatrixEventEvent.Replaced,
|
MatrixEventEvent.Replaced,
|
||||||
MatrixEventEvent.VisibilityChange,
|
MatrixEventEvent.VisibilityChange,
|
||||||
]);
|
]);
|
||||||
|
room?.reEmitter.reEmit(event, [
|
||||||
|
MatrixEventEvent.BeforeRedaction,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import {
|
|||||||
function matchesWildcard(actualValue: string, filterValue: string): boolean {
|
function matchesWildcard(actualValue: string, filterValue: string): boolean {
|
||||||
if (filterValue.endsWith("*")) {
|
if (filterValue.endsWith("*")) {
|
||||||
const typePrefix = filterValue.slice(0, -1);
|
const typePrefix = filterValue.slice(0, -1);
|
||||||
return actualValue.substr(0, typePrefix.length) === typePrefix;
|
return actualValue.slice(0, typePrefix.length) === typePrefix;
|
||||||
} else {
|
} else {
|
||||||
return actualValue === filterValue;
|
return actualValue === filterValue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import type { Request as _Request, CoreOptions } from "request";
|
|||||||
import * as callbacks from "./realtime-callbacks";
|
import * as callbacks from "./realtime-callbacks";
|
||||||
import { IUploadOpts } from "./@types/requests";
|
import { IUploadOpts } from "./@types/requests";
|
||||||
import { IAbortablePromise, IUsageLimit } from "./@types/partials";
|
import { IAbortablePromise, IUsageLimit } from "./@types/partials";
|
||||||
import { IDeferred } from "./utils";
|
import { IDeferred, sleep } from "./utils";
|
||||||
import { Callback } from "./client";
|
import { Callback } from "./client";
|
||||||
import * as utils from "./utils";
|
import * as utils from "./utils";
|
||||||
import { logger } from './logger';
|
import { logger } from './logger';
|
||||||
@@ -1055,7 +1055,7 @@ interface IErrorJson extends Partial<IUsageLimit> {
|
|||||||
* @prop {string} name Same as MatrixError.errcode but with a default unknown string.
|
* @prop {string} name Same as MatrixError.errcode but with a default unknown string.
|
||||||
* @prop {string} message The Matrix 'error' value, e.g. "Missing token."
|
* @prop {string} message The Matrix 'error' value, e.g. "Missing token."
|
||||||
* @prop {Object} data The raw Matrix error JSON used to construct this object.
|
* @prop {Object} data The raw Matrix error JSON used to construct this object.
|
||||||
* @prop {integer} httpStatus The numeric HTTP status code given
|
* @prop {number} httpStatus The numeric HTTP status code given
|
||||||
*/
|
*/
|
||||||
export class MatrixError extends Error {
|
export class MatrixError extends Error {
|
||||||
public readonly errcode: string;
|
public readonly errcode: string;
|
||||||
@@ -1105,7 +1105,7 @@ export class AbortError extends Error {
|
|||||||
* @return {any} the result of the network operation
|
* @return {any} the result of the network operation
|
||||||
* @throws {ConnectionError} If after maxAttempts the callback still throws ConnectionError
|
* @throws {ConnectionError} If after maxAttempts the callback still throws ConnectionError
|
||||||
*/
|
*/
|
||||||
export async function retryNetworkOperation<T>(maxAttempts: number, callback: () => T): Promise<T> {
|
export async function retryNetworkOperation<T>(maxAttempts: number, callback: () => Promise<T>): Promise<T> {
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
let lastConnectionError = null;
|
let lastConnectionError = null;
|
||||||
while (attempts < maxAttempts) {
|
while (attempts < maxAttempts) {
|
||||||
@@ -1114,9 +1114,9 @@ export async function retryNetworkOperation<T>(maxAttempts: number, callback: ()
|
|||||||
const timeout = 1000 * Math.pow(2, attempts);
|
const timeout = 1000 * Math.pow(2, attempts);
|
||||||
logger.log(`network operation failed ${attempts} times,` +
|
logger.log(`network operation failed ${attempts} times,` +
|
||||||
` retrying in ${timeout}ms...`);
|
` retrying in ${timeout}ms...`);
|
||||||
await new Promise(r => setTimeout(r, timeout));
|
await sleep(timeout);
|
||||||
}
|
}
|
||||||
return await callback();
|
return callback();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof ConnectionError) {
|
if (err instanceof ConnectionError) {
|
||||||
attempts += 1;
|
attempts += 1;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import request from "request";
|
import * as request from "request";
|
||||||
|
|
||||||
import * as matrixcs from "./matrix";
|
import * as matrixcs from "./matrix";
|
||||||
import * as utils from "./utils";
|
import * as utils from "./utils";
|
||||||
|
|||||||
@@ -46,9 +46,16 @@ export interface IAuthData {
|
|||||||
session?: string;
|
session?: string;
|
||||||
completed?: string[];
|
completed?: string[];
|
||||||
flows?: IFlow[];
|
flows?: IFlow[];
|
||||||
|
available_flows?: IFlow[];
|
||||||
|
stages?: string[];
|
||||||
|
required_stages?: AuthType[];
|
||||||
params?: Record<string, Record<string, any>>;
|
params?: Record<string, Record<string, any>>;
|
||||||
|
data?: Record<string, string>;
|
||||||
errcode?: string;
|
errcode?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
user_id?: string;
|
||||||
|
device_id?: string;
|
||||||
|
access_token?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AuthType {
|
export enum AuthType {
|
||||||
@@ -60,7 +67,11 @@ export enum AuthType {
|
|||||||
Sso = "m.login.sso",
|
Sso = "m.login.sso",
|
||||||
SsoUnstable = "org.matrix.login.sso",
|
SsoUnstable = "org.matrix.login.sso",
|
||||||
Dummy = "m.login.dummy",
|
Dummy = "m.login.dummy",
|
||||||
RegistrationToken = "org.matrix.msc3231.login.registration_token",
|
RegistrationToken = "m.login.registration_token",
|
||||||
|
// For backwards compatability with servers that have not yet updated to
|
||||||
|
// use the stable "m.login.registration_token" type.
|
||||||
|
// The authentication flow is the same in both cases.
|
||||||
|
UnstableRegistrationToken = "org.matrix.msc3231.login.registration_token",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAuthDict {
|
export interface IAuthDict {
|
||||||
@@ -79,7 +90,8 @@ export interface IAuthDict {
|
|||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
threepid_creds?: any;
|
threepid_creds?: any;
|
||||||
threepidCreds?: any;
|
threepidCreds?: any;
|
||||||
registrationToken?: string;
|
// For m.login.registration_token type
|
||||||
|
token?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class NoAuthFlowFoundError extends Error {
|
class NoAuthFlowFoundError extends Error {
|
||||||
@@ -198,6 +210,8 @@ export class InteractiveAuth {
|
|||||||
private chosenFlow: IFlow = null;
|
private chosenFlow: IFlow = null;
|
||||||
private currentStage: string = null;
|
private currentStage: string = null;
|
||||||
|
|
||||||
|
private emailAttempt = 1;
|
||||||
|
|
||||||
// if we are currently trying to submit an auth dict (which includes polling)
|
// if we are currently trying to submit an auth dict (which includes polling)
|
||||||
// the promise the will resolve/reject when it completes
|
// the promise the will resolve/reject when it completes
|
||||||
private submitPromise: Promise<void> = null;
|
private submitPromise: Promise<void> = null;
|
||||||
@@ -403,6 +417,34 @@ export class InteractiveAuth {
|
|||||||
this.emailSid = sid;
|
this.emailSid = sid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests a new email token and sets the email sid for the validation session
|
||||||
|
*/
|
||||||
|
public requestEmailToken = async () => {
|
||||||
|
if (!this.requestingEmailToken) {
|
||||||
|
logger.trace("Requesting email token. Attempt: " + this.emailAttempt);
|
||||||
|
// If we've picked a flow with email auth, we send the email
|
||||||
|
// now because we want the request to fail as soon as possible
|
||||||
|
// if the email address is not valid (ie. already taken or not
|
||||||
|
// registered, depending on what the operation is).
|
||||||
|
this.requestingEmailToken = true;
|
||||||
|
try {
|
||||||
|
const requestTokenResult = await this.requestEmailTokenCallback(
|
||||||
|
this.inputs.emailAddress,
|
||||||
|
this.clientSecret,
|
||||||
|
this.emailAttempt++,
|
||||||
|
this.data.session,
|
||||||
|
);
|
||||||
|
this.emailSid = requestTokenResult.sid;
|
||||||
|
logger.trace("Email token request succeeded");
|
||||||
|
} finally {
|
||||||
|
this.requestingEmailToken = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn("Could not request email token: Already requesting");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fire off a request, and either resolve the promise, or call
|
* Fire off a request, and either resolve the promise, or call
|
||||||
* startAuthStage.
|
* startAuthStage.
|
||||||
@@ -453,24 +495,9 @@ export class InteractiveAuth {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!this.emailSid && this.chosenFlow.stages.includes(AuthType.Email)) {
|
||||||
!this.emailSid &&
|
|
||||||
!this.requestingEmailToken &&
|
|
||||||
this.chosenFlow.stages.includes(AuthType.Email)
|
|
||||||
) {
|
|
||||||
// If we've picked a flow with email auth, we send the email
|
|
||||||
// now because we want the request to fail as soon as possible
|
|
||||||
// if the email address is not valid (ie. already taken or not
|
|
||||||
// registered, depending on what the operation is).
|
|
||||||
this.requestingEmailToken = true;
|
|
||||||
try {
|
try {
|
||||||
const requestTokenResult = await this.requestEmailTokenCallback(
|
await this.requestEmailToken();
|
||||||
this.inputs.emailAddress,
|
|
||||||
this.clientSecret,
|
|
||||||
1, // TODO: Multiple send attempts?
|
|
||||||
this.data.session,
|
|
||||||
);
|
|
||||||
this.emailSid = requestTokenResult.sid;
|
|
||||||
// NB. promise is not resolved here - at some point, doRequest
|
// NB. promise is not resolved here - at some point, doRequest
|
||||||
// will be called again and if the user has jumped through all
|
// will be called again and if the user has jumped through all
|
||||||
// the hoops correctly, auth will be complete and the request
|
// the hoops correctly, auth will be complete and the request
|
||||||
@@ -486,8 +513,6 @@ export class InteractiveAuth {
|
|||||||
// send the email, for whatever reason.
|
// send the email, for whatever reason.
|
||||||
this.attemptAuthDeferred.reject(e);
|
this.attemptAuthDeferred.reject(e);
|
||||||
this.attemptAuthDeferred = null;
|
this.attemptAuthDeferred = null;
|
||||||
} finally {
|
|
||||||
this.requestingEmailToken = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ limitations under the License.
|
|||||||
import { MemoryCryptoStore } from "./crypto/store/memory-crypto-store";
|
import { MemoryCryptoStore } from "./crypto/store/memory-crypto-store";
|
||||||
import { MemoryStore } from "./store/memory";
|
import { MemoryStore } from "./store/memory";
|
||||||
import { MatrixScheduler } from "./scheduler";
|
import { MatrixScheduler } from "./scheduler";
|
||||||
import { MatrixClient } from "./client";
|
import { MatrixClient, ICreateClientOpts } from "./client";
|
||||||
import { ICreateClientOpts } from "./client";
|
|
||||||
import { DeviceTrustLevel } from "./crypto/CrossSigning";
|
import { DeviceTrustLevel } from "./crypto/CrossSigning";
|
||||||
import { ISecretStorageKeyInfo } from "./crypto/api";
|
import { ISecretStorageKeyInfo } from "./crypto/api";
|
||||||
|
|
||||||
@@ -30,7 +29,6 @@ export * from "./errors";
|
|||||||
export * from "./models/beacon";
|
export * from "./models/beacon";
|
||||||
export * from "./models/event";
|
export * from "./models/event";
|
||||||
export * from "./models/room";
|
export * from "./models/room";
|
||||||
export * from "./models/group";
|
|
||||||
export * from "./models/event-timeline";
|
export * from "./models/event-timeline";
|
||||||
export * from "./models/event-timeline-set";
|
export * from "./models/event-timeline-set";
|
||||||
export * from "./models/room-member";
|
export * from "./models/room-member";
|
||||||
@@ -154,7 +152,7 @@ export interface ICryptoCallbacks {
|
|||||||
export function createClient(opts: ICreateClientOpts | string) {
|
export function createClient(opts: ICreateClientOpts | string) {
|
||||||
if (typeof opts === "string") {
|
if (typeof opts === "string") {
|
||||||
opts = {
|
opts = {
|
||||||
"baseUrl": opts as string,
|
"baseUrl": opts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
opts.request = opts.request || requestInstance;
|
opts.request = opts.request || requestInstance;
|
||||||
|
|||||||
@@ -14,20 +14,27 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { M_BEACON_INFO } from "../@types/beacon";
|
import { MBeaconEventContent } from "../@types/beacon";
|
||||||
import { BeaconInfoState, parseBeaconInfoContent } from "../content-helpers";
|
import { M_TIMESTAMP } from "../@types/location";
|
||||||
|
import { BeaconInfoState, BeaconLocationState, parseBeaconContent, parseBeaconInfoContent } from "../content-helpers";
|
||||||
import { MatrixEvent } from "../matrix";
|
import { MatrixEvent } from "../matrix";
|
||||||
|
import { sortEventsByLatestContentTimestamp } from "../utils";
|
||||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||||
|
|
||||||
export enum BeaconEvent {
|
export enum BeaconEvent {
|
||||||
New = "Beacon.new",
|
New = "Beacon.new",
|
||||||
Update = "Beacon.update",
|
Update = "Beacon.update",
|
||||||
LivenessChange = "Beacon.LivenessChange",
|
LivenessChange = "Beacon.LivenessChange",
|
||||||
|
Destroy = "Beacon.Destroy",
|
||||||
|
LocationUpdate = "Beacon.LocationUpdate",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BeaconEventHandlerMap = {
|
export type BeaconEventHandlerMap = {
|
||||||
[BeaconEvent.Update]: (event: MatrixEvent, beacon: Beacon) => void;
|
[BeaconEvent.Update]: (event: MatrixEvent, beacon: Beacon) => void;
|
||||||
[BeaconEvent.LivenessChange]: (isLive: boolean, beacon: Beacon) => void;
|
[BeaconEvent.LivenessChange]: (isLive: boolean, beacon: Beacon) => void;
|
||||||
|
[BeaconEvent.Destroy]: (beaconIdentifier: string) => void;
|
||||||
|
[BeaconEvent.LocationUpdate]: (locationState: BeaconLocationState) => void;
|
||||||
|
[BeaconEvent.Destroy]: (beaconIdentifier: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isTimestampInDuration = (
|
export const isTimestampInDuration = (
|
||||||
@@ -36,16 +43,19 @@ export const isTimestampInDuration = (
|
|||||||
timestamp: number,
|
timestamp: number,
|
||||||
): boolean => timestamp >= startTimestamp && startTimestamp + durationMs >= timestamp;
|
): boolean => timestamp >= startTimestamp && startTimestamp + durationMs >= timestamp;
|
||||||
|
|
||||||
export const isBeaconInfoEventType = (type: string) =>
|
// beacon info events are uniquely identified by
|
||||||
type.startsWith(M_BEACON_INFO.name) ||
|
// `<roomId>_<state_key>`
|
||||||
type.startsWith(M_BEACON_INFO.altName);
|
export type BeaconIdentifier = string;
|
||||||
|
export const getBeaconInfoIdentifier = (event: MatrixEvent): BeaconIdentifier =>
|
||||||
|
`${event.getRoomId()}_${event.getStateKey()}`;
|
||||||
|
|
||||||
// https://github.com/matrix-org/matrix-spec-proposals/pull/3489
|
// https://github.com/matrix-org/matrix-spec-proposals/pull/3672
|
||||||
export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.New>, BeaconEventHandlerMap> {
|
export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.New>, BeaconEventHandlerMap> {
|
||||||
public readonly roomId: string;
|
public readonly roomId: string;
|
||||||
private _beaconInfo: BeaconInfoState;
|
private _beaconInfo: BeaconInfoState;
|
||||||
private _isLive: boolean;
|
private _isLive: boolean;
|
||||||
private livenessWatchInterval: number;
|
private livenessWatchInterval: ReturnType<typeof setInterval>;
|
||||||
|
private _latestLocationState: BeaconLocationState | undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private rootEvent: MatrixEvent,
|
private rootEvent: MatrixEvent,
|
||||||
@@ -59,6 +69,10 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
|
|||||||
return this._isLive;
|
return this._isLive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get identifier(): BeaconIdentifier {
|
||||||
|
return getBeaconInfoIdentifier(this.rootEvent);
|
||||||
|
}
|
||||||
|
|
||||||
public get beaconInfoId(): string {
|
public get beaconInfoId(): string {
|
||||||
return this.rootEvent.getId();
|
return this.rootEvent.getId();
|
||||||
}
|
}
|
||||||
@@ -75,20 +89,32 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
|
|||||||
return this._beaconInfo;
|
return this._beaconInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get latestLocationState(): BeaconLocationState | undefined {
|
||||||
|
return this._latestLocationState;
|
||||||
|
}
|
||||||
|
|
||||||
public update(beaconInfoEvent: MatrixEvent): void {
|
public update(beaconInfoEvent: MatrixEvent): void {
|
||||||
if (beaconInfoEvent.getId() !== this.beaconInfoId) {
|
if (getBeaconInfoIdentifier(beaconInfoEvent) !== this.identifier) {
|
||||||
throw new Error('Invalid updating event');
|
throw new Error('Invalid updating event');
|
||||||
}
|
}
|
||||||
|
// don't update beacon with an older event
|
||||||
|
if (beaconInfoEvent.event.origin_server_ts < this.rootEvent.event.origin_server_ts) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.rootEvent = beaconInfoEvent;
|
this.rootEvent = beaconInfoEvent;
|
||||||
this.setBeaconInfo(this.rootEvent);
|
this.setBeaconInfo(this.rootEvent);
|
||||||
|
|
||||||
this.emit(BeaconEvent.Update, beaconInfoEvent, this);
|
this.emit(BeaconEvent.Update, beaconInfoEvent, this);
|
||||||
|
this.clearLatestLocation();
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
if (this.livenessWatchInterval) {
|
if (this.livenessWatchInterval) {
|
||||||
clearInterval(this.livenessWatchInterval);
|
clearInterval(this.livenessWatchInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._isLive = false;
|
||||||
|
this.emit(BeaconEvent.Destroy, this.identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -100,14 +126,51 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
|
|||||||
clearInterval(this.livenessWatchInterval);
|
clearInterval(this.livenessWatchInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.checkLiveness();
|
||||||
if (this.isLive) {
|
if (this.isLive) {
|
||||||
const expiryInMs = (this._beaconInfo?.timestamp + this._beaconInfo?.timeout + 1) - Date.now();
|
const expiryInMs = (this._beaconInfo?.timestamp + this._beaconInfo?.timeout) - Date.now();
|
||||||
if (expiryInMs > 1) {
|
if (expiryInMs > 1) {
|
||||||
this.livenessWatchInterval = setInterval(this.checkLiveness.bind(this), expiryInMs);
|
this.livenessWatchInterval = setInterval(
|
||||||
|
() => { this.monitorLiveness(); },
|
||||||
|
expiryInMs,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process Beacon locations
|
||||||
|
* Emits BeaconEvent.LocationUpdate
|
||||||
|
*/
|
||||||
|
public addLocations(beaconLocationEvents: MatrixEvent[]): void {
|
||||||
|
// discard locations for beacons that are not live
|
||||||
|
if (!this.isLive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validLocationEvents = beaconLocationEvents.filter(event => {
|
||||||
|
const content = event.getContent<MBeaconEventContent>();
|
||||||
|
const timestamp = M_TIMESTAMP.findIn<number>(content);
|
||||||
|
return (
|
||||||
|
// only include positions that were taken inside the beacon's live period
|
||||||
|
isTimestampInDuration(this._beaconInfo.timestamp, this._beaconInfo.timeout, timestamp) &&
|
||||||
|
// ignore positions older than our current latest location
|
||||||
|
(!this.latestLocationState || timestamp > this.latestLocationState.timestamp)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const latestLocationEvent = validLocationEvents.sort(sortEventsByLatestContentTimestamp)?.[0];
|
||||||
|
|
||||||
|
if (latestLocationEvent) {
|
||||||
|
this._latestLocationState = parseBeaconContent(latestLocationEvent.getContent());
|
||||||
|
this.emit(BeaconEvent.LocationUpdate, this.latestLocationState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearLatestLocation = () => {
|
||||||
|
this._latestLocationState = undefined;
|
||||||
|
this.emit(BeaconEvent.LocationUpdate, this.latestLocationState);
|
||||||
|
};
|
||||||
|
|
||||||
private setBeaconInfo(event: MatrixEvent): void {
|
private setBeaconInfo(event: MatrixEvent): void {
|
||||||
this._beaconInfo = parseBeaconInfoContent(event.getContent());
|
this._beaconInfo = parseBeaconInfoContent(event.getContent());
|
||||||
this.checkLiveness();
|
this.checkLiveness();
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export class EventContext {
|
|||||||
*
|
*
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
constructor(ourEvent: MatrixEvent) {
|
constructor(public readonly ourEvent: MatrixEvent) {
|
||||||
this.timeline = [ourEvent];
|
this.timeline = [ourEvent];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import { EventType, RelationType } from "../@types/event";
|
|||||||
import { RoomState } from "./room-state";
|
import { RoomState } from "./room-state";
|
||||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||||
|
|
||||||
// var DEBUG = false;
|
|
||||||
const DEBUG = true;
|
const DEBUG = true;
|
||||||
|
|
||||||
let debuglog: (...args: any[]) => void;
|
let debuglog: (...args: any[]) => void;
|
||||||
@@ -775,7 +774,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getAllRelationsEventForEvent(eventId: string): MatrixEvent[] {
|
public getAllRelationsEventForEvent(eventId: string): MatrixEvent[] {
|
||||||
const relationsForEvent = this.relations[eventId] || {};
|
const relationsForEvent = this.relations?.[eventId] || {};
|
||||||
const events = [];
|
const events = [];
|
||||||
for (const relationsRecord of Object.values(relationsForEvent)) {
|
for (const relationsRecord of Object.values(relationsForEvent)) {
|
||||||
for (const relations of Object.values(relationsRecord)) {
|
for (const relations of Object.values(relationsRecord)) {
|
||||||
@@ -852,14 +851,13 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
|||||||
}
|
}
|
||||||
let relationsWithEventType = relationsWithRelType[eventType];
|
let relationsWithEventType = relationsWithRelType[eventType];
|
||||||
|
|
||||||
let relatesToEvent;
|
|
||||||
if (!relationsWithEventType) {
|
if (!relationsWithEventType) {
|
||||||
relationsWithEventType = relationsWithRelType[eventType] = new Relations(
|
relationsWithEventType = relationsWithRelType[eventType] = new Relations(
|
||||||
relationType,
|
relationType,
|
||||||
eventType,
|
eventType,
|
||||||
this.room,
|
this.room,
|
||||||
);
|
);
|
||||||
relatesToEvent = this.findEventById(relatesToEventId) || this.room.getPendingEvent(relatesToEventId);
|
const relatesToEvent = this.findEventById(relatesToEventId) || this.room.getPendingEvent(relatesToEventId);
|
||||||
if (relatesToEvent) {
|
if (relatesToEvent) {
|
||||||
relationsWithEventType.setTargetEvent(relatesToEvent);
|
relationsWithEventType.setTargetEvent(relatesToEvent);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,9 +86,17 @@ export interface IEvent {
|
|||||||
unsigned: IUnsigned;
|
unsigned: IUnsigned;
|
||||||
redacts?: string;
|
redacts?: string;
|
||||||
|
|
||||||
// v1 legacy fields
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
user_id?: string;
|
user_id?: string;
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
prev_content?: IContent;
|
prev_content?: IContent;
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
age?: number;
|
age?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,11 +119,6 @@ export interface IEventRelation {
|
|||||||
key?: string;
|
key?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IVisibilityEventRelation extends IEventRelation {
|
|
||||||
visibility: "visible" | "hidden";
|
|
||||||
reason?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When an event is a visibility change event, as per MSC3531,
|
* When an event is a visibility change event, as per MSC3531,
|
||||||
* the visibility change implied by the event.
|
* the visibility change implied by the event.
|
||||||
@@ -279,7 +282,7 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
|
|||||||
public target: RoomMember = null;
|
public target: RoomMember = null;
|
||||||
public status: EventStatus = null;
|
public status: EventStatus = null;
|
||||||
public error: MatrixError = null;
|
public error: MatrixError = null;
|
||||||
public forwardLooking = true;
|
public forwardLooking = true; // only state events may be backwards looking
|
||||||
|
|
||||||
/* If the event is a `m.key.verification.request` (or to_device `m.key.verification.start`) event,
|
/* If the event is a `m.key.verification.request` (or to_device `m.key.verification.start`) event,
|
||||||
* `Crypto` will set this the `VerificationRequest` for the event
|
* `Crypto` will set this the `VerificationRequest` for the event
|
||||||
@@ -478,7 +481,7 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
|
|||||||
*
|
*
|
||||||
* @return {Object} The event content JSON, or an empty object.
|
* @return {Object} The event content JSON, or an empty object.
|
||||||
*/
|
*/
|
||||||
public getContent<T = IContent>(): T {
|
public getContent<T extends IContent = IContent>(): T {
|
||||||
if (this._localRedactionEvent) {
|
if (this._localRedactionEvent) {
|
||||||
return {} as T;
|
return {} as T;
|
||||||
} else if (this._replacingEvent) {
|
} else if (this._replacingEvent) {
|
||||||
@@ -1040,7 +1043,7 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
|
|||||||
* caused a change in the actual visibility of this event, either by making it
|
* caused a change in the actual visibility of this event, either by making it
|
||||||
* visible (if it was hidden), by making it hidden (if it was visible) or by
|
* visible (if it was hidden), by making it hidden (if it was visible) or by
|
||||||
* changing the reason (if it was hidden).
|
* changing the reason (if it was hidden).
|
||||||
* @param visibilityEvent event holding a hide/unhide payload, or nothing
|
* @param visibilityChange event holding a hide/unhide payload, or nothing
|
||||||
* if the event is being reset to its original visibility (presumably
|
* if the event is being reset to its original visibility (presumably
|
||||||
* by a visibility event being redacted).
|
* by a visibility event being redacted).
|
||||||
*/
|
*/
|
||||||
@@ -1062,11 +1065,9 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
|
|||||||
reason: reason,
|
reason: reason,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (change) {
|
|
||||||
this.emit(MatrixEventEvent.VisibilityChange, this, visible);
|
this.emit(MatrixEventEvent.VisibilityChange, this, visible);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return instructions to display or hide the message.
|
* Return instructions to display or hide the message.
|
||||||
@@ -1109,23 +1110,21 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
|
|||||||
}
|
}
|
||||||
this.event.unsigned.redacted_because = redactionEvent.event as IEvent;
|
this.event.unsigned.redacted_because = redactionEvent.event as IEvent;
|
||||||
|
|
||||||
let key;
|
for (const key in this.event) {
|
||||||
for (key in this.event) {
|
if (this.event.hasOwnProperty(key) && !REDACT_KEEP_KEYS.has(key)) {
|
||||||
if (!this.event.hasOwnProperty(key)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!REDACT_KEEP_KEYS.has(key)) {
|
|
||||||
delete this.event[key];
|
delete this.event[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the event is encrypted prune the decrypted bits
|
||||||
|
if (this.isEncrypted()) {
|
||||||
|
this.clearEvent = null;
|
||||||
|
}
|
||||||
|
|
||||||
const keeps = REDACT_KEEP_CONTENT_MAP[this.getType()] || {};
|
const keeps = REDACT_KEEP_CONTENT_MAP[this.getType()] || {};
|
||||||
const content = this.getContent();
|
const content = this.getContent();
|
||||||
for (key in content) {
|
for (const key in content) {
|
||||||
if (!content.hasOwnProperty(key)) {
|
if (content.hasOwnProperty(key) && !keeps[key]) {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!keeps[key]) {
|
|
||||||
delete content[key];
|
delete content[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1291,7 +1290,7 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get whether the event is a relation event, and of a given type if
|
* 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
|
* @param {string?} relType if given, checks that the relation is of the
|
||||||
* given type
|
* given type
|
||||||
@@ -1300,10 +1299,12 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
|
|||||||
public isRelation(relType: string = undefined): boolean {
|
public isRelation(relType: string = undefined): boolean {
|
||||||
// Relation info is lifted out of the encrypted content when sent to
|
// Relation info is lifted out of the encrypted content when sent to
|
||||||
// encrypted rooms, so we have to check `getWireContent` for this.
|
// encrypted rooms, so we have to check `getWireContent` for this.
|
||||||
const content = this.getWireContent();
|
const relation = this.getWireContent()?.["m.relates_to"];
|
||||||
const relation = content && content["m.relates_to"];
|
if (this.isState() && relation?.rel_type === RelationType.Replace) {
|
||||||
return relation && relation.rel_type && relation.event_id &&
|
// State events cannot be m.replace relations
|
||||||
((relType && relation.rel_type === relType) || !relType);
|
return false;
|
||||||
|
}
|
||||||
|
return relation?.rel_type && relation.event_id && (relType ? relation.rel_type === relType : true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1333,6 +1334,10 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
|
|||||||
if (this.isRedacted() && newEvent) {
|
if (this.isRedacted() && newEvent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// don't allow state events to be replaced using this mechanism as per MSC2676
|
||||||
|
if (this.isState()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this._replacingEvent !== newEvent) {
|
if (this._replacingEvent !== newEvent) {
|
||||||
this._replacingEvent = newEvent;
|
this._replacingEvent = newEvent;
|
||||||
this.emit(MatrixEventEvent.Replaced, this);
|
this.emit(MatrixEventEvent.Replaced, this);
|
||||||
@@ -1583,7 +1588,7 @@ const REDACT_KEEP_KEYS = new Set([
|
|||||||
'content', 'unsigned', 'origin_server_ts',
|
'content', 'unsigned', 'origin_server_ts',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// a map from event type to the .content keys we keep when an event is redacted
|
// a map from state event type to the .content keys we keep when an event is redacted
|
||||||
const REDACT_KEEP_CONTENT_MAP = {
|
const REDACT_KEEP_CONTENT_MAP = {
|
||||||
[EventType.RoomMember]: { 'membership': 1 },
|
[EventType.RoomMember]: { 'membership': 1 },
|
||||||
[EventType.RoomCreate]: { 'creator': 1 },
|
[EventType.RoomCreate]: { 'creator': 1 },
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2017 New Vector Ltd
|
|
||||||
Copyright 2019 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module models/group
|
|
||||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-imports
|
|
||||||
import { EventEmitter } from "events";
|
|
||||||
|
|
||||||
import * as utils from "../utils";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct a new Group.
|
|
||||||
*
|
|
||||||
* @param {string} groupId The ID of this group.
|
|
||||||
*
|
|
||||||
* @prop {string} groupId The ID of this group.
|
|
||||||
* @prop {string} name The human-readable display name for this group.
|
|
||||||
* @prop {string} avatarUrl The mxc URL for this group's avatar.
|
|
||||||
* @prop {string} myMembership The logged in user's membership of this group
|
|
||||||
* @prop {Object} inviter Infomation about the user who invited the logged in user
|
|
||||||
* to the group, if myMembership is 'invite'.
|
|
||||||
* @prop {string} inviter.userId The user ID of the inviter
|
|
||||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
|
||||||
*/
|
|
||||||
export function Group(groupId) {
|
|
||||||
this.groupId = groupId;
|
|
||||||
this.name = null;
|
|
||||||
this.avatarUrl = null;
|
|
||||||
this.myMembership = null;
|
|
||||||
this.inviter = null;
|
|
||||||
}
|
|
||||||
utils.inherits(Group, EventEmitter);
|
|
||||||
|
|
||||||
Group.prototype.setProfile = function(name, avatarUrl) {
|
|
||||||
if (this.name === name && this.avatarUrl === avatarUrl) return;
|
|
||||||
|
|
||||||
this.name = name || this.groupId;
|
|
||||||
this.avatarUrl = avatarUrl;
|
|
||||||
|
|
||||||
this.emit("Group.profile", this);
|
|
||||||
};
|
|
||||||
|
|
||||||
Group.prototype.setMyMembership = function(membership) {
|
|
||||||
if (this.myMembership === membership) return;
|
|
||||||
|
|
||||||
this.myMembership = membership;
|
|
||||||
|
|
||||||
this.emit("Group.myMembership", this);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the 'inviter' property. This does not emit an event (the inviter
|
|
||||||
* will only change when the user is revited / reinvited to a room),
|
|
||||||
* so set this before setting myMembership.
|
|
||||||
* @param {Object} inviter Infomation about who invited us to the room
|
|
||||||
*/
|
|
||||||
Group.prototype.setInviter = function(inviter) {
|
|
||||||
this.inviter = inviter;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires whenever a group's profile information is updated.
|
|
||||||
* This means the 'name' and 'avatarUrl' properties.
|
|
||||||
* @event module:client~MatrixClient#"Group.profile"
|
|
||||||
* @param {Group} group The group whose profile was updated.
|
|
||||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
|
||||||
* @example
|
|
||||||
* matrixClient.on("Group.profile", function(group){
|
|
||||||
* var name = group.name;
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires whenever the logged in user's membership status of
|
|
||||||
* the group is updated.
|
|
||||||
* @event module:client~MatrixClient#"Group.myMembership"
|
|
||||||
* @param {Group} group The group in which the user's membership changed
|
|
||||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
|
||||||
* @example
|
|
||||||
* matrixClient.on("Group.myMembership", function(group){
|
|
||||||
* var myMembership = group.myMembership;
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
@@ -103,7 +103,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
|
|||||||
|
|
||||||
if (this.relationType === RelationType.Annotation) {
|
if (this.relationType === RelationType.Annotation) {
|
||||||
this.addAnnotationToAggregation(event);
|
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();
|
const lastReplacement = await this.getLastReplacement();
|
||||||
this.targetEvent.makeReplaced(lastReplacement);
|
this.targetEvent.makeReplaced(lastReplacement);
|
||||||
}
|
}
|
||||||
@@ -144,7 +144,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
|
|||||||
|
|
||||||
if (this.relationType === RelationType.Annotation) {
|
if (this.relationType === RelationType.Annotation) {
|
||||||
this.removeAnnotationFromAggregation(event);
|
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();
|
const lastReplacement = await this.getLastReplacement();
|
||||||
this.targetEvent.makeReplaced(lastReplacement);
|
this.targetEvent.makeReplaced(lastReplacement);
|
||||||
}
|
}
|
||||||
@@ -261,7 +261,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
|
|||||||
if (this.relationType === RelationType.Annotation) {
|
if (this.relationType === RelationType.Annotation) {
|
||||||
// Remove the redacted annotation from aggregation by key
|
// Remove the redacted annotation from aggregation by key
|
||||||
this.removeAnnotationFromAggregation(redactedEvent);
|
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();
|
const lastReplacement = await this.getLastReplacement();
|
||||||
this.targetEvent.makeReplaced(lastReplacement);
|
this.targetEvent.makeReplaced(lastReplacement);
|
||||||
}
|
}
|
||||||
@@ -331,7 +331,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
|
|||||||
// the all-knowning server tells us that the event at some point had
|
// the all-knowning server tells us that the event at some point had
|
||||||
// this timestamp for its replacement, so any following replacement should definitely not be less
|
// this timestamp for its replacement, so any following replacement should definitely not be less
|
||||||
const replaceRelation = this.targetEvent.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
|
const replaceRelation = this.targetEvent.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
|
||||||
const minTs = replaceRelation && replaceRelation.origin_server_ts;
|
const minTs = replaceRelation?.origin_server_ts;
|
||||||
|
|
||||||
const lastReplacement = this.getRelations().reduce((last, event) => {
|
const lastReplacement = this.getRelations().reduce((last, event) => {
|
||||||
if (event.getSender() !== this.targetEvent.getSender()) {
|
if (event.getSender() !== this.targetEvent.getSender()) {
|
||||||
@@ -364,7 +364,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
|
|||||||
}
|
}
|
||||||
this.targetEvent = event;
|
this.targetEvent = event;
|
||||||
|
|
||||||
if (this.relationType === RelationType.Replace) {
|
if (this.relationType === RelationType.Replace && !this.targetEvent.isState()) {
|
||||||
const replacement = await this.getLastReplacement();
|
const replacement = await this.getLastReplacement();
|
||||||
// this is the initial update, so only call it if we already have something
|
// this is the initial update, so only call it if we already have something
|
||||||
// to not emit Event.replaced needlessly
|
// to not emit Event.replaced needlessly
|
||||||
|
|||||||
@@ -22,12 +22,13 @@ import { RoomMember } from "./room-member";
|
|||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import * as utils from "../utils";
|
import * as utils from "../utils";
|
||||||
import { EventType } from "../@types/event";
|
import { EventType } from "../@types/event";
|
||||||
import { MatrixEvent } from "./event";
|
import { MatrixEvent, MatrixEventEvent } from "./event";
|
||||||
import { MatrixClient } from "../client";
|
import { MatrixClient } from "../client";
|
||||||
import { GuestAccess, HistoryVisibility, IJoinRuleEventContent, JoinRule } from "../@types/partials";
|
import { GuestAccess, HistoryVisibility, IJoinRuleEventContent, JoinRule } from "../@types/partials";
|
||||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||||
import { Beacon, BeaconEvent, isBeaconInfoEventType, BeaconEventHandlerMap } from "./beacon";
|
import { Beacon, BeaconEvent, BeaconEventHandlerMap, getBeaconInfoIdentifier, BeaconIdentifier } from "./beacon";
|
||||||
import { TypedReEmitter } from "../ReEmitter";
|
import { TypedReEmitter } from "../ReEmitter";
|
||||||
|
import { M_BEACON, M_BEACON_INFO } from "../@types/beacon";
|
||||||
|
|
||||||
// possible statuses for out-of-band member loading
|
// possible statuses for out-of-band member loading
|
||||||
enum OobStatus {
|
enum OobStatus {
|
||||||
@@ -80,8 +81,8 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
|||||||
public events = new Map<string, Map<string, MatrixEvent>>(); // Map<eventType, Map<stateKey, MatrixEvent>>
|
public events = new Map<string, Map<string, MatrixEvent>>(); // Map<eventType, Map<stateKey, MatrixEvent>>
|
||||||
public paginationToken: string = null;
|
public paginationToken: string = null;
|
||||||
|
|
||||||
public readonly beacons = new Map<string, Beacon>();
|
public readonly beacons = new Map<BeaconIdentifier, Beacon>();
|
||||||
private liveBeaconIds: string[] = [];
|
private _liveBeaconIds: BeaconIdentifier[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct room state.
|
* Construct room state.
|
||||||
@@ -248,6 +249,10 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
|||||||
return !!this.liveBeaconIds?.length;
|
return !!this.liveBeaconIds?.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get liveBeaconIds(): BeaconIdentifier[] {
|
||||||
|
return this._liveBeaconIds;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a copy of this room state so that mutations to either won't affect the other.
|
* Creates a copy of this room state so that mutations to either won't affect the other.
|
||||||
* @return {RoomState} the copy of the room state
|
* @return {RoomState} the copy of the room state
|
||||||
@@ -330,7 +335,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBeaconInfoEventType(event.getType())) {
|
if (M_BEACON_INFO.matches(event.getType())) {
|
||||||
this.setBeacon(event);
|
this.setBeacon(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,6 +409,51 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
|||||||
this.emit(RoomStateEvent.Update, this);
|
this.emit(RoomStateEvent.Update, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public processBeaconEvents(events: MatrixEvent[], matrixClient: MatrixClient): void {
|
||||||
|
if (
|
||||||
|
!events.length ||
|
||||||
|
// discard locations if we have no beacons
|
||||||
|
!this.beacons.size
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const beaconByEventIdDict: Record<string, Beacon> =
|
||||||
|
[...this.beacons.values()].reduce((dict, beacon) => ({ ...dict, [beacon.beaconInfoId]: beacon }), {});
|
||||||
|
|
||||||
|
const processBeaconRelation = (beaconInfoEventId: string, event: MatrixEvent): void => {
|
||||||
|
if (!M_BEACON.matches(event.getType())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const beacon = beaconByEventIdDict[beaconInfoEventId];
|
||||||
|
|
||||||
|
if (beacon) {
|
||||||
|
beacon.addLocations([event]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
events.forEach((event: MatrixEvent) => {
|
||||||
|
const relatedToEventId = event.getRelation()?.event_id;
|
||||||
|
// not related to a beacon we know about
|
||||||
|
// discard
|
||||||
|
if (!beaconByEventIdDict[relatedToEventId]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
matrixClient.decryptEventIfNeeded(event);
|
||||||
|
|
||||||
|
if (event.isBeingDecrypted() || event.isDecryptionFailure()) {
|
||||||
|
// add an event listener for once the event is decrypted.
|
||||||
|
event.once(MatrixEventEvent.Decrypted, async () => {
|
||||||
|
processBeaconRelation(relatedToEventId, event);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
processBeaconRelation(relatedToEventId, event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Looks up a member by the given userId, and if it doesn't exist,
|
* Looks up a member by the given userId, and if it doesn't exist,
|
||||||
* create it and emit the `RoomState.newMember` event.
|
* create it and emit the `RoomState.newMember` event.
|
||||||
@@ -437,8 +487,24 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
|||||||
* @experimental
|
* @experimental
|
||||||
*/
|
*/
|
||||||
private setBeacon(event: MatrixEvent): void {
|
private setBeacon(event: MatrixEvent): void {
|
||||||
if (this.beacons.has(event.getId())) {
|
const beaconIdentifier = getBeaconInfoIdentifier(event);
|
||||||
return this.beacons.get(event.getId()).update(event);
|
|
||||||
|
if (this.beacons.has(beaconIdentifier)) {
|
||||||
|
const beacon = this.beacons.get(beaconIdentifier);
|
||||||
|
|
||||||
|
if (event.isRedacted()) {
|
||||||
|
if (beacon.beaconInfoId === event.getRedactionEvent()?.['redacts']) {
|
||||||
|
beacon.destroy();
|
||||||
|
this.beacons.delete(beaconIdentifier);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return beacon.update(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.isRedacted()) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const beacon = new Beacon(event);
|
const beacon = new Beacon(event);
|
||||||
@@ -446,30 +512,28 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
|||||||
this.reEmitter.reEmit<BeaconEvent, BeaconEvent>(beacon, [
|
this.reEmitter.reEmit<BeaconEvent, BeaconEvent>(beacon, [
|
||||||
BeaconEvent.New,
|
BeaconEvent.New,
|
||||||
BeaconEvent.Update,
|
BeaconEvent.Update,
|
||||||
|
BeaconEvent.Destroy,
|
||||||
BeaconEvent.LivenessChange,
|
BeaconEvent.LivenessChange,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.emit(BeaconEvent.New, event, beacon);
|
this.emit(BeaconEvent.New, event, beacon);
|
||||||
beacon.on(BeaconEvent.LivenessChange, this.onBeaconLivenessChange.bind(this));
|
beacon.on(BeaconEvent.LivenessChange, this.onBeaconLivenessChange.bind(this));
|
||||||
this.beacons.set(beacon.beaconInfoId, beacon);
|
beacon.on(BeaconEvent.Destroy, this.onBeaconLivenessChange.bind(this));
|
||||||
|
|
||||||
|
this.beacons.set(beacon.identifier, beacon);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @experimental
|
* @experimental
|
||||||
* Check liveness of room beacons
|
* Check liveness of room beacons
|
||||||
* emit RoomStateEvent.BeaconLiveness when
|
* emit RoomStateEvent.BeaconLiveness event
|
||||||
* roomstate.hasLiveBeacons has changed
|
|
||||||
*/
|
*/
|
||||||
private onBeaconLivenessChange(): void {
|
private onBeaconLivenessChange(): void {
|
||||||
const prevHasLiveBeacons = !!this.liveBeaconIds?.length;
|
this._liveBeaconIds = Array.from(this.beacons.values())
|
||||||
this.liveBeaconIds = Array.from(this.beacons.values())
|
|
||||||
.filter(beacon => beacon.isLive)
|
.filter(beacon => beacon.isLive)
|
||||||
.map(beacon => beacon.beaconInfoId);
|
.map(beacon => beacon.identifier);
|
||||||
|
|
||||||
const hasLiveBeacons = !!this.liveBeaconIds.length;
|
this.emit(RoomStateEvent.BeaconLiveness, this, this.hasLiveBeacons);
|
||||||
if (prevHasLiveBeacons !== hasLiveBeacons) {
|
|
||||||
this.emit(RoomStateEvent.BeaconLiveness, this, hasLiveBeacons);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getStateEventMatching(event: MatrixEvent): MatrixEvent | null {
|
private getStateEventMatching(event: MatrixEvent): MatrixEvent | null {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { Direction, EventTimeline } from "./event-timeline";
|
|||||||
import { getHttpUriForMxc } from "../content-repo";
|
import { getHttpUriForMxc } from "../content-repo";
|
||||||
import * as utils from "../utils";
|
import * as utils from "../utils";
|
||||||
import { normalize } from "../utils";
|
import { normalize } from "../utils";
|
||||||
import { IEvent, MatrixEvent } from "./event";
|
import { IEvent, IThreadBundledRelationship, MatrixEvent, MatrixEventEvent, MatrixEventHandlerMap } from "./event";
|
||||||
import { EventStatus } from "./event-status";
|
import { EventStatus } from "./event-status";
|
||||||
import { RoomMember } from "./room-member";
|
import { RoomMember } from "./room-member";
|
||||||
import { IRoomSummary, RoomSummary } from "./room-summary";
|
import { IRoomSummary, RoomSummary } from "./room-summary";
|
||||||
@@ -32,6 +32,7 @@ import { TypedReEmitter } from '../ReEmitter';
|
|||||||
import {
|
import {
|
||||||
EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS,
|
EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS,
|
||||||
EVENT_VISIBILITY_CHANGE_TYPE,
|
EVENT_VISIBILITY_CHANGE_TYPE,
|
||||||
|
RelationType,
|
||||||
} from "../@types/event";
|
} from "../@types/event";
|
||||||
import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
|
import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
|
||||||
import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials";
|
import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials";
|
||||||
@@ -45,8 +46,9 @@ import {
|
|||||||
FILTER_RELATED_BY_SENDERS,
|
FILTER_RELATED_BY_SENDERS,
|
||||||
ThreadFilterType,
|
ThreadFilterType,
|
||||||
} from "./thread";
|
} from "./thread";
|
||||||
import { Method } from "../http-api";
|
|
||||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||||
|
import { ReceiptType } from "../@types/read_receipts";
|
||||||
|
import { IStateEventWithRoomId } from "../@types/search";
|
||||||
|
|
||||||
// These constants are used as sane defaults when the homeserver doesn't support
|
// These constants are used as sane defaults when the homeserver doesn't support
|
||||||
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
|
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
|
||||||
@@ -54,10 +56,10 @@ import { TypedEventEmitter } from "./typed-event-emitter";
|
|||||||
// room versions which are considered okay for people to run without being asked
|
// room versions which are considered okay for people to run without being asked
|
||||||
// to upgrade (ie: "stable"). Eventually, we should remove these when all homeservers
|
// to upgrade (ie: "stable"). Eventually, we should remove these when all homeservers
|
||||||
// return an m.room_versions capability.
|
// return an m.room_versions capability.
|
||||||
const KNOWN_SAFE_ROOM_VERSION = '6';
|
const KNOWN_SAFE_ROOM_VERSION = '9';
|
||||||
const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4', '5', '6'];
|
const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||||
|
|
||||||
function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: string): MatrixEvent {
|
function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: ReceiptType): MatrixEvent {
|
||||||
// console.log("synthesizing receipt for "+event.getId());
|
// console.log("synthesizing receipt for "+event.getId());
|
||||||
return new MatrixEvent({
|
return new MatrixEvent({
|
||||||
content: {
|
content: {
|
||||||
@@ -92,13 +94,13 @@ interface IReceipt {
|
|||||||
ts: number;
|
ts: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IWrappedReceipt {
|
export interface IWrappedReceipt {
|
||||||
eventId: string;
|
eventId: string;
|
||||||
data: IReceipt;
|
data: IReceipt;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ICachedReceipt {
|
interface ICachedReceipt {
|
||||||
type: string;
|
type: ReceiptType;
|
||||||
userId: string;
|
userId: string;
|
||||||
data: IReceipt;
|
data: IReceipt;
|
||||||
}
|
}
|
||||||
@@ -107,7 +109,7 @@ type ReceiptCache = {[eventId: string]: ICachedReceipt[]};
|
|||||||
|
|
||||||
interface IReceiptContent {
|
interface IReceiptContent {
|
||||||
[eventId: string]: {
|
[eventId: string]: {
|
||||||
[type: string]: {
|
[key in ReceiptType]: {
|
||||||
[userId: string]: IReceipt;
|
[userId: string]: IReceipt;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -148,6 +150,7 @@ export interface ICreateFilterOpts {
|
|||||||
// timeline. Useful to disable for some filters that can't be achieved by the
|
// timeline. Useful to disable for some filters that can't be achieved by the
|
||||||
// client in an efficient manner
|
// client in an efficient manner
|
||||||
prepopulateTimeline?: boolean;
|
prepopulateTimeline?: boolean;
|
||||||
|
useSyncEvents?: boolean;
|
||||||
pendingEvents?: boolean;
|
pendingEvents?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,8 +170,10 @@ export enum RoomEvent {
|
|||||||
type EmittedEvents = RoomEvent
|
type EmittedEvents = RoomEvent
|
||||||
| ThreadEvent.New
|
| ThreadEvent.New
|
||||||
| ThreadEvent.Update
|
| ThreadEvent.Update
|
||||||
|
| ThreadEvent.NewReply
|
||||||
| RoomEvent.Timeline
|
| RoomEvent.Timeline
|
||||||
| RoomEvent.TimelineReset;
|
| RoomEvent.TimelineReset
|
||||||
|
| MatrixEventEvent.BeforeRedaction;
|
||||||
|
|
||||||
export type RoomEventHandlerMap = {
|
export type RoomEventHandlerMap = {
|
||||||
[RoomEvent.MyMembership]: (room: Room, membership: string, prevMembership?: string) => void;
|
[RoomEvent.MyMembership]: (room: Room, membership: string, prevMembership?: string) => void;
|
||||||
@@ -185,10 +190,10 @@ export type RoomEventHandlerMap = {
|
|||||||
oldStatus?: EventStatus,
|
oldStatus?: EventStatus,
|
||||||
) => void;
|
) => void;
|
||||||
[ThreadEvent.New]: (thread: Thread, toStartOfTimeline: boolean) => void;
|
[ThreadEvent.New]: (thread: Thread, toStartOfTimeline: boolean) => void;
|
||||||
} & ThreadHandlerMap;
|
} & ThreadHandlerMap & MatrixEventHandlerMap;
|
||||||
|
|
||||||
export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap> {
|
export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap> {
|
||||||
private readonly reEmitter: TypedReEmitter<EmittedEvents, RoomEventHandlerMap>;
|
public readonly reEmitter: TypedReEmitter<EmittedEvents, RoomEventHandlerMap>;
|
||||||
private txnToEvent: Record<string, MatrixEvent> = {}; // Pending in-flight requests { string: MatrixEvent }
|
private txnToEvent: Record<string, MatrixEvent> = {}; // Pending in-flight requests { string: MatrixEvent }
|
||||||
// receipts should clobber based on receipt_type and user_id pairs hence
|
// receipts should clobber based on receipt_type and user_id pairs hence
|
||||||
// the form of this structure. This is sub-optimal for the exposed APIs
|
// the form of this structure. This is sub-optimal for the exposed APIs
|
||||||
@@ -260,7 +265,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
/**
|
/**
|
||||||
* @experimental
|
* @experimental
|
||||||
*/
|
*/
|
||||||
public threads = new Map<string, Thread>();
|
private threads = new Map<string, Thread>();
|
||||||
public lastThread: Thread;
|
public lastThread: Thread;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -346,15 +351,6 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
RoomEvent.TimelineReset,
|
RoomEvent.TimelineReset,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (this.client?.supportsExperimentalThreads) {
|
|
||||||
Promise.all([
|
|
||||||
this.createThreadTimelineSet(),
|
|
||||||
this.createThreadTimelineSet(ThreadFilterType.My),
|
|
||||||
]).then((timelineSets) => {
|
|
||||||
this.threadsTimelineSets.push(...timelineSets);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.fixUpLegacyTimelineFields();
|
this.fixUpLegacyTimelineFields();
|
||||||
|
|
||||||
if (this.opts.pendingEventOrdering === PendingEventOrdering.Detached) {
|
if (this.opts.pendingEventOrdering === PendingEventOrdering.Detached) {
|
||||||
@@ -381,6 +377,26 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private threadTimelineSetsPromise: Promise<[EventTimelineSet, EventTimelineSet]> | null = null;
|
||||||
|
public async createThreadsTimelineSets(): Promise<[EventTimelineSet, EventTimelineSet]> {
|
||||||
|
if (this.threadTimelineSetsPromise) {
|
||||||
|
return this.threadTimelineSetsPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.client?.supportsExperimentalThreads()) {
|
||||||
|
try {
|
||||||
|
this.threadTimelineSetsPromise = Promise.all([
|
||||||
|
this.createThreadTimelineSet(),
|
||||||
|
this.createThreadTimelineSet(ThreadFilterType.My),
|
||||||
|
]);
|
||||||
|
const timelineSets = await this.threadTimelineSetsPromise;
|
||||||
|
this.threadsTimelineSets.push(...timelineSets);
|
||||||
|
} catch (e) {
|
||||||
|
this.threadTimelineSetsPromise = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bulk decrypt critical events in a room
|
* Bulk decrypt critical events in a room
|
||||||
*
|
*
|
||||||
@@ -771,16 +787,9 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadMembersFromServer(): Promise<IEvent[]> {
|
private async loadMembersFromServer(): Promise<IStateEventWithRoomId[]> {
|
||||||
const lastSyncToken = this.client.store.getSyncToken();
|
const lastSyncToken = this.client.store.getSyncToken();
|
||||||
const queryString = utils.encodeParams({
|
const response = await this.client.members(this.roomId, undefined, "leave", lastSyncToken);
|
||||||
not_membership: "leave",
|
|
||||||
at: lastSyncToken,
|
|
||||||
});
|
|
||||||
const path = utils.encodeUri("/rooms/$roomId/members?" + queryString,
|
|
||||||
{ $roomId: this.roomId });
|
|
||||||
const http = this.client.http;
|
|
||||||
const response = await http.authedRequest<{ chunk: IEvent[] }>(undefined, Method.Get, path);
|
|
||||||
return response.chunk;
|
return response.chunk;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -794,7 +803,8 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
// fails), since loadMembersIfNeeded always returns this.membersPromise
|
// fails), since loadMembersIfNeeded always returns this.membersPromise
|
||||||
// if set, which will be the result of the first (successful) call.
|
// if set, which will be the result of the first (successful) call.
|
||||||
if (rawMembersEvents === null ||
|
if (rawMembersEvents === null ||
|
||||||
(this.client.isCryptoEnabled() && this.client.isRoomEncrypted(this.roomId))) {
|
(this.client.isCryptoEnabled() && this.client.isRoomEncrypted(this.roomId))
|
||||||
|
) {
|
||||||
fromServer = true;
|
fromServer = true;
|
||||||
rawMembersEvents = await this.loadMembersFromServer();
|
rawMembersEvents = await this.loadMembersFromServer();
|
||||||
logger.log(`LL: got ${rawMembersEvents.length} ` +
|
logger.log(`LL: got ${rawMembersEvents.length} ` +
|
||||||
@@ -840,7 +850,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
if (fromServer) {
|
if (fromServer) {
|
||||||
const oobMembers = this.currentState.getMembers()
|
const oobMembers = this.currentState.getMembers()
|
||||||
.filter((m) => m.isOutOfBand())
|
.filter((m) => m.isOutOfBand())
|
||||||
.map((m) => m.events.member.event as IEvent);
|
.map((m) => m.events.member.event as IStateEventWithRoomId);
|
||||||
logger.log(`LL: telling store to write ${oobMembers.length}`
|
logger.log(`LL: telling store to write ${oobMembers.length}`
|
||||||
+ ` members for room ${this.roomId}`);
|
+ ` members for room ${this.roomId}`);
|
||||||
const store = this.client.store;
|
const store = this.client.store;
|
||||||
@@ -991,7 +1001,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an event which is stored in our unfiltered timeline set or in a thread
|
* Get an event which is stored in our unfiltered timeline set, or in a thread
|
||||||
*
|
*
|
||||||
* @param {string} eventId event ID to look for
|
* @param {string} eventId event ID to look for
|
||||||
* @return {?module:models/event.MatrixEvent} the given event, or undefined if unknown
|
* @return {?module:models/event.MatrixEvent} the given event, or undefined if unknown
|
||||||
@@ -999,9 +1009,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
public findEventById(eventId: string): MatrixEvent | undefined {
|
public findEventById(eventId: string): MatrixEvent | undefined {
|
||||||
let event = this.getUnfilteredTimelineSet().findEventById(eventId);
|
let event = this.getUnfilteredTimelineSet().findEventById(eventId);
|
||||||
|
|
||||||
if (event) {
|
if (!event) {
|
||||||
return event;
|
|
||||||
} else {
|
|
||||||
const threads = this.getThreads();
|
const threads = this.getThreads();
|
||||||
for (let i = 0; i < threads.length; i++) {
|
for (let i = 0; i < threads.length; i++) {
|
||||||
const thread = threads[i];
|
const thread = threads[i];
|
||||||
@@ -1011,6 +1019,8 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1115,14 +1125,14 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
* The aliases returned by this function may not necessarily
|
* The aliases returned by this function may not necessarily
|
||||||
* still point to this room.
|
* still point to this room.
|
||||||
* @return {array} The room's alias as an array of strings
|
* @return {array} The room's alias as an array of strings
|
||||||
|
* @deprecated this uses m.room.aliases events, replaced by Room::getAltAliases()
|
||||||
*/
|
*/
|
||||||
public getAliases(): string[] {
|
public getAliases(): string[] {
|
||||||
const aliasStrings: string[] = [];
|
const aliasStrings: string[] = [];
|
||||||
|
|
||||||
const aliasEvents = this.currentState.getStateEvents(EventType.RoomAliases);
|
const aliasEvents = this.currentState.getStateEvents(EventType.RoomAliases);
|
||||||
if (aliasEvents) {
|
if (aliasEvents) {
|
||||||
for (let i = 0; i < aliasEvents.length; ++i) {
|
for (const aliasEvent of aliasEvents) {
|
||||||
const aliasEvent = aliasEvents[i];
|
|
||||||
if (Array.isArray(aliasEvent.getContent().aliases)) {
|
if (Array.isArray(aliasEvent.getContent().aliases)) {
|
||||||
const filteredAliases = aliasEvent.getContent<{ aliases: string[] }>().aliases.filter(a => {
|
const filteredAliases = aliasEvent.getContent<{ aliases: string[] }>().aliases.filter(a => {
|
||||||
if (typeof(a) !== "string") return false;
|
if (typeof(a) !== "string") return false;
|
||||||
@@ -1132,7 +1142,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
// It's probably valid by here.
|
// It's probably valid by here.
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
Array.prototype.push.apply(aliasStrings, filteredAliases);
|
aliasStrings.push(...filteredAliases);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1190,19 +1200,14 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
timeline: EventTimeline,
|
timeline: EventTimeline,
|
||||||
paginationToken?: string,
|
paginationToken?: string,
|
||||||
): void {
|
): void {
|
||||||
timeline.getTimelineSet().addEventsToTimeline(
|
timeline.getTimelineSet().addEventsToTimeline(events, toStartOfTimeline, timeline, paginationToken);
|
||||||
events, toStartOfTimeline,
|
|
||||||
timeline, paginationToken,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @experimental
|
* @experimental
|
||||||
*/
|
*/
|
||||||
public getThread(eventId: string): Thread {
|
public getThread(eventId: string): Thread {
|
||||||
return this.getThreads().find(thread => {
|
return this.threads.get(eventId);
|
||||||
return thread.id === eventId;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1335,6 +1340,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
filter: Filter,
|
filter: Filter,
|
||||||
{
|
{
|
||||||
prepopulateTimeline = true,
|
prepopulateTimeline = true,
|
||||||
|
useSyncEvents = true,
|
||||||
pendingEvents = true,
|
pendingEvents = true,
|
||||||
}: ICreateFilterOpts = {},
|
}: ICreateFilterOpts = {},
|
||||||
): EventTimelineSet {
|
): EventTimelineSet {
|
||||||
@@ -1347,8 +1353,10 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
RoomEvent.Timeline,
|
RoomEvent.Timeline,
|
||||||
RoomEvent.TimelineReset,
|
RoomEvent.TimelineReset,
|
||||||
]);
|
]);
|
||||||
|
if (useSyncEvents) {
|
||||||
this.filteredTimelineSets[filter.filterId] = timelineSet;
|
this.filteredTimelineSets[filter.filterId] = timelineSet;
|
||||||
this.timelineSets.push(timelineSet);
|
this.timelineSets.push(timelineSet);
|
||||||
|
}
|
||||||
|
|
||||||
const unfilteredLiveTimeline = this.getLiveTimeline();
|
const unfilteredLiveTimeline = this.getLiveTimeline();
|
||||||
// Not all filter are possible to replicate client-side only
|
// Not all filter are possible to replicate client-side only
|
||||||
@@ -1376,7 +1384,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
timeline.getPaginationToken(EventTimeline.BACKWARDS),
|
timeline.getPaginationToken(EventTimeline.BACKWARDS),
|
||||||
EventTimeline.BACKWARDS,
|
EventTimeline.BACKWARDS,
|
||||||
);
|
);
|
||||||
} else {
|
} else if (useSyncEvents) {
|
||||||
const livePaginationToken = unfilteredLiveTimeline.getPaginationToken(Direction.Forward);
|
const livePaginationToken = unfilteredLiveTimeline.getPaginationToken(Direction.Forward);
|
||||||
timelineSet
|
timelineSet
|
||||||
.getLiveTimeline()
|
.getLiveTimeline()
|
||||||
@@ -1394,9 +1402,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
return timelineSet;
|
return timelineSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createThreadTimelineSet(filterType?: ThreadFilterType): Promise<EventTimelineSet> {
|
private async getThreadListFilter(filterType = ThreadFilterType.All): Promise<Filter> {
|
||||||
let timelineSet: EventTimelineSet;
|
|
||||||
if (Thread.hasServerSideSupport) {
|
|
||||||
const myUserId = this.client.getUserId();
|
const myUserId = this.client.getUserId();
|
||||||
const filter = new Filter(myUserId);
|
const filter = new Filter(myUserId);
|
||||||
|
|
||||||
@@ -1417,18 +1423,25 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
`THREAD_PANEL_${this.roomId}_${filterType}`,
|
`THREAD_PANEL_${this.roomId}_${filterType}`,
|
||||||
filter,
|
filter,
|
||||||
);
|
);
|
||||||
|
|
||||||
filter.filterId = filterId;
|
filter.filterId = filterId;
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createThreadTimelineSet(filterType?: ThreadFilterType): Promise<EventTimelineSet> {
|
||||||
|
let timelineSet: EventTimelineSet;
|
||||||
|
if (Thread.hasServerSideSupport) {
|
||||||
|
const filter = await this.getThreadListFilter(filterType);
|
||||||
|
|
||||||
timelineSet = this.getOrCreateFilteredTimelineSet(
|
timelineSet = this.getOrCreateFilteredTimelineSet(
|
||||||
filter,
|
filter,
|
||||||
{
|
{
|
||||||
prepopulateTimeline: false,
|
prepopulateTimeline: false,
|
||||||
|
useSyncEvents: false,
|
||||||
pendingEvents: false,
|
pendingEvents: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// An empty pagination token allows to paginate from the very bottom of
|
|
||||||
// the timeline set.
|
|
||||||
timelineSet.getLiveTimeline().setPaginationToken("", EventTimeline.BACKWARDS);
|
|
||||||
} else {
|
} else {
|
||||||
timelineSet = new EventTimelineSet(this, {
|
timelineSet = new EventTimelineSet(this, {
|
||||||
pendingEvents: false,
|
pendingEvents: false,
|
||||||
@@ -1449,6 +1462,86 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
return timelineSet;
|
return timelineSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public threadsReady = false;
|
||||||
|
|
||||||
|
public async fetchRoomThreads(): Promise<void> {
|
||||||
|
if (this.threadsReady || !this.client.supportsExperimentalThreads()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allThreadsFilter = await this.getThreadListFilter();
|
||||||
|
|
||||||
|
const { chunk: events } = await this.client.createMessagesRequest(
|
||||||
|
this.roomId,
|
||||||
|
"",
|
||||||
|
Number.MAX_SAFE_INTEGER,
|
||||||
|
Direction.Backward,
|
||||||
|
allThreadsFilter,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!events.length) return;
|
||||||
|
|
||||||
|
// Sorted by last_reply origin_server_ts
|
||||||
|
const threadRoots = events
|
||||||
|
.map(this.client.getEventMapper())
|
||||||
|
.sort((eventA, eventB) => {
|
||||||
|
/**
|
||||||
|
* `origin_server_ts` in a decentralised world is far from ideal
|
||||||
|
* but for lack of any better, we will have to use this
|
||||||
|
* Long term the sorting should be handled by homeservers and this
|
||||||
|
* is only meant as a short term patch
|
||||||
|
*/
|
||||||
|
const threadAMetadata = eventA
|
||||||
|
.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
|
||||||
|
const threadBMetadata = eventB
|
||||||
|
.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
|
||||||
|
return threadAMetadata.latest_event.origin_server_ts - threadBMetadata.latest_event.origin_server_ts;
|
||||||
|
});
|
||||||
|
|
||||||
|
let latestMyThreadsRootEvent: MatrixEvent;
|
||||||
|
const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||||
|
for (const rootEvent of threadRoots) {
|
||||||
|
this.threadsTimelineSets[0].addLiveEvent(
|
||||||
|
rootEvent,
|
||||||
|
DuplicateStrategy.Ignore,
|
||||||
|
false,
|
||||||
|
roomState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const threadRelationship = rootEvent
|
||||||
|
.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
|
||||||
|
if (threadRelationship.current_user_participated) {
|
||||||
|
this.threadsTimelineSets[1].addLiveEvent(
|
||||||
|
rootEvent,
|
||||||
|
DuplicateStrategy.Ignore,
|
||||||
|
false,
|
||||||
|
roomState,
|
||||||
|
);
|
||||||
|
latestMyThreadsRootEvent = rootEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.getThread(rootEvent.getId())) {
|
||||||
|
this.createThread(rootEvent.getId(), rootEvent, [], true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.client.decryptEventIfNeeded(threadRoots[threadRoots.length -1]);
|
||||||
|
if (latestMyThreadsRootEvent) {
|
||||||
|
this.client.decryptEventIfNeeded(latestMyThreadsRootEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.threadsReady = true;
|
||||||
|
|
||||||
|
this.on(ThreadEvent.NewReply, this.onThreadNewReply);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onThreadNewReply(thread: Thread): void {
|
||||||
|
for (const timelineSet of this.threadsTimelineSets) {
|
||||||
|
timelineSet.removeEvent(thread.id);
|
||||||
|
timelineSet.addLiveEvent(thread.rootEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Forget the timelineSet for this room with the given filter
|
* Forget the timelineSet for this room with the given filter
|
||||||
*
|
*
|
||||||
@@ -1463,82 +1556,127 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public findThreadForEvent(event: MatrixEvent): Thread | null {
|
public eventShouldLiveIn(event: MatrixEvent, events?: MatrixEvent[], roots?: Set<string>): {
|
||||||
if (!event) {
|
shouldLiveInRoom: boolean;
|
||||||
return null;
|
shouldLiveInThread: boolean;
|
||||||
|
threadId?: string;
|
||||||
|
} {
|
||||||
|
if (!this.client.supportsExperimentalThreads()) {
|
||||||
|
return {
|
||||||
|
shouldLiveInRoom: true,
|
||||||
|
shouldLiveInThread: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A thread root is always shown in both timelines
|
||||||
|
if (event.isThreadRoot || roots?.has(event.getId())) {
|
||||||
|
return {
|
||||||
|
shouldLiveInRoom: true,
|
||||||
|
shouldLiveInThread: true,
|
||||||
|
threadId: event.getId(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// A thread relation is always only shown in a thread
|
||||||
if (event.isThreadRelation) {
|
if (event.isThreadRelation) {
|
||||||
return this.threads.get(event.threadRootId);
|
return {
|
||||||
} else if (event.isThreadRoot) {
|
shouldLiveInRoom: false,
|
||||||
return this.threads.get(event.getId());
|
shouldLiveInThread: true,
|
||||||
|
threadId: event.threadRootId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentEventId = event.getAssociatedId();
|
||||||
|
const parentEvent = this.findEventById(parentEventId) ?? events?.find(e => e.getId() === parentEventId);
|
||||||
|
|
||||||
|
// Treat relations and redactions as extensions of their parents so evaluate parentEvent instead
|
||||||
|
if (parentEvent && (event.isRelation() || event.isRedaction())) {
|
||||||
|
return this.eventShouldLiveIn(parentEvent, events, roots);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edge case where we know the event is a relation but don't have the parentEvent
|
||||||
|
if (roots?.has(event.relationEventId)) {
|
||||||
|
return {
|
||||||
|
shouldLiveInRoom: true,
|
||||||
|
shouldLiveInThread: true,
|
||||||
|
threadId: event.relationEventId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// We've exhausted all scenarios, can safely assume that this event should live in the room timeline only
|
||||||
|
return {
|
||||||
|
shouldLiveInRoom: true,
|
||||||
|
shouldLiveInThread: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public findThreadForEvent(event?: MatrixEvent): Thread | null {
|
||||||
|
if (!event) return null;
|
||||||
|
|
||||||
|
const { threadId } = this.eventShouldLiveIn(event);
|
||||||
|
return threadId ? this.getThread(threadId) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private addThreadedEvents(threadId: string, events: MatrixEvent[], toStartOfTimeline = false): void {
|
||||||
|
let thread = this.getThread(threadId);
|
||||||
|
|
||||||
|
if (thread) {
|
||||||
|
thread.addEvents(events, toStartOfTimeline);
|
||||||
} else {
|
} else {
|
||||||
const parentEvent = this.findEventById(event.getAssociatedId());
|
const rootEvent = this.findEventById(threadId) ?? events.find(e => e.getId() === threadId);
|
||||||
return this.findThreadForEvent(parentEvent);
|
thread = this.createThread(threadId, rootEvent, events, toStartOfTimeline);
|
||||||
|
this.emit(ThreadEvent.Update, thread);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add an event to a thread's timeline. Will fire "Thread.update"
|
* Adds events to a thread's timeline. Will fire "Thread.update"
|
||||||
* @experimental
|
* @experimental
|
||||||
*/
|
*/
|
||||||
public async addThreadedEvent(event: MatrixEvent, toStartOfTimeline: boolean): Promise<void> {
|
public processThreadedEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void {
|
||||||
this.applyRedaction(event);
|
events.forEach(this.applyRedaction);
|
||||||
let thread = this.findThreadForEvent(event);
|
|
||||||
if (thread) {
|
const eventsByThread: { [threadId: string]: MatrixEvent[] } = {};
|
||||||
thread.addEvent(event, toStartOfTimeline);
|
for (const event of events) {
|
||||||
} else {
|
const { threadId, shouldLiveInThread } = this.eventShouldLiveIn(event);
|
||||||
const events = [event];
|
if (shouldLiveInThread && !eventsByThread[threadId]) {
|
||||||
let rootEvent = this.findEventById(event.threadRootId);
|
eventsByThread[threadId] = [];
|
||||||
// If the rootEvent does not exist in the current sync, then look for
|
}
|
||||||
// it over the network
|
eventsByThread[threadId]?.push(event);
|
||||||
try {
|
|
||||||
let eventData;
|
|
||||||
if (event.threadRootId) {
|
|
||||||
eventData = await this.client.fetchRoomEvent(this.roomId, event.threadRootId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!rootEvent) {
|
Object.entries(eventsByThread).map(([threadId, threadEvents]) => (
|
||||||
rootEvent = new MatrixEvent(eventData);
|
this.addThreadedEvents(threadId, threadEvents, toStartOfTimeline)
|
||||||
} else {
|
));
|
||||||
rootEvent.setUnsigned(eventData.unsigned);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
// The root event might be not be visible to the person requesting
|
|
||||||
// it. If it wasn't fetched successfully the thread will work
|
|
||||||
// in "limited" mode and won't benefit from all the APIs a homeserver
|
|
||||||
// can provide to enhance the thread experience
|
|
||||||
thread = this.createThread(rootEvent, events, toStartOfTimeline);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emit(ThreadEvent.Update, thread);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public createThread(
|
public createThread(
|
||||||
|
threadId: string,
|
||||||
rootEvent: MatrixEvent | undefined,
|
rootEvent: MatrixEvent | undefined,
|
||||||
events: MatrixEvent[] = [],
|
events: MatrixEvent[] = [],
|
||||||
toStartOfTimeline: boolean,
|
toStartOfTimeline: boolean,
|
||||||
): Thread | undefined {
|
): Thread {
|
||||||
if (rootEvent) {
|
if (rootEvent) {
|
||||||
const tl = this.getTimelineForEvent(rootEvent.getId());
|
const tl = this.getTimelineForEvent(rootEvent.getId());
|
||||||
const relatedEvents = tl?.getTimelineSet().getAllRelationsEventForEvent(rootEvent.getId());
|
const relatedEvents = tl?.getTimelineSet().getAllRelationsEventForEvent(rootEvent.getId());
|
||||||
if (relatedEvents) {
|
if (relatedEvents?.length) {
|
||||||
events = events.concat(relatedEvents);
|
// Include all relations of the root event, given it'll be visible in both timelines,
|
||||||
|
// except `m.replace` as that will already be applied atop the event using `MatrixEvent::makeReplaced`
|
||||||
|
events = events.concat(relatedEvents.filter(e => !e.isRelation(RelationType.Replace)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const thread = new Thread(rootEvent, {
|
const thread = new Thread(threadId, rootEvent, {
|
||||||
initialEvents: events,
|
initialEvents: events,
|
||||||
room: this,
|
room: this,
|
||||||
client: this.client,
|
client: this.client,
|
||||||
});
|
});
|
||||||
// If we managed to create a thread and figure out its `id`
|
|
||||||
// then we can use it
|
// If we managed to create a thread and figure out its `id` then we can use it
|
||||||
if (thread.id) {
|
|
||||||
this.threads.set(thread.id, thread);
|
this.threads.set(thread.id, thread);
|
||||||
this.reEmitter.reEmit(thread, [
|
this.reEmitter.reEmit(thread, [
|
||||||
ThreadEvent.Update,
|
ThreadEvent.Update,
|
||||||
|
ThreadEvent.NewReply,
|
||||||
RoomEvent.Timeline,
|
RoomEvent.Timeline,
|
||||||
RoomEvent.TimelineReset,
|
RoomEvent.TimelineReset,
|
||||||
]);
|
]);
|
||||||
@@ -1549,6 +1687,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
|
|
||||||
this.emit(ThreadEvent.New, thread, toStartOfTimeline);
|
this.emit(ThreadEvent.New, thread, toStartOfTimeline);
|
||||||
|
|
||||||
|
if (this.threadsReady) {
|
||||||
this.threadsTimelineSets.forEach(timelineSet => {
|
this.threadsTimelineSets.forEach(timelineSet => {
|
||||||
if (thread.rootEvent) {
|
if (thread.rootEvent) {
|
||||||
if (Thread.hasServerSideSupport) {
|
if (Thread.hasServerSideSupport) {
|
||||||
@@ -1562,12 +1701,12 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return thread;
|
return thread;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
applyRedaction(event: MatrixEvent): void {
|
private applyRedaction = (event: MatrixEvent): void => {
|
||||||
if (event.isRedaction()) {
|
if (event.isRedaction()) {
|
||||||
const redactId = event.event.redacts;
|
const redactId = event.event.redacts;
|
||||||
|
|
||||||
@@ -1577,7 +1716,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
redactedEvent.makeRedacted(event);
|
redactedEvent.makeRedacted(event);
|
||||||
|
|
||||||
// If this is in the current state, replace it with the redacted version
|
// If this is in the current state, replace it with the redacted version
|
||||||
if (redactedEvent.getStateKey()) {
|
if (redactedEvent.isState()) {
|
||||||
const currentStateEvent = this.currentState.getStateEvents(
|
const currentStateEvent = this.currentState.getStateEvents(
|
||||||
redactedEvent.getType(),
|
redactedEvent.getType(),
|
||||||
redactedEvent.getStateKey(),
|
redactedEvent.getStateKey(),
|
||||||
@@ -1611,19 +1750,9 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
// clients can say "so and so redacted an event" if they wish to. Also
|
// clients can say "so and so redacted an event" if they wish to. Also
|
||||||
// this may be needed to trigger an update.
|
// this may be needed to trigger an update.
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
private processLiveEvent(event: MatrixEvent): void {
|
||||||
* Add an event to the end of this room's live timelines. Will fire
|
|
||||||
* "Room.timeline".
|
|
||||||
*
|
|
||||||
* @param {MatrixEvent} event Event to be added
|
|
||||||
* @param {string?} duplicateStrategy 'ignore' or 'replace'
|
|
||||||
* @param {boolean} fromCache whether the sync response came from cache
|
|
||||||
* @fires module:client~MatrixClient#event:"Room.timeline"
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private addLiveEvent(event: MatrixEvent, duplicateStrategy?: DuplicateStrategy, fromCache = false): void {
|
|
||||||
this.applyRedaction(event);
|
this.applyRedaction(event);
|
||||||
|
|
||||||
// Implement MSC3531: hiding messages.
|
// Implement MSC3531: hiding messages.
|
||||||
@@ -1640,10 +1769,21 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
if (existingEvent) {
|
if (existingEvent) {
|
||||||
// remote echo of an event we sent earlier
|
// remote echo of an event we sent earlier
|
||||||
this.handleRemoteEcho(event, existingEvent);
|
this.handleRemoteEcho(event, existingEvent);
|
||||||
return;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an event to the end of this room's live timelines. Will fire
|
||||||
|
* "Room.timeline".
|
||||||
|
*
|
||||||
|
* @param {MatrixEvent} event Event to be added
|
||||||
|
* @param {string?} duplicateStrategy 'ignore' or 'replace'
|
||||||
|
* @param {boolean} fromCache whether the sync response came from cache
|
||||||
|
* @fires module:client~MatrixClient#event:"Room.timeline"
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private addLiveEvent(event: MatrixEvent, duplicateStrategy: DuplicateStrategy, fromCache = false): void {
|
||||||
// add to our timeline sets
|
// add to our timeline sets
|
||||||
for (let i = 0; i < this.timelineSets.length; i++) {
|
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||||
this.timelineSets[i].addLiveEvent(event, duplicateStrategy, fromCache);
|
this.timelineSets[i].addLiveEvent(event, duplicateStrategy, fromCache);
|
||||||
@@ -1655,7 +1795,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
// Don't synthesize RR for m.room.redaction as this causes the RR to go missing.
|
// Don't synthesize RR for m.room.redaction as this causes the RR to go missing.
|
||||||
if (event.sender && event.getType() !== EventType.RoomRedaction) {
|
if (event.sender && event.getType() !== EventType.RoomRedaction) {
|
||||||
this.addReceipt(synthesizeReceipt(
|
this.addReceipt(synthesizeReceipt(
|
||||||
event.sender.userId, event, "m.read",
|
event.sender.userId, event, ReceiptType.Read,
|
||||||
), true);
|
), true);
|
||||||
|
|
||||||
// Any live events from a user could be taken as implicit
|
// Any live events from a user could be taken as implicit
|
||||||
@@ -1794,12 +1934,11 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
* @param {module:models/event.MatrixEvent} event the relation event that needs to be aggregated.
|
* @param {module:models/event.MatrixEvent} event the relation event that needs to be aggregated.
|
||||||
*/
|
*/
|
||||||
private aggregateNonLiveRelation(event: MatrixEvent): void {
|
private aggregateNonLiveRelation(event: MatrixEvent): void {
|
||||||
const thread = this.findThreadForEvent(event);
|
const { shouldLiveInRoom, threadId } = this.eventShouldLiveIn(event);
|
||||||
if (thread) {
|
const thread = this.getThread(threadId);
|
||||||
thread.timelineSet.aggregateRelations(event);
|
thread?.timelineSet.aggregateRelations(event);
|
||||||
}
|
|
||||||
|
|
||||||
if (thread?.id === event.getAssociatedId() || !thread) {
|
if (shouldLiveInRoom) {
|
||||||
// TODO: We should consider whether this means it would be a better
|
// TODO: We should consider whether this means it would be a better
|
||||||
// design to lift the relations handling up to the room instead.
|
// design to lift the relations handling up to the room instead.
|
||||||
for (let i = 0; i < this.timelineSets.length; i++) {
|
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||||
@@ -1838,10 +1977,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
const newEventId = remoteEvent.getId();
|
const newEventId = remoteEvent.getId();
|
||||||
const oldStatus = localEvent.status;
|
const oldStatus = localEvent.status;
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(`Got remote echo for event ${oldEventId} -> ${newEventId} old status ${oldStatus}`);
|
||||||
`Got remote echo for event ${oldEventId} -> ${newEventId} ` +
|
|
||||||
`old status ${oldStatus}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// no longer pending
|
// no longer pending
|
||||||
delete this.txnToEvent[remoteEvent.getUnsigned().transaction_id];
|
delete this.txnToEvent[remoteEvent.getUnsigned().transaction_id];
|
||||||
@@ -1855,12 +1991,11 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
// any, which is good, because we don't want to try decoding it again).
|
// any, which is good, because we don't want to try decoding it again).
|
||||||
localEvent.handleRemoteEcho(remoteEvent.event);
|
localEvent.handleRemoteEcho(remoteEvent.event);
|
||||||
|
|
||||||
const thread = this.findThreadForEvent(remoteEvent);
|
const { shouldLiveInRoom, threadId } = this.eventShouldLiveIn(remoteEvent);
|
||||||
if (thread) {
|
const thread = this.getThread(threadId);
|
||||||
thread.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
|
thread?.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
|
||||||
}
|
|
||||||
|
|
||||||
if (thread?.id === remoteEvent.getAssociatedId() || !thread) {
|
if (shouldLiveInRoom) {
|
||||||
for (let i = 0; i < this.timelineSets.length; i++) {
|
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||||
const timelineSet = this.timelineSets[i];
|
const timelineSet = this.timelineSets[i];
|
||||||
|
|
||||||
@@ -1926,11 +2061,11 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
// update the event id
|
// update the event id
|
||||||
event.replaceLocalEventId(newEventId);
|
event.replaceLocalEventId(newEventId);
|
||||||
|
|
||||||
const thread = this.findThreadForEvent(event);
|
const { shouldLiveInRoom, threadId } = this.eventShouldLiveIn(event);
|
||||||
if (thread) {
|
const thread = this.getThread(threadId);
|
||||||
thread.timelineSet.replaceEventId(oldEventId, newEventId);
|
thread?.timelineSet.replaceEventId(oldEventId, newEventId);
|
||||||
}
|
|
||||||
if (thread?.id === event.getAssociatedId() || !thread) {
|
if (shouldLiveInRoom) {
|
||||||
// if the event was already in the timeline (which will be the case if
|
// if the event was already in the timeline (which will be the case if
|
||||||
// opts.pendingEventOrdering==chronological), we need to update the
|
// opts.pendingEventOrdering==chronological), we need to update the
|
||||||
// timeline map.
|
// timeline map.
|
||||||
@@ -1941,14 +2076,12 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
} else if (newStatus == EventStatus.CANCELLED) {
|
} else if (newStatus == EventStatus.CANCELLED) {
|
||||||
// remove it from the pending event list, or the timeline.
|
// remove it from the pending event list, or the timeline.
|
||||||
if (this.pendingEventList) {
|
if (this.pendingEventList) {
|
||||||
const idx = this.pendingEventList.findIndex(ev => ev.getId() === oldEventId);
|
const removedEvent = this.getPendingEvent(oldEventId);
|
||||||
if (idx !== -1) {
|
this.removePendingEvent(oldEventId);
|
||||||
const [removedEvent] = this.pendingEventList.splice(idx, 1);
|
|
||||||
if (removedEvent.isRedaction()) {
|
if (removedEvent.isRedaction()) {
|
||||||
this.revertRedactionLocalEcho(removedEvent);
|
this.revertRedactionLocalEcho(removedEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
this.removeEvent(oldEventId);
|
this.removeEvent(oldEventId);
|
||||||
}
|
}
|
||||||
this.savePendingEvents();
|
this.savePendingEvents();
|
||||||
@@ -1992,13 +2125,12 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
* @throws If <code>duplicateStrategy</code> is not falsey, 'replace' or 'ignore'.
|
* @throws If <code>duplicateStrategy</code> is not falsey, 'replace' or 'ignore'.
|
||||||
*/
|
*/
|
||||||
public addLiveEvents(events: MatrixEvent[], duplicateStrategy?: DuplicateStrategy, fromCache = false): void {
|
public addLiveEvents(events: MatrixEvent[], duplicateStrategy?: DuplicateStrategy, fromCache = false): void {
|
||||||
let i;
|
|
||||||
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
|
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
|
||||||
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
|
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
|
||||||
}
|
}
|
||||||
|
|
||||||
// sanity check that the live timeline is still live
|
// sanity check that the live timeline is still live
|
||||||
for (i = 0; i < this.timelineSets.length; i++) {
|
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||||
const liveTimeline = this.timelineSets[i].getLiveTimeline();
|
const liveTimeline = this.timelineSets[i].getLiveTimeline();
|
||||||
if (liveTimeline.getPaginationToken(EventTimeline.FORWARDS)) {
|
if (liveTimeline.getPaginationToken(EventTimeline.FORWARDS)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -2007,22 +2139,85 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (liveTimeline.getNeighbouringTimeline(EventTimeline.FORWARDS)) {
|
if (liveTimeline.getNeighbouringTimeline(EventTimeline.FORWARDS)) {
|
||||||
throw new Error(
|
throw new Error(`live timeline ${i} is no longer live - it has a neighbouring timeline`);
|
||||||
"live timeline " + i + " is no longer live - " +
|
|
||||||
"it has a neighbouring timeline",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (i = 0; i < events.length; i++) {
|
const threadRoots = this.findThreadRoots(events);
|
||||||
// TODO: We should have a filter to say "only add state event
|
const eventsByThread: { [threadId: string]: MatrixEvent[] } = {};
|
||||||
// types X Y Z to the timeline".
|
|
||||||
this.addLiveEvent(events[i], duplicateStrategy, fromCache);
|
for (const event of events) {
|
||||||
const thread = this.findThreadForEvent(events[i]);
|
// TODO: We should have a filter to say "only add state event types X Y Z to the timeline".
|
||||||
if (thread) {
|
this.processLiveEvent(event);
|
||||||
thread.addEvent(events[i], true);
|
|
||||||
|
const {
|
||||||
|
shouldLiveInRoom,
|
||||||
|
shouldLiveInThread,
|
||||||
|
threadId,
|
||||||
|
} = this.eventShouldLiveIn(event, events, threadRoots);
|
||||||
|
|
||||||
|
if (shouldLiveInThread && !eventsByThread[threadId]) {
|
||||||
|
eventsByThread[threadId] = [];
|
||||||
|
}
|
||||||
|
eventsByThread[threadId]?.push(event);
|
||||||
|
|
||||||
|
if (shouldLiveInRoom) {
|
||||||
|
this.addLiveEvent(event, duplicateStrategy, fromCache);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Object.entries(eventsByThread).forEach(([threadId, threadEvents]) => {
|
||||||
|
this.addThreadedEvents(threadId, threadEvents, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public partitionThreadedEvents(events: MatrixEvent[]): [
|
||||||
|
timelineEvents: MatrixEvent[],
|
||||||
|
threadedEvents: MatrixEvent[],
|
||||||
|
] {
|
||||||
|
// Indices to the events array, for readability
|
||||||
|
const ROOM = 0;
|
||||||
|
const THREAD = 1;
|
||||||
|
if (this.client.supportsExperimentalThreads()) {
|
||||||
|
const threadRoots = this.findThreadRoots(events);
|
||||||
|
return events.reduce((memo, event: MatrixEvent) => {
|
||||||
|
const {
|
||||||
|
shouldLiveInRoom,
|
||||||
|
shouldLiveInThread,
|
||||||
|
threadId,
|
||||||
|
} = this.eventShouldLiveIn(event, events, threadRoots);
|
||||||
|
|
||||||
|
if (shouldLiveInRoom) {
|
||||||
|
memo[ROOM].push(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldLiveInThread) {
|
||||||
|
event.setThreadId(threadId);
|
||||||
|
memo[THREAD].push(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
return memo;
|
||||||
|
}, [[], []]);
|
||||||
|
} else {
|
||||||
|
// When `experimentalThreadSupport` is disabled treat all events as timelineEvents
|
||||||
|
return [
|
||||||
|
events,
|
||||||
|
[],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given some events, find the IDs of all the thread roots that are referred to by them.
|
||||||
|
*/
|
||||||
|
private findThreadRoots(events: MatrixEvent[]): Set<string> {
|
||||||
|
const threadRoots = new Set<string>();
|
||||||
|
for (const event of events) {
|
||||||
|
if (event.isThreadRelation) {
|
||||||
|
threadRoots.add(event.relationEventId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return threadRoots;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2080,7 +2275,11 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
// set fake stripped state events if this is an invite room so logic remains
|
// set fake stripped state events if this is an invite room so logic remains
|
||||||
// consistent elsewhere.
|
// consistent elsewhere.
|
||||||
const membershipEvent = this.currentState.getStateEvents(EventType.RoomMember, this.myUserId);
|
const membershipEvent = this.currentState.getStateEvents(EventType.RoomMember, this.myUserId);
|
||||||
if (membershipEvent && membershipEvent.getContent().membership === "invite") {
|
if (membershipEvent) {
|
||||||
|
const membership = membershipEvent.getContent().membership;
|
||||||
|
this.updateMyMembership(membership);
|
||||||
|
|
||||||
|
if (membership === "invite") {
|
||||||
const strippedStateEvents = membershipEvent.getUnsigned().invite_room_state || [];
|
const strippedStateEvents = membershipEvent.getUnsigned().invite_room_state || [];
|
||||||
strippedStateEvents.forEach((strippedEvent) => {
|
strippedStateEvents.forEach((strippedEvent) => {
|
||||||
const existingEvent = this.currentState.getStateEvents(strippedEvent.type, strippedEvent.state_key);
|
const existingEvent = this.currentState.getStateEvents(strippedEvent.type, strippedEvent.state_key);
|
||||||
@@ -2097,6 +2296,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const oldName = this.name;
|
const oldName = this.name;
|
||||||
this.name = this.calculateRoomName(this.myUserId);
|
this.name = this.calculateRoomName(this.myUserId);
|
||||||
@@ -2117,14 +2317,23 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
*/
|
*/
|
||||||
public getUsersReadUpTo(event: MatrixEvent): string[] {
|
public getUsersReadUpTo(event: MatrixEvent): string[] {
|
||||||
return this.getReceiptsForEvent(event).filter(function(receipt) {
|
return this.getReceiptsForEvent(event).filter(function(receipt) {
|
||||||
return receipt.type === "m.read";
|
return [ReceiptType.Read, ReceiptType.ReadPrivate].includes(receipt.type);
|
||||||
}).map(function(receipt) {
|
}).map(function(receipt) {
|
||||||
return receipt.userId;
|
return receipt.userId;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getReadReceiptForUserId(userId: string, ignoreSynthesized = false): IWrappedReceipt | null {
|
/**
|
||||||
const [realReceipt, syntheticReceipt] = this.receipts["m.read"]?.[userId] ?? [];
|
* Gets the latest receipt for a given user in the room
|
||||||
|
* @param userId The id of the user for which we want the receipt
|
||||||
|
* @param ignoreSynthesized Whether to ignore synthesized receipts or not
|
||||||
|
* @param receiptType Optional. The type of the receipt we want to get
|
||||||
|
* @returns the latest receipts of the chosen type for the chosen user
|
||||||
|
*/
|
||||||
|
public getReadReceiptForUserId(
|
||||||
|
userId: string, ignoreSynthesized = false, receiptType = ReceiptType.Read,
|
||||||
|
): IWrappedReceipt | null {
|
||||||
|
const [realReceipt, syntheticReceipt] = this.receipts[receiptType]?.[userId] ?? [];
|
||||||
if (ignoreSynthesized) {
|
if (ignoreSynthesized) {
|
||||||
return realReceipt;
|
return realReceipt;
|
||||||
}
|
}
|
||||||
@@ -2142,8 +2351,25 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
* @return {String} ID of the latest event that the given user has read, or null.
|
* @return {String} ID of the latest event that the given user has read, or null.
|
||||||
*/
|
*/
|
||||||
public getEventReadUpTo(userId: string, ignoreSynthesized = false): string | null {
|
public getEventReadUpTo(userId: string, ignoreSynthesized = false): string | null {
|
||||||
const readReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized);
|
const timelineSet = this.getUnfilteredTimelineSet();
|
||||||
return readReceipt?.eventId ?? null;
|
const publicReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.Read);
|
||||||
|
const privateReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.ReadPrivate);
|
||||||
|
|
||||||
|
// If we have both, compare them
|
||||||
|
let comparison: number | undefined;
|
||||||
|
if (publicReadReceipt?.eventId && privateReadReceipt?.eventId) {
|
||||||
|
comparison = timelineSet.compareEventOrdering(publicReadReceipt?.eventId, privateReadReceipt?.eventId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we didn't get a comparison try to compare the ts of the receipts
|
||||||
|
if (!comparison) comparison = publicReadReceipt?.data?.ts - privateReadReceipt?.data?.ts;
|
||||||
|
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2296,7 +2522,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
}
|
}
|
||||||
this.receiptCacheByEventId[eventId].push({
|
this.receiptCacheByEventId[eventId].push({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
type: receiptType,
|
type: receiptType as ReceiptType,
|
||||||
data: receipt,
|
data: receipt,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -2309,9 +2535,9 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
* client the fact that we've sent one.
|
* client the fact that we've sent one.
|
||||||
* @param {string} userId The user ID if the receipt sender
|
* @param {string} userId The user ID if the receipt sender
|
||||||
* @param {MatrixEvent} e The event that is to be acknowledged
|
* @param {MatrixEvent} e The event that is to be acknowledged
|
||||||
* @param {string} receiptType The type of receipt
|
* @param {ReceiptType} receiptType The type of receipt
|
||||||
*/
|
*/
|
||||||
public addLocalEchoReceipt(userId: string, e: MatrixEvent, receiptType: string): void {
|
public addLocalEchoReceipt(userId: string, e: MatrixEvent, receiptType: ReceiptType): void {
|
||||||
this.addReceipt(synthesizeReceipt(userId, e, receiptType), true);
|
this.addReceipt(synthesizeReceipt(userId, e, receiptType), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2414,7 +2640,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the type of the room from the `m.room.create` event content or undefined if none is set
|
* Returns the type of the room from the `m.room.create` event content or undefined if none is set
|
||||||
* @returns {?string} the type of the room. Currently only RoomType.Space is known.
|
* @returns {?string} the type of the room.
|
||||||
*/
|
*/
|
||||||
public getType(): RoomType | string | undefined {
|
public getType(): RoomType | string | undefined {
|
||||||
const createEvent = this.currentState.getStateEvents(EventType.RoomCreate, "");
|
const createEvent = this.currentState.getStateEvents(EventType.RoomCreate, "");
|
||||||
@@ -2436,6 +2662,22 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
return this.getType() === RoomType.Space;
|
return this.getType() === RoomType.Space;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the room is a call-room as defined by MSC3417.
|
||||||
|
* @returns {boolean} true if the room's type is RoomType.UnstableCall
|
||||||
|
*/
|
||||||
|
public isCallRoom(): boolean {
|
||||||
|
return this.getType() === RoomType.UnstableCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the room is a video room.
|
||||||
|
* @returns {boolean} true if the room's type is RoomType.ElementVideo
|
||||||
|
*/
|
||||||
|
public isElementVideoRoom(): boolean {
|
||||||
|
return this.getType() === RoomType.ElementVideo;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is an internal method. Calculates the name of the room from the current
|
* This is an internal method. Calculates the name of the room from the current
|
||||||
* room state.
|
* room state.
|
||||||
|
|||||||
@@ -33,14 +33,19 @@ export class SearchResult {
|
|||||||
|
|
||||||
public static fromJson(jsonObj: ISearchResult, eventMapper: EventMapper): SearchResult {
|
public static fromJson(jsonObj: ISearchResult, eventMapper: EventMapper): SearchResult {
|
||||||
const jsonContext = jsonObj.context || {} as IResultContext;
|
const jsonContext = jsonObj.context || {} as IResultContext;
|
||||||
const eventsBefore = jsonContext.events_before || [];
|
let eventsBefore = (jsonContext.events_before || []).map(eventMapper);
|
||||||
const eventsAfter = jsonContext.events_after || [];
|
let eventsAfter = (jsonContext.events_after || []).map(eventMapper);
|
||||||
|
|
||||||
const context = new EventContext(eventMapper(jsonObj.result));
|
const context = new EventContext(eventMapper(jsonObj.result));
|
||||||
|
|
||||||
|
// Filter out any contextual events which do not correspond to the same timeline (thread or room)
|
||||||
|
const threadRootId = context.ourEvent.threadRootId;
|
||||||
|
eventsBefore = eventsBefore.filter(e => e.threadRootId === threadRootId);
|
||||||
|
eventsAfter = eventsAfter.filter(e => e.threadRootId === threadRootId);
|
||||||
|
|
||||||
context.setPaginateToken(jsonContext.start, true);
|
context.setPaginateToken(jsonContext.start, true);
|
||||||
context.addEvents(eventsBefore.map(eventMapper), true);
|
context.addEvents(eventsBefore, true);
|
||||||
context.addEvents(eventsAfter.map(eventMapper), false);
|
context.addEvents(eventsAfter, false);
|
||||||
context.setPaginateToken(jsonContext.end, false);
|
context.setPaginateToken(jsonContext.end, false);
|
||||||
|
|
||||||
return new SearchResult(jsonObj.rank, context);
|
return new SearchResult(jsonObj.rank, context);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MatrixClient, RoomEvent } from "../matrix";
|
import { MatrixClient, MatrixEventEvent, RelationType, RoomEvent } from "../matrix";
|
||||||
import { TypedReEmitter } from "../ReEmitter";
|
import { TypedReEmitter } from "../ReEmitter";
|
||||||
import { IRelationsRequestOpts } from "../@types/requests";
|
import { IRelationsRequestOpts } from "../@types/requests";
|
||||||
import { IThreadBundledRelationship, MatrixEvent } from "./event";
|
import { IThreadBundledRelationship, MatrixEvent } from "./event";
|
||||||
@@ -24,6 +24,7 @@ import { Room } from './room';
|
|||||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||||
import { RoomState } from "./room-state";
|
import { RoomState } from "./room-state";
|
||||||
import { ServerControlledNamespacedValue } from "../NamespacedValue";
|
import { ServerControlledNamespacedValue } from "../NamespacedValue";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
|
||||||
export enum ThreadEvent {
|
export enum ThreadEvent {
|
||||||
New = "Thread.new",
|
New = "Thread.new",
|
||||||
@@ -69,16 +70,21 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
|||||||
public readonly room: Room;
|
public readonly room: Room;
|
||||||
public readonly client: MatrixClient;
|
public readonly client: MatrixClient;
|
||||||
|
|
||||||
public initialEventsFetched = false;
|
public initialEventsFetched = !Thread.hasServerSideSupport;
|
||||||
|
|
||||||
public readonly id: string;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly rootEvent: MatrixEvent | undefined,
|
public readonly id: string,
|
||||||
|
public rootEvent: MatrixEvent | undefined,
|
||||||
opts: IThreadOpts,
|
opts: IThreadOpts,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
if (!opts?.room) {
|
||||||
|
// Logging/debugging for https://github.com/vector-im/element-web/issues/22141
|
||||||
|
// Hope is that we end up with a more obvious stack trace.
|
||||||
|
throw new Error("element-web#22141: A thread requires a room in order to function");
|
||||||
|
}
|
||||||
|
|
||||||
this.room = opts.room;
|
this.room = opts.room;
|
||||||
this.client = opts.client;
|
this.client = opts.client;
|
||||||
this.timelineSet = new EventTimelineSet(this.room, {
|
this.timelineSet = new EventTimelineSet(this.room, {
|
||||||
@@ -93,20 +99,38 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
|||||||
RoomEvent.TimelineReset,
|
RoomEvent.TimelineReset,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// If we weren't able to find the root event, it's probably missing
|
this.room.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
|
||||||
// and we define the thread ID from one of the thread relation
|
this.room.on(RoomEvent.Redaction, this.onRedaction);
|
||||||
if (!rootEvent) {
|
|
||||||
this.id = opts?.initialEvents
|
|
||||||
?.find(event => event.isThreadRelation)?.relationEventId;
|
|
||||||
} else {
|
|
||||||
this.id = rootEvent.getId();
|
|
||||||
}
|
|
||||||
this.initialiseThread(this.rootEvent);
|
|
||||||
|
|
||||||
opts?.initialEvents?.forEach(event => this.addEvent(event, false));
|
|
||||||
|
|
||||||
this.room.on(RoomEvent.LocalEchoUpdated, this.onEcho);
|
this.room.on(RoomEvent.LocalEchoUpdated, this.onEcho);
|
||||||
this.room.on(RoomEvent.Timeline, this.onEcho);
|
this.timelineSet.on(RoomEvent.Timeline, this.onEcho);
|
||||||
|
|
||||||
|
if (opts.initialEvents) {
|
||||||
|
this.addEvents(opts.initialEvents, false);
|
||||||
|
}
|
||||||
|
// even if this thread is thought to be originating from this client, we initialise it as we may be in a
|
||||||
|
// gappy sync and a thread around this event may already exist.
|
||||||
|
this.initialiseThread();
|
||||||
|
|
||||||
|
this.rootEvent?.setThread(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchRootEvent(): Promise<void> {
|
||||||
|
this.rootEvent = this.room.findEventById(this.id);
|
||||||
|
// If the rootEvent does not exist in the local stores, then fetch it from the server.
|
||||||
|
try {
|
||||||
|
const eventData = await this.client.fetchRoomEvent(this.roomId, this.id);
|
||||||
|
const mapper = this.client.getEventMapper();
|
||||||
|
this.rootEvent = mapper(eventData); // will merge with existing event object if such is known
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("Failed to fetch thread root to construct thread with", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The root event might be not be visible to the person requesting it.
|
||||||
|
// If it wasn't fetched successfully the thread will work in "limited" mode and won't
|
||||||
|
// benefit from all the APIs a homeserver can provide to enhance the thread experience
|
||||||
|
this.rootEvent?.setThread(this);
|
||||||
|
|
||||||
|
this.emit(ThreadEvent.Update, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static setServerSideSupport(hasServerSideSupport: boolean, useStable: boolean): void {
|
public static setServerSideSupport(hasServerSideSupport: boolean, useStable: boolean): void {
|
||||||
@@ -118,26 +142,59 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onEcho = (event: MatrixEvent) => {
|
private onBeforeRedaction = (event: MatrixEvent, redaction: MatrixEvent) => {
|
||||||
if (this.timelineSet.eventIdToTimeline(event.getId())) {
|
if (event?.isRelation(THREAD_RELATION_TYPE.name) &&
|
||||||
|
this.room.eventShouldLiveIn(event).threadId === this.id &&
|
||||||
|
event.getId() !== this.id && // the root event isn't counted in the length so ignore this redaction
|
||||||
|
!redaction.status // only respect it when it succeeds
|
||||||
|
) {
|
||||||
|
this.replyCount--;
|
||||||
this.emit(ThreadEvent.Update, this);
|
this.emit(ThreadEvent.Update, this);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onRedaction = (event: MatrixEvent) => {
|
||||||
|
if (event.threadRootId !== this.id) return; // ignore redactions for other timelines
|
||||||
|
const events = [...this.timelineSet.getLiveTimeline().getEvents()].reverse();
|
||||||
|
this.lastEvent = events.find(e => (
|
||||||
|
!e.isRedacted() &&
|
||||||
|
e.isRelation(THREAD_RELATION_TYPE.name)
|
||||||
|
)) ?? this.rootEvent;
|
||||||
|
this.emit(ThreadEvent.Update, this);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onEcho = (event: MatrixEvent) => {
|
||||||
|
if (event.threadRootId !== this.id) return; // ignore echoes for other timelines
|
||||||
|
if (this.lastEvent === event) return;
|
||||||
|
|
||||||
|
// There is a risk that the `localTimestamp` approximation will not be accurate
|
||||||
|
// when threads are used over federation. That could result in the reply
|
||||||
|
// count value drifting away from the value returned by the server
|
||||||
|
const isThreadReply = event.isRelation(THREAD_RELATION_TYPE.name);
|
||||||
|
if (!this.lastEvent || this.lastEvent.isRedacted() || (isThreadReply
|
||||||
|
&& (event.getId() !== this.lastEvent.getId())
|
||||||
|
&& (event.localTimestamp > this.lastEvent.localTimestamp))
|
||||||
|
) {
|
||||||
|
this.lastEvent = event;
|
||||||
|
if (this.lastEvent.getId() !== this.id) {
|
||||||
|
// This counting only works when server side support is enabled as we started the counting
|
||||||
|
// from the value returned within the bundled relationship
|
||||||
|
if (Thread.hasServerSideSupport) {
|
||||||
|
this.replyCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit(ThreadEvent.NewReply, this, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit(ThreadEvent.Update, this);
|
||||||
|
};
|
||||||
|
|
||||||
public get roomState(): RoomState {
|
public get roomState(): RoomState {
|
||||||
return this.room.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
return this.room.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
private addEventToTimeline(event: MatrixEvent, toStartOfTimeline: boolean): void {
|
private addEventToTimeline(event: MatrixEvent, toStartOfTimeline: boolean): void {
|
||||||
if (event.getUnsigned().transaction_id) {
|
|
||||||
const existingEvent = this.room.getEventForTxnId(event.getUnsigned().transaction_id);
|
|
||||||
if (existingEvent) {
|
|
||||||
// remote echo of an event we sent earlier
|
|
||||||
this.room.handleRemoteEcho(event, existingEvent);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.findEventById(event.getId())) {
|
if (!this.findEventById(event.getId())) {
|
||||||
this.timelineSet.addEventToTimeline(
|
this.timelineSet.addEventToTimeline(
|
||||||
event,
|
event,
|
||||||
@@ -149,6 +206,11 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public addEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void {
|
||||||
|
events.forEach(ev => this.addEvent(ev, toStartOfTimeline, false));
|
||||||
|
this.emit(ThreadEvent.Update, this);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add an event to the thread and updates
|
* Add an event to the thread and updates
|
||||||
* the tail/root references if needed
|
* the tail/root references if needed
|
||||||
@@ -156,64 +218,60 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
|||||||
* @param event The event to add
|
* @param event The event to add
|
||||||
* @param {boolean} toStartOfTimeline whether the event is being added
|
* @param {boolean} toStartOfTimeline whether the event is being added
|
||||||
* to the start (and not the end) of the timeline.
|
* to the start (and not the end) of the timeline.
|
||||||
|
* @param {boolean} emit whether to emit the Update event if the thread was updated or not.
|
||||||
*/
|
*/
|
||||||
public async addEvent(event: MatrixEvent, toStartOfTimeline: boolean): Promise<void> {
|
public addEvent(event: MatrixEvent, toStartOfTimeline: boolean, emit = true): void {
|
||||||
|
event.setThread(this);
|
||||||
|
|
||||||
|
if (!this._currentUserParticipated && event.getSender() === this.client.getUserId()) {
|
||||||
|
this._currentUserParticipated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([RelationType.Annotation, RelationType.Replace].includes(event.getRelation()?.rel_type as RelationType)) {
|
||||||
|
// Apply annotations and replace relations to the relations of the timeline only
|
||||||
|
this.timelineSet.setRelationsTarget(event);
|
||||||
|
this.timelineSet.aggregateRelations(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Add all incoming events to the thread's timeline set when there's no server support
|
// Add all incoming events to the thread's timeline set when there's no server support
|
||||||
if (!Thread.hasServerSideSupport) {
|
if (!Thread.hasServerSideSupport) {
|
||||||
// all the relevant membership info to hydrate events with a sender
|
// all the relevant membership info to hydrate events with a sender
|
||||||
// is held in the main room timeline
|
// is held in the main room timeline
|
||||||
// We want to fetch the room state from there and pass it down to this thread
|
// We want to fetch the room state from there and pass it down to this thread
|
||||||
// timeline set to let it reconcile an event with its relevant RoomMember
|
// timeline set to let it reconcile an event with its relevant RoomMember
|
||||||
|
|
||||||
event.setThread(this);
|
|
||||||
this.addEventToTimeline(event, toStartOfTimeline);
|
this.addEventToTimeline(event, toStartOfTimeline);
|
||||||
|
|
||||||
await this.client.decryptEventIfNeeded(event, {});
|
this.client.decryptEventIfNeeded(event, {});
|
||||||
}
|
} else if (!toStartOfTimeline &&
|
||||||
|
this.initialEventsFetched &&
|
||||||
if (Thread.hasServerSideSupport && this.initialEventsFetched) {
|
event.localTimestamp > this.lastReply()?.localTimestamp
|
||||||
if (event.localTimestamp > this.lastReply().localTimestamp) {
|
) {
|
||||||
|
this.fetchEditsWhereNeeded(event);
|
||||||
this.addEventToTimeline(event, false);
|
this.addEventToTimeline(event, false);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!this._currentUserParticipated && event.getSender() === this.client.getUserId()) {
|
|
||||||
this._currentUserParticipated = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isThreadReply = event.getRelation()?.rel_type === THREAD_RELATION_TYPE.name;
|
|
||||||
// If no thread support exists we want to count all thread relation
|
// If no thread support exists we want to count all thread relation
|
||||||
// added as a reply. We can't rely on the bundled relationships count
|
// added as a reply. We can't rely on the bundled relationships count
|
||||||
if (!Thread.hasServerSideSupport && isThreadReply) {
|
if ((!Thread.hasServerSideSupport || !this.rootEvent) && event.isRelation(THREAD_RELATION_TYPE.name)) {
|
||||||
this.replyCount++;
|
this.replyCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// There is a risk that the `localTimestamp` approximation will not be accurate
|
if (emit) {
|
||||||
// when threads are used over federation. That could results in the reply
|
|
||||||
// count value drifting away from the value returned by the server
|
|
||||||
if (!this.lastEvent || (isThreadReply
|
|
||||||
&& (event.getId() !== this.lastEvent.getId())
|
|
||||||
&& (event.localTimestamp > this.lastEvent.localTimestamp))
|
|
||||||
) {
|
|
||||||
this.lastEvent = event;
|
|
||||||
if (this.lastEvent.getId() !== this.id) {
|
|
||||||
// This counting only works when server side support is enabled
|
|
||||||
// as we started the counting from the value returned in the
|
|
||||||
// bundled relationship
|
|
||||||
if (Thread.hasServerSideSupport) {
|
|
||||||
this.replyCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emit(ThreadEvent.NewReply, this, event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emit(ThreadEvent.Update, this);
|
this.emit(ThreadEvent.Update, this);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private initialiseThread(rootEvent: MatrixEvent | undefined): void {
|
private getRootEventBundledRelationship(rootEvent = this.rootEvent): IThreadBundledRelationship {
|
||||||
const bundledRelationship = rootEvent
|
return rootEvent?.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
|
||||||
?.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
|
}
|
||||||
|
|
||||||
|
private async initialiseThread(): Promise<void> {
|
||||||
|
let bundledRelationship = this.getRootEventBundledRelationship();
|
||||||
|
if (Thread.hasServerSideSupport && !bundledRelationship) {
|
||||||
|
await this.fetchRootEvent();
|
||||||
|
bundledRelationship = this.getRootEventBundledRelationship();
|
||||||
|
}
|
||||||
|
|
||||||
if (Thread.hasServerSideSupport && bundledRelationship) {
|
if (Thread.hasServerSideSupport && bundledRelationship) {
|
||||||
this.replyCount = bundledRelationship.count;
|
this.replyCount = bundledRelationship.count;
|
||||||
@@ -221,27 +279,34 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
|||||||
|
|
||||||
const event = new MatrixEvent(bundledRelationship.latest_event);
|
const event = new MatrixEvent(bundledRelationship.latest_event);
|
||||||
this.setEventMetadata(event);
|
this.setEventMetadata(event);
|
||||||
|
event.setThread(this);
|
||||||
this.lastEvent = event;
|
this.lastEvent = event;
|
||||||
}
|
|
||||||
|
this.fetchEditsWhereNeeded(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async fetchInitialEvents(): Promise<{
|
this.emit(ThreadEvent.Update, this);
|
||||||
originalEvent: MatrixEvent;
|
|
||||||
events: MatrixEvent[];
|
|
||||||
nextBatch?: string;
|
|
||||||
prevBatch?: string;
|
|
||||||
} | null> {
|
|
||||||
if (!Thread.hasServerSideSupport) {
|
|
||||||
this.initialEventsFetched = true;
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
const response = await this.fetchEvents();
|
// XXX: Workaround for https://github.com/matrix-org/matrix-spec-proposals/pull/2676/files#r827240084
|
||||||
this.initialEventsFetched = true;
|
private async fetchEditsWhereNeeded(...events: MatrixEvent[]): Promise<unknown> {
|
||||||
return response;
|
return Promise.all(events.filter(e => e.isEncrypted()).map((event: MatrixEvent) => {
|
||||||
} catch (e) {
|
return this.client.relations(this.roomId, event.getId(), RelationType.Replace, event.getType(), {
|
||||||
return null;
|
limit: 1,
|
||||||
|
}).then(relations => {
|
||||||
|
if (relations.events.length) {
|
||||||
|
event.makeReplaced(relations.events[0]);
|
||||||
}
|
}
|
||||||
|
}).catch(e => {
|
||||||
|
logger.error("Failed to load edits for encrypted thread event", e);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async fetchInitialEvents(): Promise<void> {
|
||||||
|
if (this.initialEventsFetched) return;
|
||||||
|
await this.fetchEvents();
|
||||||
|
this.initialEventsFetched = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setEventMetadata(event: MatrixEvent): void {
|
private setEventMetadata(event: MatrixEvent): void {
|
||||||
@@ -253,6 +318,11 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
|||||||
* Finds an event by ID in the current thread
|
* Finds an event by ID in the current thread
|
||||||
*/
|
*/
|
||||||
public findEventById(eventId: string) {
|
public findEventById(eventId: string) {
|
||||||
|
// Check the lastEvent as it may have been created based on a bundled relationship and not in a timeline
|
||||||
|
if (this.lastEvent?.getId() === eventId) {
|
||||||
|
return this.lastEvent;
|
||||||
|
}
|
||||||
|
|
||||||
return this.timelineSet.findEventById(eventId);
|
return this.timelineSet.findEventById(eventId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +355,7 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
|||||||
* A getter for the last event added to the thread
|
* A getter for the last event added to the thread
|
||||||
*/
|
*/
|
||||||
public get replyToEvent(): MatrixEvent {
|
public get replyToEvent(): MatrixEvent {
|
||||||
return this.lastEvent;
|
return this.lastEvent ?? this.lastReply();
|
||||||
}
|
}
|
||||||
|
|
||||||
public get events(): MatrixEvent[] {
|
public get events(): MatrixEvent[] {
|
||||||
@@ -304,7 +374,7 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
|||||||
return this.timelineSet.getLiveTimeline();
|
return this.timelineSet.getLiveTimeline();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async fetchEvents(opts: IRelationsRequestOpts = { limit: 20 }): Promise<{
|
public async fetchEvents(opts: IRelationsRequestOpts = { limit: 20, direction: Direction.Backward }): Promise<{
|
||||||
originalEvent: MatrixEvent;
|
originalEvent: MatrixEvent;
|
||||||
events: MatrixEvent[];
|
events: MatrixEvent[];
|
||||||
nextBatch?: string;
|
nextBatch?: string;
|
||||||
@@ -329,12 +399,14 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
|||||||
events = [...events, originalEvent];
|
events = [...events, originalEvent];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.fetchEditsWhereNeeded(...events);
|
||||||
|
|
||||||
await Promise.all(events.map(event => {
|
await Promise.all(events.map(event => {
|
||||||
this.setEventMetadata(event);
|
this.setEventMetadata(event);
|
||||||
return this.client.decryptEventIfNeeded(event);
|
return this.client.decryptEventIfNeeded(event);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const prependEvents = !opts.direction || opts.direction === Direction.Backward;
|
const prependEvents = (opts.direction ?? Direction.Backward) === Direction.Backward;
|
||||||
|
|
||||||
this.timelineSet.addEventsToTimeline(
|
this.timelineSet.addEventsToTimeline(
|
||||||
events,
|
events,
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ export enum UserEvent {
|
|||||||
Presence = "User.presence",
|
Presence = "User.presence",
|
||||||
CurrentlyActive = "User.currentlyActive",
|
CurrentlyActive = "User.currentlyActive",
|
||||||
LastPresenceTs = "User.lastPresenceTs",
|
LastPresenceTs = "User.lastPresenceTs",
|
||||||
/* @deprecated */
|
|
||||||
_UnstableStatusMessage = "User.unstable_statusMessage",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserEventHandlerMap = {
|
export type UserEventHandlerMap = {
|
||||||
@@ -37,7 +35,6 @@ export type UserEventHandlerMap = {
|
|||||||
[UserEvent.Presence]: (event: MatrixEvent | undefined, user: User) => void;
|
[UserEvent.Presence]: (event: MatrixEvent | undefined, user: User) => void;
|
||||||
[UserEvent.CurrentlyActive]: (event: MatrixEvent | undefined, user: User) => void;
|
[UserEvent.CurrentlyActive]: (event: MatrixEvent | undefined, user: User) => void;
|
||||||
[UserEvent.LastPresenceTs]: (event: MatrixEvent | undefined, user: User) => void;
|
[UserEvent.LastPresenceTs]: (event: MatrixEvent | undefined, user: User) => void;
|
||||||
[UserEvent._UnstableStatusMessage]: (user: User) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export class User extends TypedEventEmitter<UserEvent, UserEventHandlerMap> {
|
export class User extends TypedEventEmitter<UserEvent, UserEventHandlerMap> {
|
||||||
@@ -59,8 +56,6 @@ export class User extends TypedEventEmitter<UserEvent, UserEventHandlerMap> {
|
|||||||
presence: null,
|
presence: null,
|
||||||
profile: null,
|
profile: null,
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
public unstable_statusMessage = "";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a new User. A User must have an ID and can optionally have extra
|
* Construct a new User. A User must have an ID and can optionally have extra
|
||||||
@@ -81,9 +76,6 @@ export class User extends TypedEventEmitter<UserEvent, UserEventHandlerMap> {
|
|||||||
* when a user was last active.
|
* when a user was last active.
|
||||||
* @prop {Boolean} currentlyActive Whether we should consider lastActiveAgo to be
|
* @prop {Boolean} currentlyActive Whether we should consider lastActiveAgo to be
|
||||||
* an approximation and that the user should be seen as active 'now'
|
* an approximation and that the user should be seen as active 'now'
|
||||||
* @prop {string} unstable_statusMessage The status message for the user, if known. This is
|
|
||||||
* different from the presenceStatusMsg in that this is not tied to
|
|
||||||
* the user's presence, and should be represented differently.
|
|
||||||
* @prop {Object} events The events describing this user.
|
* @prop {Object} events The events describing this user.
|
||||||
* @prop {MatrixEvent} events.presence The m.presence event for this user.
|
* @prop {MatrixEvent} events.presence The m.presence event for this user.
|
||||||
*/
|
*/
|
||||||
@@ -219,19 +211,6 @@ export class User extends TypedEventEmitter<UserEvent, UserEventHandlerMap> {
|
|||||||
public getLastActiveTs(): number {
|
public getLastActiveTs(): number {
|
||||||
return this.lastPresenceTs - this.lastActiveAgo;
|
return this.lastPresenceTs - this.lastActiveAgo;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Manually set the user's status message.
|
|
||||||
* @param {MatrixEvent} event The <code>im.vector.user_status</code> event.
|
|
||||||
* @fires module:client~MatrixClient#event:"User.unstable_statusMessage"
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line
|
|
||||||
public unstable_updateStatusMessage(event: MatrixEvent): void {
|
|
||||||
if (!event.getContent()) this.unstable_statusMessage = "";
|
|
||||||
else this.unstable_statusMessage = event.getContent()["status"];
|
|
||||||
this.updateModifiedTime();
|
|
||||||
this.emit(UserEvent._UnstableStatusMessage, this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
PushRuleSet,
|
PushRuleSet,
|
||||||
TweakName,
|
TweakName,
|
||||||
} from "./@types/PushRules";
|
} from "./@types/PushRules";
|
||||||
|
import { EventType } from "./@types/event";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module pushprocessor
|
* @module pushprocessor
|
||||||
@@ -55,31 +56,6 @@ const RULEKINDS_IN_ORDER = [
|
|||||||
// 2. We often want to start using push rules ahead of the server supporting them,
|
// 2. We often want to start using push rules ahead of the server supporting them,
|
||||||
// and so we can put them here.
|
// and so we can put them here.
|
||||||
const DEFAULT_OVERRIDE_RULES: IPushRule[] = [
|
const DEFAULT_OVERRIDE_RULES: IPushRule[] = [
|
||||||
{
|
|
||||||
// For homeservers which don't support MSC1930 yet
|
|
||||||
rule_id: ".m.rule.tombstone",
|
|
||||||
default: true,
|
|
||||||
enabled: true,
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
kind: ConditionKind.EventMatch,
|
|
||||||
key: "type",
|
|
||||||
pattern: "m.room.tombstone",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: ConditionKind.EventMatch,
|
|
||||||
key: "state_key",
|
|
||||||
pattern: "",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
actions: [
|
|
||||||
PushRuleActionName.Notify,
|
|
||||||
{
|
|
||||||
set_tweak: TweakName.Highlight,
|
|
||||||
value: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
// For homeservers which don't support MSC2153 yet
|
// For homeservers which don't support MSC2153 yet
|
||||||
rule_id: ".m.rule.reaction",
|
rule_id: ".m.rule.reaction",
|
||||||
@@ -96,6 +72,20 @@ const DEFAULT_OVERRIDE_RULES: IPushRule[] = [
|
|||||||
PushRuleActionName.DontNotify,
|
PushRuleActionName.DontNotify,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// For homeservers which don't support MSC3786 yet
|
||||||
|
rule_id: ".org.matrix.msc3786.rule.room.server_acl",
|
||||||
|
default: true,
|
||||||
|
enabled: true,
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
kind: ConditionKind.EventMatch,
|
||||||
|
key: "type",
|
||||||
|
pattern: EventType.RoomServerAcl,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions: [],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface IActionsObject {
|
export interface IActionsObject {
|
||||||
@@ -300,7 +290,7 @@ export class PushProcessor {
|
|||||||
|
|
||||||
const memberCount = room.currentState.getJoinedMemberCount();
|
const memberCount = room.currentState.getJoinedMemberCount();
|
||||||
|
|
||||||
const m = cond.is.match(/^([=<>]*)([0-9]*)$/);
|
const m = cond.is.match(/^([=<>]*)(\d*)$/);
|
||||||
if (!m) {
|
if (!m) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,19 +15,18 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventType } from "../@types/event";
|
import { EventType } from "../@types/event";
|
||||||
import { Group } from "../models/group";
|
|
||||||
import { Room } from "../models/room";
|
import { Room } from "../models/room";
|
||||||
import { User } from "../models/user";
|
import { User } from "../models/user";
|
||||||
import { IEvent, MatrixEvent } from "../models/event";
|
import { MatrixEvent } from "../models/event";
|
||||||
import { Filter } from "../filter";
|
import { Filter } from "../filter";
|
||||||
import { RoomSummary } from "../models/room-summary";
|
import { RoomSummary } from "../models/room-summary";
|
||||||
import { IMinimalEvent, IGroups, IRooms, ISyncResponse } from "../sync-accumulator";
|
import { IMinimalEvent, IRooms, ISyncResponse } from "../sync-accumulator";
|
||||||
import { IStartClientOpts } from "../client";
|
import { IStartClientOpts } from "../client";
|
||||||
|
import { IStateEventWithRoomId } from "../@types/search";
|
||||||
|
|
||||||
export interface ISavedSync {
|
export interface ISavedSync {
|
||||||
nextBatch: string;
|
nextBatch: string;
|
||||||
roomsData: IRooms;
|
roomsData: IRooms;
|
||||||
groupsData: IGroups;
|
|
||||||
accountData: IMinimalEvent[];
|
accountData: IMinimalEvent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +37,11 @@ export interface ISavedSync {
|
|||||||
export interface IStore {
|
export interface IStore {
|
||||||
readonly accountData: Record<string, MatrixEvent>; // type : content
|
readonly accountData: Record<string, MatrixEvent>; // type : content
|
||||||
|
|
||||||
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
|
// XXX: The indexeddb store exposes a non-standard emitter for the "degraded" event
|
||||||
|
// for when it falls back to being a memory store due to errors.
|
||||||
|
on?: (event: string, handler: (...args: any[]) => void) => void;
|
||||||
|
|
||||||
|
/** @return {Promise<boolean>} whether or not the database was newly created in this session. */
|
||||||
isNewlyCreated(): Promise<boolean>;
|
isNewlyCreated(): Promise<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,28 +56,6 @@ export interface IStore {
|
|||||||
*/
|
*/
|
||||||
setSyncToken(token: string): void;
|
setSyncToken(token: string): void;
|
||||||
|
|
||||||
/**
|
|
||||||
* No-op.
|
|
||||||
* @param {Group} group
|
|
||||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
|
||||||
*/
|
|
||||||
storeGroup(group: Group): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* No-op.
|
|
||||||
* @param {string} groupId
|
|
||||||
* @return {null}
|
|
||||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
|
||||||
*/
|
|
||||||
getGroup(groupId: string): Group | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* No-op.
|
|
||||||
* @return {Array} An empty array.
|
|
||||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
|
||||||
*/
|
|
||||||
getGroups(): Group[];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No-op.
|
* No-op.
|
||||||
* @param {Room} room
|
* @param {Room} room
|
||||||
@@ -128,7 +109,7 @@ export interface IStore {
|
|||||||
/**
|
/**
|
||||||
* No-op.
|
* No-op.
|
||||||
* @param {Room} room
|
* @param {Room} room
|
||||||
* @param {integer} limit
|
* @param {number} limit
|
||||||
* @return {Array}
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
scrollback(room: Room, limit: number): MatrixEvent[];
|
scrollback(room: Room, limit: number): MatrixEvent[];
|
||||||
@@ -228,9 +209,9 @@ export interface IStore {
|
|||||||
*/
|
*/
|
||||||
deleteAllData(): Promise<void>;
|
deleteAllData(): Promise<void>;
|
||||||
|
|
||||||
getOutOfBandMembers(roomId: string): Promise<IEvent[] | null>;
|
getOutOfBandMembers(roomId: string): Promise<IStateEventWithRoomId[] | null>;
|
||||||
|
|
||||||
setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void>;
|
setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void>;
|
||||||
|
|
||||||
clearOutOfBandMembers(roomId: string): Promise<void>;
|
clearOutOfBandMembers(roomId: string): Promise<void>;
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { ISavedSync } from "./index";
|
import { ISavedSync } from "./index";
|
||||||
import { IEvent, IStartClientOpts, ISyncResponse } from "..";
|
import { IEvent, IStartClientOpts, IStateEventWithRoomId, ISyncResponse } from "..";
|
||||||
|
|
||||||
export interface IIndexedDBBackend {
|
export interface IIndexedDBBackend {
|
||||||
connect(): Promise<void>;
|
connect(): Promise<void>;
|
||||||
@@ -25,8 +25,8 @@ export interface IIndexedDBBackend {
|
|||||||
getSavedSync(): Promise<ISavedSync>;
|
getSavedSync(): Promise<ISavedSync>;
|
||||||
getNextBatchToken(): Promise<string>;
|
getNextBatchToken(): Promise<string>;
|
||||||
clearDatabase(): Promise<void>;
|
clearDatabase(): Promise<void>;
|
||||||
getOutOfBandMembers(roomId: string): Promise<IEvent[] | null>;
|
getOutOfBandMembers(roomId: string): Promise<IStateEventWithRoomId[] | null>;
|
||||||
setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void>;
|
setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void>;
|
||||||
clearOutOfBandMembers(roomId: string): Promise<void>;
|
clearOutOfBandMembers(roomId: string): Promise<void>;
|
||||||
getUserPresenceEvents(): Promise<UserTuple[]>;
|
getUserPresenceEvents(): Promise<UserTuple[]>;
|
||||||
getClientOptions(): Promise<IStartClientOpts>;
|
getClientOptions(): Promise<IStartClientOpts>;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { IMinimalEvent, ISyncData, ISyncResponse, SyncAccumulator } from "../syn
|
|||||||
import * as utils from "../utils";
|
import * as utils from "../utils";
|
||||||
import * as IndexedDBHelpers from "../indexeddb-helpers";
|
import * as IndexedDBHelpers from "../indexeddb-helpers";
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import { IEvent, IStartClientOpts } from "..";
|
import { IStartClientOpts, IStateEventWithRoomId } from "..";
|
||||||
import { ISavedSync } from "./index";
|
import { ISavedSync } from "./index";
|
||||||
import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend";
|
import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend";
|
||||||
|
|
||||||
@@ -127,6 +127,8 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
|||||||
private db: IDBDatabase = null;
|
private db: IDBDatabase = null;
|
||||||
private disconnected = true;
|
private disconnected = true;
|
||||||
private _isNewlyCreated = false;
|
private _isNewlyCreated = false;
|
||||||
|
private isPersisting = false;
|
||||||
|
private pendingUserPresenceData: UserTuple[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does the actual reading from and writing to the indexeddb
|
* Does the actual reading from and writing to the indexeddb
|
||||||
@@ -215,7 +217,6 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
|||||||
this.syncAccumulator.accumulate({
|
this.syncAccumulator.accumulate({
|
||||||
next_batch: syncData.nextBatch,
|
next_batch: syncData.nextBatch,
|
||||||
rooms: syncData.roomsData,
|
rooms: syncData.roomsData,
|
||||||
groups: syncData.groupsData,
|
|
||||||
account_data: {
|
account_data: {
|
||||||
events: accountData,
|
events: accountData,
|
||||||
},
|
},
|
||||||
@@ -230,15 +231,15 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
|||||||
* @returns {Promise<event[]>} the events, potentially an empty array if OOB loading didn't yield any new members
|
* @returns {Promise<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
|
* @returns {null} in case the members for this room haven't been stored yet
|
||||||
*/
|
*/
|
||||||
public getOutOfBandMembers(roomId: string): Promise<IEvent[] | null> {
|
public getOutOfBandMembers(roomId: string): Promise<IStateEventWithRoomId[] | null> {
|
||||||
return new Promise<IEvent[] | null>((resolve, reject) => {
|
return new Promise<IStateEventWithRoomId[] | null>((resolve, reject) => {
|
||||||
const tx = this.db.transaction(["oob_membership_events"], "readonly");
|
const tx = this.db.transaction(["oob_membership_events"], "readonly");
|
||||||
const store = tx.objectStore("oob_membership_events");
|
const store = tx.objectStore("oob_membership_events");
|
||||||
const roomIndex = store.index("room");
|
const roomIndex = store.index("room");
|
||||||
const range = IDBKeyRange.only(roomId);
|
const range = IDBKeyRange.only(roomId);
|
||||||
const request = roomIndex.openCursor(range);
|
const request = roomIndex.openCursor(range);
|
||||||
|
|
||||||
const membershipEvents: IEvent[] = [];
|
const membershipEvents: IStateEventWithRoomId[] = [];
|
||||||
// did we encounter the oob_written marker object
|
// did we encounter the oob_written marker object
|
||||||
// amongst the results? That means OOB member
|
// amongst the results? That means OOB member
|
||||||
// loading already happened for this room
|
// loading already happened for this room
|
||||||
@@ -279,7 +280,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
|||||||
* @param {string} roomId
|
* @param {string} roomId
|
||||||
* @param {event[]} membershipEvents the membership events to store
|
* @param {event[]} membershipEvents the membership events to store
|
||||||
*/
|
*/
|
||||||
public async setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
|
public async setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void> {
|
||||||
logger.log(`LL: backend about to store ${membershipEvents.length}` +
|
logger.log(`LL: backend about to store ${membershipEvents.length}` +
|
||||||
` members for ${roomId}`);
|
` members for ${roomId}`);
|
||||||
const tx = this.db.transaction(["oob_membership_events"], "readwrite");
|
const tx = this.db.transaction(["oob_membership_events"], "readwrite");
|
||||||
@@ -402,24 +403,35 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
|||||||
public async syncToDatabase(userTuples: UserTuple[]): Promise<void> {
|
public async syncToDatabase(userTuples: UserTuple[]): Promise<void> {
|
||||||
const syncData = this.syncAccumulator.getJSON(true);
|
const syncData = this.syncAccumulator.getJSON(true);
|
||||||
|
|
||||||
|
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([
|
await Promise.all([
|
||||||
this.persistUserPresenceEvents(userTuples),
|
this.persistUserPresenceEvents(userTuples),
|
||||||
this.persistAccountData(syncData.accountData),
|
this.persistAccountData(syncData.accountData),
|
||||||
this.persistSyncData(syncData.nextBatch, syncData.roomsData, syncData.groupsData),
|
this.persistSyncData(syncData.nextBatch, syncData.roomsData),
|
||||||
]);
|
]);
|
||||||
|
} finally {
|
||||||
|
this.isPersisting = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persist rooms /sync data along with the next batch token.
|
* Persist rooms /sync data along with the next batch token.
|
||||||
* @param {string} nextBatch The next_batch /sync value.
|
* @param {string} nextBatch The next_batch /sync value.
|
||||||
* @param {Object} roomsData The 'rooms' /sync data from a SyncAccumulator
|
* @param {Object} roomsData The 'rooms' /sync data from a SyncAccumulator
|
||||||
* @param {Object} groupsData The 'groups' /sync data from a SyncAccumulator
|
|
||||||
* @return {Promise} Resolves if the data was persisted.
|
* @return {Promise} Resolves if the data was persisted.
|
||||||
*/
|
*/
|
||||||
private persistSyncData(
|
private persistSyncData(
|
||||||
nextBatch: string,
|
nextBatch: string,
|
||||||
roomsData: ISyncResponse["rooms"],
|
roomsData: ISyncResponse["rooms"],
|
||||||
groupsData: ISyncResponse["groups"],
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
logger.log("Persisting sync data up to", nextBatch);
|
logger.log("Persisting sync data up to", nextBatch);
|
||||||
return utils.promiseTry<void>(() => {
|
return utils.promiseTry<void>(() => {
|
||||||
@@ -429,9 +441,10 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
|||||||
clobber: "-", // constant key so will always clobber
|
clobber: "-", // constant key so will always clobber
|
||||||
nextBatch,
|
nextBatch,
|
||||||
roomsData,
|
roomsData,
|
||||||
groupsData,
|
|
||||||
}); // put == UPSERT
|
}); // put == UPSERT
|
||||||
return txnAsPromise(txn).then();
|
return txnAsPromise(txn).then(() => {
|
||||||
|
logger.log("Persisted sync data up to", nextBatch);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -534,9 +547,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
|||||||
const txn = this.db.transaction(["client_options"], "readonly");
|
const txn = this.db.transaction(["client_options"], "readonly");
|
||||||
const store = txn.objectStore("client_options");
|
const store = txn.objectStore("client_options");
|
||||||
return selectQuery(store, undefined, (cursor) => {
|
return selectQuery(store, undefined, (cursor) => {
|
||||||
if (cursor.value && cursor.value && cursor.value.options) {
|
return cursor.value?.options;
|
||||||
return cursor.value.options;
|
|
||||||
}
|
|
||||||
}).then((results) => results[0]);
|
}).then((results) => results[0]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { logger } from "../logger";
|
|||||||
import { defer, IDeferred } from "../utils";
|
import { defer, IDeferred } from "../utils";
|
||||||
import { ISavedSync } from "./index";
|
import { ISavedSync } from "./index";
|
||||||
import { IStartClientOpts } from "../client";
|
import { IStartClientOpts } from "../client";
|
||||||
import { IEvent, ISyncResponse } from "..";
|
import { IStateEventWithRoomId, ISyncResponse } from "..";
|
||||||
import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend";
|
import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend";
|
||||||
|
|
||||||
export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend {
|
export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||||
@@ -97,7 +97,7 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend {
|
|||||||
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
|
* @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
|
* @returns {null} in case the members for this room haven't been stored yet
|
||||||
*/
|
*/
|
||||||
public getOutOfBandMembers(roomId: string): Promise<IEvent[] | null> {
|
public getOutOfBandMembers(roomId: string): Promise<IStateEventWithRoomId[] | null> {
|
||||||
return this.doCmd('getOutOfBandMembers', [roomId]);
|
return this.doCmd('getOutOfBandMembers', [roomId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend {
|
|||||||
* @param {event[]} membershipEvents the membership events to store
|
* @param {event[]} membershipEvents the membership events to store
|
||||||
* @returns {Promise} when all members have been stored
|
* @returns {Promise} when all members have been stored
|
||||||
*/
|
*/
|
||||||
public setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
|
public setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void> {
|
||||||
return this.doCmd('setOutOfBandMembers', [roomId, membershipEvents]);
|
return this.doCmd('setOutOfBandMembers', [roomId, membershipEvents]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { ISavedSync } from "./index";
|
|||||||
import { IIndexedDBBackend } from "./indexeddb-backend";
|
import { IIndexedDBBackend } from "./indexeddb-backend";
|
||||||
import { ISyncResponse } from "../sync-accumulator";
|
import { ISyncResponse } from "../sync-accumulator";
|
||||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||||
|
import { IStateEventWithRoomId } from "../@types/search";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is an internal module. See {@link IndexedDBStore} for the public class.
|
* This is an internal module. See {@link IndexedDBStore} for the public class.
|
||||||
@@ -242,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 {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
|
* @returns {null} in case the members for this room haven't been stored yet
|
||||||
*/
|
*/
|
||||||
public getOutOfBandMembers = this.degradable((roomId: string): Promise<IEvent[]> => {
|
public getOutOfBandMembers = this.degradable((roomId: string): Promise<IStateEventWithRoomId[]> => {
|
||||||
return this.backend.getOutOfBandMembers(roomId);
|
return this.backend.getOutOfBandMembers(roomId);
|
||||||
}, "getOutOfBandMembers");
|
}, "getOutOfBandMembers");
|
||||||
|
|
||||||
@@ -254,10 +255,13 @@ export class IndexedDBStore extends MemoryStore {
|
|||||||
* @param {event[]} membershipEvents the membership events to store
|
* @param {event[]} membershipEvents the membership events to store
|
||||||
* @returns {Promise} when all members have been stored
|
* @returns {Promise} when all members have been stored
|
||||||
*/
|
*/
|
||||||
public setOutOfBandMembers = this.degradable((roomId: string, membershipEvents: IEvent[]): Promise<void> => {
|
public setOutOfBandMembers = this.degradable(
|
||||||
|
(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void> => {
|
||||||
super.setOutOfBandMembers(roomId, membershipEvents);
|
super.setOutOfBandMembers(roomId, membershipEvents);
|
||||||
return this.backend.setOutOfBandMembers(roomId, membershipEvents);
|
return this.backend.setOutOfBandMembers(roomId, membershipEvents);
|
||||||
}, "setOutOfBandMembers");
|
},
|
||||||
|
"setOutOfBandMembers",
|
||||||
|
);
|
||||||
|
|
||||||
public clearOutOfBandMembers = this.degradable((roomId: string) => {
|
public clearOutOfBandMembers = this.degradable((roomId: string) => {
|
||||||
super.clearOutOfBandMembers(roomId);
|
super.clearOutOfBandMembers(roomId);
|
||||||
@@ -293,7 +297,7 @@ export class IndexedDBStore extends MemoryStore {
|
|||||||
|
|
||||||
return async (...args) => {
|
return async (...args) => {
|
||||||
try {
|
try {
|
||||||
return func.call(this, ...args);
|
return await func.call(this, ...args);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("IndexedDBStore failure, degrading to MemoryStore", e);
|
logger.error("IndexedDBStore failure, degrading to MemoryStore", e);
|
||||||
this.emitter.emit("degraded", e);
|
this.emitter.emit("degraded", e);
|
||||||
|
|||||||
@@ -20,16 +20,16 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventType } from "../@types/event";
|
import { EventType } from "../@types/event";
|
||||||
import { Group } from "../models/group";
|
|
||||||
import { Room } from "../models/room";
|
import { Room } from "../models/room";
|
||||||
import { User } from "../models/user";
|
import { User } from "../models/user";
|
||||||
import { IEvent, MatrixEvent } from "../models/event";
|
import { MatrixEvent } from "../models/event";
|
||||||
import { RoomState, RoomStateEvent } from "../models/room-state";
|
import { RoomState, RoomStateEvent } from "../models/room-state";
|
||||||
import { RoomMember } from "../models/room-member";
|
import { RoomMember } from "../models/room-member";
|
||||||
import { Filter } from "../filter";
|
import { Filter } from "../filter";
|
||||||
import { ISavedSync, IStore } from "./index";
|
import { ISavedSync, IStore } from "./index";
|
||||||
import { RoomSummary } from "../models/room-summary";
|
import { RoomSummary } from "../models/room-summary";
|
||||||
import { ISyncResponse } from "../sync-accumulator";
|
import { ISyncResponse } from "../sync-accumulator";
|
||||||
|
import { IStateEventWithRoomId } from "../@types/search";
|
||||||
|
|
||||||
function isValidFilterId(filterId: string): boolean {
|
function isValidFilterId(filterId: string): boolean {
|
||||||
const isValidStr = typeof filterId === "string" &&
|
const isValidStr = typeof filterId === "string" &&
|
||||||
@@ -53,7 +53,6 @@ export interface IOpts {
|
|||||||
*/
|
*/
|
||||||
export class MemoryStore implements IStore {
|
export class MemoryStore implements IStore {
|
||||||
private rooms: Record<string, Room> = {}; // roomId: Room
|
private rooms: Record<string, Room> = {}; // roomId: Room
|
||||||
private groups: Record<string, Group> = {}; // groupId: Group
|
|
||||||
private users: Record<string, User> = {}; // userId: User
|
private users: Record<string, User> = {}; // userId: User
|
||||||
private syncToken: string = null;
|
private syncToken: string = null;
|
||||||
// userId: {
|
// userId: {
|
||||||
@@ -62,7 +61,7 @@ export class MemoryStore implements IStore {
|
|||||||
private filters: Record<string, Record<string, Filter>> = {};
|
private filters: Record<string, Record<string, Filter>> = {};
|
||||||
public accountData: Record<string, MatrixEvent> = {}; // type : content
|
public accountData: Record<string, MatrixEvent> = {}; // type : content
|
||||||
private readonly localStorage: Storage;
|
private readonly localStorage: Storage;
|
||||||
private oobMembers: Record<string, IEvent[]> = {}; // roomId: [member events]
|
private oobMembers: Record<string, IStateEventWithRoomId[]> = {}; // roomId: [member events]
|
||||||
private clientOptions = {};
|
private clientOptions = {};
|
||||||
|
|
||||||
constructor(opts: IOpts = {}) {
|
constructor(opts: IOpts = {}) {
|
||||||
@@ -90,34 +89,6 @@ export class MemoryStore implements IStore {
|
|||||||
this.syncToken = token;
|
this.syncToken = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Store the given room.
|
|
||||||
* @param {Group} group The group to be stored
|
|
||||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
|
||||||
*/
|
|
||||||
public storeGroup(group: Group) {
|
|
||||||
this.groups[group.groupId] = group;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve a group by its group ID.
|
|
||||||
* @param {string} groupId The group ID.
|
|
||||||
* @return {Group} The group or null.
|
|
||||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
|
||||||
*/
|
|
||||||
public getGroup(groupId: string): Group | null {
|
|
||||||
return this.groups[groupId] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve all known groups.
|
|
||||||
* @return {Group[]} A list of groups, which may be empty.
|
|
||||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
|
||||||
*/
|
|
||||||
public getGroups(): Group[] {
|
|
||||||
return Object.values(this.groups);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store the given room.
|
* Store the given room.
|
||||||
* @param {Room} room The room to be stored. All properties must be stored.
|
* @param {Room} room The room to be stored. All properties must be stored.
|
||||||
@@ -228,7 +199,7 @@ export class MemoryStore implements IStore {
|
|||||||
/**
|
/**
|
||||||
* Retrieve scrollback for this room.
|
* Retrieve scrollback for this room.
|
||||||
* @param {Room} room The matrix room
|
* @param {Room} room The matrix room
|
||||||
* @param {integer} limit The max number of old events to retrieve.
|
* @param {number} limit The max number of old events to retrieve.
|
||||||
* @return {Array<Object>} An array of objects which will be at most 'limit'
|
* @return {Array<Object>} An array of objects which will be at most 'limit'
|
||||||
* length and at least 0. The objects are the raw event JSON.
|
* length and at least 0. The objects are the raw event JSON.
|
||||||
*/
|
*/
|
||||||
@@ -419,7 +390,7 @@ export class MemoryStore implements IStore {
|
|||||||
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
|
* @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
|
* @returns {null} in case the members for this room haven't been stored yet
|
||||||
*/
|
*/
|
||||||
public getOutOfBandMembers(roomId: string): Promise<IEvent[] | null> {
|
public getOutOfBandMembers(roomId: string): Promise<IStateEventWithRoomId[] | null> {
|
||||||
return Promise.resolve(this.oobMembers[roomId] || null);
|
return Promise.resolve(this.oobMembers[roomId] || null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,7 +402,7 @@ export class MemoryStore implements IStore {
|
|||||||
* @param {event[]} membershipEvents the membership events to store
|
* @param {event[]} membershipEvents the membership events to store
|
||||||
* @returns {Promise} when all members have been stored
|
* @returns {Promise} when all members have been stored
|
||||||
*/
|
*/
|
||||||
public setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
|
public setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void> {
|
||||||
this.oobMembers[roomId] = membershipEvents;
|
this.oobMembers[roomId] = membershipEvents;
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ WebStorageSessionStore.prototype = {
|
|||||||
const devices = {};
|
const devices = {};
|
||||||
for (let i = 0; i < this.store.length; ++i) {
|
for (let i = 0; i < this.store.length; ++i) {
|
||||||
const key = this.store.key(i);
|
const key = this.store.key(i);
|
||||||
const userId = key.substr(prefix.length);
|
const userId = key.slice(prefix.length);
|
||||||
if (key.startsWith(prefix)) devices[userId] = getJsonItem(this.store, key);
|
if (key.startsWith(prefix)) devices[userId] = getJsonItem(this.store, key);
|
||||||
}
|
}
|
||||||
return devices;
|
return devices;
|
||||||
@@ -125,7 +125,7 @@ WebStorageSessionStore.prototype = {
|
|||||||
const deviceKeys = getKeysWithPrefix(this.store, keyEndToEndSessions(''));
|
const deviceKeys = getKeysWithPrefix(this.store, keyEndToEndSessions(''));
|
||||||
const results = {};
|
const results = {};
|
||||||
for (const k of deviceKeys) {
|
for (const k of deviceKeys) {
|
||||||
const unprefixedKey = k.substr(keyEndToEndSessions('').length);
|
const unprefixedKey = k.slice(keyEndToEndSessions('').length);
|
||||||
results[unprefixedKey] = getJsonItem(this.store, k);
|
results[unprefixedKey] = getJsonItem(this.store, k);
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
@@ -158,8 +158,8 @@ WebStorageSessionStore.prototype = {
|
|||||||
// (hence 43 characters long).
|
// (hence 43 characters long).
|
||||||
|
|
||||||
result.push({
|
result.push({
|
||||||
senderKey: key.substr(prefix.length, 43),
|
senderKey: key.slice(prefix.length, prefix.length + 43),
|
||||||
sessionId: key.substr(prefix.length + 44),
|
sessionId: key.slice(prefix.length + 44),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@@ -182,7 +182,7 @@ WebStorageSessionStore.prototype = {
|
|||||||
const roomKeys = getKeysWithPrefix(this.store, keyEndToEndRoom(''));
|
const roomKeys = getKeysWithPrefix(this.store, keyEndToEndRoom(''));
|
||||||
const results = {};
|
const results = {};
|
||||||
for (const k of roomKeys) {
|
for (const k of roomKeys) {
|
||||||
const unprefixedKey = k.substr(keyEndToEndRoom('').length);
|
const unprefixedKey = k.slice(keyEndToEndRoom('').length);
|
||||||
results[unprefixedKey] = getJsonItem(this.store, k);
|
results[unprefixedKey] = getJsonItem(this.store, k);
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventType } from "../@types/event";
|
import { EventType } from "../@types/event";
|
||||||
import { Group } from "../models/group";
|
|
||||||
import { Room } from "../models/room";
|
import { Room } from "../models/room";
|
||||||
import { User } from "../models/user";
|
import { User } from "../models/user";
|
||||||
import { IEvent, MatrixEvent } from "../models/event";
|
import { MatrixEvent } from "../models/event";
|
||||||
import { Filter } from "../filter";
|
import { Filter } from "../filter";
|
||||||
import { ISavedSync, IStore } from "./index";
|
import { ISavedSync, IStore } from "./index";
|
||||||
import { RoomSummary } from "../models/room-summary";
|
import { RoomSummary } from "../models/room-summary";
|
||||||
import { ISyncResponse } from "../sync-accumulator";
|
import { ISyncResponse } from "../sync-accumulator";
|
||||||
|
import { IStateEventWithRoomId } from "../@types/search";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a stub store. This does no-ops on most store methods.
|
* Construct a stub store. This does no-ops on most store methods.
|
||||||
@@ -58,32 +58,6 @@ export class StubStore implements IStore {
|
|||||||
this.fromToken = token;
|
this.fromToken = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* No-op.
|
|
||||||
* @param {Group} group
|
|
||||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
|
||||||
*/
|
|
||||||
public storeGroup(group: Group) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* No-op.
|
|
||||||
* @param {string} groupId
|
|
||||||
* @return {null}
|
|
||||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
|
||||||
*/
|
|
||||||
public getGroup(groupId: string): Group | null {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* No-op.
|
|
||||||
* @return {Array} An empty array.
|
|
||||||
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
|
|
||||||
*/
|
|
||||||
public getGroups(): Group[] {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No-op.
|
* No-op.
|
||||||
* @param {Room} room
|
* @param {Room} room
|
||||||
@@ -149,7 +123,7 @@ export class StubStore implements IStore {
|
|||||||
/**
|
/**
|
||||||
* No-op.
|
* No-op.
|
||||||
* @param {Room} room
|
* @param {Room} room
|
||||||
* @param {integer} limit
|
* @param {number} limit
|
||||||
* @return {Array}
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
public scrollback(room: Room, limit: number): MatrixEvent[] {
|
public scrollback(room: Room, limit: number): MatrixEvent[] {
|
||||||
@@ -269,11 +243,11 @@ export class StubStore implements IStore {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getOutOfBandMembers(): Promise<IEvent[]> {
|
public getOutOfBandMembers(): Promise<IStateEventWithRoomId[]> {
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
|
public setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void> {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user