Compare commits

...

34 Commits

Author SHA1 Message Date
RiotRobot ba371f7468 v18.0.0-rc.1 2022-05-17 18:26:33 +01:00
RiotRobot 761facf98a Prepare changelog for v18.0.0-rc.1 2022-05-17 18:26:32 +01:00
Travis Ralston c6af997542 Add a catastrophic throw to thread constructor (#2375)
This is an attempt to narrow down https://github.com/vector-im/element-web/issues/22141
2022-05-17 00:20:41 -06:00
Travis Ralston e9e8e90a94 Remove default push rule override for MSC1930 (#2376)
Folks have had since Matrix 1.0 (June 2019) to upgrade to a compatible server
2022-05-17 00:13:43 -06:00
Johannes Marbach f44510e65f Add support for HTML renderings of room topics (#2272)
* Add support for HTML renderings of room topics

Based on extensible events as defined in [MSC1767]

Relates to: vector-im/element-web#5180
Signed-off-by: Johannes Marbach <johannesm@element.io>

[MSC1767]: https://github.com/matrix-org/matrix-spec-proposals/pull/1767

* Use correct MSC

* Add overloads for setRoomTopic

* Fix indentation

* Add more tests to pass the quality gate

Co-authored-by: Johannes Marbach <jm@Johanness-Mini.fritz.box>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-05-16 10:37:34 +01:00
Michael Telatynski ba1f6ffc84 Tweak thread creation & event adding to fix bugs around relations (#2369)
* Remove legacy code which caused threads to begin life with too many events

* Update tests & behaviour
2022-05-16 09:01:39 +01:00
Michael Telatynski 3e4f02b41e Update notify-downstream.yaml (#2371) 2022-05-14 01:27:54 -06:00
Michael Telatynski af17fb27b8 Attempt to re-structure workflows to be more generic & reusable (#2364)
* Attempt to re-structure workflows to be more generic & reusable

* Iterate for reusable workflows can't call each other

* don't pass pullrequest params if no prnumber

* Comments

* Fix reusable workflow call

* Pass pr_id properly

* Fix run condition for prdetails job

* Fix needs dependency

* Stash work so far

* Fix copypasta

* Update

* Define outputs from pr_details.yml

* Fix output reporting

* Fix something or other
2022-05-13 23:14:46 +01:00
Michael Telatynski 72013341db More sonar tweaks and typing improvements (#2366)
* More sonar tweaks and typing improvements

* delint

* Write some tests

* Attempt to make TS happy

* Stash tests

* Add tests

* Add `istanbul ignore if` around logging special-case for test env

* Add test

* Comments
2022-05-13 18:08:36 +00:00
Michael Telatynski 6f445ca99a Add stopClient parameter to MatrixClient::logout (#2367)
* Add stopClient parameter to MatrixClient::logout

* Add short-circuit
2022-05-13 19:00:06 +01:00
Michael Telatynski e2af78d8d3 Make pull_request.yaml between the layers consistent and fix enforce labels (#2368) 2022-05-13 18:51:48 +01:00
Michael Telatynski 4721aa1d24 Fix up more types & Sonar warnings (#2363)
* Fix up more types & Sonar warnings

* Fix test

* Add first test for callEventHandler
2022-05-12 10:12:39 +01:00
Michael Telatynski 4d4d6e1411 NodeURL isn't needed as Node exports the standard URL c'tor as global (#2361)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2022-05-11 17:04:10 +01:00
Michael Telatynski 67f5293d6c Tweaks for sonar to correctly report on forked PRs (#2359) 2022-05-11 16:42:25 +01:00
Janne Mareike Koschinski 923ff4b282 registration: add function to re-request email token (#2357) 2022-05-11 13:17:36 +02:00
Michael Telatynski 49dd76b91e Remove redundant checkKey param on isSecretStored (#2360) 2022-05-11 10:53:52 +01:00
Michael Telatynski 34cfa51104 Pass more args to Sonar (#2358)
* Pass projectVersion to Sonar

* Fix s'more

* Fix sonar.lang.patterns.ts

* Apply more tweaks

Based on https://community.sonarsource.com/t/how-to-use-sonarcloud-with-a-forked-repository-on-github/7363/28
2022-05-10 17:11:07 +01:00
Michael Telatynski 685276f056 Pass more args to Sonar (#2356)
* Pass projectVersion to Sonar

* Fix s'more

* Fix sonar.lang.patterns.ts
2022-05-10 16:49:03 +01:00
RiotRobot 03a79dc6dd Resetting package fields for development 2022-05-10 14:50:07 +01:00
RiotRobot 83a4881498 Merge branch 'master' into develop 2022-05-10 14:50:07 +01:00
Travis Ralston 62d77231af Remove spec v1.3 check for threads (#2354)
* Remove spec v1.3 check for threads

Citation: https://matrix.to/#/!ewdjhNcPcEmYNKzlWp:t2l.io/$CkPuvKdFZyFL547JCy5J3MfvLaWUo_a1XEdmiop1PKc?via=matrix.org&via=element.io&via=envs.net

* Enable stable support always for threads

* Fix tests differently
2022-05-09 16:11:04 -06:00
Michael Telatynski 706b4d6054 Improve typing (#2352)
* Fix typing of the store interface

* Fix typed s'more

* re-add check

* Be less dumb

* arg

* Fix types
2022-05-09 11:58:52 +01:00
Šimon Brandner da69ca215b Implement changes to MSC2285 (private read receipts) (#2221)
* Add `ReceiptType`

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Implement changes to MSC2285

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Improve tests

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Apply suggestions from review

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Update `getEventReadUpTo()` to handle private read receipts

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Write tests for `getEventReadUpTo()`

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Give `getReadReceiptForUserId()` a JSDOC

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Types!

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Try to use receipt `ts`s

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-05-06 21:32:41 +02:00
Travis Ralston 95a94cdbe3 Remove hacky custom status feature (#2350)
This is unstable, so should be more than safe to just outright remove without notice.
2022-05-06 13:20:54 -06:00
Michael Telatynski 6b5f4aa0a9 Prune both clear & wire content on redaction (#2346) 2022-05-05 07:14:23 +01:00
Michael Telatynski dea3f52fe9 Another SonarQube happiness pass (#2347) 2022-05-04 21:34:21 -06:00
Michael Telatynski a388fde3e2 Tweak sonar-project.properties (#2348) 2022-05-04 15:36:00 -04:00
Šimon Brandner 1cde686a13 MSC3786: Add a default push rule to ignore m.room.server_acl events (#2333)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-05-04 16:22:43 +02:00
Michael Telatynski 592f2931dc Add label to dependency upgrade PRs (#2345) 2022-05-04 14:27:42 +01:00
Michael Telatynski 030fcb57a5 Update upgrade_dependencies.yml (#2343) 2022-05-03 22:44:57 +01:00
Michael Telatynski 8be30acb11 Apply suggestions from SonarQube (#2340) 2022-05-03 15:40:57 -06:00
github-actions[bot] b86630f0e3 Upgrade dependencies (#2342)
Co-authored-by: t3chguy <t3chguy@users.noreply.github.com>
2022-05-03 22:10:41 +01:00
Michael Telatynski 81c89f69c5 Create manual action for upgrading dependencies after rc cut (#2341) 2022-05-03 21:54:18 +01:00
Michael Telatynski 9b633251d5 Add README badges (#2335) 2022-05-03 14:52:44 -06:00
70 changed files with 1819 additions and 832 deletions
+14 -3
View File
@@ -2,13 +2,24 @@ name: Notify Downstream Projects
on:
push:
branches: [ develop ]
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
notify-matrix-react-sdk:
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: vector-im/element-web
event-type: upstream-sdk-notify
repository: ${{ matrix.repo }}
event-type: ${{ matrix.event }}
+59
View 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 }}
+5 -1
View File
@@ -1,10 +1,14 @@
name: Pull Request
on:
pull_request_target:
types: [ opened, edited, labeled, unlabeled ]
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
+91
View 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 }}
+29 -39
View File
@@ -4,44 +4,34 @@ on:
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
runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'success'
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
# 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: actions/github-script@v3.1.0
with:
script: |
const artifacts = await github.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: ${{ github.event.workflow_run.id }},
});
const matchArtifact = artifacts.data.artifacts.filter((artifact) => {
return artifact.name == "coverage"
})[0];
const download = await github.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
const fs = require('fs');
fs.writeFileSync('${{github.workspace}}/coverage.zip', Buffer.from(download.data));
- name: Extract Coverage Report
run: unzip -d coverage coverage.zip && rm coverage.zip
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
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 }}
+3
View File
@@ -3,6 +3,9 @@ on:
pull_request: { }
push:
branches: [ develop, master ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ts_lint:
name: "Typescript Syntax Check"
+4 -1
View File
@@ -2,7 +2,10 @@ name: Tests
on:
pull_request: { }
push:
branches: [ develop, main, master ]
branches: [ develop, master ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
jest:
name: Jest
@@ -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 }}
+3
View File
@@ -1,3 +1,6 @@
Changes in [18.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v18.0.0-rc.1) (2022-05-17)
============================================================================================================
Changes in [17.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v17.2.0) (2022-05-10)
==================================================================================================
+8
View File
@@ -1,3 +1,11 @@
[![npm](https://img.shields.io/npm/v/matrix-js-sdk)](https://www.npmjs.com/package/matrix-js-sdk)
![Tests](https://github.com/matrix-org/matrix-js-sdk/actions/workflows/tests.yml/badge.svg)
![Static Analysis](https://github.com/matrix-org/matrix-js-sdk/actions/workflows/static_analysis.yml/badge.svg)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=matrix-js-sdk&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=matrix-js-sdk&metric=coverage)](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=matrix-js-sdk&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=matrix-js-sdk&metric=bugs)](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
Matrix Javascript SDK
=====================
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "17.2.0",
"version": "18.0.0-rc.1",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=12.9.0"
+3 -8
View File
@@ -1,13 +1,6 @@
sonar.projectKey=matrix-js-sdk
sonar.organization=matrix-org
# This is the name and version displayed in the SonarCloud UI.
#sonar.projectName=matrix-js-sdk
#sonar.projectVersion=1.0
# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
#sonar.sources=.
# Encoding of the source code. Default is default system encoding
#sonar.sourceEncoding=UTF-8
@@ -17,5 +10,7 @@ sonar.exclusions=docs,examples,git-hooks
sonar.typescript.tsconfigPath=./tsconfig.json
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.coverage.exclusions=spec/*.ts
sonar.coverage.exclusions=spec/**/*
sonar.testExecutionReportPaths=coverage/test-report.xml
sonar.lang.patterns.ts=**/*.ts,**/*.tsx
+2 -2
View File
@@ -47,7 +47,7 @@ describe("Browserify Test", function() {
httpBackend.stop();
});
it("Sync", async function() {
it("Sync", function() {
const event = utils.mkMembership({
room: ROOM_ID,
mship: "join",
@@ -71,7 +71,7 @@ describe("Browserify Test", function() {
};
httpBackend.when("GET", "/sync").respond(200, syncData);
return await Promise.race([
return Promise.race([
httpBackend.flushAllExpected(),
new Promise((_, reject) => {
client.once("sync.unexpectedError", reject);
@@ -526,8 +526,7 @@ describe("MatrixClient event timelines", function() {
return {
original_event: THREAD_ROOT,
chunk: [THREAD_REPLY],
next_batch: "next_batch_token0",
prev_batch: "prev_batch_token0",
// no next batch as this is the oldest end of the timeline
};
});
@@ -536,8 +535,8 @@ describe("MatrixClient event timelines", function() {
const timeline = await timelinePromise;
expect(timeline.getEvents().find(e => e.getId() === THREAD_ROOT.event_id));
expect(timeline.getEvents().find(e => e.getId() === THREAD_REPLY.event_id));
expect(timeline.getEvents().find(e => e.getId() === THREAD_ROOT.event_id)).toBeTruthy();
expect(timeline.getEvents().find(e => e.getId() === THREAD_REPLY.event_id)).toBeTruthy();
});
});
+181
View File
@@ -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 * as utils from "../test-utils/test-utils";
import { CRYPTO_ENABLED } from "../../src/client";
import { MatrixEvent } from "../../src/models/event";
@@ -823,6 +839,171 @@ describe("MatrixClient", function() {
]);
});
});
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);
});
});
});
function withThreadId(event, newThreadId) {
+72 -1
View File
@@ -17,7 +17,13 @@ limitations under the License.
import { REFERENCE_RELATION } from "matrix-events-sdk";
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('makeBeaconInfoContent()', () => {
@@ -122,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>",
});
});
});
});
+105
View File
@@ -18,6 +18,8 @@ limitations under the License.
import { logger } from "../../src/logger";
import { InteractiveAuth } from "../../src/interactive-auth";
import { MatrixError } from "../../src/http-api";
import { sleep } from "../../src/utils";
import { randomString } from "../../src/randomstring";
// Trivial client object to test interactive auth
// (we do not need TestClient here)
@@ -172,4 +174,107 @@ describe("InteractiveAuth", function() {
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);
});
});
});
+83 -2
View File
@@ -29,10 +29,11 @@ import {
import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib";
import { EventStatus, MatrixEvent } from "../../src/models/event";
import { Preset } from "../../src/@types/partials";
import { ReceiptType } from "../../src/@types/read_receipts";
import * as testUtils from "../test-utils/test-utils";
import { makeBeaconInfoContent } from "../../src/content-helpers";
import { M_BEACON_INFO } from "../../src/@types/beacon";
import { Room } from "../../src";
import { ContentHelpers, Room } from "../../src";
import { makeBeaconEvent } from "../test-utils/beacon";
jest.useFakeTimers();
@@ -90,7 +91,12 @@ describe("MatrixClient", function() {
let pendingLookup = null;
function httpReq(cb, method, path, qp, data, prefix) {
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 logLine = (
@@ -992,6 +998,46 @@ describe("MatrixClient", function() {
});
});
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", () => {
const roomId = '!room:server.org';
const content = makeBeaconInfoContent(100, true);
@@ -1058,6 +1104,41 @@ describe("MatrixClient", function() {
});
});
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';
+27
View File
@@ -57,4 +57,31 @@ describe('MatrixEvent', () => {
expect(a.toSnapshot().isEquivalentTo(a)).toBe(true);
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
View 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");
});
});
});
+17
View File
@@ -1,5 +1,6 @@
import * as utils from "../test-utils/test-utils";
import { PushProcessor } from "../../src/pushprocessor";
import { EventType } from "../../src";
describe('NotificationService', function() {
const testUserId = "@ali:matrix.org";
@@ -208,6 +209,7 @@ describe('NotificationService', function() {
msgtype: "m.text",
},
});
matrixClient.pushRules = PushProcessor.rewriteDefaultRules(matrixClient.pushRules);
pushProcessor = new PushProcessor(matrixClient);
});
@@ -295,6 +297,21 @@ describe('NotificationService', function() {
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
it('should gracefully handle bad input.', function() {
+38 -4
View File
@@ -23,6 +23,7 @@ import * as utils from "../test-utils/test-utils";
import {
DuplicateStrategy,
EventStatus,
EventTimelineSet,
EventType,
JoinRule,
MatrixEvent,
@@ -31,11 +32,12 @@ import {
RoomEvent,
} from "../../src";
import { EventTimeline } from "../../src/models/event-timeline";
import { Room } from "../../src/models/room";
import { IWrappedReceipt, Room } from "../../src/models/room";
import { RoomState } from "../../src/models/room-state";
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
import { TestClient } from "../TestClient";
import { emitPromise } from "../test-utils/test-utils";
import { ReceiptType } from "../../src/@types/read_receipts";
import { Thread, ThreadEvent } from "../../src/models/thread";
describe("Room", function() {
@@ -1935,6 +1937,15 @@ describe("Room", function() {
expect(() => room.createThread(rootEvent.getId(), rootEvent, [])).not.toThrow();
});
it("creating thread from edited event should not conflate old versions of the event", () => {
const message = mkMessage();
const edit = mkEdit(message);
message.makeReplaced(edit);
const thread = room.createThread("$000", message, [], true);
expect(thread).toHaveLength(0);
});
it("Edits update the lastReply event", async () => {
room.client.supportsExperimentalThreads = () => true;
@@ -2034,17 +2045,15 @@ describe("Room", function() {
},
});
let prom = emitPromise(room, ThreadEvent.New);
const prom = emitPromise(room, ThreadEvent.New);
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
const thread = await prom;
expect(thread).toHaveLength(2);
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
prom = emitPromise(thread, ThreadEvent.Update);
const threadResponse2ReactionRedaction = mkRedaction(threadResponse2Reaction);
room.addLiveEvents([threadResponse2ReactionRedaction]);
await prom;
expect(thread).toHaveLength(2);
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
});
@@ -2286,4 +2295,29 @@ describe("Room", function() {
expect(responseRelations[0][1].has(threadReaction)).toBeTruthy();
});
});
describe("getEventReadUpTo()", () => {
const client = new TestClient(userA).client;
const room = new Room(roomId, client, userA);
it("handles missing receipt type", () => {
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
return receiptType === ReceiptType.ReadPrivate ? { eventId: "eventId" } as IWrappedReceipt : null;
};
expect(room.getEventReadUpTo(userA)).toEqual("eventId");
});
it("prefers older receipt", () => {
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
return (receiptType === ReceiptType.Read
? { eventId: "eventId1" }
: { eventId: "eventId2" }
) as IWrappedReceipt;
};
room.getUnfilteredTimelineSet = () => ({ compareEventOrdering: (event1, event2) => 1 } as EventTimelineSet);
expect(room.getEventReadUpTo(userA)).toEqual("eventId1");
});
});
});
+11 -4
View File
@@ -15,6 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { ReceiptType } from "../../src/@types/read_receipts";
import { SyncAccumulator } from "../../src/sync-accumulator";
// 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",
content: {
"$event1:localhost": {
"m.read": {
[ReceiptType.Read]: {
"@alice:localhost": { ts: 1 },
"@bob:localhost": { ts: 2 },
},
[ReceiptType.ReadPrivate]: {
"@dan:localhost": { ts: 4 },
},
"some.other.receipt.type": {
"@should_be_ignored:localhost": { key: "val" },
},
@@ -309,7 +313,7 @@ describe("SyncAccumulator", function() {
room_id: "!foo:bar",
content: {
"$event2:localhost": {
"m.read": {
[ReceiptType.Read]: {
"@bob:localhost": { ts: 2 }, // clobbers event1 receipt
"@charlie:localhost": { ts: 3 },
},
@@ -337,12 +341,15 @@ describe("SyncAccumulator", function() {
room_id: "!foo:bar",
content: {
"$event1:localhost": {
"m.read": {
[ReceiptType.Read]: {
"@alice:localhost": { ts: 1 },
},
[ReceiptType.ReadPrivate]: {
"@dan:localhost": { ts: 4 },
},
},
"$event2:localhost": {
"m.read": {
[ReceiptType.Read]: {
"@bob:localhost": { ts: 2 },
"@charlie:localhost": { ts: 3 },
},
+37 -1
View File
@@ -15,7 +15,7 @@ limitations under the License.
*/
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 { RoomMember } from "../../../src";
@@ -505,4 +505,40 @@ describe('Call', function() {
return sender?.track?.kind === "video";
}).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
View 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();
});
});
+1
View File
@@ -23,6 +23,7 @@ declare global {
// 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.
// 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 setTimeout(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
+21
View 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"
}
+49 -1
View File
@@ -17,9 +17,11 @@ limitations under the License.
import { Callback } from "../client";
import { IContent, IEvent } from "../models/event";
import { Preset, Visibility } from "./partials";
import { SearchKey } from "./search";
import { IEventWithRoomId, SearchKey } from "./search";
import { IRoomEventFilter } from "../filter";
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
/* eslint-disable camelcase */
@@ -155,4 +157,50 @@ export interface IRelationsResponse {
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 */
+62
View 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;
+5 -12
View File
@@ -17,8 +17,6 @@ limitations under the License.
/** @module auto-discovery */
import { URL as NodeURL } from "url";
import { IClientWellKnown, IWellKnownConfig } from "./client";
import { logger } from './logger';
@@ -372,16 +370,11 @@ export class AutoDiscovery {
if (!url) return false;
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;
try {
if (NodeURL) parsed = new NodeURL(url);
else parsed = new URL(url);
} catch (e) {
parsed = new URL(url);
} catch (e) {
logger.error("Could not parse url", e);
}
if (!parsed || !parsed.hostname) return false;
@@ -410,14 +403,14 @@ export class AutoDiscovery {
* the following properties:
* raw: The JSON object returned by the server.
* 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.
* @param {string} url The URL to fetch a JSON object from.
* @return {Promise<object>} Resolves to the returned state.
* @private
*/
private static async fetchWellKnownObject(url: string): Promise<IWellKnownConfig> {
return new Promise(function(resolve, reject) {
private static fetchWellKnownObject(url: string): Promise<IWellKnownConfig> {
return new Promise(function(resolve) {
// eslint-disable-next-line
const request = require("./matrix").getRequest();
if (!request) throw new Error("No request library available");
+179 -190
View File
@@ -32,7 +32,7 @@ import {
MatrixEventHandlerMap,
} from "./models/event";
import { StubStore } from "./store/stub";
import { CallEvent, CallEventHandlerMap, createNewMatrixCall, MatrixCall } from "./webrtc/call";
import { CallEvent, CallEventHandlerMap, createNewMatrixCall, MatrixCall, supportsMatrixCall } from "./webrtc/call";
import { Filter, IFilterDefinition } from "./filter";
import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from './webrtc/callEventHandler';
import * as utils from './utils';
@@ -113,6 +113,13 @@ import {
RoomMemberEventHandlerMap,
RoomStateEvent,
RoomStateEventHandlerMap,
INotificationsResponse,
IFilterResponse,
ITagsResponse,
IStatusResponse,
IPushRule,
PushRuleActionName,
IAuthDict,
} from "./matrix";
import {
CrossSigningKey,
@@ -132,6 +139,7 @@ import { Room } from "./models/room";
import {
IAddThreePidOnlyBody,
IBindThreePidBody,
IContextResponse,
ICreateRoomOpts,
IEventSearchOpts,
IGuestAccessOpts,
@@ -181,6 +189,7 @@ import { CryptoStore } from "./crypto/store/base";
import { MediaHandler } from "./webrtc/mediaHandler";
import { IRefreshTokenResponse } from "./@types/auth";
import { TypedEventEmitter } from "./models/typed-event-emitter";
import { ReceiptType } from "./@types/read_receipts";
import { Thread, THREAD_RELATION_TYPE } from "./models/thread";
import { MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
@@ -912,7 +921,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
protected syncLeftRoomsPromise: Promise<Room[]>;
protected syncedLeftRooms = false;
protected clientOpts: IStoredClientOpts;
protected clientWellKnownIntervalID: number;
protected clientWellKnownIntervalID: ReturnType<typeof setInterval>;
protected canResetTimelineCallback: ResetTimelineCallback;
// The pushprocessor caches useful things, so keep one and re-use it
@@ -930,7 +939,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
protected clientWellKnownPromise: Promise<IClientWellKnown>;
protected turnServers: ITurnServer[] = [];
protected turnServersExpiry = 0;
protected checkTurnServersIntervalID: number;
protected checkTurnServersIntervalID: ReturnType<typeof setInterval>;
protected exportedOlmDeviceToImport: IOlmDevice;
protected txnCtr = 0;
protected mediaHandler = new MediaHandler(this);
@@ -1004,10 +1013,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
});
}
// try constructing a MatrixCall to see if we are running in an environment
// which has WebRTC. If we are, listen for and handle m.call.* events.
const call = createNewMatrixCall(this, undefined, undefined);
if (call) {
if (supportsMatrixCall()) {
this.callEventHandler = new CallEventHandler(this);
this.canSupportVoip = true;
// Start listening for calls after the initial sync is done
@@ -1078,7 +1084,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// Figure out if we've read something or if it's just informational
const content = event.getContent();
const isSelf = Object.keys(content).filter(eid => {
return Object.keys(content[eid]['m.read']).includes(this.getUserId());
const read = content[eid][ReceiptType.Read];
if (read && Object.keys(read).includes(this.getUserId())) return true;
const readPrivate = content[eid][ReceiptType.ReadPrivate];
if (readPrivate && Object.keys(readPrivate).includes(this.getUserId())) return true;
return false;
}).length > 0;
if (!isSelf) return;
@@ -1164,6 +1176,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const { serverSupport, stable } = await this.doesServerSupportThread();
Thread.setServerSideSupport(serverSupport, stable);
} catch (e) {
// Most likely cause is that `doesServerSupportThread` returned `null` (as it
// is allowed to do) and thus we enter "degraded mode" on threads.
Thread.setServerSideSupport(false, true);
}
@@ -1192,6 +1206,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* clean shutdown.
*/
public stopClient() {
if (!this.clientRunning) return; // already stopped
logger.log('stopping MatrixClient');
this.clientRunning = false;
@@ -1324,7 +1340,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* dehydrated device.
* @return {Promise} A promise that resolves when the dehydrated device is stored.
*/
public async setDehydrationKey(
public setDehydrationKey(
key: Uint8Array,
keyInfo: IDehydratedDeviceKeyInfo,
deviceDisplayName?: string,
@@ -1333,7 +1349,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
logger.warn('not dehydrating device if crypto is not enabled');
return;
}
return await this.crypto.dehydrationManager.setKeyAndQueueDehydration(key, keyInfo, deviceDisplayName);
return this.crypto.dehydrationManager.setKeyAndQueueDehydration(key, keyInfo, deviceDisplayName);
}
/**
@@ -1354,11 +1370,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
logger.warn('not dehydrating device if crypto is not enabled');
return;
}
await this.crypto.dehydrationManager.setKey(
key, keyInfo, deviceDisplayName,
);
// XXX: Private member access.
return await this.crypto.dehydrationManager.dehydrateDevice();
await this.crypto.dehydrationManager.setKey(key, keyInfo, deviceDisplayName);
return this.crypto.dehydrationManager.dehydrateDevice();
}
public async exportDevice(): Promise<IExportedDevice> {
@@ -2382,18 +2395,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @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
* with, or null if it is not present or not encrypted with a trusted
* key
*/
public isSecretStored(name: string, checkKey: boolean): Promise<Record<string, ISecretStorageKeyInfo> | null> {
public isSecretStored(name: string): Promise<Record<string, ISecretStorageKeyInfo> | null> {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
return this.crypto.isSecretStored(name, checkKey);
return this.crypto.isSecretStored(name);
}
/**
@@ -2605,9 +2615,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns {Promise<IKeyBackupInfo | null>} Information object from API or null
*/
public async getKeyBackupVersion(): Promise<IKeyBackupInfo | null> {
let res;
let res: IKeyBackupInfo;
try {
res = await this.http.authedRequest(
res = await this.http.authedRequest<IKeyBackupInfo>(
undefined, Method.Get, "/room_keys/version", undefined, undefined,
{ prefix: PREFIX_UNSTABLE },
);
@@ -2618,11 +2628,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
throw e;
}
}
try {
BackupManager.checkBackupVersion(res);
} catch (e) {
throw e;
}
BackupManager.checkBackupVersion(res);
return res;
}
@@ -2726,7 +2732,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* trusted key
*/
public isKeyBackupKeyStored(): Promise<Record<string, ISecretStorageKeyInfo> | null> {
return Promise.resolve(this.isSecretStored("m.megolm_backup.v1", false /* checkKey */));
return Promise.resolve(this.isSecretStored("m.megolm_backup.v1"));
}
/**
@@ -3144,10 +3150,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
});
}
const res = await this.http.authedRequest(
const res = await this.http.authedRequest<IRoomsKeysResponse | IRoomKeysResponse | IKeyBackupSession>(
undefined, Method.Get, path.path, path.queryData, undefined,
{ prefix: PREFIX_UNSTABLE },
) as IRoomsKeysResponse | IRoomKeysResponse | IKeyBackupSession;
);
if ((res as IRoomsKeysResponse).rooms) {
const rooms = (res as IRoomsKeysResponse).rooms;
@@ -3387,11 +3393,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
$type: eventType,
});
try {
return await this.http.authedRequest(
undefined, Method.Get, path, undefined,
);
return await this.http.authedRequest(undefined, Method.Get, path);
} catch (e) {
if (e.data && e.data.errcode === 'M_NOT_FOUND') {
if (e.data?.errcode === 'M_NOT_FOUND') {
return null;
}
throw e;
@@ -3553,28 +3557,45 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* @param {string} roomId
* @param {string} topic
* @param {string} htmlTopic Optional.
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public setRoomTopic(roomId: string, topic: string, callback?: Callback): Promise<ISendEventResponse> {
return this.sendStateEvent(roomId, EventType.RoomTopic, { topic: topic }, undefined, callback);
public setRoomTopic(
roomId: string,
topic: string,
htmlTopic?: string,
): Promise<ISendEventResponse>;
public setRoomTopic(
roomId: string,
topic: string,
callback?: Callback,
): Promise<ISendEventResponse>;
public setRoomTopic(
roomId: string,
topic: string,
htmlTopicOrCallback?: string | Callback,
): Promise<ISendEventResponse> {
const isCallback = typeof htmlTopicOrCallback === 'function';
const htmlTopic = isCallback ? undefined : htmlTopicOrCallback;
const callback = isCallback ? htmlTopicOrCallback : undefined;
const content = ContentHelpers.makeTopicContent(topic, htmlTopic);
return this.sendStateEvent(roomId, EventType.RoomTopic, content, undefined, callback);
}
/**
* @param {string} roomId
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: TODO
* @return {Promise} Resolves: to an object keyed by tagId with objects containing a numeric order field.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public getRoomTags(roomId: string, callback?: Callback): Promise<unknown> { // TODO: Types
const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/", {
public getRoomTags(roomId: string, callback?: Callback): Promise<ITagsResponse> {
const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags", {
$userId: this.credentials.userId,
$roomId: roomId,
});
return this.http.authedRequest(
callback, Method.Get, path, undefined,
);
return this.http.authedRequest(callback, Method.Get, path);
}
/**
@@ -3598,7 +3619,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {string} roomId
* @param {string} tagName name of room tag to be removed
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: TODO
* @return {Promise} Resolves: void
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public deleteRoomTag(roomId: string, tagName: string, callback?: Callback): Promise<void> {
@@ -3607,7 +3628,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
$roomId: roomId,
$tag: tagName,
});
return this.http.authedRequest(callback, Method.Delete, path, undefined, undefined);
return this.http.authedRequest(callback, Method.Delete, path);
}
/**
@@ -3655,7 +3676,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
if (event?.getType() === EventType.RoomPowerLevels) {
// take a copy of the content to ensure we don't corrupt
// existing client state with a failed power level change
content = utils.deepCopy(event.getContent()) as typeof content;
content = utils.deepCopy(event.getContent());
}
content.users[userId] = powerLevel;
const path = utils.encodeUri("/rooms/$roomId/state/m.room.power_levels", {
@@ -4500,13 +4521,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* Send a receipt.
* @param {Event} event The event being acknowledged
* @param {string} receiptType The kind of receipt e.g. "m.read"
* @param {ReceiptType} receiptType The kind of receipt e.g. "m.read". Other than
* ReceiptType.Read are experimental!
* @param {object} body Additional content to send alongside the receipt.
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: to an empty object {}
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public sendReceipt(event: MatrixEvent, receiptType: string, body: any, callback?: Callback): Promise<{}> {
public sendReceipt(event: MatrixEvent, receiptType: ReceiptType, body: any, callback?: Callback): Promise<{}> {
if (typeof (body) === 'function') {
callback = body as any as Callback; // legacy
body = {};
@@ -4533,32 +4555,19 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* Send a read receipt.
* @param {Event} event The event that has been read.
* @param {object} opts The options for the read receipt.
* @param {boolean} opts.hidden True to prevent the receipt from being sent to
* other users and homeservers. Default false (send to everyone). <b>This
* property is unstable and may change in the future.</b>
* @param {ReceiptType} receiptType other than ReceiptType.Read are experimental! Optional.
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: to an empty object
* @return {Promise} Resolves: to an empty object {}
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public async sendReadReceipt(event: MatrixEvent, opts?: { hidden?: boolean }, callback?: Callback): Promise<{}> {
if (typeof (opts) === 'function') {
callback = opts as any as Callback; // legacy
opts = {};
}
if (!opts) opts = {};
public async sendReadReceipt(event: MatrixEvent, receiptType = ReceiptType.Read, callback?: Callback): Promise<{}> {
const eventId = event.getId();
const room = this.getRoom(event.getRoomId());
if (room && room.hasPendingEvent(eventId)) {
throw new Error(`Cannot set read receipt to a pending event (${eventId})`);
}
const addlContent = {
"org.matrix.msc2285.hidden": Boolean(opts.hidden),
};
return this.sendReceipt(event, "m.read", addlContent, callback);
return this.sendReceipt(event, receiptType, {}, callback);
}
/**
@@ -4571,16 +4580,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {MatrixEvent} rrEvent the event tracked by the read receipt. This is here for
* convenience because the RR and the RM are commonly updated at the same time as each
* other. The local echo of this receipt will be done if set. Optional.
* @param {object} opts Options for the read markers
* @param {object} opts.hidden True to hide the receipt from other users and homeservers.
* <b>This property is unstable and may change in the future.</b>
* @param {MatrixEvent} rpEvent the m.read.private read receipt event for when we don't
* want other users to see the read receipts. This is experimental. Optional.
* @return {Promise} Resolves: the empty object, {}.
*/
public async setRoomReadMarkers(
roomId: string,
rmEventId: string,
rrEvent: MatrixEvent,
opts: { hidden?: boolean },
rrEvent?: MatrixEvent,
rpEvent?: MatrixEvent,
): Promise<{}> {
const room = this.getRoom(roomId);
if (room && room.hasPendingEvent(rmEventId)) {
@@ -4588,18 +4596,26 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
// Add the optional RR update, do local echo like `sendReceipt`
let rrEventId;
let rrEventId: string;
if (rrEvent) {
rrEventId = rrEvent.getId();
if (room && room.hasPendingEvent(rrEventId)) {
if (room?.hasPendingEvent(rrEventId)) {
throw new Error(`Cannot set read receipt to a pending event (${rrEventId})`);
}
if (room) {
room.addLocalEchoReceipt(this.credentials.userId, rrEvent, "m.read");
}
room?.addLocalEchoReceipt(this.credentials.userId, rrEvent, ReceiptType.Read);
}
return this.setRoomReadMarkersHttpRequest(roomId, rmEventId, rrEventId, opts);
// Add the optional private RR update, do local echo like `sendReceipt`
let rpEventId: string;
if (rpEvent) {
rpEventId = rpEvent.getId();
if (room?.hasPendingEvent(rpEventId)) {
throw new Error(`Cannot set read receipt to a pending event (${rpEventId})`);
}
room?.addLocalEchoReceipt(this.credentials.userId, rpEvent, ReceiptType.ReadPrivate);
}
return this.setRoomReadMarkersHttpRequest(roomId, rmEventId, rrEventId, rpEventId);
}
/**
@@ -4858,7 +4874,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
}
const populationResults: Record<string, Error> = {}; // {roomId: Error}
const populationResults: { [roomId: string]: Error } = {};
const promises = [];
const doLeave = (roomId: string) => {
@@ -4964,7 +4980,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
membership: string,
reason?: string,
callback?: Callback,
): Promise<{}> {
): Promise<{}> { // API returns an empty object
if (utils.isFunction(reason)) {
callback = reason as any as Callback; // legacy
reason = undefined;
@@ -5072,26 +5088,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return getHttpUriForMxc(this.baseUrl, mxcUrl, width, height, resizeMethod, allowDirectLinks);
}
/**
* Sets a new status message for the user. The message may be null/falsey
* to clear the message.
* @param {string} newMessage The new message to set.
* @return {Promise} Resolves: to nothing
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public _unstable_setStatusMessage(newMessage: string): Promise<void> { // eslint-disable-line
const type = "im.vector.user_status";
return Promise.all(this.getRooms().map(async (room) => {
const isJoined = room.getMyMembership() === "join";
const looksLikeDm = room.getInvitedAndJoinedMemberCount() === 2;
if (!isJoined || !looksLikeDm) return;
// Check power level separately as it's a bit more expensive.
const maySend = room.currentState.mayClientSendStateEvent(type, this);
if (!maySend) return;
await this.sendStateEvent(room.roomId, type, { status: newMessage }, this.getUserId());
})).then(); // .then to fix return type
}
/**
* @param {Object} opts Options to apply
* @param {string} opts.presence One of "online", "offline" or "unavailable"
@@ -5125,12 +5121,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {Promise} Resolves: The presence state for this user.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public getPresence(userId: string, callback?: Callback): Promise<unknown> { // TODO: Types
public getPresence(userId: string, callback?: Callback): Promise<IStatusResponse> {
const path = utils.encodeUri("/presence/$userId/status", {
$userId: userId,
});
return this.http.authedRequest(callback, Method.Get, path, undefined, undefined);
return this.http.authedRequest(callback, Method.Get, path);
}
/**
@@ -5142,7 +5138,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* when there is no connection).
*
* @param {Room} room The room to get older messages in.
* @param {Integer} limit Optional. The maximum number of previous events to
* @param {number} limit Optional. The maximum number of previous events to
* pull in. Default: 30.
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: Room. If you are at the beginning
@@ -5273,7 +5269,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
// TODO: we should implement a backoff (as per scrollback()) to deal more nicely with HTTP errors.
const res = await this.http.authedRequest<any>(undefined, Method.Get, path, params); // TODO types
const res = await this.http.authedRequest<IContextResponse>(undefined, Method.Get, path, params);
if (!res.event) {
throw new Error("'event' not in '/context' result - homeserver too old?");
}
@@ -5452,8 +5448,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
params.from = token;
}
promise = this.http.authedRequest<any>( // TODO types
undefined, Method.Get, path, params, undefined,
promise = this.http.authedRequest<INotificationsResponse>(
undefined, Method.Get, path, params,
).then(async (res) => {
const token = res.next_token;
const matrixEvents = [];
@@ -5833,11 +5829,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const idServerUrl = new URL(this.idBaseUrl);
postParams.id_server = idServerUrl.host;
if (
this.identityServer &&
this.identityServer.getAccessToken &&
await this.doesServerAcceptIdentityAccessToken()
) {
if (this.identityServer?.getAccessToken && await this.doesServerAcceptIdentityAccessToken()) {
const identityAccessToken = await this.identityServer.getAccessToken();
if (identityAccessToken) {
postParams.id_access_token = identityAccessToken;
@@ -5854,7 +5846,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {string} roomId the id of the room.
* @return {object} the rule or undefined.
*/
public getRoomPushRule(scope: string, roomId: string): any { // TODO: Types
public getRoomPushRule(scope: string, roomId: string): IPushRule | undefined {
// There can be only room-kind push rule per room
// and its id is the room id.
if (this.pushRules) {
@@ -5881,15 +5873,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public setRoomMutePushRule(scope: string, roomId: string, mute: boolean): Promise<void> | void {
let promise: Promise<void>;
let hasDontNotifyRule;
let promise: Promise<unknown>;
let hasDontNotifyRule = false;
// Get the existing room-kind push rule if any
const roomPushRule = this.getRoomPushRule(scope, roomId);
if (roomPushRule) {
if (0 <= roomPushRule.actions.indexOf("dont_notify")) {
hasDontNotifyRule = true;
}
if (roomPushRule?.actions.includes(PushRuleActionName.DontNotify)) {
hasDontNotifyRule = true;
}
if (!mute) {
@@ -5900,24 +5890,23 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
} else {
if (!roomPushRule) {
promise = this.addPushRule(scope, PushRuleKind.RoomSpecific, roomId, {
actions: ["dont_notify"],
actions: [PushRuleActionName.DontNotify],
});
} else if (!hasDontNotifyRule) {
// Remove the existing one before setting the mute push rule
// This is a workaround to SYN-590 (Push rule update fails)
const deferred = utils.defer();
this.deletePushRule(scope, PushRuleKind.RoomSpecific, roomPushRule.rule_id)
.then(() => {
this.addPushRule(scope, PushRuleKind.RoomSpecific, roomId, {
actions: ["dont_notify"],
}).then(() => {
deferred.resolve();
}).catch((err) => {
deferred.reject(err);
});
this.deletePushRule(scope, PushRuleKind.RoomSpecific, roomPushRule.rule_id).then(() => {
this.addPushRule(scope, PushRuleKind.RoomSpecific, roomId, {
actions: [PushRuleActionName.DontNotify],
}).then(() => {
deferred.resolve();
}).catch((err) => {
deferred.reject(err);
});
}).catch((err) => {
deferred.reject(err);
});
promise = deferred.promise;
}
@@ -5935,7 +5924,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
});
}).catch((err: Error) => {
// Update it even if the previous operation fails. This can help the
// app to recover when push settings has been modifed from another client
// app to recover when push settings has been modified from another client
this.getPushRules().then((result) => {
this.pushRules = result;
reject(err);
@@ -6109,7 +6098,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
this.syncLeftRoomsPromise = syncApi.syncLeftRooms();
// cleanup locks
this.syncLeftRoomsPromise.then((res) => {
this.syncLeftRoomsPromise.then(() => {
logger.log("Marking success of sync left room request");
this.syncedLeftRooms = true; // flip the bit on success
}).finally(() => {
@@ -6129,15 +6118,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const path = utils.encodeUri("/user/$userId/filter", {
$userId: this.credentials.userId,
});
// TODO types
return this.http.authedRequest<any>(undefined, Method.Post, path, undefined, content).then((response) => {
// persist the filter
const filter = Filter.fromJson(
this.credentials.userId, response.filter_id, content,
);
this.store.storeFilter(filter);
return filter;
});
return this.http.authedRequest<IFilterResponse>(undefined, Method.Post, path, undefined, content)
.then((response) => {
// persist the filter
const filter = Filter.fromJson(this.credentials.userId, response.filter_id, content);
this.store.storeFilter(filter);
return filter;
});
}
/**
@@ -6162,9 +6149,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
$filterId: filterId,
});
return this.http.authedRequest<IFilterDefinition>(
undefined, Method.Get, path, undefined, undefined,
).then((response) => {
return this.http.authedRequest<IFilterDefinition>(undefined, Method.Get, path).then((response) => {
// persist the filter
const filter = Filter.fromJson(userId, filterId, response);
this.store.storeFilter(filter);
@@ -6559,8 +6544,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const versions = response["versions"];
const unstableFeatures = response["unstable_features"];
return (versions && versions.includes("r0.6.0"))
|| (unstableFeatures && unstableFeatures["m.separate_add_and_bind"]);
return versions?.includes("r0.6.0") || unstableFeatures?.["m.separate_add_and_bind"];
}
/**
@@ -6602,14 +6586,17 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
} | null> {
try {
const hasUnstableSupport = await this.doesServerSupportUnstableFeature("org.matrix.msc3440");
const hasStableSupport = await this.doesServerSupportUnstableFeature("org.matrix.msc3440.stable")
|| await this.isVersionSupported("v1.3");
const hasStableSupport = await this.doesServerSupportUnstableFeature("org.matrix.msc3440.stable");
// TODO: Use `this.isVersionSupported("v1.3")` for whatever spec version includes MSC3440 formally.
return {
serverSupport: hasUnstableSupport || hasStableSupport,
stable: hasStableSupport,
};
} catch (e) {
// Assume server support and stability aren't available: null/no data return.
// XXX: This should just return an object with `false` booleans instead.
return null;
}
}
@@ -7091,9 +7078,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* it is up to the caller to either reset or destroy the MatrixClient after
* this method succeeds.
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: On success, the empty object
* @param {boolean} stopClient whether to stop the client before calling /logout to prevent invalid token errors.
* @return {Promise} Resolves: On success, the empty object {}
*/
public async logout(callback?: Callback): Promise<{}> {
public async logout(callback?: Callback, stopClient = false): Promise<{}> {
if (this.crypto?.backupManager?.getKeyBackupEnabled()) {
try {
while (await this.crypto.backupManager.backupPendingKeys(200) > 0);
@@ -7104,6 +7092,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
);
}
}
if (stopClient) {
this.stopClient();
}
return this.http.authedRequest(
callback, Method.Post, '/logout',
);
@@ -7203,7 +7196,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {Object} [opts] options with optional values for the request.
* @return {Object} the response, with chunk, prev_batch and, next_batch.
*/
public async fetchRelations(
public fetchRelations(
roomId: string,
eventId: string,
relationType?: RelationType | string | null,
@@ -7231,7 +7224,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
$relationType: relationType,
$eventType: eventType,
});
return await this.http.authedRequest(
return this.http.authedRequest(
undefined, Method.Get, path, null, null, {
prefix: PREFIX_UNSTABLE,
},
@@ -7410,25 +7403,24 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {string} rrEventId ID of the event tracked by the read receipt. This is here
* for convenience because the RR and the RM are commonly updated at the same time as
* each other. Optional.
* @param {object} opts Options for the read markers.
* @param {object} opts.hidden True to hide the read receipt from other users. <b>This
* property is currently unstable and may change in the future.</b>
* @param {string} rpEventId rpEvent the m.read.private read receipt event for when we
* don't want other users to see the read receipts. This is experimental. Optional.
* @return {Promise} Resolves: the empty object, {}.
*/
public setRoomReadMarkersHttpRequest(
roomId: string,
rmEventId: string,
rrEventId: string,
opts: { hidden?: boolean },
rpEventId: string,
): Promise<{}> {
const path = utils.encodeUri("/rooms/$roomId/read_markers", {
$roomId: roomId,
});
const content = {
"m.fully_read": rmEventId,
"m.read": rrEventId,
"org.matrix.msc2285.hidden": Boolean(opts ? opts.hidden : false),
[ReceiptType.FullyRead]: rmEventId,
[ReceiptType.Read]: rrEventId,
[ReceiptType.ReadPrivate]: rpEventId,
};
return this.http.authedRequest(undefined, Method.Post, path, undefined, content);
@@ -7511,18 +7503,18 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
/**
* Delete an alias to room ID mapping. This alias must be on your local server
* Delete an alias to room ID mapping. This alias must be on your local server,
* and you must have sufficient access to do this operation.
* @param {string} alias The room alias to delete.
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: an empty object.
* @return {Promise} Resolves: an empty object {}.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public deleteAlias(alias: string, callback?: Callback): Promise<{}> {
const path = utils.encodeUri("/directory/room/$alias", {
$alias: alias,
});
return this.http.authedRequest(callback, Method.Delete, path, undefined, undefined);
return this.http.authedRequest(callback, Method.Delete, path);
}
/**
@@ -7590,7 +7582,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* in the public directory, or "private" to make
* it invisible.
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: result object
* @return {Promise} Resolves: to an empty object {}
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public setRoomDirectoryVisibility(roomId: string, visibility: Visibility, callback?: Callback): Promise<{}> {
@@ -7749,8 +7741,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public getThreePids(callback?: Callback): Promise<{ threepids: IThreepid[] }> {
const path = "/account/3pid";
return this.http.authedRequest(callback, Method.Get, path, undefined, undefined);
return this.http.authedRequest(callback, Method.Get, "/account/3pid");
}
/**
@@ -7786,7 +7777,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @param {Object} data A object with 3PID validation data from having called
* `account/3pid/<medium>/requestToken` on the homeserver.
* @return {Promise} Resolves: on success
* @return {Promise} Resolves: to an empty object {}
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public async addThreePidOnly(data: IAddThreePidOnlyBody): Promise<{}> {
@@ -7806,7 +7797,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {Object} data A object with 3PID validation data from having called
* `validate/<medium>/requestToken` on the identity server. It should also
* contain `id_server` and `id_access_token` fields as well.
* @return {Promise} Resolves: on success
* @return {Promise} Resolves: to an empty object {}
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public async bindThreePid(data: IBindThreePidBody): Promise<{}> {
@@ -7867,7 +7858,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {string} newPassword The new desired password.
* @param {boolean} logoutDevices Should all sessions be logged out after the password change. Defaults to true.
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: TODO
* @return {Promise} Resolves: to an empty object {}
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public setPassword(
@@ -7914,7 +7905,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public getDevices(): Promise<{ devices: IMyDevice[] }> {
return this.http.authedRequest(undefined, Method.Get, "/devices", undefined, undefined);
return this.http.authedRequest(undefined, Method.Get, "/devices");
}
/**
@@ -7927,7 +7918,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const path = utils.encodeUri("/devices/$device_id", {
$device_id: deviceId,
});
return this.http.authedRequest(undefined, Method.Get, path, undefined, undefined);
return this.http.authedRequest(undefined, Method.Get, path);
}
/**
@@ -7935,7 +7926,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @param {string} deviceId device to update
* @param {Object} body body of request
* @return {Promise} Resolves: result object
* @return {Promise} Resolves: to an empty object {}
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
// eslint-disable-next-line camelcase
@@ -7955,7 +7946,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public deleteDevice(deviceId: string, auth?: any): Promise<any> { // TODO: Types
public deleteDevice(deviceId: string, auth?: IAuthDict): Promise<IAuthData | {}> {
const path = utils.encodeUri("/devices/$device_id", {
$device_id: deviceId,
});
@@ -7977,7 +7968,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public deleteMultipleDevices(devices: string[], auth?: any): Promise<any> { // TODO: Types
public deleteMultipleDevices(devices: string[], auth?: IAuthDict): Promise<IAuthData | {}> {
const body: any = { devices };
if (auth) {
@@ -7996,8 +7987,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public getPushers(callback?: Callback): Promise<{ pushers: IPusher[] }> {
const path = "/pushers";
return this.http.authedRequest(callback, Method.Get, path, undefined, undefined);
return this.http.authedRequest(callback, Method.Get, "/pushers");
}
/**
@@ -8038,9 +8028,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
scope: string,
kind: PushRuleKind,
ruleId: Exclude<string, RuleId>,
body: any,
body: Pick<IPushRule, "actions" | "conditions" | "pattern">,
callback?: Callback,
): Promise<any> { // TODO: Types
): Promise<{}> {
// NB. Scope not uri encoded because devices need the '/'
const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", {
$kind: kind,
@@ -8062,7 +8052,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
kind: PushRuleKind,
ruleId: Exclude<string, RuleId>,
callback?: Callback,
): Promise<any> { // TODO: Types
): Promise<{}> {
// NB. Scope not uri encoded because devices need the '/'
const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", {
$kind: kind,
@@ -8078,7 +8068,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {string} ruleId
* @param {boolean} enabled
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: result object
* @return {Promise} Resolves: to an empty object {}
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public setPushRuleEnabled(
@@ -8104,7 +8094,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {string} ruleId
* @param {array} actions
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: result object
* @return {Promise} Resolves: to an empty object {}
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public setPushRuleActions(
@@ -8261,11 +8251,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
to: newToken,
};
const path = "/keys/changes";
return this.http.authedRequest(undefined, Method.Get, path, qps, undefined);
return this.http.authedRequest(undefined, Method.Get, "/keys/changes", qps);
}
public uploadDeviceSigningKeys(auth?: IAuthData, keys?: CrossSigningKeys): Promise<{}> {
public uploadDeviceSigningKeys(auth?: IAuthData, keys?: CrossSigningKeys): Promise<{}> { // API returns empty object
const data = Object.assign({}, keys);
if (auth) Object.assign(data, { auth });
return this.http.authedRequest(
@@ -8324,7 +8313,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @throws Error if no identity server is set
*/
public async requestEmailToken(
public requestEmailToken(
email: string,
clientSecret: string,
sendAttempt: number,
@@ -8339,7 +8328,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
next_link: nextLink,
};
return await this.http.idServerRequest(
return this.http.idServerRequest(
callback, Method.Post, "/validate/email/requestToken",
params, PREFIX_IDENTITY_V2, identityAccessToken,
);
@@ -8372,7 +8361,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @throws Error if no identity server is set
*/
public async requestMsisdnToken(
public requestMsisdnToken(
phoneCountry: string,
phoneNumber: string,
clientSecret: string,
@@ -8389,7 +8378,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
next_link: nextLink,
};
return await this.http.idServerRequest(
return this.http.idServerRequest(
callback, Method.Post, "/validate/msisdn/requestToken",
params, PREFIX_IDENTITY_V2, identityAccessToken,
);
@@ -8414,7 +8403,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @throws Error if No identity server is set
*/
public async submitMsisdnToken(
public submitMsisdnToken(
sid: string,
clientSecret: string,
msisdnToken: string,
@@ -8426,7 +8415,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
token: msisdnToken,
};
return await this.http.idServerRequest(
return this.http.idServerRequest(
undefined, Method.Post, "/validate/msisdn/submitToken",
params, PREFIX_IDENTITY_V2, identityAccessToken,
);
@@ -8675,7 +8664,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* content to send. Map from user_id to device_id to content object.
* @param {string=} txnId transaction id. One will be made up if not
* supplied.
* @return {Promise} Resolves to the result object
* @return {Promise} Resolves: to an empty object {}
*/
public sendToDevice(
eventType: string,
@@ -8707,7 +8696,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*/
public getThirdpartyProtocols(): Promise<{ [protocol: string]: IProtocol }> {
return this.http.authedRequest<Record<string, IProtocol>>(
undefined, Method.Get, "/thirdparty/protocols", undefined, undefined,
undefined, Method.Get, "/thirdparty/protocols",
).then((response) => {
// sanity check
if (!response || typeof (response) !== 'object') {
@@ -8733,7 +8722,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
$protocol: protocol,
});
return this.http.authedRequest(undefined, Method.Get, path, params, undefined);
return this.http.authedRequest(undefined, Method.Get, path, params);
}
/**
@@ -8749,7 +8738,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
$protocol: protocol,
});
return this.http.authedRequest(undefined, Method.Get, path, params, undefined);
return this.http.authedRequest(undefined, Method.Get, path, params);
}
public getTerms(serviceType: SERVICE_TYPES, baseUrl: string): Promise<any> { // TODO: Types
@@ -8947,7 +8936,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* origin_server_ts of the closest event to the timestamp in the given
* direction
*/
public async timestampToEvent(
public timestampToEvent(
roomId: string,
timestamp: number,
dir: Direction,
@@ -8956,7 +8945,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
$roomId: roomId,
});
return await this.http.authedRequest(
return this.http.authedRequest(
undefined,
Method.Get,
path,
+30 -1
View File
@@ -16,7 +16,7 @@ limitations under the License.
/** @module ContentHelpers */
import { REFERENCE_RELATION } from "matrix-events-sdk";
import { isProvided, REFERENCE_RELATION } from "matrix-events-sdk";
import { MBeaconEventContent, MBeaconInfoContent, MBeaconInfoEventContent } from "./@types/beacon";
import { MsgType } from "./@types/event";
@@ -32,6 +32,7 @@ import {
MAssetContent,
LegacyLocationEventContent,
} from "./@types/location";
import { MRoomTopicEventContent, MTopicContent, M_TOPIC } from "./@types/topic";
/**
* Generates the content for a HTML Message event
@@ -190,6 +191,34 @@ export const parseLocationEvent = (wireEventContent: LocationEventWireContent):
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
*/
+2 -2
View File
@@ -171,7 +171,7 @@ export class CrossSigningInfo {
*/
public async isStoredInSecretStorage(secretStorage: SecretStorage): Promise<Record<string, object>> {
// 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
function intersect(s: Record<string, ISecretStorageKeyInfo>) {
for (const k of Object.keys(stored)) {
@@ -181,7 +181,7 @@ export class CrossSigningInfo {
}
}
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;
}
+4 -4
View File
@@ -95,7 +95,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
// The time the save is scheduled for
private savePromiseTime: number = null;
// 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
// set of device data from the store
private hasFetched: boolean = null;
@@ -122,7 +122,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
'readonly', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
this.cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
this.hasFetched = Boolean(deviceData && deviceData.devices);
this.devices = deviceData ? deviceData.devices : {},
this.devices = deviceData ? deviceData.devices : {};
this.crossSigningInfo = deviceData ?
deviceData.crossSigningInfo || {} : {};
this.deviceTrackingStatus = deviceData ?
@@ -190,7 +190,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
let savePromise = this.savePromise;
if (savePromise === null) {
savePromise = new Promise((resolve, reject) => {
savePromise = new Promise((resolve) => {
this.resolveSavePromise = resolve;
});
this.savePromise = savePromise;
@@ -942,7 +942,7 @@ async function updateStoredDeviceKeysForUser(
async function storeDeviceKeys(
olmDevice: OlmDevice,
userStore: Record<string, DeviceInfo>,
deviceResult: any, // TODO types
deviceResult: IDownloadKeyResult["device_keys"]["user_id"]["device_id"],
): Promise<boolean> {
if (!deviceResult.keys) {
// no keys?
+4 -4
View File
@@ -909,12 +909,12 @@ export class OlmDevice {
await this.cryptoStore.storeEndToEndSessionProblem(deviceKey, type, fixed);
}
public async sessionMayHaveProblems(deviceKey: string, timestamp: number): Promise<IProblem> {
return await this.cryptoStore.getEndToEndSessionProblem(deviceKey, timestamp);
public sessionMayHaveProblems(deviceKey: string, timestamp: number): Promise<IProblem> {
return this.cryptoStore.getEndToEndSessionProblem(deviceKey, timestamp);
}
public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
return await this.cryptoStore.filterOutNotifiedErrorDevices(devices);
public filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
return this.cryptoStore.filterOutNotifiedErrorDevices(devices);
}
// Outbound group session
+2 -4
View File
@@ -78,7 +78,7 @@ export enum RoomKeyRequestState {
export class OutgoingRoomKeyRequestManager {
// handle for the delayed call to sendOutgoingRoomKeyRequests. Non-null
// 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
// of sendOutgoingRoomKeyRequests
@@ -189,9 +189,7 @@ export class OutgoingRoomKeyRequestManager {
// in state ROOM_KEY_REQUEST_STATES.SENT, so we must have
// raced with another tab to mark the request cancelled.
// Try again, to make sure the request is resent.
return await this.queueRoomKeyRequest(
requestBody, recipients, resend,
);
return this.queueRoomKeyRequest(requestBody, recipients, resend);
}
// We don't want to wait for the timer, so we send it
+7 -13
View File
@@ -329,7 +329,7 @@ export class SecretStorage {
// encoded, since this is how a key would normally be stored.
if (encInfo.passthrough) return encodeBase64(decryption.get_private_key());
return await decryption.decrypt(encInfo);
return decryption.decrypt(encInfo);
} finally {
if (decryption && decryption.free) decryption.free();
}
@@ -339,21 +339,15 @@ export class SecretStorage {
* Check if a secret is stored on the server.
*
* @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
* with, or null if it is not present or not encrypted with a trusted
* 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
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name);
if (!secretInfo) return null;
if (!secretInfo.encrypted) {
return null;
}
if (checkKey === undefined) checkKey = true;
if (!secretInfo?.encrypted) return null;
const ret = {};
@@ -598,11 +592,11 @@ export class SecretStorage {
if (keys[keyId].algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
const decryption = {
encrypt: async function(secret: string): Promise<IEncryptedPayload> {
return await encryptAES(secret, privateKey, name);
encrypt: function(secret: string): Promise<IEncryptedPayload> {
return encryptAES(secret, privateKey, name);
},
decrypt: async function(encInfo: IEncryptedPayload): Promise<string> {
return await decryptAES(encInfo, privateKey, name);
decrypt: function(encInfo: IEncryptedPayload): Promise<string> {
return decryptAES(encInfo, privateKey, name);
},
};
return [keyId, decryption];
+1 -1
View File
@@ -250,7 +250,7 @@ async function deriveKeysBrowser(key: Uint8Array, name: string): Promise<[Crypto
['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> {
+4 -4
View File
@@ -70,7 +70,7 @@ class OlmEncryption extends EncryptionAlgorithm {
return Promise.resolve();
}
this.prepPromise = this.crypto.downloadKeys(roomMembers).then((res) => {
this.prepPromise = this.crypto.downloadKeys(roomMembers).then(() => {
return this.crypto.ensureOlmSessionsForUsers(roomMembers);
}).then(() => {
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.
*/
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
// 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.
@@ -274,7 +274,7 @@ class OlmDecryption extends DecryptionAlgorithm {
});
// we want the error, but don't propagate it to the next decryption
this.olmDevice.olmPrekeyPromise = myPromise.catch(() => {});
return await myPromise;
return myPromise;
}
}
+11 -11
View File
@@ -132,18 +132,18 @@ export class BackupManager {
if (!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");
}
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];
if (!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> {
@@ -428,7 +428,7 @@ export class BackupManager {
// requests from different clients hitting the server all at
// the same time when a new key is sent
const delay = Math.random() * maxDelay;
await sleep(delay, undefined);
await sleep(delay);
let numFailures = 0; // number of consecutive failures
for (;;) {
if (!this.algorithm) {
@@ -462,7 +462,7 @@ export class BackupManager {
}
if (numFailures) {
// 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 {
@@ -474,8 +474,8 @@ export class BackupManager {
* Take some e2e keys waiting to be backed up and send them
* to the backup.
*
* @param {integer} limit Maximum number of keys to back up
* @returns {integer} Number of sessions backed up
* @param {number} limit Maximum number of keys to back up
* @returns {number} Number of sessions backed up
*/
public async backupPendingKeys(limit: number): Promise<number> {
const sessions = await this.baseApis.crypto.cryptoStore.getSessionsNeedingBackup(limit);
@@ -777,15 +777,15 @@ export class Aes256 implements BackupAlgorithm {
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);
delete plainText.session_id;
delete plainText.room_id;
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[] = [];
for (const [sessionId, sessionData] of Object.entries(sessions)) {
@@ -800,7 +800,7 @@ export class Aes256 implements BackupAlgorithm {
return keys;
}
async keyMatches(key: Uint8Array): Promise<boolean> {
public async keyMatches(key: Uint8Array): Promise<boolean> {
if (this.authData.mac) {
const { mac } = await calculateKeyCheck(key, this.authData.iv);
return this.authData.mac.replace(/=+$/g, '') === mac.replace(/=+/g, '');
+7 -5
View File
@@ -61,11 +61,13 @@ export class DehydrationManager {
private key: Uint8Array;
private keyInfo: {[props: string]: any};
private deviceDisplayName: string;
constructor(private readonly crypto: Crypto) {
this.getDehydrationKeyFromCache();
}
async getDehydrationKeyFromCache(): Promise<void> {
return await this.crypto.cryptoStore.doTxn(
public getDehydrationKeyFromCache(): Promise<void> {
return this.crypto.cryptoStore.doTxn(
'readonly',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
@@ -93,7 +95,7 @@ export class DehydrationManager {
}
/** set the key, and queue periodic dehydration to the server in the background */
async setKeyAndQueueDehydration(
public async setKeyAndQueueDehydration(
key: Uint8Array, keyInfo: {[props: string]: any} = {},
deviceDisplayName: string = undefined,
): Promise<void> {
@@ -104,7 +106,7 @@ export class DehydrationManager {
}
}
async setKey(
public async setKey(
key: Uint8Array, keyInfo: {[props: string]: any} = {},
deviceDisplayName: string = undefined,
): Promise<boolean> {
@@ -148,7 +150,7 @@ export class DehydrationManager {
}
/** returns the device id of the newly created dehydrated device */
async dehydrateDevice(): Promise<string> {
public async dehydrateDevice(): Promise<string> {
if (this.inProgress) {
logger.log("Dehydration already in progress -- not starting new dehydration");
return;
+9 -12
View File
@@ -58,7 +58,7 @@ import { keyFromPassphrase } from './key_passphrase';
import { decodeRecoveryKey, encodeRecoveryKey } from './recoverykey';
import { VerificationRequest } from "./verification/request/VerificationRequest";
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 { KeySignatureUploadError } from "../errors";
import { calculateKeyCheck, decryptAES, encryptAES } from './aes';
@@ -309,7 +309,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
private oneTimeKeyCount: number;
private needsNewFallback: boolean;
private fallbackCleanup?: number; // setTimeout ID
private fallbackCleanup?: ReturnType<typeof setTimeout>;
/**
* Cryptography bits
@@ -402,7 +402,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// try to get key from app
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");
@@ -690,7 +690,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// Cross-sign own device
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);
// Sign message key backup with cross-signing master key
@@ -1076,11 +1076,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return this.secretStorage.get(name);
}
public isSecretStored(
name: string,
checkKey?: boolean,
): Promise<Record<string, ISecretStorageKeyInfo> | null> {
return this.secretStorage.isStored(name, checkKey);
public isSecretStored(name: string): Promise<Record<string, ISecretStorageKeyInfo> | null> {
return this.secretStorage.isStored(name);
}
public requestSecret(name: string, devices: string[]): ISecretRequest {
@@ -2321,8 +2318,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
userId: string,
deviceId: string,
transactionId: string = null,
): any { // TODO types
let request;
): VerificationBase<any, any> {
let request: Request;
if (transactionId) {
request = this.toDeviceVerificationRequests.getRequestBySenderAndTxnId(userId, transactionId);
if (!request) {
@@ -2890,7 +2887,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
} else {
const content = event.getWireContent();
const alg = this.getRoomDecryptor(event.getRoomId(), content.algorithm);
return await alg.decryptEvent(event);
return alg.decryptEvent(event);
}
}
+2 -2
View File
@@ -38,7 +38,7 @@ interface IKey {
iterations: number;
}
export async function keyFromAuthData(authData: IAuthData, password: string): Promise<Uint8Array> {
export function keyFromAuthData(authData: IAuthData, password: string): Promise<Uint8Array> {
if (!global.Olm) {
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,
authData.private_key_iterations,
authData.private_key_bits || DEFAULT_BITSIZE,
+1 -1
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import bs58 from 'bs58';
import * as bs58 from 'bs58';
// picked arbitrarily but to try & avoid clashing with any bitcoin ones
// (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>(
mode: Mode,
stores: Iterable<string>,
stores: string | string[],
func: (txn: IDBTransaction) => T,
log: PrefixedLogger = logger,
): Promise<T> {
+1 -1
View File
@@ -55,7 +55,7 @@ export class VerificationBase<
private cancelled = false;
private _done = false;
private promise: Promise<void> = null;
private transactionTimeoutTimer: number = null;
private transactionTimeoutTimer: ReturnType<typeof setTimeout> = null;
protected expectedEvent: string;
private resolve: () => void;
private reject: (e: Error | MatrixEvent) => void;
@@ -184,7 +184,7 @@ export class InRoomChannel implements IVerificationChannel {
* @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.
*/
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
// some circumstances Room.timeline can get emitted twice for the same event
if (request.hasEventId(event.getId())) {
@@ -221,8 +221,7 @@ export class InRoomChannel implements IVerificationChannel {
const isRemoteEcho = !!event.getUnsigned().transaction_id;
const isSentByUs = event.getSender() === this.client.getUserId();
return await request.handleEvent(
type, event, isLiveEvent, isRemoteEcho, isSentByUs);
return request.handleEvent(type, event, isLiveEvent, isRemoteEcho, isSentByUs);
}
/**
@@ -95,7 +95,7 @@ export class VerificationRequest<
private eventsByUs = new Map<string, MatrixEvent>();
private eventsByThem = new Map<string, MatrixEvent>();
private _observeOnly = false;
private timeoutTimer: number = null;
private timeoutTimer: ReturnType<typeof setTimeout> = null;
private _accepting = false;
private _declining = false;
private verifierHasFinished = false;
+4 -4
View File
@@ -30,7 +30,7 @@ import type { Request as _Request, CoreOptions } from "request";
import * as callbacks from "./realtime-callbacks";
import { IUploadOpts } from "./@types/requests";
import { IAbortablePromise, IUsageLimit } from "./@types/partials";
import { IDeferred } from "./utils";
import { IDeferred, sleep } from "./utils";
import { Callback } from "./client";
import * as utils from "./utils";
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} message The Matrix 'error' value, e.g. "Missing token."
* @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 {
public readonly errcode: string;
@@ -1114,9 +1114,9 @@ export async function retryNetworkOperation<T>(maxAttempts: number, callback: ()
const timeout = 1000 * Math.pow(2, attempts);
logger.log(`network operation failed ${attempts} times,` +
` retrying in ${timeout}ms...`);
await new Promise(r => setTimeout(r, timeout));
await sleep(timeout);
}
return await callback();
return callback();
} catch (err) {
if (err instanceof ConnectionError) {
attempts += 1;
+1 -1
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import request from "request";
import * as request from "request";
import * as matrixcs from "./matrix";
import * as utils from "./utils";
+32 -19
View File
@@ -203,6 +203,8 @@ export class InteractiveAuth {
private chosenFlow: IFlow = null;
private currentStage: string = null;
private emailAttempt = 1;
// if we are currently trying to submit an auth dict (which includes polling)
// the promise the will resolve/reject when it completes
private submitPromise: Promise<void> = null;
@@ -408,6 +410,34 @@ export class InteractiveAuth {
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
* startAuthStage.
@@ -458,24 +488,9 @@ export class InteractiveAuth {
return;
}
if (
!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;
if (!this.emailSid && this.chosenFlow.stages.includes(AuthType.Email)) {
try {
const requestTokenResult = await this.requestEmailTokenCallback(
this.inputs.emailAddress,
this.clientSecret,
1, // TODO: Multiple send attempts?
this.data.session,
);
this.emailSid = requestTokenResult.sid;
await this.requestEmailToken();
// NB. promise is not resolved here - at some point, doRequest
// will be called again and if the user has jumped through all
// the hoops correctly, auth will be complete and the request
@@ -491,8 +506,6 @@ export class InteractiveAuth {
// send the email, for whatever reason.
this.attemptAuthDeferred.reject(e);
this.attemptAuthDeferred = null;
} finally {
this.requestingEmailToken = false;
}
}
}
+2 -3
View File
@@ -17,8 +17,7 @@ limitations under the License.
import { MemoryCryptoStore } from "./crypto/store/memory-crypto-store";
import { MemoryStore } from "./store/memory";
import { MatrixScheduler } from "./scheduler";
import { MatrixClient } from "./client";
import { ICreateClientOpts } from "./client";
import { MatrixClient, ICreateClientOpts } from "./client";
import { DeviceTrustLevel } from "./crypto/CrossSigning";
import { ISecretStorageKeyInfo } from "./crypto/api";
@@ -153,7 +152,7 @@ export interface ICryptoCallbacks {
export function createClient(opts: ICreateClientOpts | string) {
if (typeof opts === "string") {
opts = {
"baseUrl": opts as string,
"baseUrl": opts,
};
}
opts.request = opts.request || requestInstance;
+1 -1
View File
@@ -54,7 +54,7 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
public readonly roomId: string;
private _beaconInfo: BeaconInfoState;
private _isLive: boolean;
private livenessWatchInterval: number;
private livenessWatchInterval: ReturnType<typeof setInterval>;
private _latestLocationState: BeaconLocationState | undefined;
constructor(
-1
View File
@@ -28,7 +28,6 @@ import { EventType, RelationType } from "../@types/event";
import { RoomState } from "./room-state";
import { TypedEventEmitter } from "./typed-event-emitter";
// var DEBUG = false;
const DEBUG = true;
let debuglog: (...args: any[]) => void;
+12 -16
View File
@@ -1043,7 +1043,7 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
* 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
* 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
* by a visibility event being redacted).
*/
@@ -1065,9 +1065,7 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
reason: reason,
});
}
if (change) {
this.emit(MatrixEventEvent.VisibilityChange, this, visible);
}
this.emit(MatrixEventEvent.VisibilityChange, this, visible);
}
}
@@ -1112,23 +1110,21 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
}
this.event.unsigned.redacted_because = redactionEvent.event as IEvent;
let key;
for (key in this.event) {
if (!this.event.hasOwnProperty(key)) {
continue;
}
if (!REDACT_KEEP_KEYS.has(key)) {
for (const key in this.event) {
if (this.event.hasOwnProperty(key) && !REDACT_KEEP_KEYS.has(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 content = this.getContent();
for (key in content) {
if (!content.hasOwnProperty(key)) {
continue;
}
if (!keeps[key]) {
for (const key in content) {
if (content.hasOwnProperty(key) && !keeps[key]) {
delete content[key];
}
}
@@ -1589,7 +1585,7 @@ const REDACT_KEEP_KEYS = new Set([
'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 = {
[EventType.RoomMember]: { 'membership': 1 },
[EventType.RoomCreate]: { 'creator': 1 },
+1 -2
View File
@@ -26,10 +26,9 @@ import { MatrixEvent, MatrixEventEvent } from "./event";
import { MatrixClient } from "../client";
import { GuestAccess, HistoryVisibility, IJoinRuleEventContent, JoinRule } from "../@types/partials";
import { TypedEventEmitter } from "./typed-event-emitter";
import { Beacon, BeaconEvent, BeaconEventHandlerMap } from "./beacon";
import { Beacon, BeaconEvent, BeaconEventHandlerMap, getBeaconInfoIdentifier, BeaconIdentifier } from "./beacon";
import { TypedReEmitter } from "../ReEmitter";
import { M_BEACON, M_BEACON_INFO } from "../@types/beacon";
import { getBeaconInfoIdentifier, BeaconIdentifier } from "./beacon";
// possible statuses for out-of-band member loading
enum OobStatus {
+54 -25
View File
@@ -47,6 +47,7 @@ import {
ThreadFilterType,
} from "./thread";
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
@@ -58,7 +59,7 @@ import { IStateEventWithRoomId } from "../@types/search";
const KNOWN_SAFE_ROOM_VERSION = '9';
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());
return new MatrixEvent({
content: {
@@ -93,13 +94,13 @@ interface IReceipt {
ts: number;
}
interface IWrappedReceipt {
export interface IWrappedReceipt {
eventId: string;
data: IReceipt;
}
interface ICachedReceipt {
type: string;
type: ReceiptType;
userId: string;
data: IReceipt;
}
@@ -108,7 +109,7 @@ type ReceiptCache = {[eventId: string]: ICachedReceipt[]};
interface IReceiptContent {
[eventId: string]: {
[type: string]: {
[key in ReceiptType]: {
[userId: string]: IReceipt;
};
};
@@ -1124,14 +1125,14 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
* The aliases returned by this function may not necessarily
* still point to this room.
* @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[] {
const aliasStrings: string[] = [];
const aliasEvents = this.currentState.getStateEvents(EventType.RoomAliases);
if (aliasEvents) {
for (let i = 0; i < aliasEvents.length; ++i) {
const aliasEvent = aliasEvents[i];
for (const aliasEvent of aliasEvents) {
if (Array.isArray(aliasEvent.getContent().aliases)) {
const filteredAliases = aliasEvent.getContent<{ aliases: string[] }>().aliases.filter(a => {
if (typeof(a) !== "string") return false;
@@ -1141,7 +1142,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
// It's probably valid by here.
return true;
});
Array.prototype.push.apply(aliasStrings, filteredAliases);
aliasStrings.push(...filteredAliases);
}
}
}
@@ -1644,8 +1645,8 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
eventsByThread[threadId]?.push(event);
}
Object.entries(eventsByThread).map(([threadId, events]) => (
this.addThreadedEvents(threadId, events, toStartOfTimeline)
Object.entries(eventsByThread).map(([threadId, threadEvents]) => (
this.addThreadedEvents(threadId, threadEvents, toStartOfTimeline)
));
}
@@ -1658,8 +1659,10 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
if (rootEvent) {
const tl = this.getTimelineForEvent(rootEvent.getId());
const relatedEvents = tl?.getTimelineSet().getAllRelationsEventForEvent(rootEvent.getId());
if (relatedEvents) {
events = events.concat(relatedEvents);
if (relatedEvents?.length) {
// 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)));
}
}
@@ -1792,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.
if (event.sender && event.getType() !== EventType.RoomRedaction) {
this.addReceipt(synthesizeReceipt(
event.sender.userId, event, "m.read",
event.sender.userId, event, ReceiptType.Read,
), true);
// Any live events from a user could be taken as implicit
@@ -2143,23 +2146,23 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
const threadRoots = this.findThreadRoots(events);
const eventsByThread: { [threadId: string]: MatrixEvent[] } = {};
for (let i = 0; i < events.length; i++) {
for (const event of events) {
// TODO: We should have a filter to say "only add state event types X Y Z to the timeline".
this.processLiveEvent(events[i]);
this.processLiveEvent(event);
const {
shouldLiveInRoom,
shouldLiveInThread,
threadId,
} = this.eventShouldLiveIn(events[i], events, threadRoots);
} = this.eventShouldLiveIn(event, events, threadRoots);
if (shouldLiveInThread && !eventsByThread[threadId]) {
eventsByThread[threadId] = [];
}
eventsByThread[threadId]?.push(events[i]);
eventsByThread[threadId]?.push(event);
if (shouldLiveInRoom) {
this.addLiveEvent(events[i], duplicateStrategy, fromCache);
this.addLiveEvent(event, duplicateStrategy, fromCache);
}
}
@@ -2314,14 +2317,23 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
*/
public getUsersReadUpTo(event: MatrixEvent): string[] {
return this.getReceiptsForEvent(event).filter(function(receipt) {
return receipt.type === "m.read";
return [ReceiptType.Read, ReceiptType.ReadPrivate].includes(receipt.type);
}).map(function(receipt) {
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) {
return realReceipt;
}
@@ -2339,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.
*/
public getEventReadUpTo(userId: string, ignoreSynthesized = false): string | null {
const readReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized);
return readReceipt?.eventId ?? null;
const timelineSet = this.getUnfilteredTimelineSet();
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;
}
/**
@@ -2493,7 +2522,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
}
this.receiptCacheByEventId[eventId].push({
userId: userId,
type: receiptType,
type: receiptType as ReceiptType,
data: receipt,
});
});
@@ -2506,9 +2535,9 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
* client the fact that we've sent one.
* @param {string} userId The user ID if the receipt sender
* @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);
}
+9 -2
View File
@@ -79,6 +79,12 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
) {
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.client = opts.client;
this.timelineSet = new EventTimelineSet(this.room, {
@@ -221,9 +227,10 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
this._currentUserParticipated = true;
}
// Add all annotations and replace relations to the timeline so that the relations are processed accordingly
if ([RelationType.Annotation, RelationType.Replace].includes(event.getRelation()?.rel_type as RelationType)) {
this.addEventToTimeline(event, toStartOfTimeline);
// Apply annotations and replace relations to the relations of the timeline only
this.timelineSet.setRelationsTarget(event);
this.timelineSet.aggregateRelations(event);
return;
}
-21
View File
@@ -27,8 +27,6 @@ export enum UserEvent {
Presence = "User.presence",
CurrentlyActive = "User.currentlyActive",
LastPresenceTs = "User.lastPresenceTs",
/* @deprecated */
_UnstableStatusMessage = "User.unstable_statusMessage",
}
export type UserEventHandlerMap = {
@@ -37,7 +35,6 @@ export type UserEventHandlerMap = {
[UserEvent.Presence]: (event: MatrixEvent | undefined, user: User) => void;
[UserEvent.CurrentlyActive]: (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> {
@@ -59,8 +56,6 @@ export class User extends TypedEventEmitter<UserEvent, UserEventHandlerMap> {
presence: 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
@@ -81,9 +76,6 @@ export class User extends TypedEventEmitter<UserEvent, UserEventHandlerMap> {
* when a user was last active.
* @prop {Boolean} currentlyActive Whether we should consider lastActiveAgo to be
* 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 {MatrixEvent} events.presence The m.presence event for this user.
*/
@@ -219,19 +211,6 @@ export class User extends TypedEventEmitter<UserEvent, UserEventHandlerMap> {
public getLastActiveTs(): number {
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);
}
}
/**
+18 -26
View File
@@ -34,6 +34,7 @@ import {
PushRuleSet,
TweakName,
} from "./@types/PushRules";
import { EventType } from "./@types/event";
/**
* @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,
// and so we can put them here.
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
rule_id: ".m.rule.reaction",
@@ -96,6 +72,22 @@ const DEFAULT_OVERRIDE_RULES: IPushRule[] = [
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: [
PushRuleActionName.DontNotify,
],
},
];
export interface IActionsObject {
@@ -300,7 +292,7 @@ export class PushProcessor {
const memberCount = room.currentState.getJoinedMemberCount();
const m = cond.is.match(/^([=<>]*)([0-9]*)$/);
const m = cond.is.match(/^([=<>]*)(\d*)$/);
if (!m) {
return false;
}
+6 -2
View File
@@ -37,7 +37,11 @@ export interface ISavedSync {
export interface IStore {
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>;
/**
@@ -105,7 +109,7 @@ export interface IStore {
/**
* No-op.
* @param {Room} room
* @param {integer} limit
* @param {number} limit
* @return {Array}
*/
scrollback(room: Room, limit: number): MatrixEvent[];
+1 -1
View File
@@ -199,7 +199,7 @@ export class MemoryStore implements IStore {
/**
* Retrieve scrollback for this 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'
* length and at least 0. The objects are the raw event JSON.
*/
+1 -1
View File
@@ -123,7 +123,7 @@ export class StubStore implements IStore {
/**
* No-op.
* @param {Room} room
* @param {integer} limit
* @param {number} limit
* @return {Array}
*/
public scrollback(room: Room, limit: number): MatrixEvent[] {
+30 -13
View File
@@ -24,6 +24,7 @@ import { deepCopy } from "./utils";
import { IContent, IUnsigned } from "./models/event";
import { IRoomSummary } from "./models/room-summary";
import { EventType } from "./@types/event";
import { ReceiptType } from "./@types/read_receipts";
interface IOpts {
maxTimelineEntries?: number;
@@ -49,7 +50,6 @@ export interface IRoomEvent extends IMinimalEvent {
event_id: string;
sender: string;
origin_server_ts: number;
unsigned?: IUnsigned;
/** @deprecated - legacy field */
age?: number;
}
@@ -157,6 +157,7 @@ interface IRoom {
_readReceipts: {
[userId: string]: {
data: IMinimalEvent;
type: ReceiptType;
eventId: string;
};
};
@@ -416,16 +417,31 @@ export class SyncAccumulator {
// of a hassle to work with. We'll inflate this back out when
// getJSON() is called.
Object.keys(e.content).forEach((eventId) => {
if (!e.content[eventId]["m.read"]) {
if (!e.content[eventId][ReceiptType.Read] && !e.content[eventId][ReceiptType.ReadPrivate]) {
return;
}
Object.keys(e.content[eventId]["m.read"]).forEach((userId) => {
// clobber on user ID
currentData._readReceipts[userId] = {
data: e.content[eventId]["m.read"][userId],
eventId: eventId,
};
});
const read = e.content[eventId][ReceiptType.Read];
if (read) {
Object.keys(read).forEach((userId) => {
// clobber on user ID
currentData._readReceipts[userId] = {
data: e.content[eventId][ReceiptType.Read][userId],
type: ReceiptType.Read,
eventId: eventId,
};
});
}
const readPrivate = e.content[eventId][ReceiptType.ReadPrivate];
if (readPrivate) {
Object.keys(readPrivate).forEach((userId) => {
// clobber on user ID
currentData._readReceipts[userId] = {
data: e.content[eventId][ReceiptType.ReadPrivate][userId],
type: ReceiptType.ReadPrivate,
eventId: eventId,
};
});
}
});
});
}
@@ -552,11 +568,12 @@ export class SyncAccumulator {
Object.keys(roomData._readReceipts).forEach((userId) => {
const receiptData = roomData._readReceipts[userId];
if (!receiptEvent.content[receiptData.eventId]) {
receiptEvent.content[receiptData.eventId] = {
"m.read": {},
};
receiptEvent.content[receiptData.eventId] = {};
}
receiptEvent.content[receiptData.eventId]["m.read"][userId] = (
if (!receiptEvent.content[receiptData.eventId][receiptData.type]) {
receiptEvent.content[receiptData.eventId][receiptData.type] = {};
}
receiptEvent.content[receiptData.eventId][receiptData.type][userId] = (
receiptData.data
);
});
+6 -16
View File
@@ -54,6 +54,7 @@ import { IPushRules } from "./@types/PushRules";
import { RoomStateEvent } from "./models/room-state";
import { RoomMemberEvent } from "./models/room-member";
import { BeaconEvent } from "./models/beacon";
import { IEventsResponse } from "./@types/requests";
const DEBUG = true;
@@ -152,7 +153,7 @@ export class SyncApi {
private syncStateData: ISyncStateData = null; // additional data (eg. error object for failed sync)
private catchingUp = false;
private running = false;
private keepAliveTimer: number = null;
private keepAliveTimer: ReturnType<typeof setTimeout> = null;
private connectionReturnedDefer: IDeferred<boolean> = null;
private notifEvents: MatrixEvent[] = []; // accumulator of sync events in the current sync response
private failedSyncCount = 0; // Number of consecutive failed /sync requests
@@ -274,7 +275,7 @@ export class SyncApi {
getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter,
).then(function(filterId) {
qps.filter = filterId;
return client.http.authedRequest<any>( // TODO types
return client.http.authedRequest<ISyncResponse>(
undefined, Method.Get, "/sync", qps as any, undefined, localTimeoutMs,
);
}).then(async (data) => {
@@ -409,8 +410,7 @@ export class SyncApi {
}
// FIXME: gut wrenching; hard-coded timeout values
// TODO types
this.client.http.authedRequest<any>(undefined, Method.Get, "/events", {
this.client.http.authedRequest<IEventsResponse>(undefined, Method.Get, "/events", {
room_id: peekRoom.roomId,
timeout: String(30 * 1000),
from: token,
@@ -874,7 +874,7 @@ export class SyncApi {
private doSyncRequest(syncOptions: ISyncOptions, syncToken: string): IRequestPromise<ISyncResponse> {
const qps = this.getSyncParams(syncOptions, syncToken);
return this.client.http.authedRequest( // TODO types
return this.client.http.authedRequest<ISyncResponse>(
undefined, Method.Get, "/sync", qps as any, undefined,
qps.timeout + BUFFER_PERIOD_MS,
);
@@ -1295,16 +1295,6 @@ export class SyncApi {
if (e.isState() && e.getType() == "m.room.encryption" && this.opts.crypto) {
await this.opts.crypto.onCryptoEvent(e);
}
if (e.isState() && e.getType() === "im.vector.user_status") {
let user = client.store.getUser(e.getStateKey());
if (user) {
user.unstable_updateStatusMessage(e);
} else {
user = createNewUser(client, e.getStateKey());
user.unstable_updateStatusMessage(e);
client.store.storeUser(user);
}
}
};
await utils.promiseMapSeries(stateEvents, processRoomEvent);
@@ -1400,7 +1390,7 @@ export class SyncApi {
* Starts polling the connectivity check endpoint
* @param {number} delay How long to delay until the first poll.
* defaults to a short, randomised interval (to prevent
* tightlooping if /versions succeeds but /sync etc. fail).
* tight-looping if /versions succeeds but /sync etc. fail).
* @return {promise} which resolves once the connection returns
*/
private startKeepAlives(delay?: number): Promise<boolean> {
+10 -4
View File
@@ -23,7 +23,7 @@ limitations under the License.
import unhomoglyph from "unhomoglyph";
import promiseRetry from "p-retry";
import type NodeCrypto from "crypto";
import type * as NodeCrypto from "crypto";
import { MatrixEvent } from ".";
import { M_TIMESTAMP } from "./@types/location";
@@ -465,7 +465,7 @@ export function defer<T = void>(): IDeferred<T> {
export async function promiseMapSeries<T>(
promises: Array<T | Promise<T>>,
fn: (t: T) => void,
fn: (t: T) => Promise<unknown> | void, // if async/promise we don't care about the type as we only await resolution
): Promise<void> {
for (const o of promises) {
await fn(await o);
@@ -473,7 +473,7 @@ export async function promiseMapSeries<T>(
}
export function promiseTry<T>(fn: () => T | Promise<T>): Promise<T> {
return new Promise((resolve) => resolve(fn()));
return Promise.resolve(fn());
}
// Creates and awaits all promises, running no more than `chunkSize` at the same time
@@ -676,7 +676,13 @@ export function prevString(s: string, alphabet = DEFAULT_ALPHABET): string {
export function lexicographicCompare(a: string, b: string): number {
// Dev note: this exists because I'm sad that you can use math operators on strings, so I've
// hidden the operation in this function.
return (a < b) ? -1 : ((a === b) ? 0 : 1);
if (a < b) {
return -1;
} else if (a > b) {
return 1;
} else {
return 0;
}
}
const collator = new Intl.Collator();
+39 -36
View File
@@ -298,7 +298,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// yet, null if we have but they didn't send a party ID.
private opponentPartyId: string;
private opponentCaps: CallCapabilities;
private inviteTimeout: number;
private inviteTimeout: ReturnType<typeof setTimeout>;
// The logic of when & if a call is on hold is nontrivial and explained in is*OnHold
// This flag represents whether we want the other party to be on hold
@@ -322,7 +322,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
private remoteSDPStreamMetadata: SDPStreamMetadata;
private callLengthInterval: number;
private callLengthInterval: ReturnType<typeof setInterval>;
private callLength = 0;
constructor(opts: CallOpts) {
@@ -708,9 +708,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
const statsReport = await this.peerConn.getStats();
const stats = [];
for (const item of statsReport) {
statsReport.forEach(item => {
stats.push(item[1]);
}
});
return stats;
}
@@ -988,9 +988,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
* @param {string} desktopCapturerSourceId optional id of the desktop capturer source to use
* @returns {boolean} new screensharing state
*/
public async setScreensharingEnabled(
enabled: boolean, desktopCapturerSourceId?: string,
): Promise<boolean> {
public async setScreensharingEnabled(enabled: boolean, desktopCapturerSourceId?: string): Promise<boolean> {
// Skip if there is nothing to do
if (enabled && this.isScreensharing()) {
logger.warn(`There is already a screensharing stream - there is nothing to do!`);
@@ -1002,7 +1000,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// Fallback to replaceTrack()
if (!this.opponentSupportsSDPStreamMetadata()) {
return await this.setScreensharingEnabledWithoutMetadataSupport(enabled, desktopCapturerSourceId);
return this.setScreensharingEnabledWithoutMetadataSupport(enabled, desktopCapturerSourceId);
}
logger.debug(`Set screensharing enabled? ${enabled}`);
@@ -2264,6 +2262,37 @@ function setTracksEnabled(tracks: Array<MediaStreamTrack>, enabled: boolean): vo
}
}
export function supportsMatrixCall(): boolean {
// typeof prevents Node from erroring on an undefined reference
if (typeof(window) === 'undefined' || typeof(document) === 'undefined') {
// NB. We don't log here as apps try to create a call object as a test for
// whether calls are supported, so we shouldn't fill the logs up.
return false;
}
// Firefox throws on so little as accessing the RTCPeerConnection when operating in a secure mode.
// There's some information at https://bugzilla.mozilla.org/show_bug.cgi?id=1542616 though the concern
// is that the browser throwing a SecurityError will brick the client creation process.
try {
const supported = Boolean(
window.RTCPeerConnection || window.RTCSessionDescription ||
window.RTCIceCandidate || navigator.mediaDevices,
);
if (!supported) {
/* istanbul ignore if */ // Adds a lot of noise to test runs, so disable logging there.
if (process.env.NODE_ENV !== "test") {
logger.error("WebRTC is not supported in this browser / environment");
}
return false;
}
} catch (e) {
logger.error("Exception thrown when trying to access WebRTC", e);
return false;
}
return true;
}
/**
* DEPRECATED
* Use client.createCall()
@@ -2277,34 +2306,8 @@ function setTracksEnabled(tracks: Array<MediaStreamTrack>, enabled: boolean): vo
* since it's only possible to set this option on outbound calls.
* @return {MatrixCall} the call or null if the browser doesn't support calling.
*/
export function createNewMatrixCall(client: any, roomId: string, options?: CallOpts): MatrixCall {
// typeof prevents Node from erroring on an undefined reference
if (typeof(window) === 'undefined' || typeof(document) === 'undefined') {
// NB. We don't log here as apps try to create a call object as a test for
// whether calls are supported, so we shouldn't fill the logs up.
return null;
}
// Firefox throws on so little as accessing the RTCPeerConnection when operating in
// a secure mode. There's some information at https://bugzilla.mozilla.org/show_bug.cgi?id=1542616
// though the concern is that the browser throwing a SecurityError will brick the
// client creation process.
try {
const supported = Boolean(
window.RTCPeerConnection || window.RTCSessionDescription ||
window.RTCIceCandidate || navigator.mediaDevices,
);
if (!supported) {
// Adds a lot of noise to test runs, so disable logging there.
if (process.env.NODE_ENV !== "test") {
logger.error("WebRTC is not supported in this browser / environment");
}
return null;
}
} catch (e) {
logger.error("Exception thrown when trying to access WebRTC", e);
return null;
}
export function createNewMatrixCall(client: any, roomId: string, options?: CallOpts): MatrixCall | null {
if (!supportsMatrixCall()) return null;
const optionsForceTURN = options ? options.forceTURN : false;
+4 -8
View File
@@ -72,21 +72,17 @@ export class CallEventHandler {
this.client.decryptEventIfNeeded(event);
}));
const ignoreCallIds = new Set<String>();
const ignoreCallIds = new Set<string>();
// inspect the buffer and mark all calls which have been answered
// or hung up before passing them to the call event handler.
for (const ev of this.callEventBuffer) {
if (ev.getType() === EventType.CallAnswer ||
ev.getType() === EventType.CallHangup) {
if (ev.getType() === EventType.CallAnswer || ev.getType() === EventType.CallHangup) {
ignoreCallIds.add(ev.getContent().call_id);
}
}
// now loop through the buffer chronologically and inject them
for (const e of this.callEventBuffer) {
if (
e.getType() === EventType.CallInvite &&
ignoreCallIds.has(e.getContent().call_id)
) {
if (e.getType() === EventType.CallInvite && ignoreCallIds.has(e.getContent().call_id)) {
// This call has previously been answered or hung up: ignore it
continue;
}
@@ -191,7 +187,7 @@ export class CallEventHandler {
}
// Were we trying to call that user (room)?
let existingCall;
let existingCall: MatrixCall;
for (const thisCall of this.calls.values()) {
const isCalling = [CallState.WaitLocalMedia, CallState.CreateOffer, CallState.InviteSent].includes(
thisCall.state,
+1 -1
View File
@@ -69,7 +69,7 @@ export class CallFeed extends TypedEventEmitter<CallFeedEvent, EventHandlerMap>
private frequencyBinCount: Float32Array;
private speakingThreshold = SPEAKING_THRESHOLD;
private speaking = false;
private volumeLooperTimeout: number;
private volumeLooperTimeout: ReturnType<typeof setTimeout>;
constructor(opts: ICallFeedOpts) {
super();
+265 -285
View File
File diff suppressed because it is too large Load Diff