Compare commits

..

86 Commits

Author SHA1 Message Date
RiotRobot 32b2c217c7 v18.0.0-rc.2 2022-05-20 10:03:29 +01:00
RiotRobot 7da555c255 Prepare changelog for v18.0.0-rc.2 2022-05-20 10:03:28 +01:00
Michael Telatynski 88348660eb Catch promise errors in degradable (fixes #2384) (#2385) (#2388)
Co-authored-by: Lars Richard <lars.richard@iserv.eu>
(cherry picked from commit 81d884f899)

Co-authored-by: schmop <lars.richard@rocketmail.com>
2022-05-20 09:56:05 +01:00
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
RiotRobot e29ee105aa v17.2.0 2022-05-10 14:46:15 +01:00
RiotRobot 454eb7e627 Prepare changelog for v17.2.0 2022-05-10 14:46:15 +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
RiotRobot 70c6a4b567 v17.2.0-rc.1 2022-05-03 15:20:39 +01:00
RiotRobot 5cd615c495 Prepare changelog for v17.2.0-rc.1 2022-05-03 15:20:38 +01:00
Michael Telatynski 88f5ea675d Tweak sonarqube run (#2339) 2022-05-03 14:46:36 +01:00
Michael Telatynski ac5fee0a69 Fix race conditions around threads (#2331) 2022-05-03 14:25:17 +01:00
Michael Telatynski 274d6a9597 Add CI to require labels on PRs for better changelog management (#2337) 2022-05-03 10:34:55 +01:00
Michael Telatynski d190cdc307 Switch coverage to SonarQube (#2334) 2022-05-02 08:32:53 +00:00
Michael Telatynski b896111269 Apply corrections identified by SonarQube (#2336)
* Apply corrections identified by SonarQube

* Apply corrections identified by SonarQube

* Make type more flexible
2022-05-02 03:23:17 +01:00
Aaron Raimist 6137afeb28 Mark room versions 7, 8, and 9 as safe (#2318)
* Mark room versions 7, 8, and 9 as safe

Workaround for https://github.com/vector-im/element-web/issues/20166 https://github.com/vector-im/element-web/issues/11820

* Update KNOWN_SAFE_ROOM_VERSION to room version 9
2022-05-01 19:10:09 -06:00
Michael Telatynski 2ebf33544f Try SonarCloud (#2330) 2022-04-29 11:07:35 +01:00
Michael Telatynski 45f1991d4e Correctly specify minimum Node version (#2324) 2022-04-29 11:02:05 +01:00
Michael Telatynski aa283a3c9e Update CONTRIBUTING.md (#2329) 2022-04-28 15:48:21 +01:00
Kerry 34ee566d88 Live location sharing: handle encrypted messages in processBeaconEvents (#2327)
* handle encrypted locations

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix processBeaconEvents to handle encrypted events

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-04-28 16:42:37 +02:00
RiotRobot 3649cf46d3 Resetting package fields for development 2022-04-26 11:34:42 +01:00
RiotRobot 8f00eb6771 Merge branch 'master' into develop 2022-04-26 11:34:42 +01:00
Michael Telatynski 01ec51891a Merge remote-tracking branch 'origin/release-v17.1.0'
# Conflicts:
#	CHANGELOG.md
#	package.json
#	src/client.ts
#	src/models/room.ts
#	src/models/thread.ts
#	src/sync.ts
2022-04-26 11:31:20 +01:00
RiotRobot 738876a563 v17.1.0 2022-04-26 11:13:47 +01:00
RiotRobot 2d9b4b3896 Prepare changelog for v17.1.0 2022-04-26 11:13:46 +01:00
Michael Telatynski ba06e430c4 Ignore m.replace relations on state events, they're invalid (#2306)
* Ignore m.replace relations on state events, they're invalid

* Add tests

* Fix test

Co-authored-by: Kerry <kerrya@element.io>
2022-04-26 10:02:36 +00:00
Michael Telatynski ac08e52410 De-duplicate code and fix types (#2319) 2022-04-25 17:15:35 +01:00
David Baker 1bb82108b7 fix example in readme (#2315)
Co-authored-by: Matthew Hodgson <matthew@matrix.org>
2022-04-22 12:04:31 +01:00
Michael Telatynski e133005b44 Don't decrement the length count of a thread when root redacted (#2314) 2022-04-21 16:02:42 +00:00
Michael Telatynski c0cb66233a Prevent attempt to create thread with id "undefined" (#2308) 2022-04-21 08:01:24 +01:00
Michael Telatynski d82cdd3b19 Move more stuff from BK to GHA (#2309) 2022-04-20 18:09:33 +01:00
Michael Telatynski 6a51b02bed Move more stuff from BK to GHA (#2307)
* Use consistent indentation in GHA yaml files

* Prefer setup-node's dep caching

* Tidy up test_coverage job

* Move js sdk lint ci to gha

* notify react-sdk of develop merges

* Name the jobs

* test

* Update secrets

* Fixup
2022-04-20 16:25:58 +01:00
Michael Telatynski 47d2c063fa Upgrade dependencies (#2304) 2022-04-20 13:53:39 +02:00
Michael Telatynski 540514c805 Update threads handling for replies-to-thread-responses as per MSC update (#2305)
* Update threads handling for replies-to-thread-responses as per MSC update

* Update tests to match new behaviour
2022-04-19 18:15:06 +02:00
Faye Duxovni db58a66e19 Add method for checking whether our other devices are cross-signed, even when this device isn't (#2288) 2022-04-19 15:02:17 +01:00
RiotRobot 91a67bdac3 v17.1.0-rc.1 2022-04-19 14:41:27 +01:00
RiotRobot 48d3fce22d Prepare changelog for v17.1.0-rc.1 2022-04-19 14:41:26 +01:00
Hugh Nimmo-Smith 9f4598638d Add MatrixClient.doesServerSupportLogoutDevices() for MSC2457 (#2297) 2022-04-15 10:27:12 +01:00
CommanderRoot cde935629d Replace deprecated String.prototype.substr() (#2298)
.substr() is deprecated so we replace it with .slice() which works similarily but isn't deprecated

Signed-off-by: Tobias Speicher <rootcommander@gmail.com>
2022-04-14 15:23:27 -06:00
Kerry fbe81ad823 Live location sharing - expose room liveBeaconIds (#2296)
* updates rooms live beacon ids on destroy

Signed-off-by: Kerry Archibald <kerrya@element.io>

* expose live beacons ids

Signed-off-by: Kerry Archibald <kerrya@element.io>

* room state emit all the time on beacon liveness change

Signed-off-by: Kerry Archibald <kerrya@element.io>

* update comment

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-04-14 15:04:43 +02:00
Michael Telatynski c3d7a4977a Stop tracking threads if threads support is disabled (#2295) 2022-04-13 16:23:15 +01:00
Robin 9aab917836 Fix coverage diffs for PRs that aren't up to date, take 3 (#2294) 2022-04-12 20:22:14 -04:00
Jonathan de Jong 54661cca95 Change _unstable_getSharedRooms to _unstable_getMutualRooms (#2271)
* change shared to mutual

* revert name change

* use new unstable feature flag

* support both unstable endpoints
2022-04-12 17:48:48 -06:00
Michael Telatynski b58d09aa9a Prevent duplicated re-emitter setups in event-mapper (#2293) 2022-04-12 09:42:55 +01:00
RiotRobot 3b33237e51 Resetting package fields for development 2022-04-11 16:31:44 +01:00
RiotRobot b8ec62e786 Merge master 2022-04-11 16:31:21 +01:00
Michael Telatynski 286500e335 Fix issues around echo & redaction handling in threads (#2286) 2022-04-11 08:58:13 +01:00
Hugh Nimmo-Smith 5937e6a6a8 Support for MSC2457 logout_devices param for setPassword() (#2285) 2022-04-09 14:07:33 +01:00
Eric Eastwood c6c22e394b Ignore eslint --fix formatting changes in git blame (#2287)
* Ignore eslint --fix formatting changes in git blame

Docs: https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view

* Conform on white-space spelling
2022-04-08 23:52:05 -06:00
Callum Brown 378802a5ab Stabilise token authenticated registration support (#2181)
* Stabilise token authenticated registration support

Token authenticated registration was added to the Matrix specification in v1.2:
https://spec.matrix.org/v1.2/client-server-api/#token-authenticated-registration

Signed-off-by: Callum Brown <callum@calcuode.com>

* Backwards compatibility with unstable auth type

Servers are not yet widely updated with support for the stable
version of the registration token UIA type.
Clients should check if the authentication type is either
`RegistrationToken` or `UnstableRegistrationToken`.

Signed-off-by: Callum Brown <callum@calcuode.com>
2022-04-08 22:53:57 -06:00
Kerry f963feab0f Live location sharing - Aggregate beacon locations on beacons (#2268)
* add timestamp sorting util

Signed-off-by: Kerry Archibald <kerrya@element.io>

* basic wiring

Signed-off-by: Kerry Archibald <kerrya@element.io>

* quick handle for redacted beacons

Signed-off-by: Kerry Archibald <kerrya@element.io>

* remove fdescribe

Signed-off-by: Kerry Archibald <kerrya@element.io>

* test adding locations

Signed-off-by: Kerry Archibald <kerrya@element.io>

* tidy comments

Signed-off-by: Kerry Archibald <kerrya@element.io>

* test client

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix monitorLiveness for update

Signed-off-by: Kerry Archibald <kerrya@element.io>

* lint

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-04-08 12:26:05 +01:00
Germain 6d0f4e537e Fix notification panel not loading (#2283) 2022-04-08 10:34:01 +01:00
Kerry 781fdf4fdc Live location sharing - update beacon_info implementation to latest MSC (#2281)
* remove M_BEACON_INFO_VARIABLE

Signed-off-by: Kerry Archibald <kerrya@element.io>

* create beacon_info events with non-variable event type

Signed-off-by: Kerry Archibald <kerrya@element.io>

* remove isBeaconInfoEventType

Signed-off-by: Kerry Archibald <kerrya@element.io>

* refer to msc3673 instead of msc3489

Signed-off-by: Kerry Archibald <kerrya@element.io>

* remove event type suffix

Signed-off-by: Kerry Archibald <kerrya@element.io>

* update beacon identifier to use state key

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix beacon spec

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix room-state tests

Signed-off-by: Kerry Archibald <kerrya@element.io>

* add beacon identifier

Signed-off-by: Kerry Archibald <kerrya@element.io>

* dont allow update to older beacon event

Signed-off-by: Kerry Archibald <kerrya@element.io>

* lint

Signed-off-by: Kerry Archibald <kerrya@element.io>

* unnest beacon_info content

Signed-off-by: Kerry Archibald <kerrya@element.io>

* lint

Signed-off-by: Kerry Archibald <kerrya@element.io>

* check redaction event id

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-04-08 10:50:06 +02:00
Michael Telatynski dde4285cdf Fix handling of threaded messages around edits & echoes (#2267) 2022-04-07 13:46:50 +01:00
Robin 3322b47b6d Make self membership less prone to races (#2277) 2022-04-06 08:49:09 -04:00
95 changed files with 3787 additions and 1586 deletions
+3
View File
@@ -21,3 +21,6 @@ insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.{yml,yaml}]
indent_size = 2
+41
View File
@@ -0,0 +1,41 @@
# Minor white-space adjustments
1d1d59c75744e1f6a2be1cb3e0d1bd9ded5f8025
# Import ordering and spacing: eslint-plugin-import
80aaa6c32b50601f82e0c991c24e5a4590f39463
# Minor white-space adjustment
8fb036ba2d01fab66dc4373802ccf19b5cac8541
# Minor white-space adjustment
b63de6a902a9e1f8ffd7697dea33820fc04f028e
3ca84cfc491b0987eec1f13f13cae58d2032bf54
# Conform to new typescript eslint rules
a87858840b57514603f63e2abbbda4f107f05a77
5cf6684129a921295f5593173f16f192336fe0a2
# Comply with new member-delimiter-style rule
b2ad957d298720d3e026b6bd91be0c403338361a
# Fix semicolons in TS files
e2ec8952e38b8fea3f0ccaa09ecb42feeba0d923
# Migrate to `eslint-plugin-matrix-org`
# and `babel/...` to `@babel/...` migration
09fac77ce0d9bcf6637088c29afab84084f0e739
102704e91a70643bcc09721e14b0d909f0ef55c6
# Eslint formatting
cec00cd303787fa9008b6c48826e75ed438036fa
# Minor eslint changes
68bb8182e4e62d8f450f80c408c4b231b8725f1b
c979ff6696e30ab8983ac416a3590996d84d3560
f4a7395e3a3751a1a8e92dd302c49175a3296ad2
# eslint --fix for dangley commas on function calls
423175f5397910b0afe3112d6fb18283fc7d27d4
# eslint ---fix for prefer-const
7bca05af644e8b997dae81e568a3913d8f18d7ca
# Fix linting on tests
cee7f7a280a8c20bafc21c0a2911f60851f7a7ca
# eslint --fix
0fa9f7c6098822db1ae214f352fd1fe5c248b02c
# eslint --fix for lots of white-space
5abf6b9f208801c5022a47023150b5846cb0b309
# eslint --fix
7ed65407e6cdf292ce3cf659310c68d19dcd52b2
# Switch to ESLint from JSHint (Google eslint rules as a base)
e057956ede9ad1a931ff8050c411aca7907e0394
-12
View File
@@ -1,12 +0,0 @@
codecov:
allow_coverage_offsets: True
coverage:
status:
project: off
patch: off
comment:
layout: "diff, files"
behavior: default
require_changes: false
require_base: no
require_head: no
+25
View File
@@ -0,0 +1,25 @@
name: Notify Downstream Projects
on:
push:
branches: [ develop ]
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
notify-downstream:
continue-on-error: true
strategy:
fail-fast: false
matrix:
include:
- repo: vector-im/element-web
event: element-web-notify
- repo: matrix-org/matrix-react-sdk
event: upstream-sdk-notify
runs-on: ubuntu-latest
steps:
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
uses: peter-evans/repository-dispatch@v1
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
repository: ${{ matrix.repo }}
event-type: ${{ matrix.event }}
+59
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 }}
-12
View File
@@ -1,12 +0,0 @@
name: Preview Changelog
on:
pull_request_target:
types: [ opened, edited, labeled ]
jobs:
changelog:
runs-on: ubuntu-latest
steps:
- name: Preview Changelog
uses: matrix-org/allchange@main
with:
ghToken: ${{ secrets.GITHUB_TOKEN }}
+28
View File
@@ -0,0 +1,28 @@
name: Pull Request
on:
pull_request_target:
types: [ opened, edited, labeled, unlabeled, synchronize ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
changelog:
name: Preview Changelog
if: github.event.action != 'synchronize'
runs-on: ubuntu-latest
steps:
- uses: matrix-org/allchange@main
with:
ghToken: ${{ secrets.GITHUB_TOKEN }}
enforce-label:
name: Enforce Labels
runs-on: ubuntu-latest
permissions:
pull-requests: read
steps:
- uses: yogevbd/enforce-label-action@2.1.0
with:
REQUIRED_LABELS_ANY: "T-Defect,T-Deprecation,T-Enhancement,T-Task"
BANNED_LABELS: "X-Blocked"
BANNED_LABELS_DESCRIPTION: "Preventing merge whilst PR is marked blocked!"
+91
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 }}
+37
View File
@@ -0,0 +1,37 @@
name: SonarQube
on:
workflow_run:
workflows: [ "Tests" ]
types:
- completed
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
prdetails:
name: ️ PR Details
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
uses: matrix-org/matrix-js-sdk/.github/workflows/pr_details.yml@develop
with:
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
branch: ${{ github.event.workflow_run.head_branch }}
sonarqube:
name: 🩻 SonarQube
needs: prdetails
# Only wait for prdetails if it isn't skipped
if: |
always() &&
(needs.prdetails.result == 'success' || needs.prdetails.result == 'skipped') &&
github.event.workflow_run.conclusion == 'success'
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
with:
repo: ${{ github.event.workflow_run.head_repository.full_name }}
pr_id: ${{ needs.prdetails.outputs.pr_id }}
head_branch: ${{ needs.prdetails.outputs.head_branch || github.event.workflow_run.head_branch }}
base_branch: ${{ needs.prdetails.outputs.base_branch }}
revision: ${{ github.event.workflow_run.head_sha }}
coverage_workflow_name: tests.yml
coverage_run_id: ${{ github.event.workflow_run.id }}
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+56
View File
@@ -0,0 +1,56 @@
name: Static Analysis
on:
pull_request: { }
push:
branches: [ develop, master ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ts_lint:
name: "Typescript Syntax Check"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
cache: 'yarn'
- name: Install Deps
run: "yarn install"
- name: Typecheck
run: "yarn run lint:types"
js_lint:
name: "ESLint"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
cache: 'yarn'
- name: Install Deps
run: "yarn install"
- name: Run Linter
run: "yarn run lint:js"
docs:
name: "JSDoc Checker"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
cache: 'yarn'
- name: Install Deps
run: "yarn install"
- name: Generate Docs
run: "yarn run gendoc"
-31
View File
@@ -1,31 +0,0 @@
name: Test coverage
on:
pull_request: {}
push:
branches: [develop, main, master]
jobs:
test-coverage:
runs-on: ubuntu-latest
env:
# This must be set for fetchdep.sh to get the right branch
PR_NUMBER: ${{github.event.number}}
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
# If this is a pull request, make sure we check out its head rather than the
# automatically generated merge commit, so that the coverage diff excludes
# unrelated changes in the base branch
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }}
- name: Yarn cache
uses: c-hive/gha-yarn-cache@v2
- name: Run tests with coverage
run: "yarn install && yarn build && yarn coverage"
- name: Upload coverage
uses: codecov/codecov-action@v2
with:
fail_ci_if_error: false
verbose: true
+37
View File
@@ -0,0 +1,37 @@
name: Tests
on:
pull_request: { }
push:
branches: [ develop, master ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
jest:
name: Jest
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Yarn cache
uses: actions/setup-node@v3
with:
cache: 'yarn'
- name: Install dependencies
run: "yarn install"
- name: Build
run: "yarn build"
- name: Run tests with coverage
run: "yarn coverage --ci"
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
name: coverage
path: |
coverage
!coverage/lcov-report
@@ -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 }}
+39 -2
View File
@@ -1,3 +1,40 @@
Changes in [18.0.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v18.0.0-rc.2) (2022-05-20)
============================================================================================================
## 🐛 Bug Fixes
* Catch promise errors in degradable ([\#2388](https://github.com/matrix-org/matrix-js-sdk/pull/2388)). Fixes #2384.
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)
==================================================================================================
## ✨ Features
* Live location sharing: handle encrypted messages in processBeaconEvents ([\#2327](https://github.com/matrix-org/matrix-js-sdk/pull/2327)).
## 🐛 Bug Fixes
* Fix race conditions around threads ([\#2331](https://github.com/matrix-org/matrix-js-sdk/pull/2331)). Fixes vector-im/element-web#21627.
* Ignore m.replace relations on state events, they're invalid ([\#2306](https://github.com/matrix-org/matrix-js-sdk/pull/2306)). Fixes vector-im/element-web#21851.
* fix example in readme ([\#2315](https://github.com/matrix-org/matrix-js-sdk/pull/2315)).
* Don't decrement the length count of a thread when root redacted ([\#2314](https://github.com/matrix-org/matrix-js-sdk/pull/2314)).
* Prevent attempt to create thread with id "undefined" ([\#2308](https://github.com/matrix-org/matrix-js-sdk/pull/2308)).
* Update threads handling for replies-to-thread-responses as per MSC update ([\#2305](https://github.com/matrix-org/matrix-js-sdk/pull/2305)). Fixes vector-im/element-web#19678.
Changes in [17.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v17.1.0) (2022-04-26)
==================================================================================================
## ✨ Features
* Add MatrixClient.doesServerSupportLogoutDevices() for MSC2457 ([\#2297](https://github.com/matrix-org/matrix-js-sdk/pull/2297)).
* Live location sharing - expose room liveBeaconIds ([\#2296](https://github.com/matrix-org/matrix-js-sdk/pull/2296)).
* Support for MSC2457 logout_devices param for setPassword() ([\#2285](https://github.com/matrix-org/matrix-js-sdk/pull/2285)).
* Stabilise token authenticated registration support ([\#2181](https://github.com/matrix-org/matrix-js-sdk/pull/2181)). Contributed by @govynnus.
* Live location sharing - Aggregate beacon locations on beacons ([\#2268](https://github.com/matrix-org/matrix-js-sdk/pull/2268)).
## 🐛 Bug Fixes
* Prevent duplicated re-emitter setups in event-mapper ([\#2293](https://github.com/matrix-org/matrix-js-sdk/pull/2293)).
* Make self membership less prone to races ([\#2277](https://github.com/matrix-org/matrix-js-sdk/pull/2277)). Fixes vector-im/element-web#21661.
Changes in [17.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v17.0.0) (2022-04-11)
==================================================================================================
@@ -670,7 +707,7 @@ Changes in [11.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/ta
BREAKING CHANGES
---
* `MatrixCall` and related APIs have been redesigned to support multiple streams
* `MatrixCall` and related APIs have been redesigned to support multiple streams
(see [\#1660](https://github.com/matrix-org/matrix-js-sdk/pull/1660) for more details)
All changes
@@ -1343,7 +1380,7 @@ BREAKING CHANGES
---
* `RoomState` events changed to use a Map instead of an object, which changes the collection APIs available to access them.
All Changes
---
+12
View File
@@ -243,3 +243,15 @@ on Git 2.17+ you can mass signoff using rebase:
```
git rebase --signoff origin/develop
```
Merge Strategy
==============
The preferred method for merging pull requests is squash merging to keep the
commit history trim, but it is up to the discretion of the team member merging
the change. When stacking pull requests, you may wish to do the following:
1. Branch from develop to your branch (branch1), push commits onto it and open a pull request
2. Branch from your base branch (branch1) to your work branch (branch2), push commits and open a pull request configuring the base to be branch1, saying in the description that it is based on your other PR.
3. Merge the first PR using a merge commit otherwise your stacked PR will need a rebase. Github will automatically adjust the base branch of your other PR to be develop.
+12 -4
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
=====================
@@ -16,7 +24,7 @@ attached to ``window`` through which you can access the SDK. See below for how t
include libolm to enable end-to-end-encryption.
The browser bundle supports recent versions of browsers. Typically this is ES2015
or `> 0.5%, last 2 versions, Firefox ESR, not dead` if using
or `> 0.5%, last 2 versions, Firefox ESR, not dead` if using
[browserlists](https://github.com/browserslist/browserslist).
Please check [the working browser example](examples/browser) for more information.
@@ -26,11 +34,11 @@ In Node.js
Ensure you have the latest LTS version of Node.js installed.
This SDK targets Node 10 for compatibility, which translates to ES6. If you're using
This SDK targets Node 12 for compatibility, which translates to ES6. If you're using
a bundler like webpack you'll likely have to transpile dependencies, including this
SDK, to match your target browsers.
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
if you do not have it already.
``yarn add matrix-js-sdk``
@@ -307,7 +315,7 @@ The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
[libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the
application to make libolm available, via the ``Olm`` global.
It is also necessary to call ``matrixClient.initCrypto()`` after creating a new
It is also necessary to call ``await matrixClient.initCrypto()`` after creating a new
``MatrixClient`` (but **before** calling ``matrixClient.startClient()``) to
initialise the crypto layer.
+2 -2
View File
@@ -341,7 +341,7 @@ function printLine(event) {
var maxNameWidth = 15;
if (name.length > maxNameWidth) {
name = name.substr(0, maxNameWidth-1) + "\u2026";
name = name.slice(0, maxNameWidth-1) + "\u2026";
}
if (event.getType() === "m.room.message") {
@@ -398,7 +398,7 @@ function print(str, formatter) {
function fixWidth(str, len) {
if (str.length > len) {
return str.substr(0, len-2) + "\u2026";
return str.substring(0, len-2) + "\u2026";
}
else if (str.length < len) {
return str + new Array(len - str.length).join(" ");
+13 -4
View File
@@ -1,7 +1,10 @@
{
"name": "matrix-js-sdk",
"version": "17.0.0",
"version": "18.0.0-rc.2",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=12.9.0"
},
"scripts": {
"prepublishOnly": "yarn build",
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
@@ -97,6 +100,7 @@
"fake-indexeddb": "^3.1.2",
"jest": "^26.6.3",
"jest-localstorage-mock": "^2.4.6",
"jest-sonar-reporter": "^2.0.0",
"jsdoc": "^3.6.6",
"matrix-mock-request": "^1.2.3",
"rimraf": "^3.0.2",
@@ -113,9 +117,14 @@
"<rootDir>/src/**/*.{js,ts}"
],
"coverageReporters": [
"text",
"json"
]
"text-summary",
"lcov"
],
"testResultsProcessor": "jest-sonar-reporter"
},
"jestSonar": {
"reportPath": "coverage",
"sonar56x": true
},
"typings": "./lib/index.d.ts"
}
+16
View File
@@ -0,0 +1,16 @@
sonar.projectKey=matrix-js-sdk
sonar.organization=matrix-org
# Encoding of the source code. Default is default system encoding
#sonar.sourceEncoding=UTF-8
sonar.sources=src
sonar.tests=spec
sonar.exclusions=docs,examples,git-hooks
sonar.typescript.tsconfigPath=./tsconfig.json
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.coverage.exclusions=spec/**/*
sonar.testExecutionReportPaths=coverage/test-report.xml
sonar.lang.patterns.ts=**/*.ts,**/*.tsx
+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);
@@ -496,6 +496,7 @@ describe("MatrixClient event timelines", function() {
});
it("should handle thread replies with server support by fetching a contiguous thread timeline", async () => {
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(true);
client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId);
@@ -525,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
};
});
@@ -535,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();
});
});
+183 -2
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";
@@ -797,7 +813,7 @@ describe("MatrixClient", function() {
]);
});
it("sends reply to thread responses to thread timeline only", () => {
it("sends reply to thread responses to main timeline only", () => {
client.clientOpts = { experimentalThreadSupport: true };
const threadRootEvent = buildEventPollStartThreadRoot();
@@ -814,15 +830,180 @@ describe("MatrixClient", function() {
expect(timeline).toEqual([
threadRootEvent,
replyToThreadResponse,
]);
expect(threaded).toEqual([
threadRootEvent,
eventMessageInThread,
replyToThreadResponse,
]);
});
});
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) {
+3 -2
View File
@@ -42,7 +42,6 @@ export const makeBeaconInfoEvent = (
roomId: string,
contentProps: Partial<InfoContentProps> = {},
eventId?: string,
eventTypeSuffix?: string,
): MatrixEvent => {
const {
timeout, isLive, description, assetType,
@@ -51,12 +50,14 @@ export const makeBeaconInfoEvent = (
...contentProps,
};
const event = new MatrixEvent({
type: `${M_BEACON_INFO.name}.${sender}.${eventTypeSuffix || Date.now()}`,
type: M_BEACON_INFO.name,
room_id: roomId,
state_key: sender,
content: makeBeaconInfoContent(timeout, isLive, description, assetType),
});
event.event.origin_server_ts = Date.now();
// live beacons use the beacon_info event id
// set or default this
event.replaceLocalEventId(eventId || `$${Math.random()}-${Math.random()}`);
+75 -7
View File
@@ -16,9 +16,14 @@ limitations under the License.
import { REFERENCE_RELATION } from "matrix-events-sdk";
import { M_BEACON_INFO } from "../../src/@types/beacon";
import { LocationAssetType, M_ASSET, M_LOCATION, M_TIMESTAMP } from "../../src/@types/location";
import { 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()', () => {
@@ -36,11 +41,9 @@ describe('Beacon content helpers', () => {
'nice beacon_info',
LocationAssetType.Pin,
)).toEqual({
[M_BEACON_INFO.name]: {
description: 'nice beacon_info',
timeout: 1234,
live: true,
},
description: 'nice beacon_info',
timeout: 1234,
live: true,
[M_TIMESTAMP.name]: mockDateNow,
[M_ASSET.name]: {
type: LocationAssetType.Pin,
@@ -125,3 +128,68 @@ describe('Beacon content helpers', () => {
});
});
});
describe('Topic content helpers', () => {
describe('makeTopicContent()', () => {
it('creates fully defined event content without html', () => {
expect(makeTopicContent("pizza")).toEqual({
topic: "pizza",
[M_TOPIC.name]: [{
body: "pizza",
mimetype: "text/plain",
}],
});
});
it('creates fully defined event content with html', () => {
expect(makeTopicContent("pizza", "<b>pizza</b>")).toEqual({
topic: "pizza",
[M_TOPIC.name]: [{
body: "pizza",
mimetype: "text/plain",
}, {
body: "<b>pizza</b>",
mimetype: "text/html",
}],
});
});
});
describe('parseTopicContent()', () => {
it('parses event content with plain text topic without mimetype', () => {
expect(parseTopicContent({
topic: "pizza",
[M_TOPIC.name]: [{
body: "pizza",
}],
})).toEqual({
text: "pizza",
});
});
it('parses event content with plain text topic', () => {
expect(parseTopicContent({
topic: "pizza",
[M_TOPIC.name]: [{
body: "pizza",
mimetype: "text/plain",
}],
})).toEqual({
text: "pizza",
});
});
it('parses event content with html topic', () => {
expect(parseTopicContent({
topic: "pizza",
[M_TOPIC.name]: [{
body: "<b>pizza</b>",
mimetype: "text/html",
}],
})).toEqual({
text: "pizza",
html: "<b>pizza</b>",
});
});
});
});
+134
View File
@@ -883,4 +883,138 @@ describe("Cross Signing", function() {
expect(bobTrust3.isCrossSigningVerified()).toBeTruthy();
expect(bobTrust3.isTofu()).toBeTruthy();
});
it(
"should observe that our own device is cross-signed, even if this device doesn't trust the key",
async function() {
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {};
// Generate Alice's SSK etc
const aliceMasterSigning = new global.Olm.PkSigning();
const aliceMasterPrivkey = aliceMasterSigning.generate_seed();
const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey);
const aliceSigning = new global.Olm.PkSigning();
const alicePrivkey = aliceSigning.generate_seed();
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
const aliceSSK = {
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
["ed25519:" + alicePubkey]: alicePubkey,
},
};
const sskSig = aliceMasterSigning.sign(anotherjson.stringify(aliceSSK));
aliceSSK.signatures = {
"@alice:example.com": {
["ed25519:" + aliceMasterPubkey]: sskSig,
},
};
// Alice's device downloads the keys, but doesn't trust them yet
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
keys: {
master: {
user_id: "@alice:example.com",
usage: ["master"],
keys: {
["ed25519:" + aliceMasterPubkey]: aliceMasterPubkey,
},
},
self_signing: aliceSSK,
},
firstUse: 1,
unsigned: {},
});
// Alice has a second device that's cross-signed
const aliceCrossSignedDevice = {
user_id: "@alice:example.com",
device_id: "Dynabook",
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
keys: {
"curve25519:Dynabook": "somePubkey",
"ed25519:Dynabook": "someOtherPubkey",
},
};
const sig = aliceSigning.sign(anotherjson.stringify(aliceCrossSignedDevice));
aliceCrossSignedDevice.signatures = {
"@alice:example.com": {
["ed25519:" + alicePubkey]: sig,
},
};
alice.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
Dynabook: aliceCrossSignedDevice,
});
// We don't trust the cross-signing keys yet...
expect(alice.checkDeviceTrust(aliceCrossSignedDevice.device_id).isCrossSigningVerified()).toBeFalsy();
// ... but we do acknowledge that the device is signed by them
expect(alice.checkIfOwnDeviceCrossSigned(aliceCrossSignedDevice.device_id)).toBeTruthy();
},
);
it("should observe that our own device isn't cross-signed", async function() {
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {};
// Generate Alice's SSK etc
const aliceMasterSigning = new global.Olm.PkSigning();
const aliceMasterPrivkey = aliceMasterSigning.generate_seed();
const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey);
const aliceSigning = new global.Olm.PkSigning();
const alicePrivkey = aliceSigning.generate_seed();
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
const aliceSSK = {
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
["ed25519:" + alicePubkey]: alicePubkey,
},
};
const sskSig = aliceMasterSigning.sign(anotherjson.stringify(aliceSSK));
aliceSSK.signatures = {
"@alice:example.com": {
["ed25519:" + aliceMasterPubkey]: sskSig,
},
};
// Alice's device downloads the keys
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
keys: {
master: {
user_id: "@alice:example.com",
usage: ["master"],
keys: {
["ed25519:" + aliceMasterPubkey]: aliceMasterPubkey,
},
},
self_signing: aliceSSK,
},
firstUse: 1,
unsigned: {},
});
// Alice has a second device that's also not cross-signed
const aliceNotCrossSignedDevice = {
user_id: "@alice:example.com",
device_id: "Dynabook",
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
keys: {
"curve25519:Dynabook": "somePubkey",
"ed25519:Dynabook": "someOtherPubkey",
},
};
alice.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
Dynabook: aliceNotCrossSignedDevice,
});
expect(alice.checkIfOwnDeviceCrossSigned(aliceNotCrossSignedDevice.device_id)).toBeFalsy();
});
});
+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);
});
});
});
+174 -11
View File
@@ -29,10 +29,12 @@ 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();
@@ -89,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 = (
@@ -810,11 +817,12 @@ describe("MatrixClient", function() {
}
},
},
threads: {
get: jest.fn(),
},
getThread: jest.fn(),
addPendingEvent: jest.fn(),
updatePendingEvent: jest.fn(),
reEmitter: {
reEmit: jest.fn(),
},
};
beforeEach(() => {
@@ -990,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);
@@ -999,10 +1047,10 @@ describe("MatrixClient", function() {
});
it("creates new beacon info", async () => {
await client.unstable_createLiveBeacon(roomId, content, '123');
await client.unstable_createLiveBeacon(roomId, content);
// event type combined
const expectedEventType = `${M_BEACON_INFO.name}.${userId}.123`;
const expectedEventType = M_BEACON_INFO.name;
const [callback, method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
expect(callback).toBeFalsy();
expect(method).toBe('PUT');
@@ -1015,17 +1063,132 @@ describe("MatrixClient", function() {
});
it("updates beacon info with specific event type", async () => {
const eventType = `${M_BEACON_INFO.name}.${userId}.456`;
await client.unstable_setLiveBeacon(roomId, eventType, content);
await client.unstable_setLiveBeacon(roomId, content);
// event type combined
const [, , path, , requestContent] = client.http.authedRequest.mock.calls[0];
expect(path).toEqual(
`/rooms/${encodeURIComponent(roomId)}/state/` +
`${encodeURIComponent(eventType)}/${encodeURIComponent(userId)}`,
`${encodeURIComponent(M_BEACON_INFO.name)}/${encodeURIComponent(userId)}`,
);
expect(requestContent).toEqual(content);
});
describe('processBeaconEvents()', () => {
it('does nothing when events is falsy', () => {
const room = new Room(roomId, client, userId);
const roomStateProcessSpy = jest.spyOn(room.currentState, 'processBeaconEvents');
client.processBeaconEvents(room, undefined);
expect(roomStateProcessSpy).not.toHaveBeenCalled();
});
it('does nothing when events is of length 0', () => {
const room = new Room(roomId, client, userId);
const roomStateProcessSpy = jest.spyOn(room.currentState, 'processBeaconEvents');
client.processBeaconEvents(room, []);
expect(roomStateProcessSpy).not.toHaveBeenCalled();
});
it('calls room states processBeaconEvents with events', () => {
const room = new Room(roomId, client, userId);
const roomStateProcessSpy = jest.spyOn(room.currentState, 'processBeaconEvents');
const messageEvent = testUtils.mkMessage({ room: roomId, user: userId, event: true });
const beaconEvent = makeBeaconEvent(userId);
client.processBeaconEvents(room, [messageEvent, beaconEvent]);
expect(roomStateProcessSpy).toHaveBeenCalledWith([messageEvent, beaconEvent], client);
});
});
});
describe("setRoomTopic", () => {
const roomId = "!foofoofoofoofoofoo:matrix.org";
const createSendStateEventMock = (topic: string, htmlTopic?: string) => {
return jest.fn()
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
expect(roomId).toEqual(roomId);
expect(eventType).toEqual(EventType.RoomTopic);
expect(content).toMatchObject(ContentHelpers.makeTopicContent(topic, htmlTopic));
expect(stateKey).toBeUndefined();
return Promise.resolve();
});
};
it("is called with plain text topic and sends state event", async () => {
const sendStateEvent = createSendStateEventMock("pizza");
client.sendStateEvent = sendStateEvent;
await client.setRoomTopic(roomId, "pizza");
expect(sendStateEvent).toHaveBeenCalledTimes(1);
});
it("is called with plain text topic and callback and sends state event", async () => {
const sendStateEvent = createSendStateEventMock("pizza");
client.sendStateEvent = sendStateEvent;
await client.setRoomTopic(roomId, "pizza", () => {});
expect(sendStateEvent).toHaveBeenCalledTimes(1);
});
it("is called with plain text and HTML topic and sends state event", async () => {
const sendStateEvent = createSendStateEventMock("pizza", "<b>pizza</b>");
client.sendStateEvent = sendStateEvent;
await client.setRoomTopic(roomId, "pizza", "<b>pizza</b>");
expect(sendStateEvent).toHaveBeenCalledTimes(1);
});
});
describe("setPassword", () => {
const auth = { session: 'abcdef', type: 'foo' };
const newPassword = 'newpassword';
const callback = () => {};
const passwordTest = (expectedRequestContent: any, expectedCallback?: Function) => {
const [callback, method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
if (expectedCallback) {
expect(callback).toBe(expectedCallback);
} else {
expect(callback).toBeFalsy();
}
expect(method).toBe('POST');
expect(path).toEqual('/account/password');
expect(queryParams).toBeFalsy();
expect(requestContent).toEqual(expectedRequestContent);
};
beforeEach(() => {
client.http.authedRequest.mockClear().mockResolvedValue({});
});
it("no logout_devices specified", async () => {
await client.setPassword(auth, newPassword);
passwordTest({ auth, new_password: newPassword });
});
it("no logout_devices specified + callback", async () => {
await client.setPassword(auth, newPassword, callback);
passwordTest({ auth, new_password: newPassword }, callback);
});
it("overload logoutDevices=true", async () => {
await client.setPassword(auth, newPassword, true);
passwordTest({ auth, new_password: newPassword, logout_devices: true });
});
it("overload logoutDevices=true + callback", async () => {
await client.setPassword(auth, newPassword, true, callback);
passwordTest({ auth, new_password: newPassword, logout_devices: true }, callback);
});
it("overload logoutDevices=false", async () => {
await client.setPassword(auth, newPassword, false);
passwordTest({ auth, new_password: newPassword, logout_devices: false });
});
it("overload logoutDevices=false + callback", async () => {
await client.setPassword(auth, newPassword, false, callback);
passwordTest({ auth, new_password: newPassword, logout_devices: false }, callback);
});
});
});
+123 -30
View File
@@ -14,15 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { EventType } from "../../../src";
import { M_BEACON_INFO } from "../../../src/@types/beacon";
import {
isTimestampInDuration,
isBeaconInfoEventType,
Beacon,
BeaconEvent,
} from "../../../src/models/beacon";
import { makeBeaconInfoEvent } from "../../test-utils/beacon";
import { makeBeaconEvent, makeBeaconInfoEvent } from "../../test-utils/beacon";
jest.useFakeTimers();
@@ -57,27 +54,9 @@ describe('Beacon', () => {
});
});
describe('isBeaconInfoEventType', () => {
it.each([
EventType.CallAnswer,
`prefix.${M_BEACON_INFO.name}`,
`prefix.${M_BEACON_INFO.altName}`,
])('returns false for %s', (type) => {
expect(isBeaconInfoEventType(type)).toBe(false);
});
it.each([
M_BEACON_INFO.name,
M_BEACON_INFO.altName,
`${M_BEACON_INFO.name}.@test:server.org.12345`,
`${M_BEACON_INFO.altName}.@test:server.org.12345`,
])('returns true for %s', (type) => {
expect(isBeaconInfoEventType(type)).toBe(true);
});
});
describe('Beacon', () => {
const userId = '@user:server.org';
const userId2 = '@user2:server.org';
const roomId = '$room:server.org';
// 14.03.2022 16:15
const now = 1647270879403;
@@ -88,6 +67,7 @@ describe('Beacon', () => {
// without timeout of 3 hours
let liveBeaconEvent;
let notLiveBeaconEvent;
let user2BeaconEvent;
const advanceDateAndTime = (ms: number) => {
// bc liveness check uses Date.now we have to advance this mock
@@ -107,14 +87,21 @@ describe('Beacon', () => {
isLive: true,
},
'$live123',
'$live123',
);
notLiveBeaconEvent = makeBeaconInfoEvent(
userId,
roomId,
{ timeout: HOUR_MS * 3, isLive: false },
'$dead123',
'$dead123',
);
user2BeaconEvent = makeBeaconInfoEvent(
userId2,
roomId,
{
timeout: HOUR_MS * 3,
isLive: true,
},
'$user2live123',
);
// back to now
@@ -133,7 +120,7 @@ describe('Beacon', () => {
expect(beacon.isLive).toEqual(true);
expect(beacon.beaconInfoOwner).toEqual(userId);
expect(beacon.beaconInfoEventType).toEqual(liveBeaconEvent.getType());
expect(beacon.identifier).toEqual(liveBeaconEvent.getType());
expect(beacon.identifier).toEqual(`${roomId}_${userId}`);
expect(beacon.beaconInfo).toBeTruthy();
});
@@ -171,8 +158,27 @@ describe('Beacon', () => {
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
expect(() => beacon.update(notLiveBeaconEvent)).toThrow();
expect(beacon.isLive).toEqual(true);
expect(() => beacon.update(user2BeaconEvent)).toThrow();
// didnt update
expect(beacon.identifier).toEqual(`${roomId}_${userId}`);
});
it('does not update with an older event', () => {
const beacon = new Beacon(liveBeaconEvent);
const emitSpy = jest.spyOn(beacon, 'emit').mockClear();
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
const oldUpdateEvent = makeBeaconInfoEvent(
userId,
roomId,
);
// less than the original event
oldUpdateEvent.event.origin_server_ts = liveBeaconEvent.event.origin_server_ts - 1000;
beacon.update(oldUpdateEvent);
// didnt update
expect(emitSpy).not.toHaveBeenCalled();
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
});
it('updates event', () => {
@@ -182,7 +188,7 @@ describe('Beacon', () => {
expect(beacon.isLive).toEqual(true);
const updatedBeaconEvent = makeBeaconInfoEvent(
userId, roomId, { timeout: HOUR_MS * 3, isLive: false }, '$live123', '$live123');
userId, roomId, { timeout: HOUR_MS * 3, isLive: false }, '$live123');
beacon.update(updatedBeaconEvent);
expect(beacon.isLive).toEqual(false);
@@ -200,7 +206,6 @@ describe('Beacon', () => {
roomId,
{ timeout: HOUR_MS * 3, isLive: false },
beacon.beaconInfoId,
'$live123',
);
beacon.update(updatedBeaconEvent);
@@ -277,5 +282,93 @@ describe('Beacon', () => {
expect(emitSpy).toHaveBeenCalledTimes(1);
});
});
describe('addLocations', () => {
it('ignores locations when beacon is not live', () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: false }));
const emitSpy = jest.spyOn(beacon, 'emit');
beacon.addLocations([
makeBeaconEvent(userId, { beaconInfoId: beacon.beaconInfoId, timestamp: now + 1 }),
]);
expect(beacon.latestLocationState).toBeFalsy();
expect(emitSpy).not.toHaveBeenCalled();
});
it('ignores locations outside the beacon live duration', () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
const emitSpy = jest.spyOn(beacon, 'emit');
beacon.addLocations([
// beacon has now + 60000 live period
makeBeaconEvent(userId, { beaconInfoId: beacon.beaconInfoId, timestamp: now + 100000 }),
]);
expect(beacon.latestLocationState).toBeFalsy();
expect(emitSpy).not.toHaveBeenCalled();
});
it('sets latest location state to most recent location', () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
const emitSpy = jest.spyOn(beacon, 'emit');
const locations = [
// older
makeBeaconEvent(
userId, { beaconInfoId: beacon.beaconInfoId, uri: 'geo:foo', timestamp: now + 1 },
),
// newer
makeBeaconEvent(
userId, { beaconInfoId: beacon.beaconInfoId, uri: 'geo:bar', timestamp: now + 10000 },
),
// not valid
makeBeaconEvent(
userId, { beaconInfoId: beacon.beaconInfoId, uri: 'geo:baz', timestamp: now - 5 },
),
];
beacon.addLocations(locations);
const expectedLatestLocation = {
description: undefined,
timestamp: now + 10000,
uri: 'geo:bar',
};
// the newest valid location
expect(beacon.latestLocationState).toEqual(expectedLatestLocation);
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LocationUpdate, expectedLatestLocation);
});
it('ignores locations that are less recent that the current latest location', () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
const olderLocation = makeBeaconEvent(
userId, { beaconInfoId: beacon.beaconInfoId, uri: 'geo:foo', timestamp: now + 1 },
);
const newerLocation = makeBeaconEvent(
userId, { beaconInfoId: beacon.beaconInfoId, uri: 'geo:bar', timestamp: now + 10000 },
);
beacon.addLocations([newerLocation]);
// latest location set to newerLocation
expect(beacon.latestLocationState).toEqual(expect.objectContaining({
uri: 'geo:bar',
}));
const emitSpy = jest.spyOn(beacon, 'emit').mockClear();
// add older location
beacon.addLocations([olderLocation]);
// no change
expect(beacon.latestLocationState).toEqual(expect.objectContaining({
uri: 'geo:bar',
}));
// no emit
expect(emitSpy).not.toHaveBeenCalled();
});
});
});
});
+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() {
+45
View File
@@ -130,4 +130,49 @@ describe("Relations", function() {
await relationsCreated;
}
});
it("should ignore m.replace for state events", async () => {
const userId = "@bob:example.com";
const room = new Room("room123", null, userId);
const relations = new Relations("m.replace", "m.room.topic", room);
// Create an instance of a state event with rel_type m.replace
const originalTopic = new MatrixEvent({
"sender": userId,
"type": "m.room.topic",
"event_id": "$orig",
"room_id": room.roomId,
"content": {
"topic": "orig",
},
"state_key": "",
});
const badlyEditedTopic = new MatrixEvent({
"sender": userId,
"type": "m.room.topic",
"event_id": "$orig",
"room_id": room.roomId,
"content": {
"topic": "topic",
"m.new_content": {
"topic": "edit",
},
"m.relates_to": {
"event_id": "$orig",
"rel_type": "m.replace",
},
},
"state_key": "",
});
await relations.setTargetEvent(originalTopic);
expect(originalTopic.replacingEvent()).toBe(null);
expect(originalTopic.getContent().topic).toBe("orig");
await relations.addEvent(badlyEditedTopic);
expect(originalTopic.replacingEvent()).toBe(null);
expect(originalTopic.getContent().topic).toBe("orig");
expect(badlyEditedTopic.replacingEvent()).toBe(null);
expect(badlyEditedTopic.getContent().topic).toBe("topic");
});
});
+262 -17
View File
@@ -1,8 +1,14 @@
import * as utils from "../test-utils/test-utils";
import { makeBeaconInfoEvent } from "../test-utils/beacon";
import { makeBeaconEvent, makeBeaconInfoEvent } from "../test-utils/beacon";
import { filterEmitCallsByEventType } from "../test-utils/emitter";
import { RoomState, RoomStateEvent } from "../../src/models/room-state";
import { BeaconEvent } from "../../src/models/beacon";
import { BeaconEvent, getBeaconInfoIdentifier } from "../../src/models/beacon";
import { EventType, RelationType } from "../../src/@types/event";
import {
MatrixEvent,
MatrixEventEvent,
} from "../../src/models/event";
import { M_BEACON } from "../../src/@types/beacon";
describe("RoomState", function() {
const roomId = "!foo:bar";
@@ -260,7 +266,7 @@ describe("RoomState", function() {
state.setStateEvents([beaconEvent]);
expect(state.beacons.size).toEqual(1);
const beaconInstance = state.beacons.get(beaconEvent.getType());
const beaconInstance = state.beacons.get(`${roomId}_${userA}`);
expect(beaconInstance).toBeTruthy();
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.New, beaconEvent, beaconInstance);
});
@@ -275,49 +281,49 @@ describe("RoomState", function() {
// no beacon added
expect(state.beacons.size).toEqual(0);
expect(state.beacons.get(redactedBeaconEvent.getType)).toBeFalsy();
expect(state.beacons.get(getBeaconInfoIdentifier(redactedBeaconEvent))).toBeFalsy();
// no new beacon emit
expect(filterEmitCallsByEventType(BeaconEvent.New, emitSpy).length).toBeFalsy();
});
it('updates existing beacon info events in state', () => {
const beaconId = '$beacon1';
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId, beaconId);
const updatedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, beaconId, beaconId);
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
const updatedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, beaconId);
state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(beaconEvent.getType());
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
expect(beaconInstance.isLive).toEqual(true);
state.setStateEvents([updatedBeaconEvent]);
// same Beacon
expect(state.beacons.get(beaconEvent.getType())).toBe(beaconInstance);
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(beaconInstance);
// updated liveness
expect(state.beacons.get(beaconEvent.getType()).isLive).toEqual(false);
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent)).isLive).toEqual(false);
});
it('destroys and removes redacted beacon events', () => {
const beaconId = '$beacon1';
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId, beaconId);
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId, beaconId);
const redactionEvent = { event: { type: 'm.room.redaction' } };
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
const redactionEvent = { event: { type: 'm.room.redaction', redacts: beaconEvent.getId() } };
redactedBeaconEvent.makeRedacted(redactionEvent);
state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(beaconEvent.getType());
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
const destroySpy = jest.spyOn(beaconInstance, 'destroy');
expect(beaconInstance.isLive).toEqual(true);
state.setStateEvents([redactedBeaconEvent]);
expect(destroySpy).toHaveBeenCalled();
expect(state.beacons.get(beaconEvent.getType())).toBe(undefined);
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(undefined);
});
it('updates live beacon ids once after setting state events', () => {
const liveBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, '$beacon1', '$beacon1');
const deadBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, '$beacon2', '$beacon2');
const liveBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, '$beacon1');
const deadBeaconEvent = makeBeaconInfoEvent(userB, roomId, { isLive: false }, '$beacon2');
const emitSpy = jest.spyOn(state, 'emit');
@@ -334,7 +340,7 @@ describe("RoomState", function() {
state.setStateEvents([updatedLiveBeaconEvent]);
expect(state.hasLiveBeacons).toBe(false);
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(2);
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(3);
expect(emitSpy).toHaveBeenCalledWith(RoomStateEvent.BeaconLiveness, state, false);
});
});
@@ -712,4 +718,243 @@ describe("RoomState", function() {
expect(state.maySendEvent('m.room.other_thing', userB)).toEqual(false);
});
});
describe('processBeaconEvents', () => {
const beacon1 = makeBeaconInfoEvent(userA, roomId, {}, '$beacon1', '$beacon1');
const beacon2 = makeBeaconInfoEvent(userB, roomId, {}, '$beacon2', '$beacon2');
const mockClient = { decryptEventIfNeeded: jest.fn() };
beforeEach(() => {
mockClient.decryptEventIfNeeded.mockClear();
});
it('does nothing when state has no beacons', () => {
const emitSpy = jest.spyOn(state, 'emit');
state.processBeaconEvents([makeBeaconEvent(userA, { beaconInfoId: '$beacon1' })], mockClient);
expect(emitSpy).not.toHaveBeenCalled();
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
});
it('does nothing when there are no events', () => {
state.setStateEvents([beacon1, beacon2]);
const emitSpy = jest.spyOn(state, 'emit').mockClear();
state.processBeaconEvents([], mockClient);
expect(emitSpy).not.toHaveBeenCalled();
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
});
describe('without encryption', () => {
it('discards events for beacons that are not in state', () => {
const location = makeBeaconEvent(userA, {
beaconInfoId: 'some-other-beacon',
});
const otherRelatedEvent = new MatrixEvent({
sender: userA,
type: EventType.RoomMessage,
content: {
['m.relates_to']: {
event_id: 'whatever',
},
},
});
state.setStateEvents([beacon1, beacon2]);
const emitSpy = jest.spyOn(state, 'emit').mockClear();
state.processBeaconEvents([location, otherRelatedEvent], mockClient);
expect(emitSpy).not.toHaveBeenCalled();
});
it('discards events that are not beacon type', () => {
// related to beacon1
const otherRelatedEvent = new MatrixEvent({
sender: userA,
type: EventType.RoomMessage,
content: {
['m.relates_to']: {
rel_type: RelationType.Reference,
event_id: beacon1.getId(),
},
},
});
state.setStateEvents([beacon1, beacon2]);
const emitSpy = jest.spyOn(state, 'emit').mockClear();
state.processBeaconEvents([otherRelatedEvent], mockClient);
expect(emitSpy).not.toHaveBeenCalled();
});
it('adds locations to beacons', () => {
const location1 = makeBeaconEvent(userA, {
beaconInfoId: '$beacon1', timestamp: Date.now() + 1,
});
const location2 = makeBeaconEvent(userA, {
beaconInfoId: '$beacon1', timestamp: Date.now() + 2,
});
const location3 = makeBeaconEvent(userB, {
beaconInfoId: 'some-other-beacon',
});
state.setStateEvents([beacon1, beacon2], mockClient);
expect(state.beacons.size).toEqual(2);
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beacon1));
const addLocationsSpy = jest.spyOn(beaconInstance, 'addLocations');
state.processBeaconEvents([location1, location2, location3], mockClient);
expect(addLocationsSpy).toHaveBeenCalledTimes(2);
// only called with locations for beacon1
expect(addLocationsSpy).toHaveBeenCalledWith([location1]);
expect(addLocationsSpy).toHaveBeenCalledWith([location2]);
});
});
describe('with encryption', () => {
const beacon1RelationContent = { ['m.relates_to']: {
rel_type: RelationType.Reference,
event_id: beacon1.getId(),
} };
const relatedEncryptedEvent = new MatrixEvent({
sender: userA,
type: EventType.RoomMessageEncrypted,
content: beacon1RelationContent,
});
const decryptingRelatedEvent = new MatrixEvent({
sender: userA,
type: EventType.RoomMessageEncrypted,
content: beacon1RelationContent,
});
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
const failedDecryptionRelatedEvent = new MatrixEvent({
sender: userA,
type: EventType.RoomMessageEncrypted,
content: beacon1RelationContent,
});
jest.spyOn(failedDecryptionRelatedEvent, 'isDecryptionFailure').mockReturnValue(true);
it('discards events without relations', () => {
const unrelatedEvent = new MatrixEvent({
sender: userA,
type: EventType.RoomMessageEncrypted,
});
state.setStateEvents([beacon1, beacon2]);
const emitSpy = jest.spyOn(state, 'emit').mockClear();
state.processBeaconEvents([unrelatedEvent], mockClient);
expect(emitSpy).not.toHaveBeenCalled();
// discard unrelated events early
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
});
it('discards events for beacons that are not in state', () => {
const location = makeBeaconEvent(userA, {
beaconInfoId: 'some-other-beacon',
});
const otherRelatedEvent = new MatrixEvent({
sender: userA,
type: EventType.RoomMessageEncrypted,
content: {
['m.relates_to']: {
rel_type: RelationType.Reference,
event_id: 'whatever',
},
},
});
state.setStateEvents([beacon1, beacon2]);
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
state.processBeaconEvents([location, otherRelatedEvent], mockClient);
expect(addLocationsSpy).not.toHaveBeenCalled();
// discard unrelated events early
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
});
it('decrypts related events if needed', () => {
const location = makeBeaconEvent(userA, {
beaconInfoId: beacon1.getId(),
});
state.setStateEvents([beacon1, beacon2]);
state.processBeaconEvents([location, relatedEncryptedEvent], mockClient);
// discard unrelated events early
expect(mockClient.decryptEventIfNeeded).toHaveBeenCalledTimes(2);
});
it('listens for decryption on events that are being decrypted', () => {
const decryptingRelatedEvent = new MatrixEvent({
sender: userA,
type: EventType.RoomMessageEncrypted,
content: beacon1RelationContent,
});
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
// spy on event.once
const eventOnceSpy = jest.spyOn(decryptingRelatedEvent, 'once');
state.setStateEvents([beacon1, beacon2]);
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
// listener was added
expect(eventOnceSpy).toHaveBeenCalled();
});
it('listens for decryption on events that have decryption failure', () => {
const failedDecryptionRelatedEvent = new MatrixEvent({
sender: userA,
type: EventType.RoomMessageEncrypted,
content: beacon1RelationContent,
});
jest.spyOn(failedDecryptionRelatedEvent, 'isDecryptionFailure').mockReturnValue(true);
// spy on event.once
const eventOnceSpy = jest.spyOn(decryptingRelatedEvent, 'once');
state.setStateEvents([beacon1, beacon2]);
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
// listener was added
expect(eventOnceSpy).toHaveBeenCalled();
});
it('discard events that are not m.beacon type after decryption', () => {
const decryptingRelatedEvent = new MatrixEvent({
sender: userA,
type: EventType.RoomMessageEncrypted,
content: beacon1RelationContent,
});
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
state.setStateEvents([beacon1, beacon2]);
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
// this event is a message after decryption
decryptingRelatedEvent.type = EventType.RoomMessage;
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted);
expect(addLocationsSpy).not.toHaveBeenCalled();
});
it('adds locations to beacons after decryption', () => {
const decryptingRelatedEvent = new MatrixEvent({
sender: userA,
type: EventType.RoomMessageEncrypted,
content: beacon1RelationContent,
});
const locationEvent = makeBeaconEvent(userA, {
beaconInfoId: '$beacon1', timestamp: Date.now() + 1,
});
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
state.setStateEvents([beacon1, beacon2]);
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
// update type after '''decryption'''
decryptingRelatedEvent.event.type = M_BEACON.name;
decryptingRelatedEvent.event.content = locationEvent.content;
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted);
expect(addLocationsSpy).toHaveBeenCalledWith([decryptingRelatedEvent]);
});
});
});
});
+157 -43
View File
@@ -23,6 +23,7 @@ import * as utils from "../test-utils/test-utils";
import {
DuplicateStrategy,
EventStatus,
EventTimelineSet,
EventType,
JoinRule,
MatrixEvent,
@@ -31,12 +32,13 @@ 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 { ThreadEvent } from "../../src/models/thread";
import { ReceiptType } from "../../src/@types/read_receipts";
import { Thread, ThreadEvent } from "../../src/models/thread";
describe("Room", function() {
const roomId = "!foo:bar";
@@ -1471,16 +1473,13 @@ describe("Room", function() {
isRoomEncrypted: function() {
return false;
},
http: {
serverResponse,
authedRequest: function() {
if (this.serverResponse instanceof Error) {
return Promise.reject(this.serverResponse);
} else {
return Promise.resolve({ chunk: this.serverResponse });
}
},
},
members: jest.fn().mockImplementation(() => {
if (serverResponse instanceof Error) {
return Promise.reject(serverResponse);
} else {
return Promise.resolve({ chunk: serverResponse });
}
}),
store: {
storageResponse,
storedMembers: null,
@@ -1547,7 +1546,7 @@ describe("Room", function() {
}
expect(hasThrown).toEqual(true);
client.http.serverResponse = [memberEvent];
client.members.mockReturnValue({ chunk: [memberEvent] });
await room.loadMembersIfNeeded();
const memberA = room.getMember("@user_a:bar");
expect(memberA.name).toEqual("User A");
@@ -1917,7 +1916,7 @@ describe("Room", function() {
},
});
room.createThread(undefined, [eventWithoutARootEvent]);
room.createThread("$000", undefined, [eventWithoutARootEvent]);
const rootEvent = new MatrixEvent({
event_id: "$666",
@@ -1935,7 +1934,16 @@ describe("Room", function() {
},
});
expect(() => room.createThread(rootEvent, [])).not.toThrow();
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 () => {
@@ -1962,14 +1970,16 @@ describe("Room", function() {
},
});
let prom = emitPromise(room, ThreadEvent.New);
room.addLiveEvents([randomMessage, threadRoot, threadResponse]);
const thread = await emitPromise(room, ThreadEvent.New);
const thread = await prom;
expect(thread.replyToEvent).toBe(threadResponse);
expect(thread.replyToEvent.getContent().body).toBe(threadResponse.getContent().body);
prom = emitPromise(thread, ThreadEvent.Update);
room.addLiveEvents([threadResponseEdit]);
await emitPromise(thread, ThreadEvent.Update);
await prom;
expect(thread.replyToEvent.getContent().body).toBe(threadResponseEdit.getContent()["m.new_content"].body);
});
@@ -1996,15 +2006,17 @@ describe("Room", function() {
},
});
let prom = emitPromise(room, ThreadEvent.New);
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]);
const thread = await emitPromise(room, ThreadEvent.New);
const thread = await prom;
expect(thread).toHaveLength(2);
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
prom = emitPromise(thread, ThreadEvent.Update);
const threadResponse1Redaction = mkRedaction(threadResponse1);
room.addLiveEvents([threadResponse1Redaction]);
await emitPromise(thread, ThreadEvent.Update);
await prom;
expect(thread).toHaveLength(1);
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
});
@@ -2033,19 +2045,57 @@ describe("Room", function() {
},
});
const prom = emitPromise(room, ThreadEvent.New);
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
const thread = await emitPromise(room, ThreadEvent.New);
const thread = await prom;
expect(thread).toHaveLength(2);
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
const threadResponse2ReactionRedaction = mkRedaction(threadResponse2Reaction);
room.addLiveEvents([threadResponse2ReactionRedaction]);
await emitPromise(thread, ThreadEvent.Update);
expect(thread).toHaveLength(2);
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
});
it("should not decrement the length when the thread root is redacted", async () => {
room.client.supportsExperimentalThreads = () => true;
const threadRoot = mkMessage();
const threadResponse1 = mkThreadResponse(threadRoot);
threadResponse1.localTimestamp += 1000;
const threadResponse2 = mkThreadResponse(threadRoot);
threadResponse2.localTimestamp += 2000;
const threadResponse2Reaction = mkReaction(threadResponse2);
room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({
...threadRoot.event,
unsigned: {
"age": 123,
"m.relations": {
"m.thread": {
latest_event: threadResponse2.event,
count: 2,
current_user_participated: true,
},
},
},
});
let 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(room, ThreadEvent.Update);
const threadRootRedaction = mkRedaction(threadRoot);
room.addLiveEvents([threadRootRedaction]);
await prom;
expect(thread).toHaveLength(2);
});
it("Redacting the lastEvent finds a new lastEvent", async () => {
room.client.supportsExperimentalThreads = () => true;
@@ -2069,21 +2119,24 @@ describe("Room", function() {
},
});
let prom = emitPromise(room, ThreadEvent.New);
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]);
const thread = await emitPromise(room, ThreadEvent.New);
const thread = await prom;
expect(thread).toHaveLength(2);
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
prom = emitPromise(room, ThreadEvent.Update);
const threadResponse2Redaction = mkRedaction(threadResponse2);
room.addLiveEvents([threadResponse2Redaction]);
await emitPromise(thread, ThreadEvent.Update);
await prom;
expect(thread).toHaveLength(1);
expect(thread.replyToEvent.getId()).toBe(threadResponse1.getId());
prom = emitPromise(room, ThreadEvent.Update);
const threadResponse1Redaction = mkRedaction(threadResponse1);
room.addLiveEvents([threadResponse1Redaction]);
await emitPromise(thread, ThreadEvent.Update);
await prom;
expect(thread).toHaveLength(0);
expect(thread.replyToEvent.getId()).toBe(threadRoot.getId());
});
@@ -2154,36 +2207,32 @@ describe("Room", function() {
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).threadId).toBe(threadRoot.getId());
});
it("reply to thread response and its relations&redactions should be only in thread timeline", () => {
it("reply to thread response and its relations&redactions should be only in main timeline", () => {
const threadRoot = mkMessage();
const threadResponse1 = mkThreadResponse(threadRoot);
const reply1 = mkReply(threadResponse1);
const threadReaction1 = mkReaction(reply1);
const threadReaction2 = mkReaction(reply1);
const threadReaction2Redaction = mkRedaction(reply1);
const reaction1 = mkReaction(reply1);
const reaction2 = mkReaction(reply1);
const reaction2Redaction = mkRedaction(reply1);
const roots = new Set([threadRoot.getId()]);
const events = [
threadRoot,
threadResponse1,
reply1,
threadReaction1,
threadReaction2,
threadReaction2Redaction,
reaction1,
reaction2,
reaction2Redaction,
];
expect(room.eventShouldLiveIn(reply1, events, roots).shouldLiveInRoom).toBeFalsy();
expect(room.eventShouldLiveIn(reply1, events, roots).shouldLiveInThread).toBeTruthy();
expect(room.eventShouldLiveIn(reply1, events, roots).threadId).toBe(threadRoot.getId());
expect(room.eventShouldLiveIn(threadReaction1, events, roots).shouldLiveInRoom).toBeFalsy();
expect(room.eventShouldLiveIn(threadReaction1, events, roots).shouldLiveInThread).toBeTruthy();
expect(room.eventShouldLiveIn(threadReaction1, events, roots).threadId).toBe(threadRoot.getId());
expect(room.eventShouldLiveIn(threadReaction2, events, roots).shouldLiveInRoom).toBeFalsy();
expect(room.eventShouldLiveIn(threadReaction2, events, roots).shouldLiveInThread).toBeTruthy();
expect(room.eventShouldLiveIn(threadReaction2, events, roots).threadId).toBe(threadRoot.getId());
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).shouldLiveInRoom).toBeFalsy();
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).shouldLiveInThread).toBeTruthy();
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).threadId).toBe(threadRoot.getId());
expect(room.eventShouldLiveIn(reply1, events, roots).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(reply1, events, roots).shouldLiveInThread).toBeFalsy();
expect(room.eventShouldLiveIn(reaction1, events, roots).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(reaction1, events, roots).shouldLiveInThread).toBeFalsy();
expect(room.eventShouldLiveIn(reaction2, events, roots).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(reaction2, events, roots).shouldLiveInThread).toBeFalsy();
expect(room.eventShouldLiveIn(reaction2Redaction, events, roots).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(reaction2Redaction, events, roots).shouldLiveInThread).toBeFalsy();
});
it("reply to reply to thread root should only be in the main timeline", () => {
@@ -2205,5 +2254,70 @@ describe("Room", function() {
expect(room.eventShouldLiveIn(reply2, events, roots).shouldLiveInRoom).toBeTruthy();
expect(room.eventShouldLiveIn(reply2, events, roots).shouldLiveInThread).toBeFalsy();
});
it("should aggregate relations in thread event timeline set", () => {
Thread.setServerSideSupport(true, true);
const threadRoot = mkMessage();
const rootReaction = mkReaction(threadRoot);
const threadResponse = mkThreadResponse(threadRoot);
const threadReaction = mkReaction(threadResponse);
const events = [
threadRoot,
rootReaction,
threadResponse,
threadReaction,
];
room.addLiveEvents(events);
const thread = threadRoot.getThread();
expect(thread.rootEvent).toBe(threadRoot);
const rootRelations = thread.timelineSet.getRelationsForEvent(
threadRoot.getId(),
RelationType.Annotation,
EventType.Reaction,
).getSortedAnnotationsByKey();
expect(rootRelations).toHaveLength(1);
expect(rootRelations[0][0]).toEqual(rootReaction.getRelation().key);
expect(rootRelations[0][1].size).toEqual(1);
expect(rootRelations[0][1].has(rootReaction)).toBeTruthy();
const responseRelations = thread.timelineSet.getRelationsForEvent(
threadResponse.getId(),
RelationType.Annotation,
EventType.Reaction,
).getSortedAnnotationsByKey();
expect(responseRelations).toHaveLength(1);
expect(responseRelations[0][0]).toEqual(threadReaction.getRelation().key);
expect(responseRelations[0][1].size).toEqual(1);
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 },
},
+29
View File
@@ -10,8 +10,11 @@ import {
prevString,
simpleRetryOperation,
stringToBase,
sortEventsByLatestContentTimestamp,
} from "../../src/utils";
import { logger } from "../../src/logger";
import { mkMessage } from "../test-utils/test-utils";
import { makeBeaconEvent } from "../test-utils/beacon";
// TODO: Fix types throughout
@@ -506,4 +509,30 @@ describe("utils", function() {
});
});
});
describe('sortEventsByLatestContentTimestamp', () => {
const roomId = '!room:server';
const userId = '@user:server';
const eventWithoutContentTimestamp = mkMessage({ room: roomId, user: userId, event: true });
// m.beacon events have timestamp in content
const beaconEvent1 = makeBeaconEvent(userId, { timestamp: 1648804528557 });
const beaconEvent2 = makeBeaconEvent(userId, { timestamp: 1648804528558 });
const beaconEvent3 = makeBeaconEvent(userId, { timestamp: 1648804528000 });
const beaconEvent4 = makeBeaconEvent(userId, { timestamp: 0 });
it('sorts events with timestamps as later than events without', () => {
expect(
[beaconEvent4, eventWithoutContentTimestamp, beaconEvent1]
.sort(utils.sortEventsByLatestContentTimestamp),
).toEqual([
beaconEvent1, beaconEvent4, eventWithoutContentTimestamp,
]);
});
it('sorts by content timestamps correctly', () => {
expect(
[beaconEvent1, beaconEvent2, beaconEvent3].sort(sortEventsByLatestContentTimestamp),
).toEqual([beaconEvent2, beaconEvent1, beaconEvent3]);
});
});
});
+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();
});
});
+9 -19
View File
@@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { EitherAnd, RELATES_TO_RELATIONSHIP, REFERENCE_RELATION } from "matrix-events-sdk";
import { RELATES_TO_RELATIONSHIP, REFERENCE_RELATION } from "matrix-events-sdk";
import { UnstableValue } from "../NamespacedValue";
import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location";
/**
* Beacon info and beacon event types as described in MSC3489
* https://github.com/matrix-org/matrix-spec-proposals/pull/3489
* Beacon info and beacon event types as described in MSC3672
* https://github.com/matrix-org/matrix-spec-proposals/pull/3672
*/
/**
@@ -60,16 +60,11 @@ import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location";
* }
*/
/**
* Variable event type for m.beacon_info
*/
export const M_BEACON_INFO_VARIABLE = new UnstableValue("m.beacon_info.*", "org.matrix.msc3489.beacon_info.*");
/**
* Non-variable type for m.beacon_info event content
*/
export const M_BEACON_INFO = new UnstableValue("m.beacon_info", "org.matrix.msc3489.beacon_info");
export const M_BEACON = new UnstableValue("m.beacon", "org.matrix.msc3489.beacon");
export const M_BEACON_INFO = new UnstableValue("m.beacon_info", "org.matrix.msc3672.beacon_info");
export const M_BEACON = new UnstableValue("m.beacon", "org.matrix.msc3672.beacon");
export type MBeaconInfoContent = {
description?: string;
@@ -80,16 +75,11 @@ export type MBeaconInfoContent = {
live?: boolean;
};
export type MBeaconInfoEvent = EitherAnd<
{ [M_BEACON_INFO.name]: MBeaconInfoContent },
{ [M_BEACON_INFO.altName]: MBeaconInfoContent }
>;
/**
* m.beacon_info Event example from the spec
* https://github.com/matrix-org/matrix-spec-proposals/pull/3489
* https://github.com/matrix-org/matrix-spec-proposals/pull/3672
* {
"type": "m.beacon_info.@matthew:matrix.org.1",
"type": "m.beacon_info",
"state_key": "@matthew:matrix.org",
"content": {
"m.beacon_info": {
@@ -108,7 +98,7 @@ export type MBeaconInfoEvent = EitherAnd<
* m.beacon_info.* event content
*/
export type MBeaconInfoEventContent = &
MBeaconInfoEvent &
MBeaconInfoContent &
// creation timestamp of the beacon on the client
MTimestampEvent &
// the type of asset being tracked as per MSC3488
@@ -116,7 +106,7 @@ export type MBeaconInfoEventContent = &
/**
* m.beacon event example
* https://github.com/matrix-org/matrix-spec-proposals/pull/3489
* https://github.com/matrix-org/matrix-spec-proposals/pull/3672
*
* {
"type": "m.beacon",
+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;
+18 -26
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';
@@ -249,21 +247,20 @@ export class AutoDiscovery {
// Step 7: Copy any other keys directly into the clientConfig. This is for
// things like custom configuration of services.
Object.keys(wellknown)
.map((k) => {
if (k === "m.homeserver" || k === "m.identity_server") {
// Only copy selected parts of the config to avoid overwriting
// properties computed by the validation logic above.
const notProps = ["error", "state", "base_url"];
for (const prop of Object.keys(wellknown[k])) {
if (notProps.includes(prop)) continue;
clientConfig[k][prop] = wellknown[k][prop];
}
} else {
// Just copy the whole thing over otherwise
clientConfig[k] = wellknown[k];
Object.keys(wellknown).forEach((k) => {
if (k === "m.homeserver" || k === "m.identity_server") {
// Only copy selected parts of the config to avoid overwriting
// properties computed by the validation logic above.
const notProps = ["error", "state", "base_url"];
for (const prop of Object.keys(wellknown[k])) {
if (notProps.includes(prop)) continue;
clientConfig[k][prop] = wellknown[k][prop];
}
});
} else {
// Just copy the whole thing over otherwise
clientConfig[k] = wellknown[k];
}
});
// Step 8: Give the config to the caller (finally)
return Promise.resolve(clientConfig);
@@ -373,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;
@@ -411,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");
+301 -238
View File
File diff suppressed because it is too large Load Diff
+53 -11
View File
@@ -16,9 +16,9 @@ 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, M_BEACON_INFO } from "./@types/beacon";
import { MBeaconEventContent, MBeaconInfoContent, MBeaconInfoEventContent } from "./@types/beacon";
import { MsgType } from "./@types/event";
import { TEXT_NODE_TYPE } from "./@types/extensible_events";
import {
@@ -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
*/
@@ -208,11 +237,9 @@ export const makeBeaconInfoContent: MakeBeaconInfoContent = (
assetType,
timestamp,
) => ({
[M_BEACON_INFO.name]: {
description,
timeout,
live: isLive,
},
description,
timeout,
live: isLive,
[M_TIMESTAMP.name]: timestamp || Date.now(),
[M_ASSET.name]: {
type: assetType ?? LocationAssetType.Self,
@@ -227,7 +254,7 @@ export type BeaconInfoState = MBeaconInfoContent & {
* Flatten beacon info event content
*/
export const parseBeaconInfoContent = (content: MBeaconInfoEventContent): BeaconInfoState => {
const { description, timeout, live } = M_BEACON_INFO.findIn<MBeaconInfoContent>(content);
const { description, timeout, live } = content;
const { type: assetType } = M_ASSET.findIn<MAssetContent>(content);
const timestamp = M_TIMESTAMP.findIn<number>(content);
@@ -243,14 +270,14 @@ export const parseBeaconInfoContent = (content: MBeaconInfoEventContent): Beacon
export type MakeBeaconContent = (
uri: string,
timestamp: number,
beaconInfoId: string,
beaconInfoEventId: string,
description?: string,
) => MBeaconEventContent;
export const makeBeaconContent: MakeBeaconContent = (
uri,
timestamp,
beaconInfoId,
beaconInfoEventId,
description,
) => ({
[M_LOCATION.name]: {
@@ -260,6 +287,21 @@ export const makeBeaconContent: MakeBeaconContent = (
[M_TIMESTAMP.name]: timestamp,
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: beaconInfoId,
event_id: beaconInfoEventId,
},
});
export type BeaconLocationState = MLocationContent & {
timestamp: number;
};
export const parseBeaconContent = (content: MBeaconEventContent): BeaconLocationState => {
const { description, uri } = M_LOCATION.findIn<MLocationContent>(content);
const timestamp = M_TIMESTAMP.findIn<number>(content);
return {
description,
uri,
timestamp,
};
};
+2 -2
View File
@@ -73,8 +73,8 @@ export function getHttpUriForMxc(
const fragmentOffset = serverAndMediaId.indexOf("#");
let fragment = "";
if (fragmentOffset >= 0) {
fragment = serverAndMediaId.substr(fragmentOffset);
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
fragment = serverAndMediaId.slice(fragmentOffset);
serverAndMediaId = serverAndMediaId.slice(0, fragmentOffset);
}
const urlParams = (Object.keys(params).length === 0 ? "" : ("?" + utils.encodeParams(params)));
+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;
}
+6 -6
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;
@@ -309,10 +309,10 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
*/
private getDevicesFromStore(userIds: string[]): DeviceInfoMap {
const stored: DeviceInfoMap = {};
userIds.map((u) => {
userIds.forEach((u) => {
stored[u] = {};
const devices = this.getStoredDevicesForUser(u) || [];
devices.map(function(dev) {
devices.forEach(function(dev) {
stored[u][dev.deviceId] = dev;
});
});
@@ -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;
}
}
+13 -15
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> {
@@ -375,9 +375,7 @@ export class BackupManager {
);
if (device) {
sigInfo.device = device;
sigInfo.deviceTrust = await this.baseApis.checkDeviceTrust(
this.baseApis.getUserId(), sigInfo.deviceId,
);
sigInfo.deviceTrust = this.baseApis.checkDeviceTrust(this.baseApis.getUserId(), sigInfo.deviceId);
try {
await verifySignature(
this.baseApis.crypto.olmDevice,
@@ -430,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) {
@@ -464,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 {
@@ -476,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);
@@ -495,7 +493,7 @@ export class BackupManager {
rooms[roomId] = { sessions: {} };
}
const sessionData = await this.baseApis.crypto.olmDevice.exportInboundGroupSession(
const sessionData = this.baseApis.crypto.olmDevice.exportInboundGroupSession(
session.senderKey, session.sessionId, session.sessionData,
);
sessionData.algorithm = MEGOLM_ALGORITHM;
@@ -779,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)) {
@@ -802,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;
+29 -13
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
@@ -1026,7 +1026,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
const decodedBackupKey = new Uint8Array(olmlib.decodeBase64(
fixedBackupKey || sessionBackupKey,
));
await builder.addSessionBackupPrivateKeyToCache(decodedBackupKey);
builder.addSessionBackupPrivateKeyToCache(decodedBackupKey);
} else if (this.backupManager.getKeyBackupEnabled()) {
// key backup is enabled but we don't have a session backup key in SSSS: see if we have one in
// the cache or the user can provide one, and if so, write it to SSSS
@@ -1076,11 +1076,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return this.secretStorage.get(name);
}
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 {
@@ -1423,6 +1420,25 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}
}
/**
* Check whether one of our own devices is cross-signed by our
* user's stored keys, regardless of whether we trust those keys yet.
*
* @param {string} deviceId The ID of the device to check
*
* @returns {boolean} true if the device is cross-signed
*/
public checkIfOwnDeviceCrossSigned(deviceId: string): boolean {
const device = this.deviceList.getStoredDevice(this.userId, deviceId);
const userCrossSigning = this.deviceList.getStoredCrossSigningForUser(this.userId);
return userCrossSigning.checkDeviceTrust(
userCrossSigning,
device,
false,
true,
).isCrossSigningVerified();
}
/*
* Event handler for DeviceList's userNewDevices event
*/
@@ -2302,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) {
@@ -2871,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> {
@@ -228,8 +228,8 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
// (hence 43 characters long).
func({
senderKey: key.substr(KEY_INBOUND_SESSION_PREFIX.length, 43),
sessionId: key.substr(KEY_INBOUND_SESSION_PREFIX.length + 44),
senderKey: key.slice(KEY_INBOUND_SESSION_PREFIX.length, KEY_INBOUND_SESSION_PREFIX.length + 43),
sessionId: key.slice(KEY_INBOUND_SESSION_PREFIX.length + 44),
sessionData: getJsonItem(this.store, key),
});
}
@@ -299,7 +299,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
for (let i = 0; i < this.store.length; ++i) {
const key = this.store.key(i);
if (key.startsWith(prefix)) {
const roomId = key.substr(prefix.length);
const roomId = key.slice(prefix.length);
result[roomId] = getJsonItem(this.store, key);
}
}
@@ -313,8 +313,8 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
for (const session in sessionsNeedingBackup) {
if (Object.prototype.hasOwnProperty.call(sessionsNeedingBackup, session)) {
// see getAllEndToEndInboundGroupSessions for the magic number explanations
const senderKey = session.substr(0, 43);
const sessionId = session.substr(44);
const senderKey = session.slice(0, 43);
const sessionId = session.slice(44);
this.getEndToEndInboundGroupSession(
senderKey, sessionId, null,
(sessionData) => {
+4 -4
View File
@@ -418,8 +418,8 @@ export class MemoryCryptoStore implements CryptoStore {
// (hence 43 characters long).
func({
senderKey: key.substr(0, 43),
sessionId: key.substr(44),
senderKey: key.slice(0, 43),
sessionId: key.slice(44),
sessionData: this.inboundGroupSessions[key],
});
}
@@ -482,8 +482,8 @@ export class MemoryCryptoStore implements CryptoStore {
for (const session in this.sessionsNeedingBackup) {
if (this.inboundGroupSessions[session]) {
sessions.push({
senderKey: session.substr(0, 43),
sessionId: session.substr(44),
senderKey: session.slice(0, 43),
sessionId: session.slice(44),
sessionData: this.inboundGroupSessions[session],
});
if (limit && session.length >= limit) {
+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;
+1
View File
@@ -193,6 +193,7 @@ function calculateMAC(olmSAS: OlmSAS, method: string) {
}
const calculateKeyAgreement = {
// eslint-disable-next-line @typescript-eslint/naming-convention
"curve25519-hkdf-sha256": function(sas: SAS, olmSAS: OlmSAS, bytes: number): Uint8Array {
const ourInfo = `${sas.baseApis.getUserId()}|${sas.baseApis.deviceId}|`
+ `${sas.ourSASPubKey}|`;
@@ -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;
@@ -796,8 +796,7 @@ export class VerificationRequest<
}
private setupTimeout(phase: Phase): void {
const shouldTimeout = !this.timeoutTimer && !this.observeOnly &&
phase === PHASE_REQUESTED;
const shouldTimeout = !this.timeoutTimer && !this.observeOnly && phase === PHASE_REQUESTED;
if (shouldTimeout) {
this.timeoutTimer = setTimeout(this.cancelOnTimeout, this.timeout);
@@ -814,15 +813,15 @@ export class VerificationRequest<
}
}
private cancelOnTimeout = () => {
private cancelOnTimeout = async () => {
try {
if (this.initiatedByMe) {
this.cancel({
await this.cancel({
reason: "Other party didn't accept in time",
code: "m.timeout",
});
} else {
this.cancel({
await this.cancel({
reason: "User didn't accept in time",
code: "m.timeout",
});
+3 -1
View File
@@ -25,7 +25,7 @@ export interface MapperOpts {
}
export function eventMapperFor(client: MatrixClient, options: MapperOpts): EventMapper {
const preventReEmit = Boolean(options.preventReEmit);
let preventReEmit = Boolean(options.preventReEmit);
const decrypt = options.decrypt !== false;
function mapper(plainOldJsObject: Partial<IEvent>) {
@@ -43,6 +43,8 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
} else {
// merge the latest unsigned data from the server
event.setUnsigned({ ...event.getUnsigned(), ...plainOldJsObject.unsigned });
// prevent doubling up re-emitters
preventReEmit = true;
}
const thread = room?.findThreadForEvent(event);
+1 -1
View File
@@ -36,7 +36,7 @@ import {
function matchesWildcard(actualValue: string, filterValue: string): boolean {
if (filterValue.endsWith("*")) {
const typePrefix = filterValue.slice(0, -1);
return actualValue.substr(0, typePrefix.length) === typePrefix;
return actualValue.slice(0, typePrefix.length) === typePrefix;
} else {
return actualValue === filterValue;
}
+5 -5
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;
@@ -1105,7 +1105,7 @@ export class AbortError extends Error {
* @return {any} the result of the network operation
* @throws {ConnectionError} If after maxAttempts the callback still throws ConnectionError
*/
export async function retryNetworkOperation<T>(maxAttempts: number, callback: () => T): Promise<T> {
export async function retryNetworkOperation<T>(maxAttempts: number, callback: () => Promise<T>): Promise<T> {
let attempts = 0;
let lastConnectionError = null;
while (attempts < maxAttempts) {
@@ -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";
+39 -21
View File
@@ -60,7 +60,11 @@ export enum AuthType {
Sso = "m.login.sso",
SsoUnstable = "org.matrix.login.sso",
Dummy = "m.login.dummy",
RegistrationToken = "org.matrix.msc3231.login.registration_token",
RegistrationToken = "m.login.registration_token",
// For backwards compatability with servers that have not yet updated to
// use the stable "m.login.registration_token" type.
// The authentication flow is the same in both cases.
UnstableRegistrationToken = "org.matrix.msc3231.login.registration_token",
}
export interface IAuthDict {
@@ -79,7 +83,8 @@ export interface IAuthDict {
// eslint-disable-next-line camelcase
threepid_creds?: any;
threepidCreds?: any;
registrationToken?: string;
// For m.login.registration_token type
token?: string;
}
class NoAuthFlowFoundError extends Error {
@@ -198,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;
@@ -403,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.
@@ -453,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
@@ -486,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;
+67 -13
View File
@@ -14,22 +14,27 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { M_BEACON_INFO } from "../@types/beacon";
import { BeaconInfoState, parseBeaconInfoContent } from "../content-helpers";
import { MBeaconEventContent } from "../@types/beacon";
import { M_TIMESTAMP } from "../@types/location";
import { BeaconInfoState, BeaconLocationState, parseBeaconContent, parseBeaconInfoContent } from "../content-helpers";
import { MatrixEvent } from "../matrix";
import { sortEventsByLatestContentTimestamp } from "../utils";
import { TypedEventEmitter } from "./typed-event-emitter";
export enum BeaconEvent {
New = "Beacon.new",
Update = "Beacon.update",
LivenessChange = "Beacon.LivenessChange",
Destroy = "Destroy",
Destroy = "Beacon.Destroy",
LocationUpdate = "Beacon.LocationUpdate",
}
export type BeaconEventHandlerMap = {
[BeaconEvent.Update]: (event: MatrixEvent, beacon: Beacon) => void;
[BeaconEvent.LivenessChange]: (isLive: boolean, beacon: Beacon) => void;
[BeaconEvent.Destroy]: (beaconIdentifier: string) => void;
[BeaconEvent.LocationUpdate]: (locationState: BeaconLocationState) => void;
[BeaconEvent.Destroy]: (beaconIdentifier: string) => void;
};
export const isTimestampInDuration = (
@@ -38,16 +43,19 @@ export const isTimestampInDuration = (
timestamp: number,
): boolean => timestamp >= startTimestamp && startTimestamp + durationMs >= timestamp;
export const isBeaconInfoEventType = (type: string) =>
type.startsWith(M_BEACON_INFO.name) ||
type.startsWith(M_BEACON_INFO.altName);
// beacon info events are uniquely identified by
// `<roomId>_<state_key>`
export type BeaconIdentifier = string;
export const getBeaconInfoIdentifier = (event: MatrixEvent): BeaconIdentifier =>
`${event.getRoomId()}_${event.getStateKey()}`;
// https://github.com/matrix-org/matrix-spec-proposals/pull/3489
// https://github.com/matrix-org/matrix-spec-proposals/pull/3672
export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.New>, BeaconEventHandlerMap> {
public readonly roomId: string;
private _beaconInfo: BeaconInfoState;
private _isLive: boolean;
private livenessWatchInterval: number;
private livenessWatchInterval: ReturnType<typeof setInterval>;
private _latestLocationState: BeaconLocationState | undefined;
constructor(
private rootEvent: MatrixEvent,
@@ -61,8 +69,8 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
return this._isLive;
}
public get identifier(): string {
return this.beaconInfoEventType;
public get identifier(): BeaconIdentifier {
return getBeaconInfoIdentifier(this.rootEvent);
}
public get beaconInfoId(): string {
@@ -81,14 +89,23 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
return this._beaconInfo;
}
public get latestLocationState(): BeaconLocationState | undefined {
return this._latestLocationState;
}
public update(beaconInfoEvent: MatrixEvent): void {
if (beaconInfoEvent.getType() !== this.beaconInfoEventType) {
if (getBeaconInfoIdentifier(beaconInfoEvent) !== this.identifier) {
throw new Error('Invalid updating event');
}
// don't update beacon with an older event
if (beaconInfoEvent.event.origin_server_ts < this.rootEvent.event.origin_server_ts) {
return;
}
this.rootEvent = beaconInfoEvent;
this.setBeaconInfo(this.rootEvent);
this.emit(BeaconEvent.Update, beaconInfoEvent, this);
this.clearLatestLocation();
}
public destroy(): void {
@@ -109,14 +126,51 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
clearInterval(this.livenessWatchInterval);
}
this.checkLiveness();
if (this.isLive) {
const expiryInMs = (this._beaconInfo?.timestamp + this._beaconInfo?.timeout + 1) - Date.now();
const expiryInMs = (this._beaconInfo?.timestamp + this._beaconInfo?.timeout) - Date.now();
if (expiryInMs > 1) {
this.livenessWatchInterval = setInterval(this.checkLiveness.bind(this), expiryInMs);
this.livenessWatchInterval = setInterval(
() => { this.monitorLiveness(); },
expiryInMs,
);
}
}
}
/**
* Process Beacon locations
* Emits BeaconEvent.LocationUpdate
*/
public addLocations(beaconLocationEvents: MatrixEvent[]): void {
// discard locations for beacons that are not live
if (!this.isLive) {
return;
}
const validLocationEvents = beaconLocationEvents.filter(event => {
const content = event.getContent<MBeaconEventContent>();
const timestamp = M_TIMESTAMP.findIn<number>(content);
return (
// only include positions that were taken inside the beacon's live period
isTimestampInDuration(this._beaconInfo.timestamp, this._beaconInfo.timeout, timestamp) &&
// ignore positions older than our current latest location
(!this.latestLocationState || timestamp > this.latestLocationState.timestamp)
);
});
const latestLocationEvent = validLocationEvents.sort(sortEventsByLatestContentTimestamp)?.[0];
if (latestLocationEvent) {
this._latestLocationState = parseBeaconContent(latestLocationEvent.getContent());
this.emit(BeaconEvent.LocationUpdate, this.latestLocationState);
}
}
private clearLatestLocation = () => {
this._latestLocationState = undefined;
this.emit(BeaconEvent.LocationUpdate, this.latestLocationState);
};
private setBeaconInfo(event: MatrixEvent): void {
this._beaconInfo = parseBeaconInfoContent(event.getContent());
this.checkLiveness();
+1 -3
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;
@@ -852,14 +851,13 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
}
let relationsWithEventType = relationsWithRelType[eventType];
let relatesToEvent: MatrixEvent;
if (!relationsWithEventType) {
relationsWithEventType = relationsWithRelType[eventType] = new Relations(
relationType,
eventType,
this.room,
);
relatesToEvent = this.findEventById(relatesToEventId) || this.room.getPendingEvent(relatesToEventId);
const relatesToEvent = this.findEventById(relatesToEventId) || this.room.getPendingEvent(relatesToEventId);
if (relatesToEvent) {
relationsWithEventType.setTargetEvent(relatesToEvent);
}
+17 -18
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];
}
}
@@ -1303,8 +1299,7 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
public isRelation(relType: string = undefined): boolean {
// Relation info is lifted out of the encrypted content when sent to
// encrypted rooms, so we have to check `getWireContent` for this.
const content = this.getWireContent();
const relation = content && content["m.relates_to"];
const relation = this.getWireContent()?.["m.relates_to"];
return relation && relation.rel_type && relation.event_id &&
((relType && relation.rel_type === relType) || !relType);
}
@@ -1336,6 +1331,10 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
if (this.isRedacted() && newEvent) {
return;
}
// don't allow state events to be replaced using this mechanism as per MSC2676
if (this.isState()) {
return;
}
if (this._replacingEvent !== newEvent) {
this._replacingEvent = newEvent;
this.emit(MatrixEventEvent.Replaced, this);
@@ -1586,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 },
+70 -19
View File
@@ -22,12 +22,13 @@ import { RoomMember } from "./room-member";
import { logger } from '../logger';
import * as utils from "../utils";
import { EventType } from "../@types/event";
import { MatrixEvent } from "./event";
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, isBeaconInfoEventType, BeaconEventHandlerMap } from "./beacon";
import { Beacon, BeaconEvent, BeaconEventHandlerMap, getBeaconInfoIdentifier, BeaconIdentifier } from "./beacon";
import { TypedReEmitter } from "../ReEmitter";
import { M_BEACON, M_BEACON_INFO } from "../@types/beacon";
// possible statuses for out-of-band member loading
enum OobStatus {
@@ -80,8 +81,8 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
public events = new Map<string, Map<string, MatrixEvent>>(); // Map<eventType, Map<stateKey, MatrixEvent>>
public paginationToken: string = null;
public readonly beacons = new Map<string, Beacon>();
private liveBeaconIds: string[] = [];
public readonly beacons = new Map<BeaconIdentifier, Beacon>();
private _liveBeaconIds: BeaconIdentifier[] = [];
/**
* Construct room state.
@@ -248,6 +249,10 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
return !!this.liveBeaconIds?.length;
}
public get liveBeaconIds(): BeaconIdentifier[] {
return this._liveBeaconIds;
}
/**
* Creates a copy of this room state so that mutations to either won't affect the other.
* @return {RoomState} the copy of the room state
@@ -330,7 +335,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
return;
}
if (isBeaconInfoEventType(event.getType())) {
if (M_BEACON_INFO.matches(event.getType())) {
this.setBeacon(event);
}
@@ -404,6 +409,51 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
this.emit(RoomStateEvent.Update, this);
}
public processBeaconEvents(events: MatrixEvent[], matrixClient: MatrixClient): void {
if (
!events.length ||
// discard locations if we have no beacons
!this.beacons.size
) {
return;
}
const beaconByEventIdDict: Record<string, Beacon> =
[...this.beacons.values()].reduce((dict, beacon) => ({ ...dict, [beacon.beaconInfoId]: beacon }), {});
const processBeaconRelation = (beaconInfoEventId: string, event: MatrixEvent): void => {
if (!M_BEACON.matches(event.getType())) {
return;
}
const beacon = beaconByEventIdDict[beaconInfoEventId];
if (beacon) {
beacon.addLocations([event]);
}
};
events.forEach((event: MatrixEvent) => {
const relatedToEventId = event.getRelation()?.event_id;
// not related to a beacon we know about
// discard
if (!beaconByEventIdDict[relatedToEventId]) {
return;
}
matrixClient.decryptEventIfNeeded(event);
if (event.isBeingDecrypted() || event.isDecryptionFailure()) {
// add an event listener for once the event is decrypted.
event.once(MatrixEventEvent.Decrypted, async () => {
processBeaconRelation(relatedToEventId, event);
});
} else {
processBeaconRelation(relatedToEventId, event);
}
});
}
/**
* Looks up a member by the given userId, and if it doesn't exist,
* create it and emit the `RoomState.newMember` event.
@@ -437,12 +487,16 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
* @experimental
*/
private setBeacon(event: MatrixEvent): void {
if (this.beacons.has(event.getType())) {
const beacon = this.beacons.get(event.getType());
const beaconIdentifier = getBeaconInfoIdentifier(event);
if (this.beacons.has(beaconIdentifier)) {
const beacon = this.beacons.get(beaconIdentifier);
if (event.isRedacted()) {
beacon.destroy();
this.beacons.delete(event.getType());
if (beacon.beaconInfoId === event.getRedactionEvent()?.['redacts']) {
beacon.destroy();
this.beacons.delete(beaconIdentifier);
}
return;
}
@@ -464,25 +518,22 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
this.emit(BeaconEvent.New, event, beacon);
beacon.on(BeaconEvent.LivenessChange, this.onBeaconLivenessChange.bind(this));
this.beacons.set(beacon.beaconInfoEventType, beacon);
beacon.on(BeaconEvent.Destroy, this.onBeaconLivenessChange.bind(this));
this.beacons.set(beacon.identifier, beacon);
}
/**
* @experimental
* Check liveness of room beacons
* emit RoomStateEvent.BeaconLiveness when
* roomstate.hasLiveBeacons has changed
* emit RoomStateEvent.BeaconLiveness event
*/
private onBeaconLivenessChange(): void {
const prevHasLiveBeacons = !!this.liveBeaconIds?.length;
this.liveBeaconIds = Array.from(this.beacons.values())
this._liveBeaconIds = Array.from(this.beacons.values())
.filter(beacon => beacon.isLive)
.map(beacon => beacon.beaconInfoId);
.map(beacon => beacon.identifier);
const hasLiveBeacons = !!this.liveBeaconIds.length;
if (prevHasLiveBeacons !== hasLiveBeacons) {
this.emit(RoomStateEvent.BeaconLiveness, this, hasLiveBeacons);
}
this.emit(RoomStateEvent.BeaconLiveness, this, this.hasLiveBeacons);
}
private getStateEventMatching(event: MatrixEvent): MatrixEvent | null {
+134 -167
View File
@@ -22,7 +22,7 @@ import { EventTimelineSet, DuplicateStrategy } from "./event-timeline-set";
import { Direction, EventTimeline } from "./event-timeline";
import { getHttpUriForMxc } from "../content-repo";
import * as utils from "../utils";
import { defer, normalize } from "../utils";
import { normalize } from "../utils";
import { IEvent, IThreadBundledRelationship, MatrixEvent, MatrixEventEvent, MatrixEventHandlerMap } from "./event";
import { EventStatus } from "./event-status";
import { RoomMember } from "./room-member";
@@ -46,8 +46,9 @@ import {
FILTER_RELATED_BY_SENDERS,
ThreadFilterType,
} from "./thread";
import { Method } from "../http-api";
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
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
@@ -55,10 +56,10 @@ import { TypedEventEmitter } from "./typed-event-emitter";
// room versions which are considered okay for people to run without being asked
// to upgrade (ie: "stable"). Eventually, we should remove these when all homeservers
// return an m.room_versions capability.
const KNOWN_SAFE_ROOM_VERSION = '6';
const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4', '5', '6'];
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;
};
};
@@ -214,8 +215,6 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
private getTypeWarning = false;
private getVersionWarning = false;
private membersPromise?: Promise<boolean>;
// Map from threadId to pending Thread instance created by createThreadFetchRoot
private threadPromises = new Map<string, Promise<Thread>>();
// XXX: These should be read-only
/**
@@ -266,7 +265,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
/**
* @experimental
*/
public threads = new Map<string, Thread>();
private threads = new Map<string, Thread>();
public lastThread: Thread;
/**
@@ -788,16 +787,9 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
}
}
private async loadMembersFromServer(): Promise<IEvent[]> {
private async loadMembersFromServer(): Promise<IStateEventWithRoomId[]> {
const lastSyncToken = this.client.store.getSyncToken();
const queryString = utils.encodeParams({
not_membership: "leave",
at: lastSyncToken,
});
const path = utils.encodeUri("/rooms/$roomId/members?" + queryString,
{ $roomId: this.roomId });
const http = this.client.http;
const response = await http.authedRequest<{ chunk: IEvent[] }>(undefined, Method.Get, path);
const response = await this.client.members(this.roomId, undefined, "leave", lastSyncToken);
return response.chunk;
}
@@ -806,12 +798,13 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
let fromServer = false;
let rawMembersEvents = await this.client.store.getOutOfBandMembers(this.roomId);
// If the room is encrypted, we always fetch members from the server at
// least once, in case the latest state wasn't persisted properly. Note
// least once, in case the latest state wasn't persisted properly. Note
// that this function is only called once (unless loading the members
// fails), since loadMembersIfNeeded always returns this.membersPromise
// if set, which will be the result of the first (successful) call.
if (rawMembersEvents === null ||
(this.client.isCryptoEnabled() && this.client.isRoomEncrypted(this.roomId))) {
(this.client.isCryptoEnabled() && this.client.isRoomEncrypted(this.roomId))
) {
fromServer = true;
rawMembersEvents = await this.loadMembersFromServer();
logger.log(`LL: got ${rawMembersEvents.length} ` +
@@ -857,7 +850,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
if (fromServer) {
const oobMembers = this.currentState.getMembers()
.filter((m) => m.isOutOfBand())
.map((m) => m.events.member.event as IEvent);
.map((m) => m.events.member.event as IStateEventWithRoomId);
logger.log(`LL: telling store to write ${oobMembers.length}`
+ ` members for room ${this.roomId}`);
const store = this.client.store;
@@ -1132,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;
@@ -1149,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);
}
}
}
@@ -1214,9 +1207,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
* @experimental
*/
public getThread(eventId: string): Thread {
return this.getThreads().find(thread => {
return thread.id === eventId;
});
return this.threads.get(eventId);
}
/**
@@ -1474,7 +1465,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
public threadsReady = false;
public async fetchRoomThreads(): Promise<void> {
if (this.threadsReady) {
if (this.threadsReady || !this.client.supportsExperimentalThreads()) {
return;
}
@@ -1530,7 +1521,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
}
if (!this.getThread(rootEvent.getId())) {
this.createThread(rootEvent, [], true);
this.createThread(rootEvent.getId(), rootEvent, [], true);
}
}
@@ -1612,16 +1603,6 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
};
}
// A reply directly to a thread response is shown as part of the thread only, this is to provide a better
// experience when communicating with users using clients without full threads support
if (parentEvent?.isThreadRelation) {
return {
shouldLiveInRoom: false,
shouldLiveInThread: true,
threadId: parentEvent.threadRootId,
};
}
// We've exhausted all scenarios, can safely assume that this event should live in the room timeline only
return {
shouldLiveInRoom: true,
@@ -1636,58 +1617,14 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
return threadId ? this.getThread(threadId) : null;
}
public async createThreadFetchRoot(
threadId: string,
events?: MatrixEvent[],
toStartOfTimeline?: boolean,
): Promise<Thread | null> {
private addThreadedEvents(threadId: string, events: MatrixEvent[], toStartOfTimeline = false): void {
let thread = this.getThread(threadId);
if (!thread) {
const deferred = defer<Thread | null>();
this.threadPromises.set(threadId, deferred.promise);
let rootEvent = this.findEventById(threadId);
// If the rootEvent does not exist in the local stores, then fetch it from the server.
try {
const eventData = await this.client.fetchRoomEvent(this.roomId, threadId);
const mapper = this.client.getEventMapper();
rootEvent = mapper(eventData); // will merge with existing event object if such is known
} catch (e) {
logger.error("Failed to fetch thread root to construct thread with", e);
} finally {
this.threadPromises.delete(threadId);
// The root event might be not be visible to the person requesting it.
// If it wasn't fetched successfully the thread will work in "limited" mode and won't
// benefit from all the APIs a homeserver can provide to enhance the thread experience
thread = this.createThread(rootEvent, events, toStartOfTimeline);
if (thread) {
rootEvent.setThread(thread);
}
deferred.resolve(thread);
}
}
return thread;
}
private async addThreadedEvents(events: MatrixEvent[], threadId: string, toStartOfTimeline = false): Promise<void> {
let thread = this.getThread(threadId);
if (this.threadPromises.has(threadId)) {
thread = await this.threadPromises.get(threadId);
}
events = events.filter(e => e.getId() !== threadId); // filter out any root events
if (thread) {
for (const event of events) {
await thread.addEvent(event, toStartOfTimeline);
}
thread.addEvents(events, toStartOfTimeline);
} else {
thread = await this.createThreadFetchRoot(threadId, events, toStartOfTimeline);
}
if (thread) {
const rootEvent = this.findEventById(threadId) ?? events.find(e => e.getId() === threadId);
thread = this.createThread(threadId, rootEvent, events, toStartOfTimeline);
this.emit(ThreadEvent.Update, thread);
}
}
@@ -1696,75 +1633,77 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
* Adds events to a thread's timeline. Will fire "Thread.update"
* @experimental
*/
public async processThreadedEvents(events: MatrixEvent[], toStartOfTimeline: boolean): Promise<unknown> {
public processThreadedEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void {
events.forEach(this.applyRedaction);
const eventsByThread: { [threadId: string]: MatrixEvent[] } = {};
for (const event of events) {
const { threadId } = this.eventShouldLiveIn(event);
if (!eventsByThread[threadId]) {
const { threadId, shouldLiveInThread } = this.eventShouldLiveIn(event);
if (shouldLiveInThread && !eventsByThread[threadId]) {
eventsByThread[threadId] = [];
}
eventsByThread[threadId].push(event);
eventsByThread[threadId]?.push(event);
}
return Promise.all(Object.entries(eventsByThread).map(([threadId, events]) => (
this.addThreadedEvents(events, threadId, toStartOfTimeline)
)));
Object.entries(eventsByThread).map(([threadId, threadEvents]) => (
this.addThreadedEvents(threadId, threadEvents, toStartOfTimeline)
));
}
public createThread(
threadId: string,
rootEvent: MatrixEvent | undefined,
events: MatrixEvent[] = [],
toStartOfTimeline: boolean,
): Thread | undefined {
): Thread {
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)));
}
}
const thread = new Thread(rootEvent, {
const thread = new Thread(threadId, rootEvent, {
initialEvents: events,
room: this,
client: this.client,
});
// If we managed to create a thread and figure out its `id` then we can use it
if (thread.id) {
this.threads.set(thread.id, thread);
this.reEmitter.reEmit(thread, [
ThreadEvent.Update,
ThreadEvent.NewReply,
RoomEvent.Timeline,
RoomEvent.TimelineReset,
]);
this.threads.set(thread.id, thread);
this.reEmitter.reEmit(thread, [
ThreadEvent.Update,
ThreadEvent.NewReply,
RoomEvent.Timeline,
RoomEvent.TimelineReset,
]);
if (!this.lastThread || this.lastThread.rootEvent?.localTimestamp < rootEvent?.localTimestamp) {
this.lastThread = thread;
}
this.emit(ThreadEvent.New, thread, toStartOfTimeline);
if (this.threadsReady) {
this.threadsTimelineSets.forEach(timelineSet => {
if (thread.rootEvent) {
if (Thread.hasServerSideSupport) {
timelineSet.addLiveEvent(thread.rootEvent);
} else {
timelineSet.addEventToTimeline(
thread.rootEvent,
timelineSet.getLiveTimeline(),
toStartOfTimeline,
);
}
}
});
}
return thread;
if (!this.lastThread || this.lastThread.rootEvent?.localTimestamp < rootEvent?.localTimestamp) {
this.lastThread = thread;
}
this.emit(ThreadEvent.New, thread, toStartOfTimeline);
if (this.threadsReady) {
this.threadsTimelineSets.forEach(timelineSet => {
if (thread.rootEvent) {
if (Thread.hasServerSideSupport) {
timelineSet.addLiveEvent(thread.rootEvent);
} else {
timelineSet.addEventToTimeline(
thread.rootEvent,
timelineSet.getLiveTimeline(),
toStartOfTimeline,
);
}
}
});
}
return thread;
}
private applyRedaction = (event: MatrixEvent): void => {
@@ -1856,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
@@ -2205,33 +2144,30 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
}
const threadRoots = this.findThreadRoots(events);
const threadInfos = events.map(e => this.eventShouldLiveIn(e, events, threadRoots));
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,
} = threadInfos[i];
} = this.eventShouldLiveIn(event, events, threadRoots);
if (shouldLiveInThread) {
if (!eventsByThread[threadId]) {
eventsByThread[threadId] = [];
}
eventsByThread[threadId].push(events[i]);
if (shouldLiveInThread && !eventsByThread[threadId]) {
eventsByThread[threadId] = [];
}
eventsByThread[threadId]?.push(event);
if (shouldLiveInRoom) {
this.addLiveEvent(events[i], duplicateStrategy, fromCache);
this.addLiveEvent(event, duplicateStrategy, fromCache);
}
}
Object.entries(eventsByThread).forEach(([threadId, threadEvents]) => {
this.addThreadedEvents(threadEvents, threadId, false);
this.addThreadedEvents(threadId, threadEvents, false);
});
}
@@ -2339,22 +2275,27 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
// set fake stripped state events if this is an invite room so logic remains
// consistent elsewhere.
const membershipEvent = this.currentState.getStateEvents(EventType.RoomMember, this.myUserId);
if (membershipEvent && membershipEvent.getContent().membership === "invite") {
const strippedStateEvents = membershipEvent.getUnsigned().invite_room_state || [];
strippedStateEvents.forEach((strippedEvent) => {
const existingEvent = this.currentState.getStateEvents(strippedEvent.type, strippedEvent.state_key);
if (!existingEvent) {
// set the fake stripped event instead
this.currentState.setStateEvents([new MatrixEvent({
type: strippedEvent.type,
state_key: strippedEvent.state_key,
content: strippedEvent.content,
event_id: "$fake" + Date.now(),
room_id: this.roomId,
user_id: this.myUserId, // technically a lie
})]);
}
});
if (membershipEvent) {
const membership = membershipEvent.getContent().membership;
this.updateMyMembership(membership);
if (membership === "invite") {
const strippedStateEvents = membershipEvent.getUnsigned().invite_room_state || [];
strippedStateEvents.forEach((strippedEvent) => {
const existingEvent = this.currentState.getStateEvents(strippedEvent.type, strippedEvent.state_key);
if (!existingEvent) {
// set the fake stripped event instead
this.currentState.setStateEvents([new MatrixEvent({
type: strippedEvent.type,
state_key: strippedEvent.state_key,
content: strippedEvent.content,
event_id: "$fake" + Date.now(),
room_id: this.roomId,
user_id: this.myUserId, // technically a lie
})]);
}
});
}
}
const oldName = this.name;
@@ -2376,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;
}
@@ -2401,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;
}
/**
@@ -2555,7 +2522,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
}
this.receiptCacheByEventId[eventId].push({
userId: userId,
type: receiptType,
type: receiptType as ReceiptType,
data: receipt,
});
});
@@ -2568,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);
}
+96 -57
View File
@@ -70,16 +70,21 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
public readonly room: Room;
public readonly client: MatrixClient;
public initialEventsFetched = false;
public readonly id: string;
public initialEventsFetched = !Thread.hasServerSideSupport;
constructor(
public readonly rootEvent: MatrixEvent | undefined,
public readonly id: string,
public rootEvent: MatrixEvent | undefined,
opts: IThreadOpts,
) {
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, {
@@ -95,15 +100,37 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
]);
this.room.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
this.room.on(RoomEvent.Redaction, this.onRedaction);
this.room.on(RoomEvent.LocalEchoUpdated, this.onEcho);
this.timelineSet.on(RoomEvent.Timeline, this.onEcho);
// If we weren't able to find the root event, it's probably missing,
// and we define the thread ID from one of the thread relation
this.id = rootEvent?.getId() ?? opts?.initialEvents?.find(event => event.isThreadRelation)?.relationEventId;
this.initialiseThread(this.rootEvent);
if (opts.initialEvents) {
this.addEvents(opts.initialEvents, false);
}
// even if this thread is thought to be originating from this client, we initialise it as we may be in a
// gappy sync and a thread around this event may already exist.
this.initialiseThread();
opts?.initialEvents?.forEach(event => this.addEvent(event, false));
this.rootEvent?.setThread(this);
}
private async fetchRootEvent(): Promise<void> {
this.rootEvent = this.room.findEventById(this.id);
// If the rootEvent does not exist in the local stores, then fetch it from the server.
try {
const eventData = await this.client.fetchRoomEvent(this.roomId, this.id);
const mapper = this.client.getEventMapper();
this.rootEvent = mapper(eventData); // will merge with existing event object if such is known
} catch (e) {
logger.error("Failed to fetch thread root to construct thread with", e);
}
// The root event might be not be visible to the person requesting it.
// If it wasn't fetched successfully the thread will work in "limited" mode and won't
// benefit from all the APIs a homeserver can provide to enhance the thread experience
this.rootEvent?.setThread(this);
this.emit(ThreadEvent.Update, this);
}
public static setServerSideSupport(hasServerSideSupport: boolean, useStable: boolean): void {
@@ -115,23 +142,25 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
}
}
private onBeforeRedaction = (event: MatrixEvent) => {
private onBeforeRedaction = (event: MatrixEvent, redaction: MatrixEvent) => {
if (event?.isRelation(THREAD_RELATION_TYPE.name) &&
this.room.eventShouldLiveIn(event).threadId === this.id
this.room.eventShouldLiveIn(event).threadId === this.id &&
event.getId() !== this.id && // the root event isn't counted in the length so ignore this redaction
!redaction.status // only respect it when it succeeds
) {
this.replyCount--;
this.emit(ThreadEvent.Update, this);
}
};
if (this.lastEvent?.getId() === event.getId()) {
const events = [...this.timelineSet.getLiveTimeline().getEvents()].reverse();
this.lastEvent = events.find(e => (
!e.isRedacted() &&
e.getId() !== event.getId() &&
e.isRelation(THREAD_RELATION_TYPE.name)
)) ?? this.rootEvent;
this.emit(ThreadEvent.NewReply, this, this.lastEvent);
}
private onRedaction = (event: MatrixEvent) => {
if (event.threadRootId !== this.id) return; // ignore redactions for other timelines
const events = [...this.timelineSet.getLiveTimeline().getEvents()].reverse();
this.lastEvent = events.find(e => (
!e.isRedacted() &&
e.isRelation(THREAD_RELATION_TYPE.name)
)) ?? this.rootEvent;
this.emit(ThreadEvent.Update, this);
};
private onEcho = (event: MatrixEvent) => {
@@ -142,7 +171,7 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
// when threads are used over federation. That could result in the reply
// count value drifting away from the value returned by the server
const isThreadReply = event.isRelation(THREAD_RELATION_TYPE.name);
if (!this.lastEvent || (isThreadReply
if (!this.lastEvent || this.lastEvent.isRedacted() || (isThreadReply
&& (event.getId() !== this.lastEvent.getId())
&& (event.localTimestamp > this.lastEvent.localTimestamp))
) {
@@ -177,6 +206,11 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
}
}
public addEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void {
events.forEach(ev => this.addEvent(ev, toStartOfTimeline, false));
this.emit(ThreadEvent.Update, this);
}
/**
* Add an event to the thread and updates
* the tail/root references if needed
@@ -184,43 +218,60 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
* @param event The event to add
* @param {boolean} toStartOfTimeline whether the event is being added
* to the start (and not the end) of the timeline.
* @param {boolean} emit whether to emit the Update event if the thread was updated or not.
*/
public async addEvent(event: MatrixEvent, toStartOfTimeline: boolean): Promise<void> {
public addEvent(event: MatrixEvent, toStartOfTimeline: boolean, emit = true): void {
event.setThread(this);
if (!this._currentUserParticipated && event.getSender() === this.client.getUserId()) {
this._currentUserParticipated = true;
}
if ([RelationType.Annotation, RelationType.Replace].includes(event.getRelation()?.rel_type as RelationType)) {
// Apply annotations and replace relations to the relations of the timeline only
this.timelineSet.setRelationsTarget(event);
this.timelineSet.aggregateRelations(event);
return;
}
// Add all incoming events to the thread's timeline set when there's no server support
if (!Thread.hasServerSideSupport) {
// all the relevant membership info to hydrate events with a sender
// is held in the main room timeline
// We want to fetch the room state from there and pass it down to this thread
// timeline set to let it reconcile an event with its relevant RoomMember
event.setThread(this);
this.addEventToTimeline(event, toStartOfTimeline);
await this.client.decryptEventIfNeeded(event, {});
this.client.decryptEventIfNeeded(event, {});
} else if (!toStartOfTimeline &&
this.initialEventsFetched &&
event.localTimestamp > this.lastReply().localTimestamp
event.localTimestamp > this.lastReply()?.localTimestamp
) {
await this.fetchEditsWhereNeeded(event);
this.fetchEditsWhereNeeded(event);
this.addEventToTimeline(event, false);
}
if (!this._currentUserParticipated && event.getSender() === this.client.getUserId()) {
this._currentUserParticipated = true;
}
// If no thread support exists we want to count all thread relation
// added as a reply. We can't rely on the bundled relationships count
if (!Thread.hasServerSideSupport && event.isRelation(THREAD_RELATION_TYPE.name)) {
if ((!Thread.hasServerSideSupport || !this.rootEvent) && event.isRelation(THREAD_RELATION_TYPE.name)) {
this.replyCount++;
}
this.emit(ThreadEvent.Update, this);
if (emit) {
this.emit(ThreadEvent.Update, this);
}
}
private initialiseThread(rootEvent: MatrixEvent | undefined): void {
const bundledRelationship = rootEvent
?.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
private getRootEventBundledRelationship(rootEvent = this.rootEvent): IThreadBundledRelationship {
return rootEvent?.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
}
private async initialiseThread(): Promise<void> {
let bundledRelationship = this.getRootEventBundledRelationship();
if (Thread.hasServerSideSupport && !bundledRelationship) {
await this.fetchRootEvent();
bundledRelationship = this.getRootEventBundledRelationship();
}
if (Thread.hasServerSideSupport && bundledRelationship) {
this.replyCount = bundledRelationship.count;
@@ -233,6 +284,8 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
this.fetchEditsWhereNeeded(event);
}
this.emit(ThreadEvent.Update, this);
}
// XXX: Workaround for https://github.com/matrix-org/matrix-spec-proposals/pull/2676/files#r827240084
@@ -250,24 +303,10 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
}));
}
public async fetchInitialEvents(): Promise<{
originalEvent: MatrixEvent;
events: MatrixEvent[];
nextBatch?: string;
prevBatch?: string;
} | null> {
if (!Thread.hasServerSideSupport) {
this.initialEventsFetched = true;
return null;
}
try {
const response = await this.fetchEvents();
this.initialEventsFetched = true;
return response;
} catch (e) {
return null;
}
public async fetchInitialEvents(): Promise<void> {
if (this.initialEventsFetched) return;
await this.fetchEvents();
this.initialEventsFetched = true;
}
private setEventMetadata(event: MatrixEvent): void {
@@ -316,7 +355,7 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
* A getter for the last event added to the thread
*/
public get replyToEvent(): MatrixEvent {
return this.lastEvent;
return this.lastEvent ?? this.lastReply();
}
public get events(): MatrixEvent[] {
@@ -335,7 +374,7 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
return this.timelineSet.getLiveTimeline();
}
public async fetchEvents(opts: IRelationsRequestOpts = { limit: 20 }): Promise<{
public async fetchEvents(opts: IRelationsRequestOpts = { limit: 20, direction: Direction.Backward }): Promise<{
originalEvent: MatrixEvent;
events: MatrixEvent[];
nextBatch?: string;
@@ -367,7 +406,7 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
return this.client.decryptEventIfNeeded(event);
}));
const prependEvents = !opts.direction || opts.direction === Direction.Backward;
const prependEvents = (opts.direction ?? Direction.Backward) === Direction.Backward;
this.timelineSet.addEventsToTimeline(
events,
-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;
}
+10 -5
View File
@@ -17,11 +17,12 @@ limitations under the License.
import { EventType } from "../@types/event";
import { Room } from "../models/room";
import { User } from "../models/user";
import { IEvent, MatrixEvent } from "../models/event";
import { MatrixEvent } from "../models/event";
import { Filter } from "../filter";
import { RoomSummary } from "../models/room-summary";
import { IMinimalEvent, IRooms, ISyncResponse } from "../sync-accumulator";
import { IStartClientOpts } from "../client";
import { IStateEventWithRoomId } from "../@types/search";
export interface ISavedSync {
nextBatch: string;
@@ -36,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>;
/**
@@ -104,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[];
@@ -204,9 +209,9 @@ export interface IStore {
*/
deleteAllData(): Promise<void>;
getOutOfBandMembers(roomId: string): Promise<IEvent[] | null>;
getOutOfBandMembers(roomId: string): Promise<IStateEventWithRoomId[] | null>;
setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void>;
setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void>;
clearOutOfBandMembers(roomId: string): Promise<void>;
+3 -3
View File
@@ -15,7 +15,7 @@ limitations under the License.
*/
import { ISavedSync } from "./index";
import { IEvent, IStartClientOpts, ISyncResponse } from "..";
import { IEvent, IStartClientOpts, IStateEventWithRoomId, ISyncResponse } from "..";
export interface IIndexedDBBackend {
connect(): Promise<void>;
@@ -25,8 +25,8 @@ export interface IIndexedDBBackend {
getSavedSync(): Promise<ISavedSync>;
getNextBatchToken(): Promise<string>;
clearDatabase(): Promise<void>;
getOutOfBandMembers(roomId: string): Promise<IEvent[] | null>;
setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void>;
getOutOfBandMembers(roomId: string): Promise<IStateEventWithRoomId[] | null>;
setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void>;
clearOutOfBandMembers(roomId: string): Promise<void>;
getUserPresenceEvents(): Promise<UserTuple[]>;
getClientOptions(): Promise<IStartClientOpts>;
+6 -8
View File
@@ -18,7 +18,7 @@ import { IMinimalEvent, ISyncData, ISyncResponse, SyncAccumulator } from "../syn
import * as utils from "../utils";
import * as IndexedDBHelpers from "../indexeddb-helpers";
import { logger } from '../logger';
import { IEvent, IStartClientOpts } from "..";
import { IStartClientOpts, IStateEventWithRoomId } from "..";
import { ISavedSync } from "./index";
import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend";
@@ -229,15 +229,15 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
* @returns {Promise<event[]>} the events, potentially an empty array if OOB loading didn't yield any new members
* @returns {null} in case the members for this room haven't been stored yet
*/
public getOutOfBandMembers(roomId: string): Promise<IEvent[] | null> {
return new Promise<IEvent[] | null>((resolve, reject) => {
public getOutOfBandMembers(roomId: string): Promise<IStateEventWithRoomId[] | null> {
return new Promise<IStateEventWithRoomId[] | null>((resolve, reject) => {
const tx = this.db.transaction(["oob_membership_events"], "readonly");
const store = tx.objectStore("oob_membership_events");
const roomIndex = store.index("room");
const range = IDBKeyRange.only(roomId);
const request = roomIndex.openCursor(range);
const membershipEvents: IEvent[] = [];
const membershipEvents: IStateEventWithRoomId[] = [];
// did we encounter the oob_written marker object
// amongst the results? That means OOB member
// loading already happened for this room
@@ -278,7 +278,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
* @param {string} roomId
* @param {event[]} membershipEvents the membership events to store
*/
public async setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
public async setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void> {
logger.log(`LL: backend about to store ${membershipEvents.length}` +
` members for ${roomId}`);
const tx = this.db.transaction(["oob_membership_events"], "readwrite");
@@ -530,9 +530,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
const txn = this.db.transaction(["client_options"], "readonly");
const store = txn.objectStore("client_options");
return selectQuery(store, undefined, (cursor) => {
if (cursor.value && cursor.value && cursor.value.options) {
return cursor.value.options;
}
return cursor.value?.options;
}).then((results) => results[0]);
});
}
+3 -3
View File
@@ -18,7 +18,7 @@ import { logger } from "../logger";
import { defer, IDeferred } from "../utils";
import { ISavedSync } from "./index";
import { IStartClientOpts } from "../client";
import { IEvent, ISyncResponse } from "..";
import { IStateEventWithRoomId, ISyncResponse } from "..";
import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend";
export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend {
@@ -97,7 +97,7 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend {
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
* @returns {null} in case the members for this room haven't been stored yet
*/
public getOutOfBandMembers(roomId: string): Promise<IEvent[] | null> {
public getOutOfBandMembers(roomId: string): Promise<IStateEventWithRoomId[] | null> {
return this.doCmd('getOutOfBandMembers', [roomId]);
}
@@ -109,7 +109,7 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend {
* @param {event[]} membershipEvents the membership events to store
* @returns {Promise} when all members have been stored
*/
public setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
public setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void> {
return this.doCmd('setOutOfBandMembers', [roomId, membershipEvents]);
}
+10 -6
View File
@@ -26,6 +26,7 @@ import { ISavedSync } from "./index";
import { IIndexedDBBackend } from "./indexeddb-backend";
import { ISyncResponse } from "../sync-accumulator";
import { TypedEventEmitter } from "../models/typed-event-emitter";
import { IStateEventWithRoomId } from "../@types/search";
/**
* This is an internal module. See {@link IndexedDBStore} for the public class.
@@ -242,7 +243,7 @@ export class IndexedDBStore extends MemoryStore {
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
* @returns {null} in case the members for this room haven't been stored yet
*/
public getOutOfBandMembers = this.degradable((roomId: string): Promise<IEvent[]> => {
public getOutOfBandMembers = this.degradable((roomId: string): Promise<IStateEventWithRoomId[]> => {
return this.backend.getOutOfBandMembers(roomId);
}, "getOutOfBandMembers");
@@ -254,10 +255,13 @@ export class IndexedDBStore extends MemoryStore {
* @param {event[]} membershipEvents the membership events to store
* @returns {Promise} when all members have been stored
*/
public setOutOfBandMembers = this.degradable((roomId: string, membershipEvents: IEvent[]): Promise<void> => {
super.setOutOfBandMembers(roomId, membershipEvents);
return this.backend.setOutOfBandMembers(roomId, membershipEvents);
}, "setOutOfBandMembers");
public setOutOfBandMembers = this.degradable(
(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void> => {
super.setOutOfBandMembers(roomId, membershipEvents);
return this.backend.setOutOfBandMembers(roomId, membershipEvents);
},
"setOutOfBandMembers",
);
public clearOutOfBandMembers = this.degradable((roomId: string) => {
super.clearOutOfBandMembers(roomId);
@@ -293,7 +297,7 @@ export class IndexedDBStore extends MemoryStore {
return async (...args) => {
try {
return func.call(this, ...args);
return await func.call(this, ...args);
} catch (e) {
logger.error("IndexedDBStore failure, degrading to MemoryStore", e);
this.emitter.emit("degraded", e);
+6 -5
View File
@@ -22,13 +22,14 @@ limitations under the License.
import { EventType } from "../@types/event";
import { Room } from "../models/room";
import { User } from "../models/user";
import { IEvent, MatrixEvent } from "../models/event";
import { MatrixEvent } from "../models/event";
import { RoomState, RoomStateEvent } from "../models/room-state";
import { RoomMember } from "../models/room-member";
import { Filter } from "../filter";
import { ISavedSync, IStore } from "./index";
import { RoomSummary } from "../models/room-summary";
import { ISyncResponse } from "../sync-accumulator";
import { IStateEventWithRoomId } from "../@types/search";
function isValidFilterId(filterId: string): boolean {
const isValidStr = typeof filterId === "string" &&
@@ -60,7 +61,7 @@ export class MemoryStore implements IStore {
private filters: Record<string, Record<string, Filter>> = {};
public accountData: Record<string, MatrixEvent> = {}; // type : content
private readonly localStorage: Storage;
private oobMembers: Record<string, IEvent[]> = {}; // roomId: [member events]
private oobMembers: Record<string, IStateEventWithRoomId[]> = {}; // roomId: [member events]
private clientOptions = {};
constructor(opts: IOpts = {}) {
@@ -198,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.
*/
@@ -389,7 +390,7 @@ export class MemoryStore implements IStore {
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
* @returns {null} in case the members for this room haven't been stored yet
*/
public getOutOfBandMembers(roomId: string): Promise<IEvent[] | null> {
public getOutOfBandMembers(roomId: string): Promise<IStateEventWithRoomId[] | null> {
return Promise.resolve(this.oobMembers[roomId] || null);
}
@@ -401,7 +402,7 @@ export class MemoryStore implements IStore {
* @param {event[]} membershipEvents the membership events to store
* @returns {Promise} when all members have been stored
*/
public setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
public setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void> {
this.oobMembers[roomId] = membershipEvents;
return Promise.resolve();
}
+5 -5
View File
@@ -78,7 +78,7 @@ WebStorageSessionStore.prototype = {
const devices = {};
for (let i = 0; i < this.store.length; ++i) {
const key = this.store.key(i);
const userId = key.substr(prefix.length);
const userId = key.slice(prefix.length);
if (key.startsWith(prefix)) devices[userId] = getJsonItem(this.store, key);
}
return devices;
@@ -125,7 +125,7 @@ WebStorageSessionStore.prototype = {
const deviceKeys = getKeysWithPrefix(this.store, keyEndToEndSessions(''));
const results = {};
for (const k of deviceKeys) {
const unprefixedKey = k.substr(keyEndToEndSessions('').length);
const unprefixedKey = k.slice(keyEndToEndSessions('').length);
results[unprefixedKey] = getJsonItem(this.store, k);
}
return results;
@@ -158,8 +158,8 @@ WebStorageSessionStore.prototype = {
// (hence 43 characters long).
result.push({
senderKey: key.substr(prefix.length, 43),
sessionId: key.substr(prefix.length + 44),
senderKey: key.slice(prefix.length, prefix.length + 43),
sessionId: key.slice(prefix.length + 44),
});
}
return result;
@@ -182,7 +182,7 @@ WebStorageSessionStore.prototype = {
const roomKeys = getKeysWithPrefix(this.store, keyEndToEndRoom(''));
const results = {};
for (const k of roomKeys) {
const unprefixedKey = k.substr(keyEndToEndRoom('').length);
const unprefixedKey = k.slice(keyEndToEndRoom('').length);
results[unprefixedKey] = getJsonItem(this.store, k);
}
return results;
+5 -4
View File
@@ -22,11 +22,12 @@ limitations under the License.
import { EventType } from "../@types/event";
import { Room } from "../models/room";
import { User } from "../models/user";
import { IEvent, MatrixEvent } from "../models/event";
import { MatrixEvent } from "../models/event";
import { Filter } from "../filter";
import { ISavedSync, IStore } from "./index";
import { RoomSummary } from "../models/room-summary";
import { ISyncResponse } from "../sync-accumulator";
import { IStateEventWithRoomId } from "../@types/search";
/**
* Construct a stub store. This does no-ops on most store methods.
@@ -122,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[] {
@@ -242,11 +243,11 @@ export class StubStore implements IStore {
return Promise.resolve();
}
public getOutOfBandMembers(): Promise<IEvent[]> {
public getOutOfBandMembers(): Promise<IStateEventWithRoomId[]> {
return Promise.resolve(null);
}
public setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
public setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void> {
return Promise.resolve();
}
+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
);
});
+7 -21
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,
);
@@ -1170,7 +1170,6 @@ export class SyncApi {
stateEvents.forEach(function(e) {
client.emit(ClientEvent.Event, e);
});
room.updateMyMembership("invite");
});
// Handle joins
@@ -1296,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);
@@ -1317,8 +1306,6 @@ export class SyncApi {
client.emit(ClientEvent.Event, e);
});
room.updateMyMembership("join");
// Decrypt only the last message in all rooms to make sure we can generate a preview
// And decrypt all events after the recorded read receipt to ensure an accurate
// notification count
@@ -1352,8 +1339,6 @@ export class SyncApi {
accountDataEvents.forEach(function(e) {
client.emit(ClientEvent.Event, e);
});
room.updateMyMembership("leave");
});
// update the notification timeline, if appropriate.
@@ -1405,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> {
@@ -1641,6 +1626,7 @@ export class SyncApi {
// This also needs to be done before running push rules on the events as they need
// to be decorated with sender etc.
room.addLiveEvents(timelineEventList || [], null, fromCache);
this.client.processBeaconEvents(room, timelineEventList);
}
/**
+26 -6
View File
@@ -23,7 +23,9 @@ 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";
/**
* Encode a dictionary of query parameters.
@@ -425,7 +427,7 @@ export function globToRegexp(glob: string, extended?: any): string {
export function ensureNoTrailingSlash(url: string): string {
if (url && url.endsWith("/")) {
return url.substr(0, url.length - 1);
return url.slice(0, -1);
} else {
return url;
}
@@ -462,8 +464,8 @@ export function defer<T = void>(): IDeferred<T> {
}
export async function promiseMapSeries<T>(
promises: T[],
fn: (t: T) => void,
promises: Array<T | Promise<T>>,
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);
@@ -471,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
@@ -674,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();
@@ -708,3 +716,15 @@ export function recursivelyAssign(target: Object, source: Object, ignoreNullish
}
return target;
}
function getContentTimestampWithFallback(event: MatrixEvent): number {
return M_TIMESTAMP.findIn<number>(event.getContent()) ?? -1;
}
/**
* Sort events by their content m.ts property
* Latest timestamp first
*/
export function sortEventsByLatestContentTimestamp(left: MatrixEvent, right: MatrixEvent): number {
return getContentTimestampWithFallback(right) - getContentTimestampWithFallback(left);
}
+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();
+586 -529
View File
File diff suppressed because it is too large Load Diff