summaryrefslogtreecommitdiff
path: root/includes/external/matrix/node_modules/matrix-js-sdk/src
diff options
context:
space:
mode:
authorRaindropsSys <raindrops@equestria.dev>2023-11-17 23:25:29 +0100
committerRaindropsSys <raindrops@equestria.dev>2023-11-17 23:25:29 +0100
commit953ddd82e48dd206cef5ac94456549aed13b3ad5 (patch)
tree8f003106ee2e7f422e5a22d2ee04d0db302e66c0 /includes/external/matrix/node_modules/matrix-js-sdk/src
parent62a9199846b0c07c03218703b33e8385764f42d9 (diff)
downloadpluralconnect-953ddd82e48dd206cef5ac94456549aed13b3ad5.tar.gz
pluralconnect-953ddd82e48dd206cef5ac94456549aed13b3ad5.tar.bz2
pluralconnect-953ddd82e48dd206cef5ac94456549aed13b3ad5.zip
Updated 30 files and deleted 2976 files (automated)
Diffstat (limited to 'includes/external/matrix/node_modules/matrix-js-sdk/src')
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/@types/IIdentityServerProvider.ts24
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/@types/PushRules.ts209
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/@types/another-json.d.ts19
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/@types/auth.ts117
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/@types/beacon.ts140
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/@types/crypto.ts99
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/@types/event.ts251
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/@types/extensible_events.ts151
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/@types/global.d.ts99
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/@types/local_notifications.ts19
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/@types/location.ts92
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/@types/partials.ts89
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/@types/polls.ts119
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/@types/read_receipts.ts61
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/@types/requests.ts243
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/@types/search.ts119
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/@types/signed.ts25
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/@types/spaces.ts37
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/@types/synapse.ts40
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/@types/sync.ts27
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/@types/threepids.ts29
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/@types/topic.ts63
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/@types/uia.ts29
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/NamespacedValue.ts120
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/ReEmitter.ts91
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/ToDeviceMessageQueue.ts148
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/autodiscovery.ts472
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/browser-index.ts47
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/client.ts9680
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/common-crypto/CryptoBackend.ts170
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/common-crypto/README.md4
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/content-helpers.ts288
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/content-repo.ts79
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto-api.ts75
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/CrossSigning.ts803
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/DeviceList.ts989
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/EncryptionSetup.ts356
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/OlmDevice.ts1496
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/OutgoingRoomKeyRequestManager.ts485
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/RoomList.ts63
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/SecretStorage.ts583
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/aes.ts157
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/base.ts268
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/index.ts20
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/megolm.ts2208
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/olm.ts329
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/api.ts127
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/backup.ts813
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/crypto.ts50
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/dehydration.ts271
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/deviceinfo.ts161
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/index.ts3936
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/key_passphrase.ts93
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/keybackup.ts77
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/olmlib.ts566
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/recoverykey.ts62
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/base.ts226
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store-backend.ts1062
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store.ts708
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/localStorage-crypto-store.ts403
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/memory-crypto-store.ts533
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/Base.ts369
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/Error.ts76
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/IllegalMethod.ts50
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/QRCode.ts311
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/SAS.ts492
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/SASDecimal.ts37
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/Channel.ts34
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/InRoomChannel.ts356
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/ToDeviceChannel.ts354
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/VerificationRequest.ts926
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/embedded.ts347
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/errors.ts53
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/event-mapper.ts97
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/ExtensibleEvent.ts58
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/InvalidEventError.ts24
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/MessageEvent.ts145
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/PollEndEvent.ts97
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/PollResponseEvent.ts143
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/PollStartEvent.ts207
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/utilities.ts35
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/feature.ts75
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/filter-component.ts204
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/filter.ts242
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/errors.ts84
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/fetch.ts311
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/index.ts191
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/interface.ts147
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/method.ts22
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/prefix.ts48
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/utils.ts153
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/index.ts25
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/indexeddb-helpers.ts50
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/indexeddb-worker.ts24
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/interactive-auth.ts617
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/logger.ts82
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/matrix.ts110
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/MSC3089Branch.ts258
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/MSC3089TreeSpace.ts566
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/ToDeviceMessage.ts38
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/beacon.ts209
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-context.ts110
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-status.ts39
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline-set.ts906
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline.ts458
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/event.ts1631
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/invites-ignorer.ts368
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/poll.ts268
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/read-receipt.ts312
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/related-relations.ts39
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/relations-container.ts146
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/relations.ts368
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-member.ts453
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-state.ts1081
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-summary.ts44
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/room.ts3487
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/search-result.ts54
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/thread.ts669
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/typed-event-emitter.ts114
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/user.ts281
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/pushprocessor.ts770
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/randomstring.ts42
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/realtime-callbacks.ts191
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/MSC3906Rendezvous.ts264
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousChannel.ts48
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousCode.ts25
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousError.ts23
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousFailureReason.ts31
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousIntent.ts20
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousTransport.ts58
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts259
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/channels/index.ts17
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/index.ts23
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts193
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/transports/index.ts17
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/room-hierarchy.ts152
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/KeyClaimManager.ts77
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/OutgoingRequestProcessor.ts116
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/RoomEncryptor.ts153
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/browserify-index.ts31
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/constants.ts18
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/index.ts45
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/rust-crypto.ts334
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/scheduler.ts335
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/secret-storage.ts88
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/service-types.ts20
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/sliding-sync-sdk.ts1027
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/sliding-sync.ts961
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/store/index.ts248
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-backend.ts40
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-local-backend.ts597
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-remote-backend.ts203
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-store-worker.ts157
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb.ts383
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/store/local-storage-events-emitter.ts46
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/store/memory.ts436
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/store/stub.ts267
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/sync-accumulator.ts715
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/sync.ts1898
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/timeline-window.ts507
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/utils.ts770
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/audioContext.ts44
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/call.ts2962
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/callEventHandler.ts425
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/callEventTypes.ts92
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/callFeed.ts361
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/groupCall.ts1598
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/groupCallEventHandler.ts232
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/mediaHandler.ts469
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/connectionStats.ts47
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/connectionStatsReporter.ts28
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/groupCallStats.ts64
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/media/mediaSsrcHandler.ts57
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/media/mediaTrackHandler.ts71
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/media/mediaTrackStats.ts104
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/media/mediaTrackStatsHandler.ts86
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/statsReport.ts56
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/statsReportBuilder.ts110
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/statsReportEmitter.ts33
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/statsReportGatherer.ts183
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/statsValueFormatter.ts27
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/trackStatsReporter.ts117
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/transportStats.ts26
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/transportStatsReporter.ts48
184 files changed, 0 insertions, 67460 deletions
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/IIdentityServerProvider.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/IIdentityServerProvider.ts
deleted file mode 100644
index 8e30497..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/IIdentityServerProvider.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
-Copyright 2021 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.
-*/
-
-export interface IIdentityServerProvider {
- /**
- * Gets an access token for use against the identity server,
- * for the associated client.
- * @returns Promise which resolves to the access token.
- */
- getAccessToken(): Promise<string | null>;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/PushRules.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/PushRules.ts
deleted file mode 100644
index da3b01b..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/PushRules.ts
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
-Copyright 2021 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.
-*/
-
-// allow camelcase as these are things that go onto the wire
-/* eslint-disable camelcase */
-
-export enum PushRuleActionName {
- DontNotify = "dont_notify",
- Notify = "notify",
- Coalesce = "coalesce",
-}
-
-export enum TweakName {
- Highlight = "highlight",
- Sound = "sound",
-}
-
-export type Tweak<N extends TweakName, V> = {
- set_tweak: N;
- value?: V;
-};
-
-export type TweakHighlight = Tweak<TweakName.Highlight, boolean>;
-export type TweakSound = Tweak<TweakName.Sound, string>;
-
-export type Tweaks = TweakHighlight | TweakSound;
-
-export enum ConditionOperator {
- ExactEquals = "==",
- LessThan = "<",
- GreaterThan = ">",
- GreaterThanOrEqual = ">=",
- LessThanOrEqual = "<=",
-}
-
-export type PushRuleAction = Tweaks | PushRuleActionName;
-
-export type MemberCountCondition<N extends number, Op extends ConditionOperator = ConditionOperator.ExactEquals> =
- | `${Op}${N}`
- | (Op extends ConditionOperator.ExactEquals ? `${N}` : never);
-
-export type AnyMemberCountCondition = MemberCountCondition<number, ConditionOperator>;
-
-export const DMMemberCountCondition: MemberCountCondition<2> = "2";
-
-export function isDmMemberCountCondition(condition: AnyMemberCountCondition): boolean {
- return condition === "==2" || condition === "2";
-}
-
-export enum ConditionKind {
- EventMatch = "event_match",
- EventPropertyIs = "event_property_is",
- EventPropertyContains = "event_property_contains",
- ContainsDisplayName = "contains_display_name",
- RoomMemberCount = "room_member_count",
- SenderNotificationPermission = "sender_notification_permission",
- CallStarted = "call_started",
- CallStartedPrefix = "org.matrix.msc3914.call_started",
-}
-
-export interface IPushRuleCondition<N extends ConditionKind | string> {
- [k: string]: any; // for custom conditions, there can be other fields here
- kind: N;
-}
-
-export interface IEventMatchCondition extends IPushRuleCondition<ConditionKind.EventMatch> {
- key: string;
- pattern?: string;
- // Note that value property is an optimization for patterns which do not do
- // any globbing and when the key is not "content.body".
- value?: string;
-}
-
-export interface IEventPropertyIsCondition extends IPushRuleCondition<ConditionKind.EventPropertyIs> {
- key: string;
- value: string | boolean | null | number;
-}
-
-export interface IEventPropertyContainsCondition extends IPushRuleCondition<ConditionKind.EventPropertyContains> {
- key: string;
- value: string | boolean | null | number;
-}
-
-export interface IContainsDisplayNameCondition extends IPushRuleCondition<ConditionKind.ContainsDisplayName> {
- // no additional fields
-}
-
-export interface IRoomMemberCountCondition extends IPushRuleCondition<ConditionKind.RoomMemberCount> {
- is: AnyMemberCountCondition;
-}
-
-export interface ISenderNotificationPermissionCondition
- extends IPushRuleCondition<ConditionKind.SenderNotificationPermission> {
- key: string;
-}
-
-export interface ICallStartedCondition extends IPushRuleCondition<ConditionKind.CallStarted> {
- // no additional fields
-}
-
-export interface ICallStartedPrefixCondition extends IPushRuleCondition<ConditionKind.CallStartedPrefix> {
- // no additional fields
-}
-
-// XXX: custom conditions are possible but always fail, and break the typescript discriminated union so ignore them here
-// IPushRuleCondition<Exclude<string, ConditionKind>> unfortunately does not resolve this at the time of writing.
-export type PushRuleCondition =
- | IEventMatchCondition
- | IEventPropertyIsCondition
- | IEventPropertyContainsCondition
- | IContainsDisplayNameCondition
- | IRoomMemberCountCondition
- | ISenderNotificationPermissionCondition
- | ICallStartedCondition
- | ICallStartedPrefixCondition;
-
-export enum PushRuleKind {
- Override = "override",
- ContentSpecific = "content",
- RoomSpecific = "room",
- SenderSpecific = "sender",
- Underride = "underride",
-}
-
-export enum RuleId {
- Master = ".m.rule.master",
- IsUserMention = ".org.matrix.msc3952.is_user_mention",
- IsRoomMention = ".org.matrix.msc3952.is_room_mention",
- ContainsDisplayName = ".m.rule.contains_display_name",
- ContainsUserName = ".m.rule.contains_user_name",
- AtRoomNotification = ".m.rule.roomnotif",
- DM = ".m.rule.room_one_to_one",
- EncryptedDM = ".m.rule.encrypted_room_one_to_one",
- Message = ".m.rule.message",
- EncryptedMessage = ".m.rule.encrypted",
- InviteToSelf = ".m.rule.invite_for_me",
- MemberEvent = ".m.rule.member_event",
- IncomingCall = ".m.rule.call",
- SuppressNotices = ".m.rule.suppress_notices",
- Tombstone = ".m.rule.tombstone",
- PollStart = ".m.rule.poll_start",
- PollStartUnstable = ".org.matrix.msc3930.rule.poll_start",
- PollEnd = ".m.rule.poll_end",
- PollEndUnstable = ".org.matrix.msc3930.rule.poll_end",
- PollStartOneToOne = ".m.rule.poll_start_one_to_one",
- PollStartOneToOneUnstable = ".org.matrix.msc3930.rule.poll_start_one_to_one",
- PollEndOneToOne = ".m.rule.poll_end_one_to_one",
- PollEndOneToOneUnstable = ".org.matrix.msc3930.rule.poll_end_one_to_one",
-}
-
-export type PushRuleSet = {
- [k in PushRuleKind]?: IPushRule[];
-};
-
-export interface IPushRule {
- actions: PushRuleAction[];
- conditions?: PushRuleCondition[];
- default: boolean;
- enabled: boolean;
- pattern?: string;
- rule_id: RuleId | string;
-}
-
-export interface IAnnotatedPushRule extends IPushRule {
- kind: PushRuleKind;
-}
-
-export interface IPushRules {
- global: PushRuleSet;
- device?: PushRuleSet;
-}
-
-export interface IPusher {
- "app_display_name": string;
- "app_id": string;
- "data": {
- format?: string;
- url?: string; // TODO: Required if kind==http
- brand?: string; // TODO: For email notifications only? Unspecced field
- };
- "device_display_name": string;
- "kind": "http" | string;
- "lang": string;
- "profile_tag"?: string;
- "pushkey": string;
- "enabled"?: boolean | null;
- "org.matrix.msc3881.enabled"?: boolean | null;
- "device_id"?: string | null;
- "org.matrix.msc3881.device_id"?: string | null;
-}
-
-export interface IPusherRequest extends Omit<IPusher, "device_id" | "org.matrix.msc3881.device_id"> {
- append?: boolean;
-}
-
-/* eslint-enable camelcase */
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/another-json.d.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/another-json.d.ts
deleted file mode 100644
index 070332a..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/another-json.d.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
-Copyright 2021 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.
-*/
-
-declare module "another-json" {
- export function stringify(o: object): string;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/auth.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/auth.ts
deleted file mode 100644
index 2b8f5d7..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/auth.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { UnstableValue } from "../NamespacedValue";
-
-// disable lint because these are wire responses
-/* eslint-disable camelcase */
-
-/**
- * Represents a response to the CSAPI `/refresh` endpoint.
- */
-export interface IRefreshTokenResponse {
- access_token: string;
- expires_in_ms: number;
- refresh_token: string;
-}
-
-/* eslint-enable camelcase */
-
-/**
- * Response to GET login flows as per https://spec.matrix.org/v1.3/client-server-api/#get_matrixclientv3login
- */
-export interface ILoginFlowsResponse {
- flows: LoginFlow[];
-}
-
-export type LoginFlow = ISSOFlow | IPasswordFlow | ILoginFlow;
-
-export interface ILoginFlow {
- type: string;
-}
-
-export interface IPasswordFlow extends ILoginFlow {
- type: "m.login.password";
-}
-
-export const DELEGATED_OIDC_COMPATIBILITY = new UnstableValue(
- "delegated_oidc_compatibility",
- "org.matrix.msc3824.delegated_oidc_compatibility",
-);
-
-/**
- * Representation of SSO flow as per https://spec.matrix.org/v1.3/client-server-api/#client-login-via-sso
- */
-export interface ISSOFlow extends ILoginFlow {
- type: "m.login.sso" | "m.login.cas";
- // eslint-disable-next-line camelcase
- identity_providers?: IIdentityProvider[];
- [DELEGATED_OIDC_COMPATIBILITY.name]?: boolean;
- [DELEGATED_OIDC_COMPATIBILITY.altName]?: boolean;
-}
-
-export enum IdentityProviderBrand {
- Gitlab = "gitlab",
- Github = "github",
- Apple = "apple",
- Google = "google",
- Facebook = "facebook",
- Twitter = "twitter",
-}
-
-export interface IIdentityProvider {
- id: string;
- name: string;
- icon?: string;
- brand?: IdentityProviderBrand | string;
-}
-
-/**
- * Parameters to login request as per https://spec.matrix.org/v1.3/client-server-api/#login
- */
-/* eslint-disable camelcase */
-export interface ILoginParams {
- identifier?: object;
- password?: string;
- token?: string;
- device_id?: string;
- initial_device_display_name?: string;
-}
-/* eslint-enable camelcase */
-
-export enum SSOAction {
- /** The user intends to login to an existing account */
- LOGIN = "login",
-
- /** The user intends to register for a new account */
- REGISTER = "register",
-}
-
-/**
- * The result of a successful [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882)
- * `m.login.token` issuance request.
- * Note that this is UNSTABLE and subject to breaking changes without notice.
- */
-export interface LoginTokenPostResponse {
- /**
- * The token to use with `m.login.token` to authenticate.
- */
- login_token: string;
- /**
- * Expiration in seconds.
- */
- expires_in: number;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/beacon.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/beacon.ts
deleted file mode 100644
index e6bfb8f..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/beacon.ts
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
-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 { RelatesToRelationship, REFERENCE_RELATION } from "./extensible_events";
-import { UnstableValue } from "../NamespacedValue";
-import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location";
-
-/**
- * Beacon info and beacon event types as described in MSC3672
- * https://github.com/matrix-org/matrix-spec-proposals/pull/3672
- */
-
-/**
- * Beacon info events are state events.
- * We have two requirements for these events:
- * 1. they can only be written by their owner
- * 2. a user can have an arbitrary number of beacon_info events
- *
- * 1. is achieved by setting the state_key to the owners mxid.
- * Event keys in room state are a combination of `type` + `state_key`.
- * To achieve an arbitrary number of only owner-writable state events
- * we introduce a variable suffix to the event type
- *
- * @example
- * ```
- * {
- * "type": "m.beacon_info.@matthew:matrix.org.1",
- * "state_key": "@matthew:matrix.org",
- * "content": {
- * "m.beacon_info": {
- * "description": "The Matthew Tracker",
- * "timeout": 86400000,
- * },
- * // more content as described below
- * }
- * },
- * {
- * "type": "m.beacon_info.@matthew:matrix.org.2",
- * "state_key": "@matthew:matrix.org",
- * "content": {
- * "m.beacon_info": {
- * "description": "Another different Matthew tracker",
- * "timeout": 400000,
- * },
- * // more content as described below
- * }
- * }
- * ```
- */
-
-/**
- * Non-variable type for m.beacon_info event content
- */
-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;
- // how long from the last event until we consider the beacon inactive in milliseconds
- timeout: number;
- // true when this is a live location beacon
- // https://github.com/matrix-org/matrix-spec-proposals/pull/3672
- live?: boolean;
-};
-
-/**
- * m.beacon_info Event example from the spec
- * https://github.com/matrix-org/matrix-spec-proposals/pull/3672
- * @example
- * ```
- * {
- * "type": "m.beacon_info",
- * "state_key": "@matthew:matrix.org",
- * "content": {
- * "m.beacon_info": {
- * "description": "The Matthew Tracker", // same as an `m.location` description
- * "timeout": 86400000, // how long from the last event until we consider the beacon inactive in milliseconds
- * },
- * "m.ts": 1436829458432, // creation timestamp of the beacon on the client
- * "m.asset": {
- * "type": "m.self" // the type of asset being tracked as per MSC3488
- * }
- * }
- * }
- * ```
- */
-
-/**
- * m.beacon_info.* event content
- */
-export type MBeaconInfoEventContent = MBeaconInfoContent &
- // creation timestamp of the beacon on the client
- MTimestampEvent &
- // the type of asset being tracked as per MSC3488
- MAssetEvent;
-
-/**
- * m.beacon event example
- * https://github.com/matrix-org/matrix-spec-proposals/pull/3672
- * @example
- * ```
- * {
- * "type": "m.beacon",
- * "sender": "@matthew:matrix.org",
- * "content": {
- * "m.relates_to": { // from MSC2674: https://github.com/matrix-org/matrix-doc/pull/2674
- * "rel_type": "m.reference", // from MSC3267: https://github.com/matrix-org/matrix-doc/pull/3267
- * "event_id": "$beacon_info"
- * },
- * "m.location": {
- * "uri": "geo:51.5008,0.1247;u=35",
- * "description": "Arbitrary beacon information"
- * },
- * "m.ts": 1636829458432,
- * }
- * }
- * ```
- */
-
-/**
- * Content of an m.beacon event
- */
-export type MBeaconEventContent = MLocationEvent &
- // timestamp when location was taken
- MTimestampEvent &
- // relates to a beacon_info event
- RelatesToRelationship<typeof REFERENCE_RELATION>;
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/crypto.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/crypto.ts
deleted file mode 100644
index 7711840..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/crypto.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
-Copyright 2022-2023 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 type { IClearEvent } from "../models/event";
-import type { ISignatures } from "./signed";
-
-export type OlmGroupSessionExtraData = {
- untrusted?: boolean;
- sharedHistory?: boolean;
-};
-
-/**
- * The result of a (successful) call to {@link Crypto.decryptEvent}
- */
-export interface IEventDecryptionResult {
- /**
- * The plaintext payload for the event (typically containing <tt>type</tt> and <tt>content</tt> fields).
- */
- clearEvent: IClearEvent;
- /**
- * List of curve25519 keys involved in telling us about the senderCurve25519Key and claimedEd25519Key.
- * See {@link MatrixEvent#getForwardingCurve25519KeyChain}.
- */
- forwardingCurve25519KeyChain?: string[];
- /**
- * Key owned by the sender of this event. See {@link MatrixEvent#getSenderKey}.
- */
- senderCurve25519Key?: string;
- /**
- * ed25519 key claimed by the sender of this event. See {@link MatrixEvent#getClaimedEd25519Key}.
- */
- claimedEd25519Key?: string;
- untrusted?: boolean;
- /**
- * The sender doesn't authorize the unverified devices to decrypt his messages
- */
- encryptedDisabledForUnverifiedDevices?: boolean;
-}
-
-interface Extensible {
- [key: string]: any;
-}
-
-/* eslint-disable camelcase */
-
-/** The result of a call to {@link MatrixClient.exportRoomKeys} */
-export interface IMegolmSessionData extends Extensible {
- /** Sender's Curve25519 device key */
- sender_key: string;
- /** Devices which forwarded this session to us (normally empty). */
- forwarding_curve25519_key_chain: string[];
- /** Other keys the sender claims. */
- sender_claimed_keys: Record<string, string>;
- /** Room this session is used in */
- room_id: string;
- /** Unique id for the session */
- session_id: string;
- /** Base64'ed key data */
- session_key: string;
- algorithm?: string;
- untrusted?: boolean;
-}
-
-/* eslint-enable camelcase */
-
-/** the type of the `device_keys` parameter on `/_matrix/client/v3/keys/upload`
- *
- * @see https://spec.matrix.org/v1.5/client-server-api/#post_matrixclientv3keysupload
- */
-export interface IDeviceKeys {
- algorithms: Array<string>;
- device_id: string; // eslint-disable-line camelcase
- user_id: string; // eslint-disable-line camelcase
- keys: Record<string, string>;
- signatures?: ISignatures;
-}
-
-/** the type of the `one_time_keys` and `fallback_keys` parameters on `/_matrix/client/v3/keys/upload`
- *
- * @see https://spec.matrix.org/v1.5/client-server-api/#post_matrixclientv3keysupload
- */
-export interface IOneTimeKey {
- key: string;
- fallback?: boolean;
- signatures?: ISignatures;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/event.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/event.ts
deleted file mode 100644
index 17af8df..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/event.ts
+++ /dev/null
@@ -1,251 +0,0 @@
-/*
-Copyright 2020 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { UnstableValue } from "../NamespacedValue";
-
-export enum EventType {
- // Room state events
- RoomCanonicalAlias = "m.room.canonical_alias",
- RoomCreate = "m.room.create",
- RoomJoinRules = "m.room.join_rules",
- RoomMember = "m.room.member",
- RoomThirdPartyInvite = "m.room.third_party_invite",
- RoomPowerLevels = "m.room.power_levels",
- RoomName = "m.room.name",
- RoomTopic = "m.room.topic",
- RoomAvatar = "m.room.avatar",
- RoomPinnedEvents = "m.room.pinned_events",
- RoomEncryption = "m.room.encryption",
- RoomHistoryVisibility = "m.room.history_visibility",
- RoomGuestAccess = "m.room.guest_access",
- RoomServerAcl = "m.room.server_acl",
- RoomTombstone = "m.room.tombstone",
- RoomPredecessor = "org.matrix.msc3946.room_predecessor",
-
- SpaceChild = "m.space.child",
- SpaceParent = "m.space.parent",
-
- // Room timeline events
- RoomRedaction = "m.room.redaction",
- RoomMessage = "m.room.message",
- RoomMessageEncrypted = "m.room.encrypted",
- Sticker = "m.sticker",
- CallInvite = "m.call.invite",
- CallCandidates = "m.call.candidates",
- CallAnswer = "m.call.answer",
- CallHangup = "m.call.hangup",
- CallReject = "m.call.reject",
- CallSelectAnswer = "m.call.select_answer",
- CallNegotiate = "m.call.negotiate",
- CallSDPStreamMetadataChanged = "m.call.sdp_stream_metadata_changed",
- CallSDPStreamMetadataChangedPrefix = "org.matrix.call.sdp_stream_metadata_changed",
- CallReplaces = "m.call.replaces",
- CallAssertedIdentity = "m.call.asserted_identity",
- CallAssertedIdentityPrefix = "org.matrix.call.asserted_identity",
- KeyVerificationRequest = "m.key.verification.request",
- KeyVerificationStart = "m.key.verification.start",
- KeyVerificationCancel = "m.key.verification.cancel",
- KeyVerificationMac = "m.key.verification.mac",
- KeyVerificationDone = "m.key.verification.done",
- KeyVerificationKey = "m.key.verification.key",
- KeyVerificationAccept = "m.key.verification.accept",
- // Not used directly - see READY_TYPE in VerificationRequest.
- KeyVerificationReady = "m.key.verification.ready",
- // use of this is discouraged https://matrix.org/docs/spec/client_server/r0.6.1#m-room-message-feedback
- RoomMessageFeedback = "m.room.message.feedback",
- Reaction = "m.reaction",
- PollStart = "org.matrix.msc3381.poll.start",
-
- // Room ephemeral events
- Typing = "m.typing",
- Receipt = "m.receipt",
- Presence = "m.presence",
-
- // Room account_data events
- FullyRead = "m.fully_read",
- Tag = "m.tag",
- SpaceOrder = "org.matrix.msc3230.space_order", // MSC3230
-
- // User account_data events
- PushRules = "m.push_rules",
- Direct = "m.direct",
- IgnoredUserList = "m.ignored_user_list",
-
- // to_device events
- RoomKey = "m.room_key",
- RoomKeyRequest = "m.room_key_request",
- ForwardedRoomKey = "m.forwarded_room_key",
- Dummy = "m.dummy",
-
- // Group call events
- GroupCallPrefix = "org.matrix.msc3401.call",
- GroupCallMemberPrefix = "org.matrix.msc3401.call.member",
-}
-
-export enum RelationType {
- Annotation = "m.annotation",
- Replace = "m.replace",
- Reference = "m.reference",
- Thread = "m.thread",
-}
-
-export enum MsgType {
- Text = "m.text",
- Emote = "m.emote",
- Notice = "m.notice",
- Image = "m.image",
- File = "m.file",
- Audio = "m.audio",
- Location = "m.location",
- Video = "m.video",
- KeyVerificationRequest = "m.key.verification.request",
-}
-
-export const RoomCreateTypeField = "type";
-
-export enum RoomType {
- Space = "m.space",
- UnstableCall = "org.matrix.msc3417.call",
- ElementVideo = "io.element.video",
-}
-
-export const ToDeviceMessageId = "org.matrix.msgid";
-
-/**
- * Identifier for an [MSC3088](https://github.com/matrix-org/matrix-doc/pull/3088)
- * room purpose. Note that this reference is UNSTABLE and subject to breaking changes,
- * including its eventual removal.
- */
-export const UNSTABLE_MSC3088_PURPOSE = new UnstableValue("m.room.purpose", "org.matrix.msc3088.purpose");
-
-/**
- * Enabled flag for an [MSC3088](https://github.com/matrix-org/matrix-doc/pull/3088)
- * room purpose. Note that this reference is UNSTABLE and subject to breaking changes,
- * including its eventual removal.
- */
-export const UNSTABLE_MSC3088_ENABLED = new UnstableValue("m.enabled", "org.matrix.msc3088.enabled");
-
-/**
- * Subtype for an [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) space-room.
- * Note that this reference is UNSTABLE and subject to breaking changes, including its
- * eventual removal.
- */
-export const UNSTABLE_MSC3089_TREE_SUBTYPE = new UnstableValue("m.data_tree", "org.matrix.msc3089.data_tree");
-
-/**
- * Leaf type for an event in a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) space-room.
- * Note that this reference is UNSTABLE and subject to breaking changes, including its
- * eventual removal.
- */
-export const UNSTABLE_MSC3089_LEAF = new UnstableValue("m.leaf", "org.matrix.msc3089.leaf");
-
-/**
- * Branch (Leaf Reference) type for the index approach in a
- * [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) space-room. Note that this reference is
- * UNSTABLE and subject to breaking changes, including its eventual removal.
- */
-export const UNSTABLE_MSC3089_BRANCH = new UnstableValue("m.branch", "org.matrix.msc3089.branch");
-
-/**
- * Marker event type to point back at imported historical content in a room. See
- * [MSC2716](https://github.com/matrix-org/matrix-spec-proposals/pull/2716).
- * Note that this reference is UNSTABLE and subject to breaking changes,
- * including its eventual removal.
- */
-export const UNSTABLE_MSC2716_MARKER = new UnstableValue("m.room.marker", "org.matrix.msc2716.marker");
-
-/**
- * Name of the "with_relations" request property for relation based redactions.
- * {@link https://github.com/matrix-org/matrix-spec-proposals/pull/3912}
- */
-export const MSC3912_RELATION_BASED_REDACTIONS_PROP = new UnstableValue(
- "with_relations",
- "org.matrix.msc3912.with_relations",
-);
-
-/**
- * Functional members type for declaring a purpose of room members (e.g. helpful bots).
- * Note that this reference is UNSTABLE and subject to breaking changes, including its
- * eventual removal.
- *
- * Schema (TypeScript):
- * ```
- * {
- * service_members?: string[]
- * }
- * ```
- *
- * @example
- * ```
- * {
- * "service_members": [
- * "@helperbot:localhost",
- * "@reminderbot:alice.tdl"
- * ]
- * }
- * ```
- */
-export const UNSTABLE_ELEMENT_FUNCTIONAL_USERS = new UnstableValue(
- "io.element.functional_members",
- "io.element.functional_members",
-);
-
-/**
- * A type of message that affects visibility of a message,
- * as per https://github.com/matrix-org/matrix-doc/pull/3531
- *
- * @experimental
- */
-export const EVENT_VISIBILITY_CHANGE_TYPE = new UnstableValue("m.visibility", "org.matrix.msc3531.visibility");
-
-/**
- * https://github.com/matrix-org/matrix-doc/pull/3881
- *
- * @experimental
- */
-export const PUSHER_ENABLED = new UnstableValue("enabled", "org.matrix.msc3881.enabled");
-
-/**
- * https://github.com/matrix-org/matrix-doc/pull/3881
- *
- * @experimental
- */
-export const PUSHER_DEVICE_ID = new UnstableValue("device_id", "org.matrix.msc3881.device_id");
-
-/**
- * https://github.com/matrix-org/matrix-doc/pull/3890
- *
- * @experimental
- */
-export const LOCAL_NOTIFICATION_SETTINGS_PREFIX = new UnstableValue(
- "m.local_notification_settings",
- "org.matrix.msc3890.local_notification_settings",
-);
-
-export interface IEncryptedFile {
- url: string;
- mimetype?: string;
- key: {
- alg: string;
- key_ops: string[]; // eslint-disable-line camelcase
- kty: string;
- k: string;
- ext: boolean;
- };
- iv: string;
- hashes: { [alg: string]: string };
- v: string;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/extensible_events.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/extensible_events.ts
deleted file mode 100644
index db9ea18..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/extensible_events.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
-Copyright 2021 - 2023 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, NamespacedValue, Optional, UnstableValue } from "matrix-events-sdk";
-
-import { isProvided } from "../extensible_events_v1/utilities";
-
-// Types and utilities for MSC1767: Extensible events (version 1) in Matrix
-
-/**
- * Represents the stable and unstable values of a given namespace.
- */
-export type TSNamespace<N> = N extends NamespacedValue<infer S, infer U>
- ? TSNamespaceValue<S> | TSNamespaceValue<U>
- : never;
-
-/**
- * Represents a namespaced value, if the value is a string. Used to extract provided types
- * from a TSNamespace<N> (in cases where only stable *or* unstable is provided).
- */
-export type TSNamespaceValue<V> = V extends string ? V : never;
-
-/**
- * Creates a type which is V when T is `never`, otherwise T.
- */
-// See https://github.com/microsoft/TypeScript/issues/23182#issuecomment-379091887 for details on the array syntax.
-export type DefaultNever<T, V> = [T] extends [never] ? V : T;
-
-/**
- * The namespaced value for m.message
- */
-export const M_MESSAGE = new UnstableValue("m.message", "org.matrix.msc1767.message");
-
-/**
- * An m.message event rendering
- */
-export interface IMessageRendering {
- body: string;
- mimetype?: string;
-}
-
-/**
- * The content for an m.message event
- */
-export type ExtensibleMessageEventContent = EitherAnd<
- { [M_MESSAGE.name]: IMessageRendering[] },
- { [M_MESSAGE.altName]: IMessageRendering[] }
->;
-
-/**
- * The namespaced value for m.text
- */
-export const M_TEXT = new UnstableValue("m.text", "org.matrix.msc1767.text");
-
-/**
- * The content for an m.text event
- */
-export type TextEventContent = EitherAnd<{ [M_TEXT.name]: string }, { [M_TEXT.altName]: string }>;
-
-/**
- * The namespaced value for m.html
- */
-export const M_HTML = new UnstableValue("m.html", "org.matrix.msc1767.html");
-
-/**
- * The content for an m.html event
- */
-export type HtmlEventContent = EitherAnd<{ [M_HTML.name]: string }, { [M_HTML.altName]: string }>;
-
-/**
- * The content for an m.message, m.text, or m.html event
- */
-export type ExtensibleAnyMessageEventContent = ExtensibleMessageEventContent | TextEventContent | HtmlEventContent;
-
-/**
- * The namespaced value for an m.reference relation
- */
-export const REFERENCE_RELATION = new NamespacedValue("m.reference");
-
-/**
- * Represents any relation type
- */
-export type AnyRelation = TSNamespace<typeof REFERENCE_RELATION> | string;
-
-/**
- * An m.relates_to relationship
- */
-export type RelatesToRelationship<R = never, C = never> = {
- "m.relates_to": {
- // See https://github.com/microsoft/TypeScript/issues/23182#issuecomment-379091887 for array syntax
- rel_type: [R] extends [never] ? AnyRelation : TSNamespace<R>;
- event_id: string;
- } & DefaultNever<C, {}>;
-};
-
-/**
- * Partial types for a Matrix Event.
- */
-export interface IPartialEvent<TContent> {
- type: string;
- content: TContent;
-}
-
-/**
- * Represents a potentially namespaced event type.
- */
-export type ExtensibleEventType = NamespacedValue<string, string> | string;
-
-/**
- * Determines if two event types are the same, including namespaces.
- * @param given - The given event type. This will be compared
- * against the expected type.
- * @param expected - The expected event type.
- * @returns True if the given type matches the expected type.
- */
-export function isEventTypeSame(
- given: Optional<ExtensibleEventType>,
- expected: Optional<ExtensibleEventType>,
-): boolean {
- if (typeof given === "string") {
- if (typeof expected === "string") {
- return expected === given;
- } else {
- return (expected as NamespacedValue<string, string>).matches(given as string);
- }
- } else {
- if (typeof expected === "string") {
- return (given as NamespacedValue<string, string>).matches(expected as string);
- } else {
- const expectedNs = expected as NamespacedValue<string, string>;
- const givenNs = given as NamespacedValue<string, string>;
- return (
- expectedNs.matches(givenNs.name) ||
- (isProvided(givenNs.altName) && expectedNs.matches(givenNs.altName!))
- );
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/global.d.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/global.d.ts
deleted file mode 100644
index 749eb7f..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/global.d.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
-Copyright 2020 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.
-*/
-
-// this is needed to tell TS about global.Olm
-import "@matrix-org/olm";
-
-export {};
-
-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;
-
- namespace NodeJS {
- interface Global {
- localStorage: Storage;
- // marker variable used to detect both the browser & node entrypoints being used at once
- __js_sdk_entrypoint: unknown;
- }
- }
-
- interface Window {
- webkitAudioContext: typeof AudioContext;
- }
-
- interface Crypto {
- webkitSubtle?: Window["crypto"]["subtle"];
- }
-
- interface MediaDevices {
- // This is experimental and types don't know about it yet
- // https://github.com/microsoft/TypeScript/issues/33232
- getDisplayMedia(constraints: MediaStreamConstraints | DesktopCapturerConstraints): Promise<MediaStream>;
- getUserMedia(constraints: MediaStreamConstraints | DesktopCapturerConstraints): Promise<MediaStream>;
- }
-
- interface DesktopCapturerConstraints {
- audio:
- | boolean
- | {
- mandatory: {
- chromeMediaSource: string;
- chromeMediaSourceId: string;
- };
- };
- video:
- | boolean
- | {
- mandatory: {
- chromeMediaSource: string;
- chromeMediaSourceId: string;
- };
- };
- }
-
- interface DummyInterfaceWeShouldntBeUsingThis {}
-
- interface Navigator {
- // We check for the webkit-prefixed getUserMedia to detect if we're
- // on webkit: we should check if we still need to do this
- webkitGetUserMedia: DummyInterfaceWeShouldntBeUsingThis;
- }
-
- export interface ISettledFulfilled<T> {
- status: "fulfilled";
- value: T;
- }
- export interface ISettledRejected {
- status: "rejected";
- reason: any;
- }
-
- interface PromiseConstructor {
- allSettled<T>(promises: Promise<T>[]): Promise<Array<ISettledFulfilled<T> | ISettledRejected>>;
- }
-
- interface RTCRtpTransceiver {
- // This has been removed from TS
- // (https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1029),
- // but we still need this for MatrixCall::getRidOfRTXCodecs()
- setCodecPreferences(codecs: RTCRtpCodecCapability[]): void;
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/local_notifications.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/local_notifications.ts
deleted file mode 100644
index b92d986..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/local_notifications.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
-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.
-*/
-
-export interface LocalNotificationSettings {
- is_silenced: boolean;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/location.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/location.ts
deleted file mode 100644
index d1a826f..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/location.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
-Copyright 2021 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.
-*/
-
-// Types for MSC3488 - m.location: Extending events with location data
-import { EitherAnd } from "matrix-events-sdk";
-
-import { UnstableValue } from "../NamespacedValue";
-import { M_TEXT } from "./extensible_events";
-
-export enum LocationAssetType {
- Self = "m.self",
- Pin = "m.pin",
-}
-
-export const M_ASSET = new UnstableValue("m.asset", "org.matrix.msc3488.asset");
-export type MAssetContent = { type: LocationAssetType };
-/**
- * The event definition for an m.asset event (in content)
- */
-export type MAssetEvent = EitherAnd<{ [M_ASSET.name]: MAssetContent }, { [M_ASSET.altName]: MAssetContent }>;
-
-export const M_TIMESTAMP = new UnstableValue("m.ts", "org.matrix.msc3488.ts");
-/**
- * The event definition for an m.ts event (in content)
- */
-export type MTimestampEvent = EitherAnd<{ [M_TIMESTAMP.name]: number }, { [M_TIMESTAMP.altName]: number }>;
-
-export const M_LOCATION = new UnstableValue("m.location", "org.matrix.msc3488.location");
-
-export type MLocationContent = {
- uri: string;
- description?: string | null;
-};
-
-export type MLocationEvent = EitherAnd<
- { [M_LOCATION.name]: MLocationContent },
- { [M_LOCATION.altName]: MLocationContent }
->;
-
-export type MTextEvent = EitherAnd<{ [M_TEXT.name]: string }, { [M_TEXT.altName]: string }>;
-
-/* From the spec at:
- * https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md
-{
- "type": "m.room.message",
- "content": {
- "body": "Matthew was at geo:51.5008,0.1247;u=35 as of Sat Nov 13 18:50:58 2021",
- "msgtype": "m.location",
- "geo_uri": "geo:51.5008,0.1247;u=35",
- "m.location": {
- "uri": "geo:51.5008,0.1247;u=35",
- "description": "Matthew's whereabouts",
- },
- "m.asset": {
- "type": "m.self"
- },
- "m.text": "Matthew was at geo:51.5008,0.1247;u=35 as of Sat Nov 13 18:50:58 2021",
- "m.ts": 1636829458432,
- }
-}
-*/
-type OptionalTimestampEvent = MTimestampEvent | undefined;
-/**
- * The content for an m.location event
- */
-export type MLocationEventContent = MLocationEvent & MAssetEvent & MTextEvent & OptionalTimestampEvent;
-
-export type LegacyLocationEventContent = {
- body: string;
- msgtype: string;
- geo_uri: string;
-};
-
-/**
- * Possible content for location events as sent over the wire
- */
-export type LocationEventWireContent = Partial<LegacyLocationEventContent & MLocationEventContent>;
-
-export type ILocationContent = MLocationEventContent & LegacyLocationEventContent;
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/partials.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/partials.ts
deleted file mode 100644
index 49f92f3..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/partials.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
-Copyright 2021 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.
-*/
-
-export interface IImageInfo {
- size?: number;
- mimetype?: string;
- thumbnail_info?: {
- // eslint-disable-line camelcase
- w?: number;
- h?: number;
- size?: number;
- mimetype?: string;
- };
- w?: number;
- h?: number;
-}
-
-export enum Visibility {
- Public = "public",
- Private = "private",
-}
-
-export enum Preset {
- PrivateChat = "private_chat",
- TrustedPrivateChat = "trusted_private_chat",
- PublicChat = "public_chat",
-}
-
-export type ResizeMethod = "crop" | "scale";
-
-export type IdServerUnbindResult = "no-support" | "success";
-
-// Knock and private are reserved keywords which are not yet implemented.
-export enum JoinRule {
- Public = "public",
- Invite = "invite",
- /**
- * @deprecated Reserved keyword. Should not be used. Not yet implemented.
- */
- Private = "private",
- Knock = "knock",
- Restricted = "restricted",
-}
-
-export enum RestrictedAllowType {
- RoomMembership = "m.room_membership",
-}
-
-export interface IJoinRuleEventContent {
- join_rule: JoinRule; // eslint-disable-line camelcase
- allow?: {
- type: RestrictedAllowType;
- room_id: string; // eslint-disable-line camelcase
- }[];
-}
-
-export enum GuestAccess {
- CanJoin = "can_join",
- Forbidden = "forbidden",
-}
-
-export enum HistoryVisibility {
- Invited = "invited",
- Joined = "joined",
- Shared = "shared",
- WorldReadable = "world_readable",
-}
-
-export interface IUsageLimit {
- // "hs_disabled" is NOT a specced string, but is used in Synapse
- // This is tracked over at https://github.com/matrix-org/synapse/issues/9237
- // eslint-disable-next-line camelcase
- limit_type: "monthly_active_user" | "hs_disabled" | string;
- // eslint-disable-next-line camelcase
- admin_contact?: string;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/polls.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/polls.ts
deleted file mode 100644
index 3b06f93..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/polls.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
-Copyright 2022 - 2023 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, UnstableValue } from "matrix-events-sdk";
-
-import {
- ExtensibleAnyMessageEventContent,
- REFERENCE_RELATION,
- RelatesToRelationship,
- TSNamespace,
-} from "./extensible_events";
-
-/**
- * Identifier for a disclosed poll.
- */
-export const M_POLL_KIND_DISCLOSED = new UnstableValue("m.poll.disclosed", "org.matrix.msc3381.poll.disclosed");
-
-/**
- * Identifier for an undisclosed poll.
- */
-export const M_POLL_KIND_UNDISCLOSED = new UnstableValue("m.poll.undisclosed", "org.matrix.msc3381.poll.undisclosed");
-
-/**
- * Any poll kind.
- */
-export type PollKind = TSNamespace<typeof M_POLL_KIND_DISCLOSED> | TSNamespace<typeof M_POLL_KIND_UNDISCLOSED> | string;
-
-/**
- * Known poll kind namespaces.
- */
-export type KnownPollKind = typeof M_POLL_KIND_DISCLOSED | typeof M_POLL_KIND_UNDISCLOSED;
-
-/**
- * The namespaced value for m.poll.start
- */
-export const M_POLL_START = new UnstableValue("m.poll.start", "org.matrix.msc3381.poll.start");
-
-/**
- * The m.poll.start type within event content
- */
-export type PollStartSubtype = {
- question: ExtensibleAnyMessageEventContent;
- kind: PollKind;
- max_selections?: number; // default 1, always positive
- answers: PollAnswer[];
-};
-
-/**
- * A poll answer.
- */
-export type PollAnswer = ExtensibleAnyMessageEventContent & { id: string };
-
-/**
- * The event definition for an m.poll.start event (in content)
- */
-export type PollStartEvent = EitherAnd<
- { [M_POLL_START.name]: PollStartSubtype },
- { [M_POLL_START.altName]: PollStartSubtype }
->;
-
-/**
- * The content for an m.poll.start event
- */
-export type PollStartEventContent = PollStartEvent & ExtensibleAnyMessageEventContent;
-
-/**
- * The namespaced value for m.poll.response
- */
-export const M_POLL_RESPONSE = new UnstableValue("m.poll.response", "org.matrix.msc3381.poll.response");
-
-/**
- * The m.poll.response type within event content
- */
-export type PollResponseSubtype = {
- answers: string[];
-};
-
-/**
- * The event definition for an m.poll.response event (in content)
- */
-export type PollResponseEvent = EitherAnd<
- { [M_POLL_RESPONSE.name]: PollResponseSubtype },
- { [M_POLL_RESPONSE.altName]: PollResponseSubtype }
->;
-
-/**
- * The content for an m.poll.response event
- */
-export type PollResponseEventContent = PollResponseEvent & RelatesToRelationship<typeof REFERENCE_RELATION>;
-
-/**
- * The namespaced value for m.poll.end
- */
-export const M_POLL_END = new UnstableValue("m.poll.end", "org.matrix.msc3381.poll.end");
-
-/**
- * The event definition for an m.poll.end event (in content)
- */
-export type PollEndEvent = EitherAnd<{ [M_POLL_END.name]: {} }, { [M_POLL_END.altName]: {} }>;
-
-/**
- * The content for an m.poll.end event
- */
-export type PollEndEventContent = PollEndEvent &
- RelatesToRelationship<typeof REFERENCE_RELATION> &
- ExtensibleAnyMessageEventContent;
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/read_receipts.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/read_receipts.ts
deleted file mode 100644
index 7592403..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/read_receipts.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
-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 = "m.read.private",
-}
-
-export const MAIN_ROOM_TIMELINE = "main";
-
-export interface Receipt {
- ts: number;
- thread_id?: string;
-}
-
-export interface WrappedReceipt {
- eventId: string;
- data: Receipt;
-}
-
-export interface CachedReceipt {
- type: ReceiptType;
- userId: string;
- data: Receipt;
-}
-
-export type ReceiptCache = Map<string, CachedReceipt[]>;
-
-export interface ReceiptContent {
- [eventId: string]: {
- [key in ReceiptType | string]: {
- [userId: string]: Receipt;
- };
- };
-}
-
-// We will only hold a synthetic receipt if we do not have a real receipt or the synthetic is newer.
-// map: receipt type → user Id → receipt
-export type Receipts = Map<string, Map<string, [real: WrappedReceipt | null, synthetic: WrappedReceipt | null]>>;
-
-export type CachedReceiptStructure = {
- eventId: string;
- receiptType: string | ReceiptType;
- userId: string;
- receipt: Receipt;
- synthetic: boolean;
-};
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/requests.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/requests.ts
deleted file mode 100644
index 12f4d8e..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/requests.ts
+++ /dev/null
@@ -1,243 +0,0 @@
-/*
-Copyright 2021 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 { IContent, IEvent } from "../models/event";
-import { Preset, Visibility } from "./partials";
-import { IEventWithRoomId, SearchKey } from "./search";
-import { IRoomEventFilter } from "../filter";
-import { Direction } from "../models/event-timeline";
-import { PushRuleAction } from "./PushRules";
-import { IRoomEvent } from "../sync-accumulator";
-import { EventType, RelationType, RoomType } from "./event";
-
-// allow camelcase as these are things that go onto the wire
-/* eslint-disable camelcase */
-
-export interface IJoinRoomOpts {
- /**
- * True to do a room initial sync on the resulting
- * room. If false, the <strong>returned Room object will have no current state.
- * </strong> Default: true.
- */
- syncRoom?: boolean;
-
- /**
- * If the caller has a keypair 3pid invite, the signing URL is passed in this parameter.
- */
- inviteSignUrl?: string;
-
- /**
- * The server names to try and join through in addition to those that are automatically chosen.
- */
- viaServers?: string[];
-}
-
-export interface IRedactOpts {
- reason?: string;
- /**
- * Whether events related to the redacted event should be redacted.
- *
- * If specified, then any events which relate to the event being redacted with
- * any of the relationship types listed will also be redacted.
- *
- * <b>Raises an Error if the server does not support it.</b>
- * Check for server-side support before using this param with
- * <code>client.canSupport.get(Feature.RelationBasedRedactions)</code>.
- * {@link https://github.com/matrix-org/matrix-spec-proposals/pull/3912}
- */
- with_relations?: Array<RelationType | string>;
-}
-
-export interface ISendEventResponse {
- event_id: string;
-}
-
-export interface IPresenceOpts {
- // One of "online", "offline" or "unavailable"
- presence: "online" | "offline" | "unavailable";
- // The status message to attach.
- status_msg?: string;
-}
-
-export interface IPaginateOpts {
- // true to fill backwards, false to go forwards
- backwards?: boolean;
- // number of events to request
- limit?: number;
-}
-
-export interface IGuestAccessOpts {
- /**
- * True to allow guests to join this room. This
- * implicitly gives guests write access. If false or not given, guests are
- * explicitly forbidden from joining the room.
- */
- allowJoin: boolean;
- /**
- * True to set history visibility to
- * be world_readable. This gives guests read access *from this point forward*.
- * If false or not given, history visibility is not modified.
- */
- allowRead: boolean;
-}
-
-export interface ISearchOpts {
- keys?: SearchKey[];
- query: string;
-}
-
-export interface IEventSearchOpts {
- // a JSON filter object to pass in the request
- filter?: IRoomEventFilter;
- // the term to search for
- term: string;
-}
-
-export interface IInvite3PID {
- id_server: string;
- id_access_token?: string; // this gets injected by the js-sdk
- medium: string;
- address: string;
-}
-
-export interface ICreateRoomStateEvent {
- type: string;
- state_key?: string; // defaults to an empty string
- content: IContent;
-}
-
-export interface ICreateRoomOpts {
- // The alias localpart to assign to this room.
- room_alias_name?: string;
- // Either 'public' or 'private'.
- visibility?: Visibility;
- // The name to give this room.
- name?: string;
- // The topic to give this room.
- topic?: string;
- preset?: Preset;
- power_level_content_override?: {
- ban?: number;
- events?: Record<EventType | string, number>;
- events_default?: number;
- invite?: number;
- kick?: number;
- notifications?: Record<string, number>;
- redact?: number;
- state_default?: number;
- users?: Record<string, number>;
- users_default?: number;
- };
- creation_content?: object;
- initial_state?: ICreateRoomStateEvent[];
- // A list of user IDs to invite to this room.
- invite?: string[];
- invite_3pid?: IInvite3PID[];
- is_direct?: boolean;
- room_version?: string;
-}
-
-export interface IRoomDirectoryOptions {
- server?: string;
- limit?: number;
- since?: string;
-
- // Filter parameters
- filter?: {
- // String to search for
- generic_search_term?: string;
- room_types?: Array<RoomType | null>;
- };
- include_all_networks?: boolean;
- third_party_instance_id?: string;
-}
-
-export interface IAddThreePidOnlyBody {
- auth?: {
- type: string;
- session?: string;
- };
- client_secret: string;
- sid: string;
-}
-
-export interface IBindThreePidBody {
- client_secret: string;
- id_server: string;
- id_access_token: string;
- sid: string;
-}
-
-export interface IRelationsRequestOpts {
- from?: string;
- to?: string;
- limit?: number;
- dir?: Direction;
-}
-
-export interface IRelationsResponse {
- chunk: IEvent[];
- next_batch?: string;
- prev_batch?: string;
-}
-
-export interface IContextResponse {
- end: string;
- start: string;
- state: IEventWithRoomId[];
- events_before: IEventWithRoomId[];
- events_after: IEventWithRoomId[];
- event: IEventWithRoomId;
-}
-
-export interface IEventsResponse {
- chunk: IEventWithRoomId[];
- end: string;
- start: string;
-}
-
-export interface INotification {
- actions: PushRuleAction[];
- event: IRoomEvent;
- profile_tag?: string;
- read: boolean;
- room_id: string;
- ts: number;
-}
-
-export interface INotificationsResponse {
- next_token: string;
- notifications: INotification[];
-}
-
-export interface IFilterResponse {
- filter_id: string;
-}
-
-export interface ITagsResponse {
- tags: {
- [tagId: string]: {
- order: number;
- };
- };
-}
-
-export interface IStatusResponse extends IPresenceOpts {
- currently_active?: boolean;
- last_active_ago?: number;
-}
-
-/* eslint-enable camelcase */
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/search.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/search.ts
deleted file mode 100644
index 3a6d4fd..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/search.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
-Copyright 2021 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.
-*/
-
-// Types relating to the /search API
-
-import { IRoomEvent, IStateEvent } from "../sync-accumulator";
-import { IRoomEventFilter } from "../filter";
-import { SearchResult } from "../models/search-result";
-
-/* eslint-disable camelcase */
-export interface IEventWithRoomId extends IRoomEvent {
- room_id: string;
-}
-
-export interface IStateEventWithRoomId extends IStateEvent {
- room_id: string;
-}
-
-export interface IMatrixProfile {
- avatar_url?: string;
- displayname?: string;
-}
-
-export interface IResultContext {
- events_before: IEventWithRoomId[];
- events_after: IEventWithRoomId[];
- profile_info: Record<string, IMatrixProfile>;
- start?: string;
- end?: string;
-}
-
-export interface ISearchResult {
- rank: number;
- result: IEventWithRoomId;
- context: IResultContext;
-}
-
-enum GroupKey {
- RoomId = "room_id",
- Sender = "sender",
-}
-
-export interface IResultRoomEvents {
- count: number;
- highlights: string[];
- results: ISearchResult[];
- state?: { [roomId: string]: IStateEventWithRoomId[] };
- groups?: {
- [groupKey in GroupKey]: {
- [value: string]: {
- next_batch?: string;
- order: number;
- results: string[];
- };
- };
- };
- next_batch?: string;
-}
-
-interface IResultCategories {
- room_events: IResultRoomEvents;
-}
-
-export type SearchKey = "content.body" | "content.name" | "content.topic";
-
-export enum SearchOrderBy {
- Recent = "recent",
- Rank = "rank",
-}
-
-export interface ISearchRequestBody {
- search_categories: {
- room_events: {
- search_term: string;
- keys?: SearchKey[];
- filter?: IRoomEventFilter;
- order_by?: SearchOrderBy;
- event_context?: {
- before_limit?: number;
- after_limit?: number;
- include_profile?: boolean;
- };
- include_state?: boolean;
- groupings?: {
- group_by: {
- key: GroupKey;
- }[];
- };
- };
- };
-}
-
-export interface ISearchResponse {
- search_categories: IResultCategories;
-}
-
-export interface ISearchResults {
- _query?: ISearchRequestBody;
- results: SearchResult[];
- highlights: string[];
- count?: number;
- next_batch?: string;
- pendingRequest?: Promise<ISearchResults>;
- abortSignal?: AbortSignal;
-}
-/* eslint-enable camelcase */
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/signed.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/signed.ts
deleted file mode 100644
index a209f37..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/signed.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
-Copyright 2021 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.
-*/
-
-export interface ISignatures {
- [entity: string]: {
- [keyId: string]: string;
- };
-}
-
-export interface ISigned {
- signatures?: ISignatures;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/spaces.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/spaces.ts
deleted file mode 100644
index 9edab27..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/spaces.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
-Copyright 2021 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 { IPublicRoomsChunkRoom } from "../client";
-import { RoomType } from "./event";
-import { IStrippedState } from "../sync-accumulator";
-
-// Types relating to Rooms of type `m.space` and related APIs
-
-/* eslint-disable camelcase */
-export interface IHierarchyRelation extends IStrippedState {
- origin_server_ts: number;
- content: {
- order?: string;
- suggested?: boolean;
- via?: string[];
- };
-}
-
-export interface IHierarchyRoom extends IPublicRoomsChunkRoom {
- room_type?: RoomType | string;
- children_state: IHierarchyRelation[];
-}
-/* eslint-enable camelcase */
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/synapse.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/synapse.ts
deleted file mode 100644
index 1d4ce41..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/synapse.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
-Copyright 2021 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 { IdServerUnbindResult } from "./partials";
-
-// Types relating to Synapse Admin APIs
-
-/* eslint-disable camelcase */
-export interface ISynapseAdminWhoisResponse {
- user_id: string;
- devices: {
- [deviceId: string]: {
- sessions: {
- connections: {
- ip: string;
- last_seen: number; // millis since epoch
- user_agent: string;
- }[];
- }[];
- };
- };
-}
-
-export interface ISynapseAdminDeactivateResponse {
- id_server_unbind_result: IdServerUnbindResult;
-}
-/* eslint-enable camelcase */
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/sync.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/sync.ts
deleted file mode 100644
index d9a2a6f..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/sync.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
-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 { ServerControlledNamespacedValue } from "../NamespacedValue";
-
-/**
- * https://github.com/matrix-org/matrix-doc/pull/3773
- *
- * @experimental
- */
-export const UNREAD_THREAD_NOTIFICATIONS = new ServerControlledNamespacedValue(
- "unread_thread_notifications",
- "org.matrix.msc3773.unread_thread_notifications",
-);
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/threepids.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/threepids.ts
deleted file mode 100644
index c28ffc3..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/threepids.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
-Copyright 2021 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.
-*/
-
-export enum ThreepidMedium {
- Email = "email",
- Phone = "msisdn",
-}
-
-// TODO: Are these types universal, or specific to just /account/3pid?
-export interface IThreepid {
- medium: ThreepidMedium;
- address: string;
- validated_at: number; // eslint-disable-line camelcase
- added_at: number; // eslint-disable-line camelcase
- bound?: boolean;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/topic.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/topic.ts
deleted file mode 100644
index 04d1464..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/topic.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
-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 } from "matrix-events-sdk";
-
-import { UnstableValue } from "../NamespacedValue";
-import { IMessageRendering } from "./extensible_events";
-
-/**
- * Extensible topic event type based on MSC3765
- * https://github.com/matrix-org/matrix-spec-proposals/pull/3765
- *
- * @example
- * ```
- * {
- * "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;
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/uia.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/uia.ts
deleted file mode 100644
index e611420..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/@types/uia.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
-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 { IAuthData } from "../interactive-auth";
-
-/**
- * Helper type to represent HTTP request body for a UIA enabled endpoint
- */
-export type UIARequest<T> = T & {
- auth?: IAuthData;
-};
-
-/**
- * Helper type to represent HTTP response body for a UIA enabled endpoint
- */
-export type UIAResponse<T> = T | IAuthData;
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/NamespacedValue.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/NamespacedValue.ts
deleted file mode 100644
index a1a7e5d..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/NamespacedValue.ts
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
-Copyright 2021 - 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 { Optional } from "matrix-events-sdk/lib/types";
-
-/**
- * Represents a simple Matrix namespaced value. This will assume that if a stable prefix
- * is provided that the stable prefix should be used when representing the identifier.
- */
-export class NamespacedValue<S extends string, U extends string> {
- // Stable is optional, but one of the two parameters is required, hence the weird-looking types.
- // Goal is to to have developers explicitly say there is no stable value (if applicable).
- public constructor(stable: S, unstable: U);
- public constructor(stable: S, unstable?: U);
- public constructor(stable: null | undefined, unstable: U);
- public constructor(public readonly stable?: S | null, public readonly unstable?: U) {
- if (!this.unstable && !this.stable) {
- throw new Error("One of stable or unstable values must be supplied");
- }
- }
-
- public get name(): U | S {
- if (this.stable) {
- return this.stable;
- }
- return this.unstable!;
- }
-
- public get altName(): U | S | null | undefined {
- if (!this.stable) {
- return null;
- }
- return this.unstable;
- }
-
- public get names(): (U | S)[] {
- const names = [this.name];
- const altName = this.altName;
- if (altName) names.push(altName);
- return names;
- }
-
- public matches(val: string): boolean {
- return this.name === val || this.altName === val;
- }
-
- // this desperately wants https://github.com/microsoft/TypeScript/pull/26349 at the top level of the class
- // so we can instantiate `NamespacedValue<string, _, _>` as a default type for that namespace.
- public findIn<T>(obj: any): Optional<T> {
- let val: T | undefined = undefined;
- if (this.name) {
- val = obj?.[this.name];
- }
- if (!val && this.altName) {
- val = obj?.[this.altName];
- }
- return val;
- }
-
- public includedIn(arr: any[]): boolean {
- let included = false;
- if (this.name) {
- included = arr.includes(this.name);
- }
- if (!included && this.altName) {
- included = arr.includes(this.altName);
- }
- return included;
- }
-}
-
-export class ServerControlledNamespacedValue<S extends string, U extends string> extends NamespacedValue<S, U> {
- private preferUnstable = false;
-
- public setPreferUnstable(preferUnstable: boolean): void {
- this.preferUnstable = preferUnstable;
- }
-
- public get name(): U | S {
- if (this.stable && !this.preferUnstable) {
- return this.stable;
- }
- return this.unstable!;
- }
-}
-
-/**
- * Represents a namespaced value which prioritizes the unstable value over the stable
- * value.
- */
-export class UnstableValue<S extends string, U extends string> extends NamespacedValue<S, U> {
- // Note: Constructor difference is that `unstable` is *required*.
- public constructor(stable: S, unstable: U) {
- super(stable, unstable);
- if (!this.unstable) {
- throw new Error("Unstable value must be supplied");
- }
- }
-
- public get name(): U {
- return this.unstable!;
- }
-
- public get altName(): S {
- return this.stable!;
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/ReEmitter.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/ReEmitter.ts
deleted file mode 100644
index 565e8ea..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/ReEmitter.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2017 Vector Creations Ltd
-Copyright 2017 New Vector Ltd
-
-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.
-*/
-
-// eslint-disable-next-line no-restricted-imports
-import { EventEmitter } from "events";
-
-import { ListenerMap, TypedEventEmitter } from "./models/typed-event-emitter";
-
-export class ReEmitter {
- public constructor(private readonly target: EventEmitter) {}
-
- // Map from emitter to event name to re-emitter
- private reEmitters = new Map<EventEmitter, Map<string, (...args: any[]) => void>>();
-
- public reEmit(source: EventEmitter, eventNames: string[]): void {
- let reEmittersByEvent = this.reEmitters.get(source);
- if (!reEmittersByEvent) {
- reEmittersByEvent = new Map();
- this.reEmitters.set(source, reEmittersByEvent);
- }
-
- for (const eventName of eventNames) {
- // We include the source as the last argument for event handlers which may need it,
- // such as read receipt listeners on the client class which won't have the context
- // of the room.
- const forSource = (...args: any[]): void => {
- // EventEmitter special cases 'error' to make the emit function throw if no
- // handler is attached, which sort of makes sense for making sure that something
- // handles an error, but for re-emitting, there could be a listener on the original
- // source object so the test doesn't really work. We *could* try to replicate the
- // same logic and throw if there is no listener on either the source or the target,
- // but this behaviour is fairly undesireable for us anyway: the main place we throw
- // 'error' events is for calls, where error events are usually emitted some time
- // later by a different part of the code where 'emit' throwing because the app hasn't
- // added an error handler isn't terribly helpful. (A better fix in retrospect may
- // have been to just avoid using the event name 'error', but backwards compat...)
- if (eventName === "error" && this.target.listenerCount("error") === 0) return;
- this.target.emit(eventName, ...args, source);
- };
- source.on(eventName, forSource);
- reEmittersByEvent.set(eventName, forSource);
- }
- }
-
- public stopReEmitting(source: EventEmitter, eventNames: string[]): void {
- const reEmittersByEvent = this.reEmitters.get(source);
- if (!reEmittersByEvent) return; // We were never re-emitting these events in the first place
-
- for (const eventName of eventNames) {
- source.off(eventName, reEmittersByEvent.get(eventName)!);
- reEmittersByEvent.delete(eventName);
- }
-
- if (reEmittersByEvent.size === 0) this.reEmitters.delete(source);
- }
-}
-
-export class TypedReEmitter<Events extends string, Arguments extends ListenerMap<Events>> extends ReEmitter {
- public constructor(target: TypedEventEmitter<Events, Arguments>) {
- super(target);
- }
-
- public reEmit<ReEmittedEvents extends string, T extends Events & ReEmittedEvents>(
- source: TypedEventEmitter<ReEmittedEvents, any>,
- eventNames: T[],
- ): void {
- super.reEmit(source, eventNames);
- }
-
- public stopReEmitting<ReEmittedEvents extends string, T extends Events & ReEmittedEvents>(
- source: TypedEventEmitter<ReEmittedEvents, any>,
- eventNames: T[],
- ): void {
- super.stopReEmitting(source, eventNames);
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/ToDeviceMessageQueue.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/ToDeviceMessageQueue.ts
deleted file mode 100644
index 59eada4..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/ToDeviceMessageQueue.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
-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 { ToDeviceMessageId } from "./@types/event";
-import { logger } from "./logger";
-import { MatrixClient, ClientEvent } from "./client";
-import { MatrixError } from "./http-api";
-import { IndexedToDeviceBatch, ToDeviceBatch, ToDeviceBatchWithTxnId, ToDevicePayload } from "./models/ToDeviceMessage";
-import { MatrixScheduler } from "./scheduler";
-import { SyncState } from "./sync";
-import { MapWithDefault } from "./utils";
-
-const MAX_BATCH_SIZE = 20;
-
-/**
- * Maintains a queue of outgoing to-device messages, sending them
- * as soon as the homeserver is reachable.
- */
-export class ToDeviceMessageQueue {
- private sending = false;
- private running = true;
- private retryTimeout: ReturnType<typeof setTimeout> | null = null;
- private retryAttempts = 0;
-
- public constructor(private client: MatrixClient) {}
-
- public start(): void {
- this.running = true;
- this.sendQueue();
- this.client.on(ClientEvent.Sync, this.onResumedSync);
- }
-
- public stop(): void {
- this.running = false;
- if (this.retryTimeout !== null) clearTimeout(this.retryTimeout);
- this.retryTimeout = null;
- this.client.removeListener(ClientEvent.Sync, this.onResumedSync);
- }
-
- public async queueBatch(batch: ToDeviceBatch): Promise<void> {
- const batches: ToDeviceBatchWithTxnId[] = [];
- for (let i = 0; i < batch.batch.length; i += MAX_BATCH_SIZE) {
- const batchWithTxnId = {
- eventType: batch.eventType,
- batch: batch.batch.slice(i, i + MAX_BATCH_SIZE),
- txnId: this.client.makeTxnId(),
- };
- batches.push(batchWithTxnId);
- const msgmap = batchWithTxnId.batch.map(
- (msg) => `${msg.userId}/${msg.deviceId} (msgid ${msg.payload[ToDeviceMessageId]})`,
- );
- logger.info(
- `Enqueuing batch of to-device messages. type=${batch.eventType} txnid=${batchWithTxnId.txnId}`,
- msgmap,
- );
- }
-
- await this.client.store.saveToDeviceBatches(batches);
- this.sendQueue();
- }
-
- public sendQueue = async (): Promise<void> => {
- if (this.retryTimeout !== null) clearTimeout(this.retryTimeout);
- this.retryTimeout = null;
-
- if (this.sending || !this.running) return;
-
- logger.debug("Attempting to send queued to-device messages");
-
- this.sending = true;
- let headBatch: IndexedToDeviceBatch | null;
- try {
- while (this.running) {
- headBatch = await this.client.store.getOldestToDeviceBatch();
- if (headBatch === null) break;
- await this.sendBatch(headBatch);
- await this.client.store.removeToDeviceBatch(headBatch.id);
- this.retryAttempts = 0;
- }
-
- // Make sure we're still running after the async tasks: if not, stop.
- if (!this.running) return;
-
- logger.debug("All queued to-device messages sent");
- } catch (e) {
- ++this.retryAttempts;
- // eslint-disable-next-line @typescript-eslint/naming-convention
- // eslint-disable-next-line new-cap
- const retryDelay = MatrixScheduler.RETRY_BACKOFF_RATELIMIT(null, this.retryAttempts, <MatrixError>e);
- if (retryDelay === -1) {
- // the scheduler function doesn't differentiate between fatal errors and just getting
- // bored and giving up for now
- if (Math.floor((<MatrixError>e).httpStatus! / 100) === 4) {
- logger.error("Fatal error when sending to-device message - dropping to-device batch!", e);
- await this.client.store.removeToDeviceBatch(headBatch!.id);
- } else {
- logger.info("Automatic retry limit reached for to-device messages.");
- }
- return;
- }
-
- logger.info(`Failed to send batch of to-device messages. Will retry in ${retryDelay}ms`, e);
- this.retryTimeout = setTimeout(this.sendQueue, retryDelay);
- } finally {
- this.sending = false;
- }
- };
-
- /**
- * Attempts to send a batch of to-device messages.
- */
- private async sendBatch(batch: IndexedToDeviceBatch): Promise<void> {
- const contentMap: MapWithDefault<string, Map<string, ToDevicePayload>> = new MapWithDefault(() => new Map());
- for (const item of batch.batch) {
- contentMap.getOrCreate(item.userId).set(item.deviceId, item.payload);
- }
-
- logger.info(
- `Sending batch of ${batch.batch.length} to-device messages with ID ${batch.id} and txnId ${batch.txnId}`,
- );
-
- await this.client.sendToDevice(batch.eventType, contentMap, batch.txnId);
- }
-
- /**
- * Listen to sync state changes and automatically resend any pending events
- * once syncing is resumed
- */
- private onResumedSync = (state: SyncState | null, oldState: SyncState | null): void => {
- if (state === SyncState.Syncing && oldState !== SyncState.Syncing) {
- logger.info(`Resuming queue after resumed sync`);
- this.sendQueue();
- }
- };
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/autodiscovery.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/autodiscovery.ts
deleted file mode 100644
index f4a3415..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/autodiscovery.ts
+++ /dev/null
@@ -1,472 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { IClientWellKnown, IWellKnownConfig } from "./client";
-import { logger } from "./logger";
-import { MatrixError, Method, timeoutSignal } from "./http-api";
-
-// Dev note: Auto discovery is part of the spec.
-// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
-
-export enum AutoDiscoveryAction {
- SUCCESS = "SUCCESS",
- IGNORE = "IGNORE",
- PROMPT = "PROMPT",
- FAIL_PROMPT = "FAIL_PROMPT",
- FAIL_ERROR = "FAIL_ERROR",
-}
-
-enum AutoDiscoveryError {
- Invalid = "Invalid homeserver discovery response",
- GenericFailure = "Failed to get autodiscovery configuration from server",
- InvalidHsBaseUrl = "Invalid base_url for m.homeserver",
- InvalidHomeserver = "Homeserver URL does not appear to be a valid Matrix homeserver",
- InvalidIsBaseUrl = "Invalid base_url for m.identity_server",
- InvalidIdentityServer = "Identity server URL does not appear to be a valid identity server",
- InvalidIs = "Invalid identity server discovery response",
- MissingWellknown = "No .well-known JSON file found",
- InvalidJson = "Invalid JSON",
-}
-
-interface WellKnownConfig extends Omit<IWellKnownConfig, "error"> {
- state: AutoDiscoveryAction;
- error?: IWellKnownConfig["error"] | null;
-}
-
-export interface ClientConfig extends Omit<IClientWellKnown, "m.homeserver" | "m.identity_server"> {
- "m.homeserver": WellKnownConfig;
- "m.identity_server": WellKnownConfig;
-}
-
-/**
- * Utilities for automatically discovery resources, such as homeservers
- * for users to log in to.
- */
-export class AutoDiscovery {
- // Dev note: the constants defined here are related to but not
- // exactly the same as those in the spec. This is to hopefully
- // translate the meaning of the states in the spec, but also
- // support our own if needed.
-
- public static readonly ERROR_INVALID = AutoDiscoveryError.Invalid;
-
- public static readonly ERROR_GENERIC_FAILURE = AutoDiscoveryError.GenericFailure;
-
- public static readonly ERROR_INVALID_HS_BASE_URL = AutoDiscoveryError.InvalidHsBaseUrl;
-
- public static readonly ERROR_INVALID_HOMESERVER = AutoDiscoveryError.InvalidHomeserver;
-
- public static readonly ERROR_INVALID_IS_BASE_URL = AutoDiscoveryError.InvalidIsBaseUrl;
-
- public static readonly ERROR_INVALID_IDENTITY_SERVER = AutoDiscoveryError.InvalidIdentityServer;
-
- public static readonly ERROR_INVALID_IS = AutoDiscoveryError.InvalidIs;
-
- public static readonly ERROR_MISSING_WELLKNOWN = AutoDiscoveryError.MissingWellknown;
-
- public static readonly ERROR_INVALID_JSON = AutoDiscoveryError.InvalidJson;
-
- public static readonly ALL_ERRORS = Object.keys(AutoDiscoveryError);
-
- /**
- * The auto discovery failed. The client is expected to communicate
- * the error to the user and refuse logging in.
- */
- public static readonly FAIL_ERROR = AutoDiscoveryAction.FAIL_ERROR;
-
- /**
- * The auto discovery failed, however the client may still recover
- * from the problem. The client is recommended to that the same
- * action it would for PROMPT while also warning the user about
- * what went wrong. The client may also treat this the same as
- * a FAIL_ERROR state.
- */
- public static readonly FAIL_PROMPT = AutoDiscoveryAction.FAIL_PROMPT;
-
- /**
- * The auto discovery didn't fail but did not find anything of
- * interest. The client is expected to prompt the user for more
- * information, or fail if it prefers.
- */
- public static readonly PROMPT = AutoDiscoveryAction.PROMPT;
-
- /**
- * The auto discovery was successful.
- */
- public static readonly SUCCESS = AutoDiscoveryAction.SUCCESS;
-
- /**
- * Validates and verifies client configuration information for purposes
- * of logging in. Such information includes the homeserver URL
- * and identity server URL the client would want. Additional details
- * may also be included, and will be transparently brought into the
- * response object unaltered.
- * @param wellknown - The configuration object itself, as returned
- * by the .well-known auto-discovery endpoint.
- * @returns Promise which resolves to the verified
- * configuration, which may include error states. Rejects on unexpected
- * failure, not when verification fails.
- */
- public static async fromDiscoveryConfig(wellknown: IClientWellKnown): Promise<ClientConfig> {
- // Step 1 is to get the config, which is provided to us here.
-
- // We default to an error state to make the first few checks easier to
- // write. We'll update the properties of this object over the duration
- // of this function.
- const clientConfig: ClientConfig = {
- "m.homeserver": {
- state: AutoDiscovery.FAIL_ERROR,
- error: AutoDiscovery.ERROR_INVALID,
- base_url: null,
- },
- "m.identity_server": {
- // Technically, we don't have a problem with the identity server
- // config at this point.
- state: AutoDiscovery.PROMPT,
- error: null,
- base_url: null,
- },
- };
-
- if (!wellknown || !wellknown["m.homeserver"]) {
- logger.error("No m.homeserver key in config");
-
- clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
- clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID;
-
- return Promise.resolve(clientConfig);
- }
-
- if (!wellknown["m.homeserver"]["base_url"]) {
- logger.error("No m.homeserver base_url in config");
-
- clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
- clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HS_BASE_URL;
-
- return Promise.resolve(clientConfig);
- }
-
- // Step 2: Make sure the homeserver URL is valid *looking*. We'll make
- // sure it points to a homeserver in Step 3.
- const hsUrl = this.sanitizeWellKnownUrl(wellknown["m.homeserver"]["base_url"]);
- if (!hsUrl) {
- logger.error("Invalid base_url for m.homeserver");
- clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HS_BASE_URL;
- return Promise.resolve(clientConfig);
- }
-
- // Step 3: Make sure the homeserver URL points to a homeserver.
- const hsVersions = await this.fetchWellKnownObject(`${hsUrl}/_matrix/client/versions`);
- if (!hsVersions || !hsVersions.raw?.["versions"]) {
- logger.error("Invalid /versions response");
- clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HOMESERVER;
-
- // Supply the base_url to the caller because they may be ignoring liveliness
- // errors, like this one.
- clientConfig["m.homeserver"].base_url = hsUrl;
-
- return Promise.resolve(clientConfig);
- }
-
- // Step 4: Now that the homeserver looks valid, update our client config.
- clientConfig["m.homeserver"] = {
- state: AutoDiscovery.SUCCESS,
- error: null,
- base_url: hsUrl,
- };
-
- // Step 5: Try to pull out the identity server configuration
- let isUrl: string | boolean = "";
- if (wellknown["m.identity_server"]) {
- // We prepare a failing identity server response to save lines later
- // in this branch.
- const failingClientConfig: ClientConfig = {
- "m.homeserver": clientConfig["m.homeserver"],
- "m.identity_server": {
- state: AutoDiscovery.FAIL_PROMPT,
- error: AutoDiscovery.ERROR_INVALID_IS,
- base_url: null,
- },
- };
-
- // Step 5a: Make sure the URL is valid *looking*. We'll make sure it
- // points to an identity server in Step 5b.
- isUrl = this.sanitizeWellKnownUrl(wellknown["m.identity_server"]["base_url"]);
- if (!isUrl) {
- logger.error("Invalid base_url for m.identity_server");
- failingClientConfig["m.identity_server"].error = AutoDiscovery.ERROR_INVALID_IS_BASE_URL;
- return Promise.resolve(failingClientConfig);
- }
-
- // Step 5b: Verify there is an identity server listening on the provided
- // URL.
- const isResponse = await this.fetchWellKnownObject(`${isUrl}/_matrix/identity/v2`);
- if (!isResponse?.raw || isResponse.action !== AutoDiscoveryAction.SUCCESS) {
- logger.error("Invalid /v2 response");
- failingClientConfig["m.identity_server"].error = AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER;
-
- // Supply the base_url to the caller because they may be ignoring
- // liveliness errors, like this one.
- failingClientConfig["m.identity_server"].base_url = isUrl;
-
- return Promise.resolve(failingClientConfig);
- }
- }
-
- // Step 6: Now that the identity server is valid, or never existed,
- // populate the IS section.
- if (isUrl && isUrl.toString().length > 0) {
- clientConfig["m.identity_server"] = {
- state: AutoDiscovery.SUCCESS,
- error: null,
- base_url: isUrl,
- };
- }
-
- // Step 7: Copy any other keys directly into the clientConfig. This is for
- // things like custom configuration of services.
- Object.keys(wellknown).forEach((k: keyof IClientWellKnown) => {
- 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;
- type Prop = Exclude<keyof IWellKnownConfig, "error" | "state" | "base_url">;
- // @ts-ignore - ts gets unhappy as we're mixing types here
- clientConfig[k][prop as Prop] = wellknown[k]![prop as 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);
- }
-
- /**
- * Attempts to automatically discover client configuration information
- * prior to logging in. Such information includes the homeserver URL
- * and identity server URL the client would want. Additional details
- * may also be discovered, and will be transparently included in the
- * response object unaltered.
- * @param domain - The homeserver domain to perform discovery
- * on. For example, "matrix.org".
- * @returns Promise which resolves to the discovered
- * configuration, which may include error states. Rejects on unexpected
- * failure, not when discovery fails.
- */
- public static async findClientConfig(domain: string): Promise<ClientConfig> {
- if (!domain || typeof domain !== "string" || domain.length === 0) {
- throw new Error("'domain' must be a string of non-zero length");
- }
-
- // We use a .well-known lookup for all cases. According to the spec, we
- // can do other discovery mechanisms if we want such as custom lookups
- // however we won't bother with that here (mostly because the spec only
- // supports .well-known right now).
- //
- // By using .well-known, we need to ensure we at least pull out a URL
- // for the homeserver. We don't really need an identity server configuration
- // but will return one anyways (with state PROMPT) to make development
- // easier for clients. If we can't get a homeserver URL, all bets are
- // off on the rest of the config and we'll assume it is invalid too.
-
- // We default to an error state to make the first few checks easier to
- // write. We'll update the properties of this object over the duration
- // of this function.
- const clientConfig: ClientConfig = {
- "m.homeserver": {
- state: AutoDiscovery.FAIL_ERROR,
- error: AutoDiscovery.ERROR_INVALID,
- base_url: null,
- },
- "m.identity_server": {
- // Technically, we don't have a problem with the identity server
- // config at this point.
- state: AutoDiscovery.PROMPT,
- error: null,
- base_url: null,
- },
- };
-
- // Step 1: Actually request the .well-known JSON file and make sure it
- // at least has a homeserver definition.
- const wellknown = await this.fetchWellKnownObject(`https://${domain}/.well-known/matrix/client`);
- if (!wellknown || wellknown.action !== AutoDiscoveryAction.SUCCESS) {
- logger.error("No response or error when parsing .well-known");
- if (wellknown.reason) logger.error(wellknown.reason);
- if (wellknown.action === AutoDiscoveryAction.IGNORE) {
- clientConfig["m.homeserver"] = {
- state: AutoDiscovery.PROMPT,
- error: null,
- base_url: null,
- };
- } else {
- // this can only ever be FAIL_PROMPT at this point.
- clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
- clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID;
- }
- return Promise.resolve(clientConfig);
- }
-
- // Step 2: Validate and parse the config
- return AutoDiscovery.fromDiscoveryConfig(wellknown.raw!);
- }
-
- /**
- * Gets the raw discovery client configuration for the given domain name.
- * Should only be used if there's no validation to be done on the resulting
- * object, otherwise use findClientConfig().
- * @param domain - The domain to get the client config for.
- * @returns Promise which resolves to the domain's client config. Can
- * be an empty object.
- */
- public static async getRawClientConfig(domain?: string): Promise<IClientWellKnown> {
- if (!domain || typeof domain !== "string" || domain.length === 0) {
- throw new Error("'domain' must be a string of non-zero length");
- }
-
- const response = await this.fetchWellKnownObject(`https://${domain}/.well-known/matrix/client`);
- if (!response) return {};
- return response.raw || {};
- }
-
- /**
- * Sanitizes a given URL to ensure it is either an HTTP or HTTP URL and
- * is suitable for the requirements laid out by .well-known auto discovery.
- * If valid, the URL will also be stripped of any trailing slashes.
- * @param url - The potentially invalid URL to sanitize.
- * @returns The sanitized URL or a falsey value if the URL is invalid.
- * @internal
- */
- private static sanitizeWellKnownUrl(url?: string | null): string | false {
- if (!url) return false;
-
- try {
- let parsed: URL | undefined;
- try {
- parsed = new URL(url);
- } catch (e) {
- logger.error("Could not parse url", e);
- }
-
- if (!parsed?.hostname) return false;
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
-
- const port = parsed.port ? `:${parsed.port}` : "";
- const path = parsed.pathname ? parsed.pathname : "";
- let saferUrl = `${parsed.protocol}//${parsed.hostname}${port}${path}`;
- if (saferUrl.endsWith("/")) {
- saferUrl = saferUrl.substring(0, saferUrl.length - 1);
- }
- return saferUrl;
- } catch (e) {
- logger.error(e);
- return false;
- }
- }
-
- private static fetch(resource: URL | string, options?: RequestInit): ReturnType<typeof global.fetch> {
- if (this.fetchFn) {
- return this.fetchFn(resource, options);
- }
- return global.fetch(resource, options);
- }
-
- private static fetchFn?: typeof global.fetch;
-
- public static setFetchFn(fetchFn: typeof global.fetch): void {
- AutoDiscovery.fetchFn = fetchFn;
- }
-
- /**
- * Fetches a JSON object from a given URL, as expected by all .well-known
- * related lookups. If the server gives a 404 then the `action` will be
- * IGNORE. If the server returns something that isn't JSON, the `action`
- * will be FAIL_PROMPT. For any other failure the `action` will be FAIL_PROMPT.
- *
- * The returned object will be a result of the call in object form with
- * 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.
- * error: The actual Error, if one exists.
- * @param url - The URL to fetch a JSON object from.
- * @returns Promise which resolves to the returned state.
- * @internal
- */
- private static async fetchWellKnownObject(url: string): Promise<IWellKnownConfig> {
- let response: Response;
-
- try {
- response = await AutoDiscovery.fetch(url, {
- method: Method.Get,
- signal: timeoutSignal(5000),
- });
-
- if (response.status === 404) {
- return {
- raw: {},
- action: AutoDiscoveryAction.IGNORE,
- reason: AutoDiscovery.ERROR_MISSING_WELLKNOWN,
- };
- }
-
- if (!response.ok) {
- return {
- raw: {},
- action: AutoDiscoveryAction.FAIL_PROMPT,
- reason: "General failure",
- };
- }
- } catch (err) {
- const error = err as AutoDiscoveryError | string | undefined;
- let reason = "";
- if (typeof error === "object") {
- reason = (<Error>error)?.message;
- }
-
- return {
- error,
- raw: {},
- action: AutoDiscoveryAction.FAIL_PROMPT,
- reason: reason || "General failure",
- };
- }
-
- try {
- return {
- raw: await response.json(),
- action: AutoDiscoveryAction.SUCCESS,
- };
- } catch (err) {
- const error = err as Error;
- return {
- error,
- raw: {},
- action: AutoDiscoveryAction.FAIL_PROMPT,
- reason:
- (error as MatrixError)?.name === "SyntaxError"
- ? AutoDiscovery.ERROR_INVALID_JSON
- : AutoDiscovery.ERROR_INVALID,
- };
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/browser-index.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/browser-index.ts
deleted file mode 100644
index 200b2a3..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/browser-index.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
-Copyright 2019 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import * as matrixcs from "./matrix";
-
-type BrowserMatrix = typeof matrixcs;
-declare global {
- /* eslint-disable no-var, camelcase */
- var __js_sdk_entrypoint: boolean;
- var matrixcs: BrowserMatrix;
- /* eslint-enable no-var */
-}
-
-if (global.__js_sdk_entrypoint) {
- throw new Error("Multiple matrix-js-sdk entrypoints detected!");
-}
-global.__js_sdk_entrypoint = true;
-
-// just *accessing* indexedDB throws an exception in firefox with indexeddb disabled.
-let indexedDB: IDBFactory | undefined;
-try {
- indexedDB = global.indexedDB;
-} catch (e) {}
-
-// if our browser (appears to) support indexeddb, use an indexeddb crypto store.
-if (indexedDB) {
- matrixcs.setCryptoStoreFactory(() => new matrixcs.IndexedDBCryptoStore(indexedDB!, "matrix-js-sdk:crypto"));
-}
-
-// We export 3 things to make browserify happy as well as downstream projects.
-// It's awkward, but required.
-export * from "./matrix";
-export default matrixcs; // keep export for browserify package deps
-global.matrixcs = matrixcs;
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/client.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/client.ts
deleted file mode 100644
index 0e47ff6..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/client.ts
+++ /dev/null
@@ -1,9680 +0,0 @@
-/*
-Copyright 2015-2023 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.
-*/
-
-/**
- * This is an internal module. See {@link MatrixClient} for the public class.
- */
-
-import { Optional } from "matrix-events-sdk";
-
-import type { IDeviceKeys, IMegolmSessionData, IOneTimeKey } from "./@types/crypto";
-import { ISyncStateData, SyncApi, SyncApiOptions, SyncState } from "./sync";
-import {
- EventStatus,
- IContent,
- IDecryptOptions,
- IEvent,
- MatrixEvent,
- MatrixEventEvent,
- MatrixEventHandlerMap,
-} from "./models/event";
-import { StubStore } from "./store/stub";
-import { CallEvent, CallEventHandlerMap, createNewMatrixCall, MatrixCall, supportsMatrixCall } from "./webrtc/call";
-import { Filter, IFilterDefinition, IRoomEventFilter } from "./filter";
-import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from "./webrtc/callEventHandler";
-import { GroupCallEventHandlerEvent, GroupCallEventHandlerEventHandlerMap } from "./webrtc/groupCallEventHandler";
-import * as utils from "./utils";
-import { replaceParam, QueryDict, sleep, noUnsafeEventProps } from "./utils";
-import { Direction, EventTimeline } from "./models/event-timeline";
-import { IActionsObject, PushProcessor } from "./pushprocessor";
-import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery";
-import * as olmlib from "./crypto/olmlib";
-import { decodeBase64, encodeBase64 } from "./crypto/olmlib";
-import { IExportedDevice as IExportedOlmDevice } from "./crypto/OlmDevice";
-import { IOlmDevice } from "./crypto/algorithms/megolm";
-import { TypedReEmitter } from "./ReEmitter";
-import { IRoomEncryption, RoomList } from "./crypto/RoomList";
-import { logger } from "./logger";
-import { SERVICE_TYPES } from "./service-types";
-import {
- HttpApiEvent,
- HttpApiEventHandlerMap,
- Upload,
- UploadOpts,
- MatrixError,
- MatrixHttpApi,
- Method,
- retryNetworkOperation,
- ClientPrefix,
- MediaPrefix,
- IdentityPrefix,
- IHttpOpts,
- FileType,
- UploadResponse,
- HTTPError,
- IRequestOpts,
-} from "./http-api";
-import {
- Crypto,
- CryptoEvent,
- CryptoEventHandlerMap,
- fixBackupKey,
- ICryptoCallbacks,
- IBootstrapCrossSigningOpts,
- ICheckOwnCrossSigningTrustOpts,
- isCryptoAvailable,
- VerificationMethod,
- IRoomKeyRequestBody,
-} from "./crypto";
-import { DeviceInfo } from "./crypto/deviceinfo";
-import { decodeRecoveryKey } from "./crypto/recoverykey";
-import { keyFromAuthData } from "./crypto/key_passphrase";
-import { User, UserEvent, UserEventHandlerMap } from "./models/user";
-import { getHttpUriForMxc } from "./content-repo";
-import { SearchResult } from "./models/search-result";
-import { DEHYDRATION_ALGORITHM, IDehydratedDevice, IDehydratedDeviceKeyInfo } from "./crypto/dehydration";
-import {
- IKeyBackupInfo,
- IKeyBackupPrepareOpts,
- IKeyBackupRestoreOpts,
- IKeyBackupRestoreResult,
- IKeyBackupRoomSessions,
- IKeyBackupSession,
-} from "./crypto/keybackup";
-import { IIdentityServerProvider } from "./@types/IIdentityServerProvider";
-import { MatrixScheduler } from "./scheduler";
-import { BeaconEvent, BeaconEventHandlerMap } from "./models/beacon";
-import { IAuthData, IAuthDict } from "./interactive-auth";
-import { IMinimalEvent, IRoomEvent, IStateEvent } from "./sync-accumulator";
-import {
- CrossSigningKey,
- IAddSecretStorageKeyOpts,
- ICreateSecretStorageOpts,
- IEncryptedEventInfo,
- IImportRoomKeysOpts,
- IRecoveryKey,
-} from "./crypto/api";
-import { EventTimelineSet } from "./models/event-timeline-set";
-import { VerificationRequest } from "./crypto/verification/request/VerificationRequest";
-import { VerificationBase as Verification } from "./crypto/verification/Base";
-import * as ContentHelpers from "./content-helpers";
-import { CrossSigningInfo, DeviceTrustLevel, ICacheCallbacks, UserTrustLevel } from "./crypto/CrossSigning";
-import { Room, NotificationCountType, RoomEvent, RoomEventHandlerMap, RoomNameState } from "./models/room";
-import { RoomMemberEvent, RoomMemberEventHandlerMap } from "./models/room-member";
-import { RoomStateEvent, RoomStateEventHandlerMap } from "./models/room-state";
-import {
- IAddThreePidOnlyBody,
- IBindThreePidBody,
- IContextResponse,
- ICreateRoomOpts,
- IEventSearchOpts,
- IGuestAccessOpts,
- IJoinRoomOpts,
- IPaginateOpts,
- IPresenceOpts,
- IRedactOpts,
- IRelationsRequestOpts,
- IRelationsResponse,
- IRoomDirectoryOptions,
- ISearchOpts,
- ISendEventResponse,
- INotificationsResponse,
- IFilterResponse,
- ITagsResponse,
- IStatusResponse,
-} from "./@types/requests";
-import {
- EventType,
- LOCAL_NOTIFICATION_SETTINGS_PREFIX,
- MsgType,
- PUSHER_ENABLED,
- RelationType,
- RoomCreateTypeField,
- RoomType,
- UNSTABLE_MSC3088_ENABLED,
- UNSTABLE_MSC3088_PURPOSE,
- UNSTABLE_MSC3089_TREE_SUBTYPE,
- MSC3912_RELATION_BASED_REDACTIONS_PROP,
-} from "./@types/event";
-import { IdServerUnbindResult, IImageInfo, Preset, Visibility } from "./@types/partials";
-import { EventMapper, eventMapperFor, MapperOpts } from "./event-mapper";
-import { randomString } from "./randomstring";
-import { BackupManager, IKeyBackup, IKeyBackupCheck, IPreparedKeyBackupVersion, TrustInfo } from "./crypto/backup";
-import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE, MSC3089TreeSpace } from "./models/MSC3089TreeSpace";
-import { ISignatures } from "./@types/signed";
-import { IStore } from "./store";
-import { ISecretRequest } from "./crypto/SecretStorage";
-import {
- IEventWithRoomId,
- ISearchRequestBody,
- ISearchResponse,
- ISearchResults,
- IStateEventWithRoomId,
- SearchOrderBy,
-} from "./@types/search";
-import { ISynapseAdminDeactivateResponse, ISynapseAdminWhoisResponse } from "./@types/synapse";
-import { IHierarchyRoom } from "./@types/spaces";
-import {
- IPusher,
- IPusherRequest,
- IPushRule,
- IPushRules,
- PushRuleAction,
- PushRuleActionName,
- PushRuleKind,
- RuleId,
-} from "./@types/PushRules";
-import { IThreepid } from "./@types/threepids";
-import { CryptoStore, OutgoingRoomKeyRequest } from "./crypto/store/base";
-import { GroupCall, IGroupCallDataChannelOptions, GroupCallIntent, GroupCallType } from "./webrtc/groupCall";
-import { MediaHandler } from "./webrtc/mediaHandler";
-import { GroupCallEventHandler } from "./webrtc/groupCallEventHandler";
-import { LoginTokenPostResponse, ILoginFlowsResponse, IRefreshTokenResponse, SSOAction } from "./@types/auth";
-import { TypedEventEmitter } from "./models/typed-event-emitter";
-import { MAIN_ROOM_TIMELINE, ReceiptType } from "./@types/read_receipts";
-import { MSC3575SlidingSyncRequest, MSC3575SlidingSyncResponse, SlidingSync } from "./sliding-sync";
-import { SlidingSyncSdk } from "./sliding-sync-sdk";
-import {
- FeatureSupport,
- Thread,
- THREAD_RELATION_TYPE,
- determineFeatureSupport,
- ThreadFilterType,
- threadFilterTypeToFilter,
-} from "./models/thread";
-import { MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
-import { UnstableValue } from "./NamespacedValue";
-import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue";
-import { ToDeviceBatch } from "./models/ToDeviceMessage";
-import { IgnoredInvites } from "./models/invites-ignorer";
-import { UIARequest, UIAResponse } from "./@types/uia";
-import { LocalNotificationSettings } from "./@types/local_notifications";
-import { buildFeatureSupportMap, Feature, ServerSupport } from "./feature";
-import { CryptoBackend } from "./common-crypto/CryptoBackend";
-import { RUST_SDK_STORE_PREFIX } from "./rust-crypto/constants";
-import { CryptoApi } from "./crypto-api";
-import { DeviceInfoMap } from "./crypto/DeviceList";
-import { SecretStorageKeyDescription } from "./secret-storage";
-
-export type Store = IStore;
-
-export type ResetTimelineCallback = (roomId: string) => boolean;
-
-const SCROLLBACK_DELAY_MS = 3000;
-export const CRYPTO_ENABLED: boolean = isCryptoAvailable();
-const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value
-const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes
-
-export const UNSTABLE_MSC3852_LAST_SEEN_UA = new UnstableValue(
- "last_seen_user_agent",
- "org.matrix.msc3852.last_seen_user_agent",
-);
-
-interface IExportedDevice {
- olmDevice: IExportedOlmDevice;
- userId: string;
- deviceId: string;
-}
-
-export interface IKeysUploadResponse {
- one_time_key_counts: {
- // eslint-disable-line camelcase
- [algorithm: string]: number;
- };
-}
-
-export interface ICreateClientOpts {
- baseUrl: string;
-
- idBaseUrl?: string;
-
- /**
- * The data store used for sync data from the homeserver. If not specified,
- * this client will not store any HTTP responses. The `createClient` helper
- * will create a default store if needed.
- */
- store?: Store;
-
- /**
- * A store to be used for end-to-end crypto session data. If not specified,
- * end-to-end crypto will be disabled. The `createClient` helper will create
- * a default store if needed. Calls the factory supplied to
- * {@link setCryptoStoreFactory} if unspecified; or if no factory has been
- * specified, uses a default implementation (indexeddb in the browser,
- * in-memory otherwise).
- */
- cryptoStore?: CryptoStore;
-
- /**
- * The scheduler to use. If not
- * specified, this client will not retry requests on failure. This client
- * will supply its own processing function to
- * {@link MatrixScheduler#setProcessFunction}.
- */
- scheduler?: MatrixScheduler;
-
- /**
- * The function to invoke for HTTP requests.
- * Most supported environments have a global `fetch` registered to which this will fall back.
- */
- fetchFn?: typeof global.fetch;
-
- userId?: string;
-
- /**
- * A unique identifier for this device; used for tracking things like crypto
- * keys and access tokens. If not specified, end-to-end encryption will be
- * disabled.
- */
- deviceId?: string;
-
- accessToken?: string;
-
- /**
- * Identity server provider to retrieve the user's access token when accessing
- * the identity server. See also https://github.com/vector-im/element-web/issues/10615
- * which seeks to replace the previous approach of manual access tokens params
- * with this callback throughout the SDK.
- */
- identityServer?: IIdentityServerProvider;
-
- /**
- * The default maximum amount of
- * time to wait before timing out HTTP requests. If not specified, there is no timeout.
- */
- localTimeoutMs?: number;
-
- /**
- * Set to true to use
- * Authorization header instead of query param to send the access token to the server.
- *
- * Default false.
- */
- useAuthorizationHeader?: boolean;
-
- /**
- * Set to true to enable
- * improved timeline support, see {@link MatrixClient#getEventTimeline}.
- * It is disabled by default for compatibility with older clients - in particular to
- * maintain support for back-paginating the live timeline after a '/sync'
- * result with a gap.
- */
- timelineSupport?: boolean;
-
- /**
- * Extra query parameters to append
- * to all requests with this client. Useful for application services which require
- * `?user_id=`.
- */
- queryParams?: Record<string, string>;
-
- /**
- * Device data exported with
- * "exportDevice" method that must be imported to recreate this device.
- * Should only be useful for devices with end-to-end crypto enabled.
- * If provided, deviceId and userId should **NOT** be provided at the top
- * level (they are present in the exported data).
- */
- deviceToImport?: IExportedDevice;
-
- /**
- * Key used to pickle olm objects or other sensitive data.
- */
- pickleKey?: string;
-
- verificationMethods?: Array<VerificationMethod>;
-
- /**
- * Whether relaying calls through a TURN server should be forced. Default false.
- */
- forceTURN?: boolean;
-
- /**
- * Up to this many ICE candidates will be gathered when an incoming call arrives.
- * Gathering does not send data to the caller, but will communicate with the configured TURN
- * server. Default 0.
- */
- iceCandidatePoolSize?: number;
-
- /**
- * True to advertise support for call transfers to other parties on Matrix calls. Default false.
- */
- supportsCallTransfer?: boolean;
-
- /**
- * Whether to allow a fallback ICE server should be used for negotiating a
- * WebRTC connection if the homeserver doesn't provide any servers. Defaults to false.
- */
- fallbackICEServerAllowed?: boolean;
-
- /**
- * If true, to-device signalling for group calls will be encrypted
- * with Olm. Default: true.
- */
- useE2eForGroupCall?: boolean;
-
- cryptoCallbacks?: ICryptoCallbacks;
-
- /**
- * Method to generate room names for empty rooms and rooms names based on membership.
- * Defaults to a built-in English handler with basic pluralisation.
- */
- roomNameGenerator?: (roomId: string, state: RoomNameState) => string | null;
-
- /**
- * If true, participant can join group call without video and audio this has to be allowed. By default, a local
- * media stream is needed to establish a group call.
- * Default: false.
- */
- isVoipWithNoMediaAllowed?: boolean;
-}
-
-export interface IMatrixClientCreateOpts extends ICreateClientOpts {
- /**
- * Whether to allow sending messages to encrypted rooms when encryption
- * is not available internally within this SDK. This is useful if you are using an external
- * E2E proxy, for example. Defaults to false.
- */
- usingExternalCrypto?: boolean;
-}
-
-export enum PendingEventOrdering {
- Chronological = "chronological",
- Detached = "detached",
-}
-
-export interface IStartClientOpts {
- /**
- * The event `limit=` to apply to initial sync. Default: 8.
- */
- initialSyncLimit?: number;
-
- /**
- * True to put `archived=true</code> on the <code>/initialSync` request. Default: false.
- */
- includeArchivedRooms?: boolean;
-
- /**
- * True to do /profile requests on every invite event if the displayname/avatar_url is not known for this user ID. Default: false.
- */
- resolveInvitesToProfiles?: boolean;
-
- /**
- * Controls where pending messages appear in a room's timeline. If "<b>chronological</b>", messages will
- * appear in the timeline when the call to `sendEvent` was made. If "<b>detached</b>",
- * pending messages will appear in a separate list, accessbile via {@link Room#getPendingEvents}.
- * Default: "chronological".
- */
- pendingEventOrdering?: PendingEventOrdering;
-
- /**
- * The number of milliseconds to wait on /sync. Default: 30000 (30 seconds).
- */
- pollTimeout?: number;
-
- /**
- * The filter to apply to /sync calls.
- */
- filter?: Filter;
-
- /**
- * True to perform syncing without automatically updating presence.
- */
- disablePresence?: boolean;
-
- /**
- * True to not load all membership events during initial sync but fetch them when needed by calling
- * `loadOutOfBandMembers` This will override the filter option at this moment.
- */
- lazyLoadMembers?: boolean;
-
- /**
- * The number of seconds between polls to /.well-known/matrix/client, undefined to disable.
- * This should be in the order of hours. Default: undefined.
- */
- clientWellKnownPollPeriod?: number;
-
- /**
- * @deprecated use `threadSupport` instead
- */
- experimentalThreadSupport?: boolean;
-
- /**
- * Will organises events in threaded conversations when
- * a thread relation is encountered
- */
- threadSupport?: boolean;
-
- /**
- * @experimental
- */
- slidingSync?: SlidingSync;
-
- /**
- * @experimental
- */
- intentionalMentions?: boolean;
-}
-
-export interface IStoredClientOpts extends IStartClientOpts {}
-
-export enum RoomVersionStability {
- Stable = "stable",
- Unstable = "unstable",
-}
-
-export interface IRoomVersionsCapability {
- default: string;
- available: Record<string, RoomVersionStability>;
-}
-
-export interface ICapability {
- enabled: boolean;
-}
-
-export interface IChangePasswordCapability extends ICapability {}
-
-export interface IThreadsCapability extends ICapability {}
-
-interface ICapabilities {
- [key: string]: any;
- "m.change_password"?: IChangePasswordCapability;
- "m.room_versions"?: IRoomVersionsCapability;
- "io.element.thread"?: IThreadsCapability;
-}
-
-/* eslint-disable camelcase */
-export interface ICrossSigningKey {
- keys: { [algorithm: string]: string };
- signatures?: ISignatures;
- usage: string[];
- user_id: string;
-}
-
-enum CrossSigningKeyType {
- MasterKey = "master_key",
- SelfSigningKey = "self_signing_key",
- UserSigningKey = "user_signing_key",
-}
-
-export type CrossSigningKeys = Record<CrossSigningKeyType, ICrossSigningKey>;
-
-export type SendToDeviceContentMap = Map<string, Map<string, Record<string, any>>>;
-
-export interface ISignedKey {
- keys: Record<string, string>;
- signatures: ISignatures;
- user_id: string;
- algorithms: string[];
- device_id: string;
-}
-
-export type KeySignatures = Record<string, Record<string, ICrossSigningKey | ISignedKey>>;
-export interface IUploadKeySignaturesResponse {
- failures: Record<
- string,
- Record<
- string,
- {
- errcode: string;
- error: string;
- }
- >
- >;
-}
-
-export interface IPreviewUrlResponse {
- [key: string]: undefined | string | number;
- "og:title": string;
- "og:type": string;
- "og:url": string;
- "og:image"?: string;
- "og:image:type"?: string;
- "og:image:height"?: number;
- "og:image:width"?: number;
- "og:description"?: string;
- "matrix:image:size"?: number;
-}
-
-export interface ITurnServerResponse {
- uris: string[];
- username: string;
- password: string;
- ttl: number;
-}
-
-export interface ITurnServer {
- urls: string[];
- username: string;
- credential: string;
-}
-
-export interface IServerVersions {
- versions: string[];
- unstable_features: Record<string, boolean>;
-}
-
-export const M_AUTHENTICATION = new UnstableValue("m.authentication", "org.matrix.msc2965.authentication");
-
-export interface IClientWellKnown {
- [key: string]: any;
- "m.homeserver"?: IWellKnownConfig;
- "m.identity_server"?: IWellKnownConfig;
- [M_AUTHENTICATION.name]?: IDelegatedAuthConfig; // MSC2965
-}
-
-export interface IWellKnownConfig {
- raw?: IClientWellKnown;
- action?: AutoDiscoveryAction;
- reason?: string;
- error?: Error | string;
- // eslint-disable-next-line
- base_url?: string | null;
- // XXX: this is undocumented
- server_name?: string;
-}
-
-export interface IDelegatedAuthConfig {
- // MSC2965
- /** The OIDC Provider/issuer the client should use */
- issuer: string;
- /** The optional URL of the web UI where the user can manage their account */
- account?: string;
-}
-
-interface IKeyBackupPath {
- path: string;
- queryData?: {
- version: string;
- };
-}
-
-interface IMediaConfig {
- [key: string]: any; // extensible
- "m.upload.size"?: number;
-}
-
-interface IThirdPartySigned {
- sender: string;
- mxid: string;
- token: string;
- signatures: ISignatures;
-}
-
-interface IJoinRequestBody {
- third_party_signed?: IThirdPartySigned;
-}
-
-interface ITagMetadata {
- [key: string]: any;
- order: number;
-}
-
-interface IMessagesResponse {
- start?: string;
- end?: string;
- chunk: IRoomEvent[];
- state?: IStateEvent[];
-}
-
-interface IThreadedMessagesResponse {
- prev_batch: string;
- next_batch: string;
- chunk: IRoomEvent[];
- state: IStateEvent[];
-}
-
-export interface IRequestTokenResponse {
- sid: string;
- submit_url?: string;
-}
-
-export interface IRequestMsisdnTokenResponse extends IRequestTokenResponse {
- msisdn: string;
- success: boolean;
- intl_fmt: string;
-}
-
-export interface IUploadKeysRequest {
- "device_keys"?: Required<IDeviceKeys>;
- "one_time_keys"?: Record<string, IOneTimeKey>;
- "org.matrix.msc2732.fallback_keys"?: Record<string, IOneTimeKey>;
-}
-
-export interface IQueryKeysRequest {
- device_keys: { [userId: string]: string[] };
- timeout?: number;
- token?: string;
-}
-
-export interface IClaimKeysRequest {
- one_time_keys: { [userId: string]: { [deviceId: string]: string } };
- timeout?: number;
-}
-
-export interface IOpenIDToken {
- access_token: string;
- token_type: "Bearer" | string;
- matrix_server_name: string;
- expires_in: number;
-}
-
-interface IRoomInitialSyncResponse {
- room_id: string;
- membership: "invite" | "join" | "leave" | "ban";
- messages?: {
- start?: string;
- end?: string;
- chunk: IEventWithRoomId[];
- };
- state?: IStateEventWithRoomId[];
- visibility: Visibility;
- account_data?: IMinimalEvent[];
- presence: Partial<IEvent>; // legacy and undocumented, api is deprecated so this won't get attention
-}
-
-interface IJoinedRoomsResponse {
- joined_rooms: string[];
-}
-
-interface IJoinedMembersResponse {
- joined: {
- [userId: string]: {
- display_name: string;
- avatar_url: string;
- };
- };
-}
-
-export interface IRegisterRequestParams {
- auth?: IAuthData;
- username?: string;
- password?: string;
- refresh_token?: boolean;
- guest_access_token?: string;
- x_show_msisdn?: boolean;
- bind_msisdn?: boolean;
- bind_email?: boolean;
- inhibit_login?: boolean;
- initial_device_display_name?: string;
-}
-
-export interface IPublicRoomsChunkRoom {
- room_id: string;
- name?: string;
- avatar_url?: string;
- topic?: string;
- canonical_alias?: string;
- aliases?: string[];
- world_readable: boolean;
- guest_can_join: boolean;
- num_joined_members: number;
- room_type?: RoomType | string; // Added by MSC3827
-}
-
-interface IPublicRoomsResponse {
- chunk: IPublicRoomsChunkRoom[];
- next_batch?: string;
- prev_batch?: string;
- total_room_count_estimate?: number;
-}
-
-interface IUserDirectoryResponse {
- results: {
- user_id: string;
- display_name?: string;
- avatar_url?: string;
- }[];
- limited: boolean;
-}
-
-export interface IMyDevice {
- "device_id": string;
- "display_name"?: string;
- "last_seen_ip"?: string;
- "last_seen_ts"?: number;
- // UNSTABLE_MSC3852_LAST_SEEN_UA
- "last_seen_user_agent"?: string;
- "org.matrix.msc3852.last_seen_user_agent"?: string;
-}
-
-export interface Keys {
- keys: { [keyId: string]: string };
- usage: string[];
- user_id: string;
-}
-
-export interface SigningKeys extends Keys {
- signatures: ISignatures;
-}
-
-export interface DeviceKeys {
- [deviceId: string]: IDeviceKeys & {
- unsigned?: {
- device_display_name: string;
- };
- };
-}
-
-export interface IDownloadKeyResult {
- failures: { [serverName: string]: object };
- device_keys: { [userId: string]: DeviceKeys };
- // the following three fields were added in 1.1
- master_keys?: { [userId: string]: Keys };
- self_signing_keys?: { [userId: string]: SigningKeys };
- user_signing_keys?: { [userId: string]: SigningKeys };
-}
-
-export interface IClaimOTKsResult {
- failures: { [serverName: string]: object };
- one_time_keys: {
- [userId: string]: {
- [deviceId: string]: {
- [keyId: string]: {
- key: string;
- signatures: ISignatures;
- };
- };
- };
- };
-}
-
-export interface IFieldType {
- regexp: string;
- placeholder: string;
-}
-
-export interface IInstance {
- desc: string;
- icon?: string;
- fields: object;
- network_id: string;
- // XXX: this is undocumented but we rely on it: https://github.com/matrix-org/matrix-doc/issues/3203
- instance_id: string;
-}
-
-export interface IProtocol {
- user_fields: string[];
- location_fields: string[];
- icon: string;
- field_types: Record<string, IFieldType>;
- instances: IInstance[];
-}
-
-interface IThirdPartyLocation {
- alias: string;
- protocol: string;
- fields: object;
-}
-
-interface IThirdPartyUser {
- userid: string;
- protocol: string;
- fields: object;
-}
-
-interface IRoomSummary extends Omit<IPublicRoomsChunkRoom, "canonical_alias" | "aliases"> {
- room_type?: RoomType;
- membership?: string;
- is_encrypted: boolean;
-}
-
-interface IRoomKeysResponse {
- sessions: IKeyBackupRoomSessions;
-}
-
-interface IRoomsKeysResponse {
- rooms: Record<string, IRoomKeysResponse>;
-}
-
-interface IRoomHierarchy {
- rooms: IHierarchyRoom[];
- next_batch?: string;
-}
-
-export interface TimestampToEventResponse {
- event_id: string;
- origin_server_ts: string;
-}
-
-interface IWhoamiResponse {
- user_id: string;
- device_id?: string;
-}
-/* eslint-enable camelcase */
-
-// We're using this constant for methods overloading and inspect whether a variable
-// contains an eventId or not. This was required to ensure backwards compatibility
-// of methods for threads
-// Probably not the most graceful solution but does a good enough job for now
-const EVENT_ID_PREFIX = "$";
-
-export enum ClientEvent {
- Sync = "sync",
- Event = "event",
- ToDeviceEvent = "toDeviceEvent",
- AccountData = "accountData",
- Room = "Room",
- DeleteRoom = "deleteRoom",
- SyncUnexpectedError = "sync.unexpectedError",
- ClientWellKnown = "WellKnown.client",
- ReceivedVoipEvent = "received_voip_event",
- UndecryptableToDeviceEvent = "toDeviceEvent.undecryptable",
- TurnServers = "turnServers",
- TurnServersError = "turnServers.error",
-}
-
-type RoomEvents =
- | RoomEvent.Name
- | RoomEvent.Redaction
- | RoomEvent.RedactionCancelled
- | RoomEvent.Receipt
- | RoomEvent.Tags
- | RoomEvent.LocalEchoUpdated
- | RoomEvent.HistoryImportedWithinTimeline
- | RoomEvent.AccountData
- | RoomEvent.MyMembership
- | RoomEvent.Timeline
- | RoomEvent.TimelineReset;
-
-type RoomStateEvents =
- | RoomStateEvent.Events
- | RoomStateEvent.Members
- | RoomStateEvent.NewMember
- | RoomStateEvent.Update
- | RoomStateEvent.Marker;
-
-type CryptoEvents =
- | CryptoEvent.KeySignatureUploadFailure
- | CryptoEvent.KeyBackupStatus
- | CryptoEvent.KeyBackupFailed
- | CryptoEvent.KeyBackupSessionsRemaining
- | CryptoEvent.RoomKeyRequest
- | CryptoEvent.RoomKeyRequestCancellation
- | CryptoEvent.VerificationRequest
- | CryptoEvent.DeviceVerificationChanged
- | CryptoEvent.UserTrustStatusChanged
- | CryptoEvent.KeysChanged
- | CryptoEvent.Warning
- | CryptoEvent.DevicesUpdated
- | CryptoEvent.WillUpdateDevices;
-
-type MatrixEventEvents = MatrixEventEvent.Decrypted | MatrixEventEvent.Replaced | MatrixEventEvent.VisibilityChange;
-
-type RoomMemberEvents =
- | RoomMemberEvent.Name
- | RoomMemberEvent.Typing
- | RoomMemberEvent.PowerLevel
- | RoomMemberEvent.Membership;
-
-type UserEvents =
- | UserEvent.AvatarUrl
- | UserEvent.DisplayName
- | UserEvent.Presence
- | UserEvent.CurrentlyActive
- | UserEvent.LastPresenceTs;
-
-export type EmittedEvents =
- | ClientEvent
- | RoomEvents
- | RoomStateEvents
- | CryptoEvents
- | MatrixEventEvents
- | RoomMemberEvents
- | UserEvents
- | CallEvent // re-emitted by call.ts using Object.values
- | CallEventHandlerEvent.Incoming
- | GroupCallEventHandlerEvent.Incoming
- | GroupCallEventHandlerEvent.Outgoing
- | GroupCallEventHandlerEvent.Ended
- | GroupCallEventHandlerEvent.Participants
- | HttpApiEvent.SessionLoggedOut
- | HttpApiEvent.NoConsent
- | BeaconEvent;
-
-export type ClientEventHandlerMap = {
- /**
- * Fires whenever the SDK's syncing state is updated. The state can be one of:
- * <ul>
- *
- * <li>PREPARED: The client has synced with the server at least once and is
- * ready for methods to be called on it. This will be immediately followed by
- * a state of SYNCING. <i>This is the equivalent of "syncComplete" in the
- * previous API.</i></li>
- *
- * <li>CATCHUP: The client has detected the connection to the server might be
- * available again and will now try to do a sync again. As this sync might take
- * a long time (depending how long ago was last synced, and general server
- * performance) the client is put in this mode so the UI can reflect trying
- * to catch up with the server after losing connection.</li>
- *
- * <li>SYNCING : The client is currently polling for new events from the server.
- * This will be called <i>after</i> processing latest events from a sync.</li>
- *
- * <li>ERROR : The client has had a problem syncing with the server. If this is
- * called <i>before</i> PREPARED then there was a problem performing the initial
- * sync. If this is called <i>after</i> PREPARED then there was a problem polling
- * the server for updates. This may be called multiple times even if the state is
- * already ERROR. <i>This is the equivalent of "syncError" in the previous
- * API.</i></li>
- *
- * <li>RECONNECTING: The sync connection has dropped, but not (yet) in a way that
- * should be considered erroneous.
- * </li>
- *
- * <li>STOPPED: The client has stopped syncing with server due to stopClient
- * being called.
- * </li>
- * </ul>
- * State transition diagram:
- * ```
- * +---->STOPPED
- * |
- * +----->PREPARED -------> SYNCING <--+
- * | ^ | ^ |
- * | CATCHUP ----------+ | | |
- * | ^ V | |
- * null ------+ | +------- RECONNECTING |
- * | V V |
- * +------->ERROR ---------------------+
- *
- * NB: 'null' will never be emitted by this event.
- *
- * ```
- * Transitions:
- * <ul>
- *
- * <li>`null -> PREPARED` : Occurs when the initial sync is completed
- * first time. This involves setting up filters and obtaining push rules.
- *
- * <li>`null -> ERROR` : Occurs when the initial sync failed first time.
- *
- * <li>`ERROR -> PREPARED` : Occurs when the initial sync succeeds
- * after previously failing.
- *
- * <li>`PREPARED -> SYNCING` : Occurs immediately after transitioning
- * to PREPARED. Starts listening for live updates rather than catching up.
- *
- * <li>`SYNCING -> RECONNECTING` : Occurs when the live update fails.
- *
- * <li>`RECONNECTING -> RECONNECTING` : Can occur if the update calls
- * continue to fail, but the keepalive calls (to /versions) succeed.
- *
- * <li>`RECONNECTING -> ERROR` : Occurs when the keepalive call also fails
- *
- * <li>`ERROR -> SYNCING` : Occurs when the client has performed a
- * live update after having previously failed.
- *
- * <li>`ERROR -> ERROR` : Occurs when the client has failed to keepalive
- * for a second time or more.</li>
- *
- * <li>`SYNCING -> SYNCING` : Occurs when the client has performed a live
- * update. This is called <i>after</i> processing.</li>
- *
- * <li>`* -> STOPPED` : Occurs once the client has stopped syncing or
- * trying to sync after stopClient has been called.</li>
- * </ul>
- *
- * @param state - An enum representing the syncing state. One of "PREPARED",
- * "SYNCING", "ERROR", "STOPPED".
- *
- * @param prevState - An enum representing the previous syncing state.
- * One of "PREPARED", "SYNCING", "ERROR", "STOPPED" <b>or null</b>.
- *
- * @param data - Data about this transition.
- *
- * @example
- * ```
- * matrixClient.on("sync", function(state, prevState, data) {
- * switch (state) {
- * case "ERROR":
- * // update UI to say "Connection Lost"
- * break;
- * case "SYNCING":
- * // update UI to remove any "Connection Lost" message
- * break;
- * case "PREPARED":
- * // the client instance is ready to be queried.
- * var rooms = matrixClient.getRooms();
- * break;
- * }
- * });
- * ```
- */
- [ClientEvent.Sync]: (state: SyncState, lastState: SyncState | null, data?: ISyncStateData) => void;
- /**
- * Fires whenever the SDK receives a new event.
- * <p>
- * This is only fired for live events received via /sync - it is not fired for
- * events received over context, search, or pagination APIs.
- *
- * @param event - The matrix event which caused this event to fire.
- * @example
- * ```
- * matrixClient.on("event", function(event){
- * var sender = event.getSender();
- * });
- * ```
- */
- [ClientEvent.Event]: (event: MatrixEvent) => void;
- /**
- * Fires whenever the SDK receives a new to-device event.
- * @param event - The matrix event which caused this event to fire.
- * @example
- * ```
- * matrixClient.on("toDeviceEvent", function(event){
- * var sender = event.getSender();
- * });
- * ```
- */
- [ClientEvent.ToDeviceEvent]: (event: MatrixEvent) => void;
- /**
- * Fires if a to-device event is received that cannot be decrypted.
- * Encrypted to-device events will (generally) use plain Olm encryption,
- * in which case decryption failures are fatal: the event will never be
- * decryptable, unlike Megolm encrypted events where the key may simply
- * arrive later.
- *
- * An undecryptable to-device event is therefore likley to indicate problems.
- *
- * @param event - The undecyptable to-device event
- */
- [ClientEvent.UndecryptableToDeviceEvent]: (event: MatrixEvent) => void;
- /**
- * Fires whenever new user-scoped account_data is added.
- * @param event - The event describing the account_data just added
- * @param event - The previous account data, if known.
- * @example
- * ```
- * matrixClient.on("accountData", function(event, oldEvent){
- * myAccountData[event.type] = event.content;
- * });
- * ```
- */
- [ClientEvent.AccountData]: (event: MatrixEvent, lastEvent?: MatrixEvent) => void;
- /**
- * Fires whenever a new Room is added. This will fire when you are invited to a
- * room, as well as when you join a room. <strong>This event is experimental and
- * may change.</strong>
- * @param room - The newly created, fully populated room.
- * @example
- * ```
- * matrixClient.on("Room", function(room){
- * var roomId = room.roomId;
- * });
- * ```
- */
- [ClientEvent.Room]: (room: Room) => void;
- /**
- * Fires whenever a Room is removed. This will fire when you forget a room.
- * <strong>This event is experimental and may change.</strong>
- * @param roomId - The deleted room ID.
- * @example
- * ```
- * matrixClient.on("deleteRoom", function(roomId){
- * // update UI from getRooms()
- * });
- * ```
- */
- [ClientEvent.DeleteRoom]: (roomId: string) => void;
- [ClientEvent.SyncUnexpectedError]: (error: Error) => void;
- /**
- * Fires when the client .well-known info is fetched.
- *
- * @param data - The JSON object returned by the server
- */
- [ClientEvent.ClientWellKnown]: (data: IClientWellKnown) => void;
- [ClientEvent.ReceivedVoipEvent]: (event: MatrixEvent) => void;
- [ClientEvent.TurnServers]: (servers: ITurnServer[]) => void;
- [ClientEvent.TurnServersError]: (error: Error, fatal: boolean) => void;
-} & RoomEventHandlerMap &
- RoomStateEventHandlerMap &
- CryptoEventHandlerMap &
- MatrixEventHandlerMap &
- RoomMemberEventHandlerMap &
- UserEventHandlerMap &
- CallEventHandlerEventHandlerMap &
- GroupCallEventHandlerEventHandlerMap &
- CallEventHandlerMap &
- HttpApiEventHandlerMap &
- BeaconEventHandlerMap;
-
-const SSO_ACTION_PARAM = new UnstableValue("action", "org.matrix.msc3824.action");
-
-/**
- * Represents a Matrix Client. Only directly construct this if you want to use
- * custom modules. Normally, {@link createClient} should be used
- * as it specifies 'sensible' defaults for these modules.
- */
-export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHandlerMap> {
- public static readonly RESTORE_BACKUP_ERROR_BAD_KEY = "RESTORE_BACKUP_ERROR_BAD_KEY";
-
- public reEmitter = new TypedReEmitter<EmittedEvents, ClientEventHandlerMap>(this);
- public olmVersion: [number, number, number] | null = null; // populated after initCrypto
- public usingExternalCrypto = false;
- public store: Store;
- public deviceId: string | null;
- public credentials: { userId: string | null };
- public pickleKey?: string;
- public scheduler?: MatrixScheduler;
- public clientRunning = false;
- public timelineSupport = false;
- public urlPreviewCache: { [key: string]: Promise<IPreviewUrlResponse> } = {};
- public identityServer?: IIdentityServerProvider;
- public http: MatrixHttpApi<IHttpOpts & { onlyData: true }>; // XXX: Intended private, used in code.
-
- /**
- * The libolm crypto implementation, if it is in use.
- *
- * @deprecated This should not be used. Instead, use the methods exposed directly on this class or
- * (where they are available) via {@link getCrypto}.
- */
- public crypto?: Crypto; // XXX: Intended private, used in code. Being replaced by cryptoBackend
-
- private cryptoBackend?: CryptoBackend; // one of crypto or rustCrypto
- public cryptoCallbacks: ICryptoCallbacks; // XXX: Intended private, used in code.
- public callEventHandler?: CallEventHandler; // XXX: Intended private, used in code.
- public groupCallEventHandler?: GroupCallEventHandler;
- public supportsCallTransfer = false; // XXX: Intended private, used in code.
- public forceTURN = false; // XXX: Intended private, used in code.
- public iceCandidatePoolSize = 0; // XXX: Intended private, used in code.
- public idBaseUrl?: string;
- public baseUrl: string;
- public readonly isVoipWithNoMediaAllowed;
-
- // Note: these are all `protected` to let downstream consumers make mistakes if they want to.
- // We don't technically support this usage, but have reasons to do this.
-
- protected canSupportVoip = false;
- protected peekSync: SyncApi | null = null;
- protected isGuestAccount = false;
- protected ongoingScrollbacks: { [roomId: string]: { promise?: Promise<Room>; errorTs?: number } } = {};
- protected notifTimelineSet: EventTimelineSet | null = null;
- protected cryptoStore?: CryptoStore;
- protected verificationMethods?: VerificationMethod[];
- protected fallbackICEServerAllowed = false;
- protected roomList: RoomList;
- protected syncApi?: SlidingSyncSdk | SyncApi;
- public roomNameGenerator?: ICreateClientOpts["roomNameGenerator"];
- public pushRules?: IPushRules;
- protected syncLeftRoomsPromise?: Promise<Room[]>;
- protected syncedLeftRooms = false;
- protected clientOpts?: IStoredClientOpts;
- protected clientWellKnownIntervalID?: ReturnType<typeof setInterval>;
- protected canResetTimelineCallback?: ResetTimelineCallback;
-
- public canSupport = new Map<Feature, ServerSupport>();
-
- // The pushprocessor caches useful things, so keep one and re-use it
- protected pushProcessor = new PushProcessor(this);
-
- // Promise to a response of the server's /versions response
- // TODO: This should expire: https://github.com/matrix-org/matrix-js-sdk/issues/1020
- protected serverVersionsPromise?: Promise<IServerVersions>;
-
- public cachedCapabilities?: {
- capabilities: ICapabilities;
- expiration: number;
- };
- protected clientWellKnown?: IClientWellKnown;
- protected clientWellKnownPromise?: Promise<IClientWellKnown>;
- protected turnServers: ITurnServer[] = [];
- protected turnServersExpiry = 0;
- protected checkTurnServersIntervalID?: ReturnType<typeof setInterval>;
- protected exportedOlmDeviceToImport?: IExportedOlmDevice;
- protected txnCtr = 0;
- protected mediaHandler = new MediaHandler(this);
- protected sessionId: string;
- protected pendingEventEncryption = new Map<string, Promise<void>>();
-
- private useE2eForGroupCall = true;
- private toDeviceMessageQueue: ToDeviceMessageQueue;
-
- // A manager for determining which invites should be ignored.
- public readonly ignoredInvites: IgnoredInvites;
-
- public constructor(opts: IMatrixClientCreateOpts) {
- super();
-
- opts.baseUrl = utils.ensureNoTrailingSlash(opts.baseUrl);
- opts.idBaseUrl = utils.ensureNoTrailingSlash(opts.idBaseUrl);
-
- this.baseUrl = opts.baseUrl;
- this.idBaseUrl = opts.idBaseUrl;
- this.identityServer = opts.identityServer;
-
- this.usingExternalCrypto = opts.usingExternalCrypto ?? false;
- this.store = opts.store || new StubStore();
- this.deviceId = opts.deviceId || null;
- this.sessionId = randomString(10);
-
- const userId = opts.userId || null;
- this.credentials = { userId };
-
- this.http = new MatrixHttpApi(this as ConstructorParameters<typeof MatrixHttpApi>[0], {
- fetchFn: opts.fetchFn,
- baseUrl: opts.baseUrl,
- idBaseUrl: opts.idBaseUrl,
- accessToken: opts.accessToken,
- prefix: ClientPrefix.R0,
- onlyData: true,
- extraParams: opts.queryParams,
- localTimeoutMs: opts.localTimeoutMs,
- useAuthorizationHeader: opts.useAuthorizationHeader,
- });
-
- if (opts.deviceToImport) {
- if (this.deviceId) {
- logger.warn(
- "not importing device because device ID is provided to " +
- "constructor independently of exported data",
- );
- } else if (this.credentials.userId) {
- logger.warn(
- "not importing device because user ID is provided to " +
- "constructor independently of exported data",
- );
- } else if (!opts.deviceToImport.deviceId) {
- logger.warn("not importing device because no device ID in exported data");
- } else {
- this.deviceId = opts.deviceToImport.deviceId;
- this.credentials.userId = opts.deviceToImport.userId;
- // will be used during async initialization of the crypto
- this.exportedOlmDeviceToImport = opts.deviceToImport.olmDevice;
- }
- } else if (opts.pickleKey) {
- this.pickleKey = opts.pickleKey;
- }
-
- this.scheduler = opts.scheduler;
- if (this.scheduler) {
- this.scheduler.setProcessFunction(async (eventToSend: MatrixEvent) => {
- const room = this.getRoom(eventToSend.getRoomId());
- if (eventToSend.status !== EventStatus.SENDING) {
- this.updatePendingEventStatus(room, eventToSend, EventStatus.SENDING);
- }
- const res = await this.sendEventHttpRequest(eventToSend);
- if (room) {
- // ensure we update pending event before the next scheduler run so that any listeners to event id
- // updates on the synchronous event emitter get a chance to run first.
- room.updatePendingEvent(eventToSend, EventStatus.SENT, res.event_id);
- }
- return res;
- });
- }
-
- if (supportsMatrixCall()) {
- this.callEventHandler = new CallEventHandler(this);
- this.groupCallEventHandler = new GroupCallEventHandler(this);
- this.canSupportVoip = true;
- // Start listening for calls after the initial sync is done
- // We do not need to backfill the call event buffer
- // with encrypted events that might never get decrypted
- this.on(ClientEvent.Sync, this.startCallEventHandler);
- }
-
- this.on(ClientEvent.Sync, this.fixupRoomNotifications);
-
- this.timelineSupport = Boolean(opts.timelineSupport);
-
- this.cryptoStore = opts.cryptoStore;
- this.verificationMethods = opts.verificationMethods;
- this.cryptoCallbacks = opts.cryptoCallbacks || {};
-
- this.forceTURN = opts.forceTURN || false;
- this.iceCandidatePoolSize = opts.iceCandidatePoolSize === undefined ? 0 : opts.iceCandidatePoolSize;
- this.supportsCallTransfer = opts.supportsCallTransfer || false;
- this.fallbackICEServerAllowed = opts.fallbackICEServerAllowed || false;
- this.isVoipWithNoMediaAllowed = opts.isVoipWithNoMediaAllowed || false;
-
- if (opts.useE2eForGroupCall !== undefined) this.useE2eForGroupCall = opts.useE2eForGroupCall;
-
- // List of which rooms have encryption enabled: separate from crypto because
- // we still want to know which rooms are encrypted even if crypto is disabled:
- // we don't want to start sending unencrypted events to them.
- this.roomList = new RoomList(this.cryptoStore);
- this.roomNameGenerator = opts.roomNameGenerator;
-
- this.toDeviceMessageQueue = new ToDeviceMessageQueue(this);
-
- // The SDK doesn't really provide a clean way for events to recalculate the push
- // actions for themselves, so we have to kinda help them out when they are encrypted.
- // We do this so that push rules are correctly executed on events in their decrypted
- // state, such as highlights when the user's name is mentioned.
- this.on(MatrixEventEvent.Decrypted, (event) => {
- fixNotificationCountOnDecryption(this, event);
- });
-
- // Like above, we have to listen for read receipts from ourselves in order to
- // correctly handle notification counts on encrypted rooms.
- // This fixes https://github.com/vector-im/element-web/issues/9421
- this.on(RoomEvent.Receipt, (event, room) => {
- if (room && this.isRoomEncrypted(room.roomId)) {
- // Figure out if we've read something or if it's just informational
- const content = event.getContent();
- const isSelf =
- Object.keys(content).filter((eid) => {
- for (const [key, value] of Object.entries(content[eid])) {
- if (!utils.isSupportedReceiptType(key)) continue;
- if (!value) continue;
-
- if (Object.keys(value).includes(this.getUserId()!)) return true;
- }
-
- return false;
- }).length > 0;
-
- if (!isSelf) return;
-
- // Work backwards to determine how many events are unread. We also set
- // a limit for how back we'll look to avoid spinning CPU for too long.
- // If we hit the limit, we assume the count is unchanged.
- const maxHistory = 20;
- const events = room.getLiveTimeline().getEvents();
-
- let highlightCount = 0;
-
- for (let i = events.length - 1; i >= 0; i--) {
- if (i === events.length - maxHistory) return; // limit reached
-
- const event = events[i];
-
- if (room.hasUserReadEvent(this.getUserId()!, event.getId()!)) {
- // If the user has read the event, then the counting is done.
- break;
- }
-
- const pushActions = this.getPushActionsForEvent(event);
- highlightCount += pushActions?.tweaks?.highlight ? 1 : 0;
- }
-
- // Note: we don't need to handle 'total' notifications because the counts
- // will come from the server.
- room.setUnreadNotificationCount(NotificationCountType.Highlight, highlightCount);
- }
- });
-
- this.ignoredInvites = new IgnoredInvites(this);
- }
-
- /**
- * High level helper method to begin syncing and poll for new events. To listen for these
- * events, add a listener for {@link ClientEvent.Event}
- * via {@link MatrixClient#on}. Alternatively, listen for specific
- * state change events.
- * @param opts - Options to apply when syncing.
- */
- public async startClient(opts?: IStartClientOpts): Promise<void> {
- if (this.clientRunning) {
- // client is already running.
- return;
- }
- this.clientRunning = true;
- // backwards compat for when 'opts' was 'historyLen'.
- if (typeof opts === "number") {
- opts = {
- initialSyncLimit: opts,
- };
- }
-
- // Create our own user object artificially (instead of waiting for sync)
- // so it's always available, even if the user is not in any rooms etc.
- const userId = this.getUserId();
- if (userId) {
- this.store.storeUser(new User(userId));
- }
-
- // periodically poll for turn servers if we support voip
- if (this.canSupportVoip) {
- this.checkTurnServersIntervalID = setInterval(() => {
- this.checkTurnServers();
- }, TURN_CHECK_INTERVAL);
- // noinspection ES6MissingAwait
- this.checkTurnServers();
- }
-
- if (this.syncApi) {
- // This shouldn't happen since we thought the client was not running
- logger.error("Still have sync object whilst not running: stopping old one");
- this.syncApi.stop();
- }
-
- try {
- await this.getVersions();
-
- // This should be done with `canSupport`
- // TODO: https://github.com/vector-im/element-web/issues/23643
- const { threads, list, fwdPagination } = await this.doesServerSupportThread();
- Thread.setServerSideSupport(threads);
- Thread.setServerSideListSupport(list);
- Thread.setServerSideFwdPaginationSupport(fwdPagination);
- } catch (e) {
- logger.error("Can't fetch server versions, continuing to initialise sync, this will be retried later", e);
- }
-
- this.clientOpts = opts ?? {};
- if (this.clientOpts.slidingSync) {
- this.syncApi = new SlidingSyncSdk(
- this.clientOpts.slidingSync,
- this,
- this.clientOpts,
- this.buildSyncApiOptions(),
- );
- } else {
- this.syncApi = new SyncApi(this, this.clientOpts, this.buildSyncApiOptions());
- }
-
- if (this.clientOpts.hasOwnProperty("experimentalThreadSupport")) {
- logger.warn("`experimentalThreadSupport` has been deprecated, use `threadSupport` instead");
- }
-
- // If `threadSupport` is omitted and the deprecated `experimentalThreadSupport` has been passed
- // We should fallback to that value for backwards compatibility purposes
- if (
- !this.clientOpts.hasOwnProperty("threadSupport") &&
- this.clientOpts.hasOwnProperty("experimentalThreadSupport")
- ) {
- this.clientOpts.threadSupport = this.clientOpts.experimentalThreadSupport;
- }
-
- this.syncApi.sync();
-
- if (this.clientOpts.clientWellKnownPollPeriod !== undefined) {
- this.clientWellKnownIntervalID = setInterval(() => {
- this.fetchClientWellKnown();
- }, 1000 * this.clientOpts.clientWellKnownPollPeriod);
- this.fetchClientWellKnown();
- }
-
- this.toDeviceMessageQueue.start();
- }
-
- /**
- * Construct a SyncApiOptions for this client, suitable for passing into the SyncApi constructor
- */
- protected buildSyncApiOptions(): SyncApiOptions {
- return {
- crypto: this.crypto,
- cryptoCallbacks: this.cryptoBackend,
- canResetEntireTimeline: (roomId: string): boolean => {
- if (!this.canResetTimelineCallback) {
- return false;
- }
- return this.canResetTimelineCallback(roomId);
- },
- };
- }
-
- /**
- * High level helper method to stop the client from polling and allow a
- * clean shutdown.
- */
- public stopClient(): void {
- this.cryptoBackend?.stop(); // crypto might have been initialised even if the client wasn't fully started
-
- if (!this.clientRunning) return; // already stopped
-
- logger.log("stopping MatrixClient");
-
- this.clientRunning = false;
-
- this.syncApi?.stop();
- this.syncApi = undefined;
-
- this.peekSync?.stopPeeking();
-
- this.callEventHandler?.stop();
- this.groupCallEventHandler?.stop();
- this.callEventHandler = undefined;
- this.groupCallEventHandler = undefined;
-
- global.clearInterval(this.checkTurnServersIntervalID);
- this.checkTurnServersIntervalID = undefined;
-
- if (this.clientWellKnownIntervalID !== undefined) {
- global.clearInterval(this.clientWellKnownIntervalID);
- }
-
- this.toDeviceMessageQueue.stop();
- }
-
- /**
- * Try to rehydrate a device if available. The client must have been
- * initialized with a `cryptoCallback.getDehydrationKey` option, and this
- * function must be called before initCrypto and startClient are called.
- *
- * @returns Promise which resolves to undefined if a device could not be dehydrated, or
- * to the new device ID if the dehydration was successful.
- * @returns Rejects: with an error response.
- */
- public async rehydrateDevice(): Promise<string | undefined> {
- if (this.crypto) {
- throw new Error("Cannot rehydrate device after crypto is initialized");
- }
-
- if (!this.cryptoCallbacks.getDehydrationKey) {
- return;
- }
-
- const getDeviceResult = await this.getDehydratedDevice();
- if (!getDeviceResult) {
- return;
- }
-
- if (!getDeviceResult.device_data || !getDeviceResult.device_id) {
- logger.info("no dehydrated device found");
- return;
- }
-
- const account = new global.Olm.Account();
- try {
- const deviceData = getDeviceResult.device_data;
- if (deviceData.algorithm !== DEHYDRATION_ALGORITHM) {
- logger.warn("Wrong algorithm for dehydrated device");
- return;
- }
- logger.log("unpickling dehydrated device");
- const key = await this.cryptoCallbacks.getDehydrationKey(deviceData, (k) => {
- // copy the key so that it doesn't get clobbered
- account.unpickle(new Uint8Array(k), deviceData.account);
- });
- account.unpickle(key, deviceData.account);
- logger.log("unpickled device");
-
- const rehydrateResult = await this.http.authedRequest<{ success: boolean }>(
- Method.Post,
- "/dehydrated_device/claim",
- undefined,
- {
- device_id: getDeviceResult.device_id,
- },
- {
- prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2",
- },
- );
-
- if (rehydrateResult.success) {
- this.deviceId = getDeviceResult.device_id;
- logger.info("using dehydrated device");
- const pickleKey = this.pickleKey || "DEFAULT_KEY";
- this.exportedOlmDeviceToImport = {
- pickledAccount: account.pickle(pickleKey),
- sessions: [],
- pickleKey: pickleKey,
- };
- account.free();
- return this.deviceId;
- } else {
- account.free();
- logger.info("not using dehydrated device");
- return;
- }
- } catch (e) {
- account.free();
- logger.warn("could not unpickle", e);
- }
- }
-
- /**
- * Get the current dehydrated device, if any
- * @returns A promise of an object containing the dehydrated device
- */
- public async getDehydratedDevice(): Promise<IDehydratedDevice | undefined> {
- try {
- return await this.http.authedRequest<IDehydratedDevice>(
- Method.Get,
- "/dehydrated_device",
- undefined,
- undefined,
- {
- prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2",
- },
- );
- } catch (e) {
- logger.info("could not get dehydrated device", e);
- return;
- }
- }
-
- /**
- * Set the dehydration key. This will also periodically dehydrate devices to
- * the server.
- *
- * @param key - the dehydration key
- * @param keyInfo - Information about the key. Primarily for
- * information about how to generate the key from a passphrase.
- * @param deviceDisplayName - The device display name for the
- * dehydrated device.
- * @returns A promise that resolves when the dehydrated device is stored.
- */
- public async setDehydrationKey(
- key: Uint8Array,
- keyInfo: IDehydratedDeviceKeyInfo,
- deviceDisplayName?: string,
- ): Promise<void> {
- if (!this.crypto) {
- logger.warn("not dehydrating device if crypto is not enabled");
- return;
- }
- return this.crypto.dehydrationManager.setKeyAndQueueDehydration(key, keyInfo, deviceDisplayName);
- }
-
- /**
- * Creates a new dehydrated device (without queuing periodic dehydration)
- * @param key - the dehydration key
- * @param keyInfo - Information about the key. Primarily for
- * information about how to generate the key from a passphrase.
- * @param deviceDisplayName - The device display name for the
- * dehydrated device.
- * @returns the device id of the newly created dehydrated device
- */
- public async createDehydratedDevice(
- key: Uint8Array,
- keyInfo: IDehydratedDeviceKeyInfo,
- deviceDisplayName?: string,
- ): Promise<string | undefined> {
- if (!this.crypto) {
- logger.warn("not dehydrating device if crypto is not enabled");
- return;
- }
- await this.crypto.dehydrationManager.setKey(key, keyInfo, deviceDisplayName);
- return this.crypto.dehydrationManager.dehydrateDevice();
- }
-
- public async exportDevice(): Promise<IExportedDevice | undefined> {
- if (!this.crypto) {
- logger.warn("not exporting device if crypto is not enabled");
- return;
- }
- return {
- userId: this.credentials.userId!,
- deviceId: this.deviceId!,
- // XXX: Private member access.
- olmDevice: await this.crypto.olmDevice.export(),
- };
- }
-
- /**
- * Clear any data out of the persistent stores used by the client.
- *
- * @returns Promise which resolves when the stores have been cleared.
- */
- public clearStores(): Promise<void> {
- if (this.clientRunning) {
- throw new Error("Cannot clear stores while client is running");
- }
-
- const promises: Promise<void>[] = [];
-
- promises.push(this.store.deleteAllData());
- if (this.cryptoStore) {
- promises.push(this.cryptoStore.deleteAllData());
- }
-
- // delete the stores used by the rust matrix-sdk-crypto, in case they were used
- const deleteRustSdkStore = async (): Promise<void> => {
- let indexedDB: IDBFactory;
- try {
- indexedDB = global.indexedDB;
- } catch (e) {
- // No indexeddb support
- return;
- }
- for (const dbname of [
- `${RUST_SDK_STORE_PREFIX}::matrix-sdk-crypto`,
- `${RUST_SDK_STORE_PREFIX}::matrix-sdk-crypto-meta`,
- ]) {
- const prom = new Promise((resolve, reject) => {
- logger.info(`Removing IndexedDB instance ${dbname}`);
- const req = indexedDB.deleteDatabase(dbname);
- req.onsuccess = (_): void => {
- logger.info(`Removed IndexedDB instance ${dbname}`);
- resolve(0);
- };
- req.onerror = (e): void => {
- // In private browsing, Firefox has a global.indexedDB, but attempts to delete an indexeddb
- // (even a non-existent one) fail with "DOMException: A mutation operation was attempted on a
- // database that did not allow mutations."
- //
- // it seems like the only thing we can really do is ignore the error.
- logger.warn(`Failed to remove IndexedDB instance ${dbname}:`, e);
- resolve(0);
- };
- req.onblocked = (e): void => {
- logger.info(`cannot yet remove IndexedDB instance ${dbname}`);
- };
- });
- await prom;
- }
- };
- promises.push(deleteRustSdkStore());
-
- return Promise.all(promises).then(); // .then to fix types
- }
-
- /**
- * Get the user-id of the logged-in user
- *
- * @returns MXID for the logged-in user, or null if not logged in
- */
- public getUserId(): string | null {
- if (this.credentials && this.credentials.userId) {
- return this.credentials.userId;
- }
- return null;
- }
-
- /**
- * Get the user-id of the logged-in user
- *
- * @returns MXID for the logged-in user
- * @throws Error if not logged in
- */
- public getSafeUserId(): string {
- const userId = this.getUserId();
- if (!userId) {
- throw new Error("Expected logged in user but found none.");
- }
- return userId;
- }
-
- /**
- * Get the domain for this client's MXID
- * @returns Domain of this MXID
- */
- public getDomain(): string | null {
- if (this.credentials && this.credentials.userId) {
- return this.credentials.userId.replace(/^.*?:/, "");
- }
- return null;
- }
-
- /**
- * Get the local part of the current user ID e.g. "foo" in "\@foo:bar".
- * @returns The user ID localpart or null.
- */
- public getUserIdLocalpart(): string | null {
- if (this.credentials && this.credentials.userId) {
- return this.credentials.userId.split(":")[0].substring(1);
- }
- return null;
- }
-
- /**
- * Get the device ID of this client
- * @returns device ID
- */
- public getDeviceId(): string | null {
- return this.deviceId;
- }
-
- /**
- * Get the session ID of this client
- * @returns session ID
- */
- public getSessionId(): string {
- return this.sessionId;
- }
-
- /**
- * Check if the runtime environment supports VoIP calling.
- * @returns True if VoIP is supported.
- */
- public supportsVoip(): boolean {
- return this.canSupportVoip;
- }
-
- /**
- * @returns
- */
- public getMediaHandler(): MediaHandler {
- return this.mediaHandler;
- }
-
- /**
- * Set whether VoIP calls are forced to use only TURN
- * candidates. This is the same as the forceTURN option
- * when creating the client.
- * @param force - True to force use of TURN servers
- */
- public setForceTURN(force: boolean): void {
- this.forceTURN = force;
- }
-
- /**
- * Set whether to advertise transfer support to other parties on Matrix calls.
- * @param support - True to advertise the 'm.call.transferee' capability
- */
- public setSupportsCallTransfer(support: boolean): void {
- this.supportsCallTransfer = support;
- }
-
- /**
- * Returns true if to-device signalling for group calls will be encrypted with Olm.
- * If false, it will be sent unencrypted.
- * @returns boolean Whether group call signalling will be encrypted
- */
- public getUseE2eForGroupCall(): boolean {
- return this.useE2eForGroupCall;
- }
-
- /**
- * Creates a new call.
- * The place*Call methods on the returned call can be used to actually place a call
- *
- * @param roomId - The room the call is to be placed in.
- * @returns the call or null if the browser doesn't support calling.
- */
- public createCall(roomId: string): MatrixCall | null {
- return createNewMatrixCall(this, roomId);
- }
-
- /**
- * Creates a new group call and sends the associated state event
- * to alert other members that the room now has a group call.
- *
- * @param roomId - The room the call is to be placed in.
- */
- public async createGroupCall(
- roomId: string,
- type: GroupCallType,
- isPtt: boolean,
- intent: GroupCallIntent,
- dataChannelsEnabled?: boolean,
- dataChannelOptions?: IGroupCallDataChannelOptions,
- ): Promise<GroupCall> {
- if (this.getGroupCallForRoom(roomId)) {
- throw new Error(`${roomId} already has an existing group call`);
- }
-
- const room = this.getRoom(roomId);
-
- if (!room) {
- throw new Error(`Cannot find room ${roomId}`);
- }
-
- // Because without Media section a WebRTC connection is not possible, so need a RTCDataChannel to set up a
- // no media WebRTC connection anyway.
- return new GroupCall(
- this,
- room,
- type,
- isPtt,
- intent,
- undefined,
- dataChannelsEnabled || this.isVoipWithNoMediaAllowed,
- dataChannelOptions,
- this.isVoipWithNoMediaAllowed,
- ).create();
- }
-
- /**
- * Wait until an initial state for the given room has been processed by the
- * client and the client is aware of any ongoing group calls. Awaiting on
- * the promise returned by this method before calling getGroupCallForRoom()
- * avoids races where getGroupCallForRoom is called before the state for that
- * room has been processed. It does not, however, fix other races, eg. two
- * clients both creating a group call at the same time.
- * @param roomId - The room ID to wait for
- * @returns A promise that resolves once existing group calls in the room
- * have been processed.
- */
- public waitUntilRoomReadyForGroupCalls(roomId: string): Promise<void> {
- return this.groupCallEventHandler!.waitUntilRoomReadyForGroupCalls(roomId);
- }
-
- /**
- * Get an existing group call for the provided room.
- * @returns The group call or null if it doesn't already exist.
- */
- public getGroupCallForRoom(roomId: string): GroupCall | null {
- return this.groupCallEventHandler!.groupCalls.get(roomId) || null;
- }
-
- /**
- * Get the current sync state.
- * @returns the sync state, which may be null.
- * @see MatrixClient#event:"sync"
- */
- public getSyncState(): SyncState | null {
- return this.syncApi?.getSyncState() ?? null;
- }
-
- /**
- * Returns the additional data object associated with
- * the current sync state, or null if there is no
- * such data.
- * Sync errors, if available, are put in the 'error' key of
- * this object.
- */
- public getSyncStateData(): ISyncStateData | null {
- if (!this.syncApi) {
- return null;
- }
- return this.syncApi.getSyncStateData();
- }
-
- /**
- * Whether the initial sync has completed.
- * @returns True if at least one sync has happened.
- */
- public isInitialSyncComplete(): boolean {
- const state = this.getSyncState();
- if (!state) {
- return false;
- }
- return state === SyncState.Prepared || state === SyncState.Syncing;
- }
-
- /**
- * Return whether the client is configured for a guest account.
- * @returns True if this is a guest access_token (or no token is supplied).
- */
- public isGuest(): boolean {
- return this.isGuestAccount;
- }
-
- /**
- * Set whether this client is a guest account. <b>This method is experimental
- * and may change without warning.</b>
- * @param guest - True if this is a guest account.
- */
- public setGuest(guest: boolean): void {
- // EXPERIMENTAL:
- // If the token is a macaroon, it should be encoded in it that it is a 'guest'
- // access token, which means that the SDK can determine this entirely without
- // the dev manually flipping this flag.
- this.isGuestAccount = guest;
- }
-
- /**
- * Return the provided scheduler, if any.
- * @returns The scheduler or undefined
- */
- public getScheduler(): MatrixScheduler | undefined {
- return this.scheduler;
- }
-
- /**
- * Retry a backed off syncing request immediately. This should only be used when
- * the user <b>explicitly</b> attempts to retry their lost connection.
- * Will also retry any outbound to-device messages currently in the queue to be sent
- * (retries of regular outgoing events are handled separately, per-event).
- * @returns True if this resulted in a request being retried.
- */
- public retryImmediately(): boolean {
- // don't await for this promise: we just want to kick it off
- this.toDeviceMessageQueue.sendQueue();
- return this.syncApi?.retryImmediately() ?? false;
- }
-
- /**
- * Return the global notification EventTimelineSet, if any
- *
- * @returns the globl notification EventTimelineSet
- */
- public getNotifTimelineSet(): EventTimelineSet | null {
- return this.notifTimelineSet;
- }
-
- /**
- * Set the global notification EventTimelineSet
- *
- */
- public setNotifTimelineSet(set: EventTimelineSet): void {
- this.notifTimelineSet = set;
- }
-
- /**
- * Gets the capabilities of the homeserver. Always returns an object of
- * capability keys and their options, which may be empty.
- * @param fresh - True to ignore any cached values.
- * @returns Promise which resolves to the capabilities of the homeserver
- * @returns Rejects: with an error response.
- */
- public getCapabilities(fresh = false): Promise<ICapabilities> {
- const now = new Date().getTime();
-
- if (this.cachedCapabilities && !fresh) {
- if (now < this.cachedCapabilities.expiration) {
- logger.log("Returning cached capabilities");
- return Promise.resolve(this.cachedCapabilities.capabilities);
- }
- }
-
- type Response = {
- capabilities?: ICapabilities;
- };
- return this.http
- .authedRequest<Response>(Method.Get, "/capabilities")
- .catch((e: Error): Response => {
- // We swallow errors because we need a default object anyhow
- logger.error(e);
- return {};
- })
- .then((r = {}) => {
- const capabilities = r["capabilities"] || {};
-
- // If the capabilities missed the cache, cache it for a shorter amount
- // of time to try and refresh them later.
- const cacheMs = Object.keys(capabilities).length ? CAPABILITIES_CACHE_MS : 60000 + Math.random() * 5000;
-
- this.cachedCapabilities = {
- capabilities,
- expiration: now + cacheMs,
- };
-
- logger.log("Caching capabilities: ", capabilities);
- return capabilities;
- });
- }
-
- /**
- * Initialise support for end-to-end encryption in this client, using libolm.
- *
- * You should call this method after creating the matrixclient, but *before*
- * calling `startClient`, if you want to support end-to-end encryption.
- *
- * It will return a Promise which will resolve when the crypto layer has been
- * successfully initialised.
- */
- public async initCrypto(): Promise<void> {
- if (!isCryptoAvailable()) {
- throw new Error(
- `End-to-end encryption not supported in this js-sdk build: did ` +
- `you remember to load the olm library?`,
- );
- }
-
- if (this.cryptoBackend) {
- logger.warn("Attempt to re-initialise e2e encryption on MatrixClient");
- return;
- }
-
- if (!this.cryptoStore) {
- // the cryptostore is provided by sdk.createClient, so this shouldn't happen
- throw new Error(`Cannot enable encryption: no cryptoStore provided`);
- }
-
- logger.log("Crypto: Starting up crypto store...");
- await this.cryptoStore.startup();
-
- // initialise the list of encrypted rooms (whether or not crypto is enabled)
- logger.log("Crypto: initialising roomlist...");
- await this.roomList.init();
-
- const userId = this.getUserId();
- if (userId === null) {
- throw new Error(
- `Cannot enable encryption on MatrixClient with unknown userId: ` +
- `ensure userId is passed in createClient().`,
- );
- }
- if (this.deviceId === null) {
- throw new Error(
- `Cannot enable encryption on MatrixClient with unknown deviceId: ` +
- `ensure deviceId is passed in createClient().`,
- );
- }
-
- const crypto = new Crypto(
- this,
- userId,
- this.deviceId,
- this.store,
- this.cryptoStore,
- this.roomList,
- this.verificationMethods!,
- );
-
- this.reEmitter.reEmit(crypto, [
- CryptoEvent.KeyBackupFailed,
- CryptoEvent.KeyBackupSessionsRemaining,
- CryptoEvent.RoomKeyRequest,
- CryptoEvent.RoomKeyRequestCancellation,
- CryptoEvent.Warning,
- CryptoEvent.DevicesUpdated,
- CryptoEvent.WillUpdateDevices,
- CryptoEvent.DeviceVerificationChanged,
- CryptoEvent.UserTrustStatusChanged,
- CryptoEvent.KeysChanged,
- ]);
-
- logger.log("Crypto: initialising crypto object...");
- await crypto.init({
- exportedOlmDevice: this.exportedOlmDeviceToImport,
- pickleKey: this.pickleKey,
- });
- delete this.exportedOlmDeviceToImport;
-
- this.olmVersion = Crypto.getOlmVersion();
-
- // if crypto initialisation was successful, tell it to attach its event handlers.
- crypto.registerEventHandlers(this as Parameters<Crypto["registerEventHandlers"]>[0]);
- this.cryptoBackend = this.crypto = crypto;
-
- // upload our keys in the background
- this.crypto.uploadDeviceKeys().catch((e) => {
- // TODO: throwing away this error is a really bad idea.
- logger.error("Error uploading device keys", e);
- });
- }
-
- /**
- * Initialise support for end-to-end encryption in this client, using the rust matrix-sdk-crypto.
- *
- * An alternative to {@link initCrypto}.
- *
- * *WARNING*: this API is very experimental, should not be used in production, and may change without notice!
- * Eventually it will be deprecated and `initCrypto` will do the same thing.
- *
- * @experimental
- *
- * @returns a Promise which will resolve when the crypto layer has been
- * successfully initialised.
- */
- public async initRustCrypto(): Promise<void> {
- if (this.cryptoBackend) {
- logger.warn("Attempt to re-initialise e2e encryption on MatrixClient");
- return;
- }
-
- const userId = this.getUserId();
- if (userId === null) {
- throw new Error(
- `Cannot enable encryption on MatrixClient with unknown userId: ` +
- `ensure userId is passed in createClient().`,
- );
- }
- const deviceId = this.getDeviceId();
- if (deviceId === null) {
- throw new Error(
- `Cannot enable encryption on MatrixClient with unknown deviceId: ` +
- `ensure deviceId is passed in createClient().`,
- );
- }
-
- // importing rust-crypto will download the webassembly, so we delay it until we know it will be
- // needed.
- const RustCrypto = await import("./rust-crypto");
- const rustCrypto = await RustCrypto.initRustCrypto(this.http, userId, deviceId);
- this.cryptoBackend = rustCrypto;
-
- // attach the event listeners needed by RustCrypto
- this.on(RoomMemberEvent.Membership, rustCrypto.onRoomMembership.bind(rustCrypto));
- }
-
- /**
- * Access the crypto API for this client.
- *
- * If end-to-end encryption has been enabled for this client (via {@link initCrypto} or {@link initRustCrypto}),
- * returns an object giving access to the crypto API. Otherwise, returns `undefined`.
- */
- public getCrypto(): CryptoApi | undefined {
- return this.cryptoBackend;
- }
-
- /**
- * Is end-to-end crypto enabled for this client.
- * @returns True if end-to-end is enabled.
- * @deprecated prefer {@link getCrypto}
- */
- public isCryptoEnabled(): boolean {
- return !!this.cryptoBackend;
- }
-
- /**
- * Get the Ed25519 key for this device
- *
- * @returns base64-encoded ed25519 key. Null if crypto is
- * disabled.
- */
- public getDeviceEd25519Key(): string | null {
- return this.crypto?.getDeviceEd25519Key() ?? null;
- }
-
- /**
- * Get the Curve25519 key for this device
- *
- * @returns base64-encoded curve25519 key. Null if crypto is
- * disabled.
- */
- public getDeviceCurve25519Key(): string | null {
- return this.crypto?.getDeviceCurve25519Key() ?? null;
- }
-
- /**
- * @deprecated Does nothing.
- */
- public async uploadKeys(): Promise<void> {
- logger.warn("MatrixClient.uploadKeys is deprecated");
- }
-
- /**
- * Download the keys for a list of users and stores the keys in the session
- * store.
- * @param userIds - The users to fetch.
- * @param forceDownload - Always download the keys even if cached.
- *
- * @returns A promise which resolves to a map userId-\>deviceId-\>{@link DeviceInfo}
- */
- public downloadKeys(userIds: string[], forceDownload?: boolean): Promise<DeviceInfoMap> {
- if (!this.crypto) {
- return Promise.reject(new Error("End-to-end encryption disabled"));
- }
- return this.crypto.downloadKeys(userIds, forceDownload);
- }
-
- /**
- * Get the stored device keys for a user id
- *
- * @param userId - the user to list keys for.
- *
- * @returns list of devices
- */
- public getStoredDevicesForUser(userId: string): DeviceInfo[] {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.getStoredDevicesForUser(userId) || [];
- }
-
- /**
- * Get the stored device key for a user id and device id
- *
- * @param userId - the user to list keys for.
- * @param deviceId - unique identifier for the device
- *
- * @returns device or null
- */
- public getStoredDevice(userId: string, deviceId: string): DeviceInfo | null {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.getStoredDevice(userId, deviceId) || null;
- }
-
- /**
- * Mark the given device as verified
- *
- * @param userId - owner of the device
- * @param deviceId - unique identifier for the device or user's
- * cross-signing public key ID.
- *
- * @param verified - whether to mark the device as verified. defaults
- * to 'true'.
- *
- * @returns
- *
- * @remarks
- * Fires {@link CryptoEvent#DeviceVerificationChanged}
- */
- public setDeviceVerified(userId: string, deviceId: string, verified = true): Promise<void> {
- const prom = this.setDeviceVerification(userId, deviceId, verified, null, null);
-
- // if one of the user's own devices is being marked as verified / unverified,
- // check the key backup status, since whether or not we use this depends on
- // whether it has a signature from a verified device
- if (userId == this.credentials.userId) {
- this.checkKeyBackup();
- }
- return prom;
- }
-
- /**
- * Mark the given device as blocked/unblocked
- *
- * @param userId - owner of the device
- * @param deviceId - unique identifier for the device or user's
- * cross-signing public key ID.
- *
- * @param blocked - whether to mark the device as blocked. defaults
- * to 'true'.
- *
- * @returns
- *
- * @remarks
- * Fires {@link CryptoEvent.DeviceVerificationChanged}
- */
- public setDeviceBlocked(userId: string, deviceId: string, blocked = true): Promise<void> {
- return this.setDeviceVerification(userId, deviceId, null, blocked, null);
- }
-
- /**
- * Mark the given device as known/unknown
- *
- * @param userId - owner of the device
- * @param deviceId - unique identifier for the device or user's
- * cross-signing public key ID.
- *
- * @param known - whether to mark the device as known. defaults
- * to 'true'.
- *
- * @returns
- *
- * @remarks
- * Fires {@link CryptoEvent#DeviceVerificationChanged}
- */
- public setDeviceKnown(userId: string, deviceId: string, known = true): Promise<void> {
- return this.setDeviceVerification(userId, deviceId, null, null, known);
- }
-
- private async setDeviceVerification(
- userId: string,
- deviceId: string,
- verified?: boolean | null,
- blocked?: boolean | null,
- known?: boolean | null,
- ): Promise<void> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- await this.crypto.setDeviceVerification(userId, deviceId, verified, blocked, known);
- }
-
- /**
- * Request a key verification from another user, using a DM.
- *
- * @param userId - the user to request verification with
- * @param roomId - the room to use for verification
- *
- * @returns resolves to a VerificationRequest
- * when the request has been sent to the other party.
- */
- public requestVerificationDM(userId: string, roomId: string): Promise<VerificationRequest> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.requestVerificationDM(userId, roomId);
- }
-
- /**
- * Finds a DM verification request that is already in progress for the given room id
- *
- * @param roomId - the room to use for verification
- *
- * @returns the VerificationRequest that is in progress, if any
- */
- public findVerificationRequestDMInProgress(roomId: string): VerificationRequest | undefined {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.findVerificationRequestDMInProgress(roomId);
- }
-
- /**
- * Returns all to-device verification requests that are already in progress for the given user id
- *
- * @param userId - the ID of the user to query
- *
- * @returns the VerificationRequests that are in progress
- */
- public getVerificationRequestsToDeviceInProgress(userId: string): VerificationRequest[] {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.getVerificationRequestsToDeviceInProgress(userId);
- }
-
- /**
- * Request a key verification from another user.
- *
- * @param userId - the user to request verification with
- * @param devices - array of device IDs to send requests to. Defaults to
- * all devices owned by the user
- *
- * @returns resolves to a VerificationRequest
- * when the request has been sent to the other party.
- */
- public requestVerification(userId: string, devices?: string[]): Promise<VerificationRequest> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.requestVerification(userId, devices);
- }
-
- /**
- * Begin a key verification.
- *
- * @param method - the verification method to use
- * @param userId - the user to verify keys with
- * @param deviceId - the device to verify
- *
- * @returns a verification object
- * @deprecated Use `requestVerification` instead.
- */
- public beginKeyVerification(method: string, userId: string, deviceId: string): Verification<any, any> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.beginKeyVerification(method, userId, deviceId);
- }
-
- public checkSecretStorageKey(key: Uint8Array, info: SecretStorageKeyDescription): Promise<boolean> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.checkSecretStorageKey(key, info);
- }
-
- /**
- * Set the global override for whether the client should ever send encrypted
- * messages to unverified devices. This provides the default for rooms which
- * do not specify a value.
- *
- * @param value - whether to blacklist all unverified devices by default
- *
- * @deprecated Prefer direct access to {@link CryptoApi.globalBlacklistUnverifiedDevices}:
- *
- * ```javascript
- * client.getCrypto().globalBlacklistUnverifiedDevices = value;
- * ```
- */
- public setGlobalBlacklistUnverifiedDevices(value: boolean): boolean {
- if (!this.cryptoBackend) {
- throw new Error("End-to-end encryption disabled");
- }
- this.cryptoBackend.globalBlacklistUnverifiedDevices = value;
- return value;
- }
-
- /**
- * @returns whether to blacklist all unverified devices by default
- *
- * @deprecated Prefer direct access to {@link CryptoApi.globalBlacklistUnverifiedDevices}:
- *
- * ```javascript
- * value = client.getCrypto().globalBlacklistUnverifiedDevices;
- * ```
- */
- public getGlobalBlacklistUnverifiedDevices(): boolean {
- if (!this.cryptoBackend) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.cryptoBackend.globalBlacklistUnverifiedDevices;
- }
-
- /**
- * Set whether sendMessage in a room with unknown and unverified devices
- * should throw an error and not send them message. This has 'Global' for
- * symmetry with setGlobalBlacklistUnverifiedDevices but there is currently
- * no room-level equivalent for this setting.
- *
- * This API is currently UNSTABLE and may change or be removed without notice.
- *
- * @param value - whether error on unknown devices
- *
- * @deprecated Prefer direct access to {@link CryptoApi.globalBlacklistUnverifiedDevices}:
- *
- * ```ts
- * client.getCrypto().globalBlacklistUnverifiedDevices = value;
- * ```
- */
- public setGlobalErrorOnUnknownDevices(value: boolean): void {
- if (!this.cryptoBackend) {
- throw new Error("End-to-end encryption disabled");
- }
- this.cryptoBackend.globalErrorOnUnknownDevices = value;
- }
-
- /**
- * @returns whether to error on unknown devices
- *
- * This API is currently UNSTABLE and may change or be removed without notice.
- */
- public getGlobalErrorOnUnknownDevices(): boolean {
- if (!this.cryptoBackend) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.cryptoBackend.globalErrorOnUnknownDevices;
- }
-
- /**
- * Get the user's cross-signing key ID.
- *
- * The cross-signing API is currently UNSTABLE and may change without notice.
- *
- * @param type - The type of key to get the ID of. One of
- * "master", "self_signing", or "user_signing". Defaults to "master".
- *
- * @returns the key ID
- */
- public getCrossSigningId(type: CrossSigningKey | string = CrossSigningKey.Master): string | null {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.getCrossSigningId(type);
- }
-
- /**
- * Get the cross signing information for a given user.
- *
- * The cross-signing API is currently UNSTABLE and may change without notice.
- *
- * @param userId - the user ID to get the cross-signing info for.
- *
- * @returns the cross signing information for the user.
- */
- public getStoredCrossSigningForUser(userId: string): CrossSigningInfo | null {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.getStoredCrossSigningForUser(userId);
- }
-
- /**
- * Check whether a given user is trusted.
- *
- * The cross-signing API is currently UNSTABLE and may change without notice.
- *
- * @param userId - The ID of the user to check.
- */
- public checkUserTrust(userId: string): UserTrustLevel {
- if (!this.cryptoBackend) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.cryptoBackend.checkUserTrust(userId);
- }
-
- /**
- * Check whether a given device is trusted.
- *
- * The cross-signing API is currently UNSTABLE and may change without notice.
- *
- * @param userId - The ID of the user whose devices is to be checked.
- * @param deviceId - The ID of the device to check
- */
- public checkDeviceTrust(userId: string, deviceId: string): DeviceTrustLevel {
- if (!this.cryptoBackend) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.cryptoBackend.checkDeviceTrust(userId, deviceId);
- }
-
- /**
- * 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 deviceId - The ID of the device to check
- *
- * @returns true if the device is cross-signed
- */
- public checkIfOwnDeviceCrossSigned(deviceId: string): boolean {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.checkIfOwnDeviceCrossSigned(deviceId);
- }
-
- /**
- * Check the copy of our cross-signing key that we have in the device list and
- * see if we can get the private key. If so, mark it as trusted.
- * @param opts - ICheckOwnCrossSigningTrustOpts object
- */
- public checkOwnCrossSigningTrust(opts?: ICheckOwnCrossSigningTrustOpts): Promise<void> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.checkOwnCrossSigningTrust(opts);
- }
-
- /**
- * Checks that a given cross-signing private key matches a given public key.
- * This can be used by the getCrossSigningKey callback to verify that the
- * private key it is about to supply is the one that was requested.
- * @param privateKey - The private key
- * @param expectedPublicKey - The public key
- * @returns true if the key matches, otherwise false
- */
- public checkCrossSigningPrivateKey(privateKey: Uint8Array, expectedPublicKey: string): boolean {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.checkCrossSigningPrivateKey(privateKey, expectedPublicKey);
- }
-
- // deprecated: use requestVerification instead
- public legacyDeviceVerification(
- userId: string,
- deviceId: string,
- method: VerificationMethod,
- ): Promise<VerificationRequest> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.legacyDeviceVerification(userId, deviceId, method);
- }
-
- /**
- * Perform any background tasks that can be done before a message is ready to
- * send, in order to speed up sending of the message.
- * @param room - the room the event is in
- *
- * @deprecated Prefer {@link CryptoApi.prepareToEncrypt | `CryptoApi.prepareToEncrypt`}:
- *
- * ```javascript
- * client.getCrypto().prepareToEncrypt(room);
- * ```
- */
- public prepareToEncrypt(room: Room): void {
- if (!this.cryptoBackend) {
- throw new Error("End-to-end encryption disabled");
- }
- this.cryptoBackend.prepareToEncrypt(room);
- }
-
- /**
- * Checks if the user has previously published cross-signing keys
- *
- * This means downloading the devicelist for the user and checking if the list includes
- * the cross-signing pseudo-device.
- *
- * @deprecated Prefer {@link CryptoApi.userHasCrossSigningKeys | `CryptoApi.userHasCrossSigningKeys`}:
- *
- * ```javascript
- * result = client.getCrypto().userHasCrossSigningKeys();
- * ```
- */
- public userHasCrossSigningKeys(): Promise<boolean> {
- if (!this.cryptoBackend) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.cryptoBackend.userHasCrossSigningKeys();
- }
-
- /**
- * Checks whether cross signing:
- * - is enabled on this account and trusted by this device
- * - has private keys either cached locally or stored in secret storage
- *
- * If this function returns false, bootstrapCrossSigning() can be used
- * to fix things such that it returns true. That is to say, after
- * bootstrapCrossSigning() completes successfully, this function should
- * return true.
- * @returns True if cross-signing is ready to be used on this device
- */
- public isCrossSigningReady(): Promise<boolean> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.isCrossSigningReady();
- }
-
- /**
- * Bootstrap cross-signing by creating keys if needed. If everything is already
- * set up, then no changes are made, so this is safe to run to ensure
- * cross-signing is ready for use.
- *
- * This function:
- * - creates new cross-signing keys if they are not found locally cached nor in
- * secret storage (if it has been setup)
- *
- * The cross-signing API is currently UNSTABLE and may change without notice.
- */
- public bootstrapCrossSigning(opts: IBootstrapCrossSigningOpts): Promise<void> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.bootstrapCrossSigning(opts);
- }
-
- /**
- * Whether to trust a others users signatures of their devices.
- * If false, devices will only be considered 'verified' if we have
- * verified that device individually (effectively disabling cross-signing).
- *
- * Default: true
- *
- * @returns True if trusting cross-signed devices
- */
- public getCryptoTrustCrossSignedDevices(): boolean {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.getCryptoTrustCrossSignedDevices();
- }
-
- /**
- * See getCryptoTrustCrossSignedDevices
- *
- * @param val - True to trust cross-signed devices
- */
- public setCryptoTrustCrossSignedDevices(val: boolean): void {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- this.crypto.setCryptoTrustCrossSignedDevices(val);
- }
-
- /**
- * Counts the number of end to end session keys that are waiting to be backed up
- * @returns Promise which resolves to the number of sessions requiring backup
- */
- public countSessionsNeedingBackup(): Promise<number> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.countSessionsNeedingBackup();
- }
-
- /**
- * Get information about the encryption of an event
- *
- * @param event - event to be checked
- * @returns The event information.
- */
- public getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo {
- if (!this.cryptoBackend) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.cryptoBackend.getEventEncryptionInfo(event);
- }
-
- /**
- * Create a recovery key from a user-supplied passphrase.
- *
- * The Secure Secret Storage API is currently UNSTABLE and may change without notice.
- *
- * @param password - Passphrase string that can be entered by the user
- * when restoring the backup as an alternative to entering the recovery key.
- * Optional.
- * @returns Object with public key metadata, encoded private
- * recovery key which should be disposed of after displaying to the user,
- * and raw private key to avoid round tripping if needed.
- */
- public createRecoveryKeyFromPassphrase(password?: string): Promise<IRecoveryKey> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.createRecoveryKeyFromPassphrase(password);
- }
-
- /**
- * Checks whether secret storage:
- * - is enabled on this account
- * - is storing cross-signing private keys
- * - is storing session backup key (if enabled)
- *
- * If this function returns false, bootstrapSecretStorage() can be used
- * to fix things such that it returns true. That is to say, after
- * bootstrapSecretStorage() completes successfully, this function should
- * return true.
- *
- * The Secure Secret Storage API is currently UNSTABLE and may change without notice.
- *
- * @returns True if secret storage is ready to be used on this device
- */
- public isSecretStorageReady(): Promise<boolean> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.isSecretStorageReady();
- }
-
- /**
- * Bootstrap Secure Secret Storage if needed by creating a default key. If everything is
- * already set up, then no changes are made, so this is safe to run to ensure secret
- * storage is ready for use.
- *
- * This function
- * - creates a new Secure Secret Storage key if no default key exists
- * - if a key backup exists, it is migrated to store the key in the Secret
- * Storage
- * - creates a backup if none exists, and one is requested
- * - migrates Secure Secret Storage to use the latest algorithm, if an outdated
- * algorithm is found
- *
- */
- public bootstrapSecretStorage(opts: ICreateSecretStorageOpts): Promise<void> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.bootstrapSecretStorage(opts);
- }
-
- /**
- * Add a key for encrypting secrets.
- *
- * The Secure Secret Storage API is currently UNSTABLE and may change without notice.
- *
- * @param algorithm - the algorithm used by the key
- * @param opts - the options for the algorithm. The properties used
- * depend on the algorithm given.
- * @param keyName - the name of the key. If not given, a random name will be generated.
- *
- * @returns An object with:
- * keyId: the ID of the key
- * keyInfo: details about the key (iv, mac, passphrase)
- */
- public addSecretStorageKey(
- algorithm: string,
- opts: IAddSecretStorageKeyOpts,
- keyName?: string,
- ): Promise<{ keyId: string; keyInfo: SecretStorageKeyDescription }> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.addSecretStorageKey(algorithm, opts, keyName);
- }
-
- /**
- * Check whether we have a key with a given ID.
- *
- * The Secure Secret Storage API is currently UNSTABLE and may change without notice.
- *
- * @param keyId - The ID of the key to check
- * for. Defaults to the default key ID if not provided.
- * @returns Whether we have the key.
- */
- public hasSecretStorageKey(keyId?: string): Promise<boolean> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.hasSecretStorageKey(keyId);
- }
-
- /**
- * Store an encrypted secret on the server.
- *
- * The Secure Secret Storage API is currently UNSTABLE and may change without notice.
- *
- * @param name - The name of the secret
- * @param secret - The secret contents.
- * @param keys - The IDs of the keys to use to encrypt the secret or null/undefined
- * to use the default (will throw if no default key is set).
- */
- public storeSecret(name: string, secret: string, keys?: string[]): Promise<void> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.storeSecret(name, secret, keys);
- }
-
- /**
- * Get a secret from storage.
- *
- * The Secure Secret Storage API is currently UNSTABLE and may change without notice.
- *
- * @param name - the name of the secret
- *
- * @returns the contents of the secret
- */
- public getSecret(name: string): Promise<string | undefined> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.getSecret(name);
- }
-
- /**
- * Check if a secret is stored on the server.
- *
- * The Secure Secret Storage API is currently UNSTABLE and may change without notice.
- *
- * @param name - the name of the secret
- * @returns map of key name to key info the secret is encrypted
- * with, or null if it is not present or not encrypted with a trusted
- * key
- */
- public isSecretStored(name: string): Promise<Record<string, SecretStorageKeyDescription> | null> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.isSecretStored(name);
- }
-
- /**
- * Request a secret from another device.
- *
- * The Secure Secret Storage API is currently UNSTABLE and may change without notice.
- *
- * @param name - the name of the secret to request
- * @param devices - the devices to request the secret from
- *
- * @returns the secret request object
- */
- public requestSecret(name: string, devices: string[]): ISecretRequest {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.requestSecret(name, devices);
- }
-
- /**
- * Get the current default key ID for encrypting secrets.
- *
- * The Secure Secret Storage API is currently UNSTABLE and may change without notice.
- *
- * @returns The default key ID or null if no default key ID is set
- */
- public getDefaultSecretStorageKeyId(): Promise<string | null> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.getDefaultSecretStorageKeyId();
- }
-
- /**
- * Set the current default key ID for encrypting secrets.
- *
- * The Secure Secret Storage API is currently UNSTABLE and may change without notice.
- *
- * @param keyId - The new default key ID
- */
- public setDefaultSecretStorageKeyId(keyId: string): Promise<void> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.setDefaultSecretStorageKeyId(keyId);
- }
-
- /**
- * Checks that a given secret storage private key matches a given public key.
- * This can be used by the getSecretStorageKey callback to verify that the
- * private key it is about to supply is the one that was requested.
- *
- * The Secure Secret Storage API is currently UNSTABLE and may change without notice.
- *
- * @param privateKey - The private key
- * @param expectedPublicKey - The public key
- * @returns true if the key matches, otherwise false
- */
- public checkSecretStoragePrivateKey(privateKey: Uint8Array, expectedPublicKey: string): boolean {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.checkSecretStoragePrivateKey(privateKey, expectedPublicKey);
- }
-
- /**
- * Get e2e information on the device that sent an event
- *
- * @param event - event to be checked
- */
- public async getEventSenderDeviceInfo(event: MatrixEvent): Promise<DeviceInfo | null> {
- if (!this.crypto) {
- return null;
- }
- return this.crypto.getEventSenderDeviceInfo(event);
- }
-
- /**
- * Check if the sender of an event is verified
- *
- * @param event - event to be checked
- *
- * @returns true if the sender of this event has been verified using
- * {@link MatrixClient#setDeviceVerified}.
- */
- public async isEventSenderVerified(event: MatrixEvent): Promise<boolean> {
- const device = await this.getEventSenderDeviceInfo(event);
- if (!device) {
- return false;
- }
- return device.isVerified();
- }
-
- /**
- * Get outgoing room key request for this event if there is one.
- * @param event - The event to check for
- *
- * @returns A room key request, or null if there is none
- */
- public getOutgoingRoomKeyRequest(event: MatrixEvent): Promise<OutgoingRoomKeyRequest | null> {
- if (!this.crypto) {
- throw new Error("End-to-End encryption disabled");
- }
- const wireContent = event.getWireContent();
- const requestBody: IRoomKeyRequestBody = {
- session_id: wireContent.session_id,
- sender_key: wireContent.sender_key,
- algorithm: wireContent.algorithm,
- room_id: event.getRoomId()!,
- };
- if (!requestBody.session_id || !requestBody.sender_key || !requestBody.algorithm || !requestBody.room_id) {
- return Promise.resolve(null);
- }
- return this.crypto.cryptoStore.getOutgoingRoomKeyRequest(requestBody);
- }
-
- /**
- * Cancel a room key request for this event if one is ongoing and resend the
- * request.
- * @param event - event of which to cancel and resend the room
- * key request.
- * @returns A promise that will resolve when the key request is queued
- */
- public cancelAndResendEventRoomKeyRequest(event: MatrixEvent): Promise<void> {
- if (!this.crypto) {
- throw new Error("End-to-End encryption disabled");
- }
- return event.cancelAndResendKeyRequest(this.crypto, this.getUserId()!);
- }
-
- /**
- * Enable end-to-end encryption for a room. This does not modify room state.
- * Any messages sent before the returned promise resolves will be sent unencrypted.
- * @param roomId - The room ID to enable encryption in.
- * @param config - The encryption config for the room.
- * @returns A promise that will resolve when encryption is set up.
- */
- public setRoomEncryption(roomId: string, config: IRoomEncryption): Promise<void> {
- if (!this.crypto) {
- throw new Error("End-to-End encryption disabled");
- }
- return this.crypto.setRoomEncryption(roomId, config);
- }
-
- /**
- * Whether encryption is enabled for a room.
- * @param roomId - the room id to query.
- * @returns whether encryption is enabled.
- */
- public isRoomEncrypted(roomId: string): boolean {
- const room = this.getRoom(roomId);
- if (!room) {
- // we don't know about this room, so can't determine if it should be
- // encrypted. Let's assume not.
- return false;
- }
-
- // if there is an 'm.room.encryption' event in this room, it should be
- // encrypted (independently of whether we actually support encryption)
- const ev = room.currentState.getStateEvents(EventType.RoomEncryption, "");
- if (ev) {
- return true;
- }
-
- // we don't have an m.room.encrypted event, but that might be because
- // the server is hiding it from us. Check the store to see if it was
- // previously encrypted.
- return this.roomList.isRoomEncrypted(roomId);
- }
-
- /**
- * Encrypts and sends a given object via Olm to-device messages to a given
- * set of devices.
- *
- * @param userDeviceMap - mapping from userId to deviceInfo
- *
- * @param payload - fields to include in the encrypted payload
- *
- * @returns Promise which
- * resolves once the message has been encrypted and sent to the given
- * userDeviceMap, and returns the `{ contentMap, deviceInfoByDeviceId }`
- * of the successfully sent messages.
- */
- public encryptAndSendToDevices(userDeviceInfoArr: IOlmDevice<DeviceInfo>[], payload: object): Promise<void> {
- if (!this.crypto) {
- throw new Error("End-to-End encryption disabled");
- }
- return this.crypto.encryptAndSendToDevices(userDeviceInfoArr, payload);
- }
-
- /**
- * Forces the current outbound group session to be discarded such
- * that another one will be created next time an event is sent.
- *
- * @param roomId - The ID of the room to discard the session for
- *
- * @deprecated Prefer {@link CryptoApi.forceDiscardSession | `CryptoApi.forceDiscardSession`}:
- *
- */
- public forceDiscardSession(roomId: string): void {
- if (!this.cryptoBackend) {
- throw new Error("End-to-End encryption disabled");
- }
- this.cryptoBackend.forceDiscardSession(roomId);
- }
-
- /**
- * Get a list containing all of the room keys
- *
- * This should be encrypted before returning it to the user.
- *
- * @returns a promise which resolves to a list of session export objects
- *
- * @deprecated Prefer {@link CryptoApi.exportRoomKeys | `CryptoApi.exportRoomKeys`}:
- *
- * ```javascript
- * sessionData = await client.getCrypto().exportRoomKeys();
- * ```
- */
- public exportRoomKeys(): Promise<IMegolmSessionData[]> {
- if (!this.cryptoBackend) {
- return Promise.reject(new Error("End-to-end encryption disabled"));
- }
- return this.cryptoBackend.exportRoomKeys();
- }
-
- /**
- * Import a list of room keys previously exported by exportRoomKeys
- *
- * @param keys - a list of session export objects
- *
- * @returns a promise which resolves when the keys have been imported
- */
- public importRoomKeys(keys: IMegolmSessionData[], opts?: IImportRoomKeysOpts): Promise<void> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.importRoomKeys(keys, opts);
- }
-
- /**
- * Force a re-check of the local key backup status against
- * what's on the server.
- *
- * @returns Object with backup info (as returned by
- * getKeyBackupVersion) in backupInfo and
- * trust information (as returned by isKeyBackupTrusted)
- * in trustInfo.
- */
- public checkKeyBackup(): Promise<IKeyBackupCheck | null> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.backupManager.checkKeyBackup();
- }
-
- /**
- * Get information about the current key backup.
- * @returns Information object from API or null
- */
- public async getKeyBackupVersion(): Promise<IKeyBackupInfo | null> {
- let res: IKeyBackupInfo;
- try {
- res = await this.http.authedRequest<IKeyBackupInfo>(
- Method.Get,
- "/room_keys/version",
- undefined,
- undefined,
- { prefix: ClientPrefix.V3 },
- );
- } catch (e) {
- if ((<MatrixError>e).errcode === "M_NOT_FOUND") {
- return null;
- } else {
- throw e;
- }
- }
- BackupManager.checkBackupVersion(res);
- return res;
- }
-
- /**
- * @param info - key backup info dict from getKeyBackupVersion()
- */
- public isKeyBackupTrusted(info: IKeyBackupInfo): Promise<TrustInfo> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.backupManager.isKeyBackupTrusted(info);
- }
-
- /**
- * @returns true if the client is configured to back up keys to
- * the server, otherwise false. If we haven't completed a successful check
- * of key backup status yet, returns null.
- */
- public getKeyBackupEnabled(): boolean | null {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- return this.crypto.backupManager.getKeyBackupEnabled();
- }
-
- /**
- * Enable backing up of keys, using data previously returned from
- * getKeyBackupVersion.
- *
- * @param info - Backup information object as returned by getKeyBackupVersion
- * @returns Promise which resolves when complete.
- */
- public enableKeyBackup(info: IKeyBackupInfo): Promise<void> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
-
- return this.crypto.backupManager.enableKeyBackup(info);
- }
-
- /**
- * Disable backing up of keys.
- */
- public disableKeyBackup(): void {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
-
- this.crypto.backupManager.disableKeyBackup();
- }
-
- /**
- * Set up the data required to create a new backup version. The backup version
- * will not be created and enabled until createKeyBackupVersion is called.
- *
- * @param password - Passphrase string that can be entered by the user
- * when restoring the backup as an alternative to entering the recovery key.
- * Optional.
- *
- * @returns Object that can be passed to createKeyBackupVersion and
- * additionally has a 'recovery_key' member with the user-facing recovery key string.
- */
- public async prepareKeyBackupVersion(
- password?: string | Uint8Array | null,
- opts: IKeyBackupPrepareOpts = { secureSecretStorage: false },
- ): Promise<Pick<IPreparedKeyBackupVersion, "algorithm" | "auth_data" | "recovery_key">> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
-
- // eslint-disable-next-line camelcase
- const { algorithm, auth_data, recovery_key, privateKey } =
- await this.crypto.backupManager.prepareKeyBackupVersion(password);
-
- if (opts.secureSecretStorage) {
- await this.storeSecret("m.megolm_backup.v1", encodeBase64(privateKey));
- logger.info("Key backup private key stored in secret storage");
- }
-
- return {
- algorithm,
- /* eslint-disable camelcase */
- auth_data,
- recovery_key,
- /* eslint-enable camelcase */
- };
- }
-
- /**
- * Check whether the key backup private key is stored in secret storage.
- * @returns 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 isKeyBackupKeyStored(): Promise<Record<string, SecretStorageKeyDescription> | null> {
- return Promise.resolve(this.isSecretStored("m.megolm_backup.v1"));
- }
-
- /**
- * Create a new key backup version and enable it, using the information return
- * from prepareKeyBackupVersion.
- *
- * @param info - Info object from prepareKeyBackupVersion
- * @returns Object with 'version' param indicating the version created
- */
- public async createKeyBackupVersion(info: IKeyBackupInfo): Promise<IKeyBackupInfo> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
-
- await this.crypto.backupManager.createKeyBackupVersion(info);
-
- const data = {
- algorithm: info.algorithm,
- auth_data: info.auth_data,
- };
-
- // Sign the backup auth data with the device key for backwards compat with
- // older devices with cross-signing. This can probably go away very soon in
- // favour of just signing with the cross-singing master key.
- // XXX: Private member access
- await this.crypto.signObject(data.auth_data);
-
- if (
- this.cryptoCallbacks.getCrossSigningKey &&
- // XXX: Private member access
- this.crypto.crossSigningInfo.getId()
- ) {
- // now also sign the auth data with the cross-signing master key
- // we check for the callback explicitly here because we still want to be able
- // to create an un-cross-signed key backup if there is a cross-signing key but
- // no callback supplied.
- // XXX: Private member access
- await this.crypto.crossSigningInfo.signObject(data.auth_data, "master");
- }
-
- const res = await this.http.authedRequest<IKeyBackupInfo>(Method.Post, "/room_keys/version", undefined, data, {
- prefix: ClientPrefix.V3,
- });
-
- // We could assume everything's okay and enable directly, but this ensures
- // we run the same signature verification that will be used for future
- // sessions.
- await this.checkKeyBackup();
- if (!this.getKeyBackupEnabled()) {
- logger.error("Key backup not usable even though we just created it");
- }
-
- return res;
- }
-
- public async deleteKeyBackupVersion(version: string): Promise<void> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
-
- // If we're currently backing up to this backup... stop.
- // (We start using it automatically in createKeyBackupVersion
- // so this is symmetrical).
- if (this.crypto.backupManager.version) {
- this.crypto.backupManager.disableKeyBackup();
- }
-
- const path = utils.encodeUri("/room_keys/version/$version", {
- $version: version,
- });
-
- await this.http.authedRequest(Method.Delete, path, undefined, undefined, { prefix: ClientPrefix.V3 });
- }
-
- private makeKeyBackupPath(roomId: undefined, sessionId: undefined, version?: string): IKeyBackupPath;
- private makeKeyBackupPath(roomId: string, sessionId: undefined, version?: string): IKeyBackupPath;
- private makeKeyBackupPath(roomId: string, sessionId: string, version?: string): IKeyBackupPath;
- private makeKeyBackupPath(roomId?: string, sessionId?: string, version?: string): IKeyBackupPath {
- let path: string;
- if (sessionId !== undefined) {
- path = utils.encodeUri("/room_keys/keys/$roomId/$sessionId", {
- $roomId: roomId!,
- $sessionId: sessionId,
- });
- } else if (roomId !== undefined) {
- path = utils.encodeUri("/room_keys/keys/$roomId", {
- $roomId: roomId,
- });
- } else {
- path = "/room_keys/keys";
- }
- const queryData = version === undefined ? undefined : { version };
- return { path, queryData };
- }
-
- /**
- * Back up session keys to the homeserver.
- * @param roomId - ID of the room that the keys are for Optional.
- * @param sessionId - ID of the session that the keys are for Optional.
- * @param version - backup version Optional.
- * @param data - Object keys to send
- * @returns a promise that will resolve when the keys
- * are uploaded
- */
- public sendKeyBackup(
- roomId: undefined,
- sessionId: undefined,
- version: string | undefined,
- data: IKeyBackup,
- ): Promise<void>;
- public sendKeyBackup(
- roomId: string,
- sessionId: undefined,
- version: string | undefined,
- data: IKeyBackup,
- ): Promise<void>;
- public sendKeyBackup(
- roomId: string,
- sessionId: string,
- version: string | undefined,
- data: IKeyBackup,
- ): Promise<void>;
- public async sendKeyBackup(
- roomId: string | undefined,
- sessionId: string | undefined,
- version: string | undefined,
- data: IKeyBackup,
- ): Promise<void> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
-
- const path = this.makeKeyBackupPath(roomId!, sessionId!, version);
- await this.http.authedRequest(Method.Put, path.path, path.queryData, data, { prefix: ClientPrefix.V3 });
- }
-
- /**
- * Marks all group sessions as needing to be backed up and schedules them to
- * upload in the background as soon as possible.
- */
- public async scheduleAllGroupSessionsForBackup(): Promise<void> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
-
- await this.crypto.backupManager.scheduleAllGroupSessionsForBackup();
- }
-
- /**
- * Marks all group sessions as needing to be backed up without scheduling
- * them to upload in the background.
- * @returns Promise which resolves to the number of sessions requiring a backup.
- */
- public flagAllGroupSessionsForBackup(): Promise<number> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
-
- return this.crypto.backupManager.flagAllGroupSessionsForBackup();
- }
-
- public isValidRecoveryKey(recoveryKey: string): boolean {
- try {
- decodeRecoveryKey(recoveryKey);
- return true;
- } catch (e) {
- return false;
- }
- }
-
- /**
- * Get the raw key for a key backup from the password
- * Used when migrating key backups into SSSS
- *
- * The cross-signing API is currently UNSTABLE and may change without notice.
- *
- * @param password - Passphrase
- * @param backupInfo - Backup metadata from `checkKeyBackup`
- * @returns key backup key
- */
- public keyBackupKeyFromPassword(password: string, backupInfo: IKeyBackupInfo): Promise<Uint8Array> {
- return keyFromAuthData(backupInfo.auth_data, password);
- }
-
- /**
- * Get the raw key for a key backup from the recovery key
- * Used when migrating key backups into SSSS
- *
- * The cross-signing API is currently UNSTABLE and may change without notice.
- *
- * @param recoveryKey - The recovery key
- * @returns key backup key
- */
- public keyBackupKeyFromRecoveryKey(recoveryKey: string): Uint8Array {
- return decodeRecoveryKey(recoveryKey);
- }
-
- /**
- * Restore from an existing key backup via a passphrase.
- *
- * @param password - Passphrase
- * @param targetRoomId - Room ID to target a specific room.
- * Restores all rooms if omitted.
- * @param targetSessionId - Session ID to target a specific session.
- * Restores all sessions if omitted.
- * @param backupInfo - Backup metadata from `checkKeyBackup`
- * @param opts - Optional params such as callbacks
- * @returns Status of restoration with `total` and `imported`
- * key counts.
- */
- public async restoreKeyBackupWithPassword(
- password: string,
- targetRoomId: undefined,
- targetSessionId: undefined,
- backupInfo: IKeyBackupInfo,
- opts: IKeyBackupRestoreOpts,
- ): Promise<IKeyBackupRestoreResult>;
- public async restoreKeyBackupWithPassword(
- password: string,
- targetRoomId: string,
- targetSessionId: undefined,
- backupInfo: IKeyBackupInfo,
- opts: IKeyBackupRestoreOpts,
- ): Promise<IKeyBackupRestoreResult>;
- public async restoreKeyBackupWithPassword(
- password: string,
- targetRoomId: string,
- targetSessionId: string,
- backupInfo: IKeyBackupInfo,
- opts: IKeyBackupRestoreOpts,
- ): Promise<IKeyBackupRestoreResult>;
- public async restoreKeyBackupWithPassword(
- password: string,
- targetRoomId: string | undefined,
- targetSessionId: string | undefined,
- backupInfo: IKeyBackupInfo,
- opts: IKeyBackupRestoreOpts,
- ): Promise<IKeyBackupRestoreResult> {
- const privKey = await keyFromAuthData(backupInfo.auth_data, password);
- return this.restoreKeyBackup(privKey, targetRoomId!, targetSessionId!, backupInfo, opts);
- }
-
- /**
- * Restore from an existing key backup via a private key stored in secret
- * storage.
- *
- * @param backupInfo - Backup metadata from `checkKeyBackup`
- * @param targetRoomId - Room ID to target a specific room.
- * Restores all rooms if omitted.
- * @param targetSessionId - Session ID to target a specific session.
- * Restores all sessions if omitted.
- * @param opts - Optional params such as callbacks
- * @returns Status of restoration with `total` and `imported`
- * key counts.
- */
- public async restoreKeyBackupWithSecretStorage(
- backupInfo: IKeyBackupInfo,
- targetRoomId?: string,
- targetSessionId?: string,
- opts?: IKeyBackupRestoreOpts,
- ): Promise<IKeyBackupRestoreResult> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- const storedKey = await this.getSecret("m.megolm_backup.v1");
-
- // ensure that the key is in the right format. If not, fix the key and
- // store the fixed version
- const fixedKey = fixBackupKey(storedKey);
- if (fixedKey) {
- const keys = await this.crypto.getSecretStorageKey();
- await this.storeSecret("m.megolm_backup.v1", fixedKey, [keys![0]]);
- }
-
- const privKey = decodeBase64(fixedKey || storedKey!);
- return this.restoreKeyBackup(privKey, targetRoomId!, targetSessionId!, backupInfo, opts);
- }
-
- /**
- * Restore from an existing key backup via an encoded recovery key.
- *
- * @param recoveryKey - Encoded recovery key
- * @param targetRoomId - Room ID to target a specific room.
- * Restores all rooms if omitted.
- * @param targetSessionId - Session ID to target a specific session.
- * Restores all sessions if omitted.
- * @param backupInfo - Backup metadata from `checkKeyBackup`
- * @param opts - Optional params such as callbacks
-
- * @returns Status of restoration with `total` and `imported`
- * key counts.
- */
- public restoreKeyBackupWithRecoveryKey(
- recoveryKey: string,
- targetRoomId: undefined,
- targetSessionId: undefined,
- backupInfo: IKeyBackupInfo,
- opts?: IKeyBackupRestoreOpts,
- ): Promise<IKeyBackupRestoreResult>;
- public restoreKeyBackupWithRecoveryKey(
- recoveryKey: string,
- targetRoomId: string,
- targetSessionId: undefined,
- backupInfo: IKeyBackupInfo,
- opts?: IKeyBackupRestoreOpts,
- ): Promise<IKeyBackupRestoreResult>;
- public restoreKeyBackupWithRecoveryKey(
- recoveryKey: string,
- targetRoomId: string,
- targetSessionId: string,
- backupInfo: IKeyBackupInfo,
- opts?: IKeyBackupRestoreOpts,
- ): Promise<IKeyBackupRestoreResult>;
- public restoreKeyBackupWithRecoveryKey(
- recoveryKey: string,
- targetRoomId: string | undefined,
- targetSessionId: string | undefined,
- backupInfo: IKeyBackupInfo,
- opts?: IKeyBackupRestoreOpts,
- ): Promise<IKeyBackupRestoreResult> {
- const privKey = decodeRecoveryKey(recoveryKey);
- return this.restoreKeyBackup(privKey, targetRoomId!, targetSessionId!, backupInfo, opts);
- }
-
- public async restoreKeyBackupWithCache(
- targetRoomId: undefined,
- targetSessionId: undefined,
- backupInfo: IKeyBackupInfo,
- opts?: IKeyBackupRestoreOpts,
- ): Promise<IKeyBackupRestoreResult>;
- public async restoreKeyBackupWithCache(
- targetRoomId: string,
- targetSessionId: undefined,
- backupInfo: IKeyBackupInfo,
- opts?: IKeyBackupRestoreOpts,
- ): Promise<IKeyBackupRestoreResult>;
- public async restoreKeyBackupWithCache(
- targetRoomId: string,
- targetSessionId: string,
- backupInfo: IKeyBackupInfo,
- opts?: IKeyBackupRestoreOpts,
- ): Promise<IKeyBackupRestoreResult>;
- public async restoreKeyBackupWithCache(
- targetRoomId: string | undefined,
- targetSessionId: string | undefined,
- backupInfo: IKeyBackupInfo,
- opts?: IKeyBackupRestoreOpts,
- ): Promise<IKeyBackupRestoreResult> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
- const privKey = await this.crypto.getSessionBackupPrivateKey();
- if (!privKey) {
- throw new Error("Couldn't get key");
- }
- return this.restoreKeyBackup(privKey, targetRoomId!, targetSessionId!, backupInfo, opts);
- }
-
- private async restoreKeyBackup(
- privKey: ArrayLike<number>,
- targetRoomId: undefined,
- targetSessionId: undefined,
- backupInfo: IKeyBackupInfo,
- opts?: IKeyBackupRestoreOpts,
- ): Promise<IKeyBackupRestoreResult>;
- private async restoreKeyBackup(
- privKey: ArrayLike<number>,
- targetRoomId: string,
- targetSessionId: undefined,
- backupInfo: IKeyBackupInfo,
- opts?: IKeyBackupRestoreOpts,
- ): Promise<IKeyBackupRestoreResult>;
- private async restoreKeyBackup(
- privKey: ArrayLike<number>,
- targetRoomId: string,
- targetSessionId: string,
- backupInfo: IKeyBackupInfo,
- opts?: IKeyBackupRestoreOpts,
- ): Promise<IKeyBackupRestoreResult>;
- private async restoreKeyBackup(
- privKey: ArrayLike<number>,
- targetRoomId: string | undefined,
- targetSessionId: string | undefined,
- backupInfo: IKeyBackupInfo,
- opts?: IKeyBackupRestoreOpts,
- ): Promise<IKeyBackupRestoreResult> {
- const cacheCompleteCallback = opts?.cacheCompleteCallback;
- const progressCallback = opts?.progressCallback;
-
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
-
- let totalKeyCount = 0;
- let keys: IMegolmSessionData[] = [];
-
- const path = this.makeKeyBackupPath(targetRoomId!, targetSessionId!, backupInfo.version);
-
- const algorithm = await BackupManager.makeAlgorithm(backupInfo, async () => {
- return privKey;
- });
-
- const untrusted = algorithm.untrusted;
-
- try {
- // If the pubkey computed from the private data we've been given
- // doesn't match the one in the auth_data, the user has entered
- // a different recovery key / the wrong passphrase.
- if (!(await algorithm.keyMatches(privKey))) {
- return Promise.reject(new MatrixError({ errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY }));
- }
-
- // Cache the key, if possible.
- // This is async.
- this.crypto
- .storeSessionBackupPrivateKey(privKey)
- .catch((e) => {
- logger.warn("Error caching session backup key:", e);
- })
- .then(cacheCompleteCallback);
-
- if (progressCallback) {
- progressCallback({
- stage: "fetch",
- });
- }
-
- const res = await this.http.authedRequest<IRoomsKeysResponse | IRoomKeysResponse | IKeyBackupSession>(
- Method.Get,
- path.path,
- path.queryData,
- undefined,
- { prefix: ClientPrefix.V3 },
- );
-
- if ((res as IRoomsKeysResponse).rooms) {
- const rooms = (res as IRoomsKeysResponse).rooms;
- for (const [roomId, roomData] of Object.entries(rooms)) {
- if (!roomData.sessions) continue;
-
- totalKeyCount += Object.keys(roomData.sessions).length;
- const roomKeys = await algorithm.decryptSessions(roomData.sessions);
- for (const k of roomKeys) {
- k.room_id = roomId;
- keys.push(k);
- }
- }
- } else if ((res as IRoomKeysResponse).sessions) {
- const sessions = (res as IRoomKeysResponse).sessions;
- totalKeyCount = Object.keys(sessions).length;
- keys = await algorithm.decryptSessions(sessions);
- for (const k of keys) {
- k.room_id = targetRoomId!;
- }
- } else {
- totalKeyCount = 1;
- try {
- const [key] = await algorithm.decryptSessions({
- [targetSessionId!]: res as IKeyBackupSession,
- });
- key.room_id = targetRoomId!;
- key.session_id = targetSessionId!;
- keys.push(key);
- } catch (e) {
- logger.log("Failed to decrypt megolm session from backup", e);
- }
- }
- } finally {
- algorithm.free();
- }
-
- await this.importRoomKeys(keys, {
- progressCallback,
- untrusted,
- source: "backup",
- });
-
- await this.checkKeyBackup();
-
- return { total: totalKeyCount, imported: keys.length };
- }
-
- public deleteKeysFromBackup(roomId: undefined, sessionId: undefined, version?: string): Promise<void>;
- public deleteKeysFromBackup(roomId: string, sessionId: undefined, version?: string): Promise<void>;
- public deleteKeysFromBackup(roomId: string, sessionId: string, version?: string): Promise<void>;
- public async deleteKeysFromBackup(roomId?: string, sessionId?: string, version?: string): Promise<void> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
-
- const path = this.makeKeyBackupPath(roomId!, sessionId!, version);
- await this.http.authedRequest(Method.Delete, path.path, path.queryData, undefined, { prefix: ClientPrefix.V3 });
- }
-
- /**
- * Share shared-history decryption keys with the given users.
- *
- * @param roomId - the room for which keys should be shared.
- * @param userIds - a list of users to share with. The keys will be sent to
- * all of the user's current devices.
- */
- public async sendSharedHistoryKeys(roomId: string, userIds: string[]): Promise<void> {
- if (!this.crypto) {
- throw new Error("End-to-end encryption disabled");
- }
-
- const roomEncryption = this.roomList.getRoomEncryption(roomId);
- if (!roomEncryption) {
- // unknown room, or unencrypted room
- logger.error("Unknown room. Not sharing decryption keys");
- return;
- }
-
- const deviceInfos = await this.crypto.downloadKeys(userIds);
- const devicesByUser: Map<string, DeviceInfo[]> = new Map();
- for (const [userId, devices] of deviceInfos) {
- devicesByUser.set(userId, Array.from(devices.values()));
- }
-
- // XXX: Private member access
- const alg = this.crypto.getRoomDecryptor(roomId, roomEncryption.algorithm);
- if (alg.sendSharedHistoryInboundSessions) {
- await alg.sendSharedHistoryInboundSessions(devicesByUser);
- } else {
- logger.warn("Algorithm does not support sharing previous keys", roomEncryption.algorithm);
- }
- }
-
- /**
- * Get the config for the media repository.
- * @returns Promise which resolves with an object containing the config.
- */
- public getMediaConfig(): Promise<IMediaConfig> {
- return this.http.authedRequest(Method.Get, "/config", undefined, undefined, {
- prefix: MediaPrefix.R0,
- });
- }
-
- /**
- * Get the room for the given room ID.
- * This function will return a valid room for any room for which a Room event
- * has been emitted. Note in particular that other events, eg. RoomState.members
- * will be emitted for a room before this function will return the given room.
- * @param roomId - The room ID
- * @returns The Room or null if it doesn't exist or there is no data store.
- */
- public getRoom(roomId: string | undefined): Room | null {
- if (!roomId) {
- return null;
- }
- return this.store.getRoom(roomId);
- }
-
- /**
- * Retrieve all known rooms.
- * @returns A list of rooms, or an empty list if there is no data store.
- */
- public getRooms(): Room[] {
- return this.store.getRooms();
- }
-
- /**
- * Retrieve all rooms that should be displayed to the user
- * This is essentially getRooms() with some rooms filtered out, eg. old versions
- * of rooms that have been replaced or (in future) other rooms that have been
- * marked at the protocol level as not to be displayed to the user.
- *
- * @param msc3946ProcessDynamicPredecessor - if true, look for an
- * m.room.predecessor state event and
- * use it if found (MSC3946).
- * @returns A list of rooms, or an empty list if there is no data store.
- */
- public getVisibleRooms(msc3946ProcessDynamicPredecessor = false): Room[] {
- const allRooms = this.store.getRooms();
-
- const replacedRooms = new Set();
- for (const r of allRooms) {
- const predecessor = r.findPredecessor(msc3946ProcessDynamicPredecessor)?.roomId;
- if (predecessor) {
- replacedRooms.add(predecessor);
- }
- }
-
- return allRooms.filter((r) => {
- const tombstone = r.currentState.getStateEvents(EventType.RoomTombstone, "");
- if (tombstone && replacedRooms.has(r.roomId)) {
- return false;
- }
- return true;
- });
- }
-
- /**
- * Retrieve a user.
- * @param userId - The user ID to retrieve.
- * @returns A user or null if there is no data store or the user does
- * not exist.
- */
- public getUser(userId: string): User | null {
- return this.store.getUser(userId);
- }
-
- /**
- * Retrieve all known users.
- * @returns A list of users, or an empty list if there is no data store.
- */
- public getUsers(): User[] {
- return this.store.getUsers();
- }
-
- /**
- * Set account data event for the current user.
- * It will retry the request up to 5 times.
- * @param eventType - The event type
- * @param content - the contents object for the event
- * @returns Promise which resolves: an empty object
- * @returns Rejects: with an error response.
- */
- public setAccountData(eventType: EventType | string, content: IContent): Promise<{}> {
- const path = utils.encodeUri("/user/$userId/account_data/$type", {
- $userId: this.credentials.userId!,
- $type: eventType,
- });
- return retryNetworkOperation(5, () => {
- return this.http.authedRequest(Method.Put, path, undefined, content);
- });
- }
-
- /**
- * Get account data event of given type for the current user.
- * @param eventType - The event type
- * @returns The contents of the given account data event
- */
- public getAccountData(eventType: string): MatrixEvent | undefined {
- return this.store.getAccountData(eventType);
- }
-
- /**
- * Get account data event of given type for the current user. This variant
- * gets account data directly from the homeserver if the local store is not
- * ready, which can be useful very early in startup before the initial sync.
- * @param eventType - The event type
- * @returns Promise which resolves: The contents of the given account data event.
- * @returns Rejects: with an error response.
- */
- public async getAccountDataFromServer<T extends { [k: string]: any }>(eventType: string): Promise<T | null> {
- if (this.isInitialSyncComplete()) {
- const event = this.store.getAccountData(eventType);
- if (!event) {
- return null;
- }
- // The network version below returns just the content, so this branch
- // does the same to match.
- return event.getContent<T>();
- }
- const path = utils.encodeUri("/user/$userId/account_data/$type", {
- $userId: this.credentials.userId!,
- $type: eventType,
- });
- try {
- return await this.http.authedRequest(Method.Get, path);
- } catch (e) {
- if ((<MatrixError>e).data?.errcode === "M_NOT_FOUND") {
- return null;
- }
- throw e;
- }
- }
-
- public async deleteAccountData(eventType: string): Promise<void> {
- const msc3391DeleteAccountDataServerSupport = this.canSupport.get(Feature.AccountDataDeletion);
- // if deletion is not supported overwrite with empty content
- if (msc3391DeleteAccountDataServerSupport === ServerSupport.Unsupported) {
- await this.setAccountData(eventType, {});
- return;
- }
- const path = utils.encodeUri("/user/$userId/account_data/$type", {
- $userId: this.getSafeUserId(),
- $type: eventType,
- });
- const options =
- msc3391DeleteAccountDataServerSupport === ServerSupport.Unstable
- ? { prefix: "/_matrix/client/unstable/org.matrix.msc3391" }
- : undefined;
- return await this.http.authedRequest(Method.Delete, path, undefined, undefined, options);
- }
-
- /**
- * Gets the users that are ignored by this client
- * @returns The array of users that are ignored (empty if none)
- */
- public getIgnoredUsers(): string[] {
- const event = this.getAccountData("m.ignored_user_list");
- if (!event || !event.getContent() || !event.getContent()["ignored_users"]) return [];
- return Object.keys(event.getContent()["ignored_users"]);
- }
-
- /**
- * Sets the users that the current user should ignore.
- * @param userIds - the user IDs to ignore
- * @returns Promise which resolves: an empty object
- * @returns Rejects: with an error response.
- */
- public setIgnoredUsers(userIds: string[]): Promise<{}> {
- const content = { ignored_users: {} as Record<string, object> };
- userIds.forEach((u) => {
- content.ignored_users[u] = {};
- });
- return this.setAccountData("m.ignored_user_list", content);
- }
-
- /**
- * Gets whether or not a specific user is being ignored by this client.
- * @param userId - the user ID to check
- * @returns true if the user is ignored, false otherwise
- */
- public isUserIgnored(userId: string): boolean {
- return this.getIgnoredUsers().includes(userId);
- }
-
- /**
- * Join a room. If you have already joined the room, this will no-op.
- * @param roomIdOrAlias - The room ID or room alias to join.
- * @param opts - Options when joining the room.
- * @returns Promise which resolves: Room object.
- * @returns Rejects: with an error response.
- */
- public async joinRoom(roomIdOrAlias: string, opts: IJoinRoomOpts = {}): Promise<Room> {
- if (opts.syncRoom === undefined) {
- opts.syncRoom = true;
- }
-
- const room = this.getRoom(roomIdOrAlias);
- if (room?.hasMembershipState(this.credentials.userId!, "join")) {
- return Promise.resolve(room);
- }
-
- let signPromise: Promise<IThirdPartySigned | void> = Promise.resolve();
-
- if (opts.inviteSignUrl) {
- const url = new URL(opts.inviteSignUrl);
- url.searchParams.set("mxid", this.credentials.userId!);
- signPromise = this.http.requestOtherUrl<IThirdPartySigned>(Method.Post, url);
- }
-
- const queryString: Record<string, string | string[]> = {};
- if (opts.viaServers) {
- queryString["server_name"] = opts.viaServers;
- }
-
- try {
- const data: IJoinRequestBody = {};
- const signedInviteObj = await signPromise;
- if (signedInviteObj) {
- data.third_party_signed = signedInviteObj;
- }
-
- const path = utils.encodeUri("/join/$roomid", { $roomid: roomIdOrAlias });
- const res = await this.http.authedRequest<{ room_id: string }>(Method.Post, path, queryString, data);
-
- const roomId = res.room_id;
- const syncApi = new SyncApi(this, this.clientOpts, this.buildSyncApiOptions());
- const room = syncApi.createRoom(roomId);
- if (opts.syncRoom) {
- // v2 will do this for us
- // return syncApi.syncRoom(room);
- }
- return room;
- } catch (e) {
- throw e; // rethrow for reject
- }
- }
-
- /**
- * Resend an event. Will also retry any to-device messages waiting to be sent.
- * @param event - The event to resend.
- * @param room - Optional. The room the event is in. Will update the
- * timeline entry if provided.
- * @returns Promise which resolves: to an ISendEventResponse object
- * @returns Rejects: with an error response.
- */
- public resendEvent(event: MatrixEvent, room: Room): Promise<ISendEventResponse> {
- // also kick the to-device queue to retry
- this.toDeviceMessageQueue.sendQueue();
-
- this.updatePendingEventStatus(room, event, EventStatus.SENDING);
- return this.encryptAndSendEvent(room, event);
- }
-
- /**
- * Cancel a queued or unsent event.
- *
- * @param event - Event to cancel
- * @throws Error if the event is not in QUEUED, NOT_SENT or ENCRYPTING state
- */
- public cancelPendingEvent(event: MatrixEvent): void {
- if (![EventStatus.QUEUED, EventStatus.NOT_SENT, EventStatus.ENCRYPTING].includes(event.status!)) {
- throw new Error("cannot cancel an event with status " + event.status);
- }
-
- // if the event is currently being encrypted then
- if (event.status === EventStatus.ENCRYPTING) {
- this.pendingEventEncryption.delete(event.getId()!);
- } else if (this.scheduler && event.status === EventStatus.QUEUED) {
- // tell the scheduler to forget about it, if it's queued
- this.scheduler.removeEventFromQueue(event);
- }
-
- // then tell the room about the change of state, which will remove it
- // from the room's list of pending events.
- const room = this.getRoom(event.getRoomId());
- this.updatePendingEventStatus(room, event, EventStatus.CANCELLED);
- }
-
- /**
- * @returns Promise which resolves: TODO
- * @returns Rejects: with an error response.
- */
- public setRoomName(roomId: string, name: string): Promise<ISendEventResponse> {
- return this.sendStateEvent(roomId, EventType.RoomName, { name: name });
- }
-
- /**
- * @param htmlTopic - Optional.
- * @returns Promise which resolves: TODO
- * @returns Rejects: with an error response.
- */
- public setRoomTopic(roomId: string, topic: string, htmlTopic?: string): Promise<ISendEventResponse> {
- const content = ContentHelpers.makeTopicContent(topic, htmlTopic);
- return this.sendStateEvent(roomId, EventType.RoomTopic, content);
- }
-
- /**
- * @returns Promise which resolves: to an object keyed by tagId with objects containing a numeric order field.
- * @returns Rejects: with an error response.
- */
- public getRoomTags(roomId: string): Promise<ITagsResponse> {
- const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags", {
- $userId: this.credentials.userId!,
- $roomId: roomId,
- });
- return this.http.authedRequest(Method.Get, path);
- }
-
- /**
- * @param tagName - name of room tag to be set
- * @param metadata - associated with that tag to be stored
- * @returns Promise which resolves: to an empty object
- * @returns Rejects: with an error response.
- */
- public setRoomTag(roomId: string, tagName: string, metadata: ITagMetadata): Promise<{}> {
- const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", {
- $userId: this.credentials.userId!,
- $roomId: roomId,
- $tag: tagName,
- });
- return this.http.authedRequest(Method.Put, path, undefined, metadata);
- }
-
- /**
- * @param tagName - name of room tag to be removed
- * @returns Promise which resolves: to an empty object
- * @returns Rejects: with an error response.
- */
- public deleteRoomTag(roomId: string, tagName: string): Promise<{}> {
- const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", {
- $userId: this.credentials.userId!,
- $roomId: roomId,
- $tag: tagName,
- });
- return this.http.authedRequest(Method.Delete, path);
- }
-
- /**
- * @param eventType - event type to be set
- * @param content - event content
- * @returns Promise which resolves: to an empty object `{}`
- * @returns Rejects: with an error response.
- */
- public setRoomAccountData(roomId: string, eventType: string, content: Record<string, any>): Promise<{}> {
- const path = utils.encodeUri("/user/$userId/rooms/$roomId/account_data/$type", {
- $userId: this.credentials.userId!,
- $roomId: roomId,
- $type: eventType,
- });
- return this.http.authedRequest(Method.Put, path, undefined, content);
- }
-
- /**
- * Set a power level to one or multiple users.
- * @returns Promise which resolves: to an ISendEventResponse object
- * @returns Rejects: with an error response.
- */
- public setPowerLevel(
- roomId: string,
- userId: string | string[],
- powerLevel: number | undefined,
- event: MatrixEvent | null,
- ): Promise<ISendEventResponse> {
- let content = {
- users: {} as Record<string, number>,
- };
- if (event?.getType() === EventType.RoomPowerLevels) {
- // take a copy of the content to ensure we don't corrupt
- // existing client state with a failed power level change
- content = utils.deepCopy(event.getContent());
- }
-
- const users = Array.isArray(userId) ? userId : [userId];
- for (const user of users) {
- if (powerLevel == null) {
- delete content.users[user];
- } else {
- content.users[user] = powerLevel;
- }
- }
-
- const path = utils.encodeUri("/rooms/$roomId/state/m.room.power_levels", {
- $roomId: roomId,
- });
- return this.http.authedRequest(Method.Put, path, undefined, content);
- }
-
- /**
- * Create an m.beacon_info event
- * @returns
- */
- // eslint-disable-next-line @typescript-eslint/naming-convention
- public async unstable_createLiveBeacon(
- roomId: Room["roomId"],
- beaconInfoContent: MBeaconInfoEventContent,
- ): Promise<ISendEventResponse> {
- return this.unstable_setLiveBeacon(roomId, beaconInfoContent);
- }
-
- /**
- * Upsert a live beacon event
- * using a specific m.beacon_info.* event variable type
- * @param roomId - string
- * @returns
- */
- // eslint-disable-next-line @typescript-eslint/naming-convention
- public async unstable_setLiveBeacon(
- roomId: string,
- beaconInfoContent: MBeaconInfoEventContent,
- ): Promise<ISendEventResponse> {
- return this.sendStateEvent(roomId, M_BEACON_INFO.name, beaconInfoContent, this.getUserId()!);
- }
-
- public sendEvent(roomId: string, eventType: string, content: IContent, txnId?: string): Promise<ISendEventResponse>;
- public sendEvent(
- roomId: string,
- threadId: string | null,
- eventType: string,
- content: IContent,
- txnId?: string,
- ): Promise<ISendEventResponse>;
- public sendEvent(
- roomId: string,
- threadIdOrEventType: string | null,
- eventTypeOrContent: string | IContent,
- contentOrTxnId?: IContent | string,
- txnIdOrVoid?: string,
- ): Promise<ISendEventResponse> {
- let threadId: string | null;
- let eventType: string;
- let content: IContent;
- let txnId: string | undefined;
- if (!threadIdOrEventType?.startsWith(EVENT_ID_PREFIX) && threadIdOrEventType !== null) {
- txnId = contentOrTxnId as string;
- content = eventTypeOrContent as IContent;
- eventType = threadIdOrEventType;
- threadId = null;
- } else {
- txnId = txnIdOrVoid;
- content = contentOrTxnId as IContent;
- eventType = eventTypeOrContent as string;
- threadId = threadIdOrEventType;
- }
-
- // If we expect that an event is part of a thread but is missing the relation
- // we need to add it manually, as well as the reply fallback
- if (threadId && !content!["m.relates_to"]?.rel_type) {
- const isReply = !!content!["m.relates_to"]?.["m.in_reply_to"];
- content!["m.relates_to"] = {
- ...content!["m.relates_to"],
- rel_type: THREAD_RELATION_TYPE.name,
- event_id: threadId,
- // Set is_falling_back to true unless this is actually intended to be a reply
- is_falling_back: !isReply,
- };
- const thread = this.getRoom(roomId)?.getThread(threadId);
- if (thread && !isReply) {
- content!["m.relates_to"]["m.in_reply_to"] = {
- event_id:
- thread
- .lastReply((ev: MatrixEvent) => {
- return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status;
- })
- ?.getId() ?? threadId,
- };
- }
- }
-
- return this.sendCompleteEvent(roomId, threadId, { type: eventType, content }, txnId);
- }
-
- /**
- * @param eventObject - An object with the partial structure of an event, to which event_id, user_id, room_id and origin_server_ts will be added.
- * @param txnId - Optional.
- * @returns Promise which resolves: to an empty object `{}`
- * @returns Rejects: with an error response.
- */
- private sendCompleteEvent(
- roomId: string,
- threadId: string | null,
- eventObject: any,
- txnId?: string,
- ): Promise<ISendEventResponse> {
- if (!txnId) {
- txnId = this.makeTxnId();
- }
-
- // We always construct a MatrixEvent when sending because the store and scheduler use them.
- // We'll extract the params back out if it turns out the client has no scheduler or store.
- const localEvent = new MatrixEvent(
- Object.assign(eventObject, {
- event_id: "~" + roomId + ":" + txnId,
- user_id: this.credentials.userId,
- sender: this.credentials.userId,
- room_id: roomId,
- origin_server_ts: new Date().getTime(),
- }),
- );
-
- const room = this.getRoom(roomId);
- const thread = threadId ? room?.getThread(threadId) : undefined;
- if (thread) {
- localEvent.setThread(thread);
- }
-
- // set up re-emitter for this new event - this is normally the job of EventMapper but we don't use it here
- this.reEmitter.reEmit(localEvent, [MatrixEventEvent.Replaced, MatrixEventEvent.VisibilityChange]);
- room?.reEmitter.reEmit(localEvent, [MatrixEventEvent.BeforeRedaction]);
-
- // if this is a relation or redaction of an event
- // that hasn't been sent yet (e.g. with a local id starting with a ~)
- // then listen for the remote echo of that event so that by the time
- // this event does get sent, we have the correct event_id
- const targetId = localEvent.getAssociatedId();
- if (targetId?.startsWith("~")) {
- const target = room?.getPendingEvents().find((e) => e.getId() === targetId);
- target?.once(MatrixEventEvent.LocalEventIdReplaced, () => {
- localEvent.updateAssociatedId(target.getId()!);
- });
- }
-
- const type = localEvent.getType();
- logger.log(`sendEvent of type ${type} in ${roomId} with txnId ${txnId}`);
-
- localEvent.setTxnId(txnId);
- localEvent.setStatus(EventStatus.SENDING);
-
- // add this event immediately to the local store as 'sending'.
- room?.addPendingEvent(localEvent, txnId);
-
- // addPendingEvent can change the state to NOT_SENT if it believes
- // that there's other events that have failed. We won't bother to
- // try sending the event if the state has changed as such.
- if (localEvent.status === EventStatus.NOT_SENT) {
- return Promise.reject(new Error("Event blocked by other events not yet sent"));
- }
-
- return this.encryptAndSendEvent(room, localEvent);
- }
-
- /**
- * encrypts the event if necessary; adds the event to the queue, or sends it; marks the event as sent/unsent
- * @returns returns a promise which resolves with the result of the send request
- */
- protected encryptAndSendEvent(room: Room | null, event: MatrixEvent): Promise<ISendEventResponse> {
- let cancelled = false;
- // Add an extra Promise.resolve() to turn synchronous exceptions into promise rejections,
- // so that we can handle synchronous and asynchronous exceptions with the
- // same code path.
- return Promise.resolve()
- .then(() => {
- const encryptionPromise = this.encryptEventIfNeeded(event, room ?? undefined);
- if (!encryptionPromise) return null; // doesn't need encryption
-
- this.pendingEventEncryption.set(event.getId()!, encryptionPromise);
- this.updatePendingEventStatus(room, event, EventStatus.ENCRYPTING);
- return encryptionPromise.then(() => {
- if (!this.pendingEventEncryption.has(event.getId()!)) {
- // cancelled via MatrixClient::cancelPendingEvent
- cancelled = true;
- return;
- }
- this.updatePendingEventStatus(room, event, EventStatus.SENDING);
- });
- })
- .then(() => {
- if (cancelled) return {} as ISendEventResponse;
- let promise: Promise<ISendEventResponse> | null = null;
- if (this.scheduler) {
- // if this returns a promise then the scheduler has control now and will
- // resolve/reject when it is done. Internally, the scheduler will invoke
- // processFn which is set to this._sendEventHttpRequest so the same code
- // path is executed regardless.
- promise = this.scheduler.queueEvent(event);
- if (promise && this.scheduler.getQueueForEvent(event)!.length > 1) {
- // event is processed FIFO so if the length is 2 or more we know
- // this event is stuck behind an earlier event.
- this.updatePendingEventStatus(room, event, EventStatus.QUEUED);
- }
- }
-
- if (!promise) {
- promise = this.sendEventHttpRequest(event);
- if (room) {
- promise = promise.then((res) => {
- room.updatePendingEvent(event, EventStatus.SENT, res["event_id"]);
- return res;
- });
- }
- }
-
- return promise;
- })
- .catch((err) => {
- logger.error("Error sending event", err.stack || err);
- try {
- // set the error on the event before we update the status:
- // updating the status emits the event, so the state should be
- // consistent at that point.
- event.error = err;
- this.updatePendingEventStatus(room, event, EventStatus.NOT_SENT);
- } catch (e) {
- logger.error("Exception in error handler!", (<Error>e).stack || err);
- }
- if (err instanceof MatrixError) {
- err.event = event;
- }
- throw err;
- });
- }
-
- private encryptEventIfNeeded(event: MatrixEvent, room?: Room): Promise<void> | null {
- if (event.isEncrypted()) {
- // this event has already been encrypted; this happens if the
- // encryption step succeeded, but the send step failed on the first
- // attempt.
- return null;
- }
-
- if (event.isRedaction()) {
- // Redactions do not support encryption in the spec at this time,
- // whilst it mostly worked in some clients, it wasn't compliant.
- return null;
- }
-
- if (!room || !this.isRoomEncrypted(event.getRoomId()!)) {
- return null;
- }
-
- if (!this.cryptoBackend && this.usingExternalCrypto) {
- // The client has opted to allow sending messages to encrypted
- // rooms even if the room is encrypted, and we haven't setup
- // crypto. This is useful for users of matrix-org/pantalaimon
- return null;
- }
-
- if (event.getType() === EventType.Reaction) {
- // For reactions, there is a very little gained by encrypting the entire
- // event, as relation data is already kept in the clear. Event
- // encryption for a reaction effectively only obscures the event type,
- // but the purpose is still obvious from the relation data, so nothing
- // is really gained. It also causes quite a few problems, such as:
- // * triggers notifications via default push rules
- // * prevents server-side bundling for reactions
- // The reaction key / content / emoji value does warrant encrypting, but
- // this will be handled separately by encrypting just this value.
- // See https://github.com/matrix-org/matrix-doc/pull/1849#pullrequestreview-248763642
- return null;
- }
-
- if (!this.cryptoBackend) {
- throw new Error("This room is configured to use encryption, but your client does not support encryption.");
- }
-
- return this.cryptoBackend.encryptEvent(event, room);
- }
-
- /**
- * Returns the eventType that should be used taking encryption into account
- * for a given eventType.
- * @param roomId - the room for the events `eventType` relates to
- * @param eventType - the event type
- * @returns the event type taking encryption into account
- */
- private getEncryptedIfNeededEventType(
- roomId: string,
- eventType?: EventType | string | null,
- ): EventType | string | null | undefined {
- if (eventType === EventType.Reaction) return eventType;
- return this.isRoomEncrypted(roomId) ? EventType.RoomMessageEncrypted : eventType;
- }
-
- protected updatePendingEventStatus(room: Room | null, event: MatrixEvent, newStatus: EventStatus): void {
- if (room) {
- room.updatePendingEvent(event, newStatus);
- } else {
- event.setStatus(newStatus);
- }
- }
-
- private sendEventHttpRequest(event: MatrixEvent): Promise<ISendEventResponse> {
- let txnId = event.getTxnId();
- if (!txnId) {
- txnId = this.makeTxnId();
- event.setTxnId(txnId);
- }
-
- const pathParams = {
- $roomId: event.getRoomId()!,
- $eventType: event.getWireType(),
- $stateKey: event.getStateKey()!,
- $txnId: txnId,
- };
-
- let path: string;
-
- if (event.isState()) {
- let pathTemplate = "/rooms/$roomId/state/$eventType";
- if (event.getStateKey() && event.getStateKey()!.length > 0) {
- pathTemplate = "/rooms/$roomId/state/$eventType/$stateKey";
- }
- path = utils.encodeUri(pathTemplate, pathParams);
- } else if (event.isRedaction()) {
- const pathTemplate = `/rooms/$roomId/redact/$redactsEventId/$txnId`;
- path = utils.encodeUri(pathTemplate, {
- $redactsEventId: event.event.redacts!,
- ...pathParams,
- });
- } else {
- path = utils.encodeUri("/rooms/$roomId/send/$eventType/$txnId", pathParams);
- }
-
- return this.http
- .authedRequest<ISendEventResponse>(Method.Put, path, undefined, event.getWireContent())
- .then((res) => {
- logger.log(`Event sent to ${event.getRoomId()} with event id ${res.event_id}`);
- return res;
- });
- }
-
- /**
- * @param txnId - transaction id. One will be made up if not supplied.
- * @param opts - Options to pass on, may contain `reason` and `with_relations` (MSC3912)
- * @returns Promise which resolves: TODO
- * @returns Rejects: with an error response.
- * @throws Error if called with `with_relations` (MSC3912) but the server does not support it.
- * Callers should check whether the server supports MSC3912 via `MatrixClient.canSupport`.
- */
- public redactEvent(
- roomId: string,
- eventId: string,
- txnId?: string | undefined,
- opts?: IRedactOpts,
- ): Promise<ISendEventResponse>;
- public redactEvent(
- roomId: string,
- threadId: string | null,
- eventId: string,
- txnId?: string | undefined,
- opts?: IRedactOpts,
- ): Promise<ISendEventResponse>;
- public redactEvent(
- roomId: string,
- threadId: string | null,
- eventId?: string,
- txnId?: string | IRedactOpts,
- opts?: IRedactOpts,
- ): Promise<ISendEventResponse> {
- if (!eventId?.startsWith(EVENT_ID_PREFIX)) {
- opts = txnId as IRedactOpts;
- txnId = eventId;
- eventId = threadId!;
- threadId = null;
- }
- const reason = opts?.reason;
-
- if (
- opts?.with_relations &&
- this.canSupport.get(Feature.RelationBasedRedactions) === ServerSupport.Unsupported
- ) {
- throw new Error(
- "Server does not support relation based redactions " +
- `roomId ${roomId} eventId ${eventId} txnId: ${txnId} threadId ${threadId}`,
- );
- }
-
- const withRelations = opts?.with_relations
- ? {
- [this.canSupport.get(Feature.RelationBasedRedactions) === ServerSupport.Stable
- ? MSC3912_RELATION_BASED_REDACTIONS_PROP.stable!
- : MSC3912_RELATION_BASED_REDACTIONS_PROP.unstable!]: opts?.with_relations,
- }
- : {};
-
- return this.sendCompleteEvent(
- roomId,
- threadId,
- {
- type: EventType.RoomRedaction,
- content: {
- ...withRelations,
- reason,
- },
- redacts: eventId,
- },
- txnId as string,
- );
- }
-
- /**
- * @param txnId - Optional.
- * @returns Promise which resolves: to an ISendEventResponse object
- * @returns Rejects: with an error response.
- */
- public sendMessage(roomId: string, content: IContent, txnId?: string): Promise<ISendEventResponse>;
- public sendMessage(
- roomId: string,
- threadId: string | null,
- content: IContent,
- txnId?: string,
- ): Promise<ISendEventResponse>;
- public sendMessage(
- roomId: string,
- threadId: string | null | IContent,
- content?: IContent | string,
- txnId?: string,
- ): Promise<ISendEventResponse> {
- if (typeof threadId !== "string" && threadId !== null) {
- txnId = content as string;
- content = threadId as IContent;
- threadId = null;
- }
-
- const eventType: string = EventType.RoomMessage;
- const sendContent: IContent = content as IContent;
-
- return this.sendEvent(roomId, threadId as string | null, eventType, sendContent, txnId);
- }
-
- /**
- * @param txnId - Optional.
- * @returns
- * @returns Rejects: with an error response.
- */
- public sendTextMessage(roomId: string, body: string, txnId?: string): Promise<ISendEventResponse>;
- public sendTextMessage(
- roomId: string,
- threadId: string | null,
- body: string,
- txnId?: string,
- ): Promise<ISendEventResponse>;
- public sendTextMessage(
- roomId: string,
- threadId: string | null,
- body: string,
- txnId?: string,
- ): Promise<ISendEventResponse> {
- if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) {
- txnId = body;
- body = threadId;
- threadId = null;
- }
- const content = ContentHelpers.makeTextMessage(body);
- return this.sendMessage(roomId, threadId, content, txnId);
- }
-
- /**
- * @param txnId - Optional.
- * @returns Promise which resolves: to a ISendEventResponse object
- * @returns Rejects: with an error response.
- */
- public sendNotice(roomId: string, body: string, txnId?: string): Promise<ISendEventResponse>;
- public sendNotice(
- roomId: string,
- threadId: string | null,
- body: string,
- txnId?: string,
- ): Promise<ISendEventResponse>;
- public sendNotice(
- roomId: string,
- threadId: string | null,
- body: string,
- txnId?: string,
- ): Promise<ISendEventResponse> {
- if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) {
- txnId = body;
- body = threadId;
- threadId = null;
- }
- const content = ContentHelpers.makeNotice(body);
- return this.sendMessage(roomId, threadId, content, txnId);
- }
-
- /**
- * @param txnId - Optional.
- * @returns Promise which resolves: to a ISendEventResponse object
- * @returns Rejects: with an error response.
- */
- public sendEmoteMessage(roomId: string, body: string, txnId?: string): Promise<ISendEventResponse>;
- public sendEmoteMessage(
- roomId: string,
- threadId: string | null,
- body: string,
- txnId?: string,
- ): Promise<ISendEventResponse>;
- public sendEmoteMessage(
- roomId: string,
- threadId: string | null,
- body: string,
- txnId?: string,
- ): Promise<ISendEventResponse> {
- if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) {
- txnId = body;
- body = threadId;
- threadId = null;
- }
- const content = ContentHelpers.makeEmoteMessage(body);
- return this.sendMessage(roomId, threadId, content, txnId);
- }
-
- /**
- * @returns Promise which resolves: to a ISendEventResponse object
- * @returns Rejects: with an error response.
- */
- public sendImageMessage(roomId: string, url: string, info?: IImageInfo, text?: string): Promise<ISendEventResponse>;
- public sendImageMessage(
- roomId: string,
- threadId: string | null,
- url: string,
- info?: IImageInfo,
- text?: string,
- ): Promise<ISendEventResponse>;
- public sendImageMessage(
- roomId: string,
- threadId: string | null,
- url?: string | IImageInfo,
- info?: IImageInfo | string,
- text = "Image",
- ): Promise<ISendEventResponse> {
- if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) {
- text = (info as string) || "Image";
- info = url as IImageInfo;
- url = threadId as string;
- threadId = null;
- }
- const content = {
- msgtype: MsgType.Image,
- url: url,
- info: info,
- body: text,
- };
- return this.sendMessage(roomId, threadId, content);
- }
-
- /**
- * @returns Promise which resolves: to a ISendEventResponse object
- * @returns Rejects: with an error response.
- */
- public sendStickerMessage(
- roomId: string,
- url: string,
- info?: IImageInfo,
- text?: string,
- ): Promise<ISendEventResponse>;
- public sendStickerMessage(
- roomId: string,
- threadId: string | null,
- url: string,
- info?: IImageInfo,
- text?: string,
- ): Promise<ISendEventResponse>;
- public sendStickerMessage(
- roomId: string,
- threadId: string | null,
- url?: string | IImageInfo,
- info?: IImageInfo | string,
- text = "Sticker",
- ): Promise<ISendEventResponse> {
- if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) {
- text = (info as string) || "Sticker";
- info = url as IImageInfo;
- url = threadId as string;
- threadId = null;
- }
- const content = {
- url: url,
- info: info,
- body: text,
- };
-
- return this.sendEvent(roomId, threadId, EventType.Sticker, content);
- }
-
- /**
- * @returns Promise which resolves: to a ISendEventResponse object
- * @returns Rejects: with an error response.
- */
- public sendHtmlMessage(roomId: string, body: string, htmlBody: string): Promise<ISendEventResponse>;
- public sendHtmlMessage(
- roomId: string,
- threadId: string | null,
- body: string,
- htmlBody: string,
- ): Promise<ISendEventResponse>;
- public sendHtmlMessage(
- roomId: string,
- threadId: string | null,
- body: string,
- htmlBody?: string,
- ): Promise<ISendEventResponse> {
- if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) {
- htmlBody = body as string;
- body = threadId;
- threadId = null;
- }
- const content = ContentHelpers.makeHtmlMessage(body, htmlBody!);
- return this.sendMessage(roomId, threadId, content);
- }
-
- /**
- * @returns Promise which resolves: to a ISendEventResponse object
- * @returns Rejects: with an error response.
- */
- public sendHtmlNotice(roomId: string, body: string, htmlBody: string): Promise<ISendEventResponse>;
- public sendHtmlNotice(
- roomId: string,
- threadId: string | null,
- body: string,
- htmlBody: string,
- ): Promise<ISendEventResponse>;
- public sendHtmlNotice(
- roomId: string,
- threadId: string | null,
- body: string,
- htmlBody?: string,
- ): Promise<ISendEventResponse> {
- if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) {
- htmlBody = body as string;
- body = threadId;
- threadId = null;
- }
- const content = ContentHelpers.makeHtmlNotice(body, htmlBody!);
- return this.sendMessage(roomId, threadId, content);
- }
-
- /**
- * @returns Promise which resolves: to a ISendEventResponse object
- * @returns Rejects: with an error response.
- */
- public sendHtmlEmote(roomId: string, body: string, htmlBody: string): Promise<ISendEventResponse>;
- public sendHtmlEmote(
- roomId: string,
- threadId: string | null,
- body: string,
- htmlBody: string,
- ): Promise<ISendEventResponse>;
- public sendHtmlEmote(
- roomId: string,
- threadId: string | null,
- body: string,
- htmlBody?: string,
- ): Promise<ISendEventResponse> {
- if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) {
- htmlBody = body as string;
- body = threadId;
- threadId = null;
- }
- const content = ContentHelpers.makeHtmlEmote(body, htmlBody!);
- return this.sendMessage(roomId, threadId, content);
- }
-
- /**
- * Send a receipt.
- * @param event - The event being acknowledged
- * @param receiptType - The kind of receipt e.g. "m.read". Other than
- * ReceiptType.Read are experimental!
- * @param body - Additional content to send alongside the receipt.
- * @param unthreaded - An unthreaded receipt will clear room+thread notifications
- * @returns Promise which resolves: to an empty object `{}`
- * @returns Rejects: with an error response.
- */
- public async sendReceipt(event: MatrixEvent, receiptType: ReceiptType, body: any, unthreaded = false): Promise<{}> {
- if (this.isGuest()) {
- return Promise.resolve({}); // guests cannot send receipts so don't bother.
- }
-
- const path = utils.encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
- $roomId: event.getRoomId()!,
- $receiptType: receiptType,
- $eventId: event.getId()!,
- });
-
- if (!unthreaded) {
- const isThread = !!event.threadRootId;
- body = {
- ...body,
- thread_id: isThread ? event.threadRootId : MAIN_ROOM_TIMELINE,
- };
- }
-
- const promise = this.http.authedRequest<{}>(Method.Post, path, undefined, body || {});
-
- const room = this.getRoom(event.getRoomId());
- if (room && this.credentials.userId) {
- room.addLocalEchoReceipt(this.credentials.userId, event, receiptType);
- }
- return promise;
- }
-
- /**
- * Send a read receipt.
- * @param event - The event that has been read.
- * @param receiptType - other than ReceiptType.Read are experimental! Optional.
- * @returns Promise which resolves: to an empty object `{}`
- * @returns Rejects: with an error response.
- */
- public async sendReadReceipt(
- event: MatrixEvent | null,
- receiptType = ReceiptType.Read,
- unthreaded = false,
- ): Promise<{} | undefined> {
- if (!event) return;
- const eventId = event.getId()!;
- const room = this.getRoom(event.getRoomId());
- if (room?.hasPendingEvent(eventId)) {
- throw new Error(`Cannot set read receipt to a pending event (${eventId})`);
- }
-
- return this.sendReceipt(event, receiptType, {}, unthreaded);
- }
-
- /**
- * Set a marker to indicate the point in a room before which the user has read every
- * event. This can be retrieved from room account data (the event type is `m.fully_read`)
- * and displayed as a horizontal line in the timeline that is visually distinct to the
- * position of the user's own read receipt.
- * @param roomId - ID of the room that has been read
- * @param rmEventId - ID of the event that has been read
- * @param rrEvent - the event tracked by the read receipt. This is here for
- * convenience because the RR and the RM are commonly updated at the same time as each
- * other. The local echo of this receipt will be done if set. Optional.
- * @param rpEvent - the m.read.private read receipt event for when we don't
- * want other users to see the read receipts. This is experimental. Optional.
- * @returns Promise which resolves: the empty object, `{}`.
- */
- public async setRoomReadMarkers(
- roomId: string,
- rmEventId: string,
- rrEvent?: MatrixEvent,
- rpEvent?: MatrixEvent,
- ): Promise<{}> {
- const room = this.getRoom(roomId);
- if (room && room.hasPendingEvent(rmEventId)) {
- throw new Error(`Cannot set read marker to a pending event (${rmEventId})`);
- }
-
- // Add the optional RR update, do local echo like `sendReceipt`
- let rrEventId: string | undefined;
- if (rrEvent) {
- rrEventId = rrEvent.getId()!;
- if (room?.hasPendingEvent(rrEventId)) {
- throw new Error(`Cannot set read receipt to a pending event (${rrEventId})`);
- }
- room?.addLocalEchoReceipt(this.credentials.userId!, rrEvent, ReceiptType.Read);
- }
-
- // Add the optional private RR update, do local echo like `sendReceipt`
- let rpEventId: string | undefined;
- if (rpEvent) {
- rpEventId = rpEvent.getId()!;
- if (room?.hasPendingEvent(rpEventId)) {
- throw new Error(`Cannot set read receipt to a pending event (${rpEventId})`);
- }
- room?.addLocalEchoReceipt(this.credentials.userId!, rpEvent, ReceiptType.ReadPrivate);
- }
-
- return await this.setRoomReadMarkersHttpRequest(roomId, rmEventId, rrEventId, rpEventId);
- }
-
- /**
- * Get a preview of the given URL as of (roughly) the given point in time,
- * described as an object with OpenGraph keys and associated values.
- * Attributes may be synthesized where actual OG metadata is lacking.
- * Caches results to prevent hammering the server.
- * @param url - The URL to get preview data for
- * @param ts - The preferred point in time that the preview should
- * describe (ms since epoch). The preview returned will either be the most
- * recent one preceding this timestamp if available, or failing that the next
- * most recent available preview.
- * @returns Promise which resolves: Object of OG metadata.
- * @returns Rejects: with an error response.
- * May return synthesized attributes if the URL lacked OG meta.
- */
- public getUrlPreview(url: string, ts: number): Promise<IPreviewUrlResponse> {
- // bucket the timestamp to the nearest minute to prevent excessive spam to the server
- // Surely 60-second accuracy is enough for anyone.
- ts = Math.floor(ts / 60000) * 60000;
-
- const parsed = new URL(url);
- parsed.hash = ""; // strip the hash as it won't affect the preview
- url = parsed.toString();
-
- const key = ts + "_" + url;
-
- // If there's already a request in flight (or we've handled it), return that instead.
- const cachedPreview = this.urlPreviewCache[key];
- if (cachedPreview) {
- return cachedPreview;
- }
-
- const resp = this.http.authedRequest<IPreviewUrlResponse>(
- Method.Get,
- "/preview_url",
- {
- url,
- ts: ts.toString(),
- },
- undefined,
- {
- prefix: MediaPrefix.R0,
- },
- );
- // TODO: Expire the URL preview cache sometimes
- this.urlPreviewCache[key] = resp;
- return resp;
- }
-
- /**
- * @returns Promise which resolves: to an empty object `{}`
- * @returns Rejects: with an error response.
- */
- public sendTyping(roomId: string, isTyping: boolean, timeoutMs: number): Promise<{}> {
- if (this.isGuest()) {
- return Promise.resolve({}); // guests cannot send typing notifications so don't bother.
- }
-
- const path = utils.encodeUri("/rooms/$roomId/typing/$userId", {
- $roomId: roomId,
- $userId: this.getUserId()!,
- });
- const data: any = {
- typing: isTyping,
- };
- if (isTyping) {
- data.timeout = timeoutMs ? timeoutMs : 20000;
- }
- return this.http.authedRequest(Method.Put, path, undefined, data);
- }
-
- /**
- * Determines the history of room upgrades for a given room, as far as the
- * client can see. Returns an array of Rooms where the first entry is the
- * oldest and the last entry is the newest (likely current) room. If the
- * provided room is not found, this returns an empty list. This works in
- * both directions, looking for older and newer rooms of the given room.
- * @param roomId - The room ID to search from
- * @param verifyLinks - If true, the function will only return rooms
- * which can be proven to be linked. For example, rooms which have a create
- * event pointing to an old room which the client is not aware of or doesn't
- * have a matching tombstone would not be returned.
- * @param msc3946ProcessDynamicPredecessor - if true, look for
- * m.room.predecessor state events as well as create events, and prefer
- * predecessor events where they exist (MSC3946).
- * @returns An array of rooms representing the upgrade
- * history.
- */
- public getRoomUpgradeHistory(
- roomId: string,
- verifyLinks = false,
- msc3946ProcessDynamicPredecessor = false,
- ): Room[] {
- const currentRoom = this.getRoom(roomId);
- if (!currentRoom) return [];
-
- const before = this.findPredecessorRooms(currentRoom, verifyLinks, msc3946ProcessDynamicPredecessor);
- const after = this.findSuccessorRooms(currentRoom, verifyLinks, msc3946ProcessDynamicPredecessor);
-
- return [...before, currentRoom, ...after];
- }
-
- private findPredecessorRooms(room: Room, verifyLinks: boolean, msc3946ProcessDynamicPredecessor: boolean): Room[] {
- const ret: Room[] = [];
-
- // Work backwards from newer to older rooms
- let predecessorRoomId = room.findPredecessor(msc3946ProcessDynamicPredecessor)?.roomId;
- while (predecessorRoomId !== null) {
- const predecessorRoom = this.getRoom(predecessorRoomId);
- if (predecessorRoom === null) {
- break;
- }
- if (verifyLinks) {
- const tombstone = predecessorRoom.currentState.getStateEvents(EventType.RoomTombstone, "");
- if (!tombstone || tombstone.getContent()["replacement_room"] !== room.roomId) {
- break;
- }
- }
-
- // Insert at the front because we're working backwards from the currentRoom
- ret.splice(0, 0, predecessorRoom);
-
- room = predecessorRoom;
- predecessorRoomId = room.findPredecessor(msc3946ProcessDynamicPredecessor)?.roomId;
- }
- return ret;
- }
-
- private findSuccessorRooms(room: Room, verifyLinks: boolean, msc3946ProcessDynamicPredecessor: boolean): Room[] {
- const ret: Room[] = [];
-
- // Work forwards, looking at tombstone events
- let tombstoneEvent = room.currentState.getStateEvents(EventType.RoomTombstone, "");
- while (tombstoneEvent) {
- const successorRoom = this.getRoom(tombstoneEvent.getContent()["replacement_room"]);
- if (!successorRoom) break; // end of the chain
- if (successorRoom.roomId === room.roomId) break; // Tombstone is referencing its own room
-
- if (verifyLinks) {
- const predecessorRoomId = successorRoom.findPredecessor(msc3946ProcessDynamicPredecessor)?.roomId;
- if (!predecessorRoomId || predecessorRoomId !== room.roomId) {
- break;
- }
- }
-
- // Push to the end because we're looking forwards
- ret.push(successorRoom);
- const roomIds = new Set(ret.map((ref) => ref.roomId));
- if (roomIds.size < ret.length) {
- // The last room added to the list introduced a previous roomId
- // To avoid recursion, return the last rooms - 1
- return ret.slice(0, ret.length - 1);
- }
-
- // Set the current room to the reference room so we know where we're at
- room = successorRoom;
- tombstoneEvent = room.currentState.getStateEvents(EventType.RoomTombstone, "");
- }
- return ret;
- }
-
- /**
- * @param reason - Optional.
- * @returns Promise which resolves: `{}` an empty object.
- * @returns Rejects: with an error response.
- */
- public invite(roomId: string, userId: string, reason?: string): Promise<{}> {
- return this.membershipChange(roomId, userId, "invite", reason);
- }
-
- /**
- * Invite a user to a room based on their email address.
- * @param roomId - The room to invite the user to.
- * @param email - The email address to invite.
- * @returns Promise which resolves: `{}` an empty object.
- * @returns Rejects: with an error response.
- */
- public inviteByEmail(roomId: string, email: string): Promise<{}> {
- return this.inviteByThreePid(roomId, "email", email);
- }
-
- /**
- * Invite a user to a room based on a third-party identifier.
- * @param roomId - The room to invite the user to.
- * @param medium - The medium to invite the user e.g. "email".
- * @param address - The address for the specified medium.
- * @returns Promise which resolves: `{}` an empty object.
- * @returns Rejects: with an error response.
- */
- public async inviteByThreePid(roomId: string, medium: string, address: string): Promise<{}> {
- const path = utils.encodeUri("/rooms/$roomId/invite", { $roomId: roomId });
-
- const identityServerUrl = this.getIdentityServerUrl(true);
- if (!identityServerUrl) {
- return Promise.reject(
- new MatrixError({
- error: "No supplied identity server URL",
- errcode: "ORG.MATRIX.JSSDK_MISSING_PARAM",
- }),
- );
- }
- const params: Record<string, string> = {
- id_server: identityServerUrl,
- medium: medium,
- address: address,
- };
-
- if (this.identityServer?.getAccessToken && (await this.doesServerAcceptIdentityAccessToken())) {
- const identityAccessToken = await this.identityServer.getAccessToken();
- if (identityAccessToken) {
- params["id_access_token"] = identityAccessToken;
- }
- }
-
- return this.http.authedRequest(Method.Post, path, undefined, params);
- }
-
- /**
- * @returns Promise which resolves: `{}` an empty object.
- * @returns Rejects: with an error response.
- */
- public leave(roomId: string): Promise<{}> {
- return this.membershipChange(roomId, undefined, "leave");
- }
-
- /**
- * Leaves all rooms in the chain of room upgrades based on the given room. By
- * default, this will leave all the previous and upgraded rooms, including the
- * given room. To only leave the given room and any previous rooms, keeping the
- * upgraded (modern) rooms untouched supply `false` to `includeFuture`.
- * @param roomId - The room ID to start leaving at
- * @param includeFuture - If true, the whole chain (past and future) of
- * upgraded rooms will be left.
- * @returns Promise which resolves when completed with an object keyed
- * by room ID and value of the error encountered when leaving or null.
- */
- public leaveRoomChain(
- roomId: string,
- includeFuture = true,
- ): Promise<{ [roomId: string]: Error | MatrixError | null }> {
- const upgradeHistory = this.getRoomUpgradeHistory(roomId);
-
- let eligibleToLeave = upgradeHistory;
- if (!includeFuture) {
- eligibleToLeave = [];
- for (const room of upgradeHistory) {
- eligibleToLeave.push(room);
- if (room.roomId === roomId) {
- break;
- }
- }
- }
-
- const populationResults: { [roomId: string]: Error } = {};
- const promises: Promise<any>[] = [];
-
- const doLeave = (roomId: string): Promise<void> => {
- return this.leave(roomId)
- .then(() => {
- delete populationResults[roomId];
- })
- .catch((err) => {
- // suppress error
- populationResults[roomId] = err;
- });
- };
-
- for (const room of eligibleToLeave) {
- promises.push(doLeave(room.roomId));
- }
-
- return Promise.all(promises).then(() => populationResults);
- }
-
- /**
- * @param reason - Optional.
- * @returns Promise which resolves: TODO
- * @returns Rejects: with an error response.
- */
- public ban(roomId: string, userId: string, reason?: string): Promise<{}> {
- return this.membershipChange(roomId, userId, "ban", reason);
- }
-
- /**
- * @param deleteRoom - True to delete the room from the store on success.
- * Default: true.
- * @returns Promise which resolves: `{}` an empty object.
- * @returns Rejects: with an error response.
- */
- public forget(roomId: string, deleteRoom = true): Promise<{}> {
- const promise = this.membershipChange(roomId, undefined, "forget");
- if (!deleteRoom) {
- return promise;
- }
- return promise.then((response) => {
- this.store.removeRoom(roomId);
- this.emit(ClientEvent.DeleteRoom, roomId);
- return response;
- });
- }
-
- /**
- * @returns Promise which resolves: Object (currently empty)
- * @returns Rejects: with an error response.
- */
- public unban(roomId: string, userId: string): Promise<{}> {
- // unbanning != set their state to leave: this used to be
- // the case, but was then changed so that leaving was always
- // a revoking of privilege, otherwise two people racing to
- // kick / ban someone could end up banning and then un-banning
- // them.
- const path = utils.encodeUri("/rooms/$roomId/unban", {
- $roomId: roomId,
- });
- const data = {
- user_id: userId,
- };
- return this.http.authedRequest(Method.Post, path, undefined, data);
- }
-
- /**
- * @param reason - Optional.
- * @returns Promise which resolves: `{}` an empty object.
- * @returns Rejects: with an error response.
- */
- public kick(roomId: string, userId: string, reason?: string): Promise<{}> {
- const path = utils.encodeUri("/rooms/$roomId/kick", {
- $roomId: roomId,
- });
- const data = {
- user_id: userId,
- reason: reason,
- };
- return this.http.authedRequest(Method.Post, path, undefined, data);
- }
-
- private membershipChange(
- roomId: string,
- userId: string | undefined,
- membership: string,
- reason?: string,
- ): Promise<{}> {
- // API returns an empty object
- const path = utils.encodeUri("/rooms/$room_id/$membership", {
- $room_id: roomId,
- $membership: membership,
- });
- return this.http.authedRequest(Method.Post, path, undefined, {
- user_id: userId, // may be undefined e.g. on leave
- reason: reason,
- });
- }
-
- /**
- * Obtain a dict of actions which should be performed for this event according
- * to the push rules for this user. Caches the dict on the event.
- * @param event - The event to get push actions for.
- * @param forceRecalculate - forces to recalculate actions for an event
- * Useful when an event just got decrypted
- * @returns A dict of actions to perform.
- */
- public getPushActionsForEvent(event: MatrixEvent, forceRecalculate = false): IActionsObject | null {
- if (!event.getPushActions() || forceRecalculate) {
- event.setPushActions(this.pushProcessor.actionsForEvent(event));
- }
- return event.getPushActions();
- }
-
- /**
- * @param info - The kind of info to set (e.g. 'avatar_url')
- * @param data - The JSON object to set.
- * @returns
- * @returns Rejects: with an error response.
- */
- // eslint-disable-next-line camelcase
- public setProfileInfo(info: "avatar_url", data: { avatar_url: string }): Promise<{}>;
- public setProfileInfo(info: "displayname", data: { displayname: string }): Promise<{}>;
- public setProfileInfo(info: "avatar_url" | "displayname", data: object): Promise<{}> {
- const path = utils.encodeUri("/profile/$userId/$info", {
- $userId: this.credentials.userId!,
- $info: info,
- });
- return this.http.authedRequest(Method.Put, path, undefined, data);
- }
-
- /**
- * @returns Promise which resolves: `{}` an empty object.
- * @returns Rejects: with an error response.
- */
- public async setDisplayName(name: string): Promise<{}> {
- const prom = await this.setProfileInfo("displayname", { displayname: name });
- // XXX: synthesise a profile update for ourselves because Synapse is broken and won't
- const user = this.getUser(this.getUserId()!);
- if (user) {
- user.displayName = name;
- user.emit(UserEvent.DisplayName, user.events.presence, user);
- }
- return prom;
- }
-
- /**
- * @returns Promise which resolves: `{}` an empty object.
- * @returns Rejects: with an error response.
- */
- public async setAvatarUrl(url: string): Promise<{}> {
- const prom = await this.setProfileInfo("avatar_url", { avatar_url: url });
- // XXX: synthesise a profile update for ourselves because Synapse is broken and won't
- const user = this.getUser(this.getUserId()!);
- if (user) {
- user.avatarUrl = url;
- user.emit(UserEvent.AvatarUrl, user.events.presence, user);
- }
- return prom;
- }
-
- /**
- * Turn an MXC URL into an HTTP one. <strong>This method is experimental and
- * may change.</strong>
- * @param mxcUrl - The MXC URL
- * @param width - The desired width of the thumbnail.
- * @param height - The desired height of the thumbnail.
- * @param resizeMethod - The thumbnail resize method to use, either
- * "crop" or "scale".
- * @param allowDirectLinks - If true, return any non-mxc URLs
- * directly. Fetching such URLs will leak information about the user to
- * anyone they share a room with. If false, will return null for such URLs.
- * @returns the avatar URL or null.
- */
- public mxcUrlToHttp(
- mxcUrl: string,
- width?: number,
- height?: number,
- resizeMethod?: string,
- allowDirectLinks?: boolean,
- ): string | null {
- return getHttpUriForMxc(this.baseUrl, mxcUrl, width, height, resizeMethod, allowDirectLinks);
- }
-
- /**
- * @param opts - Options to apply
- * @returns Promise which resolves
- * @returns Rejects: with an error response.
- * @throws If 'presence' isn't a valid presence enum value.
- */
- public async setPresence(opts: IPresenceOpts): Promise<void> {
- const path = utils.encodeUri("/presence/$userId/status", {
- $userId: this.credentials.userId!,
- });
-
- const validStates = ["offline", "online", "unavailable"];
- if (validStates.indexOf(opts.presence) === -1) {
- throw new Error("Bad presence value: " + opts.presence);
- }
- await this.http.authedRequest(Method.Put, path, undefined, opts);
- }
-
- /**
- * @param userId - The user to get presence for
- * @returns Promise which resolves: The presence state for this user.
- * @returns Rejects: with an error response.
- */
- public getPresence(userId: string): Promise<IStatusResponse> {
- const path = utils.encodeUri("/presence/$userId/status", {
- $userId: userId,
- });
-
- return this.http.authedRequest(Method.Get, path);
- }
-
- /**
- * Retrieve older messages from the given room and put them in the timeline.
- *
- * If this is called multiple times whilst a request is ongoing, the <i>same</i>
- * Promise will be returned. If there was a problem requesting scrollback, there
- * will be a small delay before another request can be made (to prevent tight-looping
- * when there is no connection).
- *
- * @param room - The room to get older messages in.
- * @param limit - Optional. The maximum number of previous events to
- * pull in. Default: 30.
- * @returns Promise which resolves: Room. If you are at the beginning
- * of the timeline, `Room.oldState.paginationToken` will be
- * `null`.
- * @returns Rejects: with an error response.
- */
- public scrollback(room: Room, limit = 30): Promise<Room> {
- let timeToWaitMs = 0;
-
- let info = this.ongoingScrollbacks[room.roomId] || {};
- if (info.promise) {
- return info.promise;
- } else if (info.errorTs) {
- const timeWaitedMs = Date.now() - info.errorTs;
- timeToWaitMs = Math.max(SCROLLBACK_DELAY_MS - timeWaitedMs, 0);
- }
-
- if (room.oldState.paginationToken === null) {
- return Promise.resolve(room); // already at the start.
- }
- // attempt to grab more events from the store first
- const numAdded = this.store.scrollback(room, limit).length;
- if (numAdded === limit) {
- // store contained everything we needed.
- return Promise.resolve(room);
- }
- // reduce the required number of events appropriately
- limit = limit - numAdded;
-
- const promise = new Promise<Room>((resolve, reject) => {
- // wait for a time before doing this request
- // (which may be 0 in order not to special case the code paths)
- sleep(timeToWaitMs)
- .then(() => {
- return this.createMessagesRequest(
- room.roomId,
- room.oldState.paginationToken,
- limit,
- Direction.Backward,
- );
- })
- .then((res: IMessagesResponse) => {
- const matrixEvents = res.chunk.map(this.getEventMapper());
- if (res.state) {
- const stateEvents = res.state.map(this.getEventMapper());
- room.currentState.setUnknownStateEvents(stateEvents);
- }
-
- const [timelineEvents, threadedEvents] = room.partitionThreadedEvents(matrixEvents);
-
- this.processAggregatedTimelineEvents(room, timelineEvents);
- room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
- this.processThreadEvents(room, threadedEvents, true);
-
- room.oldState.paginationToken = res.end ?? null;
- if (res.chunk.length === 0) {
- room.oldState.paginationToken = null;
- }
- this.store.storeEvents(room, matrixEvents, res.end ?? null, true);
- delete this.ongoingScrollbacks[room.roomId];
- resolve(room);
- })
- .catch((err) => {
- this.ongoingScrollbacks[room.roomId] = {
- errorTs: Date.now(),
- };
- reject(err);
- });
- });
-
- info = { promise };
-
- this.ongoingScrollbacks[room.roomId] = info;
- return promise;
- }
-
- public getEventMapper(options?: MapperOpts): EventMapper {
- return eventMapperFor(this, options || {});
- }
-
- /**
- * Get an EventTimeline for the given event
- *
- * <p>If the EventTimelineSet object already has the given event in its store, the
- * corresponding timeline will be returned. Otherwise, a /context request is
- * made, and used to construct an EventTimeline.
- * If the event does not belong to this EventTimelineSet then undefined will be returned.
- *
- * @param timelineSet - The timelineSet to look for the event in, must be bound to a room
- * @param eventId - The ID of the event to look for
- *
- * @returns Promise which resolves:
- * {@link EventTimeline} including the given event
- */
- public async getEventTimeline(timelineSet: EventTimelineSet, eventId: string): Promise<Optional<EventTimeline>> {
- // don't allow any timeline support unless it's been enabled.
- if (!this.timelineSupport) {
- throw new Error(
- "timeline support is disabled. Set the 'timelineSupport'" +
- " parameter to true when creating MatrixClient to enable it.",
- );
- }
-
- if (!timelineSet?.room) {
- throw new Error("getEventTimeline only supports room timelines");
- }
-
- if (timelineSet.getTimelineForEvent(eventId)) {
- return timelineSet.getTimelineForEvent(eventId);
- }
-
- if (timelineSet.thread && this.supportsThreads()) {
- return this.getThreadTimeline(timelineSet, eventId);
- }
-
- const path = utils.encodeUri("/rooms/$roomId/context/$eventId", {
- $roomId: timelineSet.room.roomId,
- $eventId: eventId,
- });
-
- let params: Record<string, string | string[]> | undefined = undefined;
- if (this.clientOpts?.lazyLoadMembers) {
- params = { filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER) };
- }
-
- // TODO: we should implement a backoff (as per scrollback()) to deal more nicely with HTTP errors.
- const res = await this.http.authedRequest<IContextResponse>(Method.Get, path, params);
- if (!res.event) {
- throw new Error("'event' not in '/context' result - homeserver too old?");
- }
-
- // by the time the request completes, the event might have ended up in the timeline.
- if (timelineSet.getTimelineForEvent(eventId)) {
- return timelineSet.getTimelineForEvent(eventId);
- }
-
- const mapper = this.getEventMapper();
- const event = mapper(res.event);
- if (event.isRelation(THREAD_RELATION_TYPE.name)) {
- logger.warn("Tried loading a regular timeline at the position of a thread event");
- return undefined;
- }
- const events = [
- // Order events from most recent to oldest (reverse-chronological).
- // We start with the last event, since that's the point at which we have known state.
- // events_after is already backwards; events_before is forwards.
- ...res.events_after.reverse().map(mapper),
- event,
- ...res.events_before.map(mapper),
- ];
-
- // Here we handle non-thread timelines only, but still process any thread events to populate thread summaries.
- let timeline = timelineSet.getTimelineForEvent(events[0].getId());
- if (timeline) {
- timeline.getState(EventTimeline.BACKWARDS)!.setUnknownStateEvents(res.state.map(mapper));
- } else {
- timeline = timelineSet.addTimeline();
- timeline.initialiseState(res.state.map(mapper));
- timeline.getState(EventTimeline.FORWARDS)!.paginationToken = res.end;
- }
-
- const [timelineEvents, threadedEvents] = timelineSet.room.partitionThreadedEvents(events);
- timelineSet.addEventsToTimeline(timelineEvents, true, timeline, res.start);
- // The target event is not in a thread but process the contextual events, so we can show any threads around it.
- this.processThreadEvents(timelineSet.room, threadedEvents, true);
- this.processAggregatedTimelineEvents(timelineSet.room, timelineEvents);
-
- // There is no guarantee that the event ended up in "timeline" (we might have switched to a neighbouring
- // timeline) - so check the room's index again. On the other hand, there's no guarantee the event ended up
- // anywhere, if it was later redacted, so we just return the timeline we first thought of.
- return (
- timelineSet.getTimelineForEvent(eventId) ??
- timelineSet.room.findThreadForEvent(event)?.liveTimeline ?? // for Threads degraded support
- timeline
- );
- }
-
- public async getThreadTimeline(timelineSet: EventTimelineSet, eventId: string): Promise<EventTimeline | undefined> {
- if (!this.supportsThreads()) {
- throw new Error("could not get thread timeline: no client support");
- }
-
- if (!timelineSet.room) {
- throw new Error("could not get thread timeline: not a room timeline");
- }
-
- if (!timelineSet.thread) {
- throw new Error("could not get thread timeline: not a thread timeline");
- }
-
- const path = utils.encodeUri("/rooms/$roomId/context/$eventId", {
- $roomId: timelineSet.room.roomId,
- $eventId: eventId,
- });
-
- const params: Record<string, string | string[]> = {
- limit: "0",
- };
- if (this.clientOpts?.lazyLoadMembers) {
- params.filter = JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER);
- }
-
- // TODO: we should implement a backoff (as per scrollback()) to deal more nicely with HTTP errors.
- const res = await this.http.authedRequest<IContextResponse>(Method.Get, path, params);
- const mapper = this.getEventMapper();
- const event = mapper(res.event);
-
- if (!timelineSet.canContain(event)) {
- return undefined;
- }
-
- if (Thread.hasServerSideSupport) {
- if (Thread.hasServerSideFwdPaginationSupport) {
- if (!timelineSet.thread) {
- throw new Error("could not get thread timeline: not a thread timeline");
- }
-
- const thread = timelineSet.thread;
- const resOlder: IRelationsResponse = await this.fetchRelations(
- timelineSet.room.roomId,
- thread.id,
- THREAD_RELATION_TYPE.name,
- null,
- { dir: Direction.Backward, from: res.start },
- );
- const resNewer: IRelationsResponse = await this.fetchRelations(
- timelineSet.room.roomId,
- thread.id,
- THREAD_RELATION_TYPE.name,
- null,
- { dir: Direction.Forward, from: res.end },
- );
- const events = [
- // Order events from most recent to oldest (reverse-chronological).
- // We start with the last event, since that's the point at which we have known state.
- // events_after is already backwards; events_before is forwards.
- ...resNewer.chunk.reverse().map(mapper),
- event,
- ...resOlder.chunk.map(mapper),
- ];
- for (const event of events) {
- await timelineSet.thread?.processEvent(event);
- }
-
- // Here we handle non-thread timelines only, but still process any thread events to populate thread summaries.
- let timeline = timelineSet.getTimelineForEvent(event.getId());
- if (timeline) {
- timeline.getState(EventTimeline.BACKWARDS)!.setUnknownStateEvents(res.state.map(mapper));
- } else {
- timeline = timelineSet.addTimeline();
- timeline.initialiseState(res.state.map(mapper));
- }
-
- timelineSet.addEventsToTimeline(events, true, timeline, resNewer.next_batch);
- if (!resOlder.next_batch) {
- const originalEvent = await this.fetchRoomEvent(timelineSet.room.roomId, thread.id);
- timelineSet.addEventsToTimeline([mapper(originalEvent)], true, timeline, null);
- }
- timeline.setPaginationToken(resOlder.next_batch ?? null, Direction.Backward);
- timeline.setPaginationToken(resNewer.next_batch ?? null, Direction.Forward);
- this.processAggregatedTimelineEvents(timelineSet.room, events);
-
- // There is no guarantee that the event ended up in "timeline" (we might have switched to a neighbouring
- // timeline) - so check the room's index again. On the other hand, there's no guarantee the event ended up
- // anywhere, if it was later redacted, so we just return the timeline we first thought of.
- return timelineSet.getTimelineForEvent(eventId) ?? timeline;
- } else {
- // Where the event is a thread reply (not a root) and running in MSC-enabled mode the Thread timeline only
- // functions contiguously, so we have to jump through some hoops to get our target event in it.
- // XXX: workaround for https://github.com/vector-im/element-meta/issues/150
-
- const thread = timelineSet.thread;
-
- const resOlder = await this.fetchRelations(
- timelineSet.room.roomId,
- thread.id,
- THREAD_RELATION_TYPE.name,
- null,
- { dir: Direction.Backward, from: res.start },
- );
- const eventsNewer: IEvent[] = [];
- let nextBatch: Optional<string> = res.end;
- while (nextBatch) {
- const resNewer: IRelationsResponse = await this.fetchRelations(
- timelineSet.room.roomId,
- thread.id,
- THREAD_RELATION_TYPE.name,
- null,
- { dir: Direction.Forward, from: nextBatch },
- );
- nextBatch = resNewer.next_batch ?? null;
- eventsNewer.push(...resNewer.chunk);
- }
- const events = [
- // Order events from most recent to oldest (reverse-chronological).
- // We start with the last event, since that's the point at which we have known state.
- // events_after is already backwards; events_before is forwards.
- ...eventsNewer.reverse().map(mapper),
- event,
- ...resOlder.chunk.map(mapper),
- ];
- for (const event of events) {
- await timelineSet.thread?.processEvent(event);
- }
-
- // Here we handle non-thread timelines only, but still process any thread events to populate thread
- // summaries.
- const timeline = timelineSet.getLiveTimeline();
- timeline.getState(EventTimeline.BACKWARDS)!.setUnknownStateEvents(res.state.map(mapper));
-
- timelineSet.addEventsToTimeline(events, true, timeline, null);
- if (!resOlder.next_batch) {
- const originalEvent = await this.fetchRoomEvent(timelineSet.room.roomId, thread.id);
- timelineSet.addEventsToTimeline([mapper(originalEvent)], true, timeline, null);
- }
- timeline.setPaginationToken(resOlder.next_batch ?? null, Direction.Backward);
- timeline.setPaginationToken(null, Direction.Forward);
- this.processAggregatedTimelineEvents(timelineSet.room, events);
-
- return timeline;
- }
- }
- }
-
- /**
- * Get an EventTimeline for the latest events in the room. This will just
- * call `/messages` to get the latest message in the room, then use
- * `client.getEventTimeline(...)` to construct a new timeline from it.
- *
- * @param timelineSet - The timelineSet to find or add the timeline to
- *
- * @returns Promise which resolves:
- * {@link EventTimeline} timeline with the latest events in the room
- */
- public async getLatestTimeline(timelineSet: EventTimelineSet): Promise<Optional<EventTimeline>> {
- // don't allow any timeline support unless it's been enabled.
- if (!this.timelineSupport) {
- throw new Error(
- "timeline support is disabled. Set the 'timelineSupport'" +
- " parameter to true when creating MatrixClient to enable it.",
- );
- }
-
- if (!timelineSet.room) {
- throw new Error("getLatestTimeline only supports room timelines");
- }
-
- let event;
- if (timelineSet.threadListType !== null) {
- const res = await this.createThreadListMessagesRequest(
- timelineSet.room.roomId,
- null,
- 1,
- Direction.Backward,
- timelineSet.threadListType,
- timelineSet.getFilter(),
- );
- event = res.chunk?.[0];
- } else if (timelineSet.thread && Thread.hasServerSideSupport) {
- const res = await this.fetchRelations(
- timelineSet.room.roomId,
- timelineSet.thread.id,
- THREAD_RELATION_TYPE.name,
- null,
- { dir: Direction.Backward, limit: 1 },
- );
- event = res.chunk?.[0];
- } else {
- const messagesPath = utils.encodeUri("/rooms/$roomId/messages", {
- $roomId: timelineSet.room.roomId,
- });
-
- const params: Record<string, string | string[]> = {
- dir: "b",
- };
- if (this.clientOpts?.lazyLoadMembers) {
- params.filter = JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER);
- }
-
- const res = await this.http.authedRequest<IMessagesResponse>(Method.Get, messagesPath, params);
- event = res.chunk?.[0];
- }
- if (!event) {
- throw new Error("No message returned when trying to construct getLatestTimeline");
- }
-
- return this.getEventTimeline(timelineSet, event.event_id);
- }
-
- /**
- * Makes a request to /messages with the appropriate lazy loading filter set.
- * XXX: if we do get rid of scrollback (as it's not used at the moment),
- * we could inline this method again in paginateEventTimeline as that would
- * then be the only call-site
- * @param limit - the maximum amount of events the retrieve
- * @param dir - 'f' or 'b'
- * @param timelineFilter - the timeline filter to pass
- */
- // XXX: Intended private, used in code.
- public createMessagesRequest(
- roomId: string,
- fromToken: string | null,
- limit = 30,
- dir: Direction,
- timelineFilter?: Filter,
- ): Promise<IMessagesResponse> {
- const path = utils.encodeUri("/rooms/$roomId/messages", { $roomId: roomId });
-
- const params: Record<string, string> = {
- limit: limit.toString(),
- dir: dir,
- };
-
- if (fromToken) {
- params.from = fromToken;
- }
-
- let filter: IRoomEventFilter | null = null;
- if (this.clientOpts?.lazyLoadMembers) {
- // create a shallow copy of LAZY_LOADING_MESSAGES_FILTER,
- // so the timelineFilter doesn't get written into it below
- filter = Object.assign({}, Filter.LAZY_LOADING_MESSAGES_FILTER);
- }
- if (timelineFilter) {
- // XXX: it's horrific that /messages' filter parameter doesn't match
- // /sync's one - see https://matrix.org/jira/browse/SPEC-451
- filter = filter || {};
- Object.assign(filter, timelineFilter.getRoomTimelineFilterComponent()?.toJSON());
- }
- if (filter) {
- params.filter = JSON.stringify(filter);
- }
- return this.http.authedRequest(Method.Get, path, params);
- }
-
- /**
- * Makes a request to /messages with the appropriate lazy loading filter set.
- * XXX: if we do get rid of scrollback (as it's not used at the moment),
- * we could inline this method again in paginateEventTimeline as that would
- * then be the only call-site
- * @param limit - the maximum amount of events the retrieve
- * @param dir - 'f' or 'b'
- * @param timelineFilter - the timeline filter to pass
- */
- // XXX: Intended private, used by room.fetchRoomThreads
- public createThreadListMessagesRequest(
- roomId: string,
- fromToken: string | null,
- limit = 30,
- dir = Direction.Backward,
- threadListType: ThreadFilterType | null = ThreadFilterType.All,
- timelineFilter?: Filter,
- ): Promise<IMessagesResponse> {
- const path = utils.encodeUri("/rooms/$roomId/threads", { $roomId: roomId });
-
- const params: Record<string, string> = {
- limit: limit.toString(),
- dir: dir,
- include: threadFilterTypeToFilter(threadListType),
- };
-
- if (fromToken) {
- params.from = fromToken;
- }
-
- let filter: IRoomEventFilter = {};
- if (this.clientOpts?.lazyLoadMembers) {
- // create a shallow copy of LAZY_LOADING_MESSAGES_FILTER,
- // so the timelineFilter doesn't get written into it below
- filter = {
- ...Filter.LAZY_LOADING_MESSAGES_FILTER,
- };
- }
- if (timelineFilter) {
- // XXX: it's horrific that /messages' filter parameter doesn't match
- // /sync's one - see https://matrix.org/jira/browse/SPEC-451
- filter = {
- ...filter,
- ...timelineFilter.getRoomTimelineFilterComponent()?.toJSON(),
- };
- }
- if (Object.keys(filter).length) {
- params.filter = JSON.stringify(filter);
- }
-
- const opts = {
- prefix:
- Thread.hasServerSideListSupport === FeatureSupport.Stable
- ? "/_matrix/client/v1"
- : "/_matrix/client/unstable/org.matrix.msc3856",
- };
-
- return this.http
- .authedRequest<IThreadedMessagesResponse>(Method.Get, path, params, undefined, opts)
- .then((res) => ({
- ...res,
- chunk: res.chunk?.reverse(),
- start: res.prev_batch,
- end: res.next_batch,
- }));
- }
-
- /**
- * Take an EventTimeline, and back/forward-fill results.
- *
- * @param eventTimeline - timeline object to be updated
- *
- * @returns Promise which resolves to a boolean: false if there are no
- * events and we reached either end of the timeline; else true.
- */
- public paginateEventTimeline(eventTimeline: EventTimeline, opts: IPaginateOpts): Promise<boolean> {
- const isNotifTimeline = eventTimeline.getTimelineSet() === this.notifTimelineSet;
- const room = this.getRoom(eventTimeline.getRoomId()!);
- const threadListType = eventTimeline.getTimelineSet().threadListType;
- const thread = eventTimeline.getTimelineSet().thread;
-
- // TODO: we should implement a backoff (as per scrollback()) to deal more
- // nicely with HTTP errors.
- opts = opts || {};
- const backwards = opts.backwards || false;
-
- if (isNotifTimeline) {
- if (!backwards) {
- throw new Error("paginateNotifTimeline can only paginate backwards");
- }
- }
-
- const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
-
- const token = eventTimeline.getPaginationToken(dir);
- const pendingRequest = eventTimeline.paginationRequests[dir];
-
- if (pendingRequest) {
- // already a request in progress - return the existing promise
- return pendingRequest;
- }
-
- let path: string;
- let params: Record<string, string>;
- let promise: Promise<boolean>;
-
- if (isNotifTimeline) {
- path = "/notifications";
- params = {
- limit: (opts.limit ?? 30).toString(),
- only: "highlight",
- };
-
- if (token && token !== "end") {
- params.from = token;
- }
-
- promise = this.http
- .authedRequest<INotificationsResponse>(Method.Get, path, params)
- .then(async (res) => {
- const token = res.next_token;
- const matrixEvents: MatrixEvent[] = [];
-
- res.notifications = res.notifications.filter(noUnsafeEventProps);
-
- for (let i = 0; i < res.notifications.length; i++) {
- const notification = res.notifications[i];
- const event = this.getEventMapper()(notification.event);
- event.setPushActions(PushProcessor.actionListToActionsObject(notification.actions));
- event.event.room_id = notification.room_id; // XXX: gutwrenching
- matrixEvents[i] = event;
- }
-
- // No need to partition events for threads here, everything lives
- // in the notification timeline set
- const timelineSet = eventTimeline.getTimelineSet();
- timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
- this.processAggregatedTimelineEvents(timelineSet.room, matrixEvents);
-
- // if we've hit the end of the timeline, we need to stop trying to
- // paginate. We need to keep the 'forwards' token though, to make sure
- // we can recover from gappy syncs.
- if (backwards && !res.next_token) {
- eventTimeline.setPaginationToken(null, dir);
- }
- return Boolean(res.next_token);
- })
- .finally(() => {
- eventTimeline.paginationRequests[dir] = null;
- });
- eventTimeline.paginationRequests[dir] = promise;
- } else if (threadListType !== null) {
- if (!room) {
- throw new Error("Unknown room " + eventTimeline.getRoomId());
- }
-
- if (!Thread.hasServerSideFwdPaginationSupport && dir === Direction.Forward) {
- throw new Error("Cannot paginate threads forwards without server-side support for MSC 3715");
- }
-
- promise = this.createThreadListMessagesRequest(
- eventTimeline.getRoomId()!,
- token,
- opts.limit,
- dir,
- threadListType,
- eventTimeline.getFilter(),
- )
- .then((res) => {
- if (res.state) {
- const roomState = eventTimeline.getState(dir)!;
- const stateEvents = res.state.filter(noUnsafeEventProps).map(this.getEventMapper());
- roomState.setUnknownStateEvents(stateEvents);
- }
- const token = res.end;
- const matrixEvents = res.chunk.filter(noUnsafeEventProps).map(this.getEventMapper());
-
- const timelineSet = eventTimeline.getTimelineSet();
- timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
- this.processAggregatedTimelineEvents(room, matrixEvents);
- this.processThreadRoots(room, matrixEvents, backwards);
-
- // if we've hit the end of the timeline, we need to stop trying to
- // paginate. We need to keep the 'forwards' token though, to make sure
- // we can recover from gappy syncs.
- if (backwards && res.end == res.start) {
- eventTimeline.setPaginationToken(null, dir);
- }
- return res.end !== res.start;
- })
- .finally(() => {
- eventTimeline.paginationRequests[dir] = null;
- });
- eventTimeline.paginationRequests[dir] = promise;
- } else if (thread) {
- const room = this.getRoom(eventTimeline.getRoomId() ?? undefined);
- if (!room) {
- throw new Error("Unknown room " + eventTimeline.getRoomId());
- }
-
- promise = this.fetchRelations(eventTimeline.getRoomId() ?? "", thread.id, THREAD_RELATION_TYPE.name, null, {
- dir,
- limit: opts.limit,
- from: token ?? undefined,
- })
- .then(async (res) => {
- const mapper = this.getEventMapper();
- const matrixEvents = res.chunk.filter(noUnsafeEventProps).map(mapper);
-
- // Process latest events first
- for (const event of matrixEvents.slice().reverse()) {
- await thread?.processEvent(event);
- const sender = event.getSender()!;
- if (!backwards || thread?.getEventReadUpTo(sender) === null) {
- room.addLocalEchoReceipt(sender, event, ReceiptType.Read);
- }
- }
-
- const newToken = res.next_batch;
-
- const timelineSet = eventTimeline.getTimelineSet();
- timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, newToken ?? null);
- if (!newToken && backwards) {
- const originalEvent = await this.fetchRoomEvent(eventTimeline.getRoomId() ?? "", thread.id);
- timelineSet.addEventsToTimeline([mapper(originalEvent)], true, eventTimeline, null);
- }
- this.processAggregatedTimelineEvents(timelineSet.room, matrixEvents);
-
- // if we've hit the end of the timeline, we need to stop trying to
- // paginate. We need to keep the 'forwards' token though, to make sure
- // we can recover from gappy syncs.
- if (backwards && !newToken) {
- eventTimeline.setPaginationToken(null, dir);
- }
- return Boolean(newToken);
- })
- .finally(() => {
- eventTimeline.paginationRequests[dir] = null;
- });
- eventTimeline.paginationRequests[dir] = promise;
- } else {
- if (!room) {
- throw new Error("Unknown room " + eventTimeline.getRoomId());
- }
-
- promise = this.createMessagesRequest(
- eventTimeline.getRoomId()!,
- token,
- opts.limit,
- dir,
- eventTimeline.getFilter(),
- )
- .then((res) => {
- if (res.state) {
- const roomState = eventTimeline.getState(dir)!;
- const stateEvents = res.state.filter(noUnsafeEventProps).map(this.getEventMapper());
- roomState.setUnknownStateEvents(stateEvents);
- }
- const token = res.end;
- const matrixEvents = res.chunk.filter(noUnsafeEventProps).map(this.getEventMapper());
-
- const timelineSet = eventTimeline.getTimelineSet();
- const [timelineEvents] = room.partitionThreadedEvents(matrixEvents);
- timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
- this.processAggregatedTimelineEvents(room, timelineEvents);
- this.processThreadRoots(
- room,
- timelineEvents.filter((it) => it.getServerAggregatedRelation(THREAD_RELATION_TYPE.name)),
- false,
- );
-
- const atEnd = res.end === undefined || res.end === res.start;
-
- // if we've hit the end of the timeline, we need to stop trying to
- // paginate. We need to keep the 'forwards' token though, to make sure
- // we can recover from gappy syncs.
- if (backwards && atEnd) {
- eventTimeline.setPaginationToken(null, dir);
- }
- return !atEnd;
- })
- .finally(() => {
- eventTimeline.paginationRequests[dir] = null;
- });
- eventTimeline.paginationRequests[dir] = promise;
- }
-
- return promise;
- }
-
- /**
- * Reset the notifTimelineSet entirely, paginating in some historical notifs as
- * a starting point for subsequent pagination.
- */
- public resetNotifTimelineSet(): void {
- if (!this.notifTimelineSet) {
- return;
- }
-
- // FIXME: This thing is a total hack, and results in duplicate events being
- // added to the timeline both from /sync and /notifications, and lots of
- // slow and wasteful processing and pagination. The correct solution is to
- // extend /messages or /search or something to filter on notifications.
-
- // use the fictitious token 'end'. in practice we would ideally give it
- // the oldest backwards pagination token from /sync, but /sync doesn't
- // know about /notifications, so we have no choice but to start paginating
- // from the current point in time. This may well overlap with historical
- // notifs which are then inserted into the timeline by /sync responses.
- this.notifTimelineSet.resetLiveTimeline("end");
-
- // we could try to paginate a single event at this point in order to get
- // a more valid pagination token, but it just ends up with an out of order
- // timeline. given what a mess this is and given we're going to have duplicate
- // events anyway, just leave it with the dummy token for now.
- /*
- this.paginateNotifTimeline(this._notifTimelineSet.getLiveTimeline(), {
- backwards: true,
- limit: 1
- });
- */
- }
-
- /**
- * Peek into a room and receive updates about the room. This only works if the
- * history visibility for the room is world_readable.
- * @param roomId - The room to attempt to peek into.
- * @returns Promise which resolves: Room object
- * @returns Rejects: with an error response.
- */
- public peekInRoom(roomId: string): Promise<Room> {
- this.peekSync?.stopPeeking();
- this.peekSync = new SyncApi(this, this.clientOpts, this.buildSyncApiOptions());
- return this.peekSync.peek(roomId);
- }
-
- /**
- * Stop any ongoing room peeking.
- */
- public stopPeeking(): void {
- if (this.peekSync) {
- this.peekSync.stopPeeking();
- this.peekSync = null;
- }
- }
-
- /**
- * Set r/w flags for guest access in a room.
- * @param roomId - The room to configure guest access in.
- * @param opts - Options
- * @returns Promise which resolves
- * @returns Rejects: with an error response.
- */
- public setGuestAccess(roomId: string, opts: IGuestAccessOpts): Promise<void> {
- const writePromise = this.sendStateEvent(
- roomId,
- EventType.RoomGuestAccess,
- {
- guest_access: opts.allowJoin ? "can_join" : "forbidden",
- },
- "",
- );
-
- let readPromise: Promise<any> = Promise.resolve<any>(undefined);
- if (opts.allowRead) {
- readPromise = this.sendStateEvent(
- roomId,
- EventType.RoomHistoryVisibility,
- {
- history_visibility: "world_readable",
- },
- "",
- );
- }
-
- return Promise.all([readPromise, writePromise]).then(); // .then() to hide results for contract
- }
-
- /**
- * Requests an email verification token for the purposes of registration.
- * This API requests a token from the homeserver.
- * The doesServerRequireIdServerParam() method can be used to determine if
- * the server requires the id_server parameter to be provided.
- *
- * Parameters and return value are as for requestEmailToken
-
- * @param email - As requestEmailToken
- * @param clientSecret - As requestEmailToken
- * @param sendAttempt - As requestEmailToken
- * @param nextLink - As requestEmailToken
- * @returns Promise which resolves: As requestEmailToken
- */
- public requestRegisterEmailToken(
- email: string,
- clientSecret: string,
- sendAttempt: number,
- nextLink?: string,
- ): Promise<IRequestTokenResponse> {
- return this.requestTokenFromEndpoint("/register/email/requestToken", {
- email: email,
- client_secret: clientSecret,
- send_attempt: sendAttempt,
- next_link: nextLink,
- });
- }
-
- /**
- * Requests a text message verification token for the purposes of registration.
- * This API requests a token from the homeserver.
- * The doesServerRequireIdServerParam() method can be used to determine if
- * the server requires the id_server parameter to be provided.
- *
- * @param phoneCountry - The ISO 3166-1 alpha-2 code for the country in which
- * phoneNumber should be parsed relative to.
- * @param phoneNumber - The phone number, in national or international format
- * @param clientSecret - As requestEmailToken
- * @param sendAttempt - As requestEmailToken
- * @param nextLink - As requestEmailToken
- * @returns Promise which resolves: As requestEmailToken
- */
- public requestRegisterMsisdnToken(
- phoneCountry: string,
- phoneNumber: string,
- clientSecret: string,
- sendAttempt: number,
- nextLink?: string,
- ): Promise<IRequestMsisdnTokenResponse> {
- return this.requestTokenFromEndpoint("/register/msisdn/requestToken", {
- country: phoneCountry,
- phone_number: phoneNumber,
- client_secret: clientSecret,
- send_attempt: sendAttempt,
- next_link: nextLink,
- });
- }
-
- /**
- * Requests an email verification token for the purposes of adding a
- * third party identifier to an account.
- * This API requests a token from the homeserver.
- * The doesServerRequireIdServerParam() method can be used to determine if
- * the server requires the id_server parameter to be provided.
- * If an account with the given email address already exists and is
- * associated with an account other than the one the user is authed as,
- * it will either send an email to the address informing them of this
- * or return M_THREEPID_IN_USE (which one is up to the homeserver).
- *
- * @param email - As requestEmailToken
- * @param clientSecret - As requestEmailToken
- * @param sendAttempt - As requestEmailToken
- * @param nextLink - As requestEmailToken
- * @returns Promise which resolves: As requestEmailToken
- */
- public requestAdd3pidEmailToken(
- email: string,
- clientSecret: string,
- sendAttempt: number,
- nextLink?: string,
- ): Promise<IRequestTokenResponse> {
- return this.requestTokenFromEndpoint("/account/3pid/email/requestToken", {
- email: email,
- client_secret: clientSecret,
- send_attempt: sendAttempt,
- next_link: nextLink,
- });
- }
-
- /**
- * Requests a text message verification token for the purposes of adding a
- * third party identifier to an account.
- * This API proxies the identity server /validate/email/requestToken API,
- * adding specific behaviour for the addition of phone numbers to an
- * account, as requestAdd3pidEmailToken.
- *
- * @param phoneCountry - As requestRegisterMsisdnToken
- * @param phoneNumber - As requestRegisterMsisdnToken
- * @param clientSecret - As requestEmailToken
- * @param sendAttempt - As requestEmailToken
- * @param nextLink - As requestEmailToken
- * @returns Promise which resolves: As requestEmailToken
- */
- public requestAdd3pidMsisdnToken(
- phoneCountry: string,
- phoneNumber: string,
- clientSecret: string,
- sendAttempt: number,
- nextLink?: string,
- ): Promise<IRequestMsisdnTokenResponse> {
- return this.requestTokenFromEndpoint("/account/3pid/msisdn/requestToken", {
- country: phoneCountry,
- phone_number: phoneNumber,
- client_secret: clientSecret,
- send_attempt: sendAttempt,
- next_link: nextLink,
- });
- }
-
- /**
- * Requests an email verification token for the purposes of resetting
- * the password on an account.
- * This API proxies the identity server /validate/email/requestToken API,
- * adding specific behaviour for the password resetting. Specifically,
- * if no account with the given email address exists, it may either
- * return M_THREEPID_NOT_FOUND or send an email
- * to the address informing them of this (which one is up to the homeserver).
- *
- * requestEmailToken calls the equivalent API directly on the identity server,
- * therefore bypassing the password reset specific logic.
- *
- * @param email - As requestEmailToken
- * @param clientSecret - As requestEmailToken
- * @param sendAttempt - As requestEmailToken
- * @param nextLink - As requestEmailToken
- * @returns Promise which resolves: As requestEmailToken
- */
- public requestPasswordEmailToken(
- email: string,
- clientSecret: string,
- sendAttempt: number,
- nextLink?: string,
- ): Promise<IRequestTokenResponse> {
- return this.requestTokenFromEndpoint("/account/password/email/requestToken", {
- email: email,
- client_secret: clientSecret,
- send_attempt: sendAttempt,
- next_link: nextLink,
- });
- }
-
- /**
- * Requests a text message verification token for the purposes of resetting
- * the password on an account.
- * This API proxies the identity server /validate/email/requestToken API,
- * adding specific behaviour for the password resetting, as requestPasswordEmailToken.
- *
- * @param phoneCountry - As requestRegisterMsisdnToken
- * @param phoneNumber - As requestRegisterMsisdnToken
- * @param clientSecret - As requestEmailToken
- * @param sendAttempt - As requestEmailToken
- * @param nextLink - As requestEmailToken
- * @returns Promise which resolves: As requestEmailToken
- */
- public requestPasswordMsisdnToken(
- phoneCountry: string,
- phoneNumber: string,
- clientSecret: string,
- sendAttempt: number,
- nextLink: string,
- ): Promise<IRequestMsisdnTokenResponse> {
- return this.requestTokenFromEndpoint("/account/password/msisdn/requestToken", {
- country: phoneCountry,
- phone_number: phoneNumber,
- client_secret: clientSecret,
- send_attempt: sendAttempt,
- next_link: nextLink,
- });
- }
-
- /**
- * Internal utility function for requesting validation tokens from usage-specific
- * requestToken endpoints.
- *
- * @param endpoint - The endpoint to send the request to
- * @param params - Parameters for the POST request
- * @returns Promise which resolves: As requestEmailToken
- */
- private async requestTokenFromEndpoint<T extends IRequestTokenResponse>(
- endpoint: string,
- params: Record<string, any>,
- ): Promise<T> {
- const postParams = Object.assign({}, params);
-
- // If the HS supports separate add and bind, then requestToken endpoints
- // don't need an IS as they are all validated by the HS directly.
- if (!(await this.doesServerSupportSeparateAddAndBind()) && this.idBaseUrl) {
- const idServerUrl = new URL(this.idBaseUrl);
- postParams.id_server = idServerUrl.host;
-
- if (this.identityServer?.getAccessToken && (await this.doesServerAcceptIdentityAccessToken())) {
- const identityAccessToken = await this.identityServer.getAccessToken();
- if (identityAccessToken) {
- postParams.id_access_token = identityAccessToken;
- }
- }
- }
-
- return this.http.request(Method.Post, endpoint, undefined, postParams);
- }
-
- /**
- * Get the room-kind push rule associated with a room.
- * @param scope - "global" or device-specific.
- * @param roomId - the id of the room.
- * @returns the rule or undefined.
- */
- public getRoomPushRule(scope: "global" | "device", roomId: string): IPushRule | undefined {
- // There can be only room-kind push rule per room
- // and its id is the room id.
- if (this.pushRules) {
- return this.pushRules[scope]?.room?.find((rule) => rule.rule_id === roomId);
- } else {
- throw new Error("SyncApi.sync() must be done before accessing to push rules.");
- }
- }
-
- /**
- * Set a room-kind muting push rule in a room.
- * The operation also updates MatrixClient.pushRules at the end.
- * @param scope - "global" or device-specific.
- * @param roomId - the id of the room.
- * @param mute - the mute state.
- * @returns Promise which resolves: result object
- * @returns Rejects: with an error response.
- */
- public setRoomMutePushRule(scope: "global" | "device", roomId: string, mute: boolean): Promise<void> | undefined {
- let promise: Promise<unknown> | undefined;
- let hasDontNotifyRule = false;
-
- // Get the existing room-kind push rule if any
- const roomPushRule = this.getRoomPushRule(scope, roomId);
- if (roomPushRule?.actions.includes(PushRuleActionName.DontNotify)) {
- hasDontNotifyRule = true;
- }
-
- if (!mute) {
- // Remove the rule only if it is a muting rule
- if (hasDontNotifyRule) {
- promise = this.deletePushRule(scope, PushRuleKind.RoomSpecific, roomPushRule!.rule_id);
- }
- } else {
- if (!roomPushRule) {
- promise = this.addPushRule(scope, PushRuleKind.RoomSpecific, roomId, {
- actions: [PushRuleActionName.DontNotify],
- });
- } else if (!hasDontNotifyRule) {
- // Remove the existing one before setting the mute push rule
- // This is a workaround to SYN-590 (Push rule update fails)
- const deferred = utils.defer();
- this.deletePushRule(scope, PushRuleKind.RoomSpecific, roomPushRule.rule_id)
- .then(() => {
- this.addPushRule(scope, PushRuleKind.RoomSpecific, roomId, {
- actions: [PushRuleActionName.DontNotify],
- })
- .then(() => {
- deferred.resolve();
- })
- .catch((err) => {
- deferred.reject(err);
- });
- })
- .catch((err) => {
- deferred.reject(err);
- });
-
- promise = deferred.promise;
- }
- }
-
- if (promise) {
- return new Promise<void>((resolve, reject) => {
- // Update this.pushRules when the operation completes
- promise!
- .then(() => {
- this.getPushRules()
- .then((result) => {
- this.pushRules = result;
- resolve();
- })
- .catch((err) => {
- reject(err);
- });
- })
- .catch((err: Error) => {
- // Update it even if the previous operation fails. This can help the
- // app to recover when push settings has been modified from another client
- this.getPushRules()
- .then((result) => {
- this.pushRules = result;
- reject(err);
- })
- .catch((err2) => {
- reject(err);
- });
- });
- });
- }
- }
-
- public searchMessageText(opts: ISearchOpts): Promise<ISearchResponse> {
- const roomEvents: ISearchRequestBody["search_categories"]["room_events"] = {
- search_term: opts.query,
- };
-
- if ("keys" in opts) {
- roomEvents.keys = opts.keys;
- }
-
- return this.search({
- body: {
- search_categories: {
- room_events: roomEvents,
- },
- },
- });
- }
-
- /**
- * Perform a server-side search for room events.
- *
- * The returned promise resolves to an object containing the fields:
- *
- * * count: estimate of the number of results
- * * next_batch: token for back-pagination; if undefined, there are no more results
- * * highlights: a list of words to highlight from the stemming algorithm
- * * results: a list of results
- *
- * Each entry in the results list is a SearchResult.
- *
- * @returns Promise which resolves: result object
- * @returns Rejects: with an error response.
- */
- public searchRoomEvents(opts: IEventSearchOpts): Promise<ISearchResults> {
- // TODO: support search groups
-
- const body = {
- search_categories: {
- room_events: {
- search_term: opts.term,
- filter: opts.filter,
- order_by: SearchOrderBy.Recent,
- event_context: {
- before_limit: 1,
- after_limit: 1,
- include_profile: true,
- },
- },
- },
- };
-
- const searchResults: ISearchResults = {
- _query: body,
- results: [],
- highlights: [],
- };
-
- return this.search({ body: body }).then((res) => this.processRoomEventsSearch(searchResults, res));
- }
-
- /**
- * Take a result from an earlier searchRoomEvents call, and backfill results.
- *
- * @param searchResults - the results object to be updated
- * @returns Promise which resolves: updated result object
- * @returns Rejects: with an error response.
- */
- public backPaginateRoomEventsSearch<T extends ISearchResults>(searchResults: T): Promise<T> {
- // TODO: we should implement a backoff (as per scrollback()) to deal more
- // nicely with HTTP errors.
-
- if (!searchResults.next_batch) {
- return Promise.reject(new Error("Cannot backpaginate event search any further"));
- }
-
- if (searchResults.pendingRequest) {
- // already a request in progress - return the existing promise
- return searchResults.pendingRequest as Promise<T>;
- }
-
- const searchOpts = {
- body: searchResults._query!,
- next_batch: searchResults.next_batch,
- };
-
- const promise = this.search(searchOpts, searchResults.abortSignal)
- .then((res) => this.processRoomEventsSearch(searchResults, res))
- .finally(() => {
- searchResults.pendingRequest = undefined;
- });
- searchResults.pendingRequest = promise;
-
- return promise;
- }
-
- /**
- * helper for searchRoomEvents and backPaginateRoomEventsSearch. Processes the
- * response from the API call and updates the searchResults
- *
- * @returns searchResults
- * @internal
- */
- // XXX: Intended private, used in code
- public processRoomEventsSearch<T extends ISearchResults>(searchResults: T, response: ISearchResponse): T {
- const roomEvents = response.search_categories.room_events;
-
- searchResults.count = roomEvents.count;
- searchResults.next_batch = roomEvents.next_batch;
-
- // combine the highlight list with our existing list;
- const highlights = new Set<string>(roomEvents.highlights);
- searchResults.highlights.forEach((hl) => {
- highlights.add(hl);
- });
-
- // turn it back into a list.
- searchResults.highlights = Array.from(highlights);
-
- const mapper = this.getEventMapper();
-
- // append the new results to our existing results
- const resultsLength = roomEvents.results?.length ?? 0;
- for (let i = 0; i < resultsLength; i++) {
- const sr = SearchResult.fromJson(roomEvents.results[i], mapper);
- const room = this.getRoom(sr.context.getEvent().getRoomId());
- if (room) {
- // Copy over a known event sender if we can
- for (const ev of sr.context.getTimeline()) {
- const sender = room.getMember(ev.getSender()!);
- if (!ev.sender && sender) ev.sender = sender;
- }
- }
- searchResults.results.push(sr);
- }
- return searchResults;
- }
-
- /**
- * Populate the store with rooms the user has left.
- * @returns Promise which resolves: TODO - Resolved when the rooms have
- * been added to the data store.
- * @returns Rejects: with an error response.
- */
- public syncLeftRooms(): Promise<Room[]> {
- // Guard against multiple calls whilst ongoing and multiple calls post success
- if (this.syncedLeftRooms) {
- return Promise.resolve([]); // don't call syncRooms again if it succeeded.
- }
- if (this.syncLeftRoomsPromise) {
- return this.syncLeftRoomsPromise; // return the ongoing request
- }
- const syncApi = new SyncApi(this, this.clientOpts, this.buildSyncApiOptions());
- this.syncLeftRoomsPromise = syncApi.syncLeftRooms();
-
- // cleanup locks
- this.syncLeftRoomsPromise
- .then(() => {
- logger.log("Marking success of sync left room request");
- this.syncedLeftRooms = true; // flip the bit on success
- })
- .finally(() => {
- this.syncLeftRoomsPromise = undefined; // cleanup ongoing request state
- });
-
- return this.syncLeftRoomsPromise;
- }
-
- /**
- * Create a new filter.
- * @param content - The HTTP body for the request
- * @returns Promise which resolves to a Filter object.
- * @returns Rejects: with an error response.
- */
- public createFilter(content: IFilterDefinition): Promise<Filter> {
- const path = utils.encodeUri("/user/$userId/filter", {
- $userId: this.credentials.userId!,
- });
- return this.http.authedRequest<IFilterResponse>(Method.Post, path, undefined, content).then((response) => {
- // persist the filter
- const filter = Filter.fromJson(this.credentials.userId, response.filter_id, content);
- this.store.storeFilter(filter);
- return filter;
- });
- }
-
- /**
- * Retrieve a filter.
- * @param userId - The user ID of the filter owner
- * @param filterId - The filter ID to retrieve
- * @param allowCached - True to allow cached filters to be returned.
- * Default: True.
- * @returns Promise which resolves: a Filter object
- * @returns Rejects: with an error response.
- */
- public getFilter(userId: string, filterId: string, allowCached: boolean): Promise<Filter> {
- if (allowCached) {
- const filter = this.store.getFilter(userId, filterId);
- if (filter) {
- return Promise.resolve(filter);
- }
- }
-
- const path = utils.encodeUri("/user/$userId/filter/$filterId", {
- $userId: userId,
- $filterId: filterId,
- });
-
- return this.http.authedRequest<IFilterDefinition>(Method.Get, path).then((response) => {
- // persist the filter
- const filter = Filter.fromJson(userId, filterId, response);
- this.store.storeFilter(filter);
- return filter;
- });
- }
-
- /**
- * @returns Filter ID
- */
- public async getOrCreateFilter(filterName: string, filter: Filter): Promise<string> {
- const filterId = this.store.getFilterIdByName(filterName);
- let existingId: string | undefined;
-
- if (filterId) {
- // check that the existing filter matches our expectations
- try {
- const existingFilter = await this.getFilter(this.credentials.userId!, filterId, true);
- if (existingFilter) {
- const oldDef = existingFilter.getDefinition();
- const newDef = filter.getDefinition();
-
- if (utils.deepCompare(oldDef, newDef)) {
- // super, just use that.
- // debuglog("Using existing filter ID %s: %s", filterId,
- // JSON.stringify(oldDef));
- existingId = filterId;
- }
- }
- } catch (error) {
- // Synapse currently returns the following when the filter cannot be found:
- // {
- // errcode: "M_UNKNOWN",
- // name: "M_UNKNOWN",
- // message: "No row found",
- // }
- if ((<MatrixError>error).errcode !== "M_UNKNOWN" && (<MatrixError>error).errcode !== "M_NOT_FOUND") {
- throw error;
- }
- }
- // if the filter doesn't exist anymore on the server, remove from store
- if (!existingId) {
- this.store.setFilterIdByName(filterName, undefined);
- }
- }
-
- if (existingId) {
- return existingId;
- }
-
- // create a new filter
- const createdFilter = await this.createFilter(filter.getDefinition());
-
- this.store.setFilterIdByName(filterName, createdFilter.filterId);
- return createdFilter.filterId!;
- }
-
- /**
- * Gets a bearer token from the homeserver that the user can
- * present to a third party in order to prove their ownership
- * of the Matrix account they are logged into.
- * @returns Promise which resolves: Token object
- * @returns Rejects: with an error response.
- */
- public getOpenIdToken(): Promise<IOpenIDToken> {
- const path = utils.encodeUri("/user/$userId/openid/request_token", {
- $userId: this.credentials.userId!,
- });
-
- return this.http.authedRequest(Method.Post, path, undefined, {});
- }
-
- private startCallEventHandler = (): void => {
- if (this.isInitialSyncComplete()) {
- this.callEventHandler!.start();
- this.groupCallEventHandler!.start();
- this.off(ClientEvent.Sync, this.startCallEventHandler);
- }
- };
-
- /**
- * Once the client has been initialised, we want to clear notifications we
- * know for a fact should be here.
- * This issue should also be addressed on synapse's side and is tracked as part
- * of https://github.com/matrix-org/synapse/issues/14837
- *
- * We consider a room or a thread as fully read if the current user has sent
- * the last event in the live timeline of that context and if the read receipt
- * we have on record matches.
- */
- private fixupRoomNotifications = (): void => {
- if (this.isInitialSyncComplete()) {
- const unreadRooms = (this.getRooms() ?? []).filter((room) => {
- return room.getUnreadNotificationCount(NotificationCountType.Total) > 0;
- });
-
- for (const room of unreadRooms) {
- const currentUserId = this.getSafeUserId();
- room.fixupNotifications(currentUserId);
- }
-
- this.off(ClientEvent.Sync, this.fixupRoomNotifications);
- }
- };
-
- /**
- * @returns Promise which resolves: ITurnServerResponse object
- * @returns Rejects: with an error response.
- */
- public turnServer(): Promise<ITurnServerResponse> {
- return this.http.authedRequest(Method.Get, "/voip/turnServer");
- }
-
- /**
- * Get the TURN servers for this homeserver.
- * @returns The servers or an empty list.
- */
- public getTurnServers(): ITurnServer[] {
- return this.turnServers || [];
- }
-
- /**
- * Get the unix timestamp (in milliseconds) at which the current
- * TURN credentials (from getTurnServers) expire
- * @returns The expiry timestamp in milliseconds
- */
- public getTurnServersExpiry(): number {
- return this.turnServersExpiry;
- }
-
- public get pollingTurnServers(): boolean {
- return this.checkTurnServersIntervalID !== undefined;
- }
-
- // XXX: Intended private, used in code.
- public async checkTurnServers(): Promise<boolean | undefined> {
- if (!this.canSupportVoip) {
- return;
- }
-
- let credentialsGood = false;
- const remainingTime = this.turnServersExpiry - Date.now();
- if (remainingTime > TURN_CHECK_INTERVAL) {
- logger.debug("TURN creds are valid for another " + remainingTime + " ms: not fetching new ones.");
- credentialsGood = true;
- } else {
- logger.debug("Fetching new TURN credentials");
- try {
- const res = await this.turnServer();
- if (res.uris) {
- logger.log("Got TURN URIs: " + res.uris + " refresh in " + res.ttl + " secs");
- // map the response to a format that can be fed to RTCPeerConnection
- const servers: ITurnServer = {
- urls: res.uris,
- username: res.username,
- credential: res.password,
- };
- this.turnServers = [servers];
- // The TTL is in seconds but we work in ms
- this.turnServersExpiry = Date.now() + res.ttl * 1000;
- credentialsGood = true;
- this.emit(ClientEvent.TurnServers, this.turnServers);
- }
- } catch (err) {
- logger.error("Failed to get TURN URIs", err);
- if ((<HTTPError>err).httpStatus === 403) {
- // We got a 403, so there's no point in looping forever.
- logger.info("TURN access unavailable for this account: stopping credentials checks");
- if (this.checkTurnServersIntervalID !== null) global.clearInterval(this.checkTurnServersIntervalID);
- this.checkTurnServersIntervalID = undefined;
- this.emit(ClientEvent.TurnServersError, <HTTPError>err, true); // fatal
- } else {
- // otherwise, if we failed for whatever reason, try again the next time we're called.
- this.emit(ClientEvent.TurnServersError, <Error>err, false); // non-fatal
- }
- }
- }
-
- return credentialsGood;
- }
-
- /**
- * Set whether to allow a fallback ICE server should be used for negotiating a
- * WebRTC connection if the homeserver doesn't provide any servers. Defaults to
- * false.
- *
- */
- public setFallbackICEServerAllowed(allow: boolean): void {
- this.fallbackICEServerAllowed = allow;
- }
-
- /**
- * Get whether to allow a fallback ICE server should be used for negotiating a
- * WebRTC connection if the homeserver doesn't provide any servers. Defaults to
- * false.
- *
- * @returns
- */
- public isFallbackICEServerAllowed(): boolean {
- return this.fallbackICEServerAllowed;
- }
-
- /**
- * Determines if the current user is an administrator of the Synapse homeserver.
- * Returns false if untrue or the homeserver does not appear to be a Synapse
- * homeserver. <strong>This function is implementation specific and may change
- * as a result.</strong>
- * @returns true if the user appears to be a Synapse administrator.
- */
- public isSynapseAdministrator(): Promise<boolean> {
- const path = utils.encodeUri("/_synapse/admin/v1/users/$userId/admin", { $userId: this.getUserId()! });
- return this.http
- .authedRequest<{ admin: boolean }>(Method.Get, path, undefined, undefined, { prefix: "" })
- .then((r) => r.admin); // pull out the specific boolean we want
- }
-
- /**
- * Performs a whois lookup on a user using Synapse's administrator API.
- * <strong>This function is implementation specific and may change as a
- * result.</strong>
- * @param userId - the User ID to look up.
- * @returns the whois response - see Synapse docs for information.
- */
- public whoisSynapseUser(userId: string): Promise<ISynapseAdminWhoisResponse> {
- const path = utils.encodeUri("/_synapse/admin/v1/whois/$userId", { $userId: userId });
- return this.http.authedRequest(Method.Get, path, undefined, undefined, { prefix: "" });
- }
-
- /**
- * Deactivates a user using Synapse's administrator API. <strong>This
- * function is implementation specific and may change as a result.</strong>
- * @param userId - the User ID to deactivate.
- * @returns the deactivate response - see Synapse docs for information.
- */
- public deactivateSynapseUser(userId: string): Promise<ISynapseAdminDeactivateResponse> {
- const path = utils.encodeUri("/_synapse/admin/v1/deactivate/$userId", { $userId: userId });
- return this.http.authedRequest(Method.Post, path, undefined, undefined, { prefix: "" });
- }
-
- private async fetchClientWellKnown(): Promise<void> {
- // `getRawClientConfig` does not throw or reject on network errors, instead
- // it absorbs errors and returns `{}`.
- this.clientWellKnownPromise = AutoDiscovery.getRawClientConfig(this.getDomain() ?? undefined);
- this.clientWellKnown = await this.clientWellKnownPromise;
- this.emit(ClientEvent.ClientWellKnown, this.clientWellKnown);
- }
-
- public getClientWellKnown(): IClientWellKnown | undefined {
- return this.clientWellKnown;
- }
-
- public waitForClientWellKnown(): Promise<IClientWellKnown> {
- if (!this.clientRunning) {
- throw new Error("Client is not running");
- }
- return this.clientWellKnownPromise!;
- }
-
- /**
- * store client options with boolean/string/numeric values
- * to know in the next session what flags the sync data was
- * created with (e.g. lazy loading)
- * @param opts - the complete set of client options
- * @returns for store operation
- */
- public storeClientOptions(): Promise<void> {
- // XXX: Intended private, used in code
- const primTypes = ["boolean", "string", "number"];
- const serializableOpts = Object.entries(this.clientOpts!)
- .filter(([key, value]) => {
- return primTypes.includes(typeof value);
- })
- .reduce<Record<string, any>>((obj, [key, value]) => {
- obj[key] = value;
- return obj;
- }, {});
- return this.store.storeClientOptions(serializableOpts);
- }
-
- /**
- * Gets a set of room IDs in common with another user
- * @param userId - The userId to check.
- * @returns Promise which resolves to a set of rooms
- * @returns Rejects: with an error response.
- */
- // eslint-disable-next-line
- public async _unstable_getSharedRooms(userId: string): Promise<string[]> {
- const sharedRoomsSupport = await this.doesServerSupportUnstableFeature("uk.half-shot.msc2666");
- const mutualRoomsSupport = await this.doesServerSupportUnstableFeature("uk.half-shot.msc2666.mutual_rooms");
-
- if (!sharedRoomsSupport && !mutualRoomsSupport) {
- throw Error("Server does not support mutual_rooms API");
- }
-
- const path = utils.encodeUri(
- `/uk.half-shot.msc2666/user/${mutualRoomsSupport ? "mutual_rooms" : "shared_rooms"}/$userId`,
- { $userId: userId },
- );
-
- const res = await this.http.authedRequest<{ joined: string[] }>(Method.Get, path, undefined, undefined, {
- prefix: ClientPrefix.Unstable,
- });
- return res.joined;
- }
-
- /**
- * Get the API versions supported by the server, along with any
- * unstable APIs it supports
- * @returns The server /versions response
- */
- public async getVersions(): Promise<IServerVersions> {
- if (this.serverVersionsPromise) {
- return this.serverVersionsPromise;
- }
-
- this.serverVersionsPromise = this.http
- .request<IServerVersions>(
- Method.Get,
- "/_matrix/client/versions",
- undefined, // queryParams
- undefined, // data
- {
- prefix: "",
- },
- )
- .catch((e) => {
- // Need to unset this if it fails, otherwise we'll never retry
- this.serverVersionsPromise = undefined;
- // but rethrow the exception to anything that was waiting
- throw e;
- });
-
- const serverVersions = await this.serverVersionsPromise;
- this.canSupport = await buildFeatureSupportMap(serverVersions);
-
- return this.serverVersionsPromise;
- }
-
- /**
- * Check if a particular spec version is supported by the server.
- * @param version - The spec version (such as "r0.5.0") to check for.
- * @returns Whether it is supported
- */
- public async isVersionSupported(version: string): Promise<boolean> {
- const { versions } = await this.getVersions();
- return versions && versions.includes(version);
- }
-
- /**
- * Query the server to see if it supports members lazy loading
- * @returns true if server supports lazy loading
- */
- public async doesServerSupportLazyLoading(): Promise<boolean> {
- const response = await this.getVersions();
- if (!response) return false;
-
- const versions = response["versions"];
- const unstableFeatures = response["unstable_features"];
-
- return (
- (versions && versions.includes("r0.5.0")) || (unstableFeatures && unstableFeatures["m.lazy_load_members"])
- );
- }
-
- /**
- * Query the server to see if the `id_server` parameter is required
- * when registering with an 3pid, adding a 3pid or resetting password.
- * @returns true if id_server parameter is required
- */
- public async doesServerRequireIdServerParam(): Promise<boolean> {
- const response = await this.getVersions();
- if (!response) return true;
-
- const versions = response["versions"];
-
- // Supporting r0.6.0 is the same as having the flag set to false
- if (versions && versions.includes("r0.6.0")) {
- return false;
- }
-
- const unstableFeatures = response["unstable_features"];
- if (!unstableFeatures) return true;
- if (unstableFeatures["m.require_identity_server"] === undefined) {
- return true;
- } else {
- return unstableFeatures["m.require_identity_server"];
- }
- }
-
- /**
- * Query the server to see if the `id_access_token` parameter can be safely
- * passed to the homeserver. Some homeservers may trigger errors if they are not
- * prepared for the new parameter.
- * @returns true if id_access_token can be sent
- */
- public async doesServerAcceptIdentityAccessToken(): Promise<boolean> {
- const response = await this.getVersions();
- if (!response) return false;
-
- const versions = response["versions"];
- const unstableFeatures = response["unstable_features"];
- return (versions && versions.includes("r0.6.0")) || (unstableFeatures && unstableFeatures["m.id_access_token"]);
- }
-
- /**
- * Query the server to see if it supports separate 3PID add and bind functions.
- * This affects the sequence of API calls clients should use for these operations,
- * so it's helpful to be able to check for support.
- * @returns true if separate functions are supported
- */
- public async doesServerSupportSeparateAddAndBind(): Promise<boolean> {
- const response = await this.getVersions();
- if (!response) return false;
-
- const versions = response["versions"];
- const unstableFeatures = response["unstable_features"];
-
- return versions?.includes("r0.6.0") || unstableFeatures?.["m.separate_add_and_bind"];
- }
-
- /**
- * Query the server to see if it lists support for an unstable feature
- * in the /versions response
- * @param feature - the feature name
- * @returns true if the feature is supported
- */
- public async doesServerSupportUnstableFeature(feature: string): Promise<boolean> {
- const response = await this.getVersions();
- if (!response) return false;
- const unstableFeatures = response["unstable_features"];
- return unstableFeatures && !!unstableFeatures[feature];
- }
-
- /**
- * Query the server to see if it is forcing encryption to be enabled for
- * a given room preset, based on the /versions response.
- * @param presetName - The name of the preset to check.
- * @returns true if the server is forcing encryption
- * for the preset.
- */
- public async doesServerForceEncryptionForPreset(presetName: Preset): Promise<boolean> {
- const response = await this.getVersions();
- if (!response) return false;
- const unstableFeatures = response["unstable_features"];
-
- // The preset name in the versions response will be without the _chat suffix.
- const versionsPresetName = presetName.includes("_chat")
- ? presetName.substring(0, presetName.indexOf("_chat"))
- : presetName;
-
- return unstableFeatures && !!unstableFeatures[`io.element.e2ee_forced.${versionsPresetName}`];
- }
-
- public async doesServerSupportThread(): Promise<{
- threads: FeatureSupport;
- list: FeatureSupport;
- fwdPagination: FeatureSupport;
- }> {
- if (await this.isVersionSupported("v1.4")) {
- return {
- threads: FeatureSupport.Stable,
- list: FeatureSupport.Stable,
- fwdPagination: FeatureSupport.Stable,
- };
- }
-
- try {
- const [threadUnstable, threadStable, listUnstable, listStable, fwdPaginationUnstable, fwdPaginationStable] =
- await Promise.all([
- this.doesServerSupportUnstableFeature("org.matrix.msc3440"),
- this.doesServerSupportUnstableFeature("org.matrix.msc3440.stable"),
- this.doesServerSupportUnstableFeature("org.matrix.msc3856"),
- this.doesServerSupportUnstableFeature("org.matrix.msc3856.stable"),
- this.doesServerSupportUnstableFeature("org.matrix.msc3715"),
- this.doesServerSupportUnstableFeature("org.matrix.msc3715.stable"),
- ]);
-
- return {
- threads: determineFeatureSupport(threadStable, threadUnstable),
- list: determineFeatureSupport(listStable, listUnstable),
- fwdPagination: determineFeatureSupport(fwdPaginationStable, fwdPaginationUnstable),
- };
- } catch (e) {
- return {
- threads: FeatureSupport.None,
- list: FeatureSupport.None,
- fwdPagination: FeatureSupport.None,
- };
- }
- }
-
- /**
- * Query the server to see if it supports the MSC2457 `logout_devices` parameter when setting password
- * @returns true if server supports the `logout_devices` parameter
- */
- public doesServerSupportLogoutDevices(): Promise<boolean> {
- return this.isVersionSupported("r0.6.1");
- }
-
- /**
- * Get if lazy loading members is being used.
- * @returns Whether or not members are lazy loaded by this client
- */
- public hasLazyLoadMembersEnabled(): boolean {
- return !!this.clientOpts?.lazyLoadMembers;
- }
-
- /**
- * Set a function which is called when /sync returns a 'limited' response.
- * It is called with a room ID and returns a boolean. It should return 'true' if the SDK
- * can SAFELY remove events from this room. It may not be safe to remove events if there
- * are other references to the timelines for this room, e.g because the client is
- * actively viewing events in this room.
- * Default: returns false.
- * @param cb - The callback which will be invoked.
- */
- public setCanResetTimelineCallback(cb: ResetTimelineCallback): void {
- this.canResetTimelineCallback = cb;
- }
-
- /**
- * Get the callback set via `setCanResetTimelineCallback`.
- * @returns The callback or null
- */
- public getCanResetTimelineCallback(): ResetTimelineCallback | undefined {
- return this.canResetTimelineCallback;
- }
-
- /**
- * Returns relations for a given event. Handles encryption transparently,
- * with the caveat that the amount of events returned might be 0, even though you get a nextBatch.
- * When the returned promise resolves, all messages should have finished trying to decrypt.
- * @param roomId - the room of the event
- * @param eventId - the id of the event
- * @param relationType - the rel_type of the relations requested
- * @param eventType - the event type of the relations requested
- * @param opts - options with optional values for the request.
- * @returns an object with `events` as `MatrixEvent[]` and optionally `nextBatch` if more relations are available.
- */
- public async relations(
- roomId: string,
- eventId: string,
- relationType?: RelationType | string | null,
- eventType?: EventType | string | null,
- opts: IRelationsRequestOpts = { dir: Direction.Backward },
- ): Promise<{
- originalEvent?: MatrixEvent | null;
- events: MatrixEvent[];
- nextBatch?: string | null;
- prevBatch?: string | null;
- }> {
- const fetchedEventType = eventType ? this.getEncryptedIfNeededEventType(roomId, eventType) : null;
- const [eventResult, result] = await Promise.all([
- this.fetchRoomEvent(roomId, eventId),
- this.fetchRelations(roomId, eventId, relationType, fetchedEventType, opts),
- ]);
- const mapper = this.getEventMapper();
-
- const originalEvent = eventResult ? mapper(eventResult) : undefined;
- let events = result.chunk.map(mapper);
-
- if (fetchedEventType === EventType.RoomMessageEncrypted) {
- const allEvents = originalEvent ? events.concat(originalEvent) : events;
- await Promise.all(allEvents.map((e) => this.decryptEventIfNeeded(e)));
- if (eventType !== null) {
- events = events.filter((e) => e.getType() === eventType);
- }
- }
-
- if (originalEvent && relationType === RelationType.Replace) {
- events = events.filter((e) => e.getSender() === originalEvent.getSender());
- }
- return {
- originalEvent: originalEvent ?? null,
- events,
- nextBatch: result.next_batch ?? null,
- prevBatch: result.prev_batch ?? null,
- };
- }
-
- /**
- * The app may wish to see if we have a key cached without
- * triggering a user interaction.
- */
- public getCrossSigningCacheCallbacks(): ICacheCallbacks | undefined {
- // XXX: Private member access
- return this.crypto?.crossSigningInfo.getCacheCallbacks();
- }
-
- /**
- * Generates a random string suitable for use as a client secret. <strong>This
- * method is experimental and may change.</strong>
- * @returns A new client secret
- */
- public generateClientSecret(): string {
- return randomString(32);
- }
-
- /**
- * Attempts to decrypt an event
- * @param event - The event to decrypt
- * @returns A decryption promise
- */
- public decryptEventIfNeeded(event: MatrixEvent, options?: IDecryptOptions): Promise<void> {
- if (event.shouldAttemptDecryption() && this.isCryptoEnabled()) {
- event.attemptDecryption(this.cryptoBackend!, options);
- }
-
- if (event.isBeingDecrypted()) {
- return event.getDecryptionPromise()!;
- } else {
- return Promise.resolve();
- }
- }
-
- private termsUrlForService(serviceType: SERVICE_TYPES, baseUrl: string): URL {
- switch (serviceType) {
- case SERVICE_TYPES.IS:
- return this.http.getUrl("/terms", undefined, IdentityPrefix.V2, baseUrl);
- case SERVICE_TYPES.IM:
- return this.http.getUrl("/terms", undefined, "/_matrix/integrations/v1", baseUrl);
- default:
- throw new Error("Unsupported service type");
- }
- }
-
- /**
- * Get the Homeserver URL of this client
- * @returns Homeserver URL of this client
- */
- public getHomeserverUrl(): string {
- return this.baseUrl;
- }
-
- /**
- * Get the identity server URL of this client
- * @param stripProto - whether or not to strip the protocol from the URL
- * @returns Identity server URL of this client
- */
- public getIdentityServerUrl(stripProto = false): string | undefined {
- if (stripProto && (this.idBaseUrl?.startsWith("http://") || this.idBaseUrl?.startsWith("https://"))) {
- return this.idBaseUrl.split("://")[1];
- }
- return this.idBaseUrl;
- }
-
- /**
- * Set the identity server URL of this client
- * @param url - New identity server URL
- */
- public setIdentityServerUrl(url: string): void {
- this.idBaseUrl = utils.ensureNoTrailingSlash(url);
- this.http.setIdBaseUrl(this.idBaseUrl);
- }
-
- /**
- * Get the access token associated with this account.
- * @returns The access_token or null
- */
- public getAccessToken(): string | null {
- return this.http.opts.accessToken || null;
- }
-
- /**
- * Set the access token associated with this account.
- * @param token - The new access token.
- */
- public setAccessToken(token: string): void {
- this.http.opts.accessToken = token;
- }
-
- /**
- * @returns true if there is a valid access_token for this client.
- */
- public isLoggedIn(): boolean {
- return this.http.opts.accessToken !== undefined;
- }
-
- /**
- * Make up a new transaction id
- *
- * @returns a new, unique, transaction id
- */
- public makeTxnId(): string {
- return "m" + new Date().getTime() + "." + this.txnCtr++;
- }
-
- /**
- * Check whether a username is available prior to registration. An error response
- * indicates an invalid/unavailable username.
- * @param username - The username to check the availability of.
- * @returns Promise which resolves: to boolean of whether the username is available.
- */
- public isUsernameAvailable(username: string): Promise<boolean> {
- return this.http
- .authedRequest<{ available: true }>(Method.Get, "/register/available", { username })
- .then((response) => {
- return response.available;
- })
- .catch((response) => {
- if (response.errcode === "M_USER_IN_USE") {
- return false;
- }
- return Promise.reject(response);
- });
- }
-
- /**
- * @param bindThreepids - Set key 'email' to true to bind any email
- * threepid uses during registration in the identity server. Set 'msisdn' to
- * true to bind msisdn.
- * @returns Promise which resolves: TODO
- * @returns Rejects: with an error response.
- */
- public register(
- username: string,
- password: string,
- sessionId: string | null,
- auth: { session?: string; type: string },
- bindThreepids?: boolean | null | { email?: boolean; msisdn?: boolean },
- guestAccessToken?: string,
- inhibitLogin?: boolean,
- ): Promise<IAuthData> {
- // backwards compat
- if (bindThreepids === true) {
- bindThreepids = { email: true };
- } else if (bindThreepids === null || bindThreepids === undefined || bindThreepids === false) {
- bindThreepids = {};
- }
- if (sessionId) {
- auth.session = sessionId;
- }
-
- const params: IRegisterRequestParams = {
- auth: auth,
- refresh_token: true, // always ask for a refresh token - does nothing if unsupported
- };
- if (username !== undefined && username !== null) {
- params.username = username;
- }
- if (password !== undefined && password !== null) {
- params.password = password;
- }
- if (bindThreepids.email) {
- params.bind_email = true;
- }
- if (bindThreepids.msisdn) {
- params.bind_msisdn = true;
- }
- if (guestAccessToken !== undefined && guestAccessToken !== null) {
- params.guest_access_token = guestAccessToken;
- }
- if (inhibitLogin !== undefined && inhibitLogin !== null) {
- params.inhibit_login = inhibitLogin;
- }
- // Temporary parameter added to make the register endpoint advertise
- // msisdn flows. This exists because there are clients that break
- // when given stages they don't recognise. This parameter will cease
- // to be necessary once these old clients are gone.
- // Only send it if we send any params at all (the password param is
- // mandatory, so if we send any params, we'll send the password param)
- if (password !== undefined && password !== null) {
- params.x_show_msisdn = true;
- }
-
- return this.registerRequest(params);
- }
-
- /**
- * Register a guest account.
- * This method returns the auth info needed to create a new authenticated client,
- * Remember to call `setGuest(true)` on the (guest-)authenticated client, e.g:
- * ```javascript
- * const tmpClient = await sdk.createClient(MATRIX_INSTANCE);
- * const { user_id, device_id, access_token } = tmpClient.registerGuest();
- * const client = createClient({
- * baseUrl: MATRIX_INSTANCE,
- * accessToken: access_token,
- * userId: user_id,
- * deviceId: device_id,
- * })
- * client.setGuest(true);
- * ```
- *
- * @param body - JSON HTTP body to provide.
- * @returns Promise which resolves: JSON object that contains:
- * `{ user_id, device_id, access_token, home_server }`
- * @returns Rejects: with an error response.
- */
- public registerGuest({ body }: { body?: any } = {}): Promise<any> {
- // TODO: Types
- return this.registerRequest(body || {}, "guest");
- }
-
- /**
- * @param data - parameters for registration request
- * @param kind - type of user to register. may be "guest"
- * @returns Promise which resolves: to the /register response
- * @returns Rejects: with an error response.
- */
- public registerRequest(data: IRegisterRequestParams, kind?: string): Promise<IAuthData> {
- const params: { kind?: string } = {};
- if (kind) {
- params.kind = kind;
- }
-
- return this.http.request(Method.Post, "/register", params, data);
- }
-
- /**
- * Refreshes an access token using a provided refresh token. The refresh token
- * must be valid for the current access token known to the client instance.
- *
- * Note that this function will not cause a logout if the token is deemed
- * unknown by the server - the caller is responsible for managing logout
- * actions on error.
- * @param refreshToken - The refresh token.
- * @returns Promise which resolves to the new token.
- * @returns Rejects with an error response.
- */
- public refreshToken(refreshToken: string): Promise<IRefreshTokenResponse> {
- return this.http.authedRequest(
- Method.Post,
- "/refresh",
- undefined,
- { refresh_token: refreshToken },
- {
- prefix: ClientPrefix.V1,
- inhibitLogoutEmit: true, // we don't want to cause logout loops
- },
- );
- }
-
- /**
- * @returns Promise which resolves to the available login flows
- * @returns Rejects: with an error response.
- */
- public loginFlows(): Promise<ILoginFlowsResponse> {
- return this.http.request(Method.Get, "/login");
- }
-
- /**
- * @returns Promise which resolves: TODO
- * @returns Rejects: with an error response.
- */
- public login(loginType: string, data: any): Promise<any> {
- // TODO: Types
- const loginData = {
- type: loginType,
- };
-
- // merge data into loginData
- Object.assign(loginData, data);
-
- return this.http
- .authedRequest<{
- access_token?: string;
- user_id?: string;
- }>(Method.Post, "/login", undefined, loginData)
- .then((response) => {
- if (response.access_token && response.user_id) {
- this.http.opts.accessToken = response.access_token;
- this.credentials = {
- userId: response.user_id,
- };
- }
- return response;
- });
- }
-
- /**
- * @returns Promise which resolves: TODO
- * @returns Rejects: with an error response.
- */
- public loginWithPassword(user: string, password: string): Promise<any> {
- // TODO: Types
- return this.login("m.login.password", {
- user: user,
- password: password,
- });
- }
-
- /**
- * @param relayState - URL Callback after SAML2 Authentication
- * @returns Promise which resolves: TODO
- * @returns Rejects: with an error response.
- */
- public loginWithSAML2(relayState: string): Promise<any> {
- // TODO: Types
- return this.login("m.login.saml2", {
- relay_state: relayState,
- });
- }
-
- /**
- * @param redirectUrl - The URL to redirect to after the HS
- * authenticates with CAS.
- * @returns The HS URL to hit to begin the CAS login process.
- */
- public getCasLoginUrl(redirectUrl: string): string {
- return this.getSsoLoginUrl(redirectUrl, "cas");
- }
-
- /**
- * @param redirectUrl - The URL to redirect to after the HS
- * authenticates with the SSO.
- * @param loginType - The type of SSO login we are doing (sso or cas).
- * Defaults to 'sso'.
- * @param idpId - The ID of the Identity Provider being targeted, optional.
- * @param action - the SSO flow to indicate to the IdP, optional.
- * @returns The HS URL to hit to begin the SSO login process.
- */
- public getSsoLoginUrl(redirectUrl: string, loginType = "sso", idpId?: string, action?: SSOAction): string {
- let url = "/login/" + loginType + "/redirect";
- if (idpId) {
- url += "/" + idpId;
- }
-
- const params = {
- redirectUrl,
- [SSO_ACTION_PARAM.unstable!]: action,
- };
-
- return this.http.getUrl(url, params, ClientPrefix.R0).href;
- }
-
- /**
- * @param token - Login token previously received from homeserver
- * @returns Promise which resolves: TODO
- * @returns Rejects: with an error response.
- */
- public loginWithToken(token: string): Promise<any> {
- // TODO: Types
- return this.login("m.login.token", {
- token: token,
- });
- }
-
- /**
- * Logs out the current session.
- * Obviously, further calls that require authorisation should fail after this
- * method is called. The state of the MatrixClient object is not affected:
- * it is up to the caller to either reset or destroy the MatrixClient after
- * this method succeeds.
- * @param stopClient - whether to stop the client before calling /logout to prevent invalid token errors.
- * @returns Promise which resolves: On success, the empty object `{}`
- */
- public async logout(stopClient = false): Promise<{}> {
- if (this.crypto?.backupManager?.getKeyBackupEnabled()) {
- try {
- while ((await this.crypto.backupManager.backupPendingKeys(200)) > 0);
- } catch (err) {
- logger.error("Key backup request failed when logging out. Some keys may be missing from backup", err);
- }
- }
-
- if (stopClient) {
- this.stopClient();
- this.http.abort();
- }
-
- return this.http.authedRequest(Method.Post, "/logout");
- }
-
- /**
- * Deactivates the logged-in account.
- * Obviously, further calls that require authorisation should fail after this
- * method is called. The state of the MatrixClient object is not affected:
- * it is up to the caller to either reset or destroy the MatrixClient after
- * this method succeeds.
- * @param auth - Optional. Auth data to supply for User-Interactive auth.
- * @param erase - Optional. If set, send as `erase` attribute in the
- * JSON request body, indicating whether the account should be erased. Defaults
- * to false.
- * @returns Promise which resolves: On success, the empty object
- */
- public deactivateAccount(auth?: any, erase?: boolean): Promise<{}> {
- const body: any = {};
- if (auth) {
- body.auth = auth;
- }
- if (erase !== undefined) {
- body.erase = erase;
- }
-
- return this.http.authedRequest(Method.Post, "/account/deactivate", undefined, body);
- }
-
- /**
- * Make a request for an `m.login.token` to be issued as per
- * [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882).
- * The server may require User-Interactive auth.
- * Note that this is UNSTABLE and subject to breaking changes without notice.
- * @param auth - Optional. Auth data to supply for User-Interactive auth.
- * @returns Promise which resolves: On success, the token response
- * or UIA auth data.
- */
- public requestLoginToken(auth?: IAuthData): Promise<UIAResponse<LoginTokenPostResponse>> {
- const body: UIARequest<{}> = { auth };
- return this.http.authedRequest(
- Method.Post,
- "/org.matrix.msc3882/login/token",
- undefined, // no query params
- body,
- { prefix: ClientPrefix.Unstable },
- );
- }
-
- /**
- * Get the fallback URL to use for unknown interactive-auth stages.
- *
- * @param loginType - the type of stage being attempted
- * @param authSessionId - the auth session ID provided by the homeserver
- *
- * @returns HS URL to hit to for the fallback interface
- */
- public getFallbackAuthUrl(loginType: string, authSessionId: string): string {
- const path = utils.encodeUri("/auth/$loginType/fallback/web", {
- $loginType: loginType,
- });
-
- return this.http.getUrl(
- path,
- {
- session: authSessionId,
- },
- ClientPrefix.R0,
- ).href;
- }
-
- /**
- * Create a new room.
- * @param options - a list of options to pass to the /createRoom API.
- * @returns Promise which resolves: `{room_id: {string}}`
- * @returns Rejects: with an error response.
- */
- public async createRoom(options: ICreateRoomOpts): Promise<{ room_id: string }> {
- // eslint-disable-line camelcase
- // some valid options include: room_alias_name, visibility, invite
-
- // inject the id_access_token if inviting 3rd party addresses
- const invitesNeedingToken = (options.invite_3pid || []).filter((i) => !i.id_access_token);
- if (
- invitesNeedingToken.length > 0 &&
- this.identityServer?.getAccessToken &&
- (await this.doesServerAcceptIdentityAccessToken())
- ) {
- const identityAccessToken = await this.identityServer.getAccessToken();
- if (identityAccessToken) {
- for (const invite of invitesNeedingToken) {
- invite.id_access_token = identityAccessToken;
- }
- }
- }
-
- return this.http.authedRequest(Method.Post, "/createRoom", undefined, options);
- }
-
- /**
- * Fetches relations for a given event
- * @param roomId - the room of the event
- * @param eventId - the id of the event
- * @param relationType - the rel_type of the relations requested
- * @param eventType - the event type of the relations requested
- * @param opts - options with optional values for the request.
- * @returns the response, with chunk, prev_batch and, next_batch.
- */
- public fetchRelations(
- roomId: string,
- eventId: string,
- relationType?: RelationType | string | null,
- eventType?: EventType | string | null,
- opts: IRelationsRequestOpts = { dir: Direction.Backward },
- ): Promise<IRelationsResponse> {
- let params = opts as QueryDict;
- if (Thread.hasServerSideFwdPaginationSupport === FeatureSupport.Experimental) {
- params = replaceParam("dir", "org.matrix.msc3715.dir", params);
- }
- const queryString = utils.encodeParams(params);
-
- let templatedUrl = "/rooms/$roomId/relations/$eventId";
- if (relationType !== null) {
- templatedUrl += "/$relationType";
- if (eventType !== null) {
- templatedUrl += "/$eventType";
- }
- } else if (eventType !== null) {
- logger.warn(`eventType: ${eventType} ignored when fetching
- relations as relationType is null`);
- eventType = null;
- }
-
- const path = utils.encodeUri(templatedUrl + "?" + queryString, {
- $roomId: roomId,
- $eventId: eventId,
- $relationType: relationType!,
- $eventType: eventType!,
- });
- return this.http.authedRequest(Method.Get, path, undefined, undefined, {
- prefix: ClientPrefix.V1,
- });
- }
-
- /**
- * @returns Promise which resolves: TODO
- * @returns Rejects: with an error response.
- */
- public roomState(roomId: string): Promise<IStateEventWithRoomId[]> {
- const path = utils.encodeUri("/rooms/$roomId/state", { $roomId: roomId });
- return this.http.authedRequest(Method.Get, path);
- }
-
- /**
- * Get an event in a room by its event id.
- *
- * @returns Promise which resolves to an object containing the event.
- * @returns Rejects: with an error response.
- */
- public fetchRoomEvent(roomId: string, eventId: string): Promise<Partial<IEvent>> {
- const path = utils.encodeUri("/rooms/$roomId/event/$eventId", {
- $roomId: roomId,
- $eventId: eventId,
- });
- return this.http.authedRequest(Method.Get, path);
- }
-
- /**
- * @param includeMembership - the membership type to include in the response
- * @param excludeMembership - the membership type to exclude from the response
- * @param atEventId - the id of the event for which moment in the timeline the members should be returned for
- * @returns Promise which resolves: dictionary of userid to profile information
- * @returns Rejects: with an error response.
- */
- public members(
- roomId: string,
- includeMembership?: string,
- excludeMembership?: string,
- atEventId?: string,
- ): Promise<{ [userId: string]: IStateEventWithRoomId[] }> {
- const queryParams: Record<string, string> = {};
- if (includeMembership) {
- queryParams.membership = includeMembership;
- }
- if (excludeMembership) {
- queryParams.not_membership = excludeMembership;
- }
- if (atEventId) {
- queryParams.at = atEventId;
- }
-
- const queryString = utils.encodeParams(queryParams);
-
- const path = utils.encodeUri("/rooms/$roomId/members?" + queryString, { $roomId: roomId });
- return this.http.authedRequest(Method.Get, path);
- }
-
- /**
- * Upgrades a room to a new protocol version
- * @param newVersion - The target version to upgrade to
- * @returns Promise which resolves: Object with key 'replacement_room'
- * @returns Rejects: with an error response.
- */
- public upgradeRoom(roomId: string, newVersion: string): Promise<{ replacement_room: string }> {
- // eslint-disable-line camelcase
- const path = utils.encodeUri("/rooms/$roomId/upgrade", { $roomId: roomId });
- return this.http.authedRequest(Method.Post, path, undefined, { new_version: newVersion });
- }
-
- /**
- * Retrieve a state event.
- * @returns Promise which resolves: TODO
- * @returns Rejects: with an error response.
- */
- public getStateEvent(roomId: string, eventType: string, stateKey: string): Promise<Record<string, any>> {
- const pathParams = {
- $roomId: roomId,
- $eventType: eventType,
- $stateKey: stateKey,
- };
- let path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams);
- if (stateKey !== undefined) {
- path = utils.encodeUri(path + "/$stateKey", pathParams);
- }
- return this.http.authedRequest(Method.Get, path);
- }
-
- /**
- * @param opts - Options for the request function.
- * @returns Promise which resolves: TODO
- * @returns Rejects: with an error response.
- */
- public sendStateEvent(
- roomId: string,
- eventType: string,
- content: any,
- stateKey = "",
- opts: IRequestOpts = {},
- ): Promise<ISendEventResponse> {
- const pathParams = {
- $roomId: roomId,
- $eventType: eventType,
- $stateKey: stateKey,
- };
- let path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams);
- if (stateKey !== undefined) {
- path = utils.encodeUri(path + "/$stateKey", pathParams);
- }
- return this.http.authedRequest(Method.Put, path, undefined, content, opts);
- }
-
- /**
- * @returns Promise which resolves: TODO
- * @returns Rejects: with an error response.
- */
- public roomInitialSync(roomId: string, limit: number): Promise<IRoomInitialSyncResponse> {
- const path = utils.encodeUri("/rooms/$roomId/initialSync", { $roomId: roomId });
-
- return this.http.authedRequest(Method.Get, path, { limit: limit?.toString() ?? "30" });
- }
-
- /**
- * Set a marker to indicate the point in a room before which the user has read every
- * event. This can be retrieved from room account data (the event type is `m.fully_read`)
- * and displayed as a horizontal line in the timeline that is visually distinct to the
- * position of the user's own read receipt.
- * @param roomId - ID of the room that has been read
- * @param rmEventId - ID of the event that has been read
- * @param rrEventId - ID of the event tracked by the read receipt. This is here
- * for convenience because the RR and the RM are commonly updated at the same time as
- * each other. Optional.
- * @param rpEventId - rpEvent the m.read.private read receipt event for when we
- * don't want other users to see the read receipts. This is experimental. Optional.
- * @returns Promise which resolves: the empty object, `{}`.
- */
- public async setRoomReadMarkersHttpRequest(
- roomId: string,
- rmEventId: string,
- rrEventId?: string,
- rpEventId?: string,
- ): Promise<{}> {
- const path = utils.encodeUri("/rooms/$roomId/read_markers", {
- $roomId: roomId,
- });
-
- const content: IContent = {
- [ReceiptType.FullyRead]: rmEventId,
- [ReceiptType.Read]: rrEventId,
- };
-
- if (
- (await this.doesServerSupportUnstableFeature("org.matrix.msc2285.stable")) ||
- (await this.isVersionSupported("v1.4"))
- ) {
- content[ReceiptType.ReadPrivate] = rpEventId;
- }
-
- return this.http.authedRequest(Method.Post, path, undefined, content);
- }
-
- /**
- * @returns Promise which resolves: A list of the user's current rooms
- * @returns Rejects: with an error response.
- */
- public getJoinedRooms(): Promise<IJoinedRoomsResponse> {
- const path = utils.encodeUri("/joined_rooms", {});
- return this.http.authedRequest(Method.Get, path);
- }
-
- /**
- * Retrieve membership info. for a room.
- * @param roomId - ID of the room to get membership for
- * @returns Promise which resolves: A list of currently joined users
- * and their profile data.
- * @returns Rejects: with an error response.
- */
- public getJoinedRoomMembers(roomId: string): Promise<IJoinedMembersResponse> {
- const path = utils.encodeUri("/rooms/$roomId/joined_members", {
- $roomId: roomId,
- });
- return this.http.authedRequest(Method.Get, path);
- }
-
- /**
- * @param options - Options for this request
- * @param server - The remote server to query for the room list.
- * Optional. If unspecified, get the local home
- * server's public room list.
- * @param limit - Maximum number of entries to return
- * @param since - Token to paginate from
- * @returns Promise which resolves: IPublicRoomsResponse
- * @returns Rejects: with an error response.
- */
- public publicRooms({
- server,
- limit,
- since,
- ...options
- }: IRoomDirectoryOptions = {}): Promise<IPublicRoomsResponse> {
- const queryParams: QueryDict = { server, limit, since };
- if (Object.keys(options).length === 0) {
- return this.http.authedRequest(Method.Get, "/publicRooms", queryParams);
- } else {
- return this.http.authedRequest(Method.Post, "/publicRooms", queryParams, options);
- }
- }
-
- /**
- * Create an alias to room ID mapping.
- * @param alias - The room alias to create.
- * @param roomId - The room ID to link the alias to.
- * @returns Promise which resolves: an empty object `{}`
- * @returns Rejects: with an error response.
- */
- public createAlias(alias: string, roomId: string): Promise<{}> {
- const path = utils.encodeUri("/directory/room/$alias", {
- $alias: alias,
- });
- const data = {
- room_id: roomId,
- };
- return this.http.authedRequest(Method.Put, path, undefined, data);
- }
-
- /**
- * Delete an alias to room ID mapping. This alias must be on your local server,
- * and you must have sufficient access to do this operation.
- * @param alias - The room alias to delete.
- * @returns Promise which resolves: an empty object `{}`.
- * @returns Rejects: with an error response.
- */
- public deleteAlias(alias: string): Promise<{}> {
- const path = utils.encodeUri("/directory/room/$alias", {
- $alias: alias,
- });
- return this.http.authedRequest(Method.Delete, path);
- }
-
- /**
- * Gets the local aliases for the room. Note: this includes all local aliases, unlike the
- * curated list from the m.room.canonical_alias state event.
- * @param roomId - The room ID to get local aliases for.
- * @returns Promise which resolves: an object with an `aliases` property, containing an array of local aliases
- * @returns Rejects: with an error response.
- */
- public getLocalAliases(roomId: string): Promise<{ aliases: string[] }> {
- const path = utils.encodeUri("/rooms/$roomId/aliases", { $roomId: roomId });
- const prefix = ClientPrefix.V3;
- return this.http.authedRequest(Method.Get, path, undefined, undefined, { prefix });
- }
-
- /**
- * Get room info for the given alias.
- * @param alias - The room alias to resolve.
- * @returns Promise which resolves: Object with room_id and servers.
- * @returns Rejects: with an error response.
- */
- public getRoomIdForAlias(alias: string): Promise<{ room_id: string; servers: string[] }> {
- // eslint-disable-line camelcase
- // TODO: deprecate this or resolveRoomAlias
- const path = utils.encodeUri("/directory/room/$alias", {
- $alias: alias,
- });
- return this.http.authedRequest(Method.Get, path);
- }
-
- /**
- * @returns Promise which resolves: Object with room_id and servers.
- * @returns Rejects: with an error response.
- */
- // eslint-disable-next-line camelcase
- public resolveRoomAlias(roomAlias: string): Promise<{ room_id: string; servers: string[] }> {
- // TODO: deprecate this or getRoomIdForAlias
- const path = utils.encodeUri("/directory/room/$alias", { $alias: roomAlias });
- return this.http.request(Method.Get, path);
- }
-
- /**
- * Get the visibility of a room in the current HS's room directory
- * @returns Promise which resolves: TODO
- * @returns Rejects: with an error response.
- */
- public getRoomDirectoryVisibility(roomId: string): Promise<{ visibility: Visibility }> {
- const path = utils.encodeUri("/directory/list/room/$roomId", {
- $roomId: roomId,
- });
- return this.http.authedRequest(Method.Get, path);
- }
-
- /**
- * Set the visbility of a room in the current HS's room directory
- * @param visibility - "public" to make the room visible
- * in the public directory, or "private" to make
- * it invisible.
- * @returns Promise which resolves: to an empty object `{}`
- * @returns Rejects: with an error response.
- */
- public setRoomDirectoryVisibility(roomId: string, visibility: Visibility): Promise<{}> {
- const path = utils.encodeUri("/directory/list/room/$roomId", {
- $roomId: roomId,
- });
- return this.http.authedRequest(Method.Put, path, undefined, { visibility });
- }
-
- /**
- * Set the visbility of a room bridged to a 3rd party network in
- * the current HS's room directory.
- * @param networkId - the network ID of the 3rd party
- * instance under which this room is published under.
- * @param visibility - "public" to make the room visible
- * in the public directory, or "private" to make
- * it invisible.
- * @returns Promise which resolves: result object
- * @returns Rejects: with an error response.
- */
- public setRoomDirectoryVisibilityAppService(
- networkId: string,
- roomId: string,
- visibility: "public" | "private",
- ): Promise<any> {
- // TODO: Types
- const path = utils.encodeUri("/directory/list/appservice/$networkId/$roomId", {
- $networkId: networkId,
- $roomId: roomId,
- });
- return this.http.authedRequest(Method.Put, path, undefined, { visibility: visibility });
- }
-
- /**
- * Query the user directory with a term matching user IDs, display names and domains.
- * @param term - the term with which to search.
- * @param limit - the maximum number of results to return. The server will
- * apply a limit if unspecified.
- * @returns Promise which resolves: an array of results.
- */
- public searchUserDirectory({ term, limit }: { term: string; limit?: number }): Promise<IUserDirectoryResponse> {
- const body: any = {
- search_term: term,
- };
-
- if (limit !== undefined) {
- body.limit = limit;
- }
-
- return this.http.authedRequest(Method.Post, "/user_directory/search", undefined, body);
- }
-
- /**
- * Upload a file to the media repository on the homeserver.
- *
- * @param file - The object to upload. On a browser, something that
- * can be sent to XMLHttpRequest.send (typically a File). Under node.js,
- * a a Buffer, String or ReadStream.
- *
- * @param opts - options object
- *
- * @returns Promise which resolves to response object, as
- * determined by this.opts.onlyData, opts.rawResponse, and
- * opts.onlyContentUri. Rejects with an error (usually a MatrixError).
- */
- public uploadContent(file: FileType, opts?: UploadOpts): Promise<UploadResponse> {
- return this.http.uploadContent(file, opts);
- }
-
- /**
- * Cancel a file upload in progress
- * @param upload - The object returned from uploadContent
- * @returns true if canceled, otherwise false
- */
- public cancelUpload(upload: Promise<UploadResponse>): boolean {
- return this.http.cancelUpload(upload);
- }
-
- /**
- * Get a list of all file uploads in progress
- * @returns Array of objects representing current uploads.
- * Currently in progress is element 0. Keys:
- * - promise: The promise associated with the upload
- * - loaded: Number of bytes uploaded
- * - total: Total number of bytes to upload
- */
- public getCurrentUploads(): Upload[] {
- return this.http.getCurrentUploads();
- }
-
- /**
- * @param info - The kind of info to retrieve (e.g. 'displayname',
- * 'avatar_url').
- * @returns Promise which resolves: TODO
- * @returns Rejects: with an error response.
- */
- public getProfileInfo(
- userId: string,
- info?: string,
- // eslint-disable-next-line camelcase
- ): Promise<{ avatar_url?: string; displayname?: string }> {
- const path = info
- ? utils.encodeUri("/profile/$userId/$info", { $userId: userId, $info: info })
- : utils.encodeUri("/profile/$userId", { $userId: userId });
- return this.http.authedRequest(Method.Get, path);
- }
-
- /**
- * @returns Promise which resolves to a list of the user's threepids.
- * @returns Rejects: with an error response.
- */
- public getThreePids(): Promise<{ threepids: IThreepid[] }> {
- return this.http.authedRequest(Method.Get, "/account/3pid");
- }
-
- /**
- * Add a 3PID to your homeserver account and optionally bind it to an identity
- * server as well. An identity server is required as part of the `creds` object.
- *
- * This API is deprecated, and you should instead use `addThreePidOnly`
- * for homeservers that support it.
- *
- * @returns Promise which resolves: on success
- * @returns Rejects: with an error response.
- */
- public addThreePid(creds: any, bind: boolean): Promise<any> {
- // TODO: Types
- const path = "/account/3pid";
- const data = {
- threePidCreds: creds,
- bind: bind,
- };
- return this.http.authedRequest(Method.Post, path, undefined, data);
- }
-
- /**
- * Add a 3PID to your homeserver account. This API does not use an identity
- * server, as the homeserver is expected to handle 3PID ownership validation.
- *
- * You can check whether a homeserver supports this API via
- * `doesServerSupportSeparateAddAndBind`.
- *
- * @param data - A object with 3PID validation data from having called
- * `account/3pid/<medium>/requestToken` on the homeserver.
- * @returns Promise which resolves: to an empty object `{}`
- * @returns Rejects: with an error response.
- */
- public async addThreePidOnly(data: IAddThreePidOnlyBody): Promise<{}> {
- const path = "/account/3pid/add";
- const prefix = (await this.isVersionSupported("r0.6.0")) ? ClientPrefix.R0 : ClientPrefix.Unstable;
- return this.http.authedRequest(Method.Post, path, undefined, data, { prefix });
- }
-
- /**
- * Bind a 3PID for discovery onto an identity server via the homeserver. The
- * identity server handles 3PID ownership validation and the homeserver records
- * the new binding to track where all 3PIDs for the account are bound.
- *
- * You can check whether a homeserver supports this API via
- * `doesServerSupportSeparateAddAndBind`.
- *
- * @param data - A object with 3PID validation data from having called
- * `validate/<medium>/requestToken` on the identity server. It should also
- * contain `id_server` and `id_access_token` fields as well.
- * @returns Promise which resolves: to an empty object `{}`
- * @returns Rejects: with an error response.
- */
- public async bindThreePid(data: IBindThreePidBody): Promise<{}> {
- const path = "/account/3pid/bind";
- const prefix = (await this.isVersionSupported("r0.6.0")) ? ClientPrefix.R0 : ClientPrefix.Unstable;
- return this.http.authedRequest(Method.Post, path, undefined, data, { prefix });
- }
-
- /**
- * Unbind a 3PID for discovery on an identity server via the homeserver. The
- * homeserver removes its record of the binding to keep an updated record of
- * where all 3PIDs for the account are bound.
- *
- * @param medium - The threepid medium (eg. 'email')
- * @param address - The threepid address (eg. 'bob\@example.com')
- * this must be as returned by getThreePids.
- * @returns Promise which resolves: on success
- * @returns Rejects: with an error response.
- */
- public async unbindThreePid(
- medium: string,
- address: string,
- // eslint-disable-next-line camelcase
- ): Promise<{ id_server_unbind_result: IdServerUnbindResult }> {
- const path = "/account/3pid/unbind";
- const data = {
- medium,
- address,
- id_server: this.getIdentityServerUrl(true),
- };
- const prefix = (await this.isVersionSupported("r0.6.0")) ? ClientPrefix.R0 : ClientPrefix.Unstable;
- return this.http.authedRequest(Method.Post, path, undefined, data, { prefix });
- }
-
- /**
- * @param medium - The threepid medium (eg. 'email')
- * @param address - The threepid address (eg. 'bob\@example.com')
- * this must be as returned by getThreePids.
- * @returns Promise which resolves: The server response on success
- * (generally the empty JSON object)
- * @returns Rejects: with an error response.
- */
- public deleteThreePid(
- medium: string,
- address: string,
- // eslint-disable-next-line camelcase
- ): Promise<{ id_server_unbind_result: IdServerUnbindResult }> {
- const path = "/account/3pid/delete";
- return this.http.authedRequest(Method.Post, path, undefined, { medium, address });
- }
-
- /**
- * Make a request to change your password.
- * @param newPassword - The new desired password.
- * @param logoutDevices - Should all sessions be logged out after the password change. Defaults to true.
- * @returns Promise which resolves: to an empty object `{}`
- * @returns Rejects: with an error response.
- */
- public setPassword(authDict: IAuthDict, newPassword: string, logoutDevices?: boolean): Promise<{}> {
- const path = "/account/password";
- const data = {
- auth: authDict,
- new_password: newPassword,
- logout_devices: logoutDevices,
- };
-
- return this.http.authedRequest<{}>(Method.Post, path, undefined, data);
- }
-
- /**
- * Gets all devices recorded for the logged-in user
- * @returns Promise which resolves: result object
- * @returns Rejects: with an error response.
- */
- public getDevices(): Promise<{ devices: IMyDevice[] }> {
- return this.http.authedRequest(Method.Get, "/devices");
- }
-
- /**
- * Gets specific device details for the logged-in user
- * @param deviceId - device to query
- * @returns Promise which resolves: result object
- * @returns Rejects: with an error response.
- */
- public getDevice(deviceId: string): Promise<IMyDevice> {
- const path = utils.encodeUri("/devices/$device_id", {
- $device_id: deviceId,
- });
- return this.http.authedRequest(Method.Get, path);
- }
-
- /**
- * Update the given device
- *
- * @param deviceId - device to update
- * @param body - body of request
- * @returns Promise which resolves: to an empty object `{}`
- * @returns Rejects: with an error response.
- */
- // eslint-disable-next-line camelcase
- public setDeviceDetails(deviceId: string, body: { display_name: string }): Promise<{}> {
- const path = utils.encodeUri("/devices/$device_id", {
- $device_id: deviceId,
- });
-
- return this.http.authedRequest(Method.Put, path, undefined, body);
- }
-
- /**
- * Delete the given device
- *
- * @param deviceId - device to delete
- * @param auth - Optional. Auth data to supply for User-Interactive auth.
- * @returns Promise which resolves: result object
- * @returns Rejects: with an error response.
- */
- public deleteDevice(deviceId: string, auth?: IAuthDict): Promise<IAuthData | {}> {
- const path = utils.encodeUri("/devices/$device_id", {
- $device_id: deviceId,
- });
-
- const body: any = {};
-
- if (auth) {
- body.auth = auth;
- }
-
- return this.http.authedRequest(Method.Delete, path, undefined, body);
- }
-
- /**
- * Delete multiple device
- *
- * @param devices - IDs of the devices to delete
- * @param auth - Optional. Auth data to supply for User-Interactive auth.
- * @returns Promise which resolves: result object
- * @returns Rejects: with an error response.
- */
- public deleteMultipleDevices(devices: string[], auth?: IAuthDict): Promise<IAuthData | {}> {
- const body: any = { devices };
-
- if (auth) {
- body.auth = auth;
- }
-
- const path = "/delete_devices";
- return this.http.authedRequest(Method.Post, path, undefined, body);
- }
-
- /**
- * Gets all pushers registered for the logged-in user
- *
- * @returns Promise which resolves: Array of objects representing pushers
- * @returns Rejects: with an error response.
- */
- public async getPushers(): Promise<{ pushers: IPusher[] }> {
- const response = await this.http.authedRequest<{ pushers: IPusher[] }>(Method.Get, "/pushers");
-
- // Migration path for clients that connect to a homeserver that does not support
- // MSC3881 yet, see https://github.com/matrix-org/matrix-spec-proposals/blob/kerry/remote-push-toggle/proposals/3881-remote-push-notification-toggling.md#migration
- if (!(await this.doesServerSupportUnstableFeature("org.matrix.msc3881"))) {
- response.pushers = response.pushers.map((pusher) => {
- if (!pusher.hasOwnProperty(PUSHER_ENABLED.name)) {
- pusher[PUSHER_ENABLED.name] = true;
- }
- return pusher;
- });
- }
-
- return response;
- }
-
- /**
- * Adds a new pusher or updates an existing pusher
- *
- * @param pusher - Object representing a pusher
- * @returns Promise which resolves: Empty json object on success
- * @returns Rejects: with an error response.
- */
- public setPusher(pusher: IPusherRequest): Promise<{}> {
- const path = "/pushers/set";
- return this.http.authedRequest(Method.Post, path, undefined, pusher);
- }
-
- /**
- * Persists local notification settings
- * @returns Promise which resolves: an empty object
- * @returns Rejects: with an error response.
- */
- public setLocalNotificationSettings(
- deviceId: string,
- notificationSettings: LocalNotificationSettings,
- ): Promise<{}> {
- const key = `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`;
- return this.setAccountData(key, notificationSettings);
- }
-
- /**
- * Get the push rules for the account from the server.
- * @returns Promise which resolves to the push rules.
- * @returns Rejects: with an error response.
- */
- public getPushRules(): Promise<IPushRules> {
- return this.http.authedRequest<IPushRules>(Method.Get, "/pushrules/").then((rules: IPushRules) => {
- this.setPushRules(rules);
- return this.pushRules!;
- });
- }
-
- /**
- * Update the push rules for the account. This should be called whenever
- * updated push rules are available.
- */
- public setPushRules(rules: IPushRules): void {
- // Fix-up defaults, if applicable.
- this.pushRules = PushProcessor.rewriteDefaultRules(rules, this.getUserId()!);
- // Pre-calculate any necessary caches.
- this.pushProcessor.updateCachedPushRuleKeys(this.pushRules);
- }
-
- /**
- * @returns Promise which resolves: an empty object `{}`
- * @returns Rejects: with an error response.
- */
- public addPushRule(
- scope: string,
- kind: PushRuleKind,
- ruleId: Exclude<string, RuleId>,
- body: Pick<IPushRule, "actions" | "conditions" | "pattern">,
- ): Promise<{}> {
- // NB. Scope not uri encoded because devices need the '/'
- const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", {
- $kind: kind,
- $ruleId: ruleId,
- });
- return this.http.authedRequest(Method.Put, path, undefined, body);
- }
-
- /**
- * @returns Promise which resolves: an empty object `{}`
- * @returns Rejects: with an error response.
- */
- public deletePushRule(scope: string, kind: PushRuleKind, ruleId: Exclude<string, RuleId>): Promise<{}> {
- // NB. Scope not uri encoded because devices need the '/'
- const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", {
- $kind: kind,
- $ruleId: ruleId,
- });
- return this.http.authedRequest(Method.Delete, path);
- }
-
- /**
- * Enable or disable a push notification rule.
- * @returns Promise which resolves: to an empty object `{}`
- * @returns Rejects: with an error response.
- */
- public setPushRuleEnabled(
- scope: string,
- kind: PushRuleKind,
- ruleId: RuleId | string,
- enabled: boolean,
- ): Promise<{}> {
- const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/enabled", {
- $kind: kind,
- $ruleId: ruleId,
- });
- return this.http.authedRequest(Method.Put, path, undefined, { enabled: enabled });
- }
-
- /**
- * Set the actions for a push notification rule.
- * @returns Promise which resolves: to an empty object `{}`
- * @returns Rejects: with an error response.
- */
- public setPushRuleActions(
- scope: string,
- kind: PushRuleKind,
- ruleId: RuleId | string,
- actions: PushRuleAction[],
- ): Promise<{}> {
- const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/actions", {
- $kind: kind,
- $ruleId: ruleId,
- });
- return this.http.authedRequest(Method.Put, path, undefined, { actions: actions });
- }
-
- /**
- * Perform a server-side search.
- * @param next_batch - the batch token to pass in the query string
- * @param body - the JSON object to pass to the request body.
- * @param abortSignal - optional signal used to cancel the http request.
- * @returns Promise which resolves to the search response object.
- * @returns Rejects: with an error response.
- */
- public search(
- { body, next_batch: nextBatch }: { body: ISearchRequestBody; next_batch?: string },
- abortSignal?: AbortSignal,
- ): Promise<ISearchResponse> {
- const queryParams: any = {};
- if (nextBatch) {
- queryParams.next_batch = nextBatch;
- }
- return this.http.authedRequest(Method.Post, "/search", queryParams, body, { abortSignal });
- }
-
- /**
- * Upload keys
- *
- * @param content - body of upload request
- *
- * @param opts - this method no longer takes any opts,
- * used to take opts.device_id but this was not removed from the spec as a redundant parameter
- *
- * @returns Promise which resolves: result object. Rejects: with
- * an error response ({@link MatrixError}).
- */
- public uploadKeysRequest(content: IUploadKeysRequest, opts?: void): Promise<IKeysUploadResponse> {
- return this.http.authedRequest(Method.Post, "/keys/upload", undefined, content);
- }
-
- public uploadKeySignatures(content: KeySignatures): Promise<IUploadKeySignaturesResponse> {
- return this.http.authedRequest(Method.Post, "/keys/signatures/upload", undefined, content, {
- prefix: ClientPrefix.V3,
- });
- }
-
- /**
- * Download device keys
- *
- * @param userIds - list of users to get keys for
- *
- * @param token - sync token to pass in the query request, to help
- * the HS give the most recent results
- *
- * @returns Promise which resolves: result object. Rejects: with
- * an error response ({@link MatrixError}).
- */
- public downloadKeysForUsers(userIds: string[], { token }: { token?: string } = {}): Promise<IDownloadKeyResult> {
- const content: IQueryKeysRequest = {
- device_keys: {},
- };
- if (token !== undefined) {
- content.token = token;
- }
- userIds.forEach((u) => {
- content.device_keys[u] = [];
- });
-
- return this.http.authedRequest(Method.Post, "/keys/query", undefined, content);
- }
-
- /**
- * Claim one-time keys
- *
- * @param devices - a list of [userId, deviceId] pairs
- *
- * @param keyAlgorithm - desired key type
- *
- * @param timeout - the time (in milliseconds) to wait for keys from remote
- * servers
- *
- * @returns Promise which resolves: result object. Rejects: with
- * an error response ({@link MatrixError}).
- */
- public claimOneTimeKeys(
- devices: [string, string][],
- keyAlgorithm = "signed_curve25519",
- timeout?: number,
- ): Promise<IClaimOTKsResult> {
- const queries: Record<string, Record<string, string>> = {};
-
- if (keyAlgorithm === undefined) {
- keyAlgorithm = "signed_curve25519";
- }
-
- for (const [userId, deviceId] of devices) {
- const query = queries[userId] || {};
- queries[userId] = query;
- query[deviceId] = keyAlgorithm;
- }
- const content: IClaimKeysRequest = { one_time_keys: queries };
- if (timeout) {
- content.timeout = timeout;
- }
- const path = "/keys/claim";
- return this.http.authedRequest(Method.Post, path, undefined, content);
- }
-
- /**
- * Ask the server for a list of users who have changed their device lists
- * between a pair of sync tokens
- *
- *
- * @returns Promise which resolves: result object. Rejects: with
- * an error response ({@link MatrixError}).
- */
- public getKeyChanges(oldToken: string, newToken: string): Promise<{ changed: string[]; left: string[] }> {
- const qps = {
- from: oldToken,
- to: newToken,
- };
-
- return this.http.authedRequest(Method.Get, "/keys/changes", qps);
- }
-
- public uploadDeviceSigningKeys(auth?: IAuthData, keys?: CrossSigningKeys): Promise<{}> {
- // API returns empty object
- const data = Object.assign({}, keys);
- if (auth) Object.assign(data, { auth });
- return this.http.authedRequest(Method.Post, "/keys/device_signing/upload", undefined, data, {
- prefix: ClientPrefix.Unstable,
- });
- }
-
- /**
- * Register with an identity server using the OpenID token from the user's
- * Homeserver, which can be retrieved via
- * {@link MatrixClient#getOpenIdToken}.
- *
- * Note that the `/account/register` endpoint (as well as IS authentication in
- * general) was added as part of the v2 API version.
- *
- * @returns Promise which resolves: with object containing an Identity
- * Server access token.
- * @returns Rejects: with an error response.
- */
- public registerWithIdentityServer(hsOpenIdToken: IOpenIDToken): Promise<{
- access_token: string;
- token: string;
- }> {
- if (!this.idBaseUrl) {
- throw new Error("No identity server base URL set");
- }
-
- const uri = this.http.getUrl("/account/register", undefined, IdentityPrefix.V2, this.idBaseUrl);
- return this.http.requestOtherUrl(Method.Post, uri, hsOpenIdToken);
- }
-
- /**
- * Requests an email verification token directly from an identity server.
- *
- * This API is used as part of binding an email for discovery on an identity
- * server. The validation data that results should be passed to the
- * `bindThreePid` method to complete the binding process.
- *
- * @param email - The email address to request a token for
- * @param clientSecret - A secret binary string generated by the client.
- * It is recommended this be around 16 ASCII characters.
- * @param sendAttempt - If an identity server sees a duplicate request
- * with the same sendAttempt, it will not send another email.
- * To request another email to be sent, use a larger value for
- * the sendAttempt param as was used in the previous request.
- * @param nextLink - Optional If specified, the client will be redirected
- * to this link after validation.
- * @param identityAccessToken - The `access_token` field of the identity
- * server `/account/register` response (see {@link registerWithIdentityServer}).
- *
- * @returns Promise which resolves: TODO
- * @returns Rejects: with an error response.
- * @throws Error if no identity server is set
- */
- public requestEmailToken(
- email: string,
- clientSecret: string,
- sendAttempt: number,
- nextLink?: string,
- identityAccessToken?: string,
- ): Promise<IRequestTokenResponse> {
- const params: Record<string, string> = {
- client_secret: clientSecret,
- email: email,
- send_attempt: sendAttempt?.toString(),
- };
- if (nextLink) {
- params.next_link = nextLink;
- }
-
- return this.http.idServerRequest<IRequestTokenResponse>(
- Method.Post,
- "/validate/email/requestToken",
- params,
- IdentityPrefix.V2,
- identityAccessToken,
- );
- }
-
- /**
- * Requests a MSISDN verification token directly from an identity server.
- *
- * This API is used as part of binding a MSISDN for discovery on an identity
- * server. The validation data that results should be passed to the
- * `bindThreePid` method to complete the binding process.
- *
- * @param phoneCountry - The ISO 3166-1 alpha-2 code for the country in
- * which phoneNumber should be parsed relative to.
- * @param phoneNumber - The phone number, in national or international
- * format
- * @param clientSecret - A secret binary string generated by the client.
- * It is recommended this be around 16 ASCII characters.
- * @param sendAttempt - If an identity server sees a duplicate request
- * with the same sendAttempt, it will not send another SMS.
- * To request another SMS to be sent, use a larger value for
- * the sendAttempt param as was used in the previous request.
- * @param nextLink - Optional If specified, the client will be redirected
- * to this link after validation.
- * @param identityAccessToken - The `access_token` field of the Identity
- * Server `/account/register` response (see {@link registerWithIdentityServer}).
- *
- * @returns Promise which resolves to an object with a sid string
- * @returns Rejects: with an error response.
- * @throws Error if no identity server is set
- */
- public requestMsisdnToken(
- phoneCountry: string,
- phoneNumber: string,
- clientSecret: string,
- sendAttempt: number,
- nextLink?: string,
- identityAccessToken?: string,
- ): Promise<IRequestMsisdnTokenResponse> {
- const params: Record<string, string> = {
- client_secret: clientSecret,
- country: phoneCountry,
- phone_number: phoneNumber,
- send_attempt: sendAttempt?.toString(),
- };
- if (nextLink) {
- params.next_link = nextLink;
- }
-
- return this.http.idServerRequest<IRequestMsisdnTokenResponse>(
- Method.Post,
- "/validate/msisdn/requestToken",
- params,
- IdentityPrefix.V2,
- identityAccessToken,
- );
- }
-
- /**
- * Submits a MSISDN token to the identity server
- *
- * This is used when submitting the code sent by SMS to a phone number.
- * The identity server has an equivalent API for email but the js-sdk does
- * not expose this, since email is normally validated by the user clicking
- * a link rather than entering a code.
- *
- * @param sid - The sid given in the response to requestToken
- * @param clientSecret - A secret binary string generated by the client.
- * This must be the same value submitted in the requestToken call.
- * @param msisdnToken - The MSISDN token, as enetered by the user.
- * @param identityAccessToken - The `access_token` field of the Identity
- * Server `/account/register` response (see {@link registerWithIdentityServer}).
- *
- * @returns Promise which resolves: Object, currently with no parameters.
- * @returns Rejects: with an error response.
- * @throws Error if No identity server is set
- */
- public submitMsisdnToken(
- sid: string,
- clientSecret: string,
- msisdnToken: string,
- identityAccessToken: string,
- ): Promise<any> {
- // TODO: Types
- const params = {
- sid: sid,
- client_secret: clientSecret,
- token: msisdnToken,
- };
-
- return this.http.idServerRequest(
- Method.Post,
- "/validate/msisdn/submitToken",
- params,
- IdentityPrefix.V2,
- identityAccessToken,
- );
- }
-
- /**
- * Submits a MSISDN token to an arbitrary URL.
- *
- * This is used when submitting the code sent by SMS to a phone number in the
- * newer 3PID flow where the homeserver validates 3PID ownership (as part of
- * `requestAdd3pidMsisdnToken`). The homeserver response may include a
- * `submit_url` to specify where the token should be sent, and this helper can
- * be used to pass the token to this URL.
- *
- * @param url - The URL to submit the token to
- * @param sid - The sid given in the response to requestToken
- * @param clientSecret - A secret binary string generated by the client.
- * This must be the same value submitted in the requestToken call.
- * @param msisdnToken - The MSISDN token, as enetered by the user.
- *
- * @returns Promise which resolves: Object, currently with no parameters.
- * @returns Rejects: with an error response.
- */
- public submitMsisdnTokenOtherUrl(
- url: string,
- sid: string,
- clientSecret: string,
- msisdnToken: string,
- ): Promise<any> {
- // TODO: Types
- const params = {
- sid: sid,
- client_secret: clientSecret,
- token: msisdnToken,
- };
- return this.http.requestOtherUrl(Method.Post, url, params);
- }
-
- /**
- * Gets the V2 hashing information from the identity server. Primarily useful for
- * lookups.
- * @param identityAccessToken - The access token for the identity server.
- * @returns The hashing information for the identity server.
- */
- public getIdentityHashDetails(identityAccessToken: string): Promise<any> {
- // TODO: Types
- return this.http.idServerRequest(
- Method.Get,
- "/hash_details",
- undefined,
- IdentityPrefix.V2,
- identityAccessToken,
- );
- }
-
- /**
- * Performs a hashed lookup of addresses against the identity server. This is
- * only supported on identity servers which have at least the version 2 API.
- * @param addressPairs - An array of 2 element arrays.
- * The first element of each pair is the address, the second is the 3PID medium.
- * Eg: `["email@example.org", "email"]`
- * @param identityAccessToken - The access token for the identity server.
- * @returns A collection of address mappings to
- * found MXIDs. Results where no user could be found will not be listed.
- */
- public async identityHashedLookup(
- addressPairs: [string, string][],
- identityAccessToken: string,
- ): Promise<{ address: string; mxid: string }[]> {
- const params: Record<string, string | string[]> = {
- // addresses: ["email@example.org", "10005550000"],
- // algorithm: "sha256",
- // pepper: "abc123"
- };
-
- // Get hash information first before trying to do a lookup
- const hashes = await this.getIdentityHashDetails(identityAccessToken);
- if (!hashes || !hashes["lookup_pepper"] || !hashes["algorithms"]) {
- throw new Error("Unsupported identity server: bad response");
- }
-
- params["pepper"] = hashes["lookup_pepper"];
-
- const localMapping: Record<string, string> = {
- // hashed identifier => plain text address
- // For use in this function's return format
- };
-
- // When picking an algorithm, we pick the hashed over no hashes
- if (hashes["algorithms"].includes("sha256")) {
- // Abuse the olm hashing
- const olmutil = new global.Olm.Utility();
- params["addresses"] = addressPairs.map((p) => {
- const addr = p[0].toLowerCase(); // lowercase to get consistent hashes
- const med = p[1].toLowerCase();
- const hashed = olmutil
- .sha256(`${addr} ${med} ${params["pepper"]}`)
- .replace(/\+/g, "-")
- .replace(/\//g, "_"); // URL-safe base64
- // Map the hash to a known (case-sensitive) address. We use the case
- // sensitive version because the caller might be expecting that.
- localMapping[hashed] = p[0];
- return hashed;
- });
- params["algorithm"] = "sha256";
- } else if (hashes["algorithms"].includes("none")) {
- params["addresses"] = addressPairs.map((p) => {
- const addr = p[0].toLowerCase(); // lowercase to get consistent hashes
- const med = p[1].toLowerCase();
- const unhashed = `${addr} ${med}`;
- // Map the unhashed values to a known (case-sensitive) address. We use
- // the case-sensitive version because the caller might be expecting that.
- localMapping[unhashed] = p[0];
- return unhashed;
- });
- params["algorithm"] = "none";
- } else {
- throw new Error("Unsupported identity server: unknown hash algorithm");
- }
-
- const response = await this.http.idServerRequest<{
- mappings: { [address: string]: string };
- }>(Method.Post, "/lookup", params, IdentityPrefix.V2, identityAccessToken);
-
- if (!response?.["mappings"]) return []; // no results
-
- const foundAddresses: { address: string; mxid: string }[] = [];
- for (const hashed of Object.keys(response["mappings"])) {
- const mxid = response["mappings"][hashed];
- const plainAddress = localMapping[hashed];
- if (!plainAddress) {
- throw new Error("Identity server returned more results than expected");
- }
-
- foundAddresses.push({ address: plainAddress, mxid });
- }
- return foundAddresses;
- }
-
- /**
- * Looks up the public Matrix ID mapping for a given 3rd party
- * identifier from the identity server
- *
- * @param medium - The medium of the threepid, eg. 'email'
- * @param address - The textual address of the threepid
- * @param identityAccessToken - The `access_token` field of the Identity
- * Server `/account/register` response (see {@link registerWithIdentityServer}).
- *
- * @returns Promise which resolves: A threepid mapping
- * object or the empty object if no mapping
- * exists
- * @returns Rejects: with an error response.
- */
- public async lookupThreePid(medium: string, address: string, identityAccessToken: string): Promise<any> {
- // TODO: Types
- // Note: we're using the V2 API by calling this function, but our
- // function contract requires a V1 response. We therefore have to
- // convert it manually.
- const response = await this.identityHashedLookup([[address, medium]], identityAccessToken);
- const result = response.find((p) => p.address === address);
- if (!result) {
- return {};
- }
-
- const mapping = {
- address,
- medium,
- mxid: result.mxid,
-
- // We can't reasonably fill these parameters:
- // not_before
- // not_after
- // ts
- // signatures
- };
-
- return mapping;
- }
-
- /**
- * Looks up the public Matrix ID mappings for multiple 3PIDs.
- *
- * @param query - Array of arrays containing
- * [medium, address]
- * @param identityAccessToken - The `access_token` field of the Identity
- * Server `/account/register` response (see {@link registerWithIdentityServer}).
- *
- * @returns Promise which resolves: Lookup results from IS.
- * @returns Rejects: with an error response.
- */
- public async bulkLookupThreePids(query: [string, string][], identityAccessToken: string): Promise<any> {
- // TODO: Types
- // Note: we're using the V2 API by calling this function, but our
- // function contract requires a V1 response. We therefore have to
- // convert it manually.
- const response = await this.identityHashedLookup(
- // We have to reverse the query order to get [address, medium] pairs
- query.map((p) => [p[1], p[0]]),
- identityAccessToken,
- );
-
- const v1results: [medium: string, address: string, mxid: string][] = [];
- for (const mapping of response) {
- const originalQuery = query.find((p) => p[1] === mapping.address);
- if (!originalQuery) {
- throw new Error("Identity sever returned unexpected results");
- }
-
- v1results.push([
- originalQuery[0], // medium
- mapping.address,
- mapping.mxid,
- ]);
- }
-
- return { threepids: v1results };
- }
-
- /**
- * Get account info from the identity server. This is useful as a neutral check
- * to verify that other APIs are likely to approve access by testing that the
- * token is valid, terms have been agreed, etc.
- *
- * @param identityAccessToken - The `access_token` field of the Identity
- * Server `/account/register` response (see {@link registerWithIdentityServer}).
- *
- * @returns Promise which resolves: an object with account info.
- * @returns Rejects: with an error response.
- */
- public getIdentityAccount(identityAccessToken: string): Promise<any> {
- // TODO: Types
- return this.http.idServerRequest(Method.Get, "/account", undefined, IdentityPrefix.V2, identityAccessToken);
- }
-
- /**
- * Send an event to a specific list of devices.
- * This is a low-level API that simply wraps the HTTP API
- * call to send to-device messages. We recommend using
- * queueToDevice() which is a higher level API.
- *
- * @param eventType - type of event to send
- * content to send. Map from user_id to device_id to content object.
- * @param txnId - transaction id. One will be made up if not
- * supplied.
- * @returns Promise which resolves: to an empty object `{}`
- */
- public sendToDevice(eventType: string, contentMap: SendToDeviceContentMap, txnId?: string): Promise<{}> {
- const path = utils.encodeUri("/sendToDevice/$eventType/$txnId", {
- $eventType: eventType,
- $txnId: txnId ? txnId : this.makeTxnId(),
- });
-
- const body = {
- messages: utils.recursiveMapToObject(contentMap),
- };
-
- const targets = new Map<string, string[]>();
-
- for (const [userId, deviceMessages] of contentMap) {
- targets.set(userId, Array.from(deviceMessages.keys()));
- }
-
- logger.log(`PUT ${path}`, targets);
-
- return this.http.authedRequest(Method.Put, path, undefined, body);
- }
-
- /**
- * Sends events directly to specific devices using Matrix's to-device
- * messaging system. The batch will be split up into appropriately sized
- * batches for sending and stored in the store so they can be retried
- * later if they fail to send. Retries will happen automatically.
- * @param batch - The to-device messages to send
- */
- public queueToDevice(batch: ToDeviceBatch): Promise<void> {
- return this.toDeviceMessageQueue.queueBatch(batch);
- }
-
- /**
- * Get the third party protocols that can be reached using
- * this HS
- * @returns Promise which resolves to the result object
- */
- public getThirdpartyProtocols(): Promise<{ [protocol: string]: IProtocol }> {
- return this.http
- .authedRequest<Record<string, IProtocol>>(Method.Get, "/thirdparty/protocols")
- .then((response) => {
- // sanity check
- if (!response || typeof response !== "object") {
- throw new Error(`/thirdparty/protocols did not return an object: ${response}`);
- }
- return response;
- });
- }
-
- /**
- * Get information on how a specific place on a third party protocol
- * may be reached.
- * @param protocol - The protocol given in getThirdpartyProtocols()
- * @param params - Protocol-specific parameters, as given in the
- * response to getThirdpartyProtocols()
- * @returns Promise which resolves to the result object
- */
- public getThirdpartyLocation(
- protocol: string,
- params: { searchFields?: string[] },
- ): Promise<IThirdPartyLocation[]> {
- const path = utils.encodeUri("/thirdparty/location/$protocol", {
- $protocol: protocol,
- });
-
- return this.http.authedRequest(Method.Get, path, params);
- }
-
- /**
- * Get information on how a specific user on a third party protocol
- * may be reached.
- * @param protocol - The protocol given in getThirdpartyProtocols()
- * @param params - Protocol-specific parameters, as given in the
- * response to getThirdpartyProtocols()
- * @returns Promise which resolves to the result object
- */
- public getThirdpartyUser(protocol: string, params: any): Promise<IThirdPartyUser[]> {
- // TODO: Types
- const path = utils.encodeUri("/thirdparty/user/$protocol", {
- $protocol: protocol,
- });
-
- return this.http.authedRequest(Method.Get, path, params);
- }
-
- public getTerms(serviceType: SERVICE_TYPES, baseUrl: string): Promise<any> {
- // TODO: Types
- const url = this.termsUrlForService(serviceType, baseUrl);
- return this.http.requestOtherUrl(Method.Get, url);
- }
-
- public agreeToTerms(
- serviceType: SERVICE_TYPES,
- baseUrl: string,
- accessToken: string,
- termsUrls: string[],
- ): Promise<{}> {
- const url = this.termsUrlForService(serviceType, baseUrl);
- const headers = {
- Authorization: "Bearer " + accessToken,
- };
- return this.http.requestOtherUrl(
- Method.Post,
- url,
- {
- user_accepts: termsUrls,
- },
- { headers },
- );
- }
-
- /**
- * Reports an event as inappropriate to the server, which may then notify the appropriate people.
- * @param roomId - The room in which the event being reported is located.
- * @param eventId - The event to report.
- * @param score - The score to rate this content as where -100 is most offensive and 0 is inoffensive.
- * @param reason - The reason the content is being reported. May be blank.
- * @returns Promise which resolves to an empty object if successful
- */
- public reportEvent(roomId: string, eventId: string, score: number, reason: string): Promise<{}> {
- const path = utils.encodeUri("/rooms/$roomId/report/$eventId", {
- $roomId: roomId,
- $eventId: eventId,
- });
-
- return this.http.authedRequest(Method.Post, path, undefined, { score, reason });
- }
-
- /**
- * Fetches or paginates a room hierarchy as defined by MSC2946.
- * Falls back gracefully to sourcing its data from `getSpaceSummary` if this API is not yet supported by the server.
- * @param roomId - The ID of the space-room to use as the root of the summary.
- * @param limit - The maximum number of rooms to return per page.
- * @param maxDepth - The maximum depth in the tree from the root room to return.
- * @param suggestedOnly - Whether to only return rooms with suggested=true.
- * @param fromToken - The opaque token to paginate a previous request.
- * @returns the response, with next_batch & rooms fields.
- */
- public getRoomHierarchy(
- roomId: string,
- limit?: number,
- maxDepth?: number,
- suggestedOnly = false,
- fromToken?: string,
- ): Promise<IRoomHierarchy> {
- const path = utils.encodeUri("/rooms/$roomId/hierarchy", {
- $roomId: roomId,
- });
-
- const queryParams: QueryDict = {
- suggested_only: String(suggestedOnly),
- max_depth: maxDepth?.toString(),
- from: fromToken,
- limit: limit?.toString(),
- };
-
- return this.http
- .authedRequest<IRoomHierarchy>(Method.Get, path, queryParams, undefined, {
- prefix: ClientPrefix.V1,
- })
- .catch((e) => {
- if (e.errcode === "M_UNRECOGNIZED") {
- // fall back to the prefixed hierarchy API.
- return this.http.authedRequest<IRoomHierarchy>(Method.Get, path, queryParams, undefined, {
- prefix: "/_matrix/client/unstable/org.matrix.msc2946",
- });
- }
-
- throw e;
- });
- }
-
- /**
- * Creates a new file tree space with the given name. The client will pick
- * defaults for how it expects to be able to support the remaining API offered
- * by the returned class.
- *
- * Note that this is UNSTABLE and may have breaking changes without notice.
- * @param name - The name of the tree space.
- * @returns Promise which resolves to the created space.
- */
- public async unstableCreateFileTree(name: string): Promise<MSC3089TreeSpace> {
- const { room_id: roomId } = await this.createRoom({
- name: name,
- preset: Preset.PrivateChat,
- power_level_content_override: {
- ...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
- users: {
- [this.getUserId()!]: 100,
- },
- },
- creation_content: {
- [RoomCreateTypeField]: RoomType.Space,
- },
- initial_state: [
- {
- type: UNSTABLE_MSC3088_PURPOSE.name,
- state_key: UNSTABLE_MSC3089_TREE_SUBTYPE.name,
- content: {
- [UNSTABLE_MSC3088_ENABLED.name]: true,
- },
- },
- {
- type: EventType.RoomEncryption,
- state_key: "",
- content: {
- algorithm: olmlib.MEGOLM_ALGORITHM,
- },
- },
- ],
- });
- return new MSC3089TreeSpace(this, roomId);
- }
-
- /**
- * Gets a reference to a tree space, if the room ID given is a tree space. If the room
- * does not appear to be a tree space then null is returned.
- *
- * Note that this is UNSTABLE and may have breaking changes without notice.
- * @param roomId - The room ID to get a tree space reference for.
- * @returns The tree space, or null if not a tree space.
- */
- public unstableGetFileTreeSpace(roomId: string): MSC3089TreeSpace | null {
- const room = this.getRoom(roomId);
- if (room?.getMyMembership() !== "join") return null;
-
- const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, "");
- const purposeEvent = room.currentState.getStateEvents(
- UNSTABLE_MSC3088_PURPOSE.name,
- UNSTABLE_MSC3089_TREE_SUBTYPE.name,
- );
-
- if (!createEvent) throw new Error("Expected single room create event");
-
- if (!purposeEvent?.getContent()?.[UNSTABLE_MSC3088_ENABLED.name]) return null;
- if (createEvent.getContent()?.[RoomCreateTypeField] !== RoomType.Space) return null;
-
- return new MSC3089TreeSpace(this, roomId);
- }
-
- /**
- * Perform a single MSC3575 sliding sync request.
- * @param req - The request to make.
- * @param proxyBaseUrl - The base URL for the sliding sync proxy.
- * @param abortSignal - Optional signal to abort request mid-flight.
- * @returns The sliding sync response, or a standard error.
- * @throws on non 2xx status codes with an object with a field "httpStatus":number.
- */
- public slidingSync(
- req: MSC3575SlidingSyncRequest,
- proxyBaseUrl?: string,
- abortSignal?: AbortSignal,
- ): Promise<MSC3575SlidingSyncResponse> {
- const qps: Record<string, any> = {};
- if (req.pos) {
- qps.pos = req.pos;
- delete req.pos;
- }
- if (req.timeout) {
- qps.timeout = req.timeout;
- delete req.timeout;
- }
- const clientTimeout = req.clientTimeout;
- delete req.clientTimeout;
- return this.http.authedRequest<MSC3575SlidingSyncResponse>(Method.Post, "/sync", qps, req, {
- prefix: "/_matrix/client/unstable/org.matrix.msc3575",
- baseUrl: proxyBaseUrl,
- localTimeoutMs: clientTimeout,
- abortSignal,
- });
- }
-
- /**
- * @deprecated use supportsThreads() instead
- */
- public supportsExperimentalThreads(): boolean {
- logger.warn(`supportsExperimentalThreads() is deprecated, use supportThreads() instead`);
- return this.clientOpts?.experimentalThreadSupport || false;
- }
-
- /**
- * A helper to determine thread support
- * @returns a boolean to determine if threads are enabled
- */
- public supportsThreads(): boolean {
- return this.clientOpts?.threadSupport || false;
- }
-
- /**
- * A helper to determine intentional mentions support
- * @returns a boolean to determine if intentional mentions are enabled
- * @experimental
- */
- public supportsIntentionalMentions(): boolean {
- return this.clientOpts?.intentionalMentions || false;
- }
-
- /**
- * Fetches the summary of a room as defined by an initial version of MSC3266 and implemented in Synapse
- * Proposed at https://github.com/matrix-org/matrix-doc/pull/3266
- * @param roomIdOrAlias - The ID or alias of the room to get the summary of.
- * @param via - The list of servers which know about the room if only an ID was provided.
- */
- public async getRoomSummary(roomIdOrAlias: string, via?: string[]): Promise<IRoomSummary> {
- const path = utils.encodeUri("/rooms/$roomid/summary", { $roomid: roomIdOrAlias });
- return this.http.authedRequest(Method.Get, path, { via }, undefined, {
- prefix: "/_matrix/client/unstable/im.nheko.summary",
- });
- }
-
- /**
- * Processes a list of threaded events and adds them to their respective timelines
- * @param room - the room the adds the threaded events
- * @param threadedEvents - an array of the threaded events
- * @param toStartOfTimeline - the direction in which we want to add the events
- */
- public processThreadEvents(room: Room, threadedEvents: MatrixEvent[], toStartOfTimeline: boolean): void {
- room.processThreadedEvents(threadedEvents, toStartOfTimeline);
- }
-
- /**
- * Processes a list of thread roots and creates a thread model
- * @param room - the room to create the threads in
- * @param threadedEvents - an array of thread roots
- * @param toStartOfTimeline - the direction
- */
- public processThreadRoots(room: Room, threadedEvents: MatrixEvent[], toStartOfTimeline: boolean): void {
- room.processThreadRoots(threadedEvents, toStartOfTimeline);
- }
-
- public processBeaconEvents(room?: Room, events?: MatrixEvent[]): void {
- this.processAggregatedTimelineEvents(room, events);
- }
-
- /**
- * Calls aggregation functions for event types that are aggregated
- * Polls and location beacons
- * @param room - room the events belong to
- * @param events - timeline events to be processed
- * @returns
- */
- public processAggregatedTimelineEvents(room?: Room, events?: MatrixEvent[]): void {
- if (!events?.length) return;
- if (!room) return;
-
- room.currentState.processBeaconEvents(events, this);
- room.processPollEvents(events);
- }
-
- /**
- * Fetches information about the user for the configured access token.
- */
- public async whoami(): Promise<IWhoamiResponse> {
- return this.http.authedRequest(Method.Get, "/account/whoami");
- }
-
- /**
- * Find the event_id closest to the given timestamp in the given direction.
- * @returns Resolves: A promise of an object containing the event_id and
- * origin_server_ts of the closest event to the timestamp in the given direction
- * @returns Rejects: when the request fails (module:http-api.MatrixError)
- */
- public async timestampToEvent(
- roomId: string,
- timestamp: number,
- dir: Direction,
- ): Promise<TimestampToEventResponse> {
- const path = utils.encodeUri("/rooms/$roomId/timestamp_to_event", {
- $roomId: roomId,
- });
- const queryParams = {
- ts: timestamp.toString(),
- dir: dir,
- };
-
- try {
- return await this.http.authedRequest(Method.Get, path, queryParams, undefined, {
- prefix: ClientPrefix.V1,
- });
- } catch (err) {
- // Fallback to the prefixed unstable endpoint. Since the stable endpoint is
- // new, we should also try the unstable endpoint before giving up. We can
- // remove this fallback request in a year (remove after 2023-11-28).
- if (
- (<MatrixError>err).errcode === "M_UNRECOGNIZED" &&
- // XXX: The 400 status code check should be removed in the future
- // when Synapse is compliant with MSC3743.
- ((<MatrixError>err).httpStatus === 400 ||
- // This the correct standard status code for an unsupported
- // endpoint according to MSC3743. Not Found and Method Not Allowed
- // both indicate that this endpoint+verb combination is
- // not supported.
- (<MatrixError>err).httpStatus === 404 ||
- (<MatrixError>err).httpStatus === 405)
- ) {
- return await this.http.authedRequest(Method.Get, path, queryParams, undefined, {
- prefix: "/_matrix/client/unstable/org.matrix.msc3030",
- });
- }
-
- throw err;
- }
- }
-}
-
-/**
- * recalculates an accurate notifications count on event decryption.
- * Servers do not have enough knowledge about encrypted events to calculate an
- * accurate notification_count
- */
-export function fixNotificationCountOnDecryption(cli: MatrixClient, event: MatrixEvent): void {
- const ourUserId = cli.getUserId();
- const eventId = event.getId();
-
- const room = cli.getRoom(event.getRoomId());
- if (!room || !ourUserId || !eventId) return;
-
- const oldActions = event.getPushActions();
- const actions = cli.getPushActionsForEvent(event, true);
-
- const isThreadEvent = !!event.threadRootId && !event.isThreadRoot;
-
- const currentHighlightCount = room.getUnreadCountForEventContext(NotificationCountType.Highlight, event);
-
- // Ensure the unread counts are kept up to date if the event is encrypted
- // We also want to make sure that the notification count goes up if we already
- // have encrypted events to avoid other code from resetting 'highlight' to zero.
- const oldHighlight = !!oldActions?.tweaks?.highlight;
- const newHighlight = !!actions?.tweaks?.highlight;
-
- let hasReadEvent;
- if (isThreadEvent) {
- const thread = room.getThread(event.threadRootId);
- hasReadEvent = thread
- ? thread.hasUserReadEvent(ourUserId, eventId)
- : // If the thread object does not exist in the room yet, we don't
- // want to calculate notification for this event yet. We have not
- // restored the read receipts yet and can't accurately calculate
- // notifications at this stage.
- //
- // This issue can likely go away when MSC3874 is implemented
- true;
- } else {
- hasReadEvent = room.hasUserReadEvent(ourUserId, eventId);
- }
-
- if (hasReadEvent) {
- // If the event has been read, ignore it.
- return;
- }
-
- if (oldHighlight !== newHighlight || currentHighlightCount > 0) {
- // TODO: Handle mentions received while the client is offline
- // See also https://github.com/vector-im/element-web/issues/9069
- let newCount = currentHighlightCount;
- if (newHighlight && !oldHighlight) newCount++;
- if (!newHighlight && oldHighlight) newCount--;
-
- if (isThreadEvent) {
- room.setThreadUnreadNotificationCount(event.threadRootId, NotificationCountType.Highlight, newCount);
- } else {
- room.setUnreadNotificationCount(NotificationCountType.Highlight, newCount);
- }
- }
-
- // Total count is used to typically increment a room notification counter, but not loudly highlight it.
- const currentTotalCount = room.getUnreadCountForEventContext(NotificationCountType.Total, event);
-
- // `notify` is used in practice for incrementing the total count
- const newNotify = !!actions?.notify;
-
- // The room total count is NEVER incremented by the server for encrypted rooms. We basically ignore
- // the server here as it's always going to tell us to increment for encrypted events.
- if (newNotify) {
- if (isThreadEvent) {
- room.setThreadUnreadNotificationCount(
- event.threadRootId,
- NotificationCountType.Total,
- currentTotalCount + 1,
- );
- } else {
- room.setUnreadNotificationCount(NotificationCountType.Total, currentTotalCount + 1);
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/common-crypto/CryptoBackend.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/common-crypto/CryptoBackend.ts
deleted file mode 100644
index a0b4621..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/common-crypto/CryptoBackend.ts
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
-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 type { IToDeviceEvent } from "../sync-accumulator";
-import { MatrixEvent } from "../models/event";
-import { Room } from "../models/room";
-import { CryptoApi } from "../crypto-api";
-import { DeviceTrustLevel, UserTrustLevel } from "../crypto/CrossSigning";
-import { IEncryptedEventInfo } from "../crypto/api";
-import { IEventDecryptionResult } from "../@types/crypto";
-
-/**
- * Common interface for the crypto implementations
- */
-export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
- /**
- * Whether sendMessage in a room with unknown and unverified devices
- * should throw an error and not send the message. This has 'Global' for
- * symmetry with setGlobalBlacklistUnverifiedDevices but there is currently
- * no room-level equivalent for this setting.
- *
- * @remarks this is here, rather than in `CryptoApi`, because I don't think we're
- * going to support it in the rust crypto implementation.
- */
- globalErrorOnUnknownDevices: boolean;
-
- /**
- * Shut down any background processes related to crypto
- */
- stop(): void;
-
- /**
- * Get the verification level for a given user
- *
- * TODO: define this better
- *
- * @param userId - user to be checked
- */
- checkUserTrust(userId: string): UserTrustLevel;
-
- /**
- * Get the verification level for a given device
- *
- * TODO: define this better
- *
- * @param userId - user to be checked
- * @param deviceId - device to be checked
- */
- checkDeviceTrust(userId: string, deviceId: string): DeviceTrustLevel;
-
- /**
- * Encrypt an event according to the configuration of the room.
- *
- * @param event - event to be sent
- *
- * @param room - destination room.
- *
- * @returns Promise which resolves when the event has been
- * encrypted, or null if nothing was needed
- */
- encryptEvent(event: MatrixEvent, room: Room): Promise<void>;
-
- /**
- * Decrypt a received event
- *
- * @returns a promise which resolves once we have finished decrypting.
- * Rejects with an error if there is a problem decrypting the event.
- */
- decryptEvent(event: MatrixEvent): Promise<IEventDecryptionResult>;
-
- /**
- * Get information about the encryption of an event
- *
- * @param event - event to be checked
- */
- getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo;
-}
-
-/** The methods which crypto implementations should expose to the Sync api */
-export interface SyncCryptoCallbacks {
- /**
- * Called by the /sync loop whenever there are incoming to-device messages.
- *
- * The implementation may preprocess the received messages (eg, decrypt them) and return an
- * updated list of messages for dispatch to the rest of the system.
- *
- * Note that, unlike {@link ClientEvent.ToDeviceEvent} events, this is called on the raw to-device
- * messages, rather than the results of any decryption attempts.
- *
- * @param events - the received to-device messages
- * @returns A list of preprocessed to-device messages.
- */
- preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]>;
-
- /**
- * Called by the /sync loop whenever there are incoming to-device messages.
- *
- * The implementation may preprocess the received messages (eg, decrypt them) and return an
- * updated list of messages for dispatch to the rest of the system.
- *
- * Note that, unlike {@link ClientEvent.ToDeviceEvent} events, this is called on the raw to-device
- * messages, rather than the results of any decryption attempts.
- *
- * @param oneTimeKeysCounts - the received one time key counts
- * @returns A list of preprocessed to-device messages.
- */
- preprocessOneTimeKeyCounts(oneTimeKeysCounts: Map<string, number>): Promise<void>;
-
- /**
- * Called by the /sync loop whenever there are incoming to-device messages.
- *
- * The implementation may preprocess the received messages (eg, decrypt them) and return an
- * updated list of messages for dispatch to the rest of the system.
- *
- * Note that, unlike {@link ClientEvent.ToDeviceEvent} events, this is called on the raw to-device
- * messages, rather than the results of any decryption attempts.
- *
- * @param unusedFallbackKeys - the received unused fallback keys
- * @returns A list of preprocessed to-device messages.
- */
- preprocessUnusedFallbackKeys(unusedFallbackKeys: Set<string>): Promise<void>;
-
- /**
- * Called by the /sync loop whenever an m.room.encryption event is received.
- *
- * This is called before RoomStateEvents are emitted for any of the events in the /sync
- * response (even if the other events technically happened first). This works around a problem
- * if the client uses a RoomStateEvent (typically a membership event) as a trigger to send a message
- * in a new room (or one where encryption has been newly enabled): that would otherwise leave the
- * crypto layer confused because it expects crypto to be set up, but it has not yet been.
- *
- * @param room - in which the event was received
- * @param event - encryption event to be processed
- */
- onCryptoEvent(room: Room, event: MatrixEvent): Promise<void>;
-
- /**
- * Called by the /sync loop after each /sync response is processed.
- *
- * Used to complete batch processing, or to initiate background processes
- *
- * @param syncState - information about the completed sync.
- */
- onSyncCompleted(syncState: OnSyncCompletedData): void;
-}
-
-export interface OnSyncCompletedData {
- /**
- * The 'next_batch' result from /sync, which will become the 'since' token for the next call to /sync.
- */
- nextSyncToken?: string;
-
- /**
- * True if we are working our way through a backlog of events after connecting.
- */
- catchingUp?: boolean;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/common-crypto/README.md b/includes/external/matrix/node_modules/matrix-js-sdk/src/common-crypto/README.md
deleted file mode 100644
index 7af3298..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/common-crypto/README.md
+++ /dev/null
@@ -1,4 +0,0 @@
-This directory contains functionality which is common to both the legacy (libolm-based) crypto implementation,
-and the new rust-based implementation.
-
-It is an internal module, and is _not_ directly exposed to applications.
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/content-helpers.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/content-helpers.ts
deleted file mode 100644
index 6790886..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/content-helpers.ts
+++ /dev/null
@@ -1,288 +0,0 @@
-/*
-Copyright 2018 - 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 { MBeaconEventContent, MBeaconInfoContent, MBeaconInfoEventContent } from "./@types/beacon";
-import { MsgType } from "./@types/event";
-import { M_TEXT, REFERENCE_RELATION } from "./@types/extensible_events";
-import { isProvided } from "./extensible_events_v1/utilities";
-import {
- M_ASSET,
- LocationAssetType,
- M_LOCATION,
- M_TIMESTAMP,
- LocationEventWireContent,
- MLocationEventContent,
- MLocationContent,
- MAssetContent,
- LegacyLocationEventContent,
-} from "./@types/location";
-import { MRoomTopicEventContent, MTopicContent, M_TOPIC } from "./@types/topic";
-import { IContent } from "./models/event";
-
-/**
- * Generates the content for a HTML Message event
- * @param body - the plaintext body of the message
- * @param htmlBody - the HTML representation of the message
- * @returns
- */
-export function makeHtmlMessage(body: string, htmlBody: string): IContent {
- return {
- msgtype: MsgType.Text,
- format: "org.matrix.custom.html",
- body: body,
- formatted_body: htmlBody,
- };
-}
-
-/**
- * Generates the content for a HTML Notice event
- * @param body - the plaintext body of the notice
- * @param htmlBody - the HTML representation of the notice
- * @returns
- */
-export function makeHtmlNotice(body: string, htmlBody: string): IContent {
- return {
- msgtype: MsgType.Notice,
- format: "org.matrix.custom.html",
- body: body,
- formatted_body: htmlBody,
- };
-}
-
-/**
- * Generates the content for a HTML Emote event
- * @param body - the plaintext body of the emote
- * @param htmlBody - the HTML representation of the emote
- * @returns
- */
-export function makeHtmlEmote(body: string, htmlBody: string): IContent {
- return {
- msgtype: MsgType.Emote,
- format: "org.matrix.custom.html",
- body: body,
- formatted_body: htmlBody,
- };
-}
-
-/**
- * Generates the content for a Plaintext Message event
- * @param body - the plaintext body of the emote
- * @returns
- */
-export function makeTextMessage(body: string): IContent {
- return {
- msgtype: MsgType.Text,
- body: body,
- };
-}
-
-/**
- * Generates the content for a Plaintext Notice event
- * @param body - the plaintext body of the notice
- * @returns
- */
-export function makeNotice(body: string): IContent {
- return {
- msgtype: MsgType.Notice,
- body: body,
- };
-}
-
-/**
- * Generates the content for a Plaintext Emote event
- * @param body - the plaintext body of the emote
- * @returns
- */
-export function makeEmoteMessage(body: string): IContent {
- return {
- msgtype: MsgType.Emote,
- body: body,
- };
-}
-
-/** Location content helpers */
-
-export const getTextForLocationEvent = (
- uri: string | undefined,
- assetType: LocationAssetType,
- timestamp?: number,
- description?: string | null,
-): string => {
- const date = `at ${new Date(timestamp!).toISOString()}`;
- const assetName = assetType === LocationAssetType.Self ? "User" : undefined;
- const quotedDescription = description ? `"${description}"` : undefined;
-
- return [assetName, "Location", quotedDescription, uri, date].filter(Boolean).join(" ");
-};
-
-/**
- * Generates the content for a Location event
- * @param uri - a geo:// uri for the location
- * @param timestamp - the timestamp when the location was correct (milliseconds since the UNIX epoch)
- * @param description - the (optional) label for this location on the map
- * @param assetType - the (optional) asset type of this location e.g. "m.self"
- * @param text - optional. A text for the location
- */
-export const makeLocationContent = (
- // this is first but optional
- // to avoid a breaking change
- text?: string,
- uri?: string,
- timestamp?: number,
- description?: string | null,
- assetType?: LocationAssetType,
-): LegacyLocationEventContent & MLocationEventContent => {
- const defaultedText =
- text ?? getTextForLocationEvent(uri, assetType || LocationAssetType.Self, timestamp, description);
- const timestampEvent = timestamp ? { [M_TIMESTAMP.name]: timestamp } : {};
- return {
- msgtype: MsgType.Location,
- body: defaultedText,
- geo_uri: uri,
- [M_LOCATION.name]: {
- description,
- uri,
- },
- [M_ASSET.name]: {
- type: assetType || LocationAssetType.Self,
- },
- [M_TEXT.name]: defaultedText,
- ...timestampEvent,
- } as LegacyLocationEventContent & MLocationEventContent;
-};
-
-/**
- * Parse location event content and transform to
- * a backwards compatible modern m.location event format
- */
-export const parseLocationEvent = (wireEventContent: LocationEventWireContent): MLocationEventContent => {
- const location = M_LOCATION.findIn<MLocationContent>(wireEventContent);
- const asset = M_ASSET.findIn<MAssetContent>(wireEventContent);
- const timestamp = M_TIMESTAMP.findIn<number>(wireEventContent);
- const text = M_TEXT.findIn<string>(wireEventContent);
-
- const geoUri = location?.uri ?? wireEventContent?.geo_uri;
- const description = location?.description;
- const assetType = asset?.type ?? LocationAssetType.Self;
- const fallbackText = text ?? wireEventContent.body;
-
- return makeLocationContent(fallbackText, geoUri, timestamp ?? undefined, 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);
- if (!Array.isArray(mtopic)) {
- return { text: content.topic };
- }
- 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
- */
-export type MakeBeaconInfoContent = (
- timeout: number,
- isLive?: boolean,
- description?: string,
- assetType?: LocationAssetType,
- timestamp?: number,
-) => MBeaconInfoEventContent;
-
-export const makeBeaconInfoContent: MakeBeaconInfoContent = (timeout, isLive, description, assetType, timestamp) => ({
- description,
- timeout,
- live: isLive,
- [M_TIMESTAMP.name]: timestamp || Date.now(),
- [M_ASSET.name]: {
- type: assetType ?? LocationAssetType.Self,
- },
-});
-
-export type BeaconInfoState = MBeaconInfoContent & {
- assetType?: LocationAssetType;
- timestamp?: number;
-};
-/**
- * Flatten beacon info event content
- */
-export const parseBeaconInfoContent = (content: MBeaconInfoEventContent): BeaconInfoState => {
- const { description, timeout, live } = content;
- const timestamp = M_TIMESTAMP.findIn<number>(content) ?? undefined;
- const asset = M_ASSET.findIn<MAssetContent>(content);
-
- return {
- description,
- timeout,
- live,
- assetType: asset?.type,
- timestamp,
- };
-};
-
-export type MakeBeaconContent = (
- uri: string,
- timestamp: number,
- beaconInfoEventId: string,
- description?: string,
-) => MBeaconEventContent;
-
-export const makeBeaconContent: MakeBeaconContent = (uri, timestamp, beaconInfoEventId, description) => ({
- [M_LOCATION.name]: {
- description,
- uri,
- },
- [M_TIMESTAMP.name]: timestamp,
- "m.relates_to": {
- rel_type: REFERENCE_RELATION.name,
- event_id: beaconInfoEventId,
- },
-});
-
-export type BeaconLocationState = Omit<MLocationContent, "uri"> & {
- uri?: string; // override from MLocationContent to allow optionals
- timestamp?: number;
-};
-
-export const parseBeaconContent = (content: MBeaconEventContent): BeaconLocationState => {
- const location = M_LOCATION.findIn<MLocationContent>(content);
- const timestamp = M_TIMESTAMP.findIn<number>(content) ?? undefined;
-
- return {
- description: location?.description,
- uri: location?.uri,
- timestamp,
- };
-};
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/content-repo.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/content-repo.ts
deleted file mode 100644
index 2575412..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/content-repo.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
-Copyright 2015 - 2021 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 "./utils";
-
-/**
- * Get the HTTP URL for an MXC URI.
- * @param baseUrl - The base homeserver url which has a content repo.
- * @param mxc - The mxc:// URI.
- * @param width - The desired width of the thumbnail.
- * @param height - The desired height of the thumbnail.
- * @param resizeMethod - The thumbnail resize method to use, either
- * "crop" or "scale".
- * @param allowDirectLinks - If true, return any non-mxc URLs
- * directly. Fetching such URLs will leak information about the user to
- * anyone they share a room with. If false, will return the emptry string
- * for such URLs.
- * @returns The complete URL to the content.
- */
-export function getHttpUriForMxc(
- baseUrl: string,
- mxc?: string,
- width?: number,
- height?: number,
- resizeMethod?: string,
- allowDirectLinks = false,
-): string {
- if (typeof mxc !== "string" || !mxc) {
- return "";
- }
- if (mxc.indexOf("mxc://") !== 0) {
- if (allowDirectLinks) {
- return mxc;
- } else {
- return "";
- }
- }
- let serverAndMediaId = mxc.slice(6); // strips mxc://
- let prefix = "/_matrix/media/r0/download/";
- const params: Record<string, string> = {};
-
- if (width) {
- params["width"] = Math.round(width).toString();
- }
- if (height) {
- params["height"] = Math.round(height).toString();
- }
- if (resizeMethod) {
- params["method"] = resizeMethod;
- }
- if (Object.keys(params).length > 0) {
- // these are thumbnailing params so they probably want the
- // thumbnailing API...
- prefix = "/_matrix/media/r0/thumbnail/";
- }
-
- const fragmentOffset = serverAndMediaId.indexOf("#");
- let fragment = "";
- if (fragmentOffset >= 0) {
- fragment = serverAndMediaId.slice(fragmentOffset);
- serverAndMediaId = serverAndMediaId.slice(0, fragmentOffset);
- }
-
- const urlParams = Object.keys(params).length === 0 ? "" : "?" + utils.encodeParams(params);
- return baseUrl + prefix + serverAndMediaId + urlParams + fragment;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto-api.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto-api.ts
deleted file mode 100644
index 50617c9..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto-api.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
-Copyright 2023 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 type { IMegolmSessionData } from "./@types/crypto";
-import { Room } from "./models/room";
-
-/**
- * Public interface to the cryptography parts of the js-sdk
- *
- * @remarks Currently, this is a work-in-progress. In time, more methods will be added here.
- */
-export interface CryptoApi {
- /**
- * Global override for whether the client should ever send encrypted
- * messages to unverified devices. This provides the default for rooms which
- * do not specify a value.
- *
- * If true, all unverified devices will be blacklisted by default
- */
- globalBlacklistUnverifiedDevices: boolean;
-
- /**
- * Checks if the user has previously published cross-signing keys
- *
- * This means downloading the devicelist for the user and checking if the list includes
- * the cross-signing pseudo-device.
- *
- * @returns true if the user has previously published cross-signing keys
- */
- userHasCrossSigningKeys(): Promise<boolean>;
-
- /**
- * Perform any background tasks that can be done before a message is ready to
- * send, in order to speed up sending of the message.
- *
- * @param room - the room the event is in
- */
- prepareToEncrypt(room: Room): void;
-
- /**
- * Discard any existing megolm session for the given room.
- *
- * This will ensure that a new session is created on the next call to {@link prepareToEncrypt},
- * or the next time a message is sent.
- *
- * This should not normally be necessary: it should only be used as a debugging tool if there has been a
- * problem with encryption.
- *
- * @param roomId - the room to discard sessions for
- */
- forceDiscardSession(roomId: string): Promise<void>;
-
- /**
- * Get a list containing all of the room keys
- *
- * This should be encrypted before returning it to the user.
- *
- * @returns a promise which resolves to a list of
- * session export objects
- */
- exportRoomKeys(): Promise<IMegolmSessionData[]>;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/CrossSigning.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/CrossSigning.ts
deleted file mode 100644
index 31ed2d4..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/CrossSigning.ts
+++ /dev/null
@@ -1,803 +0,0 @@
-/*
-Copyright 2019 - 2021 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.
-*/
-
-/**
- * Cross signing methods
- */
-
-import { PkSigning } from "@matrix-org/olm";
-
-import { decodeBase64, encodeBase64, IObject, pkSign, pkVerify } from "./olmlib";
-import { logger } from "../logger";
-import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store";
-import { decryptAES, encryptAES } from "./aes";
-import { DeviceInfo } from "./deviceinfo";
-import { SecretStorage } from "./SecretStorage";
-import { ICrossSigningKey, ISignedKey, MatrixClient } from "../client";
-import { OlmDevice } from "./OlmDevice";
-import { ICryptoCallbacks } from ".";
-import { ISignatures } from "../@types/signed";
-import { CryptoStore, SecretStorePrivateKeys } from "./store/base";
-import { SecretStorageKeyDescription } from "../secret-storage";
-
-const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;
-
-function publicKeyFromKeyInfo(keyInfo: ICrossSigningKey): string {
- // `keys` is an object with { [`ed25519:${pubKey}`]: pubKey }
- // We assume only a single key, and we want the bare form without type
- // prefix, so we select the values.
- return Object.values(keyInfo.keys)[0];
-}
-
-export interface ICacheCallbacks {
- getCrossSigningKeyCache?(type: string, expectedPublicKey?: string): Promise<Uint8Array | null>;
- storeCrossSigningKeyCache?(type: string, key?: Uint8Array): Promise<void>;
-}
-
-export interface ICrossSigningInfo {
- keys: Record<string, ICrossSigningKey>;
- firstUse: boolean;
- crossSigningVerifiedBefore: boolean;
-}
-
-export class CrossSigningInfo {
- public keys: Record<string, ICrossSigningKey> = {};
- public firstUse = true;
- // This tracks whether we've ever verified this user with any identity.
- // When you verify a user, any devices online at the time that receive
- // the verifying signature via the homeserver will latch this to true
- // and can use it in the future to detect cases where the user has
- // become unverified later for any reason.
- private crossSigningVerifiedBefore = false;
-
- /**
- * Information about a user's cross-signing keys
- *
- * @param userId - the user that the information is about
- * @param callbacks - Callbacks used to interact with the app
- * Requires getCrossSigningKey and saveCrossSigningKeys
- * @param cacheCallbacks - Callbacks used to interact with the cache
- */
- public constructor(
- public readonly userId: string,
- private callbacks: ICryptoCallbacks = {},
- private cacheCallbacks: ICacheCallbacks = {},
- ) {}
-
- public static fromStorage(obj: ICrossSigningInfo, userId: string): CrossSigningInfo {
- const res = new CrossSigningInfo(userId);
- for (const prop in obj) {
- if (obj.hasOwnProperty(prop)) {
- // @ts-ignore - ts doesn't like this and nor should we
- res[prop] = obj[prop];
- }
- }
- return res;
- }
-
- public toStorage(): ICrossSigningInfo {
- return {
- keys: this.keys,
- firstUse: this.firstUse,
- crossSigningVerifiedBefore: this.crossSigningVerifiedBefore,
- };
- }
-
- /**
- * Calls the app callback to ask for a private key
- *
- * @param type - The key type ("master", "self_signing", or "user_signing")
- * @param expectedPubkey - The matching public key or undefined to use
- * the stored public key for the given key type.
- * @returns An array with [ public key, Olm.PkSigning ]
- */
- public async getCrossSigningKey(type: string, expectedPubkey?: string): Promise<[string, PkSigning]> {
- const shouldCache = ["master", "self_signing", "user_signing"].indexOf(type) >= 0;
-
- if (!this.callbacks.getCrossSigningKey) {
- throw new Error("No getCrossSigningKey callback supplied");
- }
-
- if (expectedPubkey === undefined) {
- expectedPubkey = this.getId(type)!;
- }
-
- function validateKey(key: Uint8Array | null): [string, PkSigning] | undefined {
- if (!key) return;
- const signing = new global.Olm.PkSigning();
- const gotPubkey = signing.init_with_seed(key);
- if (gotPubkey === expectedPubkey) {
- return [gotPubkey, signing];
- }
- signing.free();
- }
-
- let privkey: Uint8Array | null = null;
- if (this.cacheCallbacks.getCrossSigningKeyCache && shouldCache) {
- privkey = await this.cacheCallbacks.getCrossSigningKeyCache(type, expectedPubkey);
- }
-
- const cacheresult = validateKey(privkey);
- if (cacheresult) {
- return cacheresult;
- }
-
- privkey = await this.callbacks.getCrossSigningKey(type, expectedPubkey);
- const result = validateKey(privkey);
- if (result) {
- if (this.cacheCallbacks.storeCrossSigningKeyCache && shouldCache) {
- await this.cacheCallbacks.storeCrossSigningKeyCache(type, privkey!);
- }
- return result;
- }
-
- /* No keysource even returned a key */
- if (!privkey) {
- throw new Error("getCrossSigningKey callback for " + type + " returned falsey");
- }
-
- /* We got some keys from the keysource, but none of them were valid */
- throw new Error("Key type " + type + " from getCrossSigningKey callback did not match");
- }
-
- /**
- * Check whether the private keys exist in secret storage.
- * XXX: This could be static, be we often seem to have an instance when we
- * want to know this anyway...
- *
- * @param secretStorage - The secret store using account data
- * @returns 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 isStoredInSecretStorage(
- secretStorage: SecretStorage<MatrixClient | undefined>,
- ): Promise<Record<string, object> | null> {
- // check what SSSS keys have encrypted the master key (if any)
- 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, SecretStorageKeyDescription>): void {
- for (const k of Object.keys(stored)) {
- if (!s[k]) {
- delete stored[k];
- }
- }
- }
- for (const type of ["self_signing", "user_signing"]) {
- intersect((await secretStorage.isStored(`m.cross_signing.${type}`)) || {});
- }
- return Object.keys(stored).length ? stored : null;
- }
-
- /**
- * Store private keys in secret storage for use by other devices. This is
- * typically called in conjunction with the creation of new cross-signing
- * keys.
- *
- * @param keys - The keys to store
- * @param secretStorage - The secret store using account data
- */
- public static async storeInSecretStorage(
- keys: Map<string, Uint8Array>,
- secretStorage: SecretStorage<undefined>,
- ): Promise<void> {
- for (const [type, privateKey] of keys) {
- const encodedKey = encodeBase64(privateKey);
- await secretStorage.store(`m.cross_signing.${type}`, encodedKey);
- }
- }
-
- /**
- * Get private keys from secret storage created by some other device. This
- * also passes the private keys to the app-specific callback.
- *
- * @param type - The type of key to get. One of "master",
- * "self_signing", or "user_signing".
- * @param secretStorage - The secret store using account data
- * @returns The private key
- */
- public static async getFromSecretStorage(type: string, secretStorage: SecretStorage): Promise<Uint8Array | null> {
- const encodedKey = await secretStorage.get(`m.cross_signing.${type}`);
- if (!encodedKey) {
- return null;
- }
- return decodeBase64(encodedKey);
- }
-
- /**
- * Check whether the private keys exist in the local key cache.
- *
- * @param type - The type of key to get. One of "master",
- * "self_signing", or "user_signing". Optional, will check all by default.
- * @returns True if all keys are stored in the local cache.
- */
- public async isStoredInKeyCache(type?: string): Promise<boolean> {
- const cacheCallbacks = this.cacheCallbacks;
- if (!cacheCallbacks) return false;
- const types = type ? [type] : ["master", "self_signing", "user_signing"];
- for (const t of types) {
- if (!(await cacheCallbacks.getCrossSigningKeyCache?.(t))) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Get cross-signing private keys from the local cache.
- *
- * @returns A map from key type (string) to private key (Uint8Array)
- */
- public async getCrossSigningKeysFromCache(): Promise<Map<string, Uint8Array>> {
- const keys = new Map();
- const cacheCallbacks = this.cacheCallbacks;
- if (!cacheCallbacks) return keys;
- for (const type of ["master", "self_signing", "user_signing"]) {
- const privKey = await cacheCallbacks.getCrossSigningKeyCache?.(type);
- if (!privKey) {
- continue;
- }
- keys.set(type, privKey);
- }
- return keys;
- }
-
- /**
- * Get the ID used to identify the user. This can also be used to test for
- * the existence of a given key type.
- *
- * @param type - The type of key to get the ID of. One of "master",
- * "self_signing", or "user_signing". Defaults to "master".
- *
- * @returns the ID
- */
- public getId(type = "master"): string | null {
- if (!this.keys[type]) return null;
- const keyInfo = this.keys[type];
- return publicKeyFromKeyInfo(keyInfo);
- }
-
- /**
- * Create new cross-signing keys for the given key types. The public keys
- * will be held in this class, while the private keys are passed off to the
- * `saveCrossSigningKeys` application callback.
- *
- * @param level - The key types to reset
- */
- public async resetKeys(level?: CrossSigningLevel): Promise<void> {
- if (!this.callbacks.saveCrossSigningKeys) {
- throw new Error("No saveCrossSigningKeys callback supplied");
- }
-
- // If we're resetting the master key, we reset all keys
- if (level === undefined || level & CrossSigningLevel.MASTER || !this.keys.master) {
- level = CrossSigningLevel.MASTER | CrossSigningLevel.USER_SIGNING | CrossSigningLevel.SELF_SIGNING;
- } else if (level === (0 as CrossSigningLevel)) {
- return;
- }
-
- const privateKeys: Record<string, Uint8Array> = {};
- const keys: Record<string, ICrossSigningKey> = {};
- let masterSigning;
- let masterPub;
-
- try {
- if (level & CrossSigningLevel.MASTER) {
- masterSigning = new global.Olm.PkSigning();
- privateKeys.master = masterSigning.generate_seed();
- masterPub = masterSigning.init_with_seed(privateKeys.master);
- keys.master = {
- user_id: this.userId,
- usage: ["master"],
- keys: {
- ["ed25519:" + masterPub]: masterPub,
- },
- };
- } else {
- [masterPub, masterSigning] = await this.getCrossSigningKey("master");
- }
-
- if (level & CrossSigningLevel.SELF_SIGNING) {
- const sskSigning = new global.Olm.PkSigning();
- try {
- privateKeys.self_signing = sskSigning.generate_seed();
- const sskPub = sskSigning.init_with_seed(privateKeys.self_signing);
- keys.self_signing = {
- user_id: this.userId,
- usage: ["self_signing"],
- keys: {
- ["ed25519:" + sskPub]: sskPub,
- },
- };
- pkSign(keys.self_signing, masterSigning, this.userId, masterPub);
- } finally {
- sskSigning.free();
- }
- }
-
- if (level & CrossSigningLevel.USER_SIGNING) {
- const uskSigning = new global.Olm.PkSigning();
- try {
- privateKeys.user_signing = uskSigning.generate_seed();
- const uskPub = uskSigning.init_with_seed(privateKeys.user_signing);
- keys.user_signing = {
- user_id: this.userId,
- usage: ["user_signing"],
- keys: {
- ["ed25519:" + uskPub]: uskPub,
- },
- };
- pkSign(keys.user_signing, masterSigning, this.userId, masterPub);
- } finally {
- uskSigning.free();
- }
- }
-
- Object.assign(this.keys, keys);
- this.callbacks.saveCrossSigningKeys(privateKeys);
- } finally {
- if (masterSigning) {
- masterSigning.free();
- }
- }
- }
-
- /**
- * unsets the keys, used when another session has reset the keys, to disable cross-signing
- */
- public clearKeys(): void {
- this.keys = {};
- }
-
- public setKeys(keys: Record<string, ICrossSigningKey>): void {
- const signingKeys: Record<string, ICrossSigningKey> = {};
- if (keys.master) {
- if (keys.master.user_id !== this.userId) {
- const error = "Mismatched user ID " + keys.master.user_id + " in master key from " + this.userId;
- logger.error(error);
- throw new Error(error);
- }
- if (!this.keys.master) {
- // this is the first key we've seen, so first-use is true
- this.firstUse = true;
- } else if (publicKeyFromKeyInfo(keys.master) !== this.getId()) {
- // this is a different key, so first-use is false
- this.firstUse = false;
- } // otherwise, same key, so no change
- signingKeys.master = keys.master;
- } else if (this.keys.master) {
- signingKeys.master = this.keys.master;
- } else {
- throw new Error("Tried to set cross-signing keys without a master key");
- }
- const masterKey = publicKeyFromKeyInfo(signingKeys.master);
-
- // verify signatures
- if (keys.user_signing) {
- if (keys.user_signing.user_id !== this.userId) {
- const error = "Mismatched user ID " + keys.master.user_id + " in user_signing key from " + this.userId;
- logger.error(error);
- throw new Error(error);
- }
- try {
- pkVerify(keys.user_signing, masterKey, this.userId);
- } catch (e) {
- logger.error("invalid signature on user-signing key");
- // FIXME: what do we want to do here?
- throw e;
- }
- }
- if (keys.self_signing) {
- if (keys.self_signing.user_id !== this.userId) {
- const error = "Mismatched user ID " + keys.master.user_id + " in self_signing key from " + this.userId;
- logger.error(error);
- throw new Error(error);
- }
- try {
- pkVerify(keys.self_signing, masterKey, this.userId);
- } catch (e) {
- logger.error("invalid signature on self-signing key");
- // FIXME: what do we want to do here?
- throw e;
- }
- }
-
- // if everything checks out, then save the keys
- if (keys.master) {
- this.keys.master = keys.master;
- // if the master key is set, then the old self-signing and user-signing keys are obsolete
- delete this.keys["self_signing"];
- delete this.keys["user_signing"];
- }
- if (keys.self_signing) {
- this.keys.self_signing = keys.self_signing;
- }
- if (keys.user_signing) {
- this.keys.user_signing = keys.user_signing;
- }
- }
-
- public updateCrossSigningVerifiedBefore(isCrossSigningVerified: boolean): void {
- // It is critical that this value latches forward from false to true but
- // never back to false to avoid a downgrade attack.
- if (!this.crossSigningVerifiedBefore && isCrossSigningVerified) {
- this.crossSigningVerifiedBefore = true;
- }
- }
-
- public async signObject<T extends object>(data: T, type: string): Promise<T & { signatures: ISignatures }> {
- if (!this.keys[type]) {
- throw new Error("Attempted to sign with " + type + " key but no such key present");
- }
- const [pubkey, signing] = await this.getCrossSigningKey(type);
- try {
- pkSign(data, signing, this.userId, pubkey);
- return data as T & { signatures: ISignatures };
- } finally {
- signing.free();
- }
- }
-
- public async signUser(key: CrossSigningInfo): Promise<ICrossSigningKey | undefined> {
- if (!this.keys.user_signing) {
- logger.info("No user signing key: not signing user");
- return;
- }
- return this.signObject(key.keys.master, "user_signing");
- }
-
- public async signDevice(userId: string, device: DeviceInfo): Promise<ISignedKey | undefined> {
- if (userId !== this.userId) {
- throw new Error(`Trying to sign ${userId}'s device; can only sign our own device`);
- }
- if (!this.keys.self_signing) {
- logger.info("No self signing key: not signing device");
- return;
- }
- return this.signObject<Omit<ISignedKey, "signatures">>(
- {
- algorithms: device.algorithms,
- keys: device.keys,
- device_id: device.deviceId,
- user_id: userId,
- },
- "self_signing",
- );
- }
-
- /**
- * Check whether a given user is trusted.
- *
- * @param userCrossSigning - Cross signing info for user
- *
- * @returns
- */
- public checkUserTrust(userCrossSigning: CrossSigningInfo): UserTrustLevel {
- // if we're checking our own key, then it's trusted if the master key
- // and self-signing key match
- if (
- this.userId === userCrossSigning.userId &&
- this.getId() &&
- this.getId() === userCrossSigning.getId() &&
- this.getId("self_signing") &&
- this.getId("self_signing") === userCrossSigning.getId("self_signing")
- ) {
- return new UserTrustLevel(true, true, this.firstUse);
- }
-
- if (!this.keys.user_signing) {
- // If there's no user signing key, they can't possibly be verified.
- // They may be TOFU trusted though.
- return new UserTrustLevel(false, false, userCrossSigning.firstUse);
- }
-
- let userTrusted: boolean;
- const userMaster = userCrossSigning.keys.master;
- const uskId = this.getId("user_signing")!;
- try {
- pkVerify(userMaster, uskId, this.userId);
- userTrusted = true;
- } catch (e) {
- userTrusted = false;
- }
- return new UserTrustLevel(userTrusted, userCrossSigning.crossSigningVerifiedBefore, userCrossSigning.firstUse);
- }
-
- /**
- * Check whether a given device is trusted.
- *
- * @param userCrossSigning - Cross signing info for user
- * @param device - The device to check
- * @param localTrust - Whether the device is trusted locally
- * @param trustCrossSignedDevices - Whether we trust cross signed devices
- *
- * @returns
- */
- public checkDeviceTrust(
- userCrossSigning: CrossSigningInfo,
- device: DeviceInfo,
- localTrust: boolean,
- trustCrossSignedDevices: boolean,
- ): DeviceTrustLevel {
- const userTrust = this.checkUserTrust(userCrossSigning);
-
- const userSSK = userCrossSigning.keys.self_signing;
- if (!userSSK) {
- // if the user has no self-signing key then we cannot make any
- // trust assertions about this device from cross-signing
- return new DeviceTrustLevel(false, false, localTrust, trustCrossSignedDevices);
- }
-
- const deviceObj = deviceToObject(device, userCrossSigning.userId);
- try {
- // if we can verify the user's SSK from their master key...
- pkVerify(userSSK, userCrossSigning.getId()!, userCrossSigning.userId);
- // ...and this device's key from their SSK...
- pkVerify(deviceObj, publicKeyFromKeyInfo(userSSK), userCrossSigning.userId);
- // ...then we trust this device as much as far as we trust the user
- return DeviceTrustLevel.fromUserTrustLevel(userTrust, localTrust, trustCrossSignedDevices);
- } catch (e) {
- return new DeviceTrustLevel(false, false, localTrust, trustCrossSignedDevices);
- }
- }
-
- /**
- * @returns Cache callbacks
- */
- public getCacheCallbacks(): ICacheCallbacks {
- return this.cacheCallbacks;
- }
-}
-
-interface DeviceObject extends IObject {
- algorithms: string[];
- keys: Record<string, string>;
- device_id: string;
- user_id: string;
-}
-
-function deviceToObject(device: DeviceInfo, userId: string): DeviceObject {
- return {
- algorithms: device.algorithms,
- keys: device.keys,
- device_id: device.deviceId,
- user_id: userId,
- signatures: device.signatures,
- };
-}
-
-export enum CrossSigningLevel {
- MASTER = 4,
- USER_SIGNING = 2,
- SELF_SIGNING = 1,
-}
-
-/**
- * Represents the ways in which we trust a user
- */
-export class UserTrustLevel {
- public constructor(
- private readonly crossSigningVerified: boolean,
- private readonly crossSigningVerifiedBefore: boolean,
- private readonly tofu: boolean,
- ) {}
-
- /**
- * @returns true if this user is verified via any means
- */
- public isVerified(): boolean {
- return this.isCrossSigningVerified();
- }
-
- /**
- * @returns true if this user is verified via cross signing
- */
- public isCrossSigningVerified(): boolean {
- return this.crossSigningVerified;
- }
-
- /**
- * @returns true if we ever verified this user before (at least for
- * the history of verifications observed by this device).
- */
- public wasCrossSigningVerified(): boolean {
- return this.crossSigningVerifiedBefore;
- }
-
- /**
- * @returns true if this user's key is trusted on first use
- */
- public isTofu(): boolean {
- return this.tofu;
- }
-}
-
-/**
- * Represents the ways in which we trust a device
- */
-export class DeviceTrustLevel {
- public constructor(
- public readonly crossSigningVerified: boolean,
- public readonly tofu: boolean,
- private readonly localVerified: boolean,
- private readonly trustCrossSignedDevices: boolean,
- ) {}
-
- public static fromUserTrustLevel(
- userTrustLevel: UserTrustLevel,
- localVerified: boolean,
- trustCrossSignedDevices: boolean,
- ): DeviceTrustLevel {
- return new DeviceTrustLevel(
- userTrustLevel.isCrossSigningVerified(),
- userTrustLevel.isTofu(),
- localVerified,
- trustCrossSignedDevices,
- );
- }
-
- /**
- * @returns true if this device is verified via any means
- */
- public isVerified(): boolean {
- return Boolean(this.isLocallyVerified() || (this.trustCrossSignedDevices && this.isCrossSigningVerified()));
- }
-
- /**
- * @returns true if this device is verified via cross signing
- */
- public isCrossSigningVerified(): boolean {
- return this.crossSigningVerified;
- }
-
- /**
- * @returns true if this device is verified locally
- */
- public isLocallyVerified(): boolean {
- return this.localVerified;
- }
-
- /**
- * @returns true if this device is trusted from a user's key
- * that is trusted on first use
- */
- public isTofu(): boolean {
- return this.tofu;
- }
-}
-
-export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: OlmDevice): ICacheCallbacks {
- return {
- getCrossSigningKeyCache: async function (
- type: keyof SecretStorePrivateKeys,
- _expectedPublicKey: string,
- ): Promise<Uint8Array> {
- const key = await new Promise<any>((resolve) => {
- return store.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- store.getSecretStorePrivateKey(txn, resolve, type);
- });
- });
-
- if (key && key.ciphertext) {
- const pickleKey = Buffer.from(olmDevice.pickleKey);
- const decrypted = await decryptAES(key, pickleKey, type);
- return decodeBase64(decrypted);
- } else {
- return key;
- }
- },
- storeCrossSigningKeyCache: async function (
- type: keyof SecretStorePrivateKeys,
- key?: Uint8Array,
- ): Promise<void> {
- if (!(key instanceof Uint8Array)) {
- throw new Error(`storeCrossSigningKeyCache expects Uint8Array, got ${key}`);
- }
- const pickleKey = Buffer.from(olmDevice.pickleKey);
- const encryptedKey = await encryptAES(encodeBase64(key), pickleKey, type);
- return store.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- store.storeSecretStorePrivateKey(txn, type, encryptedKey);
- });
- },
- };
-}
-
-export type KeysDuringVerification = [[string, PkSigning], [string, PkSigning], [string, PkSigning], void];
-
-/**
- * Request cross-signing keys from another device during verification.
- *
- * @param baseApis - base Matrix API interface
- * @param userId - The user ID being verified
- * @param deviceId - The device ID being verified
- */
-export async function requestKeysDuringVerification(
- baseApis: MatrixClient,
- userId: string,
- deviceId: string,
-): Promise<KeysDuringVerification | void> {
- // If this is a self-verification, ask the other party for keys
- if (baseApis.getUserId() !== userId) {
- return;
- }
- logger.log("Cross-signing: Self-verification done; requesting keys");
- // This happens asynchronously, and we're not concerned about waiting for
- // it. We return here in order to test.
- return new Promise<KeysDuringVerification | void>((resolve, reject) => {
- const client = baseApis;
- const original = client.crypto!.crossSigningInfo;
-
- // We already have all of the infrastructure we need to validate and
- // cache cross-signing keys, so instead of replicating that, here we set
- // up callbacks that request them from the other device and call
- // CrossSigningInfo.getCrossSigningKey() to validate/cache
- const crossSigning = new CrossSigningInfo(
- original.userId,
- {
- getCrossSigningKey: async (type): Promise<Uint8Array> => {
- logger.debug("Cross-signing: requesting secret", type, deviceId);
- const { promise } = client.requestSecret(`m.cross_signing.${type}`, [deviceId]);
- const result = await promise;
- const decoded = decodeBase64(result);
- return Uint8Array.from(decoded);
- },
- },
- original.getCacheCallbacks(),
- );
- crossSigning.keys = original.keys;
-
- // XXX: get all keys out if we get one key out
- // https://github.com/vector-im/element-web/issues/12604
- // then change here to reject on the timeout
- // Requests can be ignored, so don't wait around forever
- const timeout = new Promise<void>((resolve) => {
- setTimeout(resolve, KEY_REQUEST_TIMEOUT_MS, new Error("Timeout"));
- });
-
- // also request and cache the key backup key
- const backupKeyPromise = (async (): Promise<void> => {
- const cachedKey = await client.crypto!.getSessionBackupPrivateKey();
- if (!cachedKey) {
- logger.info("No cached backup key found. Requesting...");
- const secretReq = client.requestSecret("m.megolm_backup.v1", [deviceId]);
- const base64Key = await secretReq.promise;
- logger.info("Got key backup key, decoding...");
- const decodedKey = decodeBase64(base64Key);
- logger.info("Decoded backup key, storing...");
- await client.crypto!.storeSessionBackupPrivateKey(Uint8Array.from(decodedKey));
- logger.info("Backup key stored. Starting backup restore...");
- const backupInfo = await client.getKeyBackupVersion();
- // no need to await for this - just let it go in the bg
- client.restoreKeyBackupWithCache(undefined, undefined, backupInfo!).then(() => {
- logger.info("Backup restored.");
- });
- }
- })();
-
- // We call getCrossSigningKey() for its side-effects
- return Promise.race<KeysDuringVerification | void>([
- Promise.all([
- crossSigning.getCrossSigningKey("master"),
- crossSigning.getCrossSigningKey("self_signing"),
- crossSigning.getCrossSigningKey("user_signing"),
- backupKeyPromise,
- ]) as Promise<KeysDuringVerification>,
- timeout,
- ]).then(resolve, reject);
- }).catch((e) => {
- logger.warn("Cross-signing: failure while requesting keys:", e);
- });
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/DeviceList.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/DeviceList.ts
deleted file mode 100644
index a1ff0eb..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/DeviceList.ts
+++ /dev/null
@@ -1,989 +0,0 @@
-/*
-Copyright 2017 - 2021 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.
-*/
-
-/**
- * Manages the list of other users' devices
- */
-
-import { logger } from "../logger";
-import { DeviceInfo, IDevice } from "./deviceinfo";
-import { CrossSigningInfo, ICrossSigningInfo } from "./CrossSigning";
-import * as olmlib from "./olmlib";
-import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store";
-import { chunkPromises, defer, IDeferred, sleep } from "../utils";
-import { DeviceKeys, IDownloadKeyResult, Keys, MatrixClient, SigningKeys } from "../client";
-import { OlmDevice } from "./OlmDevice";
-import { CryptoStore } from "./store/base";
-import { TypedEventEmitter } from "../models/typed-event-emitter";
-import { CryptoEvent, CryptoEventHandlerMap } from "./index";
-
-/* State transition diagram for DeviceList.deviceTrackingStatus
- *
- * |
- * stopTrackingDeviceList V
- * +---------------------> NOT_TRACKED
- * | |
- * +<--------------------+ | startTrackingDeviceList
- * | | V
- * | +-------------> PENDING_DOWNLOAD <--------------------+-+
- * | | ^ | | |
- * | | restart download | | start download | | invalidateUserDeviceList
- * | | client failed | | | |
- * | | | V | |
- * | +------------ DOWNLOAD_IN_PROGRESS -------------------+ |
- * | | | |
- * +<-------------------+ | download successful |
- * ^ V |
- * +----------------------- UP_TO_DATE ------------------------+
- */
-
-// constants for DeviceList.deviceTrackingStatus
-export enum TrackingStatus {
- NotTracked,
- PendingDownload,
- DownloadInProgress,
- UpToDate,
-}
-
-// user-Id → device-Id → DeviceInfo
-export type DeviceInfoMap = Map<string, Map<string, DeviceInfo>>;
-
-type EmittedEvents = CryptoEvent.WillUpdateDevices | CryptoEvent.DevicesUpdated | CryptoEvent.UserCrossSigningUpdated;
-
-export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHandlerMap> {
- private devices: { [userId: string]: { [deviceId: string]: IDevice } } = {};
-
- public crossSigningInfo: { [userId: string]: ICrossSigningInfo } = {};
-
- // map of identity keys to the user who owns it
- private userByIdentityKey: Record<string, string> = {};
-
- // which users we are tracking device status for.
- private deviceTrackingStatus: { [userId: string]: TrackingStatus } = {}; // loaded from storage in load()
-
- // The 'next_batch' sync token at the point the data was written,
- // ie. a token representing the point immediately after the
- // moment represented by the snapshot in the db.
- private syncToken: string | null = null;
-
- private keyDownloadsInProgressByUser = new Map<string, Promise<void>>();
-
- // Set whenever changes are made other than setting the sync token
- private dirty = false;
-
- // Promise resolved when device data is saved
- private savePromise: Promise<boolean> | null = null;
- // Function that resolves the save promise
- private resolveSavePromise: ((saved: boolean) => void) | null = null;
- // The time the save is scheduled for
- private savePromiseTime: number | null = null;
- // The timer used to delay the save
- private saveTimer: ReturnType<typeof setTimeout> | null = 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 = null;
-
- private readonly serialiser: DeviceListUpdateSerialiser;
-
- public constructor(
- baseApis: MatrixClient,
- private readonly cryptoStore: CryptoStore,
- olmDevice: OlmDevice,
- // Maximum number of user IDs per request to prevent server overload (#1619)
- public readonly keyDownloadChunkSize = 250,
- ) {
- super();
-
- this.serialiser = new DeviceListUpdateSerialiser(baseApis, olmDevice, this);
- }
-
- /**
- * Load the device tracking state from storage
- */
- public async load(): Promise<void> {
- await this.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
- this.cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
- this.hasFetched = Boolean(deviceData && deviceData.devices);
- this.devices = deviceData ? deviceData.devices : {};
- this.crossSigningInfo = deviceData ? deviceData.crossSigningInfo || {} : {};
- this.deviceTrackingStatus = deviceData ? deviceData.trackingStatus : {};
- this.syncToken = deviceData?.syncToken ?? null;
- this.userByIdentityKey = {};
- for (const user of Object.keys(this.devices)) {
- const userDevices = this.devices[user];
- for (const device of Object.keys(userDevices)) {
- const idKey = userDevices[device].keys["curve25519:" + device];
- if (idKey !== undefined) {
- this.userByIdentityKey[idKey] = user;
- }
- }
- }
- });
- });
-
- for (const u of Object.keys(this.deviceTrackingStatus)) {
- // if a download was in progress when we got shut down, it isn't any more.
- if (this.deviceTrackingStatus[u] == TrackingStatus.DownloadInProgress) {
- this.deviceTrackingStatus[u] = TrackingStatus.PendingDownload;
- }
- }
- }
-
- public stop(): void {
- if (this.saveTimer !== null) {
- clearTimeout(this.saveTimer);
- }
- }
-
- /**
- * Save the device tracking state to storage, if any changes are
- * pending other than updating the sync token
- *
- * The actual save will be delayed by a short amount of time to
- * aggregate multiple writes to the database.
- *
- * @param delay - Time in ms before which the save actually happens.
- * By default, the save is delayed for a short period in order to batch
- * multiple writes, but this behaviour can be disabled by passing 0.
- *
- * @returns true if the data was saved, false if
- * it was not (eg. because no changes were pending). The promise
- * will only resolve once the data is saved, so may take some time
- * to resolve.
- */
- public async saveIfDirty(delay = 500): Promise<boolean> {
- if (!this.dirty) return Promise.resolve(false);
- // Delay saves for a bit so we can aggregate multiple saves that happen
- // in quick succession (eg. when a whole room's devices are marked as known)
-
- const targetTime = Date.now() + delay;
- if (this.savePromiseTime && targetTime < this.savePromiseTime) {
- // There's a save scheduled but for after we would like: cancel
- // it & schedule one for the time we want
- clearTimeout(this.saveTimer!);
- this.saveTimer = null;
- this.savePromiseTime = null;
- // (but keep the save promise since whatever called save before
- // will still want to know when the save is done)
- }
-
- let savePromise = this.savePromise;
- if (savePromise === null) {
- savePromise = new Promise((resolve) => {
- this.resolveSavePromise = resolve;
- });
- this.savePromise = savePromise;
- }
-
- if (this.saveTimer === null) {
- const resolveSavePromise = this.resolveSavePromise;
- this.savePromiseTime = targetTime;
- this.saveTimer = setTimeout(() => {
- logger.log("Saving device tracking data", this.syncToken);
-
- // null out savePromise now (after the delay but before the write),
- // otherwise we could return the existing promise when the save has
- // actually already happened.
- this.savePromiseTime = null;
- this.saveTimer = null;
- this.savePromise = null;
- this.resolveSavePromise = null;
-
- this.cryptoStore
- .doTxn("readwrite", [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
- this.cryptoStore.storeEndToEndDeviceData(
- {
- devices: this.devices,
- crossSigningInfo: this.crossSigningInfo,
- trackingStatus: this.deviceTrackingStatus,
- syncToken: this.syncToken ?? undefined,
- },
- txn,
- );
- })
- .then(
- () => {
- // The device list is considered dirty until the write completes.
- this.dirty = false;
- resolveSavePromise?.(true);
- },
- (err) => {
- logger.error("Failed to save device tracking data", this.syncToken);
- logger.error(err);
- },
- );
- }, delay);
- }
-
- return savePromise;
- }
-
- /**
- * Gets the sync token last set with setSyncToken
- *
- * @returns The sync token
- */
- public getSyncToken(): string | null {
- return this.syncToken;
- }
-
- /**
- * Sets the sync token that the app will pass as the 'since' to the /sync
- * endpoint next time it syncs.
- * The sync token must always be set after any changes made as a result of
- * data in that sync since setting the sync token to a newer one will mean
- * those changed will not be synced from the server if a new client starts
- * up with that data.
- *
- * @param st - The sync token
- */
- public setSyncToken(st: string | null): void {
- this.syncToken = st;
- }
-
- /**
- * Ensures up to date keys for a list of users are stored in the session store,
- * downloading and storing them if they're not (or if forceDownload is
- * true).
- * @param userIds - The users to fetch.
- * @param forceDownload - Always download the keys even if cached.
- *
- * @returns A promise which resolves to a map userId-\>deviceId-\>{@link DeviceInfo}.
- */
- public downloadKeys(userIds: string[], forceDownload: boolean): Promise<DeviceInfoMap> {
- const usersToDownload: string[] = [];
- const promises: Promise<unknown>[] = [];
-
- userIds.forEach((u) => {
- const trackingStatus = this.deviceTrackingStatus[u];
- if (this.keyDownloadsInProgressByUser.has(u)) {
- // already a key download in progress/queued for this user; its results
- // will be good enough for us.
- logger.log(`downloadKeys: already have a download in progress for ` + `${u}: awaiting its result`);
- promises.push(this.keyDownloadsInProgressByUser.get(u)!);
- } else if (forceDownload || trackingStatus != TrackingStatus.UpToDate) {
- usersToDownload.push(u);
- }
- });
-
- if (usersToDownload.length != 0) {
- logger.log("downloadKeys: downloading for", usersToDownload);
- const downloadPromise = this.doKeyDownload(usersToDownload);
- promises.push(downloadPromise);
- }
-
- if (promises.length === 0) {
- logger.log("downloadKeys: already have all necessary keys");
- }
-
- return Promise.all(promises).then(() => {
- return this.getDevicesFromStore(userIds);
- });
- }
-
- /**
- * Get the stored device keys for a list of user ids
- *
- * @param userIds - the list of users to list keys for.
- *
- * @returns userId-\>deviceId-\>{@link DeviceInfo}.
- */
- private getDevicesFromStore(userIds: string[]): DeviceInfoMap {
- const stored: DeviceInfoMap = new Map();
- userIds.forEach((userId) => {
- const deviceMap = new Map();
- this.getStoredDevicesForUser(userId)?.forEach(function (device) {
- deviceMap.set(device.deviceId, device);
- });
- stored.set(userId, deviceMap);
- });
- return stored;
- }
-
- /**
- * Returns a list of all user IDs the DeviceList knows about
- *
- * @returns All known user IDs
- */
- public getKnownUserIds(): string[] {
- return Object.keys(this.devices);
- }
-
- /**
- * Get the stored device keys for a user id
- *
- * @param userId - the user to list keys for.
- *
- * @returns list of devices, or null if we haven't
- * managed to get a list of devices for this user yet.
- */
- public getStoredDevicesForUser(userId: string): DeviceInfo[] | null {
- const devs = this.devices[userId];
- if (!devs) {
- return null;
- }
- const res: DeviceInfo[] = [];
- for (const deviceId in devs) {
- if (devs.hasOwnProperty(deviceId)) {
- res.push(DeviceInfo.fromStorage(devs[deviceId], deviceId));
- }
- }
- return res;
- }
-
- /**
- * Get the stored device data for a user, in raw object form
- *
- * @param userId - the user to get data for
- *
- * @returns `deviceId->{object}` devices, or undefined if
- * there is no data for this user.
- */
- public getRawStoredDevicesForUser(userId: string): Record<string, IDevice> {
- return this.devices[userId];
- }
-
- public getStoredCrossSigningForUser(userId: string): CrossSigningInfo | null {
- if (!this.crossSigningInfo[userId]) return null;
-
- return CrossSigningInfo.fromStorage(this.crossSigningInfo[userId], userId);
- }
-
- public storeCrossSigningForUser(userId: string, info: ICrossSigningInfo): void {
- this.crossSigningInfo[userId] = info;
- this.dirty = true;
- }
-
- /**
- * Get the stored keys for a single device
- *
- *
- * @returns device, or undefined
- * if we don't know about this device
- */
- public getStoredDevice(userId: string, deviceId: string): DeviceInfo | undefined {
- const devs = this.devices[userId];
- if (!devs?.[deviceId]) {
- return undefined;
- }
- return DeviceInfo.fromStorage(devs[deviceId], deviceId);
- }
-
- /**
- * Get a user ID by one of their device's curve25519 identity key
- *
- * @param algorithm - encryption algorithm
- * @param senderKey - curve25519 key to match
- *
- * @returns user ID
- */
- public getUserByIdentityKey(algorithm: string, senderKey: string): string | null {
- if (algorithm !== olmlib.OLM_ALGORITHM && algorithm !== olmlib.MEGOLM_ALGORITHM) {
- // we only deal in olm keys
- return null;
- }
-
- return this.userByIdentityKey[senderKey];
- }
-
- /**
- * Find a device by curve25519 identity key
- *
- * @param algorithm - encryption algorithm
- * @param senderKey - curve25519 key to match
- */
- public getDeviceByIdentityKey(algorithm: string, senderKey: string): DeviceInfo | null {
- const userId = this.getUserByIdentityKey(algorithm, senderKey);
- if (!userId) {
- return null;
- }
-
- const devices = this.devices[userId];
- if (!devices) {
- return null;
- }
-
- for (const deviceId in devices) {
- if (!devices.hasOwnProperty(deviceId)) {
- continue;
- }
-
- const device = devices[deviceId];
- for (const keyId in device.keys) {
- if (!device.keys.hasOwnProperty(keyId)) {
- continue;
- }
- if (keyId.indexOf("curve25519:") !== 0) {
- continue;
- }
- const deviceKey = device.keys[keyId];
- if (deviceKey == senderKey) {
- return DeviceInfo.fromStorage(device, deviceId);
- }
- }
- }
-
- // doesn't match a known device
- return null;
- }
-
- /**
- * Replaces the list of devices for a user with the given device list
- *
- * @param userId - The user ID
- * @param devices - New device info for user
- */
- public storeDevicesForUser(userId: string, devices: Record<string, IDevice>): void {
- this.setRawStoredDevicesForUser(userId, devices);
- this.dirty = true;
- }
-
- /**
- * flag the given user for device-list tracking, if they are not already.
- *
- * This will mean that a subsequent call to refreshOutdatedDeviceLists()
- * will download the device list for the user, and that subsequent calls to
- * invalidateUserDeviceList will trigger more updates.
- *
- */
- public startTrackingDeviceList(userId: string): void {
- // sanity-check the userId. This is mostly paranoia, but if synapse
- // can't parse the userId we give it as an mxid, it 500s the whole
- // request and we can never update the device lists again (because
- // the broken userId is always 'invalid' and always included in any
- // refresh request).
- // By checking it is at least a string, we can eliminate a class of
- // silly errors.
- if (typeof userId !== "string") {
- throw new Error("userId must be a string; was " + userId);
- }
- if (!this.deviceTrackingStatus[userId]) {
- logger.log("Now tracking device list for " + userId);
- this.deviceTrackingStatus[userId] = TrackingStatus.PendingDownload;
- // we don't yet persist the tracking status, since there may be a lot
- // of calls; we save all data together once the sync is done
- this.dirty = true;
- }
- }
-
- /**
- * Mark the given user as no longer being tracked for device-list updates.
- *
- * This won't affect any in-progress downloads, which will still go on to
- * complete; it will just mean that we don't think that we have an up-to-date
- * list for future calls to downloadKeys.
- *
- */
- public stopTrackingDeviceList(userId: string): void {
- if (this.deviceTrackingStatus[userId]) {
- logger.log("No longer tracking device list for " + userId);
- this.deviceTrackingStatus[userId] = TrackingStatus.NotTracked;
-
- // we don't yet persist the tracking status, since there may be a lot
- // of calls; we save all data together once the sync is done
- this.dirty = true;
- }
- }
-
- /**
- * Set all users we're currently tracking to untracked
- *
- * This will flag each user whose devices we are tracking as in need of an
- * update.
- */
- public stopTrackingAllDeviceLists(): void {
- for (const userId of Object.keys(this.deviceTrackingStatus)) {
- this.deviceTrackingStatus[userId] = TrackingStatus.NotTracked;
- }
- this.dirty = true;
- }
-
- /**
- * Mark the cached device list for the given user outdated.
- *
- * If we are not tracking this user's devices, we'll do nothing. Otherwise
- * we flag the user as needing an update.
- *
- * This doesn't actually set off an update, so that several users can be
- * batched together. Call refreshOutdatedDeviceLists() for that.
- *
- */
- public invalidateUserDeviceList(userId: string): void {
- if (this.deviceTrackingStatus[userId]) {
- logger.log("Marking device list outdated for", userId);
- this.deviceTrackingStatus[userId] = TrackingStatus.PendingDownload;
-
- // we don't yet persist the tracking status, since there may be a lot
- // of calls; we save all data together once the sync is done
- this.dirty = true;
- }
- }
-
- /**
- * If we have users who have outdated device lists, start key downloads for them
- *
- * @returns which completes when the download completes; normally there
- * is no need to wait for this (it's mostly for the unit tests).
- */
- public refreshOutdatedDeviceLists(): Promise<void> {
- this.saveIfDirty();
-
- const usersToDownload: string[] = [];
- for (const userId of Object.keys(this.deviceTrackingStatus)) {
- const stat = this.deviceTrackingStatus[userId];
- if (stat == TrackingStatus.PendingDownload) {
- usersToDownload.push(userId);
- }
- }
-
- return this.doKeyDownload(usersToDownload);
- }
-
- /**
- * Set the stored device data for a user, in raw object form
- * Used only by internal class DeviceListUpdateSerialiser
- *
- * @param userId - the user to get data for
- *
- * @param devices - `deviceId->{object}` the new devices
- */
- public setRawStoredDevicesForUser(userId: string, devices: Record<string, IDevice>): void {
- // remove old devices from userByIdentityKey
- if (this.devices[userId] !== undefined) {
- for (const [deviceId, dev] of Object.entries(this.devices[userId])) {
- const identityKey = dev.keys["curve25519:" + deviceId];
-
- delete this.userByIdentityKey[identityKey];
- }
- }
-
- this.devices[userId] = devices;
-
- // add new devices into userByIdentityKey
- for (const [deviceId, dev] of Object.entries(devices)) {
- const identityKey = dev.keys["curve25519:" + deviceId];
-
- this.userByIdentityKey[identityKey] = userId;
- }
- }
-
- public setRawStoredCrossSigningForUser(userId: string, info: ICrossSigningInfo): void {
- this.crossSigningInfo[userId] = info;
- }
-
- /**
- * Fire off download update requests for the given users, and update the
- * device list tracking status for them, and the
- * keyDownloadsInProgressByUser map for them.
- *
- * @param users - list of userIds
- *
- * @returns resolves when all the users listed have
- * been updated. rejects if there was a problem updating any of the
- * users.
- */
- private doKeyDownload(users: string[]): Promise<void> {
- if (users.length === 0) {
- // nothing to do
- return Promise.resolve();
- }
-
- const prom = this.serialiser.updateDevicesForUsers(users, this.syncToken!).then(
- () => {
- finished(true);
- },
- (e) => {
- logger.error("Error downloading keys for " + users + ":", e);
- finished(false);
- throw e;
- },
- );
-
- users.forEach((u) => {
- this.keyDownloadsInProgressByUser.set(u, prom);
- const stat = this.deviceTrackingStatus[u];
- if (stat == TrackingStatus.PendingDownload) {
- this.deviceTrackingStatus[u] = TrackingStatus.DownloadInProgress;
- }
- });
-
- const finished = (success: boolean): void => {
- this.emit(CryptoEvent.WillUpdateDevices, users, !this.hasFetched);
- users.forEach((u) => {
- this.dirty = true;
-
- // we may have queued up another download request for this user
- // since we started this request. If that happens, we should
- // ignore the completion of the first one.
- if (this.keyDownloadsInProgressByUser.get(u) !== prom) {
- logger.log("Another update in the queue for", u, "- not marking up-to-date");
- return;
- }
- this.keyDownloadsInProgressByUser.delete(u);
- const stat = this.deviceTrackingStatus[u];
- if (stat == TrackingStatus.DownloadInProgress) {
- if (success) {
- // we didn't get any new invalidations since this download started:
- // this user's device list is now up to date.
- this.deviceTrackingStatus[u] = TrackingStatus.UpToDate;
- logger.log("Device list for", u, "now up to date");
- } else {
- this.deviceTrackingStatus[u] = TrackingStatus.PendingDownload;
- }
- }
- });
- this.saveIfDirty();
- this.emit(CryptoEvent.DevicesUpdated, users, !this.hasFetched);
- this.hasFetched = true;
- };
-
- return prom;
- }
-}
-
-/**
- * Serialises updates to device lists
- *
- * Ensures that results from /keys/query are not overwritten if a second call
- * completes *before* an earlier one.
- *
- * It currently does this by ensuring only one call to /keys/query happens at a
- * time (and queuing other requests up).
- */
-class DeviceListUpdateSerialiser {
- private downloadInProgress = false;
-
- // users which are queued for download
- // userId -> true
- private keyDownloadsQueuedByUser: Record<string, boolean> = {};
-
- // deferred which is resolved when the queued users are downloaded.
- // non-null indicates that we have users queued for download.
- private queuedQueryDeferred?: IDeferred<void>;
-
- private syncToken?: string; // The sync token we send with the requests
-
- /*
- * @param baseApis - Base API object
- * @param olmDevice - The Olm Device
- * @param deviceList - The device list object, the device list to be updated
- */
- public constructor(
- private readonly baseApis: MatrixClient,
- private readonly olmDevice: OlmDevice,
- private readonly deviceList: DeviceList,
- ) {}
-
- /**
- * Make a key query request for the given users
- *
- * @param users - list of user ids
- *
- * @param syncToken - sync token to pass in the query request, to
- * help the HS give the most recent results
- *
- * @returns resolves when all the users listed have
- * been updated. rejects if there was a problem updating any of the
- * users.
- */
- public updateDevicesForUsers(users: string[], syncToken: string): Promise<void> {
- users.forEach((u) => {
- this.keyDownloadsQueuedByUser[u] = true;
- });
-
- if (!this.queuedQueryDeferred) {
- this.queuedQueryDeferred = defer();
- }
-
- // We always take the new sync token and just use the latest one we've
- // been given, since it just needs to be at least as recent as the
- // sync response the device invalidation message arrived in
- this.syncToken = syncToken;
-
- if (this.downloadInProgress) {
- // just queue up these users
- logger.log("Queued key download for", users);
- return this.queuedQueryDeferred.promise;
- }
-
- // start a new download.
- return this.doQueuedQueries();
- }
-
- private doQueuedQueries(): Promise<void> {
- if (this.downloadInProgress) {
- throw new Error("DeviceListUpdateSerialiser.doQueuedQueries called with request active");
- }
-
- const downloadUsers = Object.keys(this.keyDownloadsQueuedByUser);
- this.keyDownloadsQueuedByUser = {};
- const deferred = this.queuedQueryDeferred;
- this.queuedQueryDeferred = undefined;
-
- logger.log("Starting key download for", downloadUsers);
- this.downloadInProgress = true;
-
- const opts: Parameters<MatrixClient["downloadKeysForUsers"]>[1] = {};
- if (this.syncToken) {
- opts.token = this.syncToken;
- }
-
- const factories: Array<() => Promise<IDownloadKeyResult>> = [];
- for (let i = 0; i < downloadUsers.length; i += this.deviceList.keyDownloadChunkSize) {
- const userSlice = downloadUsers.slice(i, i + this.deviceList.keyDownloadChunkSize);
- factories.push(() => this.baseApis.downloadKeysForUsers(userSlice, opts));
- }
-
- chunkPromises(factories, 3)
- .then(async (responses: IDownloadKeyResult[]) => {
- const dk: IDownloadKeyResult["device_keys"] = Object.assign(
- {},
- ...responses.map((res) => res.device_keys || {}),
- );
- const masterKeys: IDownloadKeyResult["master_keys"] = Object.assign(
- {},
- ...responses.map((res) => res.master_keys || {}),
- );
- const ssks: IDownloadKeyResult["self_signing_keys"] = Object.assign(
- {},
- ...responses.map((res) => res.self_signing_keys || {}),
- );
- const usks: IDownloadKeyResult["user_signing_keys"] = Object.assign(
- {},
- ...responses.map((res) => res.user_signing_keys || {}),
- );
-
- // yield to other things that want to execute in between users, to
- // avoid wedging the CPU
- // (https://github.com/vector-im/element-web/issues/3158)
- //
- // of course we ought to do this in a web worker or similar, but
- // this serves as an easy solution for now.
- for (const userId of downloadUsers) {
- await sleep(5);
- try {
- await this.processQueryResponseForUser(userId, dk[userId], {
- master: masterKeys?.[userId],
- self_signing: ssks?.[userId],
- user_signing: usks?.[userId],
- });
- } catch (e) {
- // log the error but continue, so that one bad key
- // doesn't kill the whole process
- logger.error(`Error processing keys for ${userId}:`, e);
- }
- }
- })
- .then(
- () => {
- logger.log("Completed key download for " + downloadUsers);
-
- this.downloadInProgress = false;
- deferred?.resolve();
-
- // if we have queued users, fire off another request.
- if (this.queuedQueryDeferred) {
- this.doQueuedQueries();
- }
- },
- (e) => {
- logger.warn("Error downloading keys for " + downloadUsers + ":", e);
- this.downloadInProgress = false;
- deferred?.reject(e);
- },
- );
-
- return deferred!.promise;
- }
-
- private async processQueryResponseForUser(
- userId: string,
- dkResponse: DeviceKeys,
- crossSigningResponse: {
- master?: Keys;
- self_signing?: SigningKeys;
- user_signing?: SigningKeys;
- },
- ): Promise<void> {
- logger.log("got device keys for " + userId + ":", dkResponse);
- logger.log("got cross-signing keys for " + userId + ":", crossSigningResponse);
-
- {
- // map from deviceid -> deviceinfo for this user
- const userStore: Record<string, DeviceInfo> = {};
- const devs = this.deviceList.getRawStoredDevicesForUser(userId);
- if (devs) {
- Object.keys(devs).forEach((deviceId) => {
- const d = DeviceInfo.fromStorage(devs[deviceId], deviceId);
- userStore[deviceId] = d;
- });
- }
-
- await updateStoredDeviceKeysForUser(
- this.olmDevice,
- userId,
- userStore,
- dkResponse || {},
- this.baseApis.getUserId()!,
- this.baseApis.deviceId!,
- );
-
- // put the updates into the object that will be returned as our results
- const storage: Record<string, IDevice> = {};
- Object.keys(userStore).forEach((deviceId) => {
- storage[deviceId] = userStore[deviceId].toStorage();
- });
-
- this.deviceList.setRawStoredDevicesForUser(userId, storage);
- }
-
- // now do the same for the cross-signing keys
- {
- // FIXME: should we be ignoring empty cross-signing responses, or
- // should we be dropping the keys?
- if (
- crossSigningResponse &&
- (crossSigningResponse.master || crossSigningResponse.self_signing || crossSigningResponse.user_signing)
- ) {
- const crossSigning =
- this.deviceList.getStoredCrossSigningForUser(userId) || new CrossSigningInfo(userId);
-
- crossSigning.setKeys(crossSigningResponse);
-
- this.deviceList.setRawStoredCrossSigningForUser(userId, crossSigning.toStorage());
-
- // NB. Unlike most events in the js-sdk, this one is internal to the
- // js-sdk and is not re-emitted
- this.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, userId);
- }
- }
- }
-}
-
-async function updateStoredDeviceKeysForUser(
- olmDevice: OlmDevice,
- userId: string,
- userStore: Record<string, DeviceInfo>,
- userResult: IDownloadKeyResult["device_keys"]["user_id"],
- localUserId: string,
- localDeviceId: string,
-): Promise<boolean> {
- let updated = false;
-
- // remove any devices in the store which aren't in the response
- for (const deviceId in userStore) {
- if (!userStore.hasOwnProperty(deviceId)) {
- continue;
- }
-
- if (!(deviceId in userResult)) {
- if (userId === localUserId && deviceId === localDeviceId) {
- logger.warn(`Local device ${deviceId} missing from sync, skipping removal`);
- continue;
- }
-
- logger.log("Device " + userId + ":" + deviceId + " has been removed");
- delete userStore[deviceId];
- updated = true;
- }
- }
-
- for (const deviceId in userResult) {
- if (!userResult.hasOwnProperty(deviceId)) {
- continue;
- }
-
- const deviceResult = userResult[deviceId];
-
- // check that the user_id and device_id in the response object are
- // correct
- if (deviceResult.user_id !== userId) {
- logger.warn("Mismatched user_id " + deviceResult.user_id + " in keys from " + userId + ":" + deviceId);
- continue;
- }
- if (deviceResult.device_id !== deviceId) {
- logger.warn("Mismatched device_id " + deviceResult.device_id + " in keys from " + userId + ":" + deviceId);
- continue;
- }
-
- if (await storeDeviceKeys(olmDevice, userStore, deviceResult)) {
- updated = true;
- }
- }
-
- return updated;
-}
-
-/*
- * Process a device in a /query response, and add it to the userStore
- *
- * returns (a promise for) true if a change was made, else false
- */
-async function storeDeviceKeys(
- olmDevice: OlmDevice,
- userStore: Record<string, DeviceInfo>,
- deviceResult: IDownloadKeyResult["device_keys"]["user_id"]["device_id"],
-): Promise<boolean> {
- if (!deviceResult.keys) {
- // no keys?
- return false;
- }
-
- const deviceId = deviceResult.device_id;
- const userId = deviceResult.user_id;
-
- const signKeyId = "ed25519:" + deviceId;
- const signKey = deviceResult.keys[signKeyId];
- if (!signKey) {
- logger.warn("Device " + userId + ":" + deviceId + " has no ed25519 key");
- return false;
- }
-
- const unsigned = deviceResult.unsigned || {};
- const signatures = deviceResult.signatures || {};
-
- try {
- await olmlib.verifySignature(olmDevice, deviceResult, userId, deviceId, signKey);
- } catch (e) {
- logger.warn("Unable to verify signature on device " + userId + ":" + deviceId + ":" + e);
- return false;
- }
-
- // DeviceInfo
- let deviceStore;
-
- if (deviceId in userStore) {
- // already have this device.
- deviceStore = userStore[deviceId];
-
- if (deviceStore.getFingerprint() != signKey) {
- // this should only happen if the list has been MITMed; we are
- // best off sticking with the original keys.
- //
- // Should we warn the user about it somehow?
- logger.warn("Ed25519 key for device " + userId + ":" + deviceId + " has changed");
- return false;
- }
- } else {
- userStore[deviceId] = deviceStore = new DeviceInfo(deviceId);
- }
-
- deviceStore.keys = deviceResult.keys || {};
- deviceStore.algorithms = deviceResult.algorithms || [];
- deviceStore.unsigned = unsigned;
- deviceStore.signatures = signatures;
- return true;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/EncryptionSetup.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/EncryptionSetup.ts
deleted file mode 100644
index 4efe677..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/EncryptionSetup.ts
+++ /dev/null
@@ -1,356 +0,0 @@
-/*
-Copyright 2021 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { logger } from "../logger";
-import { IContent, MatrixEvent } from "../models/event";
-import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning";
-import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store";
-import { Method, ClientPrefix } from "../http-api";
-import { Crypto, ICryptoCallbacks, IBootstrapCrossSigningOpts } from "./index";
-import {
- ClientEvent,
- ClientEventHandlerMap,
- CrossSigningKeys,
- ICrossSigningKey,
- ISignedKey,
- KeySignatures,
-} from "../client";
-import { IKeyBackupInfo } from "./keybackup";
-import { TypedEventEmitter } from "../models/typed-event-emitter";
-import { IAccountDataClient } from "./SecretStorage";
-import { SecretStorageKeyDescription } from "../secret-storage";
-
-interface ICrossSigningKeys {
- authUpload: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"];
- keys: Record<"master" | "self_signing" | "user_signing", ICrossSigningKey>;
-}
-
-/**
- * Builds an EncryptionSetupOperation by calling any of the add.. methods.
- * Once done, `buildOperation()` can be called which allows to apply to operation.
- *
- * This is used as a helper by Crypto to keep track of all the network requests
- * and other side-effects of bootstrapping, so it can be applied in one go (and retried in the future)
- * Also keeps track of all the private keys created during bootstrapping, so we don't need to prompt for them
- * more than once.
- */
-export class EncryptionSetupBuilder {
- public readonly accountDataClientAdapter: AccountDataClientAdapter;
- public readonly crossSigningCallbacks: CrossSigningCallbacks;
- public readonly ssssCryptoCallbacks: SSSSCryptoCallbacks;
-
- private crossSigningKeys?: ICrossSigningKeys;
- private keySignatures?: KeySignatures;
- private keyBackupInfo?: IKeyBackupInfo;
- private sessionBackupPrivateKey?: Uint8Array;
-
- /**
- * @param accountData - pre-existing account data, will only be read, not written.
- * @param delegateCryptoCallbacks - crypto callbacks to delegate to if the key isn't in cache yet
- */
- public constructor(accountData: Map<string, MatrixEvent>, delegateCryptoCallbacks?: ICryptoCallbacks) {
- this.accountDataClientAdapter = new AccountDataClientAdapter(accountData);
- this.crossSigningCallbacks = new CrossSigningCallbacks();
- this.ssssCryptoCallbacks = new SSSSCryptoCallbacks(delegateCryptoCallbacks);
- }
-
- /**
- * Adds new cross-signing public keys
- *
- * @param authUpload - Function called to await an interactive auth
- * flow when uploading device signing keys.
- * Args:
- * A function that makes the request requiring auth. Receives
- * the auth data as an object. Can be called multiple times, first with
- * an empty authDict, to obtain the flows.
- * @param keys - the new keys
- */
- public addCrossSigningKeys(authUpload: ICrossSigningKeys["authUpload"], keys: ICrossSigningKeys["keys"]): void {
- this.crossSigningKeys = { authUpload, keys };
- }
-
- /**
- * Adds the key backup info to be updated on the server
- *
- * Used either to create a new key backup, or add signatures
- * from the new MSK.
- *
- * @param keyBackupInfo - as received from/sent to the server
- */
- public addSessionBackup(keyBackupInfo: IKeyBackupInfo): void {
- this.keyBackupInfo = keyBackupInfo;
- }
-
- /**
- * Adds the session backup private key to be updated in the local cache
- *
- * Used after fixing the format of the key
- *
- */
- public addSessionBackupPrivateKeyToCache(privateKey: Uint8Array): void {
- this.sessionBackupPrivateKey = privateKey;
- }
-
- /**
- * Add signatures from a given user and device/x-sign key
- * Used to sign the new cross-signing key with the device key
- *
- */
- public addKeySignature(userId: string, deviceId: string, signature: ISignedKey): void {
- if (!this.keySignatures) {
- this.keySignatures = {};
- }
- const userSignatures = this.keySignatures[userId] || {};
- this.keySignatures[userId] = userSignatures;
- userSignatures[deviceId] = signature;
- }
-
- public async setAccountData(type: string, content: object): Promise<void> {
- await this.accountDataClientAdapter.setAccountData(type, content);
- }
-
- /**
- * builds the operation containing all the parts that have been added to the builder
- */
- public buildOperation(): EncryptionSetupOperation {
- const accountData = this.accountDataClientAdapter.values;
- return new EncryptionSetupOperation(accountData, this.crossSigningKeys, this.keyBackupInfo, this.keySignatures);
- }
-
- /**
- * Stores the created keys locally.
- *
- * This does not yet store the operation in a way that it can be restored,
- * but that is the idea in the future.
- */
- public async persist(crypto: Crypto): Promise<void> {
- // store private keys in cache
- if (this.crossSigningKeys) {
- const cacheCallbacks = createCryptoStoreCacheCallbacks(crypto.cryptoStore, crypto.olmDevice);
- for (const type of ["master", "self_signing", "user_signing"]) {
- logger.log(`Cache ${type} cross-signing private key locally`);
- const privateKey = this.crossSigningCallbacks.privateKeys.get(type);
- await cacheCallbacks.storeCrossSigningKeyCache?.(type, privateKey);
- }
- // store own cross-sign pubkeys as trusted
- await crypto.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- crypto.cryptoStore.storeCrossSigningKeys(txn, this.crossSigningKeys!.keys);
- });
- }
- // store session backup key in cache
- if (this.sessionBackupPrivateKey) {
- await crypto.storeSessionBackupPrivateKey(this.sessionBackupPrivateKey);
- }
- }
-}
-
-/**
- * Can be created from EncryptionSetupBuilder, or
- * (in a follow-up PR, not implemented yet) restored from storage, to retry.
- *
- * It does not have knowledge of any private keys, unlike the builder.
- */
-export class EncryptionSetupOperation {
- /**
- */
- public constructor(
- private readonly accountData: Map<string, object>,
- private readonly crossSigningKeys?: ICrossSigningKeys,
- private readonly keyBackupInfo?: IKeyBackupInfo,
- private readonly keySignatures?: KeySignatures,
- ) {}
-
- /**
- * Runs the (remaining part of, in the future) operation by sending requests to the server.
- */
- public async apply(crypto: Crypto): Promise<void> {
- const baseApis = crypto.baseApis;
- // upload cross-signing keys
- if (this.crossSigningKeys) {
- const keys: Partial<CrossSigningKeys> = {};
- for (const [name, key] of Object.entries(this.crossSigningKeys.keys)) {
- keys[((name as keyof ICrossSigningKeys["keys"]) + "_key") as keyof CrossSigningKeys] = key;
- }
-
- // We must only call `uploadDeviceSigningKeys` from inside this auth
- // helper to ensure we properly handle auth errors.
- await this.crossSigningKeys.authUpload?.((authDict) => {
- return baseApis.uploadDeviceSigningKeys(authDict, keys as CrossSigningKeys);
- });
-
- // pass the new keys to the main instance of our own CrossSigningInfo.
- crypto.crossSigningInfo.setKeys(this.crossSigningKeys.keys);
- }
- // set account data
- if (this.accountData) {
- for (const [type, content] of this.accountData) {
- await baseApis.setAccountData(type, content);
- }
- }
- // upload first cross-signing signatures with the new key
- // (e.g. signing our own device)
- if (this.keySignatures) {
- await baseApis.uploadKeySignatures(this.keySignatures);
- }
- // need to create/update key backup info
- if (this.keyBackupInfo) {
- if (this.keyBackupInfo.version) {
- // session backup signature
- // The backup is trusted because the user provided the private key.
- // Sign the backup with the cross signing key so the key backup can
- // be trusted via cross-signing.
- await baseApis.http.authedRequest(
- Method.Put,
- "/room_keys/version/" + this.keyBackupInfo.version,
- undefined,
- {
- algorithm: this.keyBackupInfo.algorithm,
- auth_data: this.keyBackupInfo.auth_data,
- },
- { prefix: ClientPrefix.V3 },
- );
- } else {
- // add new key backup
- await baseApis.http.authedRequest(Method.Post, "/room_keys/version", undefined, this.keyBackupInfo, {
- prefix: ClientPrefix.V3,
- });
- }
- }
- }
-}
-
-/**
- * Catches account data set by SecretStorage during bootstrapping by
- * implementing the methods related to account data in MatrixClient
- */
-class AccountDataClientAdapter
- extends TypedEventEmitter<ClientEvent.AccountData, ClientEventHandlerMap>
- implements IAccountDataClient
-{
- //
- public readonly values = new Map<string, MatrixEvent>();
-
- /**
- * @param existingValues - existing account data
- */
- public constructor(private readonly existingValues: Map<string, MatrixEvent>) {
- super();
- }
-
- /**
- * @returns the content of the account data
- */
- public getAccountDataFromServer<T extends { [k: string]: any }>(type: string): Promise<T> {
- return Promise.resolve(this.getAccountData(type) as T);
- }
-
- /**
- * @returns the content of the account data
- */
- public getAccountData(type: string): IContent | null {
- const modifiedValue = this.values.get(type);
- if (modifiedValue) {
- return modifiedValue;
- }
- const existingValue = this.existingValues.get(type);
- if (existingValue) {
- return existingValue.getContent();
- }
- return null;
- }
-
- public setAccountData(type: string, content: any): Promise<{}> {
- const lastEvent = this.values.get(type);
- this.values.set(type, content);
- // ensure accountData is emitted on the next tick,
- // as SecretStorage listens for it while calling this method
- // and it seems to rely on this.
- return Promise.resolve().then(() => {
- const event = new MatrixEvent({ type, content });
- this.emit(ClientEvent.AccountData, event, lastEvent);
- return {};
- });
- }
-}
-
-/**
- * Catches the private cross-signing keys set during bootstrapping
- * by both cache callbacks (see createCryptoStoreCacheCallbacks) as non-cache callbacks.
- * See CrossSigningInfo constructor
- */
-class CrossSigningCallbacks implements ICryptoCallbacks, ICacheCallbacks {
- public readonly privateKeys = new Map<string, Uint8Array>();
-
- // cache callbacks
- public getCrossSigningKeyCache(type: string, expectedPublicKey: string): Promise<Uint8Array | null> {
- return this.getCrossSigningKey(type, expectedPublicKey);
- }
-
- public storeCrossSigningKeyCache(type: string, key: Uint8Array): Promise<void> {
- this.privateKeys.set(type, key);
- return Promise.resolve();
- }
-
- // non-cache callbacks
- public getCrossSigningKey(type: string, expectedPubkey: string): Promise<Uint8Array | null> {
- return Promise.resolve(this.privateKeys.get(type) ?? null);
- }
-
- public saveCrossSigningKeys(privateKeys: Record<string, Uint8Array>): void {
- for (const [type, privateKey] of Object.entries(privateKeys)) {
- this.privateKeys.set(type, privateKey);
- }
- }
-}
-
-/**
- * Catches the 4S private key set during bootstrapping by implementing
- * the SecretStorage crypto callbacks
- */
-class SSSSCryptoCallbacks {
- private readonly privateKeys = new Map<string, Uint8Array>();
-
- public constructor(private readonly delegateCryptoCallbacks?: ICryptoCallbacks) {}
-
- public async getSecretStorageKey(
- { keys }: { keys: Record<string, SecretStorageKeyDescription> },
- name: string,
- ): Promise<[string, Uint8Array] | null> {
- for (const keyId of Object.keys(keys)) {
- const privateKey = this.privateKeys.get(keyId);
- if (privateKey) {
- return [keyId, privateKey];
- }
- }
- // if we don't have the key cached yet, ask
- // for it to the general crypto callbacks and cache it
- if (this?.delegateCryptoCallbacks?.getSecretStorageKey) {
- const result = await this.delegateCryptoCallbacks.getSecretStorageKey({ keys }, name);
- if (result) {
- const [keyId, privateKey] = result;
- this.privateKeys.set(keyId, privateKey);
- }
- return result;
- }
- return null;
- }
-
- public addPrivateKey(keyId: string, keyInfo: SecretStorageKeyDescription, privKey: Uint8Array): void {
- this.privateKeys.set(keyId, privKey);
- // Also pass along to application to cache if it wishes
- this.delegateCryptoCallbacks?.cacheSecretStorageKey?.(keyId, keyInfo, privKey);
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/OlmDevice.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/OlmDevice.ts
deleted file mode 100644
index 82a0a9a..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/OlmDevice.ts
+++ /dev/null
@@ -1,1496 +0,0 @@
-/*
-Copyright 2016 - 2021 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 { Account, InboundGroupSession, OutboundGroupSession, Session, Utility } from "@matrix-org/olm";
-
-import { logger, PrefixedLogger } from "../logger";
-import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store";
-import * as algorithms from "./algorithms";
-import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base";
-import { IOlmDevice, IOutboundGroupSessionKey } from "./algorithms/megolm";
-import { IMegolmSessionData, OlmGroupSessionExtraData } from "../@types/crypto";
-import { IMessage } from "./algorithms/olm";
-
-// The maximum size of an event is 65K, and we base64 the content, so this is a
-// reasonable approximation to the biggest plaintext we can encrypt.
-const MAX_PLAINTEXT_LENGTH = (65536 * 3) / 4;
-
-export class PayloadTooLargeError extends Error {
- public readonly data = {
- errcode: "M_TOO_LARGE",
- error: "Payload too large for encrypted message",
- };
-}
-
-function checkPayloadLength(payloadString: string): void {
- if (payloadString === undefined) {
- throw new Error("payloadString undefined");
- }
-
- if (payloadString.length > MAX_PLAINTEXT_LENGTH) {
- // might as well fail early here rather than letting the olm library throw
- // a cryptic memory allocation error.
- //
- // Note that even if we manage to do the encryption, the message send may fail,
- // because by the time we've wrapped the ciphertext in the event object, it may
- // exceed 65K. But at least we won't just fail with "abort()" in that case.
- throw new PayloadTooLargeError(
- `Message too long (${payloadString.length} bytes). ` +
- `The maximum for an encrypted message is ${MAX_PLAINTEXT_LENGTH} bytes.`,
- );
- }
-}
-
-interface IInitOpts {
- fromExportedDevice?: IExportedDevice;
- pickleKey?: string;
-}
-
-/** data stored in the session store about an inbound group session */
-export interface InboundGroupSessionData {
- room_id: string; // eslint-disable-line camelcase
- /** pickled Olm.InboundGroupSession */
- session: string;
- keysClaimed: Record<string, string>;
- /** Devices involved in forwarding this session to us (normally empty). */
- forwardingCurve25519KeyChain: string[];
- /** whether this session is untrusted. */
- untrusted?: boolean;
- /** whether this session exists during the room being set to shared history. */
- sharedHistory?: boolean;
-}
-
-export interface IDecryptedGroupMessage {
- result: string;
- keysClaimed: Record<string, string>;
- senderKey: string;
- forwardingCurve25519KeyChain: string[];
- untrusted: boolean;
-}
-
-export interface IInboundSession {
- payload: string;
- session_id: string;
-}
-
-export interface IExportedDevice {
- pickleKey: string;
- pickledAccount: string;
- sessions: ISessionInfo[];
-}
-
-interface IUnpickledSessionInfo extends Omit<ISessionInfo, "session"> {
- session: Session;
-}
-
-/* eslint-disable camelcase */
-interface IInboundGroupSessionKey {
- chain_index: number;
- key: string;
- forwarding_curve25519_key_chain: string[];
- sender_claimed_ed25519_key: string | null;
- shared_history: boolean;
- untrusted?: boolean;
-}
-/* eslint-enable camelcase */
-
-type OneTimeKeys = { curve25519: { [keyId: string]: string } };
-
-/**
- * Manages the olm cryptography functions. Each OlmDevice has a single
- * OlmAccount and a number of OlmSessions.
- *
- * Accounts and sessions are kept pickled in the cryptoStore.
- */
-export class OlmDevice {
- public pickleKey = "DEFAULT_KEY"; // set by consumers
-
- /** Curve25519 key for the account, unknown until we load the account from storage in init() */
- public deviceCurve25519Key: string | null = null;
- /** Ed25519 key for the account, unknown until we load the account from storage in init() */
- public deviceEd25519Key: string | null = null;
- private maxOneTimeKeys: number | null = null;
-
- // we don't bother stashing outboundgroupsessions in the cryptoStore -
- // instead we keep them here.
- private outboundGroupSessionStore: Record<string, string> = {};
-
- // Store a set of decrypted message indexes for each group session.
- // This partially mitigates a replay attack where a MITM resends a group
- // message into the room.
- //
- // When we decrypt a message and the message index matches a previously
- // decrypted message, one possible cause of that is that we are decrypting
- // the same event, and may not indicate an actual replay attack. For
- // example, this could happen if we receive events, forget about them, and
- // then re-fetch them when we backfill. So we store the event ID and
- // timestamp corresponding to each message index when we first decrypt it,
- // and compare these against the event ID and timestamp every time we use
- // that same index. If they match, then we're probably decrypting the same
- // event and we don't consider it a replay attack.
- //
- // Keys are strings of form "<senderKey>|<session_id>|<message_index>"
- // Values are objects of the form "{id: <event id>, timestamp: <ts>}"
- private inboundGroupSessionMessageIndexes: Record<string, { id: string; timestamp: number }> = {};
-
- // Keep track of sessions that we're starting, so that we don't start
- // multiple sessions for the same device at the same time.
- public sessionsInProgress: Record<string, Promise<void>> = {}; // set by consumers
-
- // Used by olm to serialise prekey message decryptions
- public olmPrekeyPromise: Promise<any> = Promise.resolve(); // set by consumers
-
- public constructor(private readonly cryptoStore: CryptoStore) {}
-
- /**
- * @returns The version of Olm.
- */
- public static getOlmVersion(): [number, number, number] {
- return global.Olm.get_library_version();
- }
-
- /**
- * Initialise the OlmAccount. This must be called before any other operations
- * on the OlmDevice.
- *
- * Data from an exported Olm device can be provided
- * in order to re-create this device.
- *
- * Attempts to load the OlmAccount from the crypto store, or creates one if none is
- * found.
- *
- * Reads the device keys from the OlmAccount object.
- *
- * @param fromExportedDevice - (Optional) data from exported device
- * that must be re-created.
- * If present, opts.pickleKey is ignored
- * (exported data already provides a pickle key)
- * @param pickleKey - (Optional) pickle key to set instead of default one
- */
- public async init({ pickleKey, fromExportedDevice }: IInitOpts = {}): Promise<void> {
- let e2eKeys;
- const account = new global.Olm.Account();
-
- try {
- if (fromExportedDevice) {
- if (pickleKey) {
- logger.warn("ignoring opts.pickleKey" + " because opts.fromExportedDevice is present.");
- }
- this.pickleKey = fromExportedDevice.pickleKey;
- await this.initialiseFromExportedDevice(fromExportedDevice, account);
- } else {
- if (pickleKey) {
- this.pickleKey = pickleKey;
- }
- await this.initialiseAccount(account);
- }
- e2eKeys = JSON.parse(account.identity_keys());
-
- this.maxOneTimeKeys = account.max_number_of_one_time_keys();
- } finally {
- account.free();
- }
-
- this.deviceCurve25519Key = e2eKeys.curve25519;
- this.deviceEd25519Key = e2eKeys.ed25519;
- }
-
- /**
- * Populates the crypto store using data that was exported from an existing device.
- * Note that for now only the “account” and “sessions” stores are populated;
- * Other stores will be as with a new device.
- *
- * @param exportedData - Data exported from another device
- * through the “export” method.
- * @param account - an olm account to initialize
- */
- private async initialiseFromExportedDevice(exportedData: IExportedDevice, account: Account): Promise<void> {
- await this.cryptoStore.doTxn(
- "readwrite",
- [IndexedDBCryptoStore.STORE_ACCOUNT, IndexedDBCryptoStore.STORE_SESSIONS],
- (txn) => {
- this.cryptoStore.storeAccount(txn, exportedData.pickledAccount);
- exportedData.sessions.forEach((session) => {
- const { deviceKey, sessionId } = session;
- const sessionInfo = {
- session: session.session,
- lastReceivedMessageTs: session.lastReceivedMessageTs,
- };
- this.cryptoStore.storeEndToEndSession(deviceKey!, sessionId!, sessionInfo, txn);
- });
- },
- );
- account.unpickle(this.pickleKey, exportedData.pickledAccount);
- }
-
- private async initialiseAccount(account: Account): Promise<void> {
- await this.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.cryptoStore.getAccount(txn, (pickledAccount) => {
- if (pickledAccount !== null) {
- account.unpickle(this.pickleKey, pickledAccount);
- } else {
- account.create();
- pickledAccount = account.pickle(this.pickleKey);
- this.cryptoStore.storeAccount(txn, pickledAccount);
- }
- });
- });
- }
-
- /**
- * extract our OlmAccount from the crypto store and call the given function
- * with the account object
- * The `account` object is usable only within the callback passed to this
- * function and will be freed as soon the callback returns. It is *not*
- * usable for the rest of the lifetime of the transaction.
- * This function requires a live transaction object from cryptoStore.doTxn()
- * and therefore may only be called in a doTxn() callback.
- *
- * @param txn - Opaque transaction object from cryptoStore.doTxn()
- * @internal
- */
- private getAccount(txn: unknown, func: (account: Account) => void): void {
- this.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
- const account = new global.Olm.Account();
- try {
- account.unpickle(this.pickleKey, pickledAccount!);
- func(account);
- } finally {
- account.free();
- }
- });
- }
-
- /*
- * Saves an account to the crypto store.
- * This function requires a live transaction object from cryptoStore.doTxn()
- * and therefore may only be called in a doTxn() callback.
- *
- * @param txn - Opaque transaction object from cryptoStore.doTxn()
- * @param Olm.Account object
- * @internal
- */
- private storeAccount(txn: unknown, account: Account): void {
- this.cryptoStore.storeAccount(txn, account.pickle(this.pickleKey));
- }
-
- /**
- * Export data for re-creating the Olm device later.
- * TODO export data other than just account and (P2P) sessions.
- *
- * @returns The exported data
- */
- public async export(): Promise<IExportedDevice> {
- const result: Partial<IExportedDevice> = {
- pickleKey: this.pickleKey,
- };
-
- await this.cryptoStore.doTxn(
- "readonly",
- [IndexedDBCryptoStore.STORE_ACCOUNT, IndexedDBCryptoStore.STORE_SESSIONS],
- (txn) => {
- this.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
- result.pickledAccount = pickledAccount!;
- });
- result.sessions = [];
- // Note that the pickledSession object we get in the callback
- // is not exactly the same thing you get in method _getSession
- // see documentation of IndexedDBCryptoStore.getAllEndToEndSessions
- this.cryptoStore.getAllEndToEndSessions(txn, (pickledSession) => {
- result.sessions!.push(pickledSession!);
- });
- },
- );
- return result as IExportedDevice;
- }
-
- /**
- * extract an OlmSession from the session store and call the given function
- * The session is usable only within the callback passed to this
- * function and will be freed as soon the callback returns. It is *not*
- * usable for the rest of the lifetime of the transaction.
- *
- * @param txn - Opaque transaction object from cryptoStore.doTxn()
- * @internal
- */
- private getSession(
- deviceKey: string,
- sessionId: string,
- txn: unknown,
- func: (unpickledSessionInfo: IUnpickledSessionInfo) => void,
- ): void {
- this.cryptoStore.getEndToEndSession(deviceKey, sessionId, txn, (sessionInfo: ISessionInfo | null) => {
- this.unpickleSession(sessionInfo!, func);
- });
- }
-
- /**
- * Creates a session object from a session pickle and executes the given
- * function with it. The session object is destroyed once the function
- * returns.
- *
- * @internal
- */
- private unpickleSession(
- sessionInfo: ISessionInfo,
- func: (unpickledSessionInfo: IUnpickledSessionInfo) => void,
- ): void {
- const session = new global.Olm.Session();
- try {
- session.unpickle(this.pickleKey, sessionInfo.session!);
- const unpickledSessInfo: IUnpickledSessionInfo = Object.assign({}, sessionInfo, { session });
-
- func(unpickledSessInfo);
- } finally {
- session.free();
- }
- }
-
- /**
- * store our OlmSession in the session store
- *
- * @param sessionInfo - `{session: OlmSession, lastReceivedMessageTs: int}`
- * @param txn - Opaque transaction object from cryptoStore.doTxn()
- * @internal
- */
- private saveSession(deviceKey: string, sessionInfo: IUnpickledSessionInfo, txn: unknown): void {
- const sessionId = sessionInfo.session.session_id();
- logger.debug(`Saving Olm session ${sessionId} with device ${deviceKey}: ${sessionInfo.session.describe()}`);
-
- // Why do we re-use the input object for this, overwriting the same key with a different
- // type? Is it because we want to erase the unpickled session to enforce that it's no longer
- // used? A comment would be great.
- const pickledSessionInfo = Object.assign(sessionInfo, {
- session: sessionInfo.session.pickle(this.pickleKey),
- });
- this.cryptoStore.storeEndToEndSession(deviceKey, sessionId, pickledSessionInfo, txn);
- }
-
- /**
- * get an OlmUtility and call the given function
- *
- * @returns result of func
- * @internal
- */
- private getUtility<T>(func: (utility: Utility) => T): T {
- const utility = new global.Olm.Utility();
- try {
- return func(utility);
- } finally {
- utility.free();
- }
- }
-
- /**
- * Signs a message with the ed25519 key for this account.
- *
- * @param message - message to be signed
- * @returns base64-encoded signature
- */
- public async sign(message: string): Promise<string> {
- let result: string;
- await this.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.getAccount(txn, (account: Account) => {
- result = account.sign(message);
- });
- });
- return result!;
- }
-
- /**
- * Get the current (unused, unpublished) one-time keys for this account.
- *
- * @returns one time keys; an object with the single property
- * <tt>curve25519</tt>, which is itself an object mapping key id to Curve25519
- * key.
- */
- public async getOneTimeKeys(): Promise<OneTimeKeys> {
- let result: OneTimeKeys;
- await this.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.getAccount(txn, (account) => {
- result = JSON.parse(account.one_time_keys());
- });
- });
-
- return result!;
- }
-
- /**
- * Get the maximum number of one-time keys we can store.
- *
- * @returns number of keys
- */
- public maxNumberOfOneTimeKeys(): number {
- return this.maxOneTimeKeys ?? -1;
- }
-
- /**
- * Marks all of the one-time keys as published.
- */
- public async markKeysAsPublished(): Promise<void> {
- await this.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.getAccount(txn, (account: Account) => {
- account.mark_keys_as_published();
- this.storeAccount(txn, account);
- });
- });
- }
-
- /**
- * Generate some new one-time keys
- *
- * @param numKeys - number of keys to generate
- * @returns Resolved once the account is saved back having generated the keys
- */
- public generateOneTimeKeys(numKeys: number): Promise<void> {
- return this.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.getAccount(txn, (account) => {
- account.generate_one_time_keys(numKeys);
- this.storeAccount(txn, account);
- });
- });
- }
-
- /**
- * Generate a new fallback keys
- *
- * @returns Resolved once the account is saved back having generated the key
- */
- public async generateFallbackKey(): Promise<void> {
- await this.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.getAccount(txn, (account) => {
- account.generate_fallback_key();
- this.storeAccount(txn, account);
- });
- });
- }
-
- public async getFallbackKey(): Promise<Record<string, Record<string, string>>> {
- let result: Record<string, Record<string, string>>;
- await this.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.getAccount(txn, (account: Account) => {
- result = JSON.parse(account.unpublished_fallback_key());
- });
- });
- return result!;
- }
-
- public async forgetOldFallbackKey(): Promise<void> {
- await this.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.getAccount(txn, (account: Account) => {
- account.forget_old_fallback_key();
- this.storeAccount(txn, account);
- });
- });
- }
-
- /**
- * Generate a new outbound session
- *
- * The new session will be stored in the cryptoStore.
- *
- * @param theirIdentityKey - remote user's Curve25519 identity key
- * @param theirOneTimeKey - remote user's one-time Curve25519 key
- * @returns sessionId for the outbound session.
- */
- public async createOutboundSession(theirIdentityKey: string, theirOneTimeKey: string): Promise<string> {
- let newSessionId: string;
- await this.cryptoStore.doTxn(
- "readwrite",
- [IndexedDBCryptoStore.STORE_ACCOUNT, IndexedDBCryptoStore.STORE_SESSIONS],
- (txn) => {
- this.getAccount(txn, (account: Account) => {
- const session = new global.Olm.Session();
- try {
- session.create_outbound(account, theirIdentityKey, theirOneTimeKey);
- newSessionId = session.session_id();
- this.storeAccount(txn, account);
- const sessionInfo: IUnpickledSessionInfo = {
- session,
- // Pretend we've received a message at this point, otherwise
- // if we try to send a message to the device, it won't use
- // this session
- lastReceivedMessageTs: Date.now(),
- };
- this.saveSession(theirIdentityKey, sessionInfo, txn);
- } finally {
- session.free();
- }
- });
- },
- logger.withPrefix("[createOutboundSession]"),
- );
- return newSessionId!;
- }
-
- /**
- * Generate a new inbound session, given an incoming message
- *
- * @param theirDeviceIdentityKey - remote user's Curve25519 identity key
- * @param messageType - messageType field from the received message (must be 0)
- * @param ciphertext - base64-encoded body from the received message
- *
- * @returns decrypted payload, and
- * session id of new session
- *
- * @throws Error if the received message was not valid (for instance, it didn't use a valid one-time key).
- */
- public async createInboundSession(
- theirDeviceIdentityKey: string,
- messageType: number,
- ciphertext: string,
- ): Promise<IInboundSession> {
- if (messageType !== 0) {
- throw new Error("Need messageType == 0 to create inbound session");
- }
-
- let result: { payload: string; session_id: string }; // eslint-disable-line camelcase
- await this.cryptoStore.doTxn(
- "readwrite",
- [IndexedDBCryptoStore.STORE_ACCOUNT, IndexedDBCryptoStore.STORE_SESSIONS],
- (txn) => {
- this.getAccount(txn, (account: Account) => {
- const session = new global.Olm.Session();
- try {
- session.create_inbound_from(account, theirDeviceIdentityKey, ciphertext);
- account.remove_one_time_keys(session);
- this.storeAccount(txn, account);
-
- const payloadString = session.decrypt(messageType, ciphertext);
-
- const sessionInfo: IUnpickledSessionInfo = {
- session,
- // this counts as a received message: set last received message time
- // to now
- lastReceivedMessageTs: Date.now(),
- };
- this.saveSession(theirDeviceIdentityKey, sessionInfo, txn);
-
- result = {
- payload: payloadString,
- session_id: session.session_id(),
- };
- } finally {
- session.free();
- }
- });
- },
- logger.withPrefix("[createInboundSession]"),
- );
-
- return result!;
- }
-
- /**
- * Get a list of known session IDs for the given device
- *
- * @param theirDeviceIdentityKey - Curve25519 identity key for the
- * remote device
- * @returns a list of known session ids for the device
- */
- public async getSessionIdsForDevice(theirDeviceIdentityKey: string): Promise<string[]> {
- const log = logger.withPrefix("[getSessionIdsForDevice]");
-
- if (theirDeviceIdentityKey in this.sessionsInProgress) {
- log.debug(`Waiting for Olm session for ${theirDeviceIdentityKey} to be created`);
- try {
- await this.sessionsInProgress[theirDeviceIdentityKey];
- } catch (e) {
- // if the session failed to be created, just fall through and
- // return an empty result
- }
- }
- let sessionIds: string[];
- await this.cryptoStore.doTxn(
- "readonly",
- [IndexedDBCryptoStore.STORE_SESSIONS],
- (txn) => {
- this.cryptoStore.getEndToEndSessions(theirDeviceIdentityKey, txn, (sessions) => {
- sessionIds = Object.keys(sessions);
- });
- },
- log,
- );
-
- return sessionIds!;
- }
-
- /**
- * Get the right olm session id for encrypting messages to the given identity key
- *
- * @param theirDeviceIdentityKey - Curve25519 identity key for the
- * remote device
- * @param nowait - Don't wait for an in-progress session to complete.
- * This should only be set to true of the calling function is the function
- * that marked the session as being in-progress.
- * @param log - A possibly customised log
- * @returns session id, or null if no established session
- */
- public async getSessionIdForDevice(
- theirDeviceIdentityKey: string,
- nowait = false,
- log?: PrefixedLogger,
- ): Promise<string | null> {
- const sessionInfos = await this.getSessionInfoForDevice(theirDeviceIdentityKey, nowait, log);
-
- if (sessionInfos.length === 0) {
- return null;
- }
- // Use the session that has most recently received a message
- let idxOfBest = 0;
- for (let i = 1; i < sessionInfos.length; i++) {
- const thisSessInfo = sessionInfos[i];
- const thisLastReceived =
- thisSessInfo.lastReceivedMessageTs === undefined ? 0 : thisSessInfo.lastReceivedMessageTs;
-
- const bestSessInfo = sessionInfos[idxOfBest];
- const bestLastReceived =
- bestSessInfo.lastReceivedMessageTs === undefined ? 0 : bestSessInfo.lastReceivedMessageTs;
- if (
- thisLastReceived > bestLastReceived ||
- (thisLastReceived === bestLastReceived && thisSessInfo.sessionId < bestSessInfo.sessionId)
- ) {
- idxOfBest = i;
- }
- }
- return sessionInfos[idxOfBest].sessionId;
- }
-
- /**
- * Get information on the active Olm sessions for a device.
- * <p>
- * Returns an array, with an entry for each active session. The first entry in
- * the result will be the one used for outgoing messages. Each entry contains
- * the keys 'hasReceivedMessage' (true if the session has received an incoming
- * message and is therefore past the pre-key stage), and 'sessionId'.
- *
- * @param deviceIdentityKey - Curve25519 identity key for the device
- * @param nowait - Don't wait for an in-progress session to complete.
- * This should only be set to true of the calling function is the function
- * that marked the session as being in-progress.
- * @param log - A possibly customised log
- */
- public async getSessionInfoForDevice(
- deviceIdentityKey: string,
- nowait = false,
- log = logger,
- ): Promise<{ sessionId: string; lastReceivedMessageTs: number; hasReceivedMessage: boolean }[]> {
- log = log.withPrefix("[getSessionInfoForDevice]");
-
- if (deviceIdentityKey in this.sessionsInProgress && !nowait) {
- log.debug(`Waiting for Olm session for ${deviceIdentityKey} to be created`);
- try {
- await this.sessionsInProgress[deviceIdentityKey];
- } catch (e) {
- // if the session failed to be created, then just fall through and
- // return an empty result
- }
- }
- const info: {
- lastReceivedMessageTs: number;
- hasReceivedMessage: boolean;
- sessionId: string;
- }[] = [];
-
- await this.cryptoStore.doTxn(
- "readonly",
- [IndexedDBCryptoStore.STORE_SESSIONS],
- (txn) => {
- this.cryptoStore.getEndToEndSessions(deviceIdentityKey, txn, (sessions) => {
- const sessionIds = Object.keys(sessions).sort();
- for (const sessionId of sessionIds) {
- this.unpickleSession(sessions[sessionId], (sessInfo: IUnpickledSessionInfo) => {
- info.push({
- lastReceivedMessageTs: sessInfo.lastReceivedMessageTs!,
- hasReceivedMessage: sessInfo.session.has_received_message(),
- sessionId,
- });
- });
- }
- });
- },
- log,
- );
-
- return info;
- }
-
- /**
- * Encrypt an outgoing message using an existing session
- *
- * @param theirDeviceIdentityKey - Curve25519 identity key for the
- * remote device
- * @param sessionId - the id of the active session
- * @param payloadString - payload to be encrypted and sent
- *
- * @returns ciphertext
- */
- public async encryptMessage(
- theirDeviceIdentityKey: string,
- sessionId: string,
- payloadString: string,
- ): Promise<IMessage> {
- checkPayloadLength(payloadString);
-
- let res: IMessage;
- await this.cryptoStore.doTxn(
- "readwrite",
- [IndexedDBCryptoStore.STORE_SESSIONS],
- (txn) => {
- this.getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => {
- const sessionDesc = sessionInfo.session.describe();
- logger.log(
- "encryptMessage: Olm Session ID " +
- sessionId +
- " to " +
- theirDeviceIdentityKey +
- ": " +
- sessionDesc,
- );
- res = sessionInfo.session.encrypt(payloadString);
- this.saveSession(theirDeviceIdentityKey, sessionInfo, txn);
- });
- },
- logger.withPrefix("[encryptMessage]"),
- );
- return res!;
- }
-
- /**
- * Decrypt an incoming message using an existing session
- *
- * @param theirDeviceIdentityKey - Curve25519 identity key for the
- * remote device
- * @param sessionId - the id of the active session
- * @param messageType - messageType field from the received message
- * @param ciphertext - base64-encoded body from the received message
- *
- * @returns decrypted payload.
- */
- public async decryptMessage(
- theirDeviceIdentityKey: string,
- sessionId: string,
- messageType: number,
- ciphertext: string,
- ): Promise<string> {
- let payloadString: string;
- await this.cryptoStore.doTxn(
- "readwrite",
- [IndexedDBCryptoStore.STORE_SESSIONS],
- (txn) => {
- this.getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo: IUnpickledSessionInfo) => {
- const sessionDesc = sessionInfo.session.describe();
- logger.log(
- "decryptMessage: Olm Session ID " +
- sessionId +
- " from " +
- theirDeviceIdentityKey +
- ": " +
- sessionDesc,
- );
- payloadString = sessionInfo.session.decrypt(messageType, ciphertext);
- sessionInfo.lastReceivedMessageTs = Date.now();
- this.saveSession(theirDeviceIdentityKey, sessionInfo, txn);
- });
- },
- logger.withPrefix("[decryptMessage]"),
- );
- return payloadString!;
- }
-
- /**
- * Determine if an incoming messages is a prekey message matching an existing session
- *
- * @param theirDeviceIdentityKey - Curve25519 identity key for the
- * remote device
- * @param sessionId - the id of the active session
- * @param messageType - messageType field from the received message
- * @param ciphertext - base64-encoded body from the received message
- *
- * @returns true if the received message is a prekey message which matches
- * the given session.
- */
- public async matchesSession(
- theirDeviceIdentityKey: string,
- sessionId: string,
- messageType: number,
- ciphertext: string,
- ): Promise<boolean> {
- if (messageType !== 0) {
- return false;
- }
-
- let matches: boolean;
- await this.cryptoStore.doTxn(
- "readonly",
- [IndexedDBCryptoStore.STORE_SESSIONS],
- (txn) => {
- this.getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => {
- matches = sessionInfo.session.matches_inbound(ciphertext);
- });
- },
- logger.withPrefix("[matchesSession]"),
- );
- return matches!;
- }
-
- public async recordSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
- logger.info(`Recording problem on olm session with ${deviceKey} of type ${type}. Recreating: ${fixed}`);
- await this.cryptoStore.storeEndToEndSessionProblem(deviceKey, type, fixed);
- }
-
- public sessionMayHaveProblems(deviceKey: string, timestamp: number): Promise<IProblem | null> {
- return this.cryptoStore.getEndToEndSessionProblem(deviceKey, timestamp);
- }
-
- public filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
- return this.cryptoStore.filterOutNotifiedErrorDevices(devices);
- }
-
- // Outbound group session
- // ======================
-
- /**
- * store an OutboundGroupSession in outboundGroupSessionStore
- *
- * @internal
- */
- private saveOutboundGroupSession(session: OutboundGroupSession): void {
- this.outboundGroupSessionStore[session.session_id()] = session.pickle(this.pickleKey);
- }
-
- /**
- * extract an OutboundGroupSession from outboundGroupSessionStore and call the
- * given function
- *
- * @returns result of func
- * @internal
- */
- private getOutboundGroupSession<T>(sessionId: string, func: (session: OutboundGroupSession) => T): T {
- const pickled = this.outboundGroupSessionStore[sessionId];
- if (pickled === undefined) {
- throw new Error("Unknown outbound group session " + sessionId);
- }
-
- const session = new global.Olm.OutboundGroupSession();
- try {
- session.unpickle(this.pickleKey, pickled);
- return func(session);
- } finally {
- session.free();
- }
- }
-
- /**
- * Generate a new outbound group session
- *
- * @returns sessionId for the outbound session.
- */
- public createOutboundGroupSession(): string {
- const session = new global.Olm.OutboundGroupSession();
- try {
- session.create();
- this.saveOutboundGroupSession(session);
- return session.session_id();
- } finally {
- session.free();
- }
- }
-
- /**
- * Encrypt an outgoing message with an outbound group session
- *
- * @param sessionId - the id of the outboundgroupsession
- * @param payloadString - payload to be encrypted and sent
- *
- * @returns ciphertext
- */
- public encryptGroupMessage(sessionId: string, payloadString: string): string {
- logger.log(`encrypting msg with megolm session ${sessionId}`);
-
- checkPayloadLength(payloadString);
-
- return this.getOutboundGroupSession(sessionId, (session: OutboundGroupSession) => {
- const res = session.encrypt(payloadString);
- this.saveOutboundGroupSession(session);
- return res;
- });
- }
-
- /**
- * Get the session keys for an outbound group session
- *
- * @param sessionId - the id of the outbound group session
- *
- * @returns current chain index, and
- * base64-encoded secret key.
- */
- public getOutboundGroupSessionKey(sessionId: string): IOutboundGroupSessionKey {
- return this.getOutboundGroupSession(sessionId, function (session: OutboundGroupSession) {
- return {
- chain_index: session.message_index(),
- key: session.session_key(),
- };
- });
- }
-
- // Inbound group session
- // =====================
-
- /**
- * Unpickle a session from a sessionData object and invoke the given function.
- * The session is valid only until func returns.
- *
- * @param sessionData - Object describing the session.
- * @param func - Invoked with the unpickled session
- * @returns result of func
- */
- private unpickleInboundGroupSession<T>(
- sessionData: InboundGroupSessionData,
- func: (session: InboundGroupSession) => T,
- ): T {
- const session = new global.Olm.InboundGroupSession();
- try {
- session.unpickle(this.pickleKey, sessionData.session);
- return func(session);
- } finally {
- session.free();
- }
- }
-
- /**
- * extract an InboundGroupSession from the crypto store and call the given function
- *
- * @param roomId - The room ID to extract the session for, or null to fetch
- * sessions for any room.
- * @param txn - Opaque transaction object from cryptoStore.doTxn()
- * @param func - function to call.
- *
- * @internal
- */
- private getInboundGroupSession(
- roomId: string,
- senderKey: string,
- sessionId: string,
- txn: unknown,
- func: (
- session: InboundGroupSession | null,
- data: InboundGroupSessionData | null,
- withheld: IWithheld | null,
- ) => void,
- ): void {
- this.cryptoStore.getEndToEndInboundGroupSession(
- senderKey,
- sessionId,
- txn,
- (sessionData: InboundGroupSessionData | null, withheld: IWithheld | null) => {
- if (sessionData === null) {
- func(null, null, withheld);
- return;
- }
-
- // if we were given a room ID, check that the it matches the original one for the session. This stops
- // the HS pretending a message was targeting a different room.
- if (roomId !== null && roomId !== sessionData.room_id) {
- throw new Error(
- "Mismatched room_id for inbound group session (expected " +
- sessionData.room_id +
- ", was " +
- roomId +
- ")",
- );
- }
-
- this.unpickleInboundGroupSession(sessionData, (session: InboundGroupSession) => {
- func(session, sessionData, withheld);
- });
- },
- );
- }
-
- /**
- * Add an inbound group session to the session store
- *
- * @param roomId - room in which this session will be used
- * @param senderKey - base64-encoded curve25519 key of the sender
- * @param forwardingCurve25519KeyChain - Devices involved in forwarding
- * this session to us.
- * @param sessionId - session identifier
- * @param sessionKey - base64-encoded secret key
- * @param keysClaimed - Other keys the sender claims.
- * @param exportFormat - true if the megolm keys are in export format
- * (ie, they lack an ed25519 signature)
- * @param extraSessionData - any other data to be include with the session
- */
- public async addInboundGroupSession(
- roomId: string,
- senderKey: string,
- forwardingCurve25519KeyChain: string[],
- sessionId: string,
- sessionKey: string,
- keysClaimed: Record<string, string>,
- exportFormat: boolean,
- extraSessionData: OlmGroupSessionExtraData = {},
- ): Promise<void> {
- await this.cryptoStore.doTxn(
- "readwrite",
- [
- IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
- IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
- IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS,
- ],
- (txn) => {
- /* if we already have this session, consider updating it */
- this.getInboundGroupSession(
- roomId,
- senderKey,
- sessionId,
- txn,
- (
- existingSession: InboundGroupSession | null,
- existingSessionData: InboundGroupSessionData | null,
- ) => {
- // new session.
- const session = new global.Olm.InboundGroupSession();
- try {
- if (exportFormat) {
- session.import_session(sessionKey);
- } else {
- session.create(sessionKey);
- }
- if (sessionId != session.session_id()) {
- throw new Error("Mismatched group session ID from senderKey: " + senderKey);
- }
-
- if (existingSession) {
- logger.log(`Update for megolm session ${senderKey}|${sessionId}`);
- if (existingSession.first_known_index() <= session.first_known_index()) {
- if (!existingSessionData!.untrusted || extraSessionData.untrusted) {
- // existing session has less-than-or-equal index
- // (i.e. can decrypt at least as much), and the
- // new session's trust does not win over the old
- // session's trust, so keep it
- logger.log(`Keeping existing megolm session ${senderKey}|${sessionId}`);
- return;
- }
- if (existingSession.first_known_index() < session.first_known_index()) {
- // We want to upgrade the existing session's trust,
- // but we can't just use the new session because we'll
- // lose the lower index. Check that the sessions connect
- // properly, and then manually set the existing session
- // as trusted.
- if (
- existingSession.export_session(session.first_known_index()) ===
- session.export_session(session.first_known_index())
- ) {
- logger.info(
- "Upgrading trust of existing megolm session " +
- `${senderKey}|${sessionId} based on newly-received trusted session`,
- );
- existingSessionData!.untrusted = false;
- this.cryptoStore.storeEndToEndInboundGroupSession(
- senderKey,
- sessionId,
- existingSessionData!,
- txn,
- );
- } else {
- logger.warn(
- `Newly-received megolm session ${senderKey}|$sessionId}` +
- " does not match existing session! Keeping existing session",
- );
- }
- return;
- }
- // If the sessions have the same index, go ahead and store the new trusted one.
- }
- }
-
- logger.info(
- `Storing megolm session ${senderKey}|${sessionId} with first index ` +
- session.first_known_index(),
- );
-
- const sessionData = Object.assign({}, extraSessionData, {
- room_id: roomId,
- session: session.pickle(this.pickleKey),
- keysClaimed: keysClaimed,
- forwardingCurve25519KeyChain: forwardingCurve25519KeyChain,
- });
-
- this.cryptoStore.storeEndToEndInboundGroupSession(senderKey, sessionId, sessionData, txn);
-
- if (!existingSession && extraSessionData.sharedHistory) {
- this.cryptoStore.addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn);
- }
- } finally {
- session.free();
- }
- },
- );
- },
- logger.withPrefix("[addInboundGroupSession]"),
- );
- }
-
- /**
- * Record in the data store why an inbound group session was withheld.
- *
- * @param roomId - room that the session belongs to
- * @param senderKey - base64-encoded curve25519 key of the sender
- * @param sessionId - session identifier
- * @param code - reason code
- * @param reason - human-readable version of `code`
- */
- public async addInboundGroupSessionWithheld(
- roomId: string,
- senderKey: string,
- sessionId: string,
- code: string,
- reason: string,
- ): Promise<void> {
- await this.cryptoStore.doTxn(
- "readwrite",
- [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD],
- (txn) => {
- this.cryptoStore.storeEndToEndInboundGroupSessionWithheld(
- senderKey,
- sessionId,
- {
- room_id: roomId,
- code: code,
- reason: reason,
- },
- txn,
- );
- },
- );
- }
-
- /**
- * Decrypt a received message with an inbound group session
- *
- * @param roomId - room in which the message was received
- * @param senderKey - base64-encoded curve25519 key of the sender
- * @param sessionId - session identifier
- * @param body - base64-encoded body of the encrypted message
- * @param eventId - ID of the event being decrypted
- * @param timestamp - timestamp of the event being decrypted
- *
- * @returns null if the sessionId is unknown
- */
- public async decryptGroupMessage(
- roomId: string,
- senderKey: string,
- sessionId: string,
- body: string,
- eventId: string,
- timestamp: number,
- ): Promise<IDecryptedGroupMessage | null> {
- let result: IDecryptedGroupMessage | null = null;
- // when the localstorage crypto store is used as an indexeddb backend,
- // exceptions thrown from within the inner function are not passed through
- // to the top level, so we store exceptions in a variable and raise them at
- // the end
- let error: Error;
-
- await this.cryptoStore.doTxn(
- "readwrite",
- [
- IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
- IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
- ],
- (txn) => {
- this.getInboundGroupSession(roomId, senderKey, sessionId, txn, (session, sessionData, withheld) => {
- if (session === null || sessionData === null) {
- if (withheld) {
- error = new algorithms.DecryptionError(
- "MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
- calculateWithheldMessage(withheld),
- {
- session: senderKey + "|" + sessionId,
- },
- );
- }
- result = null;
- return;
- }
- let res: ReturnType<InboundGroupSession["decrypt"]>;
- try {
- res = session.decrypt(body);
- } catch (e) {
- if ((<Error>e)?.message === "OLM.UNKNOWN_MESSAGE_INDEX" && withheld) {
- error = new algorithms.DecryptionError(
- "MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
- calculateWithheldMessage(withheld),
- {
- session: senderKey + "|" + sessionId,
- },
- );
- } else {
- error = <Error>e;
- }
- return;
- }
-
- let plaintext: string = res.plaintext;
- if (plaintext === undefined) {
- // @ts-ignore - Compatibility for older olm versions.
- plaintext = res as string;
- } else {
- // Check if we have seen this message index before to detect replay attacks.
- // If the event ID and timestamp are specified, and the match the event ID
- // and timestamp from the last time we used this message index, then we
- // don't consider it a replay attack.
- const messageIndexKey = senderKey + "|" + sessionId + "|" + res.message_index;
- if (messageIndexKey in this.inboundGroupSessionMessageIndexes) {
- const msgInfo = this.inboundGroupSessionMessageIndexes[messageIndexKey];
- if (msgInfo.id !== eventId || msgInfo.timestamp !== timestamp) {
- error = new Error(
- "Duplicate message index, possible replay attack: " + messageIndexKey,
- );
- return;
- }
- }
- this.inboundGroupSessionMessageIndexes[messageIndexKey] = {
- id: eventId,
- timestamp: timestamp,
- };
- }
-
- sessionData.session = session.pickle(this.pickleKey);
- this.cryptoStore.storeEndToEndInboundGroupSession(senderKey, sessionId, sessionData, txn);
- result = {
- result: plaintext,
- keysClaimed: sessionData.keysClaimed || {},
- senderKey: senderKey,
- forwardingCurve25519KeyChain: sessionData.forwardingCurve25519KeyChain || [],
- untrusted: !!sessionData.untrusted,
- };
- });
- },
- logger.withPrefix("[decryptGroupMessage]"),
- );
-
- if (error!) {
- throw error;
- }
- return result!;
- }
-
- /**
- * Determine if we have the keys for a given megolm session
- *
- * @param roomId - room in which the message was received
- * @param senderKey - base64-encoded curve25519 key of the sender
- * @param sessionId - session identifier
- *
- * @returns true if we have the keys to this session
- */
- public async hasInboundSessionKeys(roomId: string, senderKey: string, sessionId: string): Promise<boolean> {
- let result: boolean;
- await this.cryptoStore.doTxn(
- "readonly",
- [
- IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
- IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
- ],
- (txn) => {
- this.cryptoStore.getEndToEndInboundGroupSession(senderKey, sessionId, txn, (sessionData) => {
- if (sessionData === null) {
- result = false;
- return;
- }
-
- if (roomId !== sessionData.room_id) {
- logger.warn(
- `requested keys for inbound group session ${senderKey}|` +
- `${sessionId}, with incorrect room_id ` +
- `(expected ${sessionData.room_id}, ` +
- `was ${roomId})`,
- );
- result = false;
- } else {
- result = true;
- }
- });
- },
- logger.withPrefix("[hasInboundSessionKeys]"),
- );
-
- return result!;
- }
-
- /**
- * Extract the keys to a given megolm session, for sharing
- *
- * @param roomId - room in which the message was received
- * @param senderKey - base64-encoded curve25519 key of the sender
- * @param sessionId - session identifier
- * @param chainIndex - The chain index at which to export the session.
- * If omitted, export at the first index we know about.
- *
- * @returns
- * details of the session key. The key is a base64-encoded megolm key in
- * export format.
- *
- * @throws Error If the given chain index could not be obtained from the known
- * index (ie. the given chain index is before the first we have).
- */
- public async getInboundGroupSessionKey(
- roomId: string,
- senderKey: string,
- sessionId: string,
- chainIndex?: number,
- ): Promise<IInboundGroupSessionKey | null> {
- let result: IInboundGroupSessionKey | null = null;
- await this.cryptoStore.doTxn(
- "readonly",
- [
- IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
- IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
- ],
- (txn) => {
- this.getInboundGroupSession(roomId, senderKey, sessionId, txn, (session, sessionData) => {
- if (session === null || sessionData === null) {
- result = null;
- return;
- }
-
- if (chainIndex === undefined) {
- chainIndex = session.first_known_index();
- }
-
- const exportedSession = session.export_session(chainIndex);
-
- const claimedKeys = sessionData.keysClaimed || {};
- const senderEd25519Key = claimedKeys.ed25519 || null;
-
- const forwardingKeyChain = sessionData.forwardingCurve25519KeyChain || [];
- // older forwarded keys didn't set the "untrusted"
- // property, but can be identified by having a
- // non-empty forwarding key chain. These keys should
- // be marked as untrusted since we don't know that they
- // can be trusted
- const untrusted =
- "untrusted" in sessionData ? sessionData.untrusted : forwardingKeyChain.length > 0;
-
- result = {
- chain_index: chainIndex,
- key: exportedSession,
- forwarding_curve25519_key_chain: forwardingKeyChain,
- sender_claimed_ed25519_key: senderEd25519Key,
- shared_history: sessionData.sharedHistory || false,
- untrusted: untrusted,
- };
- });
- },
- logger.withPrefix("[getInboundGroupSessionKey]"),
- );
-
- return result;
- }
-
- /**
- * Export an inbound group session
- *
- * @param senderKey - base64-encoded curve25519 key of the sender
- * @param sessionId - session identifier
- * @param sessionData - The session object from the store
- * @returns exported session data
- */
- public exportInboundGroupSession(
- senderKey: string,
- sessionId: string,
- sessionData: InboundGroupSessionData,
- ): IMegolmSessionData {
- return this.unpickleInboundGroupSession(sessionData, (session) => {
- const messageIndex = session.first_known_index();
-
- return {
- "sender_key": senderKey,
- "sender_claimed_keys": sessionData.keysClaimed,
- "room_id": sessionData.room_id,
- "session_id": sessionId,
- "session_key": session.export_session(messageIndex),
- "forwarding_curve25519_key_chain": sessionData.forwardingCurve25519KeyChain || [],
- "first_known_index": session.first_known_index(),
- "org.matrix.msc3061.shared_history": sessionData.sharedHistory || false,
- } as IMegolmSessionData;
- });
- }
-
- public async getSharedHistoryInboundGroupSessions(
- roomId: string,
- ): Promise<[senderKey: string, sessionId: string][]> {
- let result: Promise<[senderKey: string, sessionId: string][]>;
- await this.cryptoStore.doTxn(
- "readonly",
- [IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS],
- (txn) => {
- result = this.cryptoStore.getSharedHistoryInboundGroupSessions(roomId, txn);
- },
- logger.withPrefix("[getSharedHistoryInboundGroupSessionsForRoom]"),
- );
- return result!;
- }
-
- // Utilities
- // =========
-
- /**
- * Verify an ed25519 signature.
- *
- * @param key - ed25519 key
- * @param message - message which was signed
- * @param signature - base64-encoded signature to be checked
- *
- * @throws Error if there is a problem with the verification. If the key was
- * too small then the message will be "OLM.INVALID_BASE64". If the signature
- * was invalid then the message will be "OLM.BAD_MESSAGE_MAC".
- */
- public verifySignature(key: string, message: string, signature: string): void {
- this.getUtility(function (util: Utility) {
- util.ed25519_verify(key, message, signature);
- });
- }
-}
-
-export const WITHHELD_MESSAGES: Record<string, string> = {
- "m.unverified": "The sender has disabled encrypting to unverified devices.",
- "m.blacklisted": "The sender has blocked you.",
- "m.unauthorised": "You are not authorised to read the message.",
- "m.no_olm": "Unable to establish a secure channel.",
-};
-
-/**
- * Calculate the message to use for the exception when a session key is withheld.
- *
- * @param withheld - An object that describes why the key was withheld.
- *
- * @returns the message
- *
- * @internal
- */
-function calculateWithheldMessage(withheld: IWithheld): string {
- if (withheld.code && withheld.code in WITHHELD_MESSAGES) {
- return WITHHELD_MESSAGES[withheld.code];
- } else if (withheld.reason) {
- return withheld.reason;
- } else {
- return "decryption key withheld";
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/OutgoingRoomKeyRequestManager.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/OutgoingRoomKeyRequestManager.ts
deleted file mode 100644
index 4628b3e..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/OutgoingRoomKeyRequestManager.ts
+++ /dev/null
@@ -1,485 +0,0 @@
-/*
-Copyright 2017 - 2021 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 { v4 as uuidv4 } from "uuid";
-
-import { logger } from "../logger";
-import { MatrixClient } from "../client";
-import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "./index";
-import { CryptoStore, OutgoingRoomKeyRequest } from "./store/base";
-import { EventType, ToDeviceMessageId } from "../@types/event";
-import { MapWithDefault } from "../utils";
-
-/**
- * Internal module. Management of outgoing room key requests.
- *
- * See https://docs.google.com/document/d/1m4gQkcnJkxNuBmb5NoFCIadIY-DyqqNAS3lloE73BlQ
- * for draft documentation on what we're supposed to be implementing here.
- */
-
-// delay between deciding we want some keys, and sending out the request, to
-// allow for (a) it turning up anyway, (b) grouping requests together
-const SEND_KEY_REQUESTS_DELAY_MS = 500;
-
-/**
- * possible states for a room key request
- *
- * The state machine looks like:
- * ```
- *
- * | (cancellation sent)
- * | .-------------------------------------------------.
- * | | |
- * V V (cancellation requested) |
- * UNSENT -----------------------------+ |
- * | | |
- * | | |
- * | (send successful) | CANCELLATION_PENDING_AND_WILL_RESEND
- * V | Λ
- * SENT | |
- * |-------------------------------- | --------------'
- * | | (cancellation requested with intent
- * | | to resend the original request)
- * | |
- * | (cancellation requested) |
- * V |
- * CANCELLATION_PENDING |
- * | |
- * | (cancellation sent) |
- * V |
- * (deleted) <---------------------------+
- * ```
- */
-export enum RoomKeyRequestState {
- /** request not yet sent */
- Unsent,
- /** request sent, awaiting reply */
- Sent,
- /** reply received, cancellation not yet sent */
- CancellationPending,
- /**
- * Cancellation not yet sent and will transition to UNSENT instead of
- * being deleted once the cancellation has been sent.
- */
- CancellationPendingAndWillResend,
-}
-
-interface RequestMessageBase {
- requesting_device_id: string;
- request_id: string;
-}
-
-interface RequestMessageRequest extends RequestMessageBase {
- action: "request";
- body: IRoomKeyRequestBody;
-}
-
-interface RequestMessageCancellation extends RequestMessageBase {
- action: "request_cancellation";
-}
-
-type RequestMessage = RequestMessageRequest | RequestMessageCancellation;
-
-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?: ReturnType<typeof setTimeout>;
-
- // sanity check to ensure that we don't end up with two concurrent runs
- // of sendOutgoingRoomKeyRequests
- private sendOutgoingRoomKeyRequestsRunning = false;
-
- private clientRunning = true;
-
- public constructor(
- private readonly baseApis: MatrixClient,
- private readonly deviceId: string,
- private readonly cryptoStore: CryptoStore,
- ) {}
-
- /**
- * Called when the client is stopped. Stops any running background processes.
- */
- public stop(): void {
- logger.log("stopping OutgoingRoomKeyRequestManager");
- // stop the timer on the next run
- this.clientRunning = false;
- }
-
- /**
- * Send any requests that have been queued
- */
- public sendQueuedRequests(): void {
- this.startTimer();
- }
-
- /**
- * Queue up a room key request, if we haven't already queued or sent one.
- *
- * The `requestBody` is compared (with a deep-equality check) against
- * previous queued or sent requests and if it matches, no change is made.
- * Otherwise, a request is added to the pending list, and a job is started
- * in the background to send it.
- *
- * @param resend - whether to resend the key request if there is
- * already one
- *
- * @returns resolves when the request has been added to the
- * pending list (or we have established that a similar request already
- * exists)
- */
- public async queueRoomKeyRequest(
- requestBody: IRoomKeyRequestBody,
- recipients: IRoomKeyRequestRecipient[],
- resend = false,
- ): Promise<void> {
- const req = await this.cryptoStore.getOutgoingRoomKeyRequest(requestBody);
- if (!req) {
- await this.cryptoStore.getOrAddOutgoingRoomKeyRequest({
- requestBody: requestBody,
- recipients: recipients,
- requestId: this.baseApis.makeTxnId(),
- state: RoomKeyRequestState.Unsent,
- });
- } else {
- switch (req.state) {
- case RoomKeyRequestState.CancellationPendingAndWillResend:
- case RoomKeyRequestState.Unsent:
- // nothing to do here, since we're going to send a request anyways
- return;
-
- case RoomKeyRequestState.CancellationPending: {
- // existing request is about to be cancelled. If we want to
- // resend, then change the state so that it resends after
- // cancelling. Otherwise, just cancel the cancellation.
- const state = resend
- ? RoomKeyRequestState.CancellationPendingAndWillResend
- : RoomKeyRequestState.Sent;
- await this.cryptoStore.updateOutgoingRoomKeyRequest(
- req.requestId,
- RoomKeyRequestState.CancellationPending,
- {
- state,
- cancellationTxnId: this.baseApis.makeTxnId(),
- },
- );
- break;
- }
- case RoomKeyRequestState.Sent: {
- // a request has already been sent. If we don't want to
- // resend, then do nothing. If we do want to, then cancel the
- // existing request and send a new one.
- if (resend) {
- const state = RoomKeyRequestState.CancellationPendingAndWillResend;
- const updatedReq = await this.cryptoStore.updateOutgoingRoomKeyRequest(
- req.requestId,
- RoomKeyRequestState.Sent,
- {
- state,
- cancellationTxnId: this.baseApis.makeTxnId(),
- // need to use a new transaction ID so that
- // the request gets sent
- requestTxnId: this.baseApis.makeTxnId(),
- },
- );
- if (!updatedReq) {
- // updateOutgoingRoomKeyRequest couldn't find the request
- // 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 this.queueRoomKeyRequest(requestBody, recipients, resend);
- }
-
- // We don't want to wait for the timer, so we send it
- // immediately. (We might actually end up racing with the timer,
- // but that's ok: even if we make the request twice, we'll do it
- // with the same transaction_id, so only one message will get
- // sent).
- //
- // (We also don't want to wait for the response from the server
- // here, as it will slow down processing of received keys if we
- // do.)
- try {
- await this.sendOutgoingRoomKeyRequestCancellation(updatedReq, true);
- } catch (e) {
- logger.error("Error sending room key request cancellation;" + " will retry later.", e);
- }
- // The request has transitioned from
- // CANCELLATION_PENDING_AND_WILL_RESEND to UNSENT. We
- // still need to resend the request which is now UNSENT, so
- // start the timer if it isn't already started.
- }
- break;
- }
- default:
- throw new Error("unhandled state: " + req.state);
- }
- }
- }
-
- /**
- * Cancel room key requests, if any match the given requestBody
- *
- *
- * @returns resolves when the request has been updated in our
- * pending list.
- */
- public cancelRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<unknown> {
- return this.cryptoStore.getOutgoingRoomKeyRequest(requestBody).then((req): unknown => {
- if (!req) {
- // no request was made for this key
- return;
- }
- switch (req.state) {
- case RoomKeyRequestState.CancellationPending:
- case RoomKeyRequestState.CancellationPendingAndWillResend:
- // nothing to do here
- return;
-
- case RoomKeyRequestState.Unsent:
- // just delete it
-
- // FIXME: ghahah we may have attempted to send it, and
- // not yet got a successful response. So the server
- // may have seen it, so we still need to send a cancellation
- // in that case :/
-
- logger.log("deleting unnecessary room key request for " + stringifyRequestBody(requestBody));
- return this.cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId, RoomKeyRequestState.Unsent);
-
- case RoomKeyRequestState.Sent: {
- // send a cancellation.
- return this.cryptoStore
- .updateOutgoingRoomKeyRequest(req.requestId, RoomKeyRequestState.Sent, {
- state: RoomKeyRequestState.CancellationPending,
- cancellationTxnId: this.baseApis.makeTxnId(),
- })
- .then((updatedReq) => {
- if (!updatedReq) {
- // updateOutgoingRoomKeyRequest couldn't find the
- // request in state ROOM_KEY_REQUEST_STATES.SENT,
- // so we must have raced with another tab to mark
- // the request cancelled. There is no point in
- // sending another cancellation since the other tab
- // will do it.
- logger.log(
- "Tried to cancel room key request for " +
- stringifyRequestBody(requestBody) +
- " but it was already cancelled in another tab",
- );
- return;
- }
-
- // We don't want to wait for the timer, so we send it
- // immediately. (We might actually end up racing with the timer,
- // but that's ok: even if we make the request twice, we'll do it
- // with the same transaction_id, so only one message will get
- // sent).
- //
- // (We also don't want to wait for the response from the server
- // here, as it will slow down processing of received keys if we
- // do.)
- this.sendOutgoingRoomKeyRequestCancellation(updatedReq).catch((e) => {
- logger.error("Error sending room key request cancellation;" + " will retry later.", e);
- this.startTimer();
- });
- });
- }
- default:
- throw new Error("unhandled state: " + req.state);
- }
- });
- }
-
- /**
- * Look for room key requests by target device and state
- *
- * @param userId - Target user ID
- * @param deviceId - Target device ID
- *
- * @returns resolves to a list of all the {@link OutgoingRoomKeyRequest}
- */
- public getOutgoingSentRoomKeyRequest(userId: string, deviceId: string): Promise<OutgoingRoomKeyRequest[]> {
- return this.cryptoStore.getOutgoingRoomKeyRequestsByTarget(userId, deviceId, [RoomKeyRequestState.Sent]);
- }
-
- /**
- * Find anything in `sent` state, and kick it around the loop again.
- * This is intended for situations where something substantial has changed, and we
- * don't really expect the other end to even care about the cancellation.
- * For example, after initialization or self-verification.
- * @returns An array of `queueRoomKeyRequest` outputs.
- */
- public async cancelAndResendAllOutgoingRequests(): Promise<void[]> {
- const outgoings = await this.cryptoStore.getAllOutgoingRoomKeyRequestsByState(RoomKeyRequestState.Sent);
- return Promise.all(
- outgoings.map(({ requestBody, recipients }) => this.queueRoomKeyRequest(requestBody, recipients, true)),
- );
- }
-
- // start the background timer to send queued requests, if the timer isn't
- // already running
- private startTimer(): void {
- if (this.sendOutgoingRoomKeyRequestsTimer) {
- return;
- }
-
- const startSendingOutgoingRoomKeyRequests = (): void => {
- if (this.sendOutgoingRoomKeyRequestsRunning) {
- throw new Error("RoomKeyRequestSend already in progress!");
- }
- this.sendOutgoingRoomKeyRequestsRunning = true;
-
- this.sendOutgoingRoomKeyRequests()
- .finally(() => {
- this.sendOutgoingRoomKeyRequestsRunning = false;
- })
- .catch((e) => {
- // this should only happen if there is an indexeddb error,
- // in which case we're a bit stuffed anyway.
- logger.warn(`error in OutgoingRoomKeyRequestManager: ${e}`);
- });
- };
-
- this.sendOutgoingRoomKeyRequestsTimer = setTimeout(
- startSendingOutgoingRoomKeyRequests,
- SEND_KEY_REQUESTS_DELAY_MS,
- );
- }
-
- // look for and send any queued requests. Runs itself recursively until
- // there are no more requests, or there is an error (in which case, the
- // timer will be restarted before the promise resolves).
- private async sendOutgoingRoomKeyRequests(): Promise<void> {
- if (!this.clientRunning) {
- this.sendOutgoingRoomKeyRequestsTimer = undefined;
- return;
- }
-
- const req = await this.cryptoStore.getOutgoingRoomKeyRequestByState([
- RoomKeyRequestState.CancellationPending,
- RoomKeyRequestState.CancellationPendingAndWillResend,
- RoomKeyRequestState.Unsent,
- ]);
-
- if (!req) {
- this.sendOutgoingRoomKeyRequestsTimer = undefined;
- return;
- }
-
- try {
- switch (req.state) {
- case RoomKeyRequestState.Unsent:
- await this.sendOutgoingRoomKeyRequest(req);
- break;
- case RoomKeyRequestState.CancellationPending:
- await this.sendOutgoingRoomKeyRequestCancellation(req);
- break;
- case RoomKeyRequestState.CancellationPendingAndWillResend:
- await this.sendOutgoingRoomKeyRequestCancellation(req, true);
- break;
- }
-
- // go around the loop again
- return this.sendOutgoingRoomKeyRequests();
- } catch (e) {
- logger.error("Error sending room key request; will retry later.", e);
- this.sendOutgoingRoomKeyRequestsTimer = undefined;
- }
- }
-
- // given a RoomKeyRequest, send it and update the request record
- private sendOutgoingRoomKeyRequest(req: OutgoingRoomKeyRequest): Promise<unknown> {
- logger.log(
- `Requesting keys for ${stringifyRequestBody(req.requestBody)}` +
- ` from ${stringifyRecipientList(req.recipients)}` +
- `(id ${req.requestId})`,
- );
-
- const requestMessage: RequestMessage = {
- action: "request",
- requesting_device_id: this.deviceId,
- request_id: req.requestId,
- body: req.requestBody,
- };
-
- return this.sendMessageToDevices(requestMessage, req.recipients, req.requestTxnId || req.requestId).then(() => {
- return this.cryptoStore.updateOutgoingRoomKeyRequest(req.requestId, RoomKeyRequestState.Unsent, {
- state: RoomKeyRequestState.Sent,
- });
- });
- }
-
- // Given a RoomKeyRequest, cancel it and delete the request record unless
- // andResend is set, in which case transition to UNSENT.
- private sendOutgoingRoomKeyRequestCancellation(req: OutgoingRoomKeyRequest, andResend = false): Promise<unknown> {
- logger.log(
- `Sending cancellation for key request for ` +
- `${stringifyRequestBody(req.requestBody)} to ` +
- `${stringifyRecipientList(req.recipients)} ` +
- `(cancellation id ${req.cancellationTxnId})`,
- );
-
- const requestMessage: RequestMessage = {
- action: "request_cancellation",
- requesting_device_id: this.deviceId,
- request_id: req.requestId,
- };
-
- return this.sendMessageToDevices(requestMessage, req.recipients, req.cancellationTxnId).then(() => {
- if (andResend) {
- // We want to resend, so transition to UNSENT
- return this.cryptoStore.updateOutgoingRoomKeyRequest(
- req.requestId,
- RoomKeyRequestState.CancellationPendingAndWillResend,
- { state: RoomKeyRequestState.Unsent },
- );
- }
- return this.cryptoStore.deleteOutgoingRoomKeyRequest(
- req.requestId,
- RoomKeyRequestState.CancellationPending,
- );
- });
- }
-
- // send a RoomKeyRequest to a list of recipients
- private sendMessageToDevices(
- message: RequestMessage,
- recipients: IRoomKeyRequestRecipient[],
- txnId?: string,
- ): Promise<{}> {
- const contentMap = new MapWithDefault<string, Map<string, Record<string, any>>>(() => new Map());
- for (const recip of recipients) {
- const userDeviceMap = contentMap.getOrCreate(recip.userId);
- userDeviceMap.set(recip.deviceId, {
- ...message,
- [ToDeviceMessageId]: uuidv4(),
- });
- }
-
- return this.baseApis.sendToDevice(EventType.RoomKeyRequest, contentMap, txnId);
- }
-}
-
-function stringifyRequestBody(requestBody: IRoomKeyRequestBody): string {
- // we assume that the request is for megolm keys, which are identified by
- // room id and session id
- return requestBody.room_id + " / " + requestBody.session_id;
-}
-
-function stringifyRecipientList(recipients: IRoomKeyRequestRecipient[]): string {
- return `[${recipients.map((r) => `${r.userId}:${r.deviceId}`).join(",")}]`;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/RoomList.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/RoomList.ts
deleted file mode 100644
index a73efcd..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/RoomList.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
-Copyright 2018 - 2021 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.
-*/
-
-/**
- * Manages the list of encrypted rooms
- */
-
-import { CryptoStore } from "./store/base";
-import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store";
-
-/* eslint-disable camelcase */
-export interface IRoomEncryption {
- algorithm: string;
- rotation_period_ms?: number;
- rotation_period_msgs?: number;
-}
-/* eslint-enable camelcase */
-
-export class RoomList {
- // Object of roomId -> room e2e info object (body of the m.room.encryption event)
- private roomEncryption: Record<string, IRoomEncryption> = {};
-
- public constructor(private readonly cryptoStore?: CryptoStore) {}
-
- public async init(): Promise<void> {
- await this.cryptoStore!.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
- this.cryptoStore!.getEndToEndRooms(txn, (result) => {
- this.roomEncryption = result;
- });
- });
- }
-
- public getRoomEncryption(roomId: string): IRoomEncryption {
- return this.roomEncryption[roomId] || null;
- }
-
- public isRoomEncrypted(roomId: string): boolean {
- return Boolean(this.getRoomEncryption(roomId));
- }
-
- public async setRoomEncryption(roomId: string, roomInfo: IRoomEncryption): Promise<void> {
- // important that this happens before calling into the store
- // as it prevents the Crypto::setRoomEncryption from calling
- // this twice for consecutive m.room.encryption events
- this.roomEncryption[roomId] = roomInfo;
- await this.cryptoStore!.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
- this.cryptoStore!.storeEndToEndRoom(roomId, roomInfo, txn);
- });
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/SecretStorage.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/SecretStorage.ts
deleted file mode 100644
index 5c9049f..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/SecretStorage.ts
+++ /dev/null
@@ -1,583 +0,0 @@
-/*
-Copyright 2019 - 2021 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 { v4 as uuidv4 } from "uuid";
-
-import { logger } from "../logger";
-import * as olmlib from "./olmlib";
-import { randomString } from "../randomstring";
-import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./aes";
-import { ICryptoCallbacks, IEncryptedContent } from ".";
-import { IContent, MatrixEvent } from "../models/event";
-import { ClientEvent, ClientEventHandlerMap, MatrixClient } from "../client";
-import { IAddSecretStorageKeyOpts } from "./api";
-import { TypedEventEmitter } from "../models/typed-event-emitter";
-import { defer, IDeferred } from "../utils";
-import { ToDeviceMessageId } from "../@types/event";
-import { SecretStorageKeyDescription, SecretStorageKeyDescriptionAesV1 } from "../secret-storage";
-
-export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2";
-
-// Some of the key functions use a tuple and some use an object...
-export type SecretStorageKeyTuple = [keyId: string, keyInfo: SecretStorageKeyDescription];
-export type SecretStorageKeyObject = { keyId: string; keyInfo: SecretStorageKeyDescription };
-
-export interface ISecretRequest {
- requestId: string;
- promise: Promise<string>;
- cancel: (reason: string) => void;
-}
-
-export interface IAccountDataClient extends TypedEventEmitter<ClientEvent.AccountData, ClientEventHandlerMap> {
- // Subset of MatrixClient (which also uses any for the event content)
- getAccountDataFromServer: <T extends { [k: string]: any }>(eventType: string) => Promise<T>;
- getAccountData: (eventType: string) => IContent | null;
- setAccountData: (eventType: string, content: any) => Promise<{}>;
-}
-
-interface ISecretRequestInternal {
- name: string;
- devices: string[];
- deferred: IDeferred<string>;
-}
-
-interface IDecryptors {
- encrypt: (plaintext: string) => Promise<IEncryptedPayload>;
- decrypt: (ciphertext: IEncryptedPayload) => Promise<string>;
-}
-
-interface ISecretInfo {
- encrypted: {
- [keyId: string]: IEncryptedPayload;
- };
-}
-
-/**
- * Implements Secure Secret Storage and Sharing (MSC1946)
- */
-export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
- private requests = new Map<string, ISecretRequestInternal>();
-
- // In it's pure javascript days, this was relying on some proper Javascript-style
- // type-abuse where sometimes we'd pass in a fake client object with just the account
- // data methods implemented, which is all this class needs unless you use the secret
- // sharing code, so it was fine. As a low-touch TypeScript migration, this now has
- // an extra, optional param for a real matrix client, so you can not pass it as long
- // as you don't request any secrets.
- // A better solution would probably be to split this class up into secret storage and
- // secret sharing which are really two separate things, even though they share an MSC.
- public constructor(
- private readonly accountDataAdapter: IAccountDataClient,
- private readonly cryptoCallbacks: ICryptoCallbacks,
- private readonly baseApis: B,
- ) {}
-
- public async getDefaultKeyId(): Promise<string | null> {
- const defaultKey = await this.accountDataAdapter.getAccountDataFromServer<{ key: string }>(
- "m.secret_storage.default_key",
- );
- if (!defaultKey) return null;
- return defaultKey.key;
- }
-
- public setDefaultKeyId(keyId: string): Promise<void> {
- return new Promise<void>((resolve, reject) => {
- const listener = (ev: MatrixEvent): void => {
- if (ev.getType() === "m.secret_storage.default_key" && ev.getContent().key === keyId) {
- this.accountDataAdapter.removeListener(ClientEvent.AccountData, listener);
- resolve();
- }
- };
- this.accountDataAdapter.on(ClientEvent.AccountData, listener);
-
- this.accountDataAdapter.setAccountData("m.secret_storage.default_key", { key: keyId }).catch((e) => {
- this.accountDataAdapter.removeListener(ClientEvent.AccountData, listener);
- reject(e);
- });
- });
- }
-
- /**
- * Add a key for encrypting secrets.
- *
- * @param algorithm - the algorithm used by the key.
- * @param opts - the options for the algorithm. The properties used
- * depend on the algorithm given.
- * @param keyId - the ID of the key. If not given, a random
- * ID will be generated.
- *
- * @returns An object with:
- * keyId: the ID of the key
- * keyInfo: details about the key (iv, mac, passphrase)
- */
- public async addKey(
- algorithm: string,
- opts: IAddSecretStorageKeyOpts = {},
- keyId?: string,
- ): Promise<SecretStorageKeyObject> {
- if (algorithm !== SECRET_STORAGE_ALGORITHM_V1_AES) {
- throw new Error(`Unknown key algorithm ${algorithm}`);
- }
-
- const keyInfo = { algorithm } as SecretStorageKeyDescriptionAesV1;
-
- if (opts.name) {
- keyInfo.name = opts.name;
- }
-
- if (opts.passphrase) {
- keyInfo.passphrase = opts.passphrase;
- }
- if (opts.key) {
- const { iv, mac } = await calculateKeyCheck(opts.key);
- keyInfo.iv = iv;
- keyInfo.mac = mac;
- }
-
- if (!keyId) {
- do {
- keyId = randomString(32);
- } while (
- await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescription>(
- `m.secret_storage.key.${keyId}`,
- )
- );
- }
-
- await this.accountDataAdapter.setAccountData(`m.secret_storage.key.${keyId}`, keyInfo);
-
- return {
- keyId,
- keyInfo,
- };
- }
-
- /**
- * Get the key information for a given ID.
- *
- * @param keyId - The ID of the key to check
- * for. Defaults to the default key ID if not provided.
- * @returns If the key was found, the return value is an array of
- * the form [keyId, keyInfo]. Otherwise, null is returned.
- * XXX: why is this an array when addKey returns an object?
- */
- public async getKey(keyId?: string | null): Promise<SecretStorageKeyTuple | null> {
- if (!keyId) {
- keyId = await this.getDefaultKeyId();
- }
- if (!keyId) {
- return null;
- }
-
- const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescription>(
- "m.secret_storage.key." + keyId,
- );
- return keyInfo ? [keyId, keyInfo] : null;
- }
-
- /**
- * Check whether we have a key with a given ID.
- *
- * @param keyId - The ID of the key to check
- * for. Defaults to the default key ID if not provided.
- * @returns Whether we have the key.
- */
- public async hasKey(keyId?: string): Promise<boolean> {
- return Boolean(await this.getKey(keyId));
- }
-
- /**
- * Check whether a key matches what we expect based on the key info
- *
- * @param key - the key to check
- * @param info - the key info
- *
- * @returns whether or not the key matches
- */
- public async checkKey(key: Uint8Array, info: SecretStorageKeyDescription): Promise<boolean> {
- if (info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
- if (info.mac) {
- const { mac } = await calculateKeyCheck(key, info.iv);
- return info.mac.replace(/=+$/g, "") === mac.replace(/=+$/g, "");
- } else {
- // if we have no information, we have to assume the key is right
- return true;
- }
- } else {
- throw new Error("Unknown algorithm");
- }
- }
-
- /**
- * Store an encrypted secret on the server
- *
- * @param name - The name of the secret
- * @param secret - The secret contents.
- * @param keys - The IDs of the keys to use to encrypt the secret
- * or null/undefined to use the default key.
- */
- public async store(name: string, secret: string, keys?: string[] | null): Promise<void> {
- const encrypted: Record<string, IEncryptedPayload> = {};
-
- if (!keys) {
- const defaultKeyId = await this.getDefaultKeyId();
- if (!defaultKeyId) {
- throw new Error("No keys specified and no default key present");
- }
- keys = [defaultKeyId];
- }
-
- if (keys.length === 0) {
- throw new Error("Zero keys given to encrypt with!");
- }
-
- for (const keyId of keys) {
- // get key information from key storage
- const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescription>(
- "m.secret_storage.key." + keyId,
- );
- if (!keyInfo) {
- throw new Error("Unknown key: " + keyId);
- }
-
- // encrypt secret, based on the algorithm
- if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
- const keys = { [keyId]: keyInfo };
- const [, encryption] = await this.getSecretStorageKey(keys, name);
- encrypted[keyId] = await encryption.encrypt(secret);
- } else {
- logger.warn("unknown algorithm for secret storage key " + keyId + ": " + keyInfo.algorithm);
- // do nothing if we don't understand the encryption algorithm
- }
- }
-
- // save encrypted secret
- await this.accountDataAdapter.setAccountData(name, { encrypted });
- }
-
- /**
- * Get a secret from storage.
- *
- * @param name - the name of the secret
- *
- * @returns the contents of the secret
- */
- public async get(name: string): Promise<string | undefined> {
- const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name);
- if (!secretInfo) {
- return;
- }
- if (!secretInfo.encrypted) {
- throw new Error("Content is not encrypted!");
- }
-
- // get possible keys to decrypt
- const keys: Record<string, SecretStorageKeyDescription> = {};
- for (const keyId of Object.keys(secretInfo.encrypted)) {
- // get key information from key storage
- const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescription>(
- "m.secret_storage.key." + keyId,
- );
- const encInfo = secretInfo.encrypted[keyId];
- // only use keys we understand the encryption algorithm of
- if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
- if (encInfo.iv && encInfo.ciphertext && encInfo.mac) {
- keys[keyId] = keyInfo;
- }
- }
- }
-
- if (Object.keys(keys).length === 0) {
- throw new Error(
- `Could not decrypt ${name} because none of ` +
- `the keys it is encrypted with are for a supported algorithm`,
- );
- }
-
- // fetch private key from app
- const [keyId, decryption] = await this.getSecretStorageKey(keys, name);
- const encInfo = secretInfo.encrypted[keyId];
-
- return decryption.decrypt(encInfo);
- }
-
- /**
- * Check if a secret is stored on the server.
- *
- * @param name - the name of the secret
- *
- * @returns 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): Promise<Record<string, SecretStorageKeyDescription> | null> {
- // check if secret exists
- const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name);
- if (!secretInfo?.encrypted) return null;
-
- const ret: Record<string, SecretStorageKeyDescription> = {};
-
- // filter secret encryption keys with supported algorithm
- for (const keyId of Object.keys(secretInfo.encrypted)) {
- // get key information from key storage
- const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescription>(
- "m.secret_storage.key." + keyId,
- );
- if (!keyInfo) continue;
- const encInfo = secretInfo.encrypted[keyId];
-
- // only use keys we understand the encryption algorithm of
- if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
- if (encInfo.iv && encInfo.ciphertext && encInfo.mac) {
- ret[keyId] = keyInfo;
- }
- }
- }
- return Object.keys(ret).length ? ret : null;
- }
-
- /**
- * Request a secret from another device
- *
- * @param name - the name of the secret to request
- * @param devices - the devices to request the secret from
- */
- public request(this: SecretStorage<MatrixClient>, name: string, devices: string[]): ISecretRequest {
- const requestId = this.baseApis.makeTxnId();
-
- const deferred = defer<string>();
- this.requests.set(requestId, { name, devices, deferred });
-
- const cancel = (reason: string): void => {
- // send cancellation event
- const cancelData = {
- action: "request_cancellation",
- requesting_device_id: this.baseApis.deviceId,
- request_id: requestId,
- };
- const toDevice: Map<string, typeof cancelData> = new Map();
- for (const device of devices) {
- toDevice.set(device, cancelData);
- }
- this.baseApis.sendToDevice("m.secret.request", new Map([[this.baseApis.getUserId()!, toDevice]]));
-
- // and reject the promise so that anyone waiting on it will be
- // notified
- deferred.reject(new Error(reason || "Cancelled"));
- };
-
- // send request to devices
- const requestData = {
- name,
- action: "request",
- requesting_device_id: this.baseApis.deviceId,
- request_id: requestId,
- [ToDeviceMessageId]: uuidv4(),
- };
- const toDevice: Map<string, typeof requestData> = new Map();
- for (const device of devices) {
- toDevice.set(device, requestData);
- }
- logger.info(`Request secret ${name} from ${devices}, id ${requestId}`);
- this.baseApis.sendToDevice("m.secret.request", new Map([[this.baseApis.getUserId()!, toDevice]]));
-
- return {
- requestId,
- promise: deferred.promise,
- cancel,
- };
- }
-
- public async onRequestReceived(this: SecretStorage<MatrixClient>, event: MatrixEvent): Promise<void> {
- const sender = event.getSender();
- const content = event.getContent();
- if (
- sender !== this.baseApis.getUserId() ||
- !(content.name && content.action && content.requesting_device_id && content.request_id)
- ) {
- // ignore requests from anyone else, for now
- return;
- }
- const deviceId = content.requesting_device_id;
- // check if it's a cancel
- if (content.action === "request_cancellation") {
- /*
- Looks like we intended to emit events when we got cancelations, but
- we never put anything in the _incomingRequests object, and the request
- itself doesn't use events anyway so if we were to wire up cancellations,
- they probably ought to use the same callback interface. I'm leaving them
- disabled for now while converting this file to typescript.
- if (this._incomingRequests[deviceId]
- && this._incomingRequests[deviceId][content.request_id]) {
- logger.info(
- "received request cancellation for secret (" + sender +
- ", " + deviceId + ", " + content.request_id + ")",
- );
- this.baseApis.emit("crypto.secrets.requestCancelled", {
- user_id: sender,
- device_id: deviceId,
- request_id: content.request_id,
- });
- }
- */
- } else if (content.action === "request") {
- if (deviceId === this.baseApis.deviceId) {
- // no point in trying to send ourself the secret
- return;
- }
-
- // check if we have the secret
- logger.info("received request for secret (" + sender + ", " + deviceId + ", " + content.request_id + ")");
- if (!this.cryptoCallbacks.onSecretRequested) {
- return;
- }
- const secret = await this.cryptoCallbacks.onSecretRequested(
- sender,
- deviceId,
- content.request_id,
- content.name,
- this.baseApis.checkDeviceTrust(sender, deviceId),
- );
- if (secret) {
- logger.info(`Preparing ${content.name} secret for ${deviceId}`);
- const payload = {
- type: "m.secret.send",
- content: {
- request_id: content.request_id,
- secret: secret,
- },
- };
- const encryptedContent: IEncryptedContent = {
- algorithm: olmlib.OLM_ALGORITHM,
- sender_key: this.baseApis.crypto!.olmDevice.deviceCurve25519Key!,
- ciphertext: {},
- [ToDeviceMessageId]: uuidv4(),
- };
- await olmlib.ensureOlmSessionsForDevices(
- this.baseApis.crypto!.olmDevice,
- this.baseApis,
- new Map([[sender, [this.baseApis.getStoredDevice(sender, deviceId)!]]]),
- );
- await olmlib.encryptMessageForDevice(
- encryptedContent.ciphertext,
- this.baseApis.getUserId()!,
- this.baseApis.deviceId!,
- this.baseApis.crypto!.olmDevice,
- sender,
- this.baseApis.getStoredDevice(sender, deviceId)!,
- payload,
- );
- const contentMap = new Map([[sender, new Map([[deviceId, encryptedContent]])]]);
-
- logger.info(`Sending ${content.name} secret for ${deviceId}`);
- this.baseApis.sendToDevice("m.room.encrypted", contentMap);
- } else {
- logger.info(`Request denied for ${content.name} secret for ${deviceId}`);
- }
- }
- }
-
- public onSecretReceived(this: SecretStorage<MatrixClient>, event: MatrixEvent): void {
- if (event.getSender() !== this.baseApis.getUserId()) {
- // we shouldn't be receiving secrets from anyone else, so ignore
- // because someone could be trying to send us bogus data
- return;
- }
-
- if (!olmlib.isOlmEncrypted(event)) {
- logger.error("secret event not properly encrypted");
- return;
- }
-
- const content = event.getContent();
-
- const senderKeyUser = this.baseApis.crypto!.deviceList.getUserByIdentityKey(
- olmlib.OLM_ALGORITHM,
- event.getSenderKey() || "",
- );
- if (senderKeyUser !== event.getSender()) {
- logger.error("sending device does not belong to the user it claims to be from");
- return;
- }
-
- logger.log("got secret share for request", content.request_id);
- const requestControl = this.requests.get(content.request_id);
- if (requestControl) {
- // make sure that the device that sent it is one of the devices that
- // we requested from
- const deviceInfo = this.baseApis.crypto!.deviceList.getDeviceByIdentityKey(
- olmlib.OLM_ALGORITHM,
- event.getSenderKey()!,
- );
- if (!deviceInfo) {
- logger.log("secret share from unknown device with key", event.getSenderKey());
- return;
- }
- if (!requestControl.devices.includes(deviceInfo.deviceId)) {
- logger.log("unsolicited secret share from device", deviceInfo.deviceId);
- return;
- }
- // unsure that the sender is trusted. In theory, this check is
- // unnecessary since we only accept secret shares from devices that
- // we requested from, but it doesn't hurt.
- const deviceTrust = this.baseApis.crypto!.checkDeviceInfoTrust(event.getSender()!, deviceInfo);
- if (!deviceTrust.isVerified()) {
- logger.log("secret share from unverified device");
- return;
- }
-
- logger.log(`Successfully received secret ${requestControl.name} ` + `from ${deviceInfo.deviceId}`);
- requestControl.deferred.resolve(content.secret);
- }
- }
-
- private async getSecretStorageKey(
- keys: Record<string, SecretStorageKeyDescription>,
- name: string,
- ): Promise<[string, IDecryptors]> {
- if (!this.cryptoCallbacks.getSecretStorageKey) {
- throw new Error("No getSecretStorageKey callback supplied");
- }
-
- const returned = await this.cryptoCallbacks.getSecretStorageKey({ keys }, name);
-
- if (!returned) {
- throw new Error("getSecretStorageKey callback returned falsey");
- }
- if (returned.length < 2) {
- throw new Error("getSecretStorageKey callback returned invalid data");
- }
-
- const [keyId, privateKey] = returned;
- if (!keys[keyId]) {
- throw new Error("App returned unknown key from getSecretStorageKey!");
- }
-
- if (keys[keyId].algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
- const decryption = {
- encrypt: function (secret: string): Promise<IEncryptedPayload> {
- return encryptAES(secret, privateKey, name);
- },
- decrypt: function (encInfo: IEncryptedPayload): Promise<string> {
- return decryptAES(encInfo, privateKey, name);
- },
- };
- return [keyId, decryption];
- } else {
- throw new Error("Unknown key type: " + keys[keyId].algorithm);
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/aes.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/aes.ts
deleted file mode 100644
index 48470af..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/aes.ts
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
-Copyright 2020 - 2021 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 { decodeBase64, encodeBase64 } from "./olmlib";
-import { subtleCrypto, crypto, TextEncoder } from "./crypto";
-
-// salt for HKDF, with 8 bytes of zeros
-const zeroSalt = new Uint8Array(8);
-
-export interface IEncryptedPayload {
- [key: string]: any; // extensible
- /** the initialization vector in base64 */
- iv: string;
- /** the ciphertext in base64 */
- ciphertext: string;
- /** the HMAC in base64 */
- mac: string;
-}
-
-/**
- * encrypt a string
- *
- * @param data - the plaintext to encrypt
- * @param key - the encryption key to use
- * @param name - the name of the secret
- * @param ivStr - the initialization vector to use
- */
-export async function encryptAES(
- data: string,
- key: Uint8Array,
- name: string,
- ivStr?: string,
-): Promise<IEncryptedPayload> {
- let iv: Uint8Array;
- if (ivStr) {
- iv = decodeBase64(ivStr);
- } else {
- iv = new Uint8Array(16);
- crypto.getRandomValues(iv);
-
- // clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
- // (which would mean we wouldn't be able to decrypt on Android). The loss
- // of a single bit of iv is a price we have to pay.
- iv[8] &= 0x7f;
- }
-
- const [aesKey, hmacKey] = await deriveKeys(key, name);
- const encodedData = new TextEncoder().encode(data);
-
- const ciphertext = await subtleCrypto.encrypt(
- {
- name: "AES-CTR",
- counter: iv,
- length: 64,
- },
- aesKey,
- encodedData,
- );
-
- const hmac = await subtleCrypto.sign({ name: "HMAC" }, hmacKey, ciphertext);
-
- return {
- iv: encodeBase64(iv),
- ciphertext: encodeBase64(ciphertext),
- mac: encodeBase64(hmac),
- };
-}
-
-/**
- * decrypt a string
- *
- * @param data - the encrypted data
- * @param key - the encryption key to use
- * @param name - the name of the secret
- */
-export async function decryptAES(data: IEncryptedPayload, key: Uint8Array, name: string): Promise<string> {
- const [aesKey, hmacKey] = await deriveKeys(key, name);
-
- const ciphertext = decodeBase64(data.ciphertext);
-
- if (!(await subtleCrypto.verify({ name: "HMAC" }, hmacKey, decodeBase64(data.mac), ciphertext))) {
- throw new Error(`Error decrypting secret ${name}: bad MAC`);
- }
-
- const plaintext = await subtleCrypto.decrypt(
- {
- name: "AES-CTR",
- counter: decodeBase64(data.iv),
- length: 64,
- },
- aesKey,
- ciphertext,
- );
-
- return new TextDecoder().decode(new Uint8Array(plaintext));
-}
-
-async function deriveKeys(key: Uint8Array, name: string): Promise<[CryptoKey, CryptoKey]> {
- const hkdfkey = await subtleCrypto.importKey("raw", key, { name: "HKDF" }, false, ["deriveBits"]);
- const keybits = await subtleCrypto.deriveBits(
- {
- name: "HKDF",
- salt: zeroSalt,
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore: https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/879
- info: new TextEncoder().encode(name),
- hash: "SHA-256",
- },
- hkdfkey,
- 512,
- );
-
- const aesKey = keybits.slice(0, 32);
- const hmacKey = keybits.slice(32);
-
- const aesProm = subtleCrypto.importKey("raw", aesKey, { name: "AES-CTR" }, false, ["encrypt", "decrypt"]);
-
- const hmacProm = subtleCrypto.importKey(
- "raw",
- hmacKey,
- {
- name: "HMAC",
- hash: { name: "SHA-256" },
- },
- false,
- ["sign", "verify"],
- );
-
- return Promise.all([aesProm, hmacProm]);
-}
-
-// string of zeroes, for calculating the key check
-const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
-
-/** Calculate the MAC for checking the key.
- *
- * @param key - the key to use
- * @param iv - The initialization vector as a base64-encoded string.
- * If omitted, a random initialization vector will be created.
- * @returns An object that contains, `mac` and `iv` properties.
- */
-export function calculateKeyCheck(key: Uint8Array, iv?: string): Promise<IEncryptedPayload> {
- return encryptAES(ZERO_STR, key, "", iv);
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/base.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/base.ts
deleted file mode 100644
index 6473009..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/base.ts
+++ /dev/null
@@ -1,268 +0,0 @@
-/*
-Copyright 2016 - 2021 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.
-*/
-
-/**
- * Internal module. Defines the base classes of the encryption implementations
- */
-
-import type { IMegolmSessionData } from "../../@types/crypto";
-import { MatrixClient } from "../../client";
-import { Room } from "../../models/room";
-import { OlmDevice } from "../OlmDevice";
-import { IContent, MatrixEvent, RoomMember } from "../../matrix";
-import { Crypto, IEncryptedContent, IEventDecryptionResult, IncomingRoomKeyRequest } from "..";
-import { DeviceInfo } from "../deviceinfo";
-import { IRoomEncryption } from "../RoomList";
-import { DeviceInfoMap } from "../DeviceList";
-
-/**
- * Map of registered encryption algorithm classes. A map from string to {@link EncryptionAlgorithm} class
- */
-export const ENCRYPTION_CLASSES = new Map<string, new (params: IParams) => EncryptionAlgorithm>();
-
-export type DecryptionClassParams<P extends IParams = IParams> = Omit<P, "deviceId" | "config">;
-
-/**
- * map of registered encryption algorithm classes. Map from string to {@link DecryptionAlgorithm} class
- */
-export const DECRYPTION_CLASSES = new Map<string, new (params: DecryptionClassParams) => DecryptionAlgorithm>();
-
-export interface IParams {
- /** The UserID for the local user */
- userId: string;
- /** The identifier for this device. */
- deviceId: string;
- /** crypto core */
- crypto: Crypto;
- /** olm.js wrapper */
- olmDevice: OlmDevice;
- /** base matrix api interface */
- baseApis: MatrixClient;
- /** The ID of the room we will be sending to */
- roomId?: string;
- /** The body of the m.room.encryption event */
- config: IRoomEncryption & object;
-}
-
-/**
- * base type for encryption implementations
- */
-export abstract class EncryptionAlgorithm {
- protected readonly userId: string;
- protected readonly deviceId: string;
- protected readonly crypto: Crypto;
- protected readonly olmDevice: OlmDevice;
- protected readonly baseApis: MatrixClient;
- protected readonly roomId?: string;
-
- /**
- * @param params - parameters
- */
- public constructor(params: IParams) {
- this.userId = params.userId;
- this.deviceId = params.deviceId;
- this.crypto = params.crypto;
- this.olmDevice = params.olmDevice;
- this.baseApis = params.baseApis;
- this.roomId = params.roomId;
- }
-
- /**
- * Perform any background tasks that can be done before a message is ready to
- * send, in order to speed up sending of the message.
- *
- * @param room - the room the event is in
- */
- public prepareToEncrypt(room: Room): void {}
-
- /**
- * Encrypt a message event
- *
- * @public
- *
- * @param content - event content
- *
- * @returns Promise which resolves to the new event body
- */
- public abstract encryptMessage(room: Room, eventType: string, content: IContent): Promise<IEncryptedContent>;
-
- /**
- * Called when the membership of a member of the room changes.
- *
- * @param event - event causing the change
- * @param member - user whose membership changed
- * @param oldMembership - previous membership
- * @public
- */
- public onRoomMembership(event: MatrixEvent, member: RoomMember, oldMembership?: string): void {}
-
- public reshareKeyWithDevice?(
- senderKey: string,
- sessionId: string,
- userId: string,
- device: DeviceInfo,
- ): Promise<void>;
-
- public forceDiscardSession?(): void;
-}
-
-/**
- * base type for decryption implementations
- */
-export abstract class DecryptionAlgorithm {
- protected readonly userId: string;
- protected readonly crypto: Crypto;
- protected readonly olmDevice: OlmDevice;
- protected readonly baseApis: MatrixClient;
- protected readonly roomId?: string;
-
- public constructor(params: DecryptionClassParams) {
- this.userId = params.userId;
- this.crypto = params.crypto;
- this.olmDevice = params.olmDevice;
- this.baseApis = params.baseApis;
- this.roomId = params.roomId;
- }
-
- /**
- * Decrypt an event
- *
- * @param event - undecrypted event
- *
- * @returns promise which
- * resolves once we have finished decrypting. Rejects with an
- * `algorithms.DecryptionError` if there is a problem decrypting the event.
- */
- public abstract decryptEvent(event: MatrixEvent): Promise<IEventDecryptionResult>;
-
- /**
- * Handle a key event
- *
- * @param params - event key event
- */
- public async onRoomKeyEvent(params: MatrixEvent): Promise<void> {
- // ignore by default
- }
-
- /**
- * Import a room key
- *
- * @param opts - object
- */
- public async importRoomKey(session: IMegolmSessionData, opts: object): Promise<void> {
- // ignore by default
- }
-
- /**
- * Determine if we have the keys necessary to respond to a room key request
- *
- * @returns true if we have the keys and could (theoretically) share
- * them; else false.
- */
- public hasKeysForKeyRequest(keyRequest: IncomingRoomKeyRequest): Promise<boolean> {
- return Promise.resolve(false);
- }
-
- /**
- * Send the response to a room key request
- *
- */
- public shareKeysWithDevice(keyRequest: IncomingRoomKeyRequest): void {
- throw new Error("shareKeysWithDevice not supported for this DecryptionAlgorithm");
- }
-
- /**
- * Retry decrypting all the events from a sender that haven't been
- * decrypted yet.
- *
- * @param senderKey - the sender's key
- */
- public async retryDecryptionFromSender(senderKey: string): Promise<boolean> {
- // ignore by default
- return false;
- }
-
- public onRoomKeyWithheldEvent?(event: MatrixEvent): Promise<void>;
- public sendSharedHistoryInboundSessions?(devicesByUser: Map<string, DeviceInfo[]>): Promise<void>;
-}
-
-/**
- * Exception thrown when decryption fails
- *
- * @param msg - user-visible message describing the problem
- *
- * @param details - key/value pairs reported in the logs but not shown
- * to the user.
- */
-export class DecryptionError extends Error {
- public readonly detailedString: string;
-
- public constructor(public readonly code: string, msg: string, details?: Record<string, string | Error>) {
- super(msg);
- this.code = code;
- this.name = "DecryptionError";
- this.detailedString = detailedStringForDecryptionError(this, details);
- }
-}
-
-function detailedStringForDecryptionError(err: DecryptionError, details?: Record<string, string | Error>): string {
- let result = err.name + "[msg: " + err.message;
-
- if (details) {
- result +=
- ", " +
- Object.keys(details)
- .map((k) => k + ": " + details[k])
- .join(", ");
- }
-
- result += "]";
-
- return result;
-}
-
-export class UnknownDeviceError extends Error {
- /**
- * Exception thrown specifically when we want to warn the user to consider
- * the security of their conversation before continuing
- *
- * @param msg - message describing the problem
- * @param devices - set of unknown devices per user we're warning about
- */
- public constructor(msg: string, public readonly devices: DeviceInfoMap, public event?: MatrixEvent) {
- super(msg);
- this.name = "UnknownDeviceError";
- this.devices = devices;
- }
-}
-
-/**
- * Registers an encryption/decryption class for a particular algorithm
- *
- * @param algorithm - algorithm tag to register for
- *
- * @param encryptor - {@link EncryptionAlgorithm} implementation
- *
- * @param decryptor - {@link DecryptionAlgorithm} implementation
- */
-export function registerAlgorithm<P extends IParams = IParams>(
- algorithm: string,
- encryptor: new (params: P) => EncryptionAlgorithm,
- decryptor: new (params: DecryptionClassParams<P>) => DecryptionAlgorithm,
-): void {
- ENCRYPTION_CLASSES.set(algorithm, encryptor as new (params: IParams) => EncryptionAlgorithm);
- DECRYPTION_CLASSES.set(algorithm, decryptor as new (params: DecryptionClassParams) => DecryptionAlgorithm);
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/index.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/index.ts
deleted file mode 100644
index b3c5b0e..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/index.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
-Copyright 2016 - 2021 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 "./olm";
-import "./megolm";
-
-export * from "./base";
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/megolm.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/megolm.ts
deleted file mode 100644
index 061e169..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/megolm.ts
+++ /dev/null
@@ -1,2208 +0,0 @@
-/*
-Copyright 2015 - 2021, 2023 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.
-*/
-
-/**
- * Defines m.olm encryption/decryption
- */
-
-import { v4 as uuidv4 } from "uuid";
-
-import type { IEventDecryptionResult, IMegolmSessionData } from "../../@types/crypto";
-import { logger, PrefixedLogger } from "../../logger";
-import * as olmlib from "../olmlib";
-import {
- DecryptionAlgorithm,
- DecryptionClassParams,
- DecryptionError,
- EncryptionAlgorithm,
- IParams,
- registerAlgorithm,
- UnknownDeviceError,
-} from "./base";
-import { IDecryptedGroupMessage, WITHHELD_MESSAGES } from "../OlmDevice";
-import { Room } from "../../models/room";
-import { DeviceInfo } from "../deviceinfo";
-import { IOlmSessionResult } from "../olmlib";
-import { DeviceInfoMap } from "../DeviceList";
-import { IContent, MatrixEvent } from "../../models/event";
-import { EventType, MsgType, ToDeviceMessageId } from "../../@types/event";
-import { IMegolmEncryptedContent, IncomingRoomKeyRequest, IEncryptedContent } from "../index";
-import { RoomKeyRequestState } from "../OutgoingRoomKeyRequestManager";
-import { OlmGroupSessionExtraData } from "../../@types/crypto";
-import { MatrixError } from "../../http-api";
-import { immediate, MapWithDefault } from "../../utils";
-
-// determine whether the key can be shared with invitees
-export function isRoomSharedHistory(room: Room): boolean {
- const visibilityEvent = room?.currentState?.getStateEvents("m.room.history_visibility", "");
- // NOTE: if the room visibility is unset, it would normally default to
- // "world_readable".
- // (https://spec.matrix.org/unstable/client-server-api/#server-behaviour-5)
- // But we will be paranoid here, and treat it as a situation where the room
- // is not shared-history
- const visibility = visibilityEvent?.getContent()?.history_visibility;
- return ["world_readable", "shared"].includes(visibility);
-}
-
-interface IBlockedDevice {
- code: string;
- reason: string;
- deviceInfo: DeviceInfo;
-}
-
-// map user Id → device Id → IBlockedDevice
-type BlockedMap = Map<string, Map<string, IBlockedDevice>>;
-
-export interface IOlmDevice<T = DeviceInfo> {
- userId: string;
- deviceInfo: T;
-}
-
-/**
- * Tests whether an encrypted content has a ciphertext.
- * Ciphertext can be a string or object depending on the content type {@link IEncryptedContent}.
- *
- * @param content - Encrypted content
- * @returns true: has ciphertext, else false
- */
-const hasCiphertext = (content: IEncryptedContent): boolean => {
- return typeof content.ciphertext === "string"
- ? !!content.ciphertext.length
- : !!Object.keys(content.ciphertext).length;
-};
-
-/** The result of parsing the an `m.room_key` or `m.forwarded_room_key` to-device event */
-interface RoomKey {
- /**
- * The Curve25519 key of the megolm session creator.
- *
- * For `m.room_key`, this is also the sender of the `m.room_key` to-device event.
- * For `m.forwarded_room_key`, the two are different (and the key of the sender of the
- * `m.forwarded_room_key` event is included in `forwardingKeyChain`)
- */
- senderKey: string;
- sessionId: string;
- sessionKey: string;
- exportFormat: boolean;
- roomId: string;
- algorithm: string;
- /**
- * A list of the curve25519 keys of the users involved in forwarding this key, most recent last.
- * For `m.room_key` events, this is empty.
- */
- forwardingKeyChain: string[];
- keysClaimed: Partial<Record<"ed25519", string>>;
- extraSessionData: OlmGroupSessionExtraData;
-}
-
-export interface IOutboundGroupSessionKey {
- chain_index: number;
- key: string;
-}
-
-interface IMessage {
- type: string;
- content: {
- "algorithm": string;
- "room_id": string;
- "sender_key"?: string;
- "sender_claimed_ed25519_key"?: string;
- "session_id": string;
- "session_key": string;
- "chain_index": number;
- "forwarding_curve25519_key_chain"?: string[];
- "org.matrix.msc3061.shared_history": boolean;
- };
-}
-
-interface IKeyForwardingMessage extends IMessage {
- type: "m.forwarded_room_key";
-}
-
-interface IPayload extends Partial<IMessage> {
- code?: string;
- reason?: string;
- room_id?: string;
- session_id?: string;
- algorithm?: string;
- sender_key?: string;
-}
-
-interface SharedWithData {
- // The identity key of the device we shared with
- deviceKey: string;
- // The message index of the ratchet we shared with that device
- messageIndex: number;
-}
-
-/**
- * @internal
- */
-class OutboundSessionInfo {
- /** number of times this session has been used */
- public useCount = 0;
- /** when the session was created (ms since the epoch) */
- public creationTime: number;
- /** devices with which we have shared the session key `userId -> {deviceId -> SharedWithData}` */
- public sharedWithDevices: MapWithDefault<string, Map<string, SharedWithData>> = new MapWithDefault(() => new Map());
- public blockedDevicesNotified: MapWithDefault<string, Map<string, boolean>> = new MapWithDefault(() => new Map());
-
- /**
- * @param sharedHistory - whether the session can be freely shared with
- * other group members, according to the room history visibility settings
- */
- public constructor(public readonly sessionId: string, public readonly sharedHistory = false) {
- this.creationTime = new Date().getTime();
- }
-
- /**
- * Check if it's time to rotate the session
- */
- public needsRotation(rotationPeriodMsgs: number, rotationPeriodMs: number): boolean {
- const sessionLifetime = new Date().getTime() - this.creationTime;
-
- if (this.useCount >= rotationPeriodMsgs || sessionLifetime >= rotationPeriodMs) {
- logger.log("Rotating megolm session after " + this.useCount + " messages, " + sessionLifetime + "ms");
- return true;
- }
-
- return false;
- }
-
- public markSharedWithDevice(userId: string, deviceId: string, deviceKey: string, chainIndex: number): void {
- this.sharedWithDevices.getOrCreate(userId).set(deviceId, { deviceKey, messageIndex: chainIndex });
- }
-
- public markNotifiedBlockedDevice(userId: string, deviceId: string): void {
- this.blockedDevicesNotified.getOrCreate(userId).set(deviceId, true);
- }
-
- /**
- * Determine if this session has been shared with devices which it shouldn't
- * have been.
- *
- * @param devicesInRoom - `userId -> {deviceId -> object}`
- * devices we should shared the session with.
- *
- * @returns true if we have shared the session with devices which aren't
- * in devicesInRoom.
- */
- public sharedWithTooManyDevices(devicesInRoom: DeviceInfoMap): boolean {
- for (const [userId, devices] of this.sharedWithDevices) {
- if (!devicesInRoom.has(userId)) {
- logger.log("Starting new megolm session because we shared with " + userId);
- return true;
- }
-
- for (const [deviceId] of devices) {
- if (!devicesInRoom.get(userId)?.get(deviceId)) {
- logger.log("Starting new megolm session because we shared with " + userId + ":" + deviceId);
- return true;
- }
- }
- }
-
- return false;
- }
-}
-
-/**
- * Megolm encryption implementation
- *
- * @param params - parameters, as per {@link EncryptionAlgorithm}
- */
-export class MegolmEncryption extends EncryptionAlgorithm {
- // the most recent attempt to set up a session. This is used to serialise
- // the session setups, so that we have a race-free view of which session we
- // are using, and which devices we have shared the keys with. It resolves
- // with an OutboundSessionInfo (or undefined, for the first message in the
- // room).
- private setupPromise = Promise.resolve<OutboundSessionInfo | null>(null);
-
- // Map of outbound sessions by sessions ID. Used if we need a particular
- // session (the session we're currently using to send is always obtained
- // using setupPromise).
- private outboundSessions: Record<string, OutboundSessionInfo> = {};
-
- private readonly sessionRotationPeriodMsgs: number;
- private readonly sessionRotationPeriodMs: number;
- private encryptionPreparation?: {
- promise: Promise<void>;
- startTime: number;
- cancel: () => void;
- };
-
- protected readonly roomId: string;
- private readonly prefixedLogger: PrefixedLogger;
-
- public constructor(params: IParams & Required<Pick<IParams, "roomId">>) {
- super(params);
- this.roomId = params.roomId;
- this.prefixedLogger = logger.withPrefix(`[${this.roomId} encryption]`);
-
- this.sessionRotationPeriodMsgs = params.config?.rotation_period_msgs ?? 100;
- this.sessionRotationPeriodMs = params.config?.rotation_period_ms ?? 7 * 24 * 3600 * 1000;
- }
-
- /**
- * @internal
- *
- * @param devicesInRoom - The devices in this room, indexed by user ID
- * @param blocked - The devices that are blocked, indexed by user ID
- * @param singleOlmCreationPhase - Only perform one round of olm
- * session creation
- *
- * This method updates the setupPromise field of the class by chaining a new
- * call on top of the existing promise, and then catching and discarding any
- * errors that might happen while setting up the outbound group session. This
- * is done to ensure that `setupPromise` always resolves to `null` or the
- * `OutboundSessionInfo`.
- *
- * Using `>>=` to represent the promise chaining operation, it does the
- * following:
- *
- * ```
- * setupPromise = previousSetupPromise >>= setup >>= discardErrors
- * ```
- *
- * The initial value for the `setupPromise` is a promise that resolves to
- * `null`. The forceDiscardSession() resets setupPromise to this initial
- * promise.
- *
- * @returns Promise which resolves to the
- * OutboundSessionInfo when setup is complete.
- */
- private async ensureOutboundSession(
- room: Room,
- devicesInRoom: DeviceInfoMap,
- blocked: BlockedMap,
- singleOlmCreationPhase = false,
- ): Promise<OutboundSessionInfo> {
- // takes the previous OutboundSessionInfo, and considers whether to create
- // a new one. Also shares the key with any (new) devices in the room.
- //
- // returns a promise which resolves once the keyshare is successful.
- const setup = async (oldSession: OutboundSessionInfo | null): Promise<OutboundSessionInfo> => {
- const sharedHistory = isRoomSharedHistory(room);
- const session = await this.prepareSession(devicesInRoom, sharedHistory, oldSession);
-
- await this.shareSession(devicesInRoom, sharedHistory, singleOlmCreationPhase, blocked, session);
-
- return session;
- };
-
- // first wait for the previous share to complete
- const fallible = this.setupPromise.then(setup);
-
- // Ensure any failures are logged for debugging and make sure that the
- // promise chain remains unbroken
- //
- // setupPromise resolves to `null` or the `OutboundSessionInfo` whether
- // or not the share succeeds
- this.setupPromise = fallible.catch((e) => {
- this.prefixedLogger.error(`Failed to setup outbound session`, e);
- return null;
- });
-
- // but we return a promise which only resolves if the share was successful.
- return fallible;
- }
-
- private async prepareSession(
- devicesInRoom: DeviceInfoMap,
- sharedHistory: boolean,
- session: OutboundSessionInfo | null,
- ): Promise<OutboundSessionInfo> {
- // history visibility changed
- if (session && sharedHistory !== session.sharedHistory) {
- session = null;
- }
-
- // need to make a brand new session?
- if (session?.needsRotation(this.sessionRotationPeriodMsgs, this.sessionRotationPeriodMs)) {
- this.prefixedLogger.log("Starting new megolm session because we need to rotate.");
- session = null;
- }
-
- // determine if we have shared with anyone we shouldn't have
- if (session?.sharedWithTooManyDevices(devicesInRoom)) {
- session = null;
- }
-
- if (!session) {
- this.prefixedLogger.log("Starting new megolm session");
- session = await this.prepareNewSession(sharedHistory);
- this.prefixedLogger.log(`Started new megolm session ${session.sessionId}`);
- this.outboundSessions[session.sessionId] = session;
- }
-
- return session;
- }
-
- private async shareSession(
- devicesInRoom: DeviceInfoMap,
- sharedHistory: boolean,
- singleOlmCreationPhase: boolean,
- blocked: BlockedMap,
- session: OutboundSessionInfo,
- ): Promise<void> {
- // now check if we need to share with any devices
- const shareMap: Record<string, DeviceInfo[]> = {};
-
- for (const [userId, userDevices] of devicesInRoom) {
- for (const [deviceId, deviceInfo] of userDevices) {
- const key = deviceInfo.getIdentityKey();
- if (key == this.olmDevice.deviceCurve25519Key) {
- // don't bother sending to ourself
- continue;
- }
-
- if (!session.sharedWithDevices.get(userId)?.get(deviceId)) {
- shareMap[userId] = shareMap[userId] || [];
- shareMap[userId].push(deviceInfo);
- }
- }
- }
-
- const key = this.olmDevice.getOutboundGroupSessionKey(session.sessionId);
- const payload: IPayload = {
- type: "m.room_key",
- content: {
- "algorithm": olmlib.MEGOLM_ALGORITHM,
- "room_id": this.roomId,
- "session_id": session.sessionId,
- "session_key": key.key,
- "chain_index": key.chain_index,
- "org.matrix.msc3061.shared_history": sharedHistory,
- },
- };
- const [devicesWithoutSession, olmSessions] = await olmlib.getExistingOlmSessions(
- this.olmDevice,
- this.baseApis,
- shareMap,
- );
-
- await Promise.all([
- (async (): Promise<void> => {
- // share keys with devices that we already have a session for
- const olmSessionList = Array.from(olmSessions.entries())
- .map(([userId, sessionsByUser]) =>
- Array.from(sessionsByUser.entries()).map(
- ([deviceId, session]) => `${userId}/${deviceId}: ${session.sessionId}`,
- ),
- )
- .flat(1);
- this.prefixedLogger.debug("Sharing keys with devices with existing Olm sessions:", olmSessionList);
- await this.shareKeyWithOlmSessions(session, key, payload, olmSessions);
- this.prefixedLogger.debug("Shared keys with existing Olm sessions");
- })(),
- (async (): Promise<void> => {
- const deviceList = Array.from(devicesWithoutSession.entries())
- .map(([userId, devicesByUser]) => devicesByUser.map((device) => `${userId}/${device.deviceId}`))
- .flat(1);
- this.prefixedLogger.debug(
- "Sharing keys (start phase 1) with devices without existing Olm sessions:",
- deviceList,
- );
- const errorDevices: IOlmDevice[] = [];
-
- // meanwhile, establish olm sessions for devices that we don't
- // already have a session for, and share keys with them. If
- // we're doing two phases of olm session creation, use a
- // shorter timeout when fetching one-time keys for the first
- // phase.
- const start = Date.now();
- const failedServers: string[] = [];
- await this.shareKeyWithDevices(
- session,
- key,
- payload,
- devicesWithoutSession,
- errorDevices,
- singleOlmCreationPhase ? 10000 : 2000,
- failedServers,
- );
- this.prefixedLogger.debug("Shared keys (end phase 1) with devices without existing Olm sessions");
-
- if (!singleOlmCreationPhase && Date.now() - start < 10000) {
- // perform the second phase of olm session creation if requested,
- // and if the first phase didn't take too long
- (async (): Promise<void> => {
- // Retry sending keys to devices that we were unable to establish
- // an olm session for. This time, we use a longer timeout, but we
- // do this in the background and don't block anything else while we
- // do this. We only need to retry users from servers that didn't
- // respond the first time.
- const retryDevices: MapWithDefault<string, DeviceInfo[]> = new MapWithDefault(() => []);
- const failedServerMap = new Set();
- for (const server of failedServers) {
- failedServerMap.add(server);
- }
- const failedDevices: IOlmDevice[] = [];
- for (const { userId, deviceInfo } of errorDevices) {
- const userHS = userId.slice(userId.indexOf(":") + 1);
- if (failedServerMap.has(userHS)) {
- retryDevices.getOrCreate(userId).push(deviceInfo);
- } else {
- // if we aren't going to retry, then handle it
- // as a failed device
- failedDevices.push({ userId, deviceInfo });
- }
- }
-
- const retryDeviceList = Array.from(retryDevices.entries())
- .map(([userId, devicesByUser]) =>
- devicesByUser.map((device) => `${userId}/${device.deviceId}`),
- )
- .flat(1);
-
- if (retryDeviceList.length > 0) {
- this.prefixedLogger.debug(
- "Sharing keys (start phase 2) with devices without existing Olm sessions:",
- retryDeviceList,
- );
- await this.shareKeyWithDevices(session, key, payload, retryDevices, failedDevices, 30000);
- this.prefixedLogger.debug(
- "Shared keys (end phase 2) with devices without existing Olm sessions",
- );
- }
-
- await this.notifyFailedOlmDevices(session, key, failedDevices);
- })();
- } else {
- await this.notifyFailedOlmDevices(session, key, errorDevices);
- }
- })(),
- (async (): Promise<void> => {
- this.prefixedLogger.debug(
- `There are ${blocked.size} blocked devices:`,
- Array.from(blocked.entries())
- .map(([userId, blockedByUser]) =>
- Array.from(blockedByUser.entries()).map(
- ([deviceId, _deviceInfo]) => `${userId}/${deviceId}`,
- ),
- )
- .flat(1),
- );
-
- // also, notify newly blocked devices that they're blocked
- const blockedMap: MapWithDefault<string, Map<string, { device: IBlockedDevice }>> = new MapWithDefault(
- () => new Map(),
- );
- let blockedCount = 0;
- for (const [userId, userBlockedDevices] of blocked) {
- for (const [deviceId, device] of userBlockedDevices) {
- if (session.blockedDevicesNotified.get(userId)?.get(deviceId) === undefined) {
- blockedMap.getOrCreate(userId).set(deviceId, { device });
- blockedCount++;
- }
- }
- }
-
- if (blockedCount) {
- this.prefixedLogger.debug(
- `Notifying ${blockedCount} newly blocked devices:`,
- Array.from(blockedMap.entries())
- .map(([userId, blockedByUser]) =>
- Object.entries(blockedByUser).map(([deviceId, _deviceInfo]) => `${userId}/${deviceId}`),
- )
- .flat(1),
- );
- await this.notifyBlockedDevices(session, blockedMap);
- this.prefixedLogger.debug(`Notified ${blockedCount} newly blocked devices`);
- }
- })(),
- ]);
- }
-
- /**
- * @internal
- *
- *
- * @returns session
- */
- private async prepareNewSession(sharedHistory: boolean): Promise<OutboundSessionInfo> {
- const sessionId = this.olmDevice.createOutboundGroupSession();
- const key = this.olmDevice.getOutboundGroupSessionKey(sessionId);
-
- await this.olmDevice.addInboundGroupSession(
- this.roomId,
- this.olmDevice.deviceCurve25519Key!,
- [],
- sessionId,
- key.key,
- { ed25519: this.olmDevice.deviceEd25519Key! },
- false,
- { sharedHistory },
- );
-
- // don't wait for it to complete
- this.crypto.backupManager.backupGroupSession(this.olmDevice.deviceCurve25519Key!, sessionId);
-
- return new OutboundSessionInfo(sessionId, sharedHistory);
- }
-
- /**
- * Determines what devices in devicesByUser don't have an olm session as given
- * in devicemap.
- *
- * @internal
- *
- * @param deviceMap - the devices that have olm sessions, as returned by
- * olmlib.ensureOlmSessionsForDevices.
- * @param devicesByUser - a map of user IDs to array of deviceInfo
- * @param noOlmDevices - an array to fill with devices that don't have
- * olm sessions
- *
- * @returns an array of devices that don't have olm sessions. If
- * noOlmDevices is specified, then noOlmDevices will be returned.
- */
- private getDevicesWithoutSessions(
- deviceMap: Map<string, Map<string, IOlmSessionResult>>,
- devicesByUser: Map<string, DeviceInfo[]>,
- noOlmDevices: IOlmDevice[] = [],
- ): IOlmDevice[] {
- for (const [userId, devicesToShareWith] of devicesByUser) {
- const sessionResults = deviceMap.get(userId);
-
- for (const deviceInfo of devicesToShareWith) {
- const deviceId = deviceInfo.deviceId;
-
- const sessionResult = sessionResults?.get(deviceId);
- if (!sessionResult?.sessionId) {
- // no session with this device, probably because there
- // were no one-time keys.
-
- noOlmDevices.push({ userId, deviceInfo });
- sessionResults?.delete(deviceId);
-
- // ensureOlmSessionsForUsers has already done the logging,
- // so just skip it.
- continue;
- }
- }
- }
-
- return noOlmDevices;
- }
-
- /**
- * Splits the user device map into multiple chunks to reduce the number of
- * devices we encrypt to per API call.
- *
- * @internal
- *
- * @param devicesByUser - map from userid to list of devices
- *
- * @returns the blocked devices, split into chunks
- */
- private splitDevices<T extends DeviceInfo | IBlockedDevice>(
- devicesByUser: Map<string, Map<string, { device: T }>>,
- ): IOlmDevice<T>[][] {
- const maxDevicesPerRequest = 20;
-
- // use an array where the slices of a content map gets stored
- let currentSlice: IOlmDevice<T>[] = [];
- const mapSlices = [currentSlice];
-
- for (const [userId, userDevices] of devicesByUser) {
- for (const deviceInfo of userDevices.values()) {
- currentSlice.push({
- userId: userId,
- deviceInfo: deviceInfo.device,
- });
- }
-
- // We do this in the per-user loop as we prefer that all messages to the
- // same user end up in the same API call to make it easier for the
- // server (e.g. only have to send one EDU if a remote user, etc). This
- // does mean that if a user has many devices we may go over the desired
- // limit, but its not a hard limit so that is fine.
- if (currentSlice.length > maxDevicesPerRequest) {
- // the current slice is filled up. Start inserting into the next slice
- currentSlice = [];
- mapSlices.push(currentSlice);
- }
- }
- if (currentSlice.length === 0) {
- mapSlices.pop();
- }
- return mapSlices;
- }
-
- /**
- * @internal
- *
- *
- * @param chainIndex - current chain index
- *
- * @param userDeviceMap - mapping from userId to deviceInfo
- *
- * @param payload - fields to include in the encrypted payload
- *
- * @returns Promise which resolves once the key sharing
- * for the given userDeviceMap is generated and has been sent.
- */
- private encryptAndSendKeysToDevices(
- session: OutboundSessionInfo,
- chainIndex: number,
- devices: IOlmDevice[],
- payload: IPayload,
- ): Promise<void> {
- return this.crypto
- .encryptAndSendToDevices(devices, payload)
- .then(() => {
- // store that we successfully uploaded the keys of the current slice
- for (const device of devices) {
- session.markSharedWithDevice(
- device.userId,
- device.deviceInfo.deviceId,
- device.deviceInfo.getIdentityKey(),
- chainIndex,
- );
- }
- })
- .catch((error) => {
- this.prefixedLogger.error("failed to encryptAndSendToDevices", error);
- throw error;
- });
- }
-
- /**
- * @internal
- *
- *
- * @param userDeviceMap - list of blocked devices to notify
- *
- * @param payload - fields to include in the notification payload
- *
- * @returns Promise which resolves once the notifications
- * for the given userDeviceMap is generated and has been sent.
- */
- private async sendBlockedNotificationsToDevices(
- session: OutboundSessionInfo,
- userDeviceMap: IOlmDevice<IBlockedDevice>[],
- payload: IPayload,
- ): Promise<void> {
- const contentMap: MapWithDefault<string, Map<string, IPayload>> = new MapWithDefault(() => new Map());
-
- for (const val of userDeviceMap) {
- const userId = val.userId;
- const blockedInfo = val.deviceInfo;
- const deviceInfo = blockedInfo.deviceInfo;
- const deviceId = deviceInfo.deviceId;
-
- const message = {
- ...payload,
- code: blockedInfo.code,
- reason: blockedInfo.reason,
- [ToDeviceMessageId]: uuidv4(),
- };
-
- if (message.code === "m.no_olm") {
- delete message.room_id;
- delete message.session_id;
- }
-
- contentMap.getOrCreate(userId).set(deviceId, message);
- }
-
- await this.baseApis.sendToDevice("m.room_key.withheld", contentMap);
-
- // record the fact that we notified these blocked devices
- for (const [userId, userDeviceMap] of contentMap) {
- for (const deviceId of userDeviceMap.keys()) {
- session.markNotifiedBlockedDevice(userId, deviceId);
- }
- }
- }
-
- /**
- * Re-shares a megolm session key with devices if the key has already been
- * sent to them.
- *
- * @param senderKey - The key of the originating device for the session
- * @param sessionId - ID of the outbound session to share
- * @param userId - ID of the user who owns the target device
- * @param device - The target device
- */
- public async reshareKeyWithDevice(
- senderKey: string,
- sessionId: string,
- userId: string,
- device: DeviceInfo,
- ): Promise<void> {
- const obSessionInfo = this.outboundSessions[sessionId];
- if (!obSessionInfo) {
- this.prefixedLogger.debug(`megolm session ${senderKey}|${sessionId} not found: not re-sharing keys`);
- return;
- }
-
- // The chain index of the key we previously sent this device
- if (!obSessionInfo.sharedWithDevices.has(userId)) {
- this.prefixedLogger.debug(`megolm session ${senderKey}|${sessionId} never shared with user ${userId}`);
- return;
- }
- const sessionSharedData = obSessionInfo.sharedWithDevices.get(userId)?.get(device.deviceId);
- if (sessionSharedData === undefined) {
- this.prefixedLogger.debug(
- `megolm session ${senderKey}|${sessionId} never shared with device ${userId}:${device.deviceId}`,
- );
- return;
- }
-
- if (sessionSharedData.deviceKey !== device.getIdentityKey()) {
- this.prefixedLogger.warn(
- `Megolm session ${senderKey}|${sessionId} has been shared with device ${device.deviceId} but ` +
- `with identity key ${sessionSharedData.deviceKey}. Key is now ${device.getIdentityKey()}!`,
- );
- return;
- }
-
- // get the key from the inbound session: the outbound one will already
- // have been ratcheted to the next chain index.
- const key = await this.olmDevice.getInboundGroupSessionKey(
- this.roomId,
- senderKey,
- sessionId,
- sessionSharedData.messageIndex,
- );
-
- if (!key) {
- this.prefixedLogger.warn(
- `No inbound session key found for megolm session ${senderKey}|${sessionId}: not re-sharing keys`,
- );
- return;
- }
-
- await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, new Map([[userId, [device]]]));
-
- const payload = {
- type: "m.forwarded_room_key",
- content: {
- "algorithm": olmlib.MEGOLM_ALGORITHM,
- "room_id": this.roomId,
- "session_id": sessionId,
- "session_key": key.key,
- "chain_index": key.chain_index,
- "sender_key": senderKey,
- "sender_claimed_ed25519_key": key.sender_claimed_ed25519_key,
- "forwarding_curve25519_key_chain": key.forwarding_curve25519_key_chain,
- "org.matrix.msc3061.shared_history": key.shared_history || false,
- },
- };
-
- const encryptedContent: IEncryptedContent = {
- algorithm: olmlib.OLM_ALGORITHM,
- sender_key: this.olmDevice.deviceCurve25519Key!,
- ciphertext: {},
- [ToDeviceMessageId]: uuidv4(),
- };
- await olmlib.encryptMessageForDevice(
- encryptedContent.ciphertext,
- this.userId,
- this.deviceId,
- this.olmDevice,
- userId,
- device,
- payload,
- );
-
- await this.baseApis.sendToDevice(
- "m.room.encrypted",
- new Map([[userId, new Map([[device.deviceId, encryptedContent]])]]),
- );
- this.prefixedLogger.debug(
- `Re-shared key for megolm session ${senderKey}|${sessionId} with ${userId}:${device.deviceId}`,
- );
- }
-
- /**
- * @internal
- *
- *
- * @param key - the session key as returned by
- * OlmDevice.getOutboundGroupSessionKey
- *
- * @param payload - the base to-device message payload for sharing keys
- *
- * @param devicesByUser - map from userid to list of devices
- *
- * @param errorDevices - array that will be populated with the devices that we can't get an
- * olm session for
- *
- * @param otkTimeout - The timeout in milliseconds when requesting
- * one-time keys for establishing new olm sessions.
- *
- * @param failedServers - An array to fill with remote servers that
- * failed to respond to one-time-key requests.
- */
- private async shareKeyWithDevices(
- session: OutboundSessionInfo,
- key: IOutboundGroupSessionKey,
- payload: IPayload,
- devicesByUser: Map<string, DeviceInfo[]>,
- errorDevices: IOlmDevice[],
- otkTimeout: number,
- failedServers?: string[],
- ): Promise<void> {
- const devicemap = await olmlib.ensureOlmSessionsForDevices(
- this.olmDevice,
- this.baseApis,
- devicesByUser,
- false,
- otkTimeout,
- failedServers,
- this.prefixedLogger,
- );
- this.getDevicesWithoutSessions(devicemap, devicesByUser, errorDevices);
- await this.shareKeyWithOlmSessions(session, key, payload, devicemap);
- }
-
- private async shareKeyWithOlmSessions(
- session: OutboundSessionInfo,
- key: IOutboundGroupSessionKey,
- payload: IPayload,
- deviceMap: Map<string, Map<string, IOlmSessionResult>>,
- ): Promise<void> {
- const userDeviceMaps = this.splitDevices(deviceMap);
-
- for (let i = 0; i < userDeviceMaps.length; i++) {
- const taskDetail = `megolm keys for ${session.sessionId} (slice ${i + 1}/${userDeviceMaps.length})`;
- try {
- this.prefixedLogger.debug(
- `Sharing ${taskDetail}`,
- userDeviceMaps[i].map((d) => `${d.userId}/${d.deviceInfo.deviceId}`),
- );
- await this.encryptAndSendKeysToDevices(session, key.chain_index, userDeviceMaps[i], payload);
- this.prefixedLogger.debug(`Shared ${taskDetail}`);
- } catch (e) {
- this.prefixedLogger.error(`Failed to share ${taskDetail}`);
- throw e;
- }
- }
- }
-
- /**
- * Notify devices that we weren't able to create olm sessions.
- *
- *
- *
- * @param failedDevices - the devices that we were unable to
- * create olm sessions for, as returned by shareKeyWithDevices
- */
- private async notifyFailedOlmDevices(
- session: OutboundSessionInfo,
- key: IOutboundGroupSessionKey,
- failedDevices: IOlmDevice[],
- ): Promise<void> {
- this.prefixedLogger.debug(`Notifying ${failedDevices.length} devices we failed to create Olm sessions`);
-
- // mark the devices that failed as "handled" because we don't want to try
- // to claim a one-time-key for dead devices on every message.
- for (const { userId, deviceInfo } of failedDevices) {
- const deviceId = deviceInfo.deviceId;
-
- session.markSharedWithDevice(userId, deviceId, deviceInfo.getIdentityKey(), key.chain_index);
- }
-
- const unnotifiedFailedDevices = await this.olmDevice.filterOutNotifiedErrorDevices(failedDevices);
- this.prefixedLogger.debug(
- `Need to notify ${unnotifiedFailedDevices.length} failed devices which haven't been notified before`,
- );
- const blockedMap: MapWithDefault<string, Map<string, { device: IBlockedDevice }>> = new MapWithDefault(
- () => new Map(),
- );
- for (const { userId, deviceInfo } of unnotifiedFailedDevices) {
- // we use a similar format to what
- // olmlib.ensureOlmSessionsForDevices returns, so that
- // we can use the same function to split
- blockedMap.getOrCreate(userId).set(deviceInfo.deviceId, {
- device: {
- code: "m.no_olm",
- reason: WITHHELD_MESSAGES["m.no_olm"],
- deviceInfo,
- },
- });
- }
-
- // send the notifications
- await this.notifyBlockedDevices(session, blockedMap);
- this.prefixedLogger.debug(
- `Notified ${unnotifiedFailedDevices.length} devices we failed to create Olm sessions`,
- );
- }
-
- /**
- * Notify blocked devices that they have been blocked.
- *
- *
- * @param devicesByUser - map from userid to device ID to blocked data
- */
- private async notifyBlockedDevices(
- session: OutboundSessionInfo,
- devicesByUser: Map<string, Map<string, { device: IBlockedDevice }>>,
- ): Promise<void> {
- const payload: IPayload = {
- room_id: this.roomId,
- session_id: session.sessionId,
- algorithm: olmlib.MEGOLM_ALGORITHM,
- sender_key: this.olmDevice.deviceCurve25519Key!,
- };
-
- const userDeviceMaps = this.splitDevices(devicesByUser);
-
- for (let i = 0; i < userDeviceMaps.length; i++) {
- try {
- await this.sendBlockedNotificationsToDevices(session, userDeviceMaps[i], payload);
- this.prefixedLogger.log(
- `Completed blacklist notification for ${session.sessionId} ` +
- `(slice ${i + 1}/${userDeviceMaps.length})`,
- );
- } catch (e) {
- this.prefixedLogger.log(
- `blacklist notification for ${session.sessionId} ` +
- `(slice ${i + 1}/${userDeviceMaps.length}) failed`,
- );
-
- throw e;
- }
- }
- }
-
- /**
- * Perform any background tasks that can be done before a message is ready to
- * send, in order to speed up sending of the message.
- *
- * @param room - the room the event is in
- * @returns A function that, when called, will stop the preparation
- */
- public prepareToEncrypt(room: Room): () => void {
- if (room.roomId !== this.roomId) {
- throw new Error("MegolmEncryption.prepareToEncrypt called on unexpected room");
- }
-
- if (this.encryptionPreparation != null) {
- // We're already preparing something, so don't do anything else.
- const elapsedTime = Date.now() - this.encryptionPreparation.startTime;
- this.prefixedLogger.debug(
- `Already started preparing to encrypt for this room ${elapsedTime}ms ago, skipping`,
- );
- return this.encryptionPreparation.cancel;
- }
-
- this.prefixedLogger.debug("Preparing to encrypt events");
-
- let cancelled = false;
- const isCancelled = (): boolean => cancelled;
-
- this.encryptionPreparation = {
- startTime: Date.now(),
- promise: (async (): Promise<void> => {
- try {
- // Attempt to enumerate the devices in room, and gracefully
- // handle cancellation if it occurs.
- const getDevicesResult = await this.getDevicesInRoom(room, false, isCancelled);
- if (getDevicesResult === null) return;
- const [devicesInRoom, blocked] = getDevicesResult;
-
- if (this.crypto.globalErrorOnUnknownDevices) {
- // Drop unknown devices for now. When the message gets sent, we'll
- // throw an error, but we'll still be prepared to send to the known
- // devices.
- this.removeUnknownDevices(devicesInRoom);
- }
-
- this.prefixedLogger.debug("Ensuring outbound megolm session");
- await this.ensureOutboundSession(room, devicesInRoom, blocked, true);
-
- this.prefixedLogger.debug("Ready to encrypt events");
- } catch (e) {
- this.prefixedLogger.error("Failed to prepare to encrypt events", e);
- } finally {
- delete this.encryptionPreparation;
- }
- })(),
-
- cancel: (): void => {
- // The caller has indicated that the process should be cancelled,
- // so tell the promise that we'd like to halt, and reset the preparation state.
- cancelled = true;
- delete this.encryptionPreparation;
- },
- };
-
- return this.encryptionPreparation.cancel;
- }
-
- /**
- * @param content - plaintext event content
- *
- * @returns Promise which resolves to the new event body
- */
- public async encryptMessage(room: Room, eventType: string, content: IContent): Promise<IMegolmEncryptedContent> {
- this.prefixedLogger.log("Starting to encrypt event");
-
- if (this.encryptionPreparation != null) {
- // If we started sending keys, wait for it to be done.
- // FIXME: check if we need to cancel
- // (https://github.com/matrix-org/matrix-js-sdk/issues/1255)
- try {
- await this.encryptionPreparation.promise;
- } catch (e) {
- // ignore any errors -- if the preparation failed, we'll just
- // restart everything here
- }
- }
-
- /**
- * When using in-room messages and the room has encryption enabled,
- * clients should ensure that encryption does not hinder the verification.
- */
- const forceDistributeToUnverified = this.isVerificationEvent(eventType, content);
- const [devicesInRoom, blocked] = await this.getDevicesInRoom(room, forceDistributeToUnverified);
-
- // check if any of these devices are not yet known to the user.
- // if so, warn the user so they can verify or ignore.
- if (this.crypto.globalErrorOnUnknownDevices) {
- this.checkForUnknownDevices(devicesInRoom);
- }
-
- const session = await this.ensureOutboundSession(room, devicesInRoom, blocked);
- const payloadJson = {
- room_id: this.roomId,
- type: eventType,
- content: content,
- };
-
- const ciphertext = this.olmDevice.encryptGroupMessage(session.sessionId, JSON.stringify(payloadJson));
- const encryptedContent: IEncryptedContent = {
- algorithm: olmlib.MEGOLM_ALGORITHM,
- sender_key: this.olmDevice.deviceCurve25519Key!,
- ciphertext: ciphertext,
- session_id: session.sessionId,
- // Include our device ID so that recipients can send us a
- // m.new_device message if they don't have our session key.
- // XXX: Do we still need this now that m.new_device messages
- // no longer exist since #483?
- device_id: this.deviceId,
- };
-
- session.useCount++;
- return encryptedContent;
- }
-
- private isVerificationEvent(eventType: string, content: IContent): boolean {
- switch (eventType) {
- case EventType.KeyVerificationCancel:
- case EventType.KeyVerificationDone:
- case EventType.KeyVerificationMac:
- case EventType.KeyVerificationStart:
- case EventType.KeyVerificationKey:
- case EventType.KeyVerificationReady:
- case EventType.KeyVerificationAccept: {
- return true;
- }
- case EventType.RoomMessage: {
- return content["msgtype"] === MsgType.KeyVerificationRequest;
- }
- default: {
- return false;
- }
- }
- }
-
- /**
- * Forces the current outbound group session to be discarded such
- * that another one will be created next time an event is sent.
- *
- * This should not normally be necessary.
- */
- public forceDiscardSession(): void {
- this.setupPromise = this.setupPromise.then(() => null);
- }
-
- /**
- * Checks the devices we're about to send to and see if any are entirely
- * unknown to the user. If so, warn the user, and mark them as known to
- * give the user a chance to go verify them before re-sending this message.
- *
- * @param devicesInRoom - `userId -> {deviceId -> object}`
- * devices we should shared the session with.
- */
- private checkForUnknownDevices(devicesInRoom: DeviceInfoMap): void {
- const unknownDevices: MapWithDefault<string, Map<string, DeviceInfo>> = new MapWithDefault(() => new Map());
-
- for (const [userId, userDevices] of devicesInRoom) {
- for (const [deviceId, device] of userDevices) {
- if (device.isUnverified() && !device.isKnown()) {
- unknownDevices.getOrCreate(userId).set(deviceId, device);
- }
- }
- }
-
- if (unknownDevices.size) {
- // it'd be kind to pass unknownDevices up to the user in this error
- throw new UnknownDeviceError(
- "This room contains unknown devices which have not been verified. " +
- "We strongly recommend you verify them before continuing.",
- unknownDevices,
- );
- }
- }
-
- /**
- * Remove unknown devices from a set of devices. The devicesInRoom parameter
- * will be modified.
- *
- * @param devicesInRoom - `userId -> {deviceId -> object}`
- * devices we should shared the session with.
- */
- private removeUnknownDevices(devicesInRoom: DeviceInfoMap): void {
- for (const [userId, userDevices] of devicesInRoom) {
- for (const [deviceId, device] of userDevices) {
- if (device.isUnverified() && !device.isKnown()) {
- userDevices.delete(deviceId);
- }
- }
-
- if (userDevices.size === 0) {
- devicesInRoom.delete(userId);
- }
- }
- }
-
- /**
- * Get the list of unblocked devices for all users in the room
- *
- * @param forceDistributeToUnverified - if set to true will include the unverified devices
- * even if setting is set to block them (useful for verification)
- * @param isCancelled - will cause the procedure to abort early if and when it starts
- * returning `true`. If omitted, cancellation won't happen.
- *
- * @returns Promise which resolves to `null`, or an array whose
- * first element is a {@link DeviceInfoMap} indicating
- * the devices that messages should be encrypted to, and whose second
- * element is a map from userId to deviceId to data indicating the devices
- * that are in the room but that have been blocked.
- * If `isCancelled` is provided and returns `true` while processing, `null`
- * will be returned.
- * If `isCancelled` is not provided, the Promise will never resolve to `null`.
- */
- private async getDevicesInRoom(
- room: Room,
- forceDistributeToUnverified?: boolean,
- ): Promise<[DeviceInfoMap, BlockedMap]>;
- private async getDevicesInRoom(
- room: Room,
- forceDistributeToUnverified?: boolean,
- isCancelled?: () => boolean,
- ): Promise<null | [DeviceInfoMap, BlockedMap]>;
- private async getDevicesInRoom(
- room: Room,
- forceDistributeToUnverified = false,
- isCancelled?: () => boolean,
- ): Promise<null | [DeviceInfoMap, BlockedMap]> {
- const members = await room.getEncryptionTargetMembers();
- this.prefixedLogger.debug(
- `Encrypting for users (shouldEncryptForInvitedMembers: ${room.shouldEncryptForInvitedMembers()}):`,
- members.map((u) => `${u.userId} (${u.membership})`),
- );
-
- const roomMembers = members.map(function (u) {
- return u.userId;
- });
-
- // The global value is treated as a default for when rooms don't specify a value.
- let isBlacklisting = this.crypto.globalBlacklistUnverifiedDevices;
- const isRoomBlacklisting = room.getBlacklistUnverifiedDevices();
- if (typeof isRoomBlacklisting === "boolean") {
- isBlacklisting = isRoomBlacklisting;
- }
-
- // We are happy to use a cached version here: we assume that if we already
- // have a list of the user's devices, then we already share an e2e room
- // with them, which means that they will have announced any new devices via
- // device_lists in their /sync response. This cache should then be maintained
- // using all the device_lists changes and left fields.
- // See https://github.com/vector-im/element-web/issues/2305 for details.
- const devices = await this.crypto.downloadKeys(roomMembers, false);
-
- if (isCancelled?.() === true) {
- return null;
- }
-
- const blocked = new MapWithDefault<string, Map<string, IBlockedDevice>>(() => new Map());
- // remove any blocked devices
- for (const [userId, userDevices] of devices) {
- for (const [deviceId, userDevice] of userDevices) {
- // Yield prior to checking each device so that we don't block
- // updating/rendering for too long.
- // See https://github.com/vector-im/element-web/issues/21612
- if (isCancelled !== undefined) await immediate();
- if (isCancelled?.() === true) return null;
- const deviceTrust = this.crypto.checkDeviceTrust(userId, deviceId);
-
- if (
- userDevice.isBlocked() ||
- (!deviceTrust.isVerified() && isBlacklisting && !forceDistributeToUnverified)
- ) {
- const blockedDevices = blocked.getOrCreate(userId);
- const isBlocked = userDevice.isBlocked();
- blockedDevices.set(deviceId, {
- code: isBlocked ? "m.blacklisted" : "m.unverified",
- reason: WITHHELD_MESSAGES[isBlocked ? "m.blacklisted" : "m.unverified"],
- deviceInfo: userDevice,
- });
- userDevices.delete(deviceId);
- }
- }
- }
-
- return [devices, blocked];
- }
-}
-
-/**
- * Megolm decryption implementation
- *
- * @param params - parameters, as per {@link DecryptionAlgorithm}
- */
-export class MegolmDecryption extends DecryptionAlgorithm {
- // events which we couldn't decrypt due to unknown sessions /
- // indexes, or which we could only decrypt with untrusted keys:
- // map from senderKey|sessionId to Set of MatrixEvents
- private pendingEvents = new Map<string, Map<string, Set<MatrixEvent>>>();
-
- // this gets stubbed out by the unit tests.
- private olmlib = olmlib;
-
- protected readonly roomId: string;
- private readonly prefixedLogger: PrefixedLogger;
-
- public constructor(params: DecryptionClassParams<IParams & Required<Pick<IParams, "roomId">>>) {
- super(params);
- this.roomId = params.roomId;
- this.prefixedLogger = logger.withPrefix(`[${this.roomId} decryption]`);
- }
-
- /**
- * returns a promise which resolves to a
- * {@link EventDecryptionResult} once we have finished
- * decrypting, or rejects with an `algorithms.DecryptionError` if there is a
- * problem decrypting the event.
- */
- public async decryptEvent(event: MatrixEvent): Promise<IEventDecryptionResult> {
- const content = event.getWireContent();
-
- if (!content.sender_key || !content.session_id || !content.ciphertext) {
- throw new DecryptionError("MEGOLM_MISSING_FIELDS", "Missing fields in input");
- }
-
- // we add the event to the pending list *before* we start decryption.
- //
- // then, if the key turns up while decryption is in progress (and
- // decryption fails), we will schedule a retry.
- // (fixes https://github.com/vector-im/element-web/issues/5001)
- this.addEventToPendingList(event);
-
- let res: IDecryptedGroupMessage | null;
- try {
- res = await this.olmDevice.decryptGroupMessage(
- event.getRoomId()!,
- content.sender_key,
- content.session_id,
- content.ciphertext,
- event.getId()!,
- event.getTs(),
- );
- } catch (e) {
- if ((<Error>e).name === "DecryptionError") {
- // re-throw decryption errors as-is
- throw e;
- }
-
- let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR";
-
- if ((<MatrixError>e)?.message === "OLM.UNKNOWN_MESSAGE_INDEX") {
- this.requestKeysForEvent(event);
-
- errorCode = "OLM_UNKNOWN_MESSAGE_INDEX";
- }
-
- throw new DecryptionError(errorCode, e instanceof Error ? e.message : "Unknown Error: Error is undefined", {
- session: content.sender_key + "|" + content.session_id,
- });
- }
-
- if (res === null) {
- // We've got a message for a session we don't have.
- // try and get the missing key from the backup first
- this.crypto.backupManager.queryKeyBackupRateLimited(event.getRoomId(), content.session_id).catch(() => {});
-
- // (XXX: We might actually have received this key since we started
- // decrypting, in which case we'll have scheduled a retry, and this
- // request will be redundant. We could probably check to see if the
- // event is still in the pending list; if not, a retry will have been
- // scheduled, so we needn't send out the request here.)
- this.requestKeysForEvent(event);
-
- // See if there was a problem with the olm session at the time the
- // event was sent. Use a fuzz factor of 2 minutes.
- const problem = await this.olmDevice.sessionMayHaveProblems(content.sender_key, event.getTs() - 120000);
- if (problem) {
- this.prefixedLogger.info(
- `When handling UISI from ${event.getSender()} (sender key ${content.sender_key}): ` +
- `recent session problem with that sender:`,
- problem,
- );
- let problemDescription = PROBLEM_DESCRIPTIONS[problem.type as "no_olm"] || PROBLEM_DESCRIPTIONS.unknown;
- if (problem.fixed) {
- problemDescription += " Trying to create a new secure channel and re-requesting the keys.";
- }
- throw new DecryptionError("MEGOLM_UNKNOWN_INBOUND_SESSION_ID", problemDescription, {
- session: content.sender_key + "|" + content.session_id,
- });
- }
-
- throw new DecryptionError(
- "MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
- "The sender's device has not sent us the keys for this message.",
- {
- session: content.sender_key + "|" + content.session_id,
- },
- );
- }
-
- // Success. We can remove the event from the pending list, if
- // that hasn't already happened. However, if the event was
- // decrypted with an untrusted key, leave it on the pending
- // list so it will be retried if we find a trusted key later.
- if (!res.untrusted) {
- this.removeEventFromPendingList(event);
- }
-
- const payload = JSON.parse(res.result);
-
- // belt-and-braces check that the room id matches that indicated by the HS
- // (this is somewhat redundant, since the megolm session is scoped to the
- // room, so neither the sender nor a MITM can lie about the room_id).
- if (payload.room_id !== event.getRoomId()) {
- throw new DecryptionError("MEGOLM_BAD_ROOM", "Message intended for room " + payload.room_id);
- }
-
- return {
- clearEvent: payload,
- senderCurve25519Key: res.senderKey,
- claimedEd25519Key: res.keysClaimed.ed25519,
- forwardingCurve25519KeyChain: res.forwardingCurve25519KeyChain,
- untrusted: res.untrusted,
- };
- }
-
- private requestKeysForEvent(event: MatrixEvent): void {
- const wireContent = event.getWireContent();
-
- const recipients = event.getKeyRequestRecipients(this.userId);
-
- this.crypto.requestRoomKey(
- {
- room_id: event.getRoomId()!,
- algorithm: wireContent.algorithm,
- sender_key: wireContent.sender_key,
- session_id: wireContent.session_id,
- },
- recipients,
- );
- }
-
- /**
- * Add an event to the list of those awaiting their session keys.
- *
- * @internal
- *
- */
- private addEventToPendingList(event: MatrixEvent): void {
- const content = event.getWireContent();
- const senderKey = content.sender_key;
- const sessionId = content.session_id;
- if (!this.pendingEvents.has(senderKey)) {
- this.pendingEvents.set(senderKey, new Map<string, Set<MatrixEvent>>());
- }
- const senderPendingEvents = this.pendingEvents.get(senderKey)!;
- if (!senderPendingEvents.has(sessionId)) {
- senderPendingEvents.set(sessionId, new Set());
- }
- senderPendingEvents.get(sessionId)?.add(event);
- }
-
- /**
- * Remove an event from the list of those awaiting their session keys.
- *
- * @internal
- *
- */
- private removeEventFromPendingList(event: MatrixEvent): void {
- const content = event.getWireContent();
- const senderKey = content.sender_key;
- const sessionId = content.session_id;
- const senderPendingEvents = this.pendingEvents.get(senderKey);
- const pendingEvents = senderPendingEvents?.get(sessionId);
- if (!pendingEvents) {
- return;
- }
-
- pendingEvents.delete(event);
- if (pendingEvents.size === 0) {
- senderPendingEvents!.delete(sessionId);
- }
- if (senderPendingEvents!.size === 0) {
- this.pendingEvents.delete(senderKey);
- }
- }
-
- /**
- * Parse a RoomKey out of an `m.room_key` event.
- *
- * @param event - the event containing the room key.
- *
- * @returns The `RoomKey` if it could be successfully parsed out of the
- * event.
- *
- * @internal
- *
- */
- private roomKeyFromEvent(event: MatrixEvent): RoomKey | undefined {
- const senderKey = event.getSenderKey()!;
- const content = event.getContent<Partial<IMessage["content"]>>();
- const extraSessionData: OlmGroupSessionExtraData = {};
-
- if (!content.room_id || !content.session_key || !content.session_id || !content.algorithm) {
- this.prefixedLogger.error("key event is missing fields");
- return;
- }
-
- if (!olmlib.isOlmEncrypted(event)) {
- this.prefixedLogger.error("key event not properly encrypted");
- return;
- }
-
- if (content["org.matrix.msc3061.shared_history"]) {
- extraSessionData.sharedHistory = true;
- }
-
- const roomKey: RoomKey = {
- senderKey: senderKey,
- sessionId: content.session_id,
- sessionKey: content.session_key,
- extraSessionData,
- exportFormat: false,
- roomId: content.room_id,
- algorithm: content.algorithm,
- forwardingKeyChain: [],
- keysClaimed: event.getKeysClaimed(),
- };
-
- return roomKey;
- }
-
- /**
- * Parse a RoomKey out of an `m.forwarded_room_key` event.
- *
- * @param event - the event containing the forwarded room key.
- *
- * @returns The `RoomKey` if it could be successfully parsed out of the
- * event.
- *
- * @internal
- *
- */
- private forwardedRoomKeyFromEvent(event: MatrixEvent): RoomKey | undefined {
- // the properties in m.forwarded_room_key are a superset of those in m.room_key, so
- // start by parsing the m.room_key fields.
- const roomKey = this.roomKeyFromEvent(event);
-
- if (!roomKey) {
- return;
- }
-
- const senderKey = event.getSenderKey()!;
- const content = event.getContent<Partial<IMessage["content"]>>();
-
- const senderKeyUser = this.baseApis.crypto!.deviceList.getUserByIdentityKey(olmlib.OLM_ALGORITHM, senderKey);
-
- // We received this to-device event from event.getSenderKey(), but the original
- // creator of the room key is claimed in the content.
- const claimedCurve25519Key = content.sender_key;
- const claimedEd25519Key = content.sender_claimed_ed25519_key;
-
- let forwardingKeyChain = Array.isArray(content.forwarding_curve25519_key_chain)
- ? content.forwarding_curve25519_key_chain
- : [];
-
- // copy content before we modify it
- forwardingKeyChain = forwardingKeyChain.slice();
- forwardingKeyChain.push(senderKey);
-
- // Check if we have all the fields we need.
- if (senderKeyUser !== event.getSender()) {
- this.prefixedLogger.error("sending device does not belong to the user it claims to be from");
- return;
- }
-
- if (!claimedCurve25519Key) {
- this.prefixedLogger.error("forwarded_room_key event is missing sender_key field");
- return;
- }
-
- if (!claimedEd25519Key) {
- this.prefixedLogger.error(`forwarded_room_key_event is missing sender_claimed_ed25519_key field`);
- return;
- }
-
- const keysClaimed = {
- ed25519: claimedEd25519Key,
- };
-
- // FIXME: We're reusing the same field to track both:
- //
- // 1. The Olm identity we've received this room key from.
- // 2. The Olm identity deduced (in the trusted case) or claiming (in the
- // untrusted case) to be the original creator of this room key.
- //
- // We now overwrite the value tracking usage 1 with the value tracking usage 2.
- roomKey.senderKey = claimedCurve25519Key;
- // Replace our keysClaimed as well.
- roomKey.keysClaimed = keysClaimed;
- roomKey.exportFormat = true;
- roomKey.forwardingKeyChain = forwardingKeyChain;
- // forwarded keys are always untrusted
- roomKey.extraSessionData.untrusted = true;
-
- return roomKey;
- }
-
- /**
- * Determine if we should accept the forwarded room key that was found in the given
- * event.
- *
- * @param event - An `m.forwarded_room_key` event.
- * @param roomKey - The room key that was found in the event.
- *
- * @returns promise that will resolve to a boolean telling us if it's ok to
- * accept the given forwarded room key.
- *
- * @internal
- *
- */
- private async shouldAcceptForwardedKey(event: MatrixEvent, roomKey: RoomKey): Promise<boolean> {
- const senderKey = event.getSenderKey()!;
-
- const sendingDevice =
- this.crypto.deviceList.getDeviceByIdentityKey(olmlib.OLM_ALGORITHM, senderKey) ?? undefined;
- const deviceTrust = this.crypto.checkDeviceInfoTrust(event.getSender()!, sendingDevice);
-
- // Using the plaintext sender here is fine since we checked that the
- // sender matches to the user id in the device keys when this event was
- // originally decrypted. This can obviously only happen if the device
- // keys have been downloaded, but if they haven't the
- // `deviceTrust.isVerified()` flag would be false as well.
- //
- // It would still be far nicer if the `sendingDevice` had a user ID
- // attached to it that went through signature checks.
- const fromUs = event.getSender() === this.baseApis.getUserId();
- const keyFromOurVerifiedDevice = deviceTrust.isVerified() && fromUs;
- const weRequested = await this.wasRoomKeyRequested(event, roomKey);
- const fromInviter = this.wasRoomKeyForwardedByInviter(event, roomKey);
- const sharedAsHistory = this.wasRoomKeyForwardedAsHistory(roomKey);
-
- return (weRequested && keyFromOurVerifiedDevice) || (fromInviter && sharedAsHistory);
- }
-
- /**
- * Did we ever request the given room key from the event sender and its
- * accompanying device.
- *
- * @param event - An `m.forwarded_room_key` event.
- * @param roomKey - The room key that was found in the event.
- *
- * @internal
- *
- */
- private async wasRoomKeyRequested(event: MatrixEvent, roomKey: RoomKey): Promise<boolean> {
- // We send the `m.room_key_request` out as a wildcard to-device request,
- // otherwise we would have to duplicate the same content for each
- // device. This is why we need to pass in "*" as the device id here.
- const outgoingRequests = await this.crypto.cryptoStore.getOutgoingRoomKeyRequestsByTarget(
- event.getSender()!,
- "*",
- [RoomKeyRequestState.Sent],
- );
-
- return outgoingRequests.some(
- (req) => req.requestBody.room_id === roomKey.roomId && req.requestBody.session_id === roomKey.sessionId,
- );
- }
-
- private wasRoomKeyForwardedByInviter(event: MatrixEvent, roomKey: RoomKey): boolean {
- // TODO: This is supposed to have a time limit. We should only accept
- // such keys if we happen to receive them for a recently joined room.
- const room = this.baseApis.getRoom(roomKey.roomId);
- const senderKey = event.getSenderKey();
-
- if (!senderKey) {
- return false;
- }
-
- const senderKeyUser = this.crypto.deviceList.getUserByIdentityKey(olmlib.OLM_ALGORITHM, senderKey);
-
- if (!senderKeyUser) {
- return false;
- }
-
- const memberEvent = room?.getMember(this.userId)?.events.member;
- const fromInviter =
- memberEvent?.getSender() === senderKeyUser ||
- (memberEvent?.getUnsigned()?.prev_sender === senderKeyUser &&
- memberEvent?.getPrevContent()?.membership === "invite");
-
- if (room && fromInviter) {
- return true;
- } else {
- return false;
- }
- }
-
- private wasRoomKeyForwardedAsHistory(roomKey: RoomKey): boolean {
- const room = this.baseApis.getRoom(roomKey.roomId);
-
- // If the key is not for a known room, then something fishy is going on,
- // so we reject the key out of caution. In practice, this is a bit moot
- // because we'll only accept shared_history forwarded by the inviter, and
- // we won't know who was the inviter for an unknown room, so we'll reject
- // it anyway.
- if (room && roomKey.extraSessionData.sharedHistory) {
- return true;
- } else {
- return false;
- }
- }
-
- /**
- * Check if a forwarded room key should be parked.
- *
- * A forwarded room key should be parked if it's a key for a room we're not
- * in. We park the forwarded room key in case *this sender* invites us to
- * that room later.
- */
- private shouldParkForwardedKey(roomKey: RoomKey): boolean {
- const room = this.baseApis.getRoom(roomKey.roomId);
-
- if (!room && roomKey.extraSessionData.sharedHistory) {
- return true;
- } else {
- return false;
- }
- }
-
- /**
- * Park the given room key to our store.
- *
- * @param event - An `m.forwarded_room_key` event.
- * @param roomKey - The room key that was found in the event.
- *
- * @internal
- *
- */
- private async parkForwardedKey(event: MatrixEvent, roomKey: RoomKey): Promise<void> {
- const parkedData = {
- senderId: event.getSender()!,
- senderKey: roomKey.senderKey,
- sessionId: roomKey.sessionId,
- sessionKey: roomKey.sessionKey,
- keysClaimed: roomKey.keysClaimed,
- forwardingCurve25519KeyChain: roomKey.forwardingKeyChain,
- };
- await this.crypto.cryptoStore.doTxn(
- "readwrite",
- ["parked_shared_history"],
- (txn) => this.crypto.cryptoStore.addParkedSharedHistory(roomKey.roomId, parkedData, txn),
- logger.withPrefix("[addParkedSharedHistory]"),
- );
- }
-
- /**
- * Add the given room key to our store.
- *
- * @param roomKey - The room key that should be added to the store.
- *
- * @internal
- *
- */
- private async addRoomKey(roomKey: RoomKey): Promise<void> {
- try {
- await this.olmDevice.addInboundGroupSession(
- roomKey.roomId,
- roomKey.senderKey,
- roomKey.forwardingKeyChain,
- roomKey.sessionId,
- roomKey.sessionKey,
- roomKey.keysClaimed,
- roomKey.exportFormat,
- roomKey.extraSessionData,
- );
-
- // have another go at decrypting events sent with this session.
- if (await this.retryDecryption(roomKey.senderKey, roomKey.sessionId, !roomKey.extraSessionData.untrusted)) {
- // cancel any outstanding room key requests for this session.
- // Only do this if we managed to decrypt every message in the
- // session, because if we didn't, we leave the other key
- // requests in the hopes that someone sends us a key that
- // includes an earlier index.
- this.crypto.cancelRoomKeyRequest({
- algorithm: roomKey.algorithm,
- room_id: roomKey.roomId,
- session_id: roomKey.sessionId,
- sender_key: roomKey.senderKey,
- });
- }
-
- // don't wait for the keys to be backed up for the server
- await this.crypto.backupManager.backupGroupSession(roomKey.senderKey, roomKey.sessionId);
- } catch (e) {
- this.prefixedLogger.error(`Error handling m.room_key_event: ${e}`);
- }
- }
-
- /**
- * Handle room keys that have been forwarded to us as an
- * `m.forwarded_room_key` event.
- *
- * Forwarded room keys need special handling since we have no way of knowing
- * who the original creator of the room key was. This naturally means that
- * forwarded room keys are always untrusted and should only be accepted in
- * some cases.
- *
- * @param event - An `m.forwarded_room_key` event.
- *
- * @internal
- *
- */
- private async onForwardedRoomKey(event: MatrixEvent): Promise<void> {
- const roomKey = this.forwardedRoomKeyFromEvent(event);
-
- if (!roomKey) {
- return;
- }
-
- if (await this.shouldAcceptForwardedKey(event, roomKey)) {
- await this.addRoomKey(roomKey);
- } else if (this.shouldParkForwardedKey(roomKey)) {
- await this.parkForwardedKey(event, roomKey);
- }
- }
-
- public async onRoomKeyEvent(event: MatrixEvent): Promise<void> {
- if (event.getType() == "m.forwarded_room_key") {
- await this.onForwardedRoomKey(event);
- } else {
- const roomKey = this.roomKeyFromEvent(event);
-
- if (!roomKey) {
- return;
- }
-
- await this.addRoomKey(roomKey);
- }
- }
-
- /**
- * @param event - key event
- */
- public async onRoomKeyWithheldEvent(event: MatrixEvent): Promise<void> {
- const content = event.getContent();
- const senderKey = content.sender_key;
-
- if (content.code === "m.no_olm") {
- await this.onNoOlmWithheldEvent(event);
- } else if (content.code === "m.unavailable") {
- // this simply means that the other device didn't have the key, which isn't very useful information. Don't
- // record it in the storage
- } else {
- await this.olmDevice.addInboundGroupSessionWithheld(
- content.room_id,
- senderKey,
- content.session_id,
- content.code,
- content.reason,
- );
- }
-
- // Having recorded the problem, retry decryption on any affected messages.
- // It's unlikely we'll be able to decrypt sucessfully now, but this will
- // update the error message.
- //
- if (content.session_id) {
- await this.retryDecryption(senderKey, content.session_id);
- } else {
- // no_olm messages aren't specific to a given megolm session, so
- // we trigger retrying decryption for all the messages from the sender's
- // key, so that we can update the error message to indicate the olm
- // session problem.
- await this.retryDecryptionFromSender(senderKey);
- }
- }
-
- private async onNoOlmWithheldEvent(event: MatrixEvent): Promise<void> {
- const content = event.getContent();
- const senderKey = content.sender_key;
- const sender = event.getSender()!;
- this.prefixedLogger.warn(`${sender}:${senderKey} was unable to establish an olm session with us`);
- // if the sender says that they haven't been able to establish an olm
- // session, let's proactively establish one
-
- if (await this.olmDevice.getSessionIdForDevice(senderKey)) {
- // a session has already been established, so we don't need to
- // create a new one.
- this.prefixedLogger.debug("New session already created. Not creating a new one.");
- await this.olmDevice.recordSessionProblem(senderKey, "no_olm", true);
- return;
- }
- let device = this.crypto.deviceList.getDeviceByIdentityKey(content.algorithm, senderKey);
- if (!device) {
- // if we don't know about the device, fetch the user's devices again
- // and retry before giving up
- await this.crypto.downloadKeys([sender], false);
- device = this.crypto.deviceList.getDeviceByIdentityKey(content.algorithm, senderKey);
- if (!device) {
- this.prefixedLogger.info(
- "Couldn't find device for identity key " + senderKey + ": not establishing session",
- );
- await this.olmDevice.recordSessionProblem(senderKey, "no_olm", false);
- return;
- }
- }
-
- // XXX: switch this to use encryptAndSendToDevices() rather than duplicating it?
-
- await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, new Map([[sender, [device]]]), false);
- const encryptedContent: IEncryptedContent = {
- algorithm: olmlib.OLM_ALGORITHM,
- sender_key: this.olmDevice.deviceCurve25519Key!,
- ciphertext: {},
- [ToDeviceMessageId]: uuidv4(),
- };
- await olmlib.encryptMessageForDevice(
- encryptedContent.ciphertext,
- this.userId,
- undefined,
- this.olmDevice,
- sender,
- device,
- { type: "m.dummy" },
- );
-
- await this.olmDevice.recordSessionProblem(senderKey, "no_olm", true);
-
- await this.baseApis.sendToDevice(
- "m.room.encrypted",
- new Map([[sender, new Map([[device.deviceId, encryptedContent]])]]),
- );
- }
-
- public hasKeysForKeyRequest(keyRequest: IncomingRoomKeyRequest): Promise<boolean> {
- const body = keyRequest.requestBody;
-
- return this.olmDevice.hasInboundSessionKeys(
- body.room_id,
- body.sender_key,
- body.session_id,
- // TODO: ratchet index
- );
- }
-
- public shareKeysWithDevice(keyRequest: IncomingRoomKeyRequest): void {
- const userId = keyRequest.userId;
- const deviceId = keyRequest.deviceId;
- const deviceInfo = this.crypto.getStoredDevice(userId, deviceId)!;
- const body = keyRequest.requestBody;
-
- // XXX: switch this to use encryptAndSendToDevices()?
-
- this.olmlib
- .ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, new Map([[userId, [deviceInfo]]]))
- .then((devicemap) => {
- const olmSessionResult = devicemap.get(userId)?.get(deviceId);
- if (!olmSessionResult?.sessionId) {
- // no session with this device, probably because there
- // were no one-time keys.
- //
- // ensureOlmSessionsForUsers has already done the logging,
- // so just skip it.
- return null;
- }
-
- this.prefixedLogger.log(
- "sharing keys for session " +
- body.sender_key +
- "|" +
- body.session_id +
- " with device " +
- userId +
- ":" +
- deviceId,
- );
-
- return this.buildKeyForwardingMessage(body.room_id, body.sender_key, body.session_id);
- })
- .then((payload) => {
- const encryptedContent: IEncryptedContent = {
- algorithm: olmlib.OLM_ALGORITHM,
- sender_key: this.olmDevice.deviceCurve25519Key!,
- ciphertext: {},
- [ToDeviceMessageId]: uuidv4(),
- };
-
- return this.olmlib
- .encryptMessageForDevice(
- encryptedContent.ciphertext,
- this.userId,
- undefined,
- this.olmDevice,
- userId,
- deviceInfo,
- payload!,
- )
- .then(() => {
- // TODO: retries
- return this.baseApis.sendToDevice(
- "m.room.encrypted",
- new Map([[userId, new Map([[deviceId, encryptedContent]])]]),
- );
- });
- });
- }
-
- private async buildKeyForwardingMessage(
- roomId: string,
- senderKey: string,
- sessionId: string,
- ): Promise<IKeyForwardingMessage> {
- const key = await this.olmDevice.getInboundGroupSessionKey(roomId, senderKey, sessionId);
-
- return {
- type: "m.forwarded_room_key",
- content: {
- "algorithm": olmlib.MEGOLM_ALGORITHM,
- "room_id": roomId,
- "sender_key": senderKey,
- "sender_claimed_ed25519_key": key!.sender_claimed_ed25519_key!,
- "session_id": sessionId,
- "session_key": key!.key,
- "chain_index": key!.chain_index,
- "forwarding_curve25519_key_chain": key!.forwarding_curve25519_key_chain,
- "org.matrix.msc3061.shared_history": key!.shared_history || false,
- },
- };
- }
-
- /**
- * @param untrusted - whether the key should be considered as untrusted
- * @param source - where the key came from
- */
- public importRoomKey(
- session: IMegolmSessionData,
- { untrusted, source }: { untrusted?: boolean; source?: string } = {},
- ): Promise<void> {
- const extraSessionData: OlmGroupSessionExtraData = {};
- if (untrusted || session.untrusted) {
- extraSessionData.untrusted = true;
- }
- if (session["org.matrix.msc3061.shared_history"]) {
- extraSessionData.sharedHistory = true;
- }
- return this.olmDevice
- .addInboundGroupSession(
- session.room_id,
- session.sender_key,
- session.forwarding_curve25519_key_chain,
- session.session_id,
- session.session_key,
- session.sender_claimed_keys,
- true,
- extraSessionData,
- )
- .then(() => {
- if (source !== "backup") {
- // don't wait for it to complete
- this.crypto.backupManager.backupGroupSession(session.sender_key, session.session_id).catch((e) => {
- // This throws if the upload failed, but this is fine
- // since it will have written it to the db and will retry.
- this.prefixedLogger.log("Failed to back up megolm session", e);
- });
- }
- // have another go at decrypting events sent with this session.
- this.retryDecryption(session.sender_key, session.session_id, !extraSessionData.untrusted);
- });
- }
-
- /**
- * Have another go at decrypting events after we receive a key. Resolves once
- * decryption has been re-attempted on all events.
- *
- * @internal
- * @param forceRedecryptIfUntrusted - whether messages that were already
- * successfully decrypted using untrusted keys should be re-decrypted
- *
- * @returns whether all messages were successfully
- * decrypted with trusted keys
- */
- private async retryDecryption(
- senderKey: string,
- sessionId: string,
- forceRedecryptIfUntrusted?: boolean,
- ): Promise<boolean> {
- const senderPendingEvents = this.pendingEvents.get(senderKey);
- if (!senderPendingEvents) {
- return true;
- }
-
- const pending = senderPendingEvents.get(sessionId);
- if (!pending) {
- return true;
- }
-
- const pendingList = [...pending];
- this.prefixedLogger.debug(
- "Retrying decryption on events:",
- pendingList.map((e) => `${e.getId()}`),
- );
-
- await Promise.all(
- pendingList.map(async (ev) => {
- try {
- await ev.attemptDecryption(this.crypto, { isRetry: true, forceRedecryptIfUntrusted });
- } catch (e) {
- // don't die if something goes wrong
- }
- }),
- );
-
- // If decrypted successfully with trusted keys, they'll have
- // been removed from pendingEvents
- return !this.pendingEvents.get(senderKey)?.has(sessionId);
- }
-
- public async retryDecryptionFromSender(senderKey: string): Promise<boolean> {
- const senderPendingEvents = this.pendingEvents.get(senderKey);
- if (!senderPendingEvents) {
- return true;
- }
-
- this.pendingEvents.delete(senderKey);
-
- await Promise.all(
- [...senderPendingEvents].map(async ([_sessionId, pending]) => {
- await Promise.all(
- [...pending].map(async (ev) => {
- try {
- await ev.attemptDecryption(this.crypto);
- } catch (e) {
- // don't die if something goes wrong
- }
- }),
- );
- }),
- );
-
- return !this.pendingEvents.has(senderKey);
- }
-
- public async sendSharedHistoryInboundSessions(devicesByUser: Map<string, DeviceInfo[]>): Promise<void> {
- await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser);
-
- const sharedHistorySessions = await this.olmDevice.getSharedHistoryInboundGroupSessions(this.roomId);
- this.prefixedLogger.log(
- `Sharing history in with users ${Array.from(devicesByUser.keys())}`,
- sharedHistorySessions.map(([senderKey, sessionId]) => `${senderKey}|${sessionId}`),
- );
- for (const [senderKey, sessionId] of sharedHistorySessions) {
- const payload = await this.buildKeyForwardingMessage(this.roomId, senderKey, sessionId);
-
- // FIXME: use encryptAndSendToDevices() rather than duplicating it here.
- const promises: Promise<unknown>[] = [];
- const contentMap: Map<string, Map<string, IEncryptedContent>> = new Map();
- for (const [userId, devices] of devicesByUser) {
- const deviceMessages = new Map();
- contentMap.set(userId, deviceMessages);
- for (const deviceInfo of devices) {
- const encryptedContent: IEncryptedContent = {
- algorithm: olmlib.OLM_ALGORITHM,
- sender_key: this.olmDevice.deviceCurve25519Key!,
- ciphertext: {},
- [ToDeviceMessageId]: uuidv4(),
- };
- deviceMessages.set(deviceInfo.deviceId, encryptedContent);
- promises.push(
- olmlib.encryptMessageForDevice(
- encryptedContent.ciphertext,
- this.userId,
- undefined,
- this.olmDevice,
- userId,
- deviceInfo,
- payload,
- ),
- );
- }
- }
- await Promise.all(promises);
-
- // prune out any devices that encryptMessageForDevice could not encrypt for,
- // in which case it will have just not added anything to the ciphertext object.
- // There's no point sending messages to devices if we couldn't encrypt to them,
- // since that's effectively a blank message.
- for (const [userId, deviceMessages] of contentMap) {
- for (const [deviceId, content] of deviceMessages) {
- if (!hasCiphertext(content)) {
- this.prefixedLogger.log("No ciphertext for device " + userId + ":" + deviceId + ": pruning");
- deviceMessages.delete(deviceId);
- }
- }
- // No devices left for that user? Strip that too.
- if (deviceMessages.size === 0) {
- this.prefixedLogger.log("Pruned all devices for user " + userId);
- contentMap.delete(userId);
- }
- }
-
- // Is there anything left?
- if (contentMap.size === 0) {
- this.prefixedLogger.log("No users left to send to: aborting");
- return;
- }
-
- await this.baseApis.sendToDevice("m.room.encrypted", contentMap);
- }
- }
-}
-
-const PROBLEM_DESCRIPTIONS = {
- no_olm: "The sender was unable to establish a secure channel.",
- unknown: "The secure channel with the sender was corrupted.",
-};
-
-registerAlgorithm(olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption);
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/olm.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/olm.ts
deleted file mode 100644
index 1a79554..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/algorithms/olm.ts
+++ /dev/null
@@ -1,329 +0,0 @@
-/*
-Copyright 2016 - 2021 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.
-*/
-
-/**
- * Defines m.olm encryption/decryption
- */
-
-import type { IEventDecryptionResult } from "../../@types/crypto";
-import { logger } from "../../logger";
-import * as olmlib from "../olmlib";
-import { DeviceInfo } from "../deviceinfo";
-import { DecryptionAlgorithm, DecryptionError, EncryptionAlgorithm, registerAlgorithm } from "./base";
-import { Room } from "../../models/room";
-import { IContent, MatrixEvent } from "../../models/event";
-import { IEncryptedContent, IOlmEncryptedContent } from "../index";
-import { IInboundSession } from "../OlmDevice";
-
-const DeviceVerification = DeviceInfo.DeviceVerification;
-
-export interface IMessage {
- type: number;
- body: string;
-}
-
-/**
- * Olm encryption implementation
- *
- * @param params - parameters, as per {@link EncryptionAlgorithm}
- */
-class OlmEncryption extends EncryptionAlgorithm {
- private sessionPrepared = false;
- private prepPromise: Promise<void> | null = null;
-
- /**
- * @internal
-
- * @param roomMembers - list of currently-joined users in the room
- * @returns Promise which resolves when setup is complete
- */
- private ensureSession(roomMembers: string[]): Promise<void> {
- if (this.prepPromise) {
- // prep already in progress
- return this.prepPromise;
- }
-
- if (this.sessionPrepared) {
- // prep already done
- return Promise.resolve();
- }
-
- this.prepPromise = this.crypto
- .downloadKeys(roomMembers)
- .then(() => {
- return this.crypto.ensureOlmSessionsForUsers(roomMembers);
- })
- .then(() => {
- this.sessionPrepared = true;
- })
- .finally(() => {
- this.prepPromise = null;
- });
-
- return this.prepPromise;
- }
-
- /**
- * @param content - plaintext event content
- *
- * @returns Promise which resolves to the new event body
- */
- public async encryptMessage(room: Room, eventType: string, content: IContent): Promise<IOlmEncryptedContent> {
- // pick the list of recipients based on the membership list.
- //
- // TODO: there is a race condition here! What if a new user turns up
- // just as you are sending a secret message?
-
- const members = await room.getEncryptionTargetMembers();
-
- const users = members.map(function (u) {
- return u.userId;
- });
-
- await this.ensureSession(users);
-
- const payloadFields = {
- room_id: room.roomId,
- type: eventType,
- content: content,
- };
-
- const encryptedContent: IEncryptedContent = {
- algorithm: olmlib.OLM_ALGORITHM,
- sender_key: this.olmDevice.deviceCurve25519Key!,
- ciphertext: {},
- };
-
- const promises: Promise<void>[] = [];
-
- for (const userId of users) {
- const devices = this.crypto.getStoredDevicesForUser(userId) || [];
-
- for (const deviceInfo of devices) {
- const key = deviceInfo.getIdentityKey();
- if (key == this.olmDevice.deviceCurve25519Key) {
- // don't bother sending to ourself
- continue;
- }
- if (deviceInfo.verified == DeviceVerification.BLOCKED) {
- // don't bother setting up sessions with blocked users
- continue;
- }
-
- promises.push(
- olmlib.encryptMessageForDevice(
- encryptedContent.ciphertext,
- this.userId,
- this.deviceId,
- this.olmDevice,
- userId,
- deviceInfo,
- payloadFields,
- ),
- );
- }
- }
-
- return Promise.all(promises).then(() => encryptedContent);
- }
-}
-
-/**
- * Olm decryption implementation
- *
- * @param params - parameters, as per {@link DecryptionAlgorithm}
- */
-class OlmDecryption extends DecryptionAlgorithm {
- /**
- * returns a promise which resolves to a
- * {@link EventDecryptionResult} once we have finished
- * decrypting. Rejects with an `algorithms.DecryptionError` if there is a
- * problem decrypting the event.
- */
- public async decryptEvent(event: MatrixEvent): Promise<IEventDecryptionResult> {
- const content = event.getWireContent();
- const deviceKey = content.sender_key;
- const ciphertext = content.ciphertext;
-
- if (!ciphertext) {
- throw new DecryptionError("OLM_MISSING_CIPHERTEXT", "Missing ciphertext");
- }
-
- if (!(this.olmDevice.deviceCurve25519Key! in ciphertext)) {
- throw new DecryptionError("OLM_NOT_INCLUDED_IN_RECIPIENTS", "Not included in recipients");
- }
- const message = ciphertext[this.olmDevice.deviceCurve25519Key!];
- let payloadString: string;
-
- try {
- payloadString = await this.decryptMessage(deviceKey, message);
- } catch (e) {
- throw new DecryptionError("OLM_BAD_ENCRYPTED_MESSAGE", "Bad Encrypted Message", {
- sender: deviceKey,
- err: e as Error,
- });
- }
-
- const payload = JSON.parse(payloadString);
-
- // check that we were the intended recipient, to avoid unknown-key attack
- // https://github.com/vector-im/vector-web/issues/2483
- if (payload.recipient != this.userId) {
- throw new DecryptionError("OLM_BAD_RECIPIENT", "Message was intented for " + payload.recipient);
- }
-
- if (payload.recipient_keys.ed25519 != this.olmDevice.deviceEd25519Key) {
- throw new DecryptionError("OLM_BAD_RECIPIENT_KEY", "Message not intended for this device", {
- intended: payload.recipient_keys.ed25519,
- our_key: this.olmDevice.deviceEd25519Key!,
- });
- }
-
- // check that the device that encrypted the event belongs to the user
- // that the event claims it's from. We need to make sure that our
- // device list is up-to-date. If the device is unknown, we can only
- // assume that the device logged out. Some event handlers, such as
- // secret sharing, may be more strict and reject events that come from
- // unknown devices.
- await this.crypto.deviceList.downloadKeys([event.getSender()!], false);
- const senderKeyUser = this.crypto.deviceList.getUserByIdentityKey(olmlib.OLM_ALGORITHM, deviceKey);
- if (senderKeyUser !== event.getSender() && senderKeyUser != undefined) {
- throw new DecryptionError("OLM_BAD_SENDER", "Message claimed to be from " + event.getSender(), {
- real_sender: senderKeyUser,
- });
- }
-
- // check that the original sender matches what the homeserver told us, to
- // avoid people masquerading as others.
- // (this check is also provided via the sender's embedded ed25519 key,
- // which is checked elsewhere).
- if (payload.sender != event.getSender()) {
- throw new DecryptionError("OLM_FORWARDED_MESSAGE", "Message forwarded from " + payload.sender, {
- reported_sender: event.getSender()!,
- });
- }
-
- // Olm events intended for a room have a room_id.
- if (payload.room_id !== event.getRoomId()) {
- throw new DecryptionError("OLM_BAD_ROOM", "Message intended for room " + payload.room_id, {
- reported_room: event.getRoomId() || "ROOM_ID_UNDEFINED",
- });
- }
-
- const claimedKeys = payload.keys || {};
-
- return {
- clearEvent: payload,
- senderCurve25519Key: deviceKey,
- claimedEd25519Key: claimedKeys.ed25519 || null,
- };
- }
-
- /**
- * Attempt to decrypt an Olm message
- *
- * @param theirDeviceIdentityKey - Curve25519 identity key of the sender
- * @param message - message object, with 'type' and 'body' fields
- *
- * @returns payload, if decrypted successfully.
- */
- 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.
- if (message.type !== 0) {
- // not a prekey message: we can safely just try & decrypt it
- return this.reallyDecryptMessage(theirDeviceIdentityKey, message);
- } else {
- const myPromise = this.olmDevice.olmPrekeyPromise.then(() => {
- return this.reallyDecryptMessage(theirDeviceIdentityKey, message);
- });
- // we want the error, but don't propagate it to the next decryption
- this.olmDevice.olmPrekeyPromise = myPromise.catch(() => {});
- return myPromise;
- }
- }
-
- private async reallyDecryptMessage(theirDeviceIdentityKey: string, message: IMessage): Promise<string> {
- const sessionIds = await this.olmDevice.getSessionIdsForDevice(theirDeviceIdentityKey);
-
- // try each session in turn.
- const decryptionErrors: Record<string, string> = {};
- for (const sessionId of sessionIds) {
- try {
- const payload = await this.olmDevice.decryptMessage(
- theirDeviceIdentityKey,
- sessionId,
- message.type,
- message.body,
- );
- logger.log("Decrypted Olm message from " + theirDeviceIdentityKey + " with session " + sessionId);
- return payload;
- } catch (e) {
- const foundSession = await this.olmDevice.matchesSession(
- theirDeviceIdentityKey,
- sessionId,
- message.type,
- message.body,
- );
-
- if (foundSession) {
- // decryption failed, but it was a prekey message matching this
- // session, so it should have worked.
- throw new Error(
- "Error decrypting prekey message with existing session id " +
- sessionId +
- ": " +
- (<Error>e).message,
- );
- }
-
- // otherwise it's probably a message for another session; carry on, but
- // keep a record of the error
- decryptionErrors[sessionId] = (<Error>e).message;
- }
- }
-
- if (message.type !== 0) {
- // not a prekey message, so it should have matched an existing session, but it
- // didn't work.
-
- if (sessionIds.length === 0) {
- throw new Error("No existing sessions");
- }
-
- throw new Error(
- "Error decrypting non-prekey message with existing sessions: " + JSON.stringify(decryptionErrors),
- );
- }
-
- // prekey message which doesn't match any existing sessions: make a new
- // session.
-
- let res: IInboundSession;
- try {
- res = await this.olmDevice.createInboundSession(theirDeviceIdentityKey, message.type, message.body);
- } catch (e) {
- decryptionErrors["(new)"] = (<Error>e).message;
- throw new Error("Error decrypting prekey message: " + JSON.stringify(decryptionErrors));
- }
-
- logger.log("created new inbound Olm session ID " + res.session_id + " with " + theirDeviceIdentityKey);
- return res.payload;
- }
-}
-
-registerAlgorithm(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption);
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/api.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/api.ts
deleted file mode 100644
index 9e9ba52..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/api.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
-Copyright 2021 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 { DeviceInfo } from "./deviceinfo";
-import { IKeyBackupInfo } from "./keybackup";
-import { PassphraseInfo } from "../secret-storage";
-
-/* re-exports for backwards compatibility. */
-export {
- PassphraseInfo as IPassphraseInfo,
- SecretStorageKeyDescription as ISecretStorageKeyInfo,
-} from "../secret-storage";
-
-// TODO: Merge this with crypto.js once converted
-
-export enum CrossSigningKey {
- Master = "master",
- SelfSigning = "self_signing",
- UserSigning = "user_signing",
-}
-
-export interface IEncryptedEventInfo {
- /**
- * whether the event is encrypted (if not encrypted, some of the other properties may not be set)
- */
- encrypted: boolean;
-
- /**
- * the sender's key
- */
- senderKey: string;
-
- /**
- * the algorithm used to encrypt the event
- */
- algorithm: string;
-
- /**
- * whether we can be sure that the owner of the senderKey sent the event
- */
- authenticated: boolean;
-
- /**
- * the sender's device information, if available
- */
- sender?: DeviceInfo;
-
- /**
- * if the event's ed25519 and curve25519 keys don't match (only meaningful if `sender` is set)
- */
- mismatchedSender: boolean;
-}
-
-export interface IRecoveryKey {
- keyInfo?: IAddSecretStorageKeyOpts;
- privateKey: Uint8Array;
- encodedPrivateKey?: string;
-}
-
-export interface ICreateSecretStorageOpts {
- /**
- * Function called to await a secret storage key creation flow.
- * @returns Promise resolving to an object with public key metadata, encoded private
- * recovery key which should be disposed of after displaying to the user,
- * and raw private key to avoid round tripping if needed.
- */
- createSecretStorageKey?: () => Promise<IRecoveryKey>;
-
- /**
- * The current key backup object. If passed,
- * the passphrase and recovery key from this backup will be used.
- */
- keyBackupInfo?: IKeyBackupInfo;
-
- /**
- * If true, a new key backup version will be
- * created and the private key stored in the new SSSS store. Ignored if keyBackupInfo
- * is supplied.
- */
- setupNewKeyBackup?: boolean;
-
- /**
- * Reset even if keys already exist.
- */
- setupNewSecretStorage?: boolean;
-
- /**
- * Function called to get the user's
- * current key backup passphrase. Should return a promise that resolves with a Uint8Array
- * containing the key, or rejects if the key cannot be obtained.
- */
- getKeyBackupPassphrase?: () => Promise<Uint8Array>;
-}
-
-export interface IAddSecretStorageKeyOpts {
- pubkey?: string;
- passphrase?: PassphraseInfo;
- name?: string;
- key?: Uint8Array;
-}
-
-export interface IImportOpts {
- stage: string; // TODO: Enum
- successes: number;
- failures: number;
- total: number;
-}
-
-export interface IImportRoomKeysOpts {
- /** called with an object that has a "stage" param */
- progressCallback?: (stage: IImportOpts) => void;
- untrusted?: boolean;
- source?: string; // TODO: Enum
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/backup.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/backup.ts
deleted file mode 100644
index d240bda..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/backup.ts
+++ /dev/null
@@ -1,813 +0,0 @@
-/*
-Copyright 2021 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.
-*/
-
-/**
- * Classes for dealing with key backup.
- */
-
-import type { IMegolmSessionData } from "../@types/crypto";
-import { MatrixClient } from "../client";
-import { logger } from "../logger";
-import { MEGOLM_ALGORITHM, verifySignature } from "./olmlib";
-import { DeviceInfo } from "./deviceinfo";
-import { DeviceTrustLevel } from "./CrossSigning";
-import { keyFromPassphrase } from "./key_passphrase";
-import { safeSet, sleep } from "../utils";
-import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store";
-import { encodeRecoveryKey } from "./recoverykey";
-import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./aes";
-import {
- Curve25519SessionData,
- IAes256AuthData,
- ICurve25519AuthData,
- IKeyBackupInfo,
- IKeyBackupSession,
-} from "./keybackup";
-import { UnstableValue } from "../NamespacedValue";
-import { CryptoEvent } from "./index";
-import { crypto } from "./crypto";
-import { HTTPError, MatrixError } from "../http-api";
-
-const KEY_BACKUP_KEYS_PER_REQUEST = 200;
-const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms
-
-type AuthData = IKeyBackupInfo["auth_data"];
-
-type SigInfo = {
- deviceId: string;
- valid?: boolean | null; // true: valid, false: invalid, null: cannot attempt validation
- device?: DeviceInfo | null;
- crossSigningId?: boolean;
- deviceTrust?: DeviceTrustLevel;
-};
-
-export type TrustInfo = {
- usable: boolean; // is the backup trusted, true iff there is a sig that is valid & from a trusted device
- sigs: SigInfo[];
- // eslint-disable-next-line camelcase
- trusted_locally?: boolean;
-};
-
-export interface IKeyBackupCheck {
- backupInfo?: IKeyBackupInfo;
- trustInfo: TrustInfo;
-}
-
-/* eslint-disable camelcase */
-export interface IPreparedKeyBackupVersion {
- algorithm: string;
- auth_data: AuthData;
- recovery_key: string;
- privateKey: Uint8Array;
-}
-/* eslint-enable camelcase */
-
-/** A function used to get the secret key for a backup.
- */
-type GetKey = () => Promise<ArrayLike<number>>;
-
-interface BackupAlgorithmClass {
- algorithmName: string;
- // initialize from an existing backup
- init(authData: AuthData, getKey: GetKey): Promise<BackupAlgorithm>;
-
- // prepare a brand new backup
- prepare(key?: string | Uint8Array | null): Promise<[Uint8Array, AuthData]>;
-
- checkBackupVersion(info: IKeyBackupInfo): void;
-}
-
-interface BackupAlgorithm {
- untrusted: boolean;
- encryptSession(data: Record<string, any>): Promise<Curve25519SessionData | IEncryptedPayload>;
- decryptSessions(ciphertexts: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]>;
- authData: AuthData;
- keyMatches(key: ArrayLike<number>): Promise<boolean>;
- free(): void;
-}
-
-export interface IKeyBackup {
- rooms: {
- [roomId: string]: {
- sessions: {
- [sessionId: string]: IKeyBackupSession;
- };
- };
- };
-}
-
-/**
- * Manages the key backup.
- */
-export class BackupManager {
- private algorithm: BackupAlgorithm | undefined;
- public backupInfo: IKeyBackupInfo | undefined; // The info dict from /room_keys/version
- public checkedForBackup: boolean; // Have we checked the server for a backup we can use?
- private sendingBackups: boolean; // Are we currently sending backups?
- private sessionLastCheckAttemptedTime: Record<string, number> = {}; // When did we last try to check the server for a given session id?
-
- public constructor(private readonly baseApis: MatrixClient, public readonly getKey: GetKey) {
- this.checkedForBackup = false;
- this.sendingBackups = false;
- }
-
- public get version(): string | undefined {
- return this.backupInfo && this.backupInfo.version;
- }
-
- /**
- * Performs a quick check to ensure that the backup info looks sane.
- *
- * Throws an error if a problem is detected.
- *
- * @param info - the key backup info
- */
- public static checkBackupVersion(info: IKeyBackupInfo): void {
- const Algorithm = algorithmsByName[info.algorithm];
- if (!Algorithm) {
- throw new Error("Unknown backup algorithm: " + info.algorithm);
- }
- if (typeof info.auth_data !== "object") {
- throw new Error("Invalid backup data returned");
- }
- return Algorithm.checkBackupVersion(info);
- }
-
- public static makeAlgorithm(info: IKeyBackupInfo, getKey: GetKey): Promise<BackupAlgorithm> {
- const Algorithm = algorithmsByName[info.algorithm];
- if (!Algorithm) {
- throw new Error("Unknown backup algorithm");
- }
- return Algorithm.init(info.auth_data, getKey);
- }
-
- public async enableKeyBackup(info: IKeyBackupInfo): Promise<void> {
- this.backupInfo = info;
- if (this.algorithm) {
- this.algorithm.free();
- }
-
- this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey);
-
- this.baseApis.emit(CryptoEvent.KeyBackupStatus, true);
-
- // There may be keys left over from a partially completed backup, so
- // schedule a send to check.
- this.scheduleKeyBackupSend();
- }
-
- /**
- * Disable backing up of keys.
- */
- public disableKeyBackup(): void {
- if (this.algorithm) {
- this.algorithm.free();
- }
- this.algorithm = undefined;
-
- this.backupInfo = undefined;
-
- this.baseApis.emit(CryptoEvent.KeyBackupStatus, false);
- }
-
- public getKeyBackupEnabled(): boolean | null {
- if (!this.checkedForBackup) {
- return null;
- }
- return Boolean(this.algorithm);
- }
-
- public async prepareKeyBackupVersion(
- key?: string | Uint8Array | null,
- algorithm?: string | undefined,
- ): Promise<IPreparedKeyBackupVersion> {
- const Algorithm = algorithm ? algorithmsByName[algorithm] : DefaultAlgorithm;
- if (!Algorithm) {
- throw new Error("Unknown backup algorithm");
- }
-
- const [privateKey, authData] = await Algorithm.prepare(key);
- const recoveryKey = encodeRecoveryKey(privateKey)!;
- return {
- algorithm: Algorithm.algorithmName,
- auth_data: authData,
- recovery_key: recoveryKey,
- privateKey,
- };
- }
-
- public async createKeyBackupVersion(info: IKeyBackupInfo): Promise<void> {
- this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey);
- }
-
- /**
- * Check the server for an active key backup and
- * if one is present and has a valid signature from
- * one of the user's verified devices, start backing up
- * to it.
- */
- public async checkAndStart(): Promise<IKeyBackupCheck | null> {
- logger.log("Checking key backup status...");
- if (this.baseApis.isGuest()) {
- logger.log("Skipping key backup check since user is guest");
- this.checkedForBackup = true;
- return null;
- }
- let backupInfo: IKeyBackupInfo | undefined;
- try {
- backupInfo = (await this.baseApis.getKeyBackupVersion()) ?? undefined;
- } catch (e) {
- logger.log("Error checking for active key backup", e);
- if ((<HTTPError>e).httpStatus === 404) {
- // 404 is returned when the key backup does not exist, so that
- // counts as successfully checking.
- this.checkedForBackup = true;
- }
- return null;
- }
- this.checkedForBackup = true;
-
- const trustInfo = await this.isKeyBackupTrusted(backupInfo);
-
- if (trustInfo.usable && !this.backupInfo) {
- logger.log(`Found usable key backup v${backupInfo!.version}: enabling key backups`);
- await this.enableKeyBackup(backupInfo!);
- } else if (!trustInfo.usable && this.backupInfo) {
- logger.log("No usable key backup: disabling key backup");
- this.disableKeyBackup();
- } else if (!trustInfo.usable && !this.backupInfo) {
- logger.log("No usable key backup: not enabling key backup");
- } else if (trustInfo.usable && this.backupInfo) {
- // may not be the same version: if not, we should switch
- if (backupInfo!.version !== this.backupInfo.version) {
- logger.log(
- `On backup version ${this.backupInfo.version} but ` +
- `found version ${backupInfo!.version}: switching.`,
- );
- this.disableKeyBackup();
- await this.enableKeyBackup(backupInfo!);
- // We're now using a new backup, so schedule all the keys we have to be
- // uploaded to the new backup. This is a bit of a workaround to upload
- // keys to a new backup in *most* cases, but it won't cover all cases
- // because we don't remember what backup version we uploaded keys to:
- // see https://github.com/vector-im/element-web/issues/14833
- await this.scheduleAllGroupSessionsForBackup();
- } else {
- logger.log(`Backup version ${backupInfo!.version} still current`);
- }
- }
-
- return { backupInfo, trustInfo };
- }
-
- /**
- * Forces a re-check of the key backup and enables/disables it
- * as appropriate.
- *
- * @returns Object with backup info (as returned by
- * getKeyBackupVersion) in backupInfo and
- * trust information (as returned by isKeyBackupTrusted)
- * in trustInfo.
- */
- public async checkKeyBackup(): Promise<IKeyBackupCheck | null> {
- this.checkedForBackup = false;
- return this.checkAndStart();
- }
-
- /**
- * Attempts to retrieve a session from a key backup, if enough time
- * has elapsed since the last check for this session id.
- */
- public async queryKeyBackupRateLimited(
- targetRoomId: string | undefined,
- targetSessionId: string | undefined,
- ): Promise<void> {
- if (!this.backupInfo) {
- return;
- }
-
- const now = new Date().getTime();
- if (
- !this.sessionLastCheckAttemptedTime[targetSessionId!] ||
- now - this.sessionLastCheckAttemptedTime[targetSessionId!] > KEY_BACKUP_CHECK_RATE_LIMIT
- ) {
- this.sessionLastCheckAttemptedTime[targetSessionId!] = now;
- await this.baseApis.restoreKeyBackupWithCache(targetRoomId!, targetSessionId!, this.backupInfo, {});
- }
- }
-
- /**
- * Check if the given backup info is trusted.
- *
- * @param backupInfo - key backup info dict from /room_keys/version
- */
- public async isKeyBackupTrusted(backupInfo?: IKeyBackupInfo): Promise<TrustInfo> {
- const ret = {
- usable: false,
- trusted_locally: false,
- sigs: [] as SigInfo[],
- };
-
- if (!backupInfo || !backupInfo.algorithm || !backupInfo.auth_data || !backupInfo.auth_data.signatures) {
- logger.info("Key backup is absent or missing required data");
- return ret;
- }
-
- const userId = this.baseApis.getUserId()!;
- const privKey = await this.baseApis.crypto!.getSessionBackupPrivateKey();
- if (privKey) {
- let algorithm: BackupAlgorithm | null = null;
- try {
- algorithm = await BackupManager.makeAlgorithm(backupInfo, async () => privKey);
-
- if (await algorithm.keyMatches(privKey)) {
- logger.info("Backup is trusted locally");
- ret.trusted_locally = true;
- }
- } catch {
- // do nothing -- if we have an error, then we don't mark it as
- // locally trusted
- } finally {
- algorithm?.free();
- }
- }
-
- const mySigs = backupInfo.auth_data.signatures[userId] || {};
-
- for (const keyId of Object.keys(mySigs)) {
- const keyIdParts = keyId.split(":");
- if (keyIdParts[0] !== "ed25519") {
- logger.log("Ignoring unknown signature type: " + keyIdParts[0]);
- continue;
- }
- // Could be a cross-signing master key, but just say this is the device
- // ID for backwards compat
- const sigInfo: SigInfo = { deviceId: keyIdParts[1] };
-
- // first check to see if it's from our cross-signing key
- const crossSigningId = this.baseApis.crypto!.crossSigningInfo.getId();
- if (crossSigningId === sigInfo.deviceId) {
- sigInfo.crossSigningId = true;
- try {
- await verifySignature(
- this.baseApis.crypto!.olmDevice,
- backupInfo.auth_data,
- userId,
- sigInfo.deviceId,
- crossSigningId,
- );
- sigInfo.valid = true;
- } catch (e) {
- logger.warn("Bad signature from cross signing key " + crossSigningId, e);
- sigInfo.valid = false;
- }
- ret.sigs.push(sigInfo);
- continue;
- }
-
- // Now look for a sig from a device
- // At some point this can probably go away and we'll just support
- // it being signed by the cross-signing master key
- const device = this.baseApis.crypto!.deviceList.getStoredDevice(userId, sigInfo.deviceId);
- if (device) {
- sigInfo.device = device;
- sigInfo.deviceTrust = this.baseApis.checkDeviceTrust(userId, sigInfo.deviceId);
- try {
- await verifySignature(
- this.baseApis.crypto!.olmDevice,
- backupInfo.auth_data,
- userId,
- device.deviceId,
- device.getFingerprint(),
- );
- sigInfo.valid = true;
- } catch (e) {
- logger.info(
- "Bad signature from key ID " +
- keyId +
- " userID " +
- this.baseApis.getUserId() +
- " device ID " +
- device.deviceId +
- " fingerprint: " +
- device.getFingerprint(),
- backupInfo.auth_data,
- e,
- );
- sigInfo.valid = false;
- }
- } else {
- sigInfo.valid = null; // Can't determine validity because we don't have the signing device
- logger.info("Ignoring signature from unknown key " + keyId);
- }
- ret.sigs.push(sigInfo);
- }
-
- ret.usable = ret.sigs.some((s) => {
- return s.valid && ((s.device && s.deviceTrust?.isVerified()) || s.crossSigningId);
- });
- return ret;
- }
-
- /**
- * Schedules sending all keys waiting to be sent to the backup, if not already
- * scheduled. Retries if necessary.
- *
- * @param maxDelay - Maximum delay to wait in ms. 0 means no delay.
- */
- public async scheduleKeyBackupSend(maxDelay = 10000): Promise<void> {
- if (this.sendingBackups) return;
-
- this.sendingBackups = true;
-
- try {
- // wait between 0 and `maxDelay` seconds, to avoid backup
- // 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);
- let numFailures = 0; // number of consecutive failures
- for (;;) {
- if (!this.algorithm) {
- return;
- }
- try {
- const numBackedUp = await this.backupPendingKeys(KEY_BACKUP_KEYS_PER_REQUEST);
- if (numBackedUp === 0) {
- // no sessions left needing backup: we're done
- return;
- }
- numFailures = 0;
- } catch (err) {
- numFailures++;
- logger.log("Key backup request failed", err);
- if ((<MatrixError>err).data) {
- if (
- (<MatrixError>err).data.errcode == "M_NOT_FOUND" ||
- (<MatrixError>err).data.errcode == "M_WRONG_ROOM_KEYS_VERSION"
- ) {
- // Re-check key backup status on error, so we can be
- // sure to present the current situation when asked.
- await this.checkKeyBackup();
- // Backup version has changed or this backup version
- // has been deleted
- this.baseApis.crypto!.emit(CryptoEvent.KeyBackupFailed, (<MatrixError>err).data.errcode!);
- throw err;
- }
- }
- }
- if (numFailures) {
- // exponential backoff if we have failures
- await sleep(1000 * Math.pow(2, Math.min(numFailures - 1, 4)));
- }
- }
- } finally {
- this.sendingBackups = false;
- }
- }
-
- /**
- * Take some e2e keys waiting to be backed up and send them
- * to the backup.
- *
- * @param limit - Maximum number of keys to back up
- * @returns Number of sessions backed up
- */
- public async backupPendingKeys(limit: number): Promise<number> {
- const sessions = await this.baseApis.crypto!.cryptoStore.getSessionsNeedingBackup(limit);
- if (!sessions.length) {
- return 0;
- }
-
- let remaining = await this.baseApis.crypto!.cryptoStore.countSessionsNeedingBackup();
- this.baseApis.crypto!.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
-
- const rooms: IKeyBackup["rooms"] = {};
- for (const session of sessions) {
- const roomId = session.sessionData!.room_id;
- safeSet(rooms, roomId, rooms[roomId] || { sessions: {} });
-
- const sessionData = this.baseApis.crypto!.olmDevice.exportInboundGroupSession(
- session.senderKey,
- session.sessionId,
- session.sessionData!,
- );
- sessionData.algorithm = MEGOLM_ALGORITHM;
-
- const forwardedCount = (sessionData.forwarding_curve25519_key_chain || []).length;
-
- const userId = this.baseApis.crypto!.deviceList.getUserByIdentityKey(MEGOLM_ALGORITHM, session.senderKey);
- const device =
- this.baseApis.crypto!.deviceList.getDeviceByIdentityKey(MEGOLM_ALGORITHM, session.senderKey) ??
- undefined;
- const verified = this.baseApis.crypto!.checkDeviceInfoTrust(userId!, device).isVerified();
-
- safeSet(rooms[roomId]["sessions"], session.sessionId, {
- first_message_index: sessionData.first_known_index,
- forwarded_count: forwardedCount,
- is_verified: verified,
- session_data: await this.algorithm!.encryptSession(sessionData),
- });
- }
-
- await this.baseApis.sendKeyBackup(undefined, undefined, this.backupInfo!.version, { rooms });
-
- await this.baseApis.crypto!.cryptoStore.unmarkSessionsNeedingBackup(sessions);
- remaining = await this.baseApis.crypto!.cryptoStore.countSessionsNeedingBackup();
- this.baseApis.crypto!.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
-
- return sessions.length;
- }
-
- public async backupGroupSession(senderKey: string, sessionId: string): Promise<void> {
- await this.baseApis.crypto!.cryptoStore.markSessionsNeedingBackup([
- {
- senderKey: senderKey,
- sessionId: sessionId,
- },
- ]);
-
- if (this.backupInfo) {
- // don't wait for this to complete: it will delay so
- // happens in the background
- this.scheduleKeyBackupSend();
- }
- // if this.backupInfo is not set, then the keys will be backed up when
- // this.enableKeyBackup is called
- }
-
- /**
- * Marks all group sessions as needing to be backed up and schedules them to
- * upload in the background as soon as possible.
- */
- public async scheduleAllGroupSessionsForBackup(): Promise<void> {
- await this.flagAllGroupSessionsForBackup();
-
- // Schedule keys to upload in the background as soon as possible.
- this.scheduleKeyBackupSend(0 /* maxDelay */);
- }
-
- /**
- * Marks all group sessions as needing to be backed up without scheduling
- * them to upload in the background.
- * @returns Promise which resolves to the number of sessions now requiring a backup
- * (which will be equal to the number of sessions in the store).
- */
- public async flagAllGroupSessionsForBackup(): Promise<number> {
- await this.baseApis.crypto!.cryptoStore.doTxn(
- "readwrite",
- [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, IndexedDBCryptoStore.STORE_BACKUP],
- (txn) => {
- this.baseApis.crypto!.cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => {
- if (session !== null) {
- this.baseApis.crypto!.cryptoStore.markSessionsNeedingBackup([session], txn);
- }
- });
- },
- );
-
- const remaining = await this.baseApis.crypto!.cryptoStore.countSessionsNeedingBackup();
- this.baseApis.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining);
- return remaining;
- }
-
- /**
- * Counts the number of end to end session keys that are waiting to be backed up
- * @returns Promise which resolves to the number of sessions requiring backup
- */
- public countSessionsNeedingBackup(): Promise<number> {
- return this.baseApis.crypto!.cryptoStore.countSessionsNeedingBackup();
- }
-}
-
-export class Curve25519 implements BackupAlgorithm {
- public static algorithmName = "m.megolm_backup.v1.curve25519-aes-sha2";
-
- public constructor(
- public authData: ICurve25519AuthData,
- private publicKey: any, // FIXME: PkEncryption
- private getKey: () => Promise<Uint8Array>,
- ) {}
-
- public static async init(authData: AuthData, getKey: () => Promise<Uint8Array>): Promise<Curve25519> {
- if (!authData || !("public_key" in authData)) {
- throw new Error("auth_data missing required information");
- }
- const publicKey = new global.Olm.PkEncryption();
- publicKey.set_recipient_key(authData.public_key);
- return new Curve25519(authData as ICurve25519AuthData, publicKey, getKey);
- }
-
- public static async prepare(key?: string | Uint8Array | null): Promise<[Uint8Array, AuthData]> {
- const decryption = new global.Olm.PkDecryption();
- try {
- const authData: Partial<ICurve25519AuthData> = {};
- if (!key) {
- authData.public_key = decryption.generate_key();
- } else if (key instanceof Uint8Array) {
- authData.public_key = decryption.init_with_private_key(key);
- } else {
- const derivation = await keyFromPassphrase(key);
- authData.private_key_salt = derivation.salt;
- authData.private_key_iterations = derivation.iterations;
- authData.public_key = decryption.init_with_private_key(derivation.key);
- }
- const publicKey = new global.Olm.PkEncryption();
- publicKey.set_recipient_key(authData.public_key);
-
- return [decryption.get_private_key(), authData as AuthData];
- } finally {
- decryption.free();
- }
- }
-
- public static checkBackupVersion(info: IKeyBackupInfo): void {
- if (!("public_key" in info.auth_data)) {
- throw new Error("Invalid backup data returned");
- }
- }
-
- public get untrusted(): boolean {
- return true;
- }
-
- public async encryptSession(data: Record<string, any>): Promise<Curve25519SessionData> {
- const plainText: Record<string, any> = Object.assign({}, data);
- delete plainText.session_id;
- delete plainText.room_id;
- delete plainText.first_known_index;
- return this.publicKey.encrypt(JSON.stringify(plainText));
- }
-
- public async decryptSessions(
- sessions: Record<string, IKeyBackupSession<Curve25519SessionData>>,
- ): Promise<IMegolmSessionData[]> {
- const privKey = await this.getKey();
- const decryption = new global.Olm.PkDecryption();
- try {
- const backupPubKey = decryption.init_with_private_key(privKey);
-
- if (backupPubKey !== this.authData.public_key) {
- throw new MatrixError({ errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY });
- }
-
- const keys: IMegolmSessionData[] = [];
-
- for (const [sessionId, sessionData] of Object.entries(sessions)) {
- try {
- const decrypted = JSON.parse(
- decryption.decrypt(
- sessionData.session_data.ephemeral,
- sessionData.session_data.mac,
- sessionData.session_data.ciphertext,
- ),
- );
- decrypted.session_id = sessionId;
- keys.push(decrypted);
- } catch (e) {
- logger.log("Failed to decrypt megolm session from backup", e, sessionData);
- }
- }
- return keys;
- } finally {
- decryption.free();
- }
- }
-
- public async keyMatches(key: Uint8Array): Promise<boolean> {
- const decryption = new global.Olm.PkDecryption();
- let pubKey: string;
- try {
- pubKey = decryption.init_with_private_key(key);
- } finally {
- decryption.free();
- }
-
- return pubKey === this.authData.public_key;
- }
-
- public free(): void {
- this.publicKey.free();
- }
-}
-
-function randomBytes(size: number): Uint8Array {
- const buf = new Uint8Array(size);
- crypto.getRandomValues(buf);
- return buf;
-}
-
-const UNSTABLE_MSC3270_NAME = new UnstableValue(
- "m.megolm_backup.v1.aes-hmac-sha2",
- "org.matrix.msc3270.v1.aes-hmac-sha2",
-);
-
-export class Aes256 implements BackupAlgorithm {
- public static algorithmName = UNSTABLE_MSC3270_NAME.name;
-
- public constructor(public readonly authData: IAes256AuthData, private readonly key: Uint8Array) {}
-
- public static async init(authData: IAes256AuthData, getKey: () => Promise<Uint8Array>): Promise<Aes256> {
- if (!authData) {
- throw new Error("auth_data missing");
- }
- const key = await getKey();
- if (authData.mac) {
- const { mac } = await calculateKeyCheck(key, authData.iv);
- if (authData.mac.replace(/=+$/g, "") !== mac.replace(/=+/g, "")) {
- throw new Error("Key does not match");
- }
- }
- return new Aes256(authData, key);
- }
-
- public static async prepare(key?: string | Uint8Array | null): Promise<[Uint8Array, AuthData]> {
- let outKey: Uint8Array;
- const authData: Partial<IAes256AuthData> = {};
- if (!key) {
- outKey = randomBytes(32);
- } else if (key instanceof Uint8Array) {
- outKey = new Uint8Array(key);
- } else {
- const derivation = await keyFromPassphrase(key);
- authData.private_key_salt = derivation.salt;
- authData.private_key_iterations = derivation.iterations;
- outKey = derivation.key;
- }
-
- const { iv, mac } = await calculateKeyCheck(outKey);
- authData.iv = iv;
- authData.mac = mac;
-
- return [outKey, authData as AuthData];
- }
-
- public static checkBackupVersion(info: IKeyBackupInfo): void {
- if (!("iv" in info.auth_data && "mac" in info.auth_data)) {
- throw new Error("Invalid backup data returned");
- }
- }
-
- public get untrusted(): boolean {
- return false;
- }
-
- public encryptSession(data: Record<string, any>): Promise<IEncryptedPayload> {
- const plainText: Record<string, any> = Object.assign({}, data);
- delete plainText.session_id;
- delete plainText.room_id;
- delete plainText.first_known_index;
- return encryptAES(JSON.stringify(plainText), this.key, data.session_id);
- }
-
- public async decryptSessions(
- sessions: Record<string, IKeyBackupSession<IEncryptedPayload>>,
- ): Promise<IMegolmSessionData[]> {
- const keys: IMegolmSessionData[] = [];
-
- for (const [sessionId, sessionData] of Object.entries(sessions)) {
- try {
- const decrypted = JSON.parse(await decryptAES(sessionData.session_data, this.key, sessionId));
- decrypted.session_id = sessionId;
- keys.push(decrypted);
- } catch (e) {
- logger.log("Failed to decrypt megolm session from backup", e, sessionData);
- }
- }
- return keys;
- }
-
- 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, "");
- } else {
- // if we have no information, we have to assume the key is right
- return true;
- }
- }
-
- public free(): void {
- this.key.fill(0);
- }
-}
-
-export const algorithmsByName: Record<string, BackupAlgorithmClass> = {
- [Curve25519.algorithmName]: Curve25519,
- [Aes256.algorithmName]: Aes256,
-};
-
-export const DefaultAlgorithm: BackupAlgorithmClass = Curve25519;
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/crypto.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/crypto.ts
deleted file mode 100644
index 704754f..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/crypto.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { logger } from "../logger";
-
-export let crypto = global.window?.crypto;
-export let subtleCrypto = global.window?.crypto?.subtle ?? global.window?.crypto?.webkitSubtle;
-export let TextEncoder = global.window?.TextEncoder;
-
-/* eslint-disable @typescript-eslint/no-var-requires */
-if (!crypto) {
- try {
- crypto = require("crypto").webcrypto;
- } catch (e) {
- logger.error("Failed to load webcrypto", e);
- }
-}
-if (!subtleCrypto) {
- subtleCrypto = crypto?.subtle;
-}
-if (!TextEncoder) {
- try {
- TextEncoder = require("util").TextEncoder;
- } catch (e) {
- logger.error("Failed to load TextEncoder util", e);
- }
-}
-/* eslint-enable @typescript-eslint/no-var-requires */
-
-export function setCrypto(_crypto: Crypto): void {
- crypto = _crypto;
- subtleCrypto = _crypto.subtle ?? _crypto.webkitSubtle;
-}
-
-export function setTextEncoder(_TextEncoder: typeof TextEncoder): void {
- TextEncoder = _TextEncoder;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/dehydration.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/dehydration.ts
deleted file mode 100644
index 373b236..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/dehydration.ts
+++ /dev/null
@@ -1,271 +0,0 @@
-/*
-Copyright 2020-2021 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 anotherjson from "another-json";
-
-import type { IDeviceKeys, IOneTimeKey } from "../@types/crypto";
-import { decodeBase64, encodeBase64 } from "./olmlib";
-import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store";
-import { decryptAES, encryptAES } from "./aes";
-import { logger } from "../logger";
-import { Crypto } from "./index";
-import { Method } from "../http-api";
-import { SecretStorageKeyDescription } from "../secret-storage";
-
-export interface IDehydratedDevice {
- device_id: string; // eslint-disable-line camelcase
- device_data: SecretStorageKeyDescription & {
- // eslint-disable-line camelcase
- algorithm: string;
- account: string; // pickle
- };
-}
-
-export interface IDehydratedDeviceKeyInfo {
- passphrase?: string;
-}
-
-export const DEHYDRATION_ALGORITHM = "org.matrix.msc2697.v1.olm.libolm_pickle";
-
-const oneweek = 7 * 24 * 60 * 60 * 1000;
-
-export class DehydrationManager {
- private inProgress = false;
- private timeoutId: any;
- private key?: Uint8Array;
- private keyInfo?: { [props: string]: any };
- private deviceDisplayName?: string;
-
- public constructor(private readonly crypto: Crypto) {
- this.getDehydrationKeyFromCache();
- }
-
- public getDehydrationKeyFromCache(): Promise<void> {
- return this.crypto.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.crypto.cryptoStore.getSecretStorePrivateKey(
- txn,
- async (result) => {
- if (result) {
- const { key, keyInfo, deviceDisplayName, time } = result;
- const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey);
- const decrypted = await decryptAES(key, pickleKey, DEHYDRATION_ALGORITHM);
- this.key = decodeBase64(decrypted);
- this.keyInfo = keyInfo;
- this.deviceDisplayName = deviceDisplayName;
- const now = Date.now();
- const delay = Math.max(1, time + oneweek - now);
- this.timeoutId = global.setTimeout(this.dehydrateDevice.bind(this), delay);
- }
- },
- "dehydration",
- );
- });
- }
-
- /** set the key, and queue periodic dehydration to the server in the background */
- public async setKeyAndQueueDehydration(
- key: Uint8Array,
- keyInfo: { [props: string]: any } = {},
- deviceDisplayName?: string,
- ): Promise<void> {
- const matches = await this.setKey(key, keyInfo, deviceDisplayName);
- if (!matches) {
- // start dehydration in the background
- this.dehydrateDevice();
- }
- }
-
- public async setKey(
- key: Uint8Array,
- keyInfo: { [props: string]: any } = {},
- deviceDisplayName?: string,
- ): Promise<boolean | undefined> {
- if (!key) {
- // unsetting the key -- cancel any pending dehydration task
- if (this.timeoutId) {
- global.clearTimeout(this.timeoutId);
- this.timeoutId = undefined;
- }
- // clear storage
- await this.crypto.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.crypto.cryptoStore.storeSecretStorePrivateKey(txn, "dehydration", null);
- });
- this.key = undefined;
- this.keyInfo = undefined;
- return;
- }
-
- // Check to see if it's the same key as before. If it's different,
- // dehydrate a new device. If it's the same, we can keep the same
- // device. (Assume that keyInfo and deviceDisplayName will be the
- // same if the key is the same.)
- let matches: boolean = !!this.key && key.length == this.key.length;
- for (let i = 0; matches && i < key.length; i++) {
- if (key[i] != this.key![i]) {
- matches = false;
- }
- }
- if (!matches) {
- this.key = key;
- this.keyInfo = keyInfo;
- this.deviceDisplayName = deviceDisplayName;
- }
- return matches;
- }
-
- /** returns the device id of the newly created dehydrated device */
- public async dehydrateDevice(): Promise<string | undefined> {
- if (this.inProgress) {
- logger.log("Dehydration already in progress -- not starting new dehydration");
- return;
- }
- this.inProgress = true;
- if (this.timeoutId) {
- global.clearTimeout(this.timeoutId);
- this.timeoutId = undefined;
- }
- try {
- const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey);
-
- // update the crypto store with the timestamp
- const key = await encryptAES(encodeBase64(this.key!), pickleKey, DEHYDRATION_ALGORITHM);
- await this.crypto.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.crypto.cryptoStore.storeSecretStorePrivateKey(txn, "dehydration", {
- keyInfo: this.keyInfo,
- key,
- deviceDisplayName: this.deviceDisplayName!,
- time: Date.now(),
- });
- });
- logger.log("Attempting to dehydrate device");
-
- logger.log("Creating account");
- // create the account and all the necessary keys
- const account = new global.Olm.Account();
- account.create();
- const e2eKeys = JSON.parse(account.identity_keys());
-
- const maxKeys = account.max_number_of_one_time_keys();
- // FIXME: generate in small batches?
- account.generate_one_time_keys(maxKeys / 2);
- account.generate_fallback_key();
- const otks: Record<string, string> = JSON.parse(account.one_time_keys());
- const fallbacks: Record<string, string> = JSON.parse(account.fallback_key());
- account.mark_keys_as_published();
-
- // dehydrate the account and store it on the server
- const pickledAccount = account.pickle(new Uint8Array(this.key!));
-
- const deviceData: { [props: string]: any } = {
- algorithm: DEHYDRATION_ALGORITHM,
- account: pickledAccount,
- };
- if (this.keyInfo!.passphrase) {
- deviceData.passphrase = this.keyInfo!.passphrase;
- }
-
- logger.log("Uploading account to server");
- // eslint-disable-next-line camelcase
- const dehydrateResult = await this.crypto.baseApis.http.authedRequest<{ device_id: string }>(
- Method.Put,
- "/dehydrated_device",
- undefined,
- {
- device_data: deviceData,
- initial_device_display_name: this.deviceDisplayName,
- },
- {
- prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2",
- },
- );
-
- // send the keys to the server
- const deviceId = dehydrateResult.device_id;
- logger.log("Preparing device keys", deviceId);
- const deviceKeys: IDeviceKeys = {
- algorithms: this.crypto.supportedAlgorithms,
- device_id: deviceId,
- user_id: this.crypto.userId,
- keys: {
- [`ed25519:${deviceId}`]: e2eKeys.ed25519,
- [`curve25519:${deviceId}`]: e2eKeys.curve25519,
- },
- };
- const deviceSignature = account.sign(anotherjson.stringify(deviceKeys));
- deviceKeys.signatures = {
- [this.crypto.userId]: {
- [`ed25519:${deviceId}`]: deviceSignature,
- },
- };
- if (this.crypto.crossSigningInfo.getId("self_signing")) {
- await this.crypto.crossSigningInfo.signObject(deviceKeys, "self_signing");
- }
-
- logger.log("Preparing one-time keys");
- const oneTimeKeys: Record<string, IOneTimeKey> = {};
- for (const [keyId, key] of Object.entries(otks.curve25519)) {
- const k: IOneTimeKey = { key };
- const signature = account.sign(anotherjson.stringify(k));
- k.signatures = {
- [this.crypto.userId]: {
- [`ed25519:${deviceId}`]: signature,
- },
- };
- oneTimeKeys[`signed_curve25519:${keyId}`] = k;
- }
-
- logger.log("Preparing fallback keys");
- const fallbackKeys: Record<string, IOneTimeKey> = {};
- for (const [keyId, key] of Object.entries(fallbacks.curve25519)) {
- const k: IOneTimeKey = { key, fallback: true };
- const signature = account.sign(anotherjson.stringify(k));
- k.signatures = {
- [this.crypto.userId]: {
- [`ed25519:${deviceId}`]: signature,
- },
- };
- fallbackKeys[`signed_curve25519:${keyId}`] = k;
- }
-
- logger.log("Uploading keys to server");
- await this.crypto.baseApis.http.authedRequest(
- Method.Post,
- "/keys/upload/" + encodeURI(deviceId),
- undefined,
- {
- "device_keys": deviceKeys,
- "one_time_keys": oneTimeKeys,
- "org.matrix.msc2732.fallback_keys": fallbackKeys,
- },
- );
- logger.log("Done dehydrating");
-
- // dehydrate again in a week
- this.timeoutId = global.setTimeout(this.dehydrateDevice.bind(this), oneweek);
-
- return deviceId;
- } finally {
- this.inProgress = false;
- }
- }
-
- public stop(): void {
- if (this.timeoutId) {
- global.clearTimeout(this.timeoutId);
- this.timeoutId = undefined;
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/deviceinfo.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/deviceinfo.ts
deleted file mode 100644
index b4bb4fd..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/deviceinfo.ts
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
-Copyright 2016 - 2021 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 { ISignatures } from "../@types/signed";
-
-export interface IDevice {
- keys: Record<string, string>;
- algorithms: string[];
- verified: DeviceVerification;
- known: boolean;
- unsigned?: Record<string, any>;
- signatures?: ISignatures;
-}
-
-enum DeviceVerification {
- Blocked = -1,
- Unverified = 0,
- Verified = 1,
-}
-
-/**
- * Information about a user's device
- */
-export class DeviceInfo {
- /**
- * rehydrate a DeviceInfo from the session store
- *
- * @param obj - raw object from session store
- * @param deviceId - id of the device
- *
- * @returns new DeviceInfo
- */
- public static fromStorage(obj: Partial<IDevice>, deviceId: string): DeviceInfo {
- const res = new DeviceInfo(deviceId);
- for (const prop in obj) {
- if (obj.hasOwnProperty(prop)) {
- // @ts-ignore - this is messy and typescript doesn't like it
- res[prop as keyof IDevice] = obj[prop as keyof IDevice];
- }
- }
- return res;
- }
-
- public static DeviceVerification = {
- VERIFIED: DeviceVerification.Verified,
- UNVERIFIED: DeviceVerification.Unverified,
- BLOCKED: DeviceVerification.Blocked,
- };
-
- /** list of algorithms supported by this device */
- public algorithms: string[] = [];
- /** a map from `<key type>:<id> -> <base64-encoded key>` */
- public keys: Record<string, string> = {};
- /** whether the device has been verified/blocked by the user */
- public verified = DeviceVerification.Unverified;
- /**
- * whether the user knows of this device's existence
- * (useful when warning the user that a user has added new devices)
- */
- public known = false;
- /** additional data from the homeserver */
- public unsigned: Record<string, any> = {};
- public signatures: ISignatures = {};
-
- /**
- * @param deviceId - id of the device
- */
- public constructor(public readonly deviceId: string) {}
-
- /**
- * Prepare a DeviceInfo for JSON serialisation in the session store
- *
- * @returns deviceinfo with non-serialised members removed
- */
- public toStorage(): IDevice {
- return {
- algorithms: this.algorithms,
- keys: this.keys,
- verified: this.verified,
- known: this.known,
- unsigned: this.unsigned,
- signatures: this.signatures,
- };
- }
-
- /**
- * Get the fingerprint for this device (ie, the Ed25519 key)
- *
- * @returns base64-encoded fingerprint of this device
- */
- public getFingerprint(): string {
- return this.keys["ed25519:" + this.deviceId];
- }
-
- /**
- * Get the identity key for this device (ie, the Curve25519 key)
- *
- * @returns base64-encoded identity key of this device
- */
- public getIdentityKey(): string {
- return this.keys["curve25519:" + this.deviceId];
- }
-
- /**
- * Get the configured display name for this device, if any
- *
- * @returns displayname
- */
- public getDisplayName(): string | null {
- return this.unsigned.device_display_name || null;
- }
-
- /**
- * Returns true if this device is blocked
- *
- * @returns true if blocked
- */
- public isBlocked(): boolean {
- return this.verified == DeviceVerification.Blocked;
- }
-
- /**
- * Returns true if this device is verified
- *
- * @returns true if verified
- */
- public isVerified(): boolean {
- return this.verified == DeviceVerification.Verified;
- }
-
- /**
- * Returns true if this device is unverified
- *
- * @returns true if unverified
- */
- public isUnverified(): boolean {
- return this.verified == DeviceVerification.Unverified;
- }
-
- /**
- * Returns true if the user knows about this device's existence
- *
- * @returns true if known
- */
- public isKnown(): boolean {
- return this.known === true;
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/index.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/index.ts
deleted file mode 100644
index 68df6ca..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/index.ts
+++ /dev/null
@@ -1,3936 +0,0 @@
-/*
-Copyright 2016 OpenMarket Ltd
-Copyright 2017 Vector Creations Ltd
-Copyright 2018-2019 New Vector Ltd
-Copyright 2019-2021 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 anotherjson from "another-json";
-import { v4 as uuidv4 } from "uuid";
-
-import type { IDeviceKeys, IEventDecryptionResult, IMegolmSessionData, IOneTimeKey } from "../@types/crypto";
-import type { PkDecryption, PkSigning } from "@matrix-org/olm";
-import { EventType, ToDeviceMessageId } from "../@types/event";
-import { TypedReEmitter } from "../ReEmitter";
-import { logger } from "../logger";
-import { IExportedDevice, OlmDevice } from "./OlmDevice";
-import { IOlmDevice } from "./algorithms/megolm";
-import * as olmlib from "./olmlib";
-import { DeviceInfoMap, DeviceList } from "./DeviceList";
-import { DeviceInfo, IDevice } from "./deviceinfo";
-import type { DecryptionAlgorithm, EncryptionAlgorithm } from "./algorithms";
-import * as algorithms from "./algorithms";
-import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from "./CrossSigning";
-import { EncryptionSetupBuilder } from "./EncryptionSetup";
-import {
- IAccountDataClient,
- ISecretRequest,
- SECRET_STORAGE_ALGORITHM_V1_AES,
- SecretStorage,
- SecretStorageKeyObject,
- SecretStorageKeyTuple,
-} from "./SecretStorage";
-import {
- IAddSecretStorageKeyOpts,
- ICreateSecretStorageOpts,
- IEncryptedEventInfo,
- IImportRoomKeysOpts,
- IRecoveryKey,
-} from "./api";
-import { OutgoingRoomKeyRequestManager } from "./OutgoingRoomKeyRequestManager";
-import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store";
-import { VerificationBase } from "./verification/Base";
-import { ReciprocateQRCode, SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD } from "./verification/QRCode";
-import { SAS as SASVerification } from "./verification/SAS";
-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, Request } from "./verification/request/ToDeviceChannel";
-import { IllegalMethod } from "./verification/IllegalMethod";
-import { KeySignatureUploadError } from "../errors";
-import { calculateKeyCheck, decryptAES, encryptAES } from "./aes";
-import { DehydrationManager } from "./dehydration";
-import { BackupManager } from "./backup";
-import { IStore } from "../store";
-import { Room, RoomEvent } from "../models/room";
-import { RoomMember, RoomMemberEvent } from "../models/room-member";
-import { EventStatus, IEvent, MatrixEvent, MatrixEventEvent } from "../models/event";
-import { ToDeviceBatch } from "../models/ToDeviceMessage";
-import {
- ClientEvent,
- ICrossSigningKey,
- IKeysUploadResponse,
- ISignedKey,
- IUploadKeySignaturesResponse,
- MatrixClient,
-} from "../client";
-import type { IRoomEncryption, RoomList } from "./RoomList";
-import { IKeyBackupInfo } from "./keybackup";
-import { ISyncStateData } from "../sync";
-import { CryptoStore } from "./store/base";
-import { IVerificationChannel } from "./verification/request/Channel";
-import { TypedEventEmitter } from "../models/typed-event-emitter";
-import { IContent } from "../models/event";
-import { ISyncResponse, IToDeviceEvent } from "../sync-accumulator";
-import { ISignatures } from "../@types/signed";
-import { IMessage } from "./algorithms/olm";
-import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
-import { RoomState, RoomStateEvent } from "../models/room-state";
-import { MapWithDefault, recursiveMapToObject } from "../utils";
-import { SecretStorageKeyDescription } from "../secret-storage";
-
-const DeviceVerification = DeviceInfo.DeviceVerification;
-
-const defaultVerificationMethods = {
- [ReciprocateQRCode.NAME]: ReciprocateQRCode,
- [SASVerification.NAME]: SASVerification,
-
- // These two can't be used for actual verification, but we do
- // need to be able to define them here for the verification flows
- // to start.
- [SHOW_QR_CODE_METHOD]: IllegalMethod,
- [SCAN_QR_CODE_METHOD]: IllegalMethod,
-} as const;
-
-/**
- * verification method names
- */
-// legacy export identifier
-export const verificationMethods = {
- RECIPROCATE_QR_CODE: ReciprocateQRCode.NAME,
- SAS: SASVerification.NAME,
-} as const;
-
-export type VerificationMethod = keyof typeof verificationMethods | string;
-
-export function isCryptoAvailable(): boolean {
- return Boolean(global.Olm);
-}
-
-const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000;
-
-interface IInitOpts {
- exportedOlmDevice?: IExportedDevice;
- pickleKey?: string;
-}
-
-export interface IBootstrapCrossSigningOpts {
- /** Optional. Reset even if keys already exist. */
- setupNewCrossSigning?: boolean;
- /**
- * A function that makes the request requiring auth. Receives the auth data as an object.
- * Can be called multiple times, first with an empty authDict, to obtain the flows.
- */
- authUploadDeviceSigningKeys?(makeRequest: (authData: any) => Promise<{}>): Promise<void>;
-}
-
-export interface ICryptoCallbacks {
- getCrossSigningKey?: (keyType: string, pubKey: string) => Promise<Uint8Array | null>;
- saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
- shouldUpgradeDeviceVerifications?: (users: Record<string, any>) => Promise<string[]>;
- getSecretStorageKey?: (
- keys: { keys: Record<string, SecretStorageKeyDescription> },
- name: string,
- ) => Promise<[string, Uint8Array] | null>;
- cacheSecretStorageKey?: (keyId: string, keyInfo: SecretStorageKeyDescription, key: Uint8Array) => void;
- onSecretRequested?: (
- userId: string,
- deviceId: string,
- requestId: string,
- secretName: string,
- deviceTrust: DeviceTrustLevel,
- ) => Promise<string | undefined>;
- getDehydrationKey?: (
- keyInfo: SecretStorageKeyDescription,
- checkFunc: (key: Uint8Array) => void,
- ) => Promise<Uint8Array>;
- getBackupKey?: () => Promise<Uint8Array>;
-}
-
-/* eslint-disable camelcase */
-interface IRoomKey {
- room_id: string;
- algorithm: string;
-}
-
-/**
- * The parameters of a room key request. The details of the request may
- * vary with the crypto algorithm, but the management and storage layers for
- * outgoing requests expect it to have 'room_id' and 'session_id' properties.
- */
-export interface IRoomKeyRequestBody extends IRoomKey {
- session_id: string;
- sender_key: string;
-}
-
-/* eslint-enable camelcase */
-
-interface IDeviceVerificationUpgrade {
- devices: DeviceInfo[];
- crossSigningInfo: CrossSigningInfo;
-}
-
-export interface ICheckOwnCrossSigningTrustOpts {
- allowPrivateKeyRequests?: boolean;
-}
-
-interface IUserOlmSession {
- deviceIdKey: string;
- sessions: {
- sessionId: string;
- hasReceivedMessage: boolean;
- }[];
-}
-
-export interface IRoomKeyRequestRecipient {
- userId: string;
- deviceId: string;
-}
-
-interface ISignableObject {
- signatures?: ISignatures;
- unsigned?: object;
-}
-
-export interface IRequestsMap {
- getRequest(event: MatrixEvent): VerificationRequest | undefined;
- getRequestByChannel(channel: IVerificationChannel): VerificationRequest | undefined;
- setRequest(event: MatrixEvent, request: VerificationRequest): void;
- setRequestByChannel(channel: IVerificationChannel, request: VerificationRequest): void;
-}
-
-/* eslint-disable camelcase */
-export interface IOlmEncryptedContent {
- algorithm: typeof olmlib.OLM_ALGORITHM;
- sender_key: string;
- ciphertext: Record<string, IMessage>;
- [ToDeviceMessageId]?: string;
-}
-
-export interface IMegolmEncryptedContent {
- algorithm: typeof olmlib.MEGOLM_ALGORITHM;
- sender_key: string;
- session_id: string;
- device_id: string;
- ciphertext: string;
- [ToDeviceMessageId]?: string;
-}
-/* eslint-enable camelcase */
-
-export type IEncryptedContent = IOlmEncryptedContent | IMegolmEncryptedContent;
-
-export enum CryptoEvent {
- DeviceVerificationChanged = "deviceVerificationChanged",
- UserTrustStatusChanged = "userTrustStatusChanged",
- UserCrossSigningUpdated = "userCrossSigningUpdated",
- RoomKeyRequest = "crypto.roomKeyRequest",
- RoomKeyRequestCancellation = "crypto.roomKeyRequestCancellation",
- KeyBackupStatus = "crypto.keyBackupStatus",
- KeyBackupFailed = "crypto.keyBackupFailed",
- KeyBackupSessionsRemaining = "crypto.keyBackupSessionsRemaining",
- KeySignatureUploadFailure = "crypto.keySignatureUploadFailure",
- VerificationRequest = "crypto.verification.request",
- Warning = "crypto.warning",
- WillUpdateDevices = "crypto.willUpdateDevices",
- DevicesUpdated = "crypto.devicesUpdated",
- KeysChanged = "crossSigning.keysChanged",
-}
-
-export type CryptoEventHandlerMap = {
- /**
- * Fires when a device is marked as verified/unverified/blocked/unblocked by
- * {@link MatrixClient#setDeviceVerified|MatrixClient.setDeviceVerified} or
- * {@link MatrixClient#setDeviceBlocked|MatrixClient.setDeviceBlocked}.
- *
- * @param userId - the owner of the verified device
- * @param deviceId - the id of the verified device
- * @param deviceInfo - updated device information
- */
- [CryptoEvent.DeviceVerificationChanged]: (userId: string, deviceId: string, device: DeviceInfo) => void;
- /**
- * Fires when the trust status of a user changes
- * If userId is the userId of the logged-in user, this indicated a change
- * in the trust status of the cross-signing data on the account.
- *
- * The cross-signing API is currently UNSTABLE and may change without notice.
- * @experimental
- *
- * @param userId - the userId of the user in question
- * @param trustLevel - The new trust level of the user
- */
- [CryptoEvent.UserTrustStatusChanged]: (userId: string, trustLevel: UserTrustLevel) => void;
- /**
- * Fires when we receive a room key request
- *
- * @param req - request details
- */
- [CryptoEvent.RoomKeyRequest]: (request: IncomingRoomKeyRequest) => void;
- /**
- * Fires when we receive a room key request cancellation
- */
- [CryptoEvent.RoomKeyRequestCancellation]: (request: IncomingRoomKeyRequestCancellation) => void;
- /**
- * Fires whenever the status of e2e key backup changes, as returned by getKeyBackupEnabled()
- * @param enabled - true if key backup has been enabled, otherwise false
- * @example
- * ```
- * matrixClient.on("crypto.keyBackupStatus", function(enabled){
- * if (enabled) {
- * [...]
- * }
- * });
- * ```
- */
- [CryptoEvent.KeyBackupStatus]: (enabled: boolean) => void;
- [CryptoEvent.KeyBackupFailed]: (errcode: string) => void;
- [CryptoEvent.KeyBackupSessionsRemaining]: (remaining: number) => void;
- [CryptoEvent.KeySignatureUploadFailure]: (
- failures: IUploadKeySignaturesResponse["failures"],
- source: "checkOwnCrossSigningTrust" | "afterCrossSigningLocalKeyChange" | "setDeviceVerification",
- upload: (opts: { shouldEmit: boolean }) => Promise<void>,
- ) => void;
- /**
- * Fires when a key verification is requested.
- */
- [CryptoEvent.VerificationRequest]: (request: VerificationRequest<any>) => void;
- /**
- * Fires when the app may wish to warn the user about something related
- * the end-to-end crypto.
- *
- * @param type - One of the strings listed above
- */
- [CryptoEvent.Warning]: (type: string) => void;
- /**
- * Fires when the user's cross-signing keys have changed or cross-signing
- * has been enabled/disabled. The client can use getStoredCrossSigningForUser
- * with the user ID of the logged in user to check if cross-signing is
- * enabled on the account. If enabled, it can test whether the current key
- * is trusted using with checkUserTrust with the user ID of the logged
- * in user. The checkOwnCrossSigningTrust function may be used to reconcile
- * the trust in the account key.
- *
- * The cross-signing API is currently UNSTABLE and may change without notice.
- * @experimental
- */
- [CryptoEvent.KeysChanged]: (data: {}) => void;
- /**
- * Fires whenever the stored devices for a user will be updated
- * @param users - A list of user IDs that will be updated
- * @param initialFetch - If true, the store is empty (apart
- * from our own device) and is being seeded.
- */
- [CryptoEvent.WillUpdateDevices]: (users: string[], initialFetch: boolean) => void;
- /**
- * Fires whenever the stored devices for a user have changed
- * @param users - A list of user IDs that were updated
- * @param initialFetch - If true, the store was empty (apart
- * from our own device) and has been seeded.
- */
- [CryptoEvent.DevicesUpdated]: (users: string[], initialFetch: boolean) => void;
- [CryptoEvent.UserCrossSigningUpdated]: (userId: string) => void;
-};
-
-export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap> implements CryptoBackend {
- /**
- * @returns The version of Olm.
- */
- public static getOlmVersion(): [number, number, number] {
- return OlmDevice.getOlmVersion();
- }
-
- public readonly backupManager: BackupManager;
- public readonly crossSigningInfo: CrossSigningInfo;
- public readonly olmDevice: OlmDevice;
- public readonly deviceList: DeviceList;
- public readonly dehydrationManager: DehydrationManager;
- public readonly secretStorage: SecretStorage;
-
- private readonly reEmitter: TypedReEmitter<CryptoEvent, CryptoEventHandlerMap>;
- private readonly verificationMethods: Map<VerificationMethod, typeof VerificationBase>;
- public readonly supportedAlgorithms: string[];
- private readonly outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager;
- private readonly toDeviceVerificationRequests: ToDeviceRequests;
- public readonly inRoomVerificationRequests: InRoomRequests;
-
- private trustCrossSignedDevices = true;
- // the last time we did a check for the number of one-time-keys on the server.
- private lastOneTimeKeyCheck: number | null = null;
- private oneTimeKeyCheckInProgress = false;
-
- // EncryptionAlgorithm instance for each room
- private roomEncryptors = new Map<string, EncryptionAlgorithm>();
- // map from algorithm to DecryptionAlgorithm instance, for each room
- private roomDecryptors = new Map<string, Map<string, DecryptionAlgorithm>>();
-
- private deviceKeys: Record<string, string> = {}; // type: key
-
- public globalBlacklistUnverifiedDevices = false;
- public globalErrorOnUnknownDevices = true;
-
- // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
- // we received in the current sync.
- private receivedRoomKeyRequests: IncomingRoomKeyRequest[] = [];
- private receivedRoomKeyRequestCancellations: IncomingRoomKeyRequestCancellation[] = [];
- // true if we are currently processing received room key requests
- private processingRoomKeyRequests = false;
- // controls whether device tracking is delayed
- // until calling encryptEvent or trackRoomDevices,
- // or done immediately upon enabling room encryption.
- private lazyLoadMembers = false;
- // in case lazyLoadMembers is true,
- // track if an initial tracking of all the room members
- // has happened for a given room. This is delayed
- // to avoid loading room members as long as possible.
- private roomDeviceTrackingState: { [roomId: string]: Promise<void> } = {};
-
- // The timestamp of the last time we forced establishment
- // of a new session for each device, in milliseconds.
- // {
- // userId: {
- // deviceId: 1234567890000,
- // },
- // }
- // Map: user Id → device Id → timestamp
- private lastNewSessionForced: MapWithDefault<string, MapWithDefault<string, number>> = new MapWithDefault(
- () => new MapWithDefault(() => 0),
- );
-
- // This flag will be unset whilst the client processes a sync response
- // so that we don't start requesting keys until we've actually finished
- // processing the response.
- private sendKeyRequestsImmediately = false;
-
- private oneTimeKeyCount?: number;
- private needsNewFallback?: boolean;
- private fallbackCleanup?: ReturnType<typeof setTimeout>;
-
- /**
- * Cryptography bits
- *
- * This module is internal to the js-sdk; the public API is via MatrixClient.
- *
- * @internal
- *
- * @param baseApis - base matrix api interface
- *
- * @param userId - The user ID for the local user
- *
- * @param deviceId - The identifier for this device.
- *
- * @param clientStore - the MatrixClient data store.
- *
- * @param cryptoStore - storage for the crypto layer.
- *
- * @param roomList - An initialised RoomList object
- *
- * @param verificationMethods - Array of verification methods to use.
- * Each element can either be a string from MatrixClient.verificationMethods
- * or a class that implements a verification method.
- */
- public constructor(
- public readonly baseApis: MatrixClient,
- public readonly userId: string,
- private readonly deviceId: string,
- private readonly clientStore: IStore,
- public readonly cryptoStore: CryptoStore,
- private readonly roomList: RoomList,
- verificationMethods: Array<VerificationMethod | (typeof VerificationBase & { NAME: string })>,
- ) {
- super();
- this.reEmitter = new TypedReEmitter(this);
-
- if (verificationMethods) {
- this.verificationMethods = new Map();
- for (const method of verificationMethods) {
- if (typeof method === "string") {
- if (defaultVerificationMethods[method]) {
- this.verificationMethods.set(
- method,
- <typeof VerificationBase>defaultVerificationMethods[method],
- );
- }
- } else if (method["NAME"]) {
- this.verificationMethods.set(method["NAME"], method as typeof VerificationBase);
- } else {
- logger.warn(`Excluding unknown verification method ${method}`);
- }
- }
- } else {
- this.verificationMethods = new Map(Object.entries(defaultVerificationMethods)) as Map<
- VerificationMethod,
- typeof VerificationBase
- >;
- }
-
- this.backupManager = new BackupManager(baseApis, async () => {
- // try to get key from cache
- const cachedKey = await this.getSessionBackupPrivateKey();
- if (cachedKey) {
- return cachedKey;
- }
-
- // try to get key from secret storage
- const storedKey = await this.getSecret("m.megolm_backup.v1");
-
- if (storedKey) {
- // ensure that the key is in the right format. If not, fix the key and
- // store the fixed version
- const fixedKey = fixBackupKey(storedKey);
- if (fixedKey) {
- const keys = await this.getSecretStorageKey();
- await this.storeSecret("m.megolm_backup.v1", fixedKey, [keys![0]]);
- }
-
- return olmlib.decodeBase64(fixedKey || storedKey);
- }
-
- // try to get key from app
- if (this.baseApis.cryptoCallbacks && this.baseApis.cryptoCallbacks.getBackupKey) {
- return this.baseApis.cryptoCallbacks.getBackupKey();
- }
-
- throw new Error("Unable to get private key");
- });
-
- this.olmDevice = new OlmDevice(cryptoStore);
- this.deviceList = new DeviceList(baseApis, cryptoStore, this.olmDevice);
-
- // XXX: This isn't removed at any point, but then none of the event listeners
- // this class sets seem to be removed at any point... :/
- this.deviceList.on(CryptoEvent.UserCrossSigningUpdated, this.onDeviceListUserCrossSigningUpdated);
- this.reEmitter.reEmit(this.deviceList, [CryptoEvent.DevicesUpdated, CryptoEvent.WillUpdateDevices]);
-
- this.supportedAlgorithms = Array.from(algorithms.DECRYPTION_CLASSES.keys());
-
- this.outgoingRoomKeyRequestManager = new OutgoingRoomKeyRequestManager(
- baseApis,
- this.deviceId,
- this.cryptoStore,
- );
-
- this.toDeviceVerificationRequests = new ToDeviceRequests();
- this.inRoomVerificationRequests = new InRoomRequests();
-
- const cryptoCallbacks = this.baseApis.cryptoCallbacks || {};
- const cacheCallbacks = createCryptoStoreCacheCallbacks(cryptoStore, this.olmDevice);
-
- this.crossSigningInfo = new CrossSigningInfo(userId, cryptoCallbacks, cacheCallbacks);
- // Yes, we pass the client twice here: see SecretStorage
- this.secretStorage = new SecretStorage(baseApis as IAccountDataClient, cryptoCallbacks, baseApis);
- this.dehydrationManager = new DehydrationManager(this);
-
- // Assuming no app-supplied callback, default to getting from SSSS.
- if (!cryptoCallbacks.getCrossSigningKey && cryptoCallbacks.getSecretStorageKey) {
- cryptoCallbacks.getCrossSigningKey = async (type): Promise<Uint8Array | null> => {
- return CrossSigningInfo.getFromSecretStorage(type, this.secretStorage);
- };
- }
- }
-
- /**
- * Initialise the crypto module so that it is ready for use
- *
- * Returns a promise which resolves once the crypto module is ready for use.
- *
- * @param exportedOlmDevice - (Optional) data from exported device
- * that must be re-created.
- */
- public async init({ exportedOlmDevice, pickleKey }: IInitOpts = {}): Promise<void> {
- logger.log("Crypto: initialising Olm...");
- await global.Olm.init();
- logger.log(
- exportedOlmDevice
- ? "Crypto: initialising Olm device from exported device..."
- : "Crypto: initialising Olm device...",
- );
- await this.olmDevice.init({ fromExportedDevice: exportedOlmDevice, pickleKey });
- logger.log("Crypto: loading device list...");
- await this.deviceList.load();
-
- // build our device keys: these will later be uploaded
- this.deviceKeys["ed25519:" + this.deviceId] = this.olmDevice.deviceEd25519Key!;
- this.deviceKeys["curve25519:" + this.deviceId] = this.olmDevice.deviceCurve25519Key!;
-
- logger.log("Crypto: fetching own devices...");
- let myDevices = this.deviceList.getRawStoredDevicesForUser(this.userId);
-
- if (!myDevices) {
- myDevices = {};
- }
-
- if (!myDevices[this.deviceId]) {
- // add our own deviceinfo to the cryptoStore
- logger.log("Crypto: adding this device to the store...");
- const deviceInfo = {
- keys: this.deviceKeys,
- algorithms: this.supportedAlgorithms,
- verified: DeviceVerification.VERIFIED,
- known: true,
- };
-
- myDevices[this.deviceId] = deviceInfo;
- this.deviceList.storeDevicesForUser(this.userId, myDevices);
- this.deviceList.saveIfDirty();
- }
-
- await this.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.cryptoStore.getCrossSigningKeys(txn, (keys) => {
- // can be an empty object after resetting cross-signing keys, see storeTrustedSelfKeys
- if (keys && Object.keys(keys).length !== 0) {
- logger.log("Loaded cross-signing public keys from crypto store");
- this.crossSigningInfo.setKeys(keys);
- }
- });
- });
- // make sure we are keeping track of our own devices
- // (this is important for key backups & things)
- this.deviceList.startTrackingDeviceList(this.userId);
-
- logger.log("Crypto: checking for key backup...");
- this.backupManager.checkAndStart();
- }
-
- /**
- * Whether to trust a others users signatures of their devices.
- * If false, devices will only be considered 'verified' if we have
- * verified that device individually (effectively disabling cross-signing).
- *
- * Default: true
- *
- * @returns True if trusting cross-signed devices
- */
- public getCryptoTrustCrossSignedDevices(): boolean {
- return this.trustCrossSignedDevices;
- }
-
- /**
- * See getCryptoTrustCrossSignedDevices
-
- * This may be set before initCrypto() is called to ensure no races occur.
- *
- * @param val - True to trust cross-signed devices
- */
- public setCryptoTrustCrossSignedDevices(val: boolean): void {
- this.trustCrossSignedDevices = val;
-
- for (const userId of this.deviceList.getKnownUserIds()) {
- const devices = this.deviceList.getRawStoredDevicesForUser(userId);
- for (const deviceId of Object.keys(devices)) {
- const deviceTrust = this.checkDeviceTrust(userId, deviceId);
- // If the device is locally verified then isVerified() is always true,
- // so this will only have caused the value to change if the device is
- // cross-signing verified but not locally verified
- if (!deviceTrust.isLocallyVerified() && deviceTrust.isCrossSigningVerified()) {
- const deviceObj = this.deviceList.getStoredDevice(userId, deviceId)!;
- this.emit(CryptoEvent.DeviceVerificationChanged, userId, deviceId, deviceObj);
- }
- }
- }
- }
-
- /**
- * Create a recovery key from a user-supplied passphrase.
- *
- * @param password - Passphrase string that can be entered by the user
- * when restoring the backup as an alternative to entering the recovery key.
- * Optional.
- * @returns Object with public key metadata, encoded private
- * recovery key which should be disposed of after displaying to the user,
- * and raw private key to avoid round tripping if needed.
- */
- public async createRecoveryKeyFromPassphrase(password?: string): Promise<IRecoveryKey> {
- const decryption = new global.Olm.PkDecryption();
- try {
- const keyInfo: Partial<IRecoveryKey["keyInfo"]> = {};
- if (password) {
- const derivation = await keyFromPassphrase(password);
- keyInfo.passphrase = {
- algorithm: "m.pbkdf2",
- iterations: derivation.iterations,
- salt: derivation.salt,
- };
- keyInfo.pubkey = decryption.init_with_private_key(derivation.key);
- } else {
- keyInfo.pubkey = decryption.generate_key();
- }
- const privateKey = decryption.get_private_key();
- const encodedPrivateKey = encodeRecoveryKey(privateKey);
- return {
- keyInfo: keyInfo as IRecoveryKey["keyInfo"],
- encodedPrivateKey,
- privateKey,
- };
- } finally {
- decryption?.free();
- }
- }
-
- /**
- * Checks if the user has previously published cross-signing keys
- *
- * This means downloading the devicelist for the user and checking if the list includes
- * the cross-signing pseudo-device.
- *
- * @internal
- */
- public async userHasCrossSigningKeys(): Promise<boolean> {
- await this.downloadKeys([this.userId]);
- return this.deviceList.getStoredCrossSigningForUser(this.userId) !== null;
- }
-
- /**
- * Checks whether cross signing:
- * - is enabled on this account and trusted by this device
- * - has private keys either cached locally or stored in secret storage
- *
- * If this function returns false, bootstrapCrossSigning() can be used
- * to fix things such that it returns true. That is to say, after
- * bootstrapCrossSigning() completes successfully, this function should
- * return true.
- *
- * The cross-signing API is currently UNSTABLE and may change without notice.
- *
- * @returns True if cross-signing is ready to be used on this device
- */
- public async isCrossSigningReady(): Promise<boolean> {
- const publicKeysOnDevice = this.crossSigningInfo.getId();
- const privateKeysExistSomewhere =
- (await this.crossSigningInfo.isStoredInKeyCache()) ||
- (await this.crossSigningInfo.isStoredInSecretStorage(this.secretStorage));
-
- return !!(publicKeysOnDevice && privateKeysExistSomewhere);
- }
-
- /**
- * Checks whether secret storage:
- * - is enabled on this account
- * - is storing cross-signing private keys
- * - is storing session backup key (if enabled)
- *
- * If this function returns false, bootstrapSecretStorage() can be used
- * to fix things such that it returns true. That is to say, after
- * bootstrapSecretStorage() completes successfully, this function should
- * return true.
- *
- * The Secure Secret Storage API is currently UNSTABLE and may change without notice.
- *
- * @returns True if secret storage is ready to be used on this device
- */
- public async isSecretStorageReady(): Promise<boolean> {
- const secretStorageKeyInAccount = await this.secretStorage.hasKey();
- const privateKeysInStorage = await this.crossSigningInfo.isStoredInSecretStorage(this.secretStorage);
- const sessionBackupInStorage =
- !this.backupManager.getKeyBackupEnabled() || (await this.baseApis.isKeyBackupKeyStored());
-
- return !!(secretStorageKeyInAccount && privateKeysInStorage && sessionBackupInStorage);
- }
-
- /**
- * Bootstrap cross-signing by creating keys if needed. If everything is already
- * set up, then no changes are made, so this is safe to run to ensure
- * cross-signing is ready for use.
- *
- * This function:
- * - creates new cross-signing keys if they are not found locally cached nor in
- * secret storage (if it has been setup)
- *
- * The cross-signing API is currently UNSTABLE and may change without notice.
- *
- * @param authUploadDeviceSigningKeys - Function
- * called to await an interactive auth flow when uploading device signing keys.
- * @param setupNewCrossSigning - Optional. Reset even if keys
- * already exist.
- * Args:
- * A function that makes the request requiring auth. Receives the
- * auth data as an object. Can be called multiple times, first with an empty
- * authDict, to obtain the flows.
- */
- public async bootstrapCrossSigning({
- authUploadDeviceSigningKeys,
- setupNewCrossSigning,
- }: IBootstrapCrossSigningOpts = {}): Promise<void> {
- logger.log("Bootstrapping cross-signing");
-
- const delegateCryptoCallbacks = this.baseApis.cryptoCallbacks;
- const builder = new EncryptionSetupBuilder(this.baseApis.store.accountData, delegateCryptoCallbacks);
- const crossSigningInfo = new CrossSigningInfo(
- this.userId,
- builder.crossSigningCallbacks,
- builder.crossSigningCallbacks,
- );
-
- // Reset the cross-signing keys
- const resetCrossSigning = async (): Promise<void> => {
- crossSigningInfo.resetKeys();
- // Sign master key with device key
- await this.signObject(crossSigningInfo.keys.master);
-
- // Store auth flow helper function, as we need to call it when uploading
- // to ensure we handle auth errors properly.
- builder.addCrossSigningKeys(authUploadDeviceSigningKeys, crossSigningInfo.keys);
-
- // Cross-sign own device
- const device = this.deviceList.getStoredDevice(this.userId, this.deviceId)!;
- const deviceSignature = await crossSigningInfo.signDevice(this.userId, device);
- builder.addKeySignature(this.userId, this.deviceId, deviceSignature!);
-
- // Sign message key backup with cross-signing master key
- if (this.backupManager.backupInfo) {
- await crossSigningInfo.signObject(this.backupManager.backupInfo.auth_data, "master");
- builder.addSessionBackup(this.backupManager.backupInfo);
- }
- };
-
- const publicKeysOnDevice = this.crossSigningInfo.getId();
- const privateKeysInCache = await this.crossSigningInfo.isStoredInKeyCache();
- const privateKeysInStorage = await this.crossSigningInfo.isStoredInSecretStorage(this.secretStorage);
- const privateKeysExistSomewhere = privateKeysInCache || privateKeysInStorage;
-
- // Log all relevant state for easier parsing of debug logs.
- logger.log({
- setupNewCrossSigning,
- publicKeysOnDevice,
- privateKeysInCache,
- privateKeysInStorage,
- privateKeysExistSomewhere,
- });
-
- if (!privateKeysExistSomewhere || setupNewCrossSigning) {
- logger.log("Cross-signing private keys not found locally or in secret storage, " + "creating new keys");
- // If a user has multiple devices, it important to only call bootstrap
- // as part of some UI flow (and not silently during startup), as they
- // may have setup cross-signing on a platform which has not saved keys
- // to secret storage, and this would reset them. In such a case, you
- // should prompt the user to verify any existing devices first (and
- // request private keys from those devices) before calling bootstrap.
- await resetCrossSigning();
- } else if (publicKeysOnDevice && privateKeysInCache) {
- logger.log("Cross-signing public keys trusted and private keys found locally");
- } else if (privateKeysInStorage) {
- logger.log(
- "Cross-signing private keys not found locally, but they are available " +
- "in secret storage, reading storage and caching locally",
- );
- await this.checkOwnCrossSigningTrust({
- allowPrivateKeyRequests: true,
- });
- }
-
- // Assuming no app-supplied callback, default to storing new private keys in
- // secret storage if it exists. If it does not, it is assumed this will be
- // done as part of setting up secret storage later.
- const crossSigningPrivateKeys = builder.crossSigningCallbacks.privateKeys;
- if (crossSigningPrivateKeys.size && !this.baseApis.cryptoCallbacks.saveCrossSigningKeys) {
- const secretStorage = new SecretStorage(
- builder.accountDataClientAdapter,
- builder.ssssCryptoCallbacks,
- undefined,
- );
- if (await secretStorage.hasKey()) {
- logger.log("Storing new cross-signing private keys in secret storage");
- // This is writing to in-memory account data in
- // builder.accountDataClientAdapter so won't fail
- await CrossSigningInfo.storeInSecretStorage(crossSigningPrivateKeys, secretStorage);
- }
- }
-
- const operation = builder.buildOperation();
- await operation.apply(this);
- // This persists private keys and public keys as trusted,
- // only do this if apply succeeded for now as retry isn't in place yet
- await builder.persist(this);
-
- logger.log("Cross-signing ready");
- }
-
- /**
- * Bootstrap Secure Secret Storage if needed by creating a default key. If everything is
- * already set up, then no changes are made, so this is safe to run to ensure secret
- * storage is ready for use.
- *
- * This function
- * - creates a new Secure Secret Storage key if no default key exists
- * - if a key backup exists, it is migrated to store the key in the Secret
- * Storage
- * - creates a backup if none exists, and one is requested
- * - migrates Secure Secret Storage to use the latest algorithm, if an outdated
- * algorithm is found
- *
- * The Secure Secret Storage API is currently UNSTABLE and may change without notice.
- *
- * @param createSecretStorageKey - Optional. Function
- * called to await a secret storage key creation flow.
- * Returns a Promise which resolves to an object with public key metadata, encoded private
- * recovery key which should be disposed of after displaying to the user,
- * and raw private key to avoid round tripping if needed.
- * @param keyBackupInfo - The current key backup object. If passed,
- * the passphrase and recovery key from this backup will be used.
- * @param setupNewKeyBackup - If true, a new key backup version will be
- * created and the private key stored in the new SSSS store. Ignored if keyBackupInfo
- * is supplied.
- * @param setupNewSecretStorage - Optional. Reset even if keys already exist.
- * @param getKeyBackupPassphrase - Optional. Function called to get the user's
- * current key backup passphrase. Should return a promise that resolves with a Buffer
- * containing the key, or rejects if the key cannot be obtained.
- * Returns:
- * A promise which resolves to key creation data for
- * SecretStorage#addKey: an object with `passphrase` etc fields.
- */
- // TODO this does not resolve with what it says it does
- public async bootstrapSecretStorage({
- createSecretStorageKey = async (): Promise<IRecoveryKey> => ({} as IRecoveryKey),
- keyBackupInfo,
- setupNewKeyBackup,
- setupNewSecretStorage,
- getKeyBackupPassphrase,
- }: ICreateSecretStorageOpts = {}): Promise<void> {
- logger.log("Bootstrapping Secure Secret Storage");
- const delegateCryptoCallbacks = this.baseApis.cryptoCallbacks;
- const builder = new EncryptionSetupBuilder(this.baseApis.store.accountData, delegateCryptoCallbacks);
- const secretStorage = new SecretStorage(
- builder.accountDataClientAdapter,
- builder.ssssCryptoCallbacks,
- undefined,
- );
-
- // the ID of the new SSSS key, if we create one
- let newKeyId: string | null = null;
-
- // create a new SSSS key and set it as default
- const createSSSS = async (opts: IAddSecretStorageKeyOpts, privateKey?: Uint8Array): Promise<string> => {
- if (privateKey) {
- opts.key = privateKey;
- }
-
- const { keyId, keyInfo } = await secretStorage.addKey(SECRET_STORAGE_ALGORITHM_V1_AES, opts);
-
- if (privateKey) {
- // make the private key available to encrypt 4S secrets
- builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyInfo, privateKey);
- }
-
- await secretStorage.setDefaultKeyId(keyId);
- return keyId;
- };
-
- const ensureCanCheckPassphrase = async (keyId: string, keyInfo: SecretStorageKeyDescription): Promise<void> => {
- if (!keyInfo.mac) {
- const key = await this.baseApis.cryptoCallbacks.getSecretStorageKey?.(
- { keys: { [keyId]: keyInfo } },
- "",
- );
- if (key) {
- const privateKey = key[1];
- builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyInfo, privateKey);
- const { iv, mac } = await calculateKeyCheck(privateKey);
- keyInfo.iv = iv;
- keyInfo.mac = mac;
-
- await builder.setAccountData(`m.secret_storage.key.${keyId}`, keyInfo);
- }
- }
- };
-
- const signKeyBackupWithCrossSigning = async (keyBackupAuthData: IKeyBackupInfo["auth_data"]): Promise<void> => {
- if (this.crossSigningInfo.getId() && (await this.crossSigningInfo.isStoredInKeyCache("master"))) {
- try {
- logger.log("Adding cross-signing signature to key backup");
- await this.crossSigningInfo.signObject(keyBackupAuthData, "master");
- } catch (e) {
- // This step is not critical (just helpful), so we catch here
- // and continue if it fails.
- logger.error("Signing key backup with cross-signing keys failed", e);
- }
- } else {
- logger.warn("Cross-signing keys not available, skipping signature on key backup");
- }
- };
-
- const oldSSSSKey = await this.getSecretStorageKey();
- const [oldKeyId, oldKeyInfo] = oldSSSSKey || [null, null];
- const storageExists =
- !setupNewSecretStorage && oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES;
-
- // Log all relevant state for easier parsing of debug logs.
- logger.log({
- keyBackupInfo,
- setupNewKeyBackup,
- setupNewSecretStorage,
- storageExists,
- oldKeyInfo,
- });
-
- if (!storageExists && !keyBackupInfo) {
- // either we don't have anything, or we've been asked to restart
- // from scratch
- logger.log("Secret storage does not exist, creating new storage key");
-
- // if we already have a usable default SSSS key and aren't resetting
- // SSSS just use it. otherwise, create a new one
- // Note: we leave the old SSSS key in place: there could be other
- // secrets using it, in theory. We could move them to the new key but a)
- // that would mean we'd need to prompt for the old passphrase, and b)
- // it's not clear that would be the right thing to do anyway.
- const { keyInfo = {} as IAddSecretStorageKeyOpts, privateKey } = await createSecretStorageKey();
- newKeyId = await createSSSS(keyInfo, privateKey);
- } else if (!storageExists && keyBackupInfo) {
- // we have an existing backup, but no SSSS
- logger.log("Secret storage does not exist, using key backup key");
-
- // if we have the backup key already cached, use it; otherwise use the
- // callback to prompt for the key
- const backupKey = (await this.getSessionBackupPrivateKey()) || (await getKeyBackupPassphrase?.());
-
- // create a new SSSS key and use the backup key as the new SSSS key
- const opts = {} as IAddSecretStorageKeyOpts;
-
- if (keyBackupInfo.auth_data.private_key_salt && keyBackupInfo.auth_data.private_key_iterations) {
- // FIXME: ???
- opts.passphrase = {
- algorithm: "m.pbkdf2",
- iterations: keyBackupInfo.auth_data.private_key_iterations,
- salt: keyBackupInfo.auth_data.private_key_salt,
- bits: 256,
- };
- }
-
- newKeyId = await createSSSS(opts, backupKey);
-
- // store the backup key in secret storage
- await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(backupKey!), [newKeyId]);
-
- // The backup is trusted because the user provided the private key.
- // Sign the backup with the cross-signing key so the key backup can
- // be trusted via cross-signing.
- await signKeyBackupWithCrossSigning(keyBackupInfo.auth_data);
-
- builder.addSessionBackup(keyBackupInfo);
- } else {
- // 4S is already set up
- logger.log("Secret storage exists");
-
- if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
- // make sure that the default key has the information needed to
- // check the passphrase
- await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo);
- }
- }
-
- // If we have cross-signing private keys cached, store them in secret
- // storage if they are not there already.
- if (
- !this.baseApis.cryptoCallbacks.saveCrossSigningKeys &&
- (await this.isCrossSigningReady()) &&
- (newKeyId || !(await this.crossSigningInfo.isStoredInSecretStorage(secretStorage)))
- ) {
- logger.log("Copying cross-signing private keys from cache to secret storage");
- const crossSigningPrivateKeys = await this.crossSigningInfo.getCrossSigningKeysFromCache();
- // This is writing to in-memory account data in
- // builder.accountDataClientAdapter so won't fail
- await CrossSigningInfo.storeInSecretStorage(crossSigningPrivateKeys, secretStorage);
- }
-
- if (setupNewKeyBackup && !keyBackupInfo) {
- logger.log("Creating new message key backup version");
- const info = await this.baseApis.prepareKeyBackupVersion(
- null /* random key */,
- // don't write to secret storage, as it will write to this.secretStorage.
- // Here, we want to capture all the side-effects of bootstrapping,
- // and want to write to the local secretStorage object
- { secureSecretStorage: false },
- );
- // write the key ourselves to 4S
- const privateKey = decodeRecoveryKey(info.recovery_key);
- await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(privateKey));
-
- // create keyBackupInfo object to add to builder
- const data: IKeyBackupInfo = {
- algorithm: info.algorithm,
- auth_data: info.auth_data,
- };
-
- // Sign with cross-signing master key
- await signKeyBackupWithCrossSigning(data.auth_data);
-
- // sign with the device fingerprint
- await this.signObject(data.auth_data);
-
- builder.addSessionBackup(data);
- }
-
- // Cache the session backup key
- const sessionBackupKey = await secretStorage.get("m.megolm_backup.v1");
- if (sessionBackupKey) {
- logger.info("Got session backup key from secret storage: caching");
- // fix up the backup key if it's in the wrong format, and replace
- // in secret storage
- const fixedBackupKey = fixBackupKey(sessionBackupKey);
- if (fixedBackupKey) {
- const keyId = newKeyId || oldKeyId;
- await secretStorage.store("m.megolm_backup.v1", fixedBackupKey, keyId ? [keyId] : null);
- }
- const decodedBackupKey = new Uint8Array(olmlib.decodeBase64(fixedBackupKey || sessionBackupKey));
- 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
- const backupKey = (await this.getSessionBackupPrivateKey()) || (await getKeyBackupPassphrase?.());
- if (!backupKey) {
- // This will require user intervention to recover from since we don't have the key
- // backup key anywhere. The user should probably just set up a new key backup and
- // the key for the new backup will be stored. If we hit this scenario in the wild
- // with any frequency, we should do more than just log an error.
- logger.error("Key backup is enabled but couldn't get key backup key!");
- return;
- }
- logger.info("Got session backup key from cache/user that wasn't in SSSS: saving to SSSS");
- await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(backupKey));
- }
-
- const operation = builder.buildOperation();
- await operation.apply(this);
- // this persists private keys and public keys as trusted,
- // only do this if apply succeeded for now as retry isn't in place yet
- await builder.persist(this);
-
- logger.log("Secure Secret Storage ready");
- }
-
- public addSecretStorageKey(
- algorithm: string,
- opts: IAddSecretStorageKeyOpts,
- keyID?: string,
- ): Promise<SecretStorageKeyObject> {
- return this.secretStorage.addKey(algorithm, opts, keyID);
- }
-
- public hasSecretStorageKey(keyID?: string): Promise<boolean> {
- return this.secretStorage.hasKey(keyID);
- }
-
- public getSecretStorageKey(keyID?: string): Promise<SecretStorageKeyTuple | null> {
- return this.secretStorage.getKey(keyID);
- }
-
- public storeSecret(name: string, secret: string, keys?: string[]): Promise<void> {
- return this.secretStorage.store(name, secret, keys);
- }
-
- public getSecret(name: string): Promise<string | undefined> {
- return this.secretStorage.get(name);
- }
-
- public isSecretStored(name: string): Promise<Record<string, SecretStorageKeyDescription> | null> {
- return this.secretStorage.isStored(name);
- }
-
- public requestSecret(name: string, devices: string[]): ISecretRequest {
- if (!devices) {
- devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(this.userId));
- }
- return this.secretStorage.request(name, devices);
- }
-
- public getDefaultSecretStorageKeyId(): Promise<string | null> {
- return this.secretStorage.getDefaultKeyId();
- }
-
- public setDefaultSecretStorageKeyId(k: string): Promise<void> {
- return this.secretStorage.setDefaultKeyId(k);
- }
-
- public checkSecretStorageKey(key: Uint8Array, info: SecretStorageKeyDescription): Promise<boolean> {
- return this.secretStorage.checkKey(key, info);
- }
-
- /**
- * Checks that a given secret storage private key matches a given public key.
- * This can be used by the getSecretStorageKey callback to verify that the
- * private key it is about to supply is the one that was requested.
- *
- * @param privateKey - The private key
- * @param expectedPublicKey - The public key
- * @returns true if the key matches, otherwise false
- */
- public checkSecretStoragePrivateKey(privateKey: Uint8Array, expectedPublicKey: string): boolean {
- let decryption: PkDecryption | null = null;
- try {
- decryption = new global.Olm.PkDecryption();
- const gotPubkey = decryption.init_with_private_key(privateKey);
- // make sure it agrees with the given pubkey
- return gotPubkey === expectedPublicKey;
- } finally {
- decryption?.free();
- }
- }
-
- /**
- * Fetches the backup private key, if cached
- * @returns the key, if any, or null
- */
- public async getSessionBackupPrivateKey(): Promise<Uint8Array | null> {
- let key = await new Promise<any>((resolve) => {
- // TODO types
- this.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.cryptoStore.getSecretStorePrivateKey(txn, resolve, "m.megolm_backup.v1");
- });
- });
-
- // make sure we have a Uint8Array, rather than a string
- if (key && typeof key === "string") {
- key = new Uint8Array(olmlib.decodeBase64(fixBackupKey(key) || key));
- await this.storeSessionBackupPrivateKey(key);
- }
- if (key && key.ciphertext) {
- const pickleKey = Buffer.from(this.olmDevice.pickleKey);
- const decrypted = await decryptAES(key, pickleKey, "m.megolm_backup.v1");
- key = olmlib.decodeBase64(decrypted);
- }
- return key;
- }
-
- /**
- * Stores the session backup key to the cache
- * @param key - the private key
- * @returns a promise so you can catch failures
- */
- public async storeSessionBackupPrivateKey(key: ArrayLike<number>): Promise<void> {
- if (!(key instanceof Uint8Array)) {
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
- throw new Error(`storeSessionBackupPrivateKey expects Uint8Array, got ${key}`);
- }
- const pickleKey = Buffer.from(this.olmDevice.pickleKey);
- const encryptedKey = await encryptAES(olmlib.encodeBase64(key), pickleKey, "m.megolm_backup.v1");
- return this.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.cryptoStore.storeSecretStorePrivateKey(txn, "m.megolm_backup.v1", encryptedKey);
- });
- }
-
- /**
- * Checks that a given cross-signing private key matches a given public key.
- * This can be used by the getCrossSigningKey callback to verify that the
- * private key it is about to supply is the one that was requested.
- *
- * @param privateKey - The private key
- * @param expectedPublicKey - The public key
- * @returns true if the key matches, otherwise false
- */
- public checkCrossSigningPrivateKey(privateKey: Uint8Array, expectedPublicKey: string): boolean {
- let signing: PkSigning | null = null;
- try {
- signing = new global.Olm.PkSigning();
- const gotPubkey = signing.init_with_seed(privateKey);
- // make sure it agrees with the given pubkey
- return gotPubkey === expectedPublicKey;
- } finally {
- signing?.free();
- }
- }
-
- /**
- * Run various follow-up actions after cross-signing keys have changed locally
- * (either by resetting the keys for the account or by getting them from secret
- * storage), such as signing the current device, upgrading device
- * verifications, etc.
- */
- private async afterCrossSigningLocalKeyChange(): Promise<void> {
- logger.info("Starting cross-signing key change post-processing");
-
- // sign the current device with the new key, and upload to the server
- const device = this.deviceList.getStoredDevice(this.userId, this.deviceId)!;
- const signedDevice = await this.crossSigningInfo.signDevice(this.userId, device);
- logger.info(`Starting background key sig upload for ${this.deviceId}`);
-
- const upload = ({ shouldEmit = false }): Promise<void> => {
- return this.baseApis
- .uploadKeySignatures({
- [this.userId]: {
- [this.deviceId]: signedDevice!,
- },
- })
- .then((response) => {
- const { failures } = response || {};
- if (Object.keys(failures || []).length > 0) {
- if (shouldEmit) {
- this.baseApis.emit(
- CryptoEvent.KeySignatureUploadFailure,
- failures,
- "afterCrossSigningLocalKeyChange",
- upload, // continuation
- );
- }
- throw new KeySignatureUploadError("Key upload failed", { failures });
- }
- logger.info(`Finished background key sig upload for ${this.deviceId}`);
- })
- .catch((e) => {
- logger.error(`Error during background key sig upload for ${this.deviceId}`, e);
- });
- };
- upload({ shouldEmit: true });
-
- const shouldUpgradeCb = this.baseApis.cryptoCallbacks.shouldUpgradeDeviceVerifications;
- if (shouldUpgradeCb) {
- logger.info("Starting device verification upgrade");
-
- // Check all users for signatures if upgrade callback present
- // FIXME: do this in batches
- const users: Record<string, IDeviceVerificationUpgrade> = {};
- for (const [userId, crossSigningInfo] of Object.entries(this.deviceList.crossSigningInfo)) {
- const upgradeInfo = await this.checkForDeviceVerificationUpgrade(
- userId,
- CrossSigningInfo.fromStorage(crossSigningInfo, userId),
- );
- if (upgradeInfo) {
- users[userId] = upgradeInfo;
- }
- }
-
- if (Object.keys(users).length > 0) {
- logger.info(`Found ${Object.keys(users).length} verif users to upgrade`);
- try {
- const usersToUpgrade = await shouldUpgradeCb({ users: users });
- if (usersToUpgrade) {
- for (const userId of usersToUpgrade) {
- if (userId in users) {
- await this.baseApis.setDeviceVerified(userId, users[userId].crossSigningInfo.getId()!);
- }
- }
- }
- } catch (e) {
- logger.log("shouldUpgradeDeviceVerifications threw an error: not upgrading", e);
- }
- }
-
- logger.info("Finished device verification upgrade");
- }
-
- logger.info("Finished cross-signing key change post-processing");
- }
-
- /**
- * Check if a user's cross-signing key is a candidate for upgrading from device
- * verification.
- *
- * @param userId - the user whose cross-signing information is to be checked
- * @param crossSigningInfo - the cross-signing information to check
- */
- private async checkForDeviceVerificationUpgrade(
- userId: string,
- crossSigningInfo: CrossSigningInfo,
- ): Promise<IDeviceVerificationUpgrade | undefined> {
- // only upgrade if this is the first cross-signing key that we've seen for
- // them, and if their cross-signing key isn't already verified
- const trustLevel = this.crossSigningInfo.checkUserTrust(crossSigningInfo);
- if (crossSigningInfo.firstUse && !trustLevel.isVerified()) {
- const devices = this.deviceList.getRawStoredDevicesForUser(userId);
- const deviceIds = await this.checkForValidDeviceSignature(userId, crossSigningInfo.keys.master, devices);
- if (deviceIds.length) {
- return {
- devices: deviceIds.map((deviceId) => DeviceInfo.fromStorage(devices[deviceId], deviceId)),
- crossSigningInfo,
- };
- }
- }
- }
-
- /**
- * Check if the cross-signing key is signed by a verified device.
- *
- * @param userId - the user ID whose key is being checked
- * @param key - the key that is being checked
- * @param devices - the user's devices. Should be a map from device ID
- * to device info
- */
- private async checkForValidDeviceSignature(
- userId: string,
- key: ICrossSigningKey,
- devices: Record<string, IDevice>,
- ): Promise<string[]> {
- const deviceIds: string[] = [];
- if (devices && key.signatures && key.signatures[userId]) {
- for (const signame of Object.keys(key.signatures[userId])) {
- const [, deviceId] = signame.split(":", 2);
- if (deviceId in devices && devices[deviceId].verified === DeviceVerification.VERIFIED) {
- try {
- await olmlib.verifySignature(
- this.olmDevice,
- key,
- userId,
- deviceId,
- devices[deviceId].keys[signame],
- );
- deviceIds.push(deviceId);
- } catch (e) {}
- }
- }
- }
- return deviceIds;
- }
-
- /**
- * Get the user's cross-signing key ID.
- *
- * @param type - The type of key to get the ID of. One of
- * "master", "self_signing", or "user_signing". Defaults to "master".
- *
- * @returns the key ID
- */
- public getCrossSigningId(type: string): string | null {
- return this.crossSigningInfo.getId(type);
- }
-
- /**
- * Get the cross signing information for a given user.
- *
- * @param userId - the user ID to get the cross-signing info for.
- *
- * @returns the cross signing information for the user.
- */
- public getStoredCrossSigningForUser(userId: string): CrossSigningInfo | null {
- return this.deviceList.getStoredCrossSigningForUser(userId);
- }
-
- /**
- * Check whether a given user is trusted.
- *
- * @param userId - The ID of the user to check.
- *
- * @returns
- */
- public checkUserTrust(userId: string): UserTrustLevel {
- const userCrossSigning = this.deviceList.getStoredCrossSigningForUser(userId);
- if (!userCrossSigning) {
- return new UserTrustLevel(false, false, false);
- }
- return this.crossSigningInfo.checkUserTrust(userCrossSigning);
- }
-
- /**
- * Check whether a given device is trusted.
- *
- * @param userId - The ID of the user whose devices is to be checked.
- * @param deviceId - The ID of the device to check
- *
- * @returns
- */
- public checkDeviceTrust(userId: string, deviceId: string): DeviceTrustLevel {
- const device = this.deviceList.getStoredDevice(userId, deviceId);
- return this.checkDeviceInfoTrust(userId, device);
- }
-
- /**
- * Check whether a given deviceinfo is trusted.
- *
- * @param userId - The ID of the user whose devices is to be checked.
- * @param device - The device info object to check
- *
- * @returns
- */
- public checkDeviceInfoTrust(userId: string, device?: DeviceInfo): DeviceTrustLevel {
- const trustedLocally = !!device?.isVerified();
-
- const userCrossSigning = this.deviceList.getStoredCrossSigningForUser(userId);
- if (device && userCrossSigning) {
- // The trustCrossSignedDevices only affects trust of other people's cross-signing
- // signatures
- const trustCrossSig = this.trustCrossSignedDevices || userId === this.userId;
- return this.crossSigningInfo.checkDeviceTrust(userCrossSigning, device, trustedLocally, trustCrossSig);
- } else {
- return new DeviceTrustLevel(false, false, trustedLocally, false);
- }
- }
-
- /**
- * 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 deviceId - The ID of the device to check
- *
- * @returns true if the device is cross-signed
- */
- public checkIfOwnDeviceCrossSigned(deviceId: string): boolean {
- const device = this.deviceList.getStoredDevice(this.userId, deviceId);
- if (!device) return false;
- const userCrossSigning = this.deviceList.getStoredCrossSigningForUser(this.userId);
- return (
- userCrossSigning?.checkDeviceTrust(userCrossSigning, device, false, true).isCrossSigningVerified() ?? false
- );
- }
-
- /*
- * Event handler for DeviceList's userNewDevices event
- */
- private onDeviceListUserCrossSigningUpdated = async (userId: string): Promise<void> => {
- if (userId === this.userId) {
- // An update to our own cross-signing key.
- // Get the new key first:
- const newCrossSigning = this.deviceList.getStoredCrossSigningForUser(userId);
- const seenPubkey = newCrossSigning ? newCrossSigning.getId() : null;
- const currentPubkey = this.crossSigningInfo.getId();
- const changed = currentPubkey !== seenPubkey;
-
- if (currentPubkey && seenPubkey && !changed) {
- // If it's not changed, just make sure everything is up to date
- await this.checkOwnCrossSigningTrust();
- } else {
- // We'll now be in a state where cross-signing on the account is not trusted
- // because our locally stored cross-signing keys will not match the ones
- // on the server for our account. So we clear our own stored cross-signing keys,
- // effectively disabling cross-signing until the user gets verified by the device
- // that reset the keys
- this.storeTrustedSelfKeys(null);
- // emit cross-signing has been disabled
- this.emit(CryptoEvent.KeysChanged, {});
- // as the trust for our own user has changed,
- // also emit an event for this
- this.emit(CryptoEvent.UserTrustStatusChanged, this.userId, this.checkUserTrust(userId));
- }
- } else {
- await this.checkDeviceVerifications(userId);
-
- // Update verified before latch using the current state and save the new
- // latch value in the device list store.
- const crossSigning = this.deviceList.getStoredCrossSigningForUser(userId);
- if (crossSigning) {
- crossSigning.updateCrossSigningVerifiedBefore(this.checkUserTrust(userId).isCrossSigningVerified());
- this.deviceList.setRawStoredCrossSigningForUser(userId, crossSigning.toStorage());
- }
-
- this.emit(CryptoEvent.UserTrustStatusChanged, userId, this.checkUserTrust(userId));
- }
- };
-
- /**
- * Check the copy of our cross-signing key that we have in the device list and
- * see if we can get the private key. If so, mark it as trusted.
- */
- public async checkOwnCrossSigningTrust({
- allowPrivateKeyRequests = false,
- }: ICheckOwnCrossSigningTrustOpts = {}): Promise<void> {
- const userId = this.userId;
-
- // Before proceeding, ensure our cross-signing public keys have been
- // downloaded via the device list.
- await this.downloadKeys([this.userId]);
-
- // Also check which private keys are locally cached.
- const crossSigningPrivateKeys = await this.crossSigningInfo.getCrossSigningKeysFromCache();
-
- // If we see an update to our own master key, check it against the master
- // key we have and, if it matches, mark it as verified
-
- // First, get the new cross-signing info
- const newCrossSigning = this.deviceList.getStoredCrossSigningForUser(userId);
- if (!newCrossSigning) {
- logger.error(
- "Got cross-signing update event for user " + userId + " but no new cross-signing information found!",
- );
- return;
- }
-
- const seenPubkey = newCrossSigning.getId()!;
- const masterChanged = this.crossSigningInfo.getId() !== seenPubkey;
- const masterExistsNotLocallyCached = newCrossSigning.getId() && !crossSigningPrivateKeys.has("master");
- if (masterChanged) {
- logger.info("Got new master public key", seenPubkey);
- }
- if (allowPrivateKeyRequests && (masterChanged || masterExistsNotLocallyCached)) {
- logger.info("Attempting to retrieve cross-signing master private key");
- let signing: PkSigning | null = null;
- // It's important for control flow that we leave any errors alone for
- // higher levels to handle so that e.g. cancelling access properly
- // aborts any larger operation as well.
- try {
- const ret = await this.crossSigningInfo.getCrossSigningKey("master", seenPubkey);
- signing = ret[1];
- logger.info("Got cross-signing master private key");
- } finally {
- signing?.free();
- }
- }
-
- const oldSelfSigningId = this.crossSigningInfo.getId("self_signing");
- const oldUserSigningId = this.crossSigningInfo.getId("user_signing");
-
- // Update the version of our keys in our cross-signing object and the local store
- this.storeTrustedSelfKeys(newCrossSigning.keys);
-
- const selfSigningChanged = oldSelfSigningId !== newCrossSigning.getId("self_signing");
- const userSigningChanged = oldUserSigningId !== newCrossSigning.getId("user_signing");
-
- const selfSigningExistsNotLocallyCached =
- newCrossSigning.getId("self_signing") && !crossSigningPrivateKeys.has("self_signing");
- const userSigningExistsNotLocallyCached =
- newCrossSigning.getId("user_signing") && !crossSigningPrivateKeys.has("user_signing");
-
- const keySignatures: Record<string, ISignedKey> = {};
-
- if (selfSigningChanged) {
- logger.info("Got new self-signing key", newCrossSigning.getId("self_signing"));
- }
- if (allowPrivateKeyRequests && (selfSigningChanged || selfSigningExistsNotLocallyCached)) {
- logger.info("Attempting to retrieve cross-signing self-signing private key");
- let signing: PkSigning | null = null;
- try {
- const ret = await this.crossSigningInfo.getCrossSigningKey(
- "self_signing",
- newCrossSigning.getId("self_signing")!,
- );
- signing = ret[1];
- logger.info("Got cross-signing self-signing private key");
- } finally {
- signing?.free();
- }
-
- const device = this.deviceList.getStoredDevice(this.userId, this.deviceId)!;
- const signedDevice = await this.crossSigningInfo.signDevice(this.userId, device);
- keySignatures[this.deviceId] = signedDevice!;
- }
- if (userSigningChanged) {
- logger.info("Got new user-signing key", newCrossSigning.getId("user_signing"));
- }
- if (allowPrivateKeyRequests && (userSigningChanged || userSigningExistsNotLocallyCached)) {
- logger.info("Attempting to retrieve cross-signing user-signing private key");
- let signing: PkSigning | null = null;
- try {
- const ret = await this.crossSigningInfo.getCrossSigningKey(
- "user_signing",
- newCrossSigning.getId("user_signing")!,
- );
- signing = ret[1];
- logger.info("Got cross-signing user-signing private key");
- } finally {
- signing?.free();
- }
- }
-
- if (masterChanged) {
- const masterKey = this.crossSigningInfo.keys.master;
- await this.signObject(masterKey);
- const deviceSig = masterKey.signatures![this.userId]["ed25519:" + this.deviceId];
- // Include only the _new_ device signature in the upload.
- // We may have existing signatures from deleted devices, which will cause
- // the entire upload to fail.
- keySignatures[this.crossSigningInfo.getId()!] = Object.assign({} as ISignedKey, masterKey, {
- signatures: {
- [this.userId]: {
- ["ed25519:" + this.deviceId]: deviceSig,
- },
- },
- });
- }
-
- const keysToUpload = Object.keys(keySignatures);
- if (keysToUpload.length) {
- const upload = ({ shouldEmit = false }): Promise<void> => {
- logger.info(`Starting background key sig upload for ${keysToUpload}`);
- return this.baseApis
- .uploadKeySignatures({ [this.userId]: keySignatures })
- .then((response) => {
- const { failures } = response || {};
- logger.info(`Finished background key sig upload for ${keysToUpload}`);
- if (Object.keys(failures || []).length > 0) {
- if (shouldEmit) {
- this.baseApis.emit(
- CryptoEvent.KeySignatureUploadFailure,
- failures,
- "checkOwnCrossSigningTrust",
- upload,
- );
- }
- throw new KeySignatureUploadError("Key upload failed", { failures });
- }
- })
- .catch((e) => {
- logger.error(`Error during background key sig upload for ${keysToUpload}`, e);
- });
- };
- upload({ shouldEmit: true });
- }
-
- this.emit(CryptoEvent.UserTrustStatusChanged, userId, this.checkUserTrust(userId));
-
- if (masterChanged) {
- this.emit(CryptoEvent.KeysChanged, {});
- await this.afterCrossSigningLocalKeyChange();
- }
-
- // Now we may be able to trust our key backup
- await this.backupManager.checkKeyBackup();
- // FIXME: if we previously trusted the backup, should we automatically sign
- // the backup with the new key (if not already signed)?
- }
-
- /**
- * Store a set of keys as our own, trusted, cross-signing keys.
- *
- * @param keys - The new trusted set of keys
- */
- private async storeTrustedSelfKeys(keys: Record<string, ICrossSigningKey> | null): Promise<void> {
- if (keys) {
- this.crossSigningInfo.setKeys(keys);
- } else {
- this.crossSigningInfo.clearKeys();
- }
- await this.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
- this.cryptoStore.storeCrossSigningKeys(txn, this.crossSigningInfo.keys);
- });
- }
-
- /**
- * Check if the master key is signed by a verified device, and if so, prompt
- * the application to mark it as verified.
- *
- * @param userId - the user ID whose key should be checked
- */
- private async checkDeviceVerifications(userId: string): Promise<void> {
- const shouldUpgradeCb = this.baseApis.cryptoCallbacks.shouldUpgradeDeviceVerifications;
- if (!shouldUpgradeCb) {
- // Upgrading skipped when callback is not present.
- return;
- }
- logger.info(`Starting device verification upgrade for ${userId}`);
- if (this.crossSigningInfo.keys.user_signing) {
- const crossSigningInfo = this.deviceList.getStoredCrossSigningForUser(userId);
- if (crossSigningInfo) {
- const upgradeInfo = await this.checkForDeviceVerificationUpgrade(userId, crossSigningInfo);
- if (upgradeInfo) {
- const usersToUpgrade = await shouldUpgradeCb({
- users: {
- [userId]: upgradeInfo,
- },
- });
- if (usersToUpgrade.includes(userId)) {
- await this.baseApis.setDeviceVerified(userId, crossSigningInfo.getId()!);
- }
- }
- }
- }
- logger.info(`Finished device verification upgrade for ${userId}`);
- }
-
- /**
- */
- public enableLazyLoading(): void {
- this.lazyLoadMembers = true;
- }
-
- /**
- * Tell the crypto module to register for MatrixClient events which it needs to
- * listen for
- *
- * @param eventEmitter - event source where we can register
- * for event notifications
- */
- public registerEventHandlers(
- eventEmitter: TypedEventEmitter<
- RoomMemberEvent.Membership | ClientEvent.ToDeviceEvent | RoomEvent.Timeline | MatrixEventEvent.Decrypted,
- any
- >,
- ): void {
- eventEmitter.on(RoomMemberEvent.Membership, this.onMembership);
- eventEmitter.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
- eventEmitter.on(RoomEvent.Timeline, this.onTimelineEvent);
- eventEmitter.on(MatrixEventEvent.Decrypted, this.onTimelineEvent);
- }
-
- /**
- * @deprecated this does nothing and will be removed in a future version
- */
- public start(): void {
- logger.warn("MatrixClient.crypto.start() is deprecated");
- }
-
- /** Stop background processes related to crypto */
- public stop(): void {
- this.outgoingRoomKeyRequestManager.stop();
- this.deviceList.stop();
- this.dehydrationManager.stop();
- }
-
- /**
- * Get the Ed25519 key for this device
- *
- * @returns base64-encoded ed25519 key.
- */
- public getDeviceEd25519Key(): string | null {
- return this.olmDevice.deviceEd25519Key;
- }
-
- /**
- * Get the Curve25519 key for this device
- *
- * @returns base64-encoded curve25519 key.
- */
- public getDeviceCurve25519Key(): string | null {
- return this.olmDevice.deviceCurve25519Key;
- }
-
- /**
- * Set the global override for whether the client should ever send encrypted
- * messages to unverified devices. This provides the default for rooms which
- * do not specify a value.
- *
- * @param value - whether to blacklist all unverified devices by default
- *
- * @deprecated For external code, use {@link MatrixClient#setGlobalBlacklistUnverifiedDevices}. For
- * internal code, set {@link MatrixClient#globalBlacklistUnverifiedDevices} directly.
- */
- public setGlobalBlacklistUnverifiedDevices(value: boolean): void {
- this.globalBlacklistUnverifiedDevices = value;
- }
-
- /**
- * @returns whether to blacklist all unverified devices by default
- *
- * @deprecated For external code, use {@link MatrixClient#getGlobalBlacklistUnverifiedDevices}. For
- * internal code, reference {@link MatrixClient#globalBlacklistUnverifiedDevices} directly.
- */
- public getGlobalBlacklistUnverifiedDevices(): boolean {
- return this.globalBlacklistUnverifiedDevices;
- }
-
- /**
- * Upload the device keys to the homeserver.
- * @returns A promise that will resolve when the keys are uploaded.
- */
- public uploadDeviceKeys(): Promise<IKeysUploadResponse> {
- const deviceKeys = {
- algorithms: this.supportedAlgorithms,
- device_id: this.deviceId,
- keys: this.deviceKeys,
- user_id: this.userId,
- };
-
- return this.signObject(deviceKeys).then(() => {
- return this.baseApis.uploadKeysRequest({
- device_keys: deviceKeys as Required<IDeviceKeys>,
- });
- });
- }
-
- /**
- * Stores the current one_time_key count which will be handled later (in a call of
- * onSyncCompleted). The count is e.g. coming from a /sync response.
- *
- * @param currentCount - The current count of one_time_keys to be stored
- */
- public updateOneTimeKeyCount(currentCount: number): void {
- if (isFinite(currentCount)) {
- this.oneTimeKeyCount = currentCount;
- } else {
- throw new TypeError("Parameter for updateOneTimeKeyCount has to be a number");
- }
- }
-
- public setNeedsNewFallback(needsNewFallback: boolean): void {
- this.needsNewFallback = needsNewFallback;
- }
-
- public getNeedsNewFallback(): boolean {
- return !!this.needsNewFallback;
- }
-
- // check if it's time to upload one-time keys, and do so if so.
- private maybeUploadOneTimeKeys(): void {
- // frequency with which to check & upload one-time keys
- const uploadPeriod = 1000 * 60; // one minute
-
- // max number of keys to upload at once
- // Creating keys can be an expensive operation so we limit the
- // number we generate in one go to avoid blocking the application
- // for too long.
- const maxKeysPerCycle = 5;
-
- if (this.oneTimeKeyCheckInProgress) {
- return;
- }
-
- const now = Date.now();
- if (this.lastOneTimeKeyCheck !== null && now - this.lastOneTimeKeyCheck < uploadPeriod) {
- // we've done a key upload recently.
- return;
- }
-
- this.lastOneTimeKeyCheck = now;
-
- // We need to keep a pool of one time public keys on the server so that
- // other devices can start conversations with us. But we can only store
- // a finite number of private keys in the olm Account object.
- // To complicate things further then can be a delay between a device
- // claiming a public one time key from the server and it sending us a
- // message. We need to keep the corresponding private key locally until
- // we receive the message.
- // But that message might never arrive leaving us stuck with duff
- // private keys clogging up our local storage.
- // So we need some kind of engineering compromise to balance all of
- // these factors.
-
- // Check how many keys we can store in the Account object.
- const maxOneTimeKeys = this.olmDevice.maxNumberOfOneTimeKeys();
- // Try to keep at most half that number on the server. This leaves the
- // rest of the slots free to hold keys that have been claimed from the
- // server but we haven't received a message for.
- // If we run out of slots when generating new keys then olm will
- // discard the oldest private keys first. This will eventually clean
- // out stale private keys that won't receive a message.
- const keyLimit = Math.floor(maxOneTimeKeys / 2);
-
- const uploadLoop = async (keyCount: number): Promise<void> => {
- while (keyLimit > keyCount || this.getNeedsNewFallback()) {
- // Ask olm to generate new one time keys, then upload them to synapse.
- if (keyLimit > keyCount) {
- logger.info("generating oneTimeKeys");
- const keysThisLoop = Math.min(keyLimit - keyCount, maxKeysPerCycle);
- await this.olmDevice.generateOneTimeKeys(keysThisLoop);
- }
-
- if (this.getNeedsNewFallback()) {
- const fallbackKeys = await this.olmDevice.getFallbackKey();
- // if fallbackKeys is non-empty, we've already generated a
- // fallback key, but it hasn't been published yet, so we
- // can use that instead of generating a new one
- if (!fallbackKeys.curve25519 || Object.keys(fallbackKeys.curve25519).length == 0) {
- logger.info("generating fallback key");
- if (this.fallbackCleanup) {
- // cancel any pending fallback cleanup because generating
- // a new fallback key will already drop the old fallback
- // that would have been dropped, and we don't want to kill
- // the current key
- clearTimeout(this.fallbackCleanup);
- delete this.fallbackCleanup;
- }
- await this.olmDevice.generateFallbackKey();
- }
- }
-
- logger.info("calling uploadOneTimeKeys");
- const res = await this.uploadOneTimeKeys();
- if (res.one_time_key_counts && res.one_time_key_counts.signed_curve25519) {
- // if the response contains a more up to date value use this
- // for the next loop
- keyCount = res.one_time_key_counts.signed_curve25519;
- } else {
- throw new Error(
- "response for uploading keys does not contain " + "one_time_key_counts.signed_curve25519",
- );
- }
- }
- };
-
- this.oneTimeKeyCheckInProgress = true;
- Promise.resolve()
- .then(() => {
- if (this.oneTimeKeyCount !== undefined) {
- // We already have the current one_time_key count from a /sync response.
- // Use this value instead of asking the server for the current key count.
- return Promise.resolve(this.oneTimeKeyCount);
- }
- // ask the server how many keys we have
- return this.baseApis.uploadKeysRequest({}).then((res) => {
- return res.one_time_key_counts.signed_curve25519 || 0;
- });
- })
- .then((keyCount) => {
- // Start the uploadLoop with the current keyCount. The function checks if
- // we need to upload new keys or not.
- // If there are too many keys on the server then we don't need to
- // create any more keys.
- return uploadLoop(keyCount);
- })
- .catch((e) => {
- logger.error("Error uploading one-time keys", e.stack || e);
- })
- .finally(() => {
- // reset oneTimeKeyCount to prevent start uploading based on old data.
- // it will be set again on the next /sync-response
- this.oneTimeKeyCount = undefined;
- this.oneTimeKeyCheckInProgress = false;
- });
- }
-
- // returns a promise which resolves to the response
- private async uploadOneTimeKeys(): Promise<IKeysUploadResponse> {
- const promises: Promise<unknown>[] = [];
-
- let fallbackJson: Record<string, IOneTimeKey> | undefined;
- if (this.getNeedsNewFallback()) {
- fallbackJson = {};
- const fallbackKeys = await this.olmDevice.getFallbackKey();
- for (const [keyId, key] of Object.entries(fallbackKeys.curve25519)) {
- const k = { key, fallback: true };
- fallbackJson["signed_curve25519:" + keyId] = k;
- promises.push(this.signObject(k));
- }
- this.setNeedsNewFallback(false);
- }
-
- const oneTimeKeys = await this.olmDevice.getOneTimeKeys();
- const oneTimeJson: Record<string, { key: string }> = {};
-
- for (const keyId in oneTimeKeys.curve25519) {
- if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) {
- const k = {
- key: oneTimeKeys.curve25519[keyId],
- };
- oneTimeJson["signed_curve25519:" + keyId] = k;
- promises.push(this.signObject(k));
- }
- }
-
- await Promise.all(promises);
-
- const requestBody: Record<string, any> = {
- one_time_keys: oneTimeJson,
- };
-
- if (fallbackJson) {
- requestBody["org.matrix.msc2732.fallback_keys"] = fallbackJson;
- requestBody["fallback_keys"] = fallbackJson;
- }
-
- const res = await this.baseApis.uploadKeysRequest(requestBody);
-
- if (fallbackJson) {
- this.fallbackCleanup = setTimeout(() => {
- delete this.fallbackCleanup;
- this.olmDevice.forgetOldFallbackKey();
- }, 60 * 60 * 1000);
- }
-
- await this.olmDevice.markKeysAsPublished();
- return res;
- }
-
- /**
- * Download the keys for a list of users and stores the keys in the session
- * store.
- * @param userIds - The users to fetch.
- * @param forceDownload - Always download the keys even if cached.
- *
- * @returns A promise which resolves to a map `userId->deviceId->{@link DeviceInfo}`.
- */
- public downloadKeys(userIds: string[], forceDownload?: boolean): Promise<DeviceInfoMap> {
- return this.deviceList.downloadKeys(userIds, !!forceDownload);
- }
-
- /**
- * Get the stored device keys for a user id
- *
- * @param userId - the user to list keys for.
- *
- * @returns list of devices, or null if we haven't
- * managed to get a list of devices for this user yet.
- */
- public getStoredDevicesForUser(userId: string): Array<DeviceInfo> | null {
- return this.deviceList.getStoredDevicesForUser(userId);
- }
-
- /**
- * Get the stored keys for a single device
- *
- *
- * @returns device, or undefined
- * if we don't know about this device
- */
- public getStoredDevice(userId: string, deviceId: string): DeviceInfo | undefined {
- return this.deviceList.getStoredDevice(userId, deviceId);
- }
-
- /**
- * Save the device list, if necessary
- *
- * @param delay - Time in ms before which the save actually happens.
- * By default, the save is delayed for a short period in order to batch
- * multiple writes, but this behaviour can be disabled by passing 0.
- *
- * @returns true if the data was saved, false if
- * it was not (eg. because no changes were pending). The promise
- * will only resolve once the data is saved, so may take some time
- * to resolve.
- */
- public saveDeviceList(delay: number): Promise<boolean> {
- return this.deviceList.saveIfDirty(delay);
- }
-
- /**
- * Update the blocked/verified state of the given device
- *
- * @param userId - owner of the device
- * @param deviceId - unique identifier for the device or user's
- * cross-signing public key ID.
- *
- * @param verified - whether to mark the device as verified. Null to
- * leave unchanged.
- *
- * @param blocked - whether to mark the device as blocked. Null to
- * leave unchanged.
- *
- * @param known - whether to mark that the user has been made aware of
- * the existence of this device. Null to leave unchanged
- *
- * @param keys - The list of keys that was present
- * during the device verification. This will be double checked with the list
- * of keys the given device has currently.
- *
- * @returns updated DeviceInfo
- */
- public async setDeviceVerification(
- userId: string,
- deviceId: string,
- verified: boolean | null = null,
- blocked: boolean | null = null,
- known: boolean | null = null,
- keys?: Record<string, string>,
- ): Promise<DeviceInfo | CrossSigningInfo> {
- // Check if the 'device' is actually a cross signing key
- // The js-sdk's verification treats cross-signing keys as devices
- // and so uses this method to mark them verified.
- const xsk = this.deviceList.getStoredCrossSigningForUser(userId);
- if (xsk && xsk.getId() === deviceId) {
- if (blocked !== null || known !== null) {
- throw new Error("Cannot set blocked or known for a cross-signing key");
- }
- if (!verified) {
- throw new Error("Cannot set a cross-signing key as unverified");
- }
- const gotKeyId = keys ? Object.values(keys)[0] : null;
- if (keys && (Object.values(keys).length !== 1 || gotKeyId !== xsk.getId())) {
- throw new Error(`Key did not match expected value: expected ${xsk.getId()}, got ${gotKeyId}`);
- }
-
- if (!this.crossSigningInfo.getId() && userId === this.crossSigningInfo.userId) {
- this.storeTrustedSelfKeys(xsk.keys);
- // This will cause our own user trust to change, so emit the event
- this.emit(CryptoEvent.UserTrustStatusChanged, this.userId, this.checkUserTrust(userId));
- }
-
- // Now sign the master key with our user signing key (unless it's ourself)
- if (userId !== this.userId) {
- logger.info("Master key " + xsk.getId() + " for " + userId + " marked verified. Signing...");
- const device = await this.crossSigningInfo.signUser(xsk);
- if (device) {
- const upload = async ({ shouldEmit = false }): Promise<void> => {
- logger.info("Uploading signature for " + userId + "...");
- const response = await this.baseApis.uploadKeySignatures({
- [userId]: {
- [deviceId]: device,
- },
- });
- const { failures } = response || {};
- if (Object.keys(failures || []).length > 0) {
- if (shouldEmit) {
- this.baseApis.emit(
- CryptoEvent.KeySignatureUploadFailure,
- failures,
- "setDeviceVerification",
- upload,
- );
- }
- /* Throwing here causes the process to be cancelled and the other
- * user to be notified */
- throw new KeySignatureUploadError("Key upload failed", { failures });
- }
- };
- await upload({ shouldEmit: true });
-
- // This will emit events when it comes back down the sync
- // (we could do local echo to speed things up)
- }
- return device as any; // TODO types
- } else {
- return xsk;
- }
- }
-
- const devices = this.deviceList.getRawStoredDevicesForUser(userId);
- if (!devices || !devices[deviceId]) {
- throw new Error("Unknown device " + userId + ":" + deviceId);
- }
-
- const dev = devices[deviceId];
- let verificationStatus = dev.verified;
-
- if (verified) {
- if (keys) {
- for (const [keyId, key] of Object.entries(keys)) {
- if (dev.keys[keyId] !== key) {
- throw new Error(`Key did not match expected value: expected ${key}, got ${dev.keys[keyId]}`);
- }
- }
- }
- verificationStatus = DeviceVerification.VERIFIED;
- } else if (verified !== null && verificationStatus == DeviceVerification.VERIFIED) {
- verificationStatus = DeviceVerification.UNVERIFIED;
- }
-
- if (blocked) {
- verificationStatus = DeviceVerification.BLOCKED;
- } else if (blocked !== null && verificationStatus == DeviceVerification.BLOCKED) {
- verificationStatus = DeviceVerification.UNVERIFIED;
- }
-
- let knownStatus = dev.known;
- if (known !== null) {
- knownStatus = known;
- }
-
- if (dev.verified !== verificationStatus || dev.known !== knownStatus) {
- dev.verified = verificationStatus;
- dev.known = knownStatus;
- this.deviceList.storeDevicesForUser(userId, devices);
- this.deviceList.saveIfDirty();
- }
-
- // do cross-signing
- if (verified && userId === this.userId) {
- logger.info("Own device " + deviceId + " marked verified: signing");
-
- // Signing only needed if other device not already signed
- let device: ISignedKey | undefined;
- const deviceTrust = this.checkDeviceTrust(userId, deviceId);
- if (deviceTrust.isCrossSigningVerified()) {
- logger.log(`Own device ${deviceId} already cross-signing verified`);
- } else {
- device = (await this.crossSigningInfo.signDevice(userId, DeviceInfo.fromStorage(dev, deviceId)))!;
- }
-
- if (device) {
- const upload = async ({ shouldEmit = false }): Promise<void> => {
- logger.info("Uploading signature for " + deviceId);
- const response = await this.baseApis.uploadKeySignatures({
- [userId]: {
- [deviceId]: device!,
- },
- });
- const { failures } = response || {};
- if (Object.keys(failures || []).length > 0) {
- if (shouldEmit) {
- this.baseApis.emit(
- CryptoEvent.KeySignatureUploadFailure,
- failures,
- "setDeviceVerification",
- upload, // continuation
- );
- }
- throw new KeySignatureUploadError("Key upload failed", { failures });
- }
- };
- await upload({ shouldEmit: true });
- // XXX: we'll need to wait for the device list to be updated
- }
- }
-
- const deviceObj = DeviceInfo.fromStorage(dev, deviceId);
- this.emit(CryptoEvent.DeviceVerificationChanged, userId, deviceId, deviceObj);
- return deviceObj;
- }
-
- public findVerificationRequestDMInProgress(roomId: string): VerificationRequest | undefined {
- return this.inRoomVerificationRequests.findRequestInProgress(roomId);
- }
-
- public getVerificationRequestsToDeviceInProgress(userId: string): VerificationRequest[] {
- return this.toDeviceVerificationRequests.getRequestsInProgress(userId);
- }
-
- public requestVerificationDM(userId: string, roomId: string): Promise<VerificationRequest> {
- const existingRequest = this.inRoomVerificationRequests.findRequestInProgress(roomId);
- if (existingRequest) {
- return Promise.resolve(existingRequest);
- }
- const channel = new InRoomChannel(this.baseApis, roomId, userId);
- return this.requestVerificationWithChannel(userId, channel, this.inRoomVerificationRequests);
- }
-
- public requestVerification(userId: string, devices?: string[]): Promise<VerificationRequest> {
- if (!devices) {
- devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(userId));
- }
- const existingRequest = this.toDeviceVerificationRequests.findRequestInProgress(userId, devices);
- if (existingRequest) {
- return Promise.resolve(existingRequest);
- }
- const channel = new ToDeviceChannel(this.baseApis, userId, devices, ToDeviceChannel.makeTransactionId());
- return this.requestVerificationWithChannel(userId, channel, this.toDeviceVerificationRequests);
- }
-
- private async requestVerificationWithChannel(
- userId: string,
- channel: IVerificationChannel,
- requestsMap: IRequestsMap,
- ): Promise<VerificationRequest> {
- let request = new VerificationRequest(channel, this.verificationMethods, this.baseApis);
- // if transaction id is already known, add request
- if (channel.transactionId) {
- requestsMap.setRequestByChannel(channel, request);
- }
- await request.sendRequest();
- // don't replace the request created by a racing remote echo
- const racingRequest = requestsMap.getRequestByChannel(channel);
- if (racingRequest) {
- request = racingRequest;
- } else {
- logger.log(
- `Crypto: adding new request to ` + `requestsByTxnId with id ${channel.transactionId} ${channel.roomId}`,
- );
- requestsMap.setRequestByChannel(channel, request);
- }
- return request;
- }
-
- public beginKeyVerification(
- method: string,
- userId: string,
- deviceId: string,
- transactionId: string | null = null,
- ): VerificationBase<any, any> {
- let request: Request | undefined;
- if (transactionId) {
- request = this.toDeviceVerificationRequests.getRequestBySenderAndTxnId(userId, transactionId);
- if (!request) {
- throw new Error(`No request found for user ${userId} with ` + `transactionId ${transactionId}`);
- }
- } else {
- transactionId = ToDeviceChannel.makeTransactionId();
- const channel = new ToDeviceChannel(this.baseApis, userId, [deviceId], transactionId, deviceId);
- request = new VerificationRequest(channel, this.verificationMethods, this.baseApis);
- this.toDeviceVerificationRequests.setRequestBySenderAndTxnId(userId, transactionId, request);
- }
- return request.beginKeyVerification(method, { userId, deviceId });
- }
-
- public async legacyDeviceVerification(
- userId: string,
- deviceId: string,
- method: VerificationMethod,
- ): Promise<VerificationRequest> {
- const transactionId = ToDeviceChannel.makeTransactionId();
- const channel = new ToDeviceChannel(this.baseApis, userId, [deviceId], transactionId, deviceId);
- const request = new VerificationRequest(channel, this.verificationMethods, this.baseApis);
- this.toDeviceVerificationRequests.setRequestBySenderAndTxnId(userId, transactionId, request);
- const verifier = request.beginKeyVerification(method, { userId, deviceId });
- // either reject by an error from verify() while sending .start
- // or resolve when the request receives the
- // local (fake remote) echo for sending the .start event
- await Promise.race([verifier.verify(), request.waitFor((r) => r.started)]);
- return request;
- }
-
- /**
- * Get information on the active olm sessions with a user
- * <p>
- * Returns a map from device id to an object with keys 'deviceIdKey' (the
- * device's curve25519 identity key) and 'sessions' (an array of objects in the
- * same format as that returned by
- * {@link OlmDevice#getSessionInfoForDevice}).
- * <p>
- * This method is provided for debugging purposes.
- *
- * @param userId - id of user to inspect
- */
- public async getOlmSessionsForUser(userId: string): Promise<Record<string, IUserOlmSession>> {
- const devices = this.getStoredDevicesForUser(userId) || [];
- const result: { [deviceId: string]: IUserOlmSession } = {};
- for (const device of devices) {
- const deviceKey = device.getIdentityKey();
- const sessions = await this.olmDevice.getSessionInfoForDevice(deviceKey);
-
- result[device.deviceId] = {
- deviceIdKey: deviceKey,
- sessions: sessions,
- };
- }
- return result;
- }
-
- /**
- * Get the device which sent an event
- *
- * @param event - event to be checked
- */
- public getEventSenderDeviceInfo(event: MatrixEvent): DeviceInfo | null {
- const senderKey = event.getSenderKey();
- const algorithm = event.getWireContent().algorithm;
-
- if (!senderKey || !algorithm) {
- return null;
- }
-
- if (event.isKeySourceUntrusted()) {
- // we got the key for this event from a source that we consider untrusted
- return null;
- }
-
- // senderKey is the Curve25519 identity key of the device which the event
- // was sent from. In the case of Megolm, it's actually the Curve25519
- // identity key of the device which set up the Megolm session.
-
- const device = this.deviceList.getDeviceByIdentityKey(algorithm, senderKey);
-
- if (device === null) {
- // we haven't downloaded the details of this device yet.
- return null;
- }
-
- // so far so good, but now we need to check that the sender of this event
- // hadn't advertised someone else's Curve25519 key as their own. We do that
- // by checking the Ed25519 claimed by the event (or, in the case of megolm,
- // the event which set up the megolm session), to check that it matches the
- // fingerprint of the purported sending device.
- //
- // (see https://github.com/vector-im/vector-web/issues/2215)
-
- const claimedKey = event.getClaimedEd25519Key();
- if (!claimedKey) {
- logger.warn("Event " + event.getId() + " claims no ed25519 key: " + "cannot verify sending device");
- return null;
- }
-
- if (claimedKey !== device.getFingerprint()) {
- logger.warn(
- "Event " +
- event.getId() +
- " claims ed25519 key " +
- claimedKey +
- " but sender device has key " +
- device.getFingerprint(),
- );
- return null;
- }
-
- return device;
- }
-
- /**
- * Get information about the encryption of an event
- *
- * @param event - event to be checked
- *
- * @returns An object with the fields:
- * - encrypted: whether the event is encrypted (if not encrypted, some of the
- * other properties may not be set)
- * - senderKey: the sender's key
- * - algorithm: the algorithm used to encrypt the event
- * - authenticated: whether we can be sure that the owner of the senderKey
- * sent the event
- * - sender: the sender's device information, if available
- * - mismatchedSender: if the event's ed25519 and curve25519 keys don't match
- * (only meaningful if `sender` is set)
- */
- public getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo {
- const ret: Partial<IEncryptedEventInfo> = {};
-
- ret.senderKey = event.getSenderKey() ?? undefined;
- ret.algorithm = event.getWireContent().algorithm;
-
- if (!ret.senderKey || !ret.algorithm) {
- ret.encrypted = false;
- return ret as IEncryptedEventInfo;
- }
- ret.encrypted = true;
-
- if (event.isKeySourceUntrusted()) {
- // we got the key this event from somewhere else
- // TODO: check if we can trust the forwarders.
- ret.authenticated = false;
- } else {
- ret.authenticated = true;
- }
-
- // senderKey is the Curve25519 identity key of the device which the event
- // was sent from. In the case of Megolm, it's actually the Curve25519
- // identity key of the device which set up the Megolm session.
-
- ret.sender = this.deviceList.getDeviceByIdentityKey(ret.algorithm, ret.senderKey) ?? undefined;
-
- // so far so good, but now we need to check that the sender of this event
- // hadn't advertised someone else's Curve25519 key as their own. We do that
- // by checking the Ed25519 claimed by the event (or, in the case of megolm,
- // the event which set up the megolm session), to check that it matches the
- // fingerprint of the purported sending device.
- //
- // (see https://github.com/vector-im/vector-web/issues/2215)
-
- const claimedKey = event.getClaimedEd25519Key();
- if (!claimedKey) {
- logger.warn("Event " + event.getId() + " claims no ed25519 key: " + "cannot verify sending device");
- ret.mismatchedSender = true;
- }
-
- if (ret.sender && claimedKey !== ret.sender.getFingerprint()) {
- logger.warn(
- "Event " +
- event.getId() +
- " claims ed25519 key " +
- claimedKey +
- "but sender device has key " +
- ret.sender.getFingerprint(),
- );
- ret.mismatchedSender = true;
- }
-
- return ret as IEncryptedEventInfo;
- }
-
- /**
- * Forces the current outbound group session to be discarded such
- * that another one will be created next time an event is sent.
- *
- * @param roomId - The ID of the room to discard the session for
- *
- * This should not normally be necessary.
- */
- public forceDiscardSession(roomId: string): Promise<void> {
- const alg = this.roomEncryptors.get(roomId);
- if (alg === undefined) throw new Error("Room not encrypted");
- if (alg.forceDiscardSession === undefined) {
- throw new Error("Room encryption algorithm doesn't support session discarding");
- }
- alg.forceDiscardSession();
- return Promise.resolve();
- }
-
- /**
- * Configure a room to use encryption (ie, save a flag in the cryptoStore).
- *
- * @param roomId - The room ID to enable encryption in.
- *
- * @param config - The encryption config for the room.
- *
- * @param inhibitDeviceQuery - true to suppress device list query for
- * users in the room (for now). In case lazy loading is enabled,
- * the device query is always inhibited as the members are not tracked.
- *
- * @deprecated It is normally incorrect to call this method directly. Encryption
- * is enabled by receiving an `m.room.encryption` event (which we may have sent
- * previously).
- */
- public async setRoomEncryption(
- roomId: string,
- config: IRoomEncryption,
- inhibitDeviceQuery?: boolean,
- ): Promise<void> {
- const room = this.clientStore.getRoom(roomId);
- if (!room) {
- throw new Error(`Unable to enable encryption tracking devices in unknown room ${roomId}`);
- }
- await this.setRoomEncryptionImpl(room, config);
- if (!this.lazyLoadMembers && !inhibitDeviceQuery) {
- this.deviceList.refreshOutdatedDeviceLists();
- }
- }
-
- /**
- * Set up encryption for a room.
- *
- * This is called when an <tt>m.room.encryption</tt> event is received. It saves a flag
- * for the room in the cryptoStore (if it wasn't already set), sets up an "encryptor" for
- * the room, and enables device-list tracking for the room.
- *
- * It does <em>not</em> initiate a device list query for the room. That is normally
- * done once we finish processing the sync, in onSyncCompleted.
- *
- * @param room - The room to enable encryption in.
- * @param config - The encryption config for the room.
- */
- private async setRoomEncryptionImpl(room: Room, config: IRoomEncryption): Promise<void> {
- const roomId = room.roomId;
-
- // ignore crypto events with no algorithm defined
- // This will happen if a crypto event is redacted before we fetch the room state
- // It would otherwise just throw later as an unknown algorithm would, but we may
- // as well catch this here
- if (!config.algorithm) {
- logger.log("Ignoring setRoomEncryption with no algorithm");
- return;
- }
-
- // if state is being replayed from storage, we might already have a configuration
- // for this room as they are persisted as well.
- // We just need to make sure the algorithm is initialized in this case.
- // However, if the new config is different,
- // we should bail out as room encryption can't be changed once set.
- const existingConfig = this.roomList.getRoomEncryption(roomId);
- if (existingConfig) {
- if (JSON.stringify(existingConfig) != JSON.stringify(config)) {
- logger.error("Ignoring m.room.encryption event which requests " + "a change of config in " + roomId);
- return;
- }
- }
- // if we already have encryption in this room, we should ignore this event,
- // as it would reset the encryption algorithm.
- // This is at least expected to be called twice, as sync calls onCryptoEvent
- // for both the timeline and state sections in the /sync response,
- // the encryption event would appear in both.
- // If it's called more than twice though,
- // it signals a bug on client or server.
- const existingAlg = this.roomEncryptors.get(roomId);
- if (existingAlg) {
- return;
- }
-
- // _roomList.getRoomEncryption will not race with _roomList.setRoomEncryption
- // because it first stores in memory. We should await the promise only
- // after all the in-memory state (roomEncryptors and _roomList) has been updated
- // to avoid races when calling this method multiple times. Hence keep a hold of the promise.
- let storeConfigPromise: Promise<void> | null = null;
- if (!existingConfig) {
- storeConfigPromise = this.roomList.setRoomEncryption(roomId, config);
- }
-
- const AlgClass = algorithms.ENCRYPTION_CLASSES.get(config.algorithm);
- if (!AlgClass) {
- throw new Error("Unable to encrypt with " + config.algorithm);
- }
-
- const alg = new AlgClass({
- userId: this.userId,
- deviceId: this.deviceId,
- crypto: this,
- olmDevice: this.olmDevice,
- baseApis: this.baseApis,
- roomId,
- config,
- });
- this.roomEncryptors.set(roomId, alg);
-
- if (storeConfigPromise) {
- await storeConfigPromise;
- }
-
- logger.log(`Enabling encryption in ${roomId}`);
-
- // we don't want to force a download of the full membership list of this room, but as soon as we have that
- // list we can start tracking the device list.
- if (room.membersLoaded()) {
- await this.trackRoomDevicesImpl(room);
- } else {
- // wait for the membership list to be loaded
- const onState = (_state: RoomState): void => {
- room.off(RoomStateEvent.Update, onState);
- if (room.membersLoaded()) {
- this.trackRoomDevicesImpl(room).catch((e) => {
- logger.error(`Error enabling device tracking in ${roomId}`, e);
- });
- }
- };
- room.on(RoomStateEvent.Update, onState);
- }
- }
-
- /**
- * Make sure we are tracking the device lists for all users in this room.
- *
- * @param roomId - The room ID to start tracking devices in.
- * @returns when all devices for the room have been fetched and marked to track
- * @deprecated there's normally no need to call this function: device list tracking
- * will be enabled as soon as we have the full membership list.
- */
- public trackRoomDevices(roomId: string): Promise<void> {
- const room = this.clientStore.getRoom(roomId);
- if (!room) {
- throw new Error(`Unable to start tracking devices in unknown room ${roomId}`);
- }
- return this.trackRoomDevicesImpl(room);
- }
-
- /**
- * Make sure we are tracking the device lists for all users in this room.
- *
- * This is normally called when we are about to send an encrypted event, to make sure
- * we have all the devices in the room; but it is also called when processing an
- * m.room.encryption state event (if lazy-loading is disabled), or when members are
- * loaded (if lazy-loading is enabled), to prepare the device list.
- *
- * @param room - Room to enable device-list tracking in
- */
- private trackRoomDevicesImpl(room: Room): Promise<void> {
- const roomId = room.roomId;
- const trackMembers = async (): Promise<void> => {
- // not an encrypted room
- if (!this.roomEncryptors.has(roomId)) {
- return;
- }
- logger.log(`Starting to track devices for room ${roomId} ...`);
- const members = await room.getEncryptionTargetMembers();
- members.forEach((m) => {
- this.deviceList.startTrackingDeviceList(m.userId);
- });
- };
-
- let promise = this.roomDeviceTrackingState[roomId];
- if (!promise) {
- promise = trackMembers();
- this.roomDeviceTrackingState[roomId] = promise.catch((err) => {
- delete this.roomDeviceTrackingState[roomId];
- throw err;
- });
- }
- return promise;
- }
-
- /**
- * Try to make sure we have established olm sessions for all known devices for
- * the given users.
- *
- * @param users - list of user ids
- * @param force - If true, force a new Olm session to be created. Default false.
- *
- * @returns resolves once the sessions are complete, to
- * an Object mapping from userId to deviceId to
- * {@link OlmSessionResult}
- */
- public ensureOlmSessionsForUsers(
- users: string[],
- force?: boolean,
- ): Promise<Map<string, Map<string, olmlib.IOlmSessionResult>>> {
- // map user Id → DeviceInfo[]
- const devicesByUser: Map<string, DeviceInfo[]> = new Map();
-
- for (const userId of users) {
- const userDevices: DeviceInfo[] = [];
- devicesByUser.set(userId, userDevices);
-
- const devices = this.getStoredDevicesForUser(userId) || [];
- for (const deviceInfo of devices) {
- const key = deviceInfo.getIdentityKey();
- if (key == this.olmDevice.deviceCurve25519Key) {
- // don't bother setting up session to ourself
- continue;
- }
- if (deviceInfo.verified == DeviceVerification.BLOCKED) {
- // don't bother setting up sessions with blocked users
- continue;
- }
-
- userDevices.push(deviceInfo);
- }
- }
-
- return olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser, force);
- }
-
- /**
- * Get a list containing all of the room keys
- *
- * @returns a list of session export objects
- */
- public async exportRoomKeys(): Promise<IMegolmSessionData[]> {
- const exportedSessions: IMegolmSessionData[] = [];
- await this.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
- this.cryptoStore.getAllEndToEndInboundGroupSessions(txn, (s) => {
- if (s === null) return;
-
- const sess = this.olmDevice.exportInboundGroupSession(s.senderKey, s.sessionId, s.sessionData!);
- delete sess.first_known_index;
- sess.algorithm = olmlib.MEGOLM_ALGORITHM;
- exportedSessions.push(sess);
- });
- });
-
- return exportedSessions;
- }
-
- /**
- * Import a list of room keys previously exported by exportRoomKeys
- *
- * @param keys - a list of session export objects
- * @returns a promise which resolves once the keys have been imported
- */
- public importRoomKeys(keys: IMegolmSessionData[], opts: IImportRoomKeysOpts = {}): Promise<void> {
- let successes = 0;
- let failures = 0;
- const total = keys.length;
-
- function updateProgress(): void {
- opts.progressCallback?.({
- stage: "load_keys",
- successes,
- failures,
- total,
- });
- }
-
- return Promise.all(
- keys.map((key) => {
- if (!key.room_id || !key.algorithm) {
- logger.warn("ignoring room key entry with missing fields", key);
- failures++;
- if (opts.progressCallback) {
- updateProgress();
- }
- return null;
- }
-
- const alg = this.getRoomDecryptor(key.room_id, key.algorithm);
- return alg.importRoomKey(key, opts).finally(() => {
- successes++;
- if (opts.progressCallback) {
- updateProgress();
- }
- });
- }),
- ).then();
- }
-
- /**
- * Counts the number of end to end session keys that are waiting to be backed up
- * @returns Promise which resolves to the number of sessions requiring backup
- */
- public countSessionsNeedingBackup(): Promise<number> {
- return this.backupManager.countSessionsNeedingBackup();
- }
-
- /**
- * Perform any background tasks that can be done before a message is ready to
- * send, in order to speed up sending of the message.
- *
- * @param room - the room the event is in
- */
- public prepareToEncrypt(room: Room): void {
- const alg = this.roomEncryptors.get(room.roomId);
- if (alg) {
- alg.prepareToEncrypt(room);
- }
- }
-
- /**
- * Encrypt an event according to the configuration of the room.
- *
- * @param event - event to be sent
- *
- * @param room - destination room.
- *
- * @returns Promise which resolves when the event has been
- * encrypted, or null if nothing was needed
- */
- public async encryptEvent(event: MatrixEvent, room: Room): Promise<void> {
- const roomId = event.getRoomId()!;
-
- const alg = this.roomEncryptors.get(roomId);
- if (!alg) {
- // MatrixClient has already checked that this room should be encrypted,
- // so this is an unexpected situation.
- throw new Error(
- "Room " +
- roomId +
- " was previously configured to use encryption, but is " +
- "no longer. Perhaps the homeserver is hiding the " +
- "configuration event.",
- );
- }
-
- // wait for all the room devices to be loaded
- await this.trackRoomDevicesImpl(room);
-
- let content = event.getContent();
- // If event has an m.relates_to then we need
- // to put this on the wrapping event instead
- const mRelatesTo = content["m.relates_to"];
- if (mRelatesTo) {
- // Clone content here so we don't remove `m.relates_to` from the local-echo
- content = Object.assign({}, content);
- delete content["m.relates_to"];
- }
-
- // Treat element's performance metrics the same as `m.relates_to` (when present)
- const elementPerfMetrics = content["io.element.performance_metrics"];
- if (elementPerfMetrics) {
- content = Object.assign({}, content);
- delete content["io.element.performance_metrics"];
- }
-
- const encryptedContent = (await alg.encryptMessage(room, event.getType(), content)) as IContent;
-
- if (mRelatesTo) {
- encryptedContent["m.relates_to"] = mRelatesTo;
- }
- if (elementPerfMetrics) {
- encryptedContent["io.element.performance_metrics"] = elementPerfMetrics;
- }
-
- event.makeEncrypted(
- "m.room.encrypted",
- encryptedContent,
- this.olmDevice.deviceCurve25519Key!,
- this.olmDevice.deviceEd25519Key!,
- );
- }
-
- /**
- * Decrypt a received event
- *
- *
- * @returns resolves once we have
- * finished decrypting. Rejects with an `algorithms.DecryptionError` if there
- * is a problem decrypting the event.
- */
- public async decryptEvent(event: MatrixEvent): Promise<IEventDecryptionResult> {
- if (event.isRedacted()) {
- // Try to decrypt the redaction event, to support encrypted
- // redaction reasons. If we can't decrypt, just fall back to using
- // the original redacted_because.
- const redactionEvent = new MatrixEvent({
- room_id: event.getRoomId(),
- ...event.getUnsigned().redacted_because,
- });
- let redactedBecause: IEvent = event.getUnsigned().redacted_because!;
- if (redactionEvent.isEncrypted()) {
- try {
- const decryptedEvent = await this.decryptEvent(redactionEvent);
- redactedBecause = decryptedEvent.clearEvent as IEvent;
- } catch (e) {
- logger.warn("Decryption of redaction failed. Falling back to unencrypted event.", e);
- }
- }
-
- return {
- clearEvent: {
- room_id: event.getRoomId(),
- type: "m.room.message",
- content: {},
- unsigned: {
- redacted_because: redactedBecause,
- },
- },
- };
- } else {
- const content = event.getWireContent();
- const alg = this.getRoomDecryptor(event.getRoomId()!, content.algorithm);
- return alg.decryptEvent(event);
- }
- }
-
- /**
- * Handle the notification from /sync or /keys/changes that device lists have
- * been changed.
- *
- * @param syncData - Object containing sync tokens associated with this sync
- * @param syncDeviceLists - device_lists field from /sync, or response from
- * /keys/changes
- */
- public async handleDeviceListChanges(
- syncData: ISyncStateData,
- syncDeviceLists: Required<ISyncResponse>["device_lists"],
- ): Promise<void> {
- // Initial syncs don't have device change lists. We'll either get the complete list
- // of changes for the interval or will have invalidated everything in willProcessSync
- if (!syncData.oldSyncToken) return;
-
- // Here, we're relying on the fact that we only ever save the sync data after
- // sucessfully saving the device list data, so we're guaranteed that the device
- // list store is at least as fresh as the sync token from the sync store, ie.
- // any device changes received in sync tokens prior to the 'next' token here
- // have been processed and are reflected in the current device list.
- // If we didn't make this assumption, we'd have to use the /keys/changes API
- // to get key changes between the sync token in the device list and the 'old'
- // sync token used here to make sure we didn't miss any.
- await this.evalDeviceListChanges(syncDeviceLists);
- }
-
- /**
- * Send a request for some room keys, if we have not already done so
- *
- * @param resend - whether to resend the key request if there is
- * already one
- *
- * @returns a promise that resolves when the key request is queued
- */
- public requestRoomKey(
- requestBody: IRoomKeyRequestBody,
- recipients: IRoomKeyRequestRecipient[],
- resend = false,
- ): Promise<void> {
- return this.outgoingRoomKeyRequestManager
- .queueRoomKeyRequest(requestBody, recipients, resend)
- .then(() => {
- if (this.sendKeyRequestsImmediately) {
- this.outgoingRoomKeyRequestManager.sendQueuedRequests();
- }
- })
- .catch((e) => {
- // this normally means we couldn't talk to the store
- logger.error("Error requesting key for event", e);
- });
- }
-
- /**
- * Cancel any earlier room key request
- *
- * @param requestBody - parameters to match for cancellation
- */
- public cancelRoomKeyRequest(requestBody: IRoomKeyRequestBody): void {
- this.outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody).catch((e) => {
- logger.warn("Error clearing pending room key requests", e);
- });
- }
-
- /**
- * Re-send any outgoing key requests, eg after verification
- * @returns
- */
- public async cancelAndResendAllOutgoingKeyRequests(): Promise<void> {
- await this.outgoingRoomKeyRequestManager.cancelAndResendAllOutgoingRequests();
- }
-
- /**
- * handle an m.room.encryption event
- *
- * @param room - in which the event was received
- * @param event - encryption event to be processed
- */
- public async onCryptoEvent(room: Room, event: MatrixEvent): Promise<void> {
- const content = event.getContent<IRoomEncryption>();
- await this.setRoomEncryptionImpl(room, content);
- }
-
- /**
- * Called before the result of a sync is processed
- *
- * @param syncData - the data from the 'MatrixClient.sync' event
- */
- public async onSyncWillProcess(syncData: ISyncStateData): Promise<void> {
- if (!syncData.oldSyncToken) {
- // If there is no old sync token, we start all our tracking from
- // scratch, so mark everything as untracked. onCryptoEvent will
- // be called for all e2e rooms during the processing of the sync,
- // at which point we'll start tracking all the users of that room.
- logger.log("Initial sync performed - resetting device tracking state");
- this.deviceList.stopTrackingAllDeviceLists();
- // we always track our own device list (for key backups etc)
- this.deviceList.startTrackingDeviceList(this.userId);
- this.roomDeviceTrackingState = {};
- }
-
- this.sendKeyRequestsImmediately = false;
- }
-
- /**
- * handle the completion of a /sync
- *
- * This is called after the processing of each successful /sync response.
- * It is an opportunity to do a batch process on the information received.
- *
- * @param syncData - the data from the 'MatrixClient.sync' event
- */
- public async onSyncCompleted(syncData: OnSyncCompletedData): Promise<void> {
- this.deviceList.setSyncToken(syncData.nextSyncToken ?? null);
- this.deviceList.saveIfDirty();
-
- // we always track our own device list (for key backups etc)
- this.deviceList.startTrackingDeviceList(this.userId);
-
- this.deviceList.refreshOutdatedDeviceLists();
-
- // we don't start uploading one-time keys until we've caught up with
- // to-device messages, to help us avoid throwing away one-time-keys that we
- // are about to receive messages for
- // (https://github.com/vector-im/element-web/issues/2782).
- if (!syncData.catchingUp) {
- this.maybeUploadOneTimeKeys();
- this.processReceivedRoomKeyRequests();
-
- // likewise don't start requesting keys until we've caught up
- // on to_device messages, otherwise we'll request keys that we're
- // just about to get.
- this.outgoingRoomKeyRequestManager.sendQueuedRequests();
-
- // Sync has finished so send key requests straight away.
- this.sendKeyRequestsImmediately = true;
- }
- }
-
- /**
- * Trigger the appropriate invalidations and removes for a given
- * device list
- *
- * @param deviceLists - device_lists field from /sync, or response from
- * /keys/changes
- */
- private async evalDeviceListChanges(deviceLists: Required<ISyncResponse>["device_lists"]): Promise<void> {
- if (Array.isArray(deviceLists?.changed)) {
- deviceLists.changed.forEach((u) => {
- this.deviceList.invalidateUserDeviceList(u);
- });
- }
-
- if (Array.isArray(deviceLists?.left) && deviceLists.left.length) {
- // Check we really don't share any rooms with these users
- // any more: the server isn't required to give us the
- // exact correct set.
- const e2eUserIds = new Set(await this.getTrackedE2eUsers());
-
- deviceLists.left.forEach((u) => {
- if (!e2eUserIds.has(u)) {
- this.deviceList.stopTrackingDeviceList(u);
- }
- });
- }
- }
-
- /**
- * Get a list of all the IDs of users we share an e2e room with
- * for which we are tracking devices already
- *
- * @returns List of user IDs
- */
- private async getTrackedE2eUsers(): Promise<string[]> {
- const e2eUserIds: string[] = [];
- for (const room of this.getTrackedE2eRooms()) {
- const members = await room.getEncryptionTargetMembers();
- for (const member of members) {
- e2eUserIds.push(member.userId);
- }
- }
- return e2eUserIds;
- }
-
- /**
- * Get a list of the e2e-enabled rooms we are members of,
- * and for which we are already tracking the devices
- *
- * @returns
- */
- private getTrackedE2eRooms(): Room[] {
- return this.clientStore.getRooms().filter((room) => {
- // check for rooms with encryption enabled
- const alg = this.roomEncryptors.get(room.roomId);
- if (!alg) {
- return false;
- }
- if (!this.roomDeviceTrackingState[room.roomId]) {
- return false;
- }
-
- // ignore any rooms which we have left
- const myMembership = room.getMyMembership();
- return myMembership === "join" || myMembership === "invite";
- });
- }
-
- /**
- * Encrypts and sends a given object via Olm to-device messages to a given
- * set of devices.
- * @param userDeviceInfoArr - the devices to send to
- * @param payload - fields to include in the encrypted payload
- * @returns Promise which
- * resolves once the message has been encrypted and sent to the given
- * userDeviceMap, and returns the `{ contentMap, deviceInfoByDeviceId }`
- * of the successfully sent messages.
- */
- public async encryptAndSendToDevices(userDeviceInfoArr: IOlmDevice<DeviceInfo>[], payload: object): Promise<void> {
- const toDeviceBatch: ToDeviceBatch = {
- eventType: EventType.RoomMessageEncrypted,
- batch: [],
- };
-
- try {
- await Promise.all(
- userDeviceInfoArr.map(async ({ userId, deviceInfo }) => {
- const deviceId = deviceInfo.deviceId;
- const encryptedContent: IEncryptedContent = {
- algorithm: olmlib.OLM_ALGORITHM,
- sender_key: this.olmDevice.deviceCurve25519Key!,
- ciphertext: {},
- [ToDeviceMessageId]: uuidv4(),
- };
-
- toDeviceBatch.batch.push({
- userId,
- deviceId,
- payload: encryptedContent,
- });
-
- await olmlib.ensureOlmSessionsForDevices(
- this.olmDevice,
- this.baseApis,
- new Map([[userId, [deviceInfo]]]),
- );
- await olmlib.encryptMessageForDevice(
- encryptedContent.ciphertext,
- this.userId,
- this.deviceId,
- this.olmDevice,
- userId,
- deviceInfo,
- payload,
- );
- }),
- );
-
- // prune out any devices that encryptMessageForDevice could not encrypt for,
- // in which case it will have just not added anything to the ciphertext object.
- // There's no point sending messages to devices if we couldn't encrypt to them,
- // since that's effectively a blank message.
- toDeviceBatch.batch = toDeviceBatch.batch.filter((msg) => {
- if (Object.keys(msg.payload.ciphertext).length > 0) {
- return true;
- } else {
- logger.log(`No ciphertext for device ${msg.userId}:${msg.deviceId}: pruning`);
- return false;
- }
- });
-
- try {
- await this.baseApis.queueToDevice(toDeviceBatch);
- } catch (e) {
- logger.error("sendToDevice failed", e);
- throw e;
- }
- } catch (e) {
- logger.error("encryptAndSendToDevices promises failed", e);
- throw e;
- }
- }
-
- private onMembership = (event: MatrixEvent, member: RoomMember, oldMembership?: string): void => {
- try {
- this.onRoomMembership(event, member, oldMembership);
- } catch (e) {
- logger.error("Error handling membership change:", e);
- }
- };
-
- public async preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]> {
- // all we do here is filter out encrypted to-device messages with the wrong algorithm. Decryption
- // happens later in decryptEvent, via the EventMapper
- return events.filter((toDevice) => {
- if (
- toDevice.type === EventType.RoomMessageEncrypted &&
- !["m.olm.v1.curve25519-aes-sha2"].includes(toDevice.content?.algorithm)
- ) {
- logger.log("Ignoring invalid encrypted to-device event from " + toDevice.sender);
- return false;
- }
- return true;
- });
- }
-
- public preprocessOneTimeKeyCounts(oneTimeKeysCounts: Map<string, number>): Promise<void> {
- const currentCount = oneTimeKeysCounts.get("signed_curve25519") || 0;
- this.updateOneTimeKeyCount(currentCount);
- return Promise.resolve();
- }
-
- public preprocessUnusedFallbackKeys(unusedFallbackKeys: Set<string>): Promise<void> {
- this.setNeedsNewFallback(!unusedFallbackKeys.has("signed_curve25519"));
- return Promise.resolve();
- }
-
- private onToDeviceEvent = (event: MatrixEvent): void => {
- try {
- logger.log(
- `received to-device ${event.getType()} from: ` +
- `${event.getSender()} id: ${event.getContent()[ToDeviceMessageId]}`,
- );
-
- if (event.getType() == "m.room_key" || event.getType() == "m.forwarded_room_key") {
- this.onRoomKeyEvent(event);
- } else if (event.getType() == "m.room_key_request") {
- this.onRoomKeyRequestEvent(event);
- } else if (event.getType() === "m.secret.request") {
- this.secretStorage.onRequestReceived(event);
- } else if (event.getType() === "m.secret.send") {
- this.secretStorage.onSecretReceived(event);
- } else if (event.getType() === "m.room_key.withheld") {
- this.onRoomKeyWithheldEvent(event);
- } else if (event.getContent().transaction_id) {
- this.onKeyVerificationMessage(event);
- } else if (event.getContent().msgtype === "m.bad.encrypted") {
- this.onToDeviceBadEncrypted(event);
- } else if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) {
- if (!event.isBeingDecrypted()) {
- event.attemptDecryption(this);
- }
- // once the event has been decrypted, try again
- event.once(MatrixEventEvent.Decrypted, (ev) => {
- this.onToDeviceEvent(ev);
- });
- }
- } catch (e) {
- logger.error("Error handling toDeviceEvent:", e);
- }
- };
-
- /**
- * Handle a key event
- *
- * @internal
- * @param event - key event
- */
- private onRoomKeyEvent(event: MatrixEvent): void {
- const content = event.getContent();
-
- if (!content.room_id || !content.algorithm) {
- logger.error("key event is missing fields");
- return;
- }
-
- if (!this.backupManager.checkedForBackup) {
- // don't bother awaiting on this - the important thing is that we retry if we
- // haven't managed to check before
- this.backupManager.checkAndStart();
- }
-
- const alg = this.getRoomDecryptor(content.room_id, content.algorithm);
- alg.onRoomKeyEvent(event);
- }
-
- /**
- * Handle a key withheld event
- *
- * @internal
- * @param event - key withheld event
- */
- private onRoomKeyWithheldEvent(event: MatrixEvent): void {
- const content = event.getContent();
-
- if (
- (content.code !== "m.no_olm" && (!content.room_id || !content.session_id)) ||
- !content.algorithm ||
- !content.sender_key
- ) {
- logger.error("key withheld event is missing fields");
- return;
- }
-
- logger.info(
- `Got room key withheld event from ${event.getSender()} ` +
- `for ${content.algorithm} session ${content.sender_key}|${content.session_id} ` +
- `in room ${content.room_id} with code ${content.code} (${content.reason})`,
- );
-
- const alg = this.getRoomDecryptor(content.room_id, content.algorithm);
- if (alg.onRoomKeyWithheldEvent) {
- alg.onRoomKeyWithheldEvent(event);
- }
- if (!content.room_id) {
- // retry decryption for all events sent by the sender_key. This will
- // update the events to show a message indicating that the olm session was
- // wedged.
- const roomDecryptors = this.getRoomDecryptors(content.algorithm);
- for (const decryptor of roomDecryptors) {
- decryptor.retryDecryptionFromSender(content.sender_key);
- }
- }
- }
-
- /**
- * Handle a general key verification event.
- *
- * @internal
- * @param event - verification start event
- */
- private onKeyVerificationMessage(event: MatrixEvent): void {
- if (!ToDeviceChannel.validateEvent(event, this.baseApis)) {
- return;
- }
- const createRequest = (event: MatrixEvent): VerificationRequest | undefined => {
- if (!ToDeviceChannel.canCreateRequest(ToDeviceChannel.getEventType(event))) {
- return;
- }
- const content = event.getContent();
- const deviceId = content && content.from_device;
- if (!deviceId) {
- return;
- }
- const userId = event.getSender()!;
- const channel = new ToDeviceChannel(this.baseApis, userId, [deviceId]);
- return new VerificationRequest(channel, this.verificationMethods, this.baseApis);
- };
- this.handleVerificationEvent(event, this.toDeviceVerificationRequests, createRequest);
- }
-
- /**
- * Handle key verification requests sent as timeline events
- *
- * @internal
- * @param event - the timeline event
- * @param room - not used
- * @param atStart - not used
- * @param removed - not used
- * @param whether - this is a live event
- */
- private onTimelineEvent = (
- event: MatrixEvent,
- room: Room,
- atStart: boolean,
- removed: boolean,
- { liveEvent = true } = {},
- ): void => {
- if (!InRoomChannel.validateEvent(event, this.baseApis)) {
- return;
- }
- const createRequest = (event: MatrixEvent): VerificationRequest => {
- const channel = new InRoomChannel(this.baseApis, event.getRoomId()!);
- return new VerificationRequest(channel, this.verificationMethods, this.baseApis);
- };
- this.handleVerificationEvent(event, this.inRoomVerificationRequests, createRequest, liveEvent);
- };
-
- private async handleVerificationEvent(
- event: MatrixEvent,
- requestsMap: IRequestsMap,
- createRequest: (event: MatrixEvent) => VerificationRequest | undefined,
- isLiveEvent = true,
- ): Promise<void> {
- // Wait for event to get its final ID with pendingEventOrdering: "chronological", since DM channels depend on it.
- if (event.isSending() && event.status != EventStatus.SENT) {
- let eventIdListener: () => void;
- let statusListener: () => void;
- try {
- await new Promise<void>((resolve, reject) => {
- eventIdListener = resolve;
- statusListener = (): void => {
- if (event.status == EventStatus.CANCELLED) {
- reject(new Error("Event status set to CANCELLED."));
- }
- };
- event.once(MatrixEventEvent.LocalEventIdReplaced, eventIdListener);
- event.on(MatrixEventEvent.Status, statusListener);
- });
- } catch (err) {
- logger.error("error while waiting for the verification event to be sent: ", err);
- return;
- } finally {
- event.removeListener(MatrixEventEvent.LocalEventIdReplaced, eventIdListener!);
- event.removeListener(MatrixEventEvent.Status, statusListener!);
- }
- }
- let request: VerificationRequest | undefined = requestsMap.getRequest(event);
- let isNewRequest = false;
- if (!request) {
- request = createRequest(event);
- // a request could not be made from this event, so ignore event
- if (!request) {
- logger.log(
- `Crypto: could not find VerificationRequest for ` +
- `${event.getType()}, and could not create one, so ignoring.`,
- );
- return;
- }
- isNewRequest = true;
- requestsMap.setRequest(event, request);
- }
- event.setVerificationRequest(request);
- try {
- await request.channel.handleEvent(event, request, isLiveEvent);
- } catch (err) {
- logger.error("error while handling verification event", err);
- }
- const shouldEmit =
- isNewRequest &&
- !request.initiatedByMe &&
- !request.invalid && // check it has enough events to pass the UNSENT stage
- !request.observeOnly;
- if (shouldEmit) {
- this.baseApis.emit(CryptoEvent.VerificationRequest, request);
- }
- }
-
- /**
- * Handle a toDevice event that couldn't be decrypted
- *
- * @internal
- * @param event - undecryptable event
- */
- private async onToDeviceBadEncrypted(event: MatrixEvent): Promise<void> {
- const content = event.getWireContent();
- const sender = event.getSender();
- const algorithm = content.algorithm;
- const deviceKey = content.sender_key;
-
- this.baseApis.emit(ClientEvent.UndecryptableToDeviceEvent, event);
-
- // retry decryption for all events sent by the sender_key. This will
- // update the events to show a message indicating that the olm session was
- // wedged.
- const retryDecryption = (): void => {
- const roomDecryptors = this.getRoomDecryptors(olmlib.MEGOLM_ALGORITHM);
- for (const decryptor of roomDecryptors) {
- decryptor.retryDecryptionFromSender(deviceKey);
- }
- };
-
- if (sender === undefined || deviceKey === undefined || deviceKey === undefined) {
- return;
- }
-
- // check when we last forced a new session with this device: if we've already done so
- // recently, don't do it again.
- const lastNewSessionDevices = this.lastNewSessionForced.getOrCreate(sender);
- const lastNewSessionForced = lastNewSessionDevices.getOrCreate(deviceKey);
- if (lastNewSessionForced + MIN_FORCE_SESSION_INTERVAL_MS > Date.now()) {
- logger.debug(
- "New session already forced with device " +
- sender +
- ":" +
- deviceKey +
- " at " +
- lastNewSessionForced +
- ": not forcing another",
- );
- await this.olmDevice.recordSessionProblem(deviceKey, "wedged", true);
- retryDecryption();
- return;
- }
-
- // establish a new olm session with this device since we're failing to decrypt messages
- // on a current session.
- // Note that an undecryptable message from another device could easily be spoofed -
- // is there anything we can do to mitigate this?
- let device = this.deviceList.getDeviceByIdentityKey(algorithm, deviceKey);
- if (!device) {
- // if we don't know about the device, fetch the user's devices again
- // and retry before giving up
- await this.downloadKeys([sender], false);
- device = this.deviceList.getDeviceByIdentityKey(algorithm, deviceKey);
- if (!device) {
- logger.info("Couldn't find device for identity key " + deviceKey + ": not re-establishing session");
- await this.olmDevice.recordSessionProblem(deviceKey, "wedged", false);
- retryDecryption();
- return;
- }
- }
- const devicesByUser = new Map([[sender, [device]]]);
- await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser, true);
-
- lastNewSessionDevices.set(deviceKey, Date.now());
-
- // Now send a blank message on that session so the other side knows about it.
- // (The keyshare request is sent in the clear so that won't do)
- // We send this first such that, as long as the toDevice messages arrive in the
- // same order we sent them, the other end will get this first, set up the new session,
- // then get the keyshare request and send the key over this new session (because it
- // is the session it has most recently received a message on).
- const encryptedContent: IEncryptedContent = {
- algorithm: olmlib.OLM_ALGORITHM,
- sender_key: this.olmDevice.deviceCurve25519Key!,
- ciphertext: {},
- [ToDeviceMessageId]: uuidv4(),
- };
- await olmlib.encryptMessageForDevice(
- encryptedContent.ciphertext,
- this.userId,
- this.deviceId,
- this.olmDevice,
- sender,
- device,
- { type: "m.dummy" },
- );
-
- await this.olmDevice.recordSessionProblem(deviceKey, "wedged", true);
- retryDecryption();
-
- await this.baseApis.sendToDevice(
- "m.room.encrypted",
- new Map([[sender, new Map([[device.deviceId, encryptedContent]])]]),
- );
-
- // Most of the time this probably won't be necessary since we'll have queued up a key request when
- // we failed to decrypt the message and will be waiting a bit for the key to arrive before sending
- // it. This won't always be the case though so we need to re-send any that have already been sent
- // to avoid races.
- const requestsToResend = await this.outgoingRoomKeyRequestManager.getOutgoingSentRoomKeyRequest(
- sender,
- device.deviceId,
- );
- for (const keyReq of requestsToResend) {
- this.requestRoomKey(keyReq.requestBody, keyReq.recipients, true);
- }
- }
-
- /**
- * Handle a change in the membership state of a member of a room
- *
- * @internal
- * @param event - event causing the change
- * @param member - user whose membership changed
- * @param oldMembership - previous membership
- */
- private onRoomMembership(event: MatrixEvent, member: RoomMember, oldMembership?: string): void {
- // this event handler is registered on the *client* (as opposed to the room
- // member itself), which means it is only called on changes to the *live*
- // membership state (ie, it is not called when we back-paginate, nor when
- // we load the state in the initialsync).
- //
- // Further, it is automatically registered and called when new members
- // arrive in the room.
-
- const roomId = member.roomId;
-
- const alg = this.roomEncryptors.get(roomId);
- if (!alg) {
- // not encrypting in this room
- return;
- }
- // only mark users in this room as tracked if we already started tracking in this room
- // this way we don't start device queries after sync on behalf of this room which we won't use
- // the result of anyway, as we'll need to do a query again once all the members are fetched
- // by calling _trackRoomDevices
- if (roomId in this.roomDeviceTrackingState) {
- if (member.membership == "join") {
- logger.log("Join event for " + member.userId + " in " + roomId);
- // make sure we are tracking the deviceList for this user
- this.deviceList.startTrackingDeviceList(member.userId);
- } else if (
- member.membership == "invite" &&
- this.clientStore.getRoom(roomId)?.shouldEncryptForInvitedMembers()
- ) {
- logger.log("Invite event for " + member.userId + " in " + roomId);
- this.deviceList.startTrackingDeviceList(member.userId);
- }
- }
-
- alg.onRoomMembership(event, member, oldMembership);
- }
-
- /**
- * Called when we get an m.room_key_request event.
- *
- * @internal
- * @param event - key request event
- */
- private onRoomKeyRequestEvent(event: MatrixEvent): void {
- const content = event.getContent();
- if (content.action === "request") {
- // Queue it up for now, because they tend to arrive before the room state
- // events at initial sync, and we want to see if we know anything about the
- // room before passing them on to the app.
- const req = new IncomingRoomKeyRequest(event);
- this.receivedRoomKeyRequests.push(req);
- } else if (content.action === "request_cancellation") {
- const req = new IncomingRoomKeyRequestCancellation(event);
- this.receivedRoomKeyRequestCancellations.push(req);
- }
- }
-
- /**
- * Process any m.room_key_request events which were queued up during the
- * current sync.
- *
- * @internal
- */
- private async processReceivedRoomKeyRequests(): Promise<void> {
- if (this.processingRoomKeyRequests) {
- // we're still processing last time's requests; keep queuing new ones
- // up for now.
- return;
- }
- this.processingRoomKeyRequests = true;
-
- try {
- // we need to grab and clear the queues in the synchronous bit of this method,
- // so that we don't end up racing with the next /sync.
- const requests = this.receivedRoomKeyRequests;
- this.receivedRoomKeyRequests = [];
- const cancellations = this.receivedRoomKeyRequestCancellations;
- this.receivedRoomKeyRequestCancellations = [];
-
- // Process all of the requests, *then* all of the cancellations.
- //
- // This makes sure that if we get a request and its cancellation in the
- // same /sync result, then we process the request before the
- // cancellation (and end up with a cancelled request), rather than the
- // cancellation before the request (and end up with an outstanding
- // request which should have been cancelled.)
- await Promise.all(requests.map((req) => this.processReceivedRoomKeyRequest(req)));
- await Promise.all(
- cancellations.map((cancellation) => this.processReceivedRoomKeyRequestCancellation(cancellation)),
- );
- } catch (e) {
- logger.error(`Error processing room key requsts: ${e}`);
- } finally {
- this.processingRoomKeyRequests = false;
- }
- }
-
- /**
- * Helper for processReceivedRoomKeyRequests
- *
- */
- private async processReceivedRoomKeyRequest(req: IncomingRoomKeyRequest): Promise<void> {
- const userId = req.userId;
- const deviceId = req.deviceId;
-
- const body = req.requestBody;
- const roomId = body.room_id;
- const alg = body.algorithm;
-
- logger.log(
- `m.room_key_request from ${userId}:${deviceId}` +
- ` for ${roomId} / ${body.session_id} (id ${req.requestId})`,
- );
-
- if (userId !== this.userId) {
- if (!this.roomEncryptors.get(roomId)) {
- logger.debug(`room key request for unencrypted room ${roomId}`);
- return;
- }
- const encryptor = this.roomEncryptors.get(roomId)!;
- const device = this.deviceList.getStoredDevice(userId, deviceId);
- if (!device) {
- logger.debug(`Ignoring keyshare for unknown device ${userId}:${deviceId}`);
- return;
- }
-
- try {
- await encryptor.reshareKeyWithDevice!(body.sender_key, body.session_id, userId, device);
- } catch (e) {
- logger.warn(
- "Failed to re-share keys for session " +
- body.session_id +
- " with device " +
- userId +
- ":" +
- device.deviceId,
- e,
- );
- }
- return;
- }
-
- if (deviceId === this.deviceId) {
- // We'll always get these because we send room key requests to
- // '*' (ie. 'all devices') which includes the sending device,
- // so ignore requests from ourself because apart from it being
- // very silly, it won't work because an Olm session cannot send
- // messages to itself.
- // The log here is probably superfluous since we know this will
- // always happen, but let's log anyway for now just in case it
- // causes issues.
- logger.log("Ignoring room key request from ourselves");
- return;
- }
-
- // todo: should we queue up requests we don't yet have keys for,
- // in case they turn up later?
-
- // if we don't have a decryptor for this room/alg, we don't have
- // the keys for the requested events, and can drop the requests.
- if (!this.roomDecryptors.has(roomId)) {
- logger.log(`room key request for unencrypted room ${roomId}`);
- return;
- }
-
- const decryptor = this.roomDecryptors.get(roomId)!.get(alg);
- if (!decryptor) {
- logger.log(`room key request for unknown alg ${alg} in room ${roomId}`);
- return;
- }
-
- if (!(await decryptor.hasKeysForKeyRequest(req))) {
- logger.log(`room key request for unknown session ${roomId} / ` + body.session_id);
- return;
- }
-
- req.share = (): void => {
- decryptor.shareKeysWithDevice(req);
- };
-
- // if the device is verified already, share the keys
- if (this.checkDeviceTrust(userId, deviceId).isVerified()) {
- logger.log("device is already verified: sharing keys");
- req.share();
- return;
- }
-
- this.emit(CryptoEvent.RoomKeyRequest, req);
- }
-
- /**
- * Helper for processReceivedRoomKeyRequests
- *
- */
- private async processReceivedRoomKeyRequestCancellation(
- cancellation: IncomingRoomKeyRequestCancellation,
- ): Promise<void> {
- logger.log(
- `m.room_key_request cancellation for ${cancellation.userId}:` +
- `${cancellation.deviceId} (id ${cancellation.requestId})`,
- );
-
- // we should probably only notify the app of cancellations we told it
- // about, but we don't currently have a record of that, so we just pass
- // everything through.
- this.emit(CryptoEvent.RoomKeyRequestCancellation, cancellation);
- }
-
- /**
- * Get a decryptor for a given room and algorithm.
- *
- * If we already have a decryptor for the given room and algorithm, return
- * it. Otherwise try to instantiate it.
- *
- * @internal
- *
- * @param roomId - room id for decryptor. If undefined, a temporary
- * decryptor is instantiated.
- *
- * @param algorithm - crypto algorithm
- *
- * @throws {@link DecryptionError} if the algorithm is unknown
- */
- public getRoomDecryptor(roomId: string | null, algorithm: string): DecryptionAlgorithm {
- let decryptors: Map<string, DecryptionAlgorithm> | undefined;
- let alg: DecryptionAlgorithm | undefined;
-
- if (roomId) {
- decryptors = this.roomDecryptors.get(roomId);
- if (!decryptors) {
- decryptors = new Map<string, DecryptionAlgorithm>();
- this.roomDecryptors.set(roomId, decryptors);
- }
-
- alg = decryptors.get(algorithm);
- if (alg) {
- return alg;
- }
- }
-
- const AlgClass = algorithms.DECRYPTION_CLASSES.get(algorithm);
- if (!AlgClass) {
- throw new algorithms.DecryptionError(
- "UNKNOWN_ENCRYPTION_ALGORITHM",
- 'Unknown encryption algorithm "' + algorithm + '".',
- );
- }
- alg = new AlgClass({
- userId: this.userId,
- crypto: this,
- olmDevice: this.olmDevice,
- baseApis: this.baseApis,
- roomId: roomId ?? undefined,
- });
-
- if (decryptors) {
- decryptors.set(algorithm, alg);
- }
- return alg;
- }
-
- /**
- * Get all the room decryptors for a given encryption algorithm.
- *
- * @param algorithm - The encryption algorithm
- *
- * @returns An array of room decryptors
- */
- private getRoomDecryptors(algorithm: string): DecryptionAlgorithm[] {
- const decryptors: DecryptionAlgorithm[] = [];
- for (const d of this.roomDecryptors.values()) {
- if (d.has(algorithm)) {
- decryptors.push(d.get(algorithm)!);
- }
- }
- return decryptors;
- }
-
- /**
- * sign the given object with our ed25519 key
- *
- * @param obj - Object to which we will add a 'signatures' property
- */
- public async signObject<T extends ISignableObject & object>(obj: T): Promise<void> {
- const sigs = new Map(Object.entries(obj.signatures || {}));
- const unsigned = obj.unsigned;
-
- delete obj.signatures;
- delete obj.unsigned;
-
- const userSignatures = sigs.get(this.userId) || {};
- sigs.set(this.userId, userSignatures);
- userSignatures["ed25519:" + this.deviceId] = await this.olmDevice.sign(anotherjson.stringify(obj));
- obj.signatures = recursiveMapToObject(sigs);
- if (unsigned !== undefined) obj.unsigned = unsigned;
- }
-}
-
-/**
- * Fix up the backup key, that may be in the wrong format due to a bug in a
- * migration step. Some backup keys were stored as a comma-separated list of
- * integers, rather than a base64-encoded byte array. If this function is
- * passed a string that looks like a list of integers rather than a base64
- * string, it will attempt to convert it to the right format.
- *
- * @param key - the key to check
- * @returns If the key is in the wrong format, then the fixed
- * key will be returned. Otherwise null will be returned.
- *
- */
-export function fixBackupKey(key?: string): string | null {
- if (typeof key !== "string" || key.indexOf(",") < 0) {
- return null;
- }
- const fixedKey = Uint8Array.from(key.split(","), (x) => parseInt(x));
- return olmlib.encodeBase64(fixedKey);
-}
-
-/**
- * Represents a received m.room_key_request event
- */
-export class IncomingRoomKeyRequest {
- /** user requesting the key */
- public readonly userId: string;
- /** device requesting the key */
- public readonly deviceId: string;
- /** unique id for the request */
- public readonly requestId: string;
- public readonly requestBody: IRoomKeyRequestBody;
- /**
- * callback which, when called, will ask
- * the relevant crypto algorithm implementation to share the keys for
- * this request.
- */
- public share: () => void;
-
- public constructor(event: MatrixEvent) {
- const content = event.getContent();
-
- this.userId = event.getSender()!;
- this.deviceId = content.requesting_device_id;
- this.requestId = content.request_id;
- this.requestBody = content.body || {};
- this.share = (): void => {
- throw new Error("don't know how to share keys for this request yet");
- };
- }
-}
-
-/**
- * Represents a received m.room_key_request cancellation
- */
-class IncomingRoomKeyRequestCancellation {
- /** user requesting the cancellation */
- public readonly userId: string;
- /** device requesting the cancellation */
- public readonly deviceId: string;
- /** unique id for the request to be cancelled */
- public readonly requestId: string;
-
- public constructor(event: MatrixEvent) {
- const content = event.getContent();
-
- this.userId = event.getSender()!;
- this.deviceId = content.requesting_device_id;
- this.requestId = content.request_id;
- }
-}
-
-// a number of types are re-exported for backwards compatibility, in case any applications are referencing it.
-export type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/key_passphrase.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/key_passphrase.ts
deleted file mode 100644
index f6fe7b6..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/key_passphrase.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
-Copyright 2018 - 2021 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 { randomString } from "../randomstring";
-import { subtleCrypto, TextEncoder } from "./crypto";
-
-const DEFAULT_ITERATIONS = 500000;
-
-const DEFAULT_BITSIZE = 256;
-
-/* eslint-disable camelcase */
-interface IAuthData {
- private_key_salt?: string;
- private_key_iterations?: number;
- private_key_bits?: number;
-}
-/* eslint-enable camelcase */
-
-interface IKey {
- key: Uint8Array;
- salt: string;
- iterations: number;
-}
-
-export function keyFromAuthData(authData: IAuthData, password: string): Promise<Uint8Array> {
- if (!global.Olm) {
- throw new Error("Olm is not available");
- }
-
- if (!authData.private_key_salt || !authData.private_key_iterations) {
- throw new Error("Salt and/or iterations not found: " + "this backup cannot be restored with a passphrase");
- }
-
- return deriveKey(
- password,
- authData.private_key_salt,
- authData.private_key_iterations,
- authData.private_key_bits || DEFAULT_BITSIZE,
- );
-}
-
-export async function keyFromPassphrase(password: string): Promise<IKey> {
- if (!global.Olm) {
- throw new Error("Olm is not available");
- }
-
- const salt = randomString(32);
-
- const key = await deriveKey(password, salt, DEFAULT_ITERATIONS, DEFAULT_BITSIZE);
-
- return { key, salt, iterations: DEFAULT_ITERATIONS };
-}
-
-export async function deriveKey(
- password: string,
- salt: string,
- iterations: number,
- numBits = DEFAULT_BITSIZE,
-): Promise<Uint8Array> {
- if (!subtleCrypto || !TextEncoder) {
- throw new Error("Password-based backup is not available on this platform");
- }
-
- const key = await subtleCrypto.importKey("raw", new TextEncoder().encode(password), { name: "PBKDF2" }, false, [
- "deriveBits",
- ]);
-
- const keybits = await subtleCrypto.deriveBits(
- {
- name: "PBKDF2",
- salt: new TextEncoder().encode(salt),
- iterations: iterations,
- hash: "SHA-512",
- },
- key,
- numBits,
- );
-
- return new Uint8Array(keybits);
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/keybackup.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/keybackup.ts
deleted file mode 100644
index 67e213c..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/keybackup.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
-Copyright 2021 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 { ISigned } from "../@types/signed";
-import { IEncryptedPayload } from "./aes";
-
-export interface Curve25519SessionData {
- ciphertext: string;
- ephemeral: string;
- mac: string;
-}
-
-/* eslint-disable camelcase */
-export interface IKeyBackupSession<T = Curve25519SessionData | IEncryptedPayload> {
- first_message_index: number;
- forwarded_count: number;
- is_verified: boolean;
- session_data: T;
-}
-
-export interface IKeyBackupRoomSessions {
- [sessionId: string]: IKeyBackupSession;
-}
-
-export interface ICurve25519AuthData {
- public_key: string;
- private_key_salt?: string;
- private_key_iterations?: number;
- private_key_bits?: number;
-}
-
-export interface IAes256AuthData {
- iv: string;
- mac: string;
- private_key_salt?: string;
- private_key_iterations?: number;
-}
-
-export interface IKeyBackupInfo {
- algorithm: string;
- auth_data: ISigned & (ICurve25519AuthData | IAes256AuthData);
- count?: number;
- etag?: string;
- version?: string; // number contained within
-}
-/* eslint-enable camelcase */
-
-export interface IKeyBackupPrepareOpts {
- /**
- * Whether to use Secure Secret Storage to store the key encrypting key backups.
- * Optional, defaults to false.
- */
- secureSecretStorage: boolean;
-}
-
-export interface IKeyBackupRestoreResult {
- total: number;
- imported: number;
-}
-
-export interface IKeyBackupRestoreOpts {
- cacheCompleteCallback?: () => void;
- progressCallback?: (progress: { stage: string }) => void;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/olmlib.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/olmlib.ts
deleted file mode 100644
index c37b7f0..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/olmlib.ts
+++ /dev/null
@@ -1,566 +0,0 @@
-/*
-Copyright 2016 - 2021 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.
-*/
-
-/**
- * Utilities common to olm encryption algorithms
- */
-
-import anotherjson from "another-json";
-
-import type { PkSigning } from "@matrix-org/olm";
-import type { IOneTimeKey } from "../@types/crypto";
-import { OlmDevice } from "./OlmDevice";
-import { DeviceInfo } from "./deviceinfo";
-import { logger } from "../logger";
-import { IClaimOTKsResult, MatrixClient } from "../client";
-import { ISignatures } from "../@types/signed";
-import { MatrixEvent } from "../models/event";
-import { EventType } from "../@types/event";
-import { IMessage } from "./algorithms/olm";
-import { MapWithDefault } from "../utils";
-
-enum Algorithm {
- Olm = "m.olm.v1.curve25519-aes-sha2",
- Megolm = "m.megolm.v1.aes-sha2",
- MegolmBackup = "m.megolm_backup.v1.curve25519-aes-sha2",
-}
-
-/**
- * matrix algorithm tag for olm
- */
-export const OLM_ALGORITHM = Algorithm.Olm;
-
-/**
- * matrix algorithm tag for megolm
- */
-export const MEGOLM_ALGORITHM = Algorithm.Megolm;
-
-/**
- * matrix algorithm tag for megolm backups
- */
-export const MEGOLM_BACKUP_ALGORITHM = Algorithm.MegolmBackup;
-
-export interface IOlmSessionResult {
- /** device info */
- device: DeviceInfo;
- /** base64 olm session id; null if no session could be established */
- sessionId: string | null;
-}
-
-/**
- * Encrypt an event payload for an Olm device
- *
- * @param resultsObject - The `ciphertext` property
- * of the m.room.encrypted event to which to add our result
- *
- * @param olmDevice - olm.js wrapper
- * @param payloadFields - fields to include in the encrypted payload
- *
- * Returns a promise which resolves (to undefined) when the payload
- * has been encrypted into `resultsObject`
- */
-export async function encryptMessageForDevice(
- resultsObject: Record<string, IMessage>,
- ourUserId: string,
- ourDeviceId: string | undefined,
- olmDevice: OlmDevice,
- recipientUserId: string,
- recipientDevice: DeviceInfo,
- payloadFields: Record<string, any>,
-): Promise<void> {
- const deviceKey = recipientDevice.getIdentityKey();
- const sessionId = await olmDevice.getSessionIdForDevice(deviceKey);
- if (sessionId === null) {
- // If we don't have a session for a device then
- // we can't encrypt a message for it.
- logger.log(
- `[olmlib.encryptMessageForDevice] Unable to find Olm session for device ` +
- `${recipientUserId}:${recipientDevice.deviceId}`,
- );
- return;
- }
-
- logger.log(
- `[olmlib.encryptMessageForDevice] Using Olm session ${sessionId} for device ` +
- `${recipientUserId}:${recipientDevice.deviceId}`,
- );
-
- const payload = {
- sender: ourUserId,
- // TODO this appears to no longer be used whatsoever
- sender_device: ourDeviceId,
-
- // Include the Ed25519 key so that the recipient knows what
- // device this message came from.
- // We don't need to include the curve25519 key since the
- // recipient will already know this from the olm headers.
- // When combined with the device keys retrieved from the
- // homeserver signed by the ed25519 key this proves that
- // the curve25519 key and the ed25519 key are owned by
- // the same device.
- keys: {
- ed25519: olmDevice.deviceEd25519Key,
- },
-
- // include the recipient device details in the payload,
- // to avoid unknown key attacks, per
- // https://github.com/vector-im/vector-web/issues/2483
- recipient: recipientUserId,
- recipient_keys: {
- ed25519: recipientDevice.getFingerprint(),
- },
- ...payloadFields,
- };
-
- // TODO: technically, a bunch of that stuff only needs to be included for
- // pre-key messages: after that, both sides know exactly which devices are
- // involved in the session. If we're looking to reduce data transfer in the
- // future, we could elide them for subsequent messages.
-
- resultsObject[deviceKey] = await olmDevice.encryptMessage(deviceKey, sessionId, JSON.stringify(payload));
-}
-
-interface IExistingOlmSession {
- device: DeviceInfo;
- sessionId: string | null;
-}
-
-/**
- * Get the existing olm sessions for the given devices, and the devices that
- * don't have olm sessions.
- *
- *
- *
- * @param devicesByUser - map from userid to list of devices to ensure sessions for
- *
- * @returns resolves to an array. The first element of the array is a
- * a map of user IDs to arrays of deviceInfo, representing the devices that
- * don't have established olm sessions. The second element of the array is
- * a map from userId to deviceId to {@link OlmSessionResult}
- */
-export async function getExistingOlmSessions(
- olmDevice: OlmDevice,
- baseApis: MatrixClient,
- devicesByUser: Record<string, DeviceInfo[]>,
-): Promise<[Map<string, DeviceInfo[]>, Map<string, Map<string, IExistingOlmSession>>]> {
- // map user Id → DeviceInfo[]
- const devicesWithoutSession: MapWithDefault<string, DeviceInfo[]> = new MapWithDefault(() => []);
- // map user Id → device Id → IExistingOlmSession
- const sessions: MapWithDefault<string, Map<string, IExistingOlmSession>> = new MapWithDefault(() => new Map());
-
- const promises: Promise<void>[] = [];
-
- for (const [userId, devices] of Object.entries(devicesByUser)) {
- for (const deviceInfo of devices) {
- const deviceId = deviceInfo.deviceId;
- const key = deviceInfo.getIdentityKey();
- promises.push(
- (async (): Promise<void> => {
- const sessionId = await olmDevice.getSessionIdForDevice(key, true);
- if (sessionId === null) {
- devicesWithoutSession.getOrCreate(userId).push(deviceInfo);
- } else {
- sessions.getOrCreate(userId).set(deviceId, {
- device: deviceInfo,
- sessionId: sessionId,
- });
- }
- })(),
- );
- }
- }
-
- await Promise.all(promises);
-
- return [devicesWithoutSession, sessions];
-}
-
-/**
- * Try to make sure we have established olm sessions for the given devices.
- *
- * @param devicesByUser - map from userid to list of devices to ensure sessions for
- *
- * @param force - If true, establish a new session even if one
- * already exists.
- *
- * @param otkTimeout - The timeout in milliseconds when requesting
- * one-time keys for establishing new olm sessions.
- *
- * @param failedServers - An array to fill with remote servers that
- * failed to respond to one-time-key requests.
- *
- * @param log - A possibly customised log
- *
- * @returns resolves once the sessions are complete, to
- * an Object mapping from userId to deviceId to
- * {@link OlmSessionResult}
- */
-export async function ensureOlmSessionsForDevices(
- olmDevice: OlmDevice,
- baseApis: MatrixClient,
- devicesByUser: Map<string, DeviceInfo[]>,
- force = false,
- otkTimeout?: number,
- failedServers?: string[],
- log = logger,
-): Promise<Map<string, Map<string, IOlmSessionResult>>> {
- const devicesWithoutSession: [string, string][] = [
- // [userId, deviceId], ...
- ];
- // map user Id → device Id → IExistingOlmSession
- const result: Map<string, Map<string, IExistingOlmSession>> = new Map();
- // map device key → resolve session fn
- const resolveSession: Map<string, (sessionId?: string) => void> = new Map();
-
- // Mark all sessions this task intends to update as in progress. It is
- // important to do this for all devices this task cares about in a single
- // synchronous operation, as otherwise it is possible to have deadlocks
- // where multiple tasks wait indefinitely on another task to update some set
- // of common devices.
- for (const devices of devicesByUser.values()) {
- for (const deviceInfo of devices) {
- const key = deviceInfo.getIdentityKey();
-
- if (key === olmDevice.deviceCurve25519Key) {
- // We don't start sessions with ourself, so there's no need to
- // mark it in progress.
- continue;
- }
-
- if (!olmDevice.sessionsInProgress[key]) {
- // pre-emptively mark the session as in-progress to avoid race
- // conditions. If we find that we already have a session, then
- // we'll resolve
- olmDevice.sessionsInProgress[key] = new Promise((resolve) => {
- resolveSession.set(key, (v: any): void => {
- delete olmDevice.sessionsInProgress[key];
- resolve(v);
- });
- });
- }
- }
- }
-
- for (const [userId, devices] of devicesByUser) {
- const resultDevices = new Map();
- result.set(userId, resultDevices);
-
- for (const deviceInfo of devices) {
- const deviceId = deviceInfo.deviceId;
- const key = deviceInfo.getIdentityKey();
-
- if (key === olmDevice.deviceCurve25519Key) {
- // We should never be trying to start a session with ourself.
- // Apart from talking to yourself being the first sign of madness,
- // olm sessions can't do this because they get confused when
- // they get a message and see that the 'other side' has started a
- // new chain when this side has an active sender chain.
- // If you see this message being logged in the wild, we should find
- // the thing that is trying to send Olm messages to itself and fix it.
- log.info("Attempted to start session with ourself! Ignoring");
- // We must fill in the section in the return value though, as callers
- // expect it to be there.
- resultDevices.set(deviceId, {
- device: deviceInfo,
- sessionId: null,
- });
- continue;
- }
-
- const forWhom = `for ${key} (${userId}:${deviceId})`;
- const sessionId = await olmDevice.getSessionIdForDevice(key, !!resolveSession.get(key), log);
- const resolveSessionFn = resolveSession.get(key);
- if (sessionId !== null && resolveSessionFn) {
- // we found a session, but we had marked the session as
- // in-progress, so resolve it now, which will unmark it and
- // unblock anything that was waiting
- resolveSessionFn();
- }
- if (sessionId === null || force) {
- if (force) {
- log.info(`Forcing new Olm session ${forWhom}`);
- } else {
- log.info(`Making new Olm session ${forWhom}`);
- }
- devicesWithoutSession.push([userId, deviceId]);
- }
- resultDevices.set(deviceId, {
- device: deviceInfo,
- sessionId: sessionId,
- });
- }
- }
-
- if (devicesWithoutSession.length === 0) {
- return result;
- }
-
- const oneTimeKeyAlgorithm = "signed_curve25519";
- let res: IClaimOTKsResult;
- let taskDetail = `one-time keys for ${devicesWithoutSession.length} devices`;
- try {
- log.debug(`Claiming ${taskDetail}`);
- res = await baseApis.claimOneTimeKeys(devicesWithoutSession, oneTimeKeyAlgorithm, otkTimeout);
- log.debug(`Claimed ${taskDetail}`);
- } catch (e) {
- for (const resolver of resolveSession.values()) {
- resolver();
- }
- log.log(`Failed to claim ${taskDetail}`, e, devicesWithoutSession);
- throw e;
- }
-
- if (failedServers && "failures" in res) {
- failedServers.push(...Object.keys(res.failures));
- }
-
- const otkResult = res.one_time_keys || ({} as IClaimOTKsResult["one_time_keys"]);
- const promises: Promise<void>[] = [];
- for (const [userId, devices] of devicesByUser) {
- const userRes = otkResult[userId] || {};
- for (const deviceInfo of devices) {
- const deviceId = deviceInfo.deviceId;
- const key = deviceInfo.getIdentityKey();
-
- if (key === olmDevice.deviceCurve25519Key) {
- // We've already logged about this above. Skip here too
- // otherwise we'll log saying there are no one-time keys
- // which will be confusing.
- continue;
- }
-
- if (result.get(userId)?.get(deviceId)?.sessionId && !force) {
- // we already have a result for this device
- continue;
- }
-
- const deviceRes = userRes[deviceId] || {};
- let oneTimeKey: IOneTimeKey | null = null;
- for (const keyId in deviceRes) {
- if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) {
- oneTimeKey = deviceRes[keyId];
- }
- }
-
- if (!oneTimeKey) {
- log.warn(`No one-time keys (alg=${oneTimeKeyAlgorithm}) ` + `for device ${userId}:${deviceId}`);
- resolveSession.get(key)?.();
- continue;
- }
-
- promises.push(
- _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo).then(
- (sid) => {
- resolveSession.get(key)?.(sid ?? undefined);
- const deviceInfo = result.get(userId)?.get(deviceId);
- if (deviceInfo) deviceInfo.sessionId = sid;
- },
- (e) => {
- resolveSession.get(key)?.();
- throw e;
- },
- ),
- );
- }
- }
-
- taskDetail = `Olm sessions for ${promises.length} devices`;
- log.debug(`Starting ${taskDetail}`);
- await Promise.all(promises);
- log.debug(`Started ${taskDetail}`);
- return result;
-}
-
-async function _verifyKeyAndStartSession(
- olmDevice: OlmDevice,
- oneTimeKey: IOneTimeKey,
- userId: string,
- deviceInfo: DeviceInfo,
-): Promise<string | null> {
- const deviceId = deviceInfo.deviceId;
- try {
- await verifySignature(olmDevice, oneTimeKey, userId, deviceId, deviceInfo.getFingerprint());
- } catch (e) {
- logger.error("Unable to verify signature on one-time key for device " + userId + ":" + deviceId + ":", e);
- return null;
- }
-
- let sid;
- try {
- sid = await olmDevice.createOutboundSession(deviceInfo.getIdentityKey(), oneTimeKey.key);
- } catch (e) {
- // possibly a bad key
- logger.error("Error starting olm session with device " + userId + ":" + deviceId + ": " + e);
- return null;
- }
-
- logger.log("Started new olm sessionid " + sid + " for device " + userId + ":" + deviceId);
- return sid;
-}
-
-export interface IObject {
- unsigned?: object;
- signatures?: ISignatures;
-}
-
-/**
- * Verify the signature on an object
- *
- * @param olmDevice - olm wrapper to use for verify op
- *
- * @param obj - object to check signature on.
- *
- * @param signingUserId - ID of the user whose signature should be checked
- *
- * @param signingDeviceId - ID of the device whose signature should be checked
- *
- * @param signingKey - base64-ed ed25519 public key
- *
- * Returns a promise which resolves (to undefined) if the the signature is good,
- * or rejects with an Error if it is bad.
- */
-export async function verifySignature(
- olmDevice: OlmDevice,
- obj: IOneTimeKey | IObject,
- signingUserId: string,
- signingDeviceId: string,
- signingKey: string,
-): Promise<void> {
- const signKeyId = "ed25519:" + signingDeviceId;
- const signatures = obj.signatures || {};
- const userSigs = signatures[signingUserId] || {};
- const signature = userSigs[signKeyId];
- if (!signature) {
- throw Error("No signature");
- }
-
- // prepare the canonical json: remove unsigned and signatures, and stringify with anotherjson
- const mangledObj = Object.assign({}, obj);
- if ("unsigned" in mangledObj) {
- delete mangledObj.unsigned;
- }
- delete mangledObj.signatures;
- const json = anotherjson.stringify(mangledObj);
-
- olmDevice.verifySignature(signingKey, json, signature);
-}
-
-/**
- * Sign a JSON object using public key cryptography
- * @param obj - Object to sign. The object will be modified to include
- * the new signature
- * @param key - the signing object or the private key
- * seed
- * @param userId - The user ID who owns the signing key
- * @param pubKey - The public key (ignored if key is a seed)
- * @returns the signature for the object
- */
-export function pkSign(obj: object & IObject, key: Uint8Array | PkSigning, userId: string, pubKey: string): string {
- let createdKey = false;
- if (key instanceof Uint8Array) {
- const keyObj = new global.Olm.PkSigning();
- pubKey = keyObj.init_with_seed(key);
- key = keyObj;
- createdKey = true;
- }
- const sigs = obj.signatures || {};
- delete obj.signatures;
- const unsigned = obj.unsigned;
- if (obj.unsigned) delete obj.unsigned;
- try {
- const mysigs = sigs[userId] || {};
- sigs[userId] = mysigs;
-
- return (mysigs["ed25519:" + pubKey] = key.sign(anotherjson.stringify(obj)));
- } finally {
- obj.signatures = sigs;
- if (unsigned) obj.unsigned = unsigned;
- if (createdKey) {
- key.free();
- }
- }
-}
-
-/**
- * Verify a signed JSON object
- * @param obj - Object to verify
- * @param pubKey - The public key to use to verify
- * @param userId - The user ID who signed the object
- */
-export function pkVerify(obj: IObject, pubKey: string, userId: string): void {
- const keyId = "ed25519:" + pubKey;
- if (!(obj.signatures && obj.signatures[userId] && obj.signatures[userId][keyId])) {
- throw new Error("No signature");
- }
- const signature = obj.signatures[userId][keyId];
- const util = new global.Olm.Utility();
- const sigs = obj.signatures;
- delete obj.signatures;
- const unsigned = obj.unsigned;
- if (obj.unsigned) delete obj.unsigned;
- try {
- util.ed25519_verify(pubKey, anotherjson.stringify(obj), signature);
- } finally {
- obj.signatures = sigs;
- if (unsigned) obj.unsigned = unsigned;
- util.free();
- }
-}
-
-/**
- * Check that an event was encrypted using olm.
- */
-export function isOlmEncrypted(event: MatrixEvent): boolean {
- if (!event.getSenderKey()) {
- logger.error("Event has no sender key (not encrypted?)");
- return false;
- }
- if (
- event.getWireType() !== EventType.RoomMessageEncrypted ||
- !["m.olm.v1.curve25519-aes-sha2"].includes(event.getWireContent().algorithm)
- ) {
- logger.error("Event was not encrypted using an appropriate algorithm");
- return false;
- }
- return true;
-}
-
-/**
- * Encode a typed array of uint8 as base64.
- * @param uint8Array - The data to encode.
- * @returns The base64.
- */
-export function encodeBase64(uint8Array: ArrayBuffer | Uint8Array): string {
- return Buffer.from(uint8Array).toString("base64");
-}
-
-/**
- * Encode a typed array of uint8 as unpadded base64.
- * @param uint8Array - The data to encode.
- * @returns The unpadded base64.
- */
-export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string {
- return encodeBase64(uint8Array).replace(/=+$/g, "");
-}
-
-/**
- * Decode a base64 string to a typed array of uint8.
- * @param base64 - The base64 to decode.
- * @returns The decoded data.
- */
-export function decodeBase64(base64: string): Uint8Array {
- return Buffer.from(base64, "base64");
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/recoverykey.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/recoverykey.ts
deleted file mode 100644
index 4107b76..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/recoverykey.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-
-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 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)
-const OLM_RECOVERY_KEY_PREFIX = [0x8b, 0x01];
-
-export function encodeRecoveryKey(key: ArrayLike<number>): string | undefined {
- const buf = Buffer.alloc(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
- buf.set(OLM_RECOVERY_KEY_PREFIX, 0);
- buf.set(key, OLM_RECOVERY_KEY_PREFIX.length);
-
- let parity = 0;
- for (let i = 0; i < buf.length - 1; ++i) {
- parity ^= buf[i];
- }
- buf[buf.length - 1] = parity;
- const base58key = bs58.encode(buf);
-
- return base58key.match(/.{1,4}/g)?.join(" ");
-}
-
-export function decodeRecoveryKey(recoveryKey: string): Uint8Array {
- const result = bs58.decode(recoveryKey.replace(/ /g, ""));
-
- let parity = 0;
- for (const b of result) {
- parity ^= b;
- }
- if (parity !== 0) {
- throw new Error("Incorrect parity");
- }
-
- for (let i = 0; i < OLM_RECOVERY_KEY_PREFIX.length; ++i) {
- if (result[i] !== OLM_RECOVERY_KEY_PREFIX[i]) {
- throw new Error("Incorrect prefix");
- }
- }
-
- if (result.length !== OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH + 1) {
- throw new Error("Incorrect length");
- }
-
- return Uint8Array.from(
- result.slice(OLM_RECOVERY_KEY_PREFIX.length, OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH),
- );
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/base.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/base.ts
deleted file mode 100644
index 4c88ec2..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/base.ts
+++ /dev/null
@@ -1,226 +0,0 @@
-/*
-Copyright 2021 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 { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
-import { RoomKeyRequestState } from "../OutgoingRoomKeyRequestManager";
-import { ICrossSigningKey } from "../../client";
-import { IOlmDevice } from "../algorithms/megolm";
-import { TrackingStatus } from "../DeviceList";
-import { IRoomEncryption } from "../RoomList";
-import { IDevice } from "../deviceinfo";
-import { ICrossSigningInfo } from "../CrossSigning";
-import { PrefixedLogger } from "../../logger";
-import { InboundGroupSessionData } from "../OlmDevice";
-import { MatrixEvent } from "../../models/event";
-import { DehydrationManager } from "../dehydration";
-import { IEncryptedPayload } from "../aes";
-
-/**
- * Internal module. Definitions for storage for the crypto module
- */
-
-export interface SecretStorePrivateKeys {
- "dehydration": {
- keyInfo: DehydrationManager["keyInfo"];
- key: IEncryptedPayload;
- deviceDisplayName: string;
- time: number;
- } | null;
- "m.megolm_backup.v1": IEncryptedPayload;
-}
-
-/**
- * Abstraction of things that can store data required for end-to-end encryption
- */
-export interface CryptoStore {
- startup(): Promise<CryptoStore>;
- deleteAllData(): Promise<void>;
- getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest>;
- getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null>;
- getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null>;
- getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]>;
- getOutgoingRoomKeyRequestsByTarget(
- userId: string,
- deviceId: string,
- wantedStates: number[],
- ): Promise<OutgoingRoomKeyRequest[]>;
- updateOutgoingRoomKeyRequest(
- requestId: string,
- expectedState: number,
- updates: Partial<OutgoingRoomKeyRequest>,
- ): Promise<OutgoingRoomKeyRequest | null>;
- deleteOutgoingRoomKeyRequest(requestId: string, expectedState: number): Promise<OutgoingRoomKeyRequest | null>;
-
- // Olm Account
- getAccount(txn: unknown, func: (accountPickle: string | null) => void): void;
- storeAccount(txn: unknown, accountPickle: string): void;
- getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void;
- getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
- txn: unknown,
- func: (key: SecretStorePrivateKeys[K] | null) => void,
- type: K,
- ): void;
- storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void;
- storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
- txn: unknown,
- type: K,
- key: SecretStorePrivateKeys[K],
- ): void;
-
- // Olm Sessions
- countEndToEndSessions(txn: unknown, func: (count: number) => void): void;
- getEndToEndSession(
- deviceKey: string,
- sessionId: string,
- txn: unknown,
- func: (session: ISessionInfo | null) => void,
- ): void;
- getEndToEndSessions(
- deviceKey: string,
- txn: unknown,
- func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
- ): void;
- getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo | null) => void): void;
- storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void;
- storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void>;
- getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null>;
- filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]>;
-
- // Inbound Group Sessions
- getEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- txn: unknown,
- func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
- ): void;
- getAllEndToEndInboundGroupSessions(txn: unknown, func: (session: ISession | null) => void): void;
- addEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: InboundGroupSessionData,
- txn: unknown,
- ): void;
- storeEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: InboundGroupSessionData,
- txn: unknown,
- ): void;
- storeEndToEndInboundGroupSessionWithheld(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: IWithheld,
- txn: unknown,
- ): void;
-
- // Device Data
- getEndToEndDeviceData(txn: unknown, func: (deviceData: IDeviceData | null) => void): void;
- storeEndToEndDeviceData(deviceData: IDeviceData, txn: unknown): void;
- storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: unknown): void;
- getEndToEndRooms(txn: unknown, func: (rooms: Record<string, IRoomEncryption>) => void): void;
- getSessionsNeedingBackup(limit: number): Promise<ISession[]>;
- countSessionsNeedingBackup(txn?: unknown): Promise<number>;
- unmarkSessionsNeedingBackup(sessions: ISession[], txn?: unknown): Promise<void>;
- markSessionsNeedingBackup(sessions: ISession[], txn?: unknown): Promise<void>;
- addSharedHistoryInboundGroupSession(roomId: string, senderKey: string, sessionId: string, txn?: unknown): void;
- getSharedHistoryInboundGroupSessions(
- roomId: string,
- txn?: unknown,
- ): Promise<[senderKey: string, sessionId: string][]>;
- addParkedSharedHistory(roomId: string, data: ParkedSharedHistory, txn?: unknown): void;
- takeParkedSharedHistory(roomId: string, txn?: unknown): Promise<ParkedSharedHistory[]>;
-
- // Session key backups
- doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T, log?: PrefixedLogger): Promise<T>;
-}
-
-export type Mode = "readonly" | "readwrite";
-
-export interface ISession {
- senderKey: string;
- sessionId: string;
- sessionData?: InboundGroupSessionData;
-}
-
-export interface ISessionInfo {
- deviceKey?: string;
- sessionId?: string;
- session?: string;
- lastReceivedMessageTs?: number;
-}
-
-export interface IDeviceData {
- devices: {
- [userId: string]: {
- [deviceId: string]: IDevice;
- };
- };
- trackingStatus: {
- [userId: string]: TrackingStatus;
- };
- crossSigningInfo?: Record<string, ICrossSigningInfo>;
- syncToken?: string;
-}
-
-export interface IProblem {
- type: string;
- fixed: boolean;
- time: number;
-}
-
-export interface IWithheld {
- // eslint-disable-next-line camelcase
- room_id: string;
- code: string;
- reason: string;
-}
-
-/**
- * Represents an outgoing room key request
- */
-export interface OutgoingRoomKeyRequest {
- /**
- * Unique id for this request. Used for both an id within the request for later pairing with a cancellation,
- * and for the transaction id when sending the to_device messages to our local server.
- */
- requestId: string;
- requestTxnId?: string;
- /**
- * Transaction id for the cancellation, if any
- */
- cancellationTxnId?: string;
- /**
- * List of recipients for the request
- */
- recipients: IRoomKeyRequestRecipient[];
- /**
- * Parameters for the request
- */
- requestBody: IRoomKeyRequestBody;
- /**
- * current state of this request (states are defined in {@link OutgoingRoomKeyRequestManager})
- */
- state: RoomKeyRequestState;
-}
-
-export interface ParkedSharedHistory {
- senderId: string;
- senderKey: string;
- sessionId: string;
- sessionKey: string;
- keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>; // XXX: Less type dependence on MatrixEvent
- forwardingCurve25519KeyChain: string[];
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store-backend.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store-backend.ts
deleted file mode 100644
index 7827697..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store-backend.ts
+++ /dev/null
@@ -1,1062 +0,0 @@
-/*
-Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { logger, PrefixedLogger } from "../../logger";
-import * as utils from "../../utils";
-import {
- CryptoStore,
- IDeviceData,
- IProblem,
- ISession,
- ISessionInfo,
- IWithheld,
- Mode,
- OutgoingRoomKeyRequest,
- ParkedSharedHistory,
- SecretStorePrivateKeys,
-} from "./base";
-import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
-import { ICrossSigningKey } from "../../client";
-import { IOlmDevice } from "../algorithms/megolm";
-import { IRoomEncryption } from "../RoomList";
-import { InboundGroupSessionData } from "../OlmDevice";
-
-const PROFILE_TRANSACTIONS = false;
-
-/**
- * Implementation of a CryptoStore which is backed by an existing
- * IndexedDB connection. Generally you want IndexedDBCryptoStore
- * which connects to the database and defers to one of these.
- */
-export class Backend implements CryptoStore {
- private nextTxnId = 0;
-
- /**
- */
- public constructor(private db: IDBDatabase) {
- // make sure we close the db on `onversionchange` - otherwise
- // attempts to delete the database will block (and subsequent
- // attempts to re-create it will also block).
- db.onversionchange = (): void => {
- logger.log(`versionchange for indexeddb ${this.db.name}: closing`);
- db.close();
- };
- }
-
- public async startup(): Promise<CryptoStore> {
- // No work to do, as the startup is done by the caller (e.g IndexedDBCryptoStore)
- // by passing us a ready IDBDatabase instance
- return this;
- }
- public async deleteAllData(): Promise<void> {
- throw Error("This is not implemented, call IDBFactory::deleteDatabase(dbName) instead.");
- }
-
- /**
- * Look for an existing outgoing room key request, and if none is found,
- * add a new one
- *
- *
- * @returns resolves to
- * {@link OutgoingRoomKeyRequest}: either the
- * same instance as passed in, or the existing one.
- */
- public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> {
- const requestBody = request.requestBody;
-
- return new Promise((resolve, reject) => {
- const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite");
- txn.onerror = reject;
-
- // first see if we already have an entry for this request.
- this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
- if (existing) {
- // this entry matches the request - return it.
- logger.log(
- `already have key request outstanding for ` +
- `${requestBody.room_id} / ${requestBody.session_id}: ` +
- `not sending another`,
- );
- resolve(existing);
- return;
- }
-
- // we got to the end of the list without finding a match
- // - add the new request.
- logger.log(`enqueueing key request for ${requestBody.room_id} / ` + requestBody.session_id);
- txn.oncomplete = (): void => {
- resolve(request);
- };
- const store = txn.objectStore("outgoingRoomKeyRequests");
- store.add(request);
- });
- });
- }
-
- /**
- * Look for an existing room key request
- *
- * @param requestBody - existing request to look for
- *
- * @returns resolves to the matching
- * {@link OutgoingRoomKeyRequest}, or null if
- * not found
- */
- public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null> {
- return new Promise((resolve, reject) => {
- const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
- txn.onerror = reject;
-
- this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
- resolve(existing);
- });
- });
- }
-
- /**
- * look for an existing room key request in the db
- *
- * @internal
- * @param txn - database transaction
- * @param requestBody - existing request to look for
- * @param callback - function to call with the results of the
- * search. Either passed a matching
- * {@link OutgoingRoomKeyRequest}, or null if
- * not found.
- */
- // eslint-disable-next-line @typescript-eslint/naming-convention
- private _getOutgoingRoomKeyRequest(
- txn: IDBTransaction,
- requestBody: IRoomKeyRequestBody,
- callback: (req: OutgoingRoomKeyRequest | null) => void,
- ): void {
- const store = txn.objectStore("outgoingRoomKeyRequests");
-
- const idx = store.index("session");
- const cursorReq = idx.openCursor([requestBody.room_id, requestBody.session_id]);
-
- cursorReq.onsuccess = (): void => {
- const cursor = cursorReq.result;
- if (!cursor) {
- // no match found
- callback(null);
- return;
- }
-
- const existing = cursor.value;
-
- if (utils.deepCompare(existing.requestBody, requestBody)) {
- // got a match
- callback(existing);
- return;
- }
-
- // look at the next entry in the index
- cursor.continue();
- };
- }
-
- /**
- * Look for room key requests by state
- *
- * @param wantedStates - list of acceptable states
- *
- * @returns resolves to the a
- * {@link OutgoingRoomKeyRequest}, or null if
- * there are no pending requests in those states. If there are multiple
- * requests in those states, an arbitrary one is chosen.
- */
- public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null> {
- if (wantedStates.length === 0) {
- return Promise.resolve(null);
- }
-
- // this is a bit tortuous because we need to make sure we do the lookup
- // in a single transaction, to avoid having a race with the insertion
- // code.
-
- // index into the wantedStates array
- let stateIndex = 0;
- let result: OutgoingRoomKeyRequest;
-
- function onsuccess(this: IDBRequest<IDBCursorWithValue | null>): void {
- const cursor = this.result;
- if (cursor) {
- // got a match
- result = cursor.value;
- return;
- }
-
- // try the next state in the list
- stateIndex++;
- if (stateIndex >= wantedStates.length) {
- // no matches
- return;
- }
-
- const wantedState = wantedStates[stateIndex];
- const cursorReq = (this.source as IDBIndex).openCursor(wantedState);
- cursorReq.onsuccess = onsuccess;
- }
-
- const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
- const store = txn.objectStore("outgoingRoomKeyRequests");
-
- const wantedState = wantedStates[stateIndex];
- const cursorReq = store.index("state").openCursor(wantedState);
- cursorReq.onsuccess = onsuccess;
-
- return promiseifyTxn(txn).then(() => result);
- }
-
- /**
- *
- * @returns All elements in a given state
- */
- public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]> {
- return new Promise((resolve, reject) => {
- const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
- const store = txn.objectStore("outgoingRoomKeyRequests");
- const index = store.index("state");
- const request = index.getAll(wantedState);
-
- request.onsuccess = (): void => resolve(request.result);
- request.onerror = (): void => reject(request.error);
- });
- }
-
- public getOutgoingRoomKeyRequestsByTarget(
- userId: string,
- deviceId: string,
- wantedStates: number[],
- ): Promise<OutgoingRoomKeyRequest[]> {
- let stateIndex = 0;
- const results: OutgoingRoomKeyRequest[] = [];
-
- function onsuccess(this: IDBRequest<IDBCursorWithValue | null>): void {
- const cursor = this.result;
- if (cursor) {
- const keyReq = cursor.value;
- if (
- keyReq.recipients.some(
- (recipient: IRoomKeyRequestRecipient) =>
- recipient.userId === userId && recipient.deviceId === deviceId,
- )
- ) {
- results.push(keyReq);
- }
- cursor.continue();
- } else {
- // try the next state in the list
- stateIndex++;
- if (stateIndex >= wantedStates.length) {
- // no matches
- return;
- }
-
- const wantedState = wantedStates[stateIndex];
- const cursorReq = (this.source as IDBIndex).openCursor(wantedState);
- cursorReq.onsuccess = onsuccess;
- }
- }
-
- const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
- const store = txn.objectStore("outgoingRoomKeyRequests");
-
- const wantedState = wantedStates[stateIndex];
- const cursorReq = store.index("state").openCursor(wantedState);
- cursorReq.onsuccess = onsuccess;
-
- return promiseifyTxn(txn).then(() => results);
- }
-
- /**
- * Look for an existing room key request by id and state, and update it if
- * found
- *
- * @param requestId - ID of request to update
- * @param expectedState - state we expect to find the request in
- * @param updates - name/value map of updates to apply
- *
- * @returns resolves to
- * {@link OutgoingRoomKeyRequest}
- * updated request, or null if no matching row was found
- */
- public updateOutgoingRoomKeyRequest(
- requestId: string,
- expectedState: number,
- updates: Partial<OutgoingRoomKeyRequest>,
- ): Promise<OutgoingRoomKeyRequest | null> {
- let result: OutgoingRoomKeyRequest | null = null;
-
- function onsuccess(this: IDBRequest<IDBCursorWithValue | null>): void {
- const cursor = this.result;
- if (!cursor) {
- return;
- }
- const data = cursor.value;
- if (data.state != expectedState) {
- logger.warn(
- `Cannot update room key request from ${expectedState} ` +
- `as it was already updated to ${data.state}`,
- );
- return;
- }
- Object.assign(data, updates);
- cursor.update(data);
- result = data;
- }
-
- const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite");
- const cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId);
- cursorReq.onsuccess = onsuccess;
- return promiseifyTxn(txn).then(() => result);
- }
-
- /**
- * Look for an existing room key request by id and state, and delete it if
- * found
- *
- * @param requestId - ID of request to update
- * @param expectedState - state we expect to find the request in
- *
- * @returns resolves once the operation is completed
- */
- public deleteOutgoingRoomKeyRequest(
- requestId: string,
- expectedState: number,
- ): Promise<OutgoingRoomKeyRequest | null> {
- const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite");
- const cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId);
- cursorReq.onsuccess = (): void => {
- const cursor = cursorReq.result;
- if (!cursor) {
- return;
- }
- const data = cursor.value;
- if (data.state != expectedState) {
- logger.warn(`Cannot delete room key request in state ${data.state} ` + `(expected ${expectedState})`);
- return;
- }
- cursor.delete();
- };
- return promiseifyTxn<OutgoingRoomKeyRequest | null>(txn);
- }
-
- // Olm Account
-
- public getAccount(txn: IDBTransaction, func: (accountPickle: string | null) => void): void {
- const objectStore = txn.objectStore("account");
- const getReq = objectStore.get("-");
- getReq.onsuccess = function (): void {
- try {
- func(getReq.result || null);
- } catch (e) {
- abortWithException(txn, <Error>e);
- }
- };
- }
-
- public storeAccount(txn: IDBTransaction, accountPickle: string): void {
- const objectStore = txn.objectStore("account");
- objectStore.put(accountPickle, "-");
- }
-
- public getCrossSigningKeys(
- txn: IDBTransaction,
- func: (keys: Record<string, ICrossSigningKey> | null) => void,
- ): void {
- const objectStore = txn.objectStore("account");
- const getReq = objectStore.get("crossSigningKeys");
- getReq.onsuccess = function (): void {
- try {
- func(getReq.result || null);
- } catch (e) {
- abortWithException(txn, <Error>e);
- }
- };
- }
-
- public getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
- txn: IDBTransaction,
- func: (key: SecretStorePrivateKeys[K] | null) => void,
- type: K,
- ): void {
- const objectStore = txn.objectStore("account");
- const getReq = objectStore.get(`ssss_cache:${type}`);
- getReq.onsuccess = function (): void {
- try {
- func(getReq.result || null);
- } catch (e) {
- abortWithException(txn, <Error>e);
- }
- };
- }
-
- public storeCrossSigningKeys(txn: IDBTransaction, keys: Record<string, ICrossSigningKey>): void {
- const objectStore = txn.objectStore("account");
- objectStore.put(keys, "crossSigningKeys");
- }
-
- public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
- txn: IDBTransaction,
- type: K,
- key: SecretStorePrivateKeys[K],
- ): void {
- const objectStore = txn.objectStore("account");
- objectStore.put(key, `ssss_cache:${type}`);
- }
-
- // Olm Sessions
-
- public countEndToEndSessions(txn: IDBTransaction, func: (count: number) => void): void {
- const objectStore = txn.objectStore("sessions");
- const countReq = objectStore.count();
- countReq.onsuccess = function (): void {
- try {
- func(countReq.result);
- } catch (e) {
- abortWithException(txn, <Error>e);
- }
- };
- }
-
- public getEndToEndSessions(
- deviceKey: string,
- txn: IDBTransaction,
- func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
- ): void {
- const objectStore = txn.objectStore("sessions");
- const idx = objectStore.index("deviceKey");
- const getReq = idx.openCursor(deviceKey);
- const results: Parameters<Parameters<Backend["getEndToEndSessions"]>[2]>[0] = {};
- getReq.onsuccess = function (): void {
- const cursor = getReq.result;
- if (cursor) {
- results[cursor.value.sessionId] = {
- session: cursor.value.session,
- lastReceivedMessageTs: cursor.value.lastReceivedMessageTs,
- };
- cursor.continue();
- } else {
- try {
- func(results);
- } catch (e) {
- abortWithException(txn, <Error>e);
- }
- }
- };
- }
-
- public getEndToEndSession(
- deviceKey: string,
- sessionId: string,
- txn: IDBTransaction,
- func: (session: ISessionInfo | null) => void,
- ): void {
- const objectStore = txn.objectStore("sessions");
- const getReq = objectStore.get([deviceKey, sessionId]);
- getReq.onsuccess = function (): void {
- try {
- if (getReq.result) {
- func({
- session: getReq.result.session,
- lastReceivedMessageTs: getReq.result.lastReceivedMessageTs,
- });
- } else {
- func(null);
- }
- } catch (e) {
- abortWithException(txn, <Error>e);
- }
- };
- }
-
- public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo | null) => void): void {
- const objectStore = txn.objectStore("sessions");
- const getReq = objectStore.openCursor();
- getReq.onsuccess = function (): void {
- try {
- const cursor = getReq.result;
- if (cursor) {
- func(cursor.value);
- cursor.continue();
- } else {
- func(null);
- }
- } catch (e) {
- abortWithException(txn, <Error>e);
- }
- };
- }
-
- public storeEndToEndSession(
- deviceKey: string,
- sessionId: string,
- sessionInfo: ISessionInfo,
- txn: IDBTransaction,
- ): void {
- const objectStore = txn.objectStore("sessions");
- objectStore.put({
- deviceKey,
- sessionId,
- session: sessionInfo.session,
- lastReceivedMessageTs: sessionInfo.lastReceivedMessageTs,
- });
- }
-
- public async storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
- const txn = this.db.transaction("session_problems", "readwrite");
- const objectStore = txn.objectStore("session_problems");
- objectStore.put({
- deviceKey,
- type,
- fixed,
- time: Date.now(),
- });
- await promiseifyTxn(txn);
- }
-
- public async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
- let result: IProblem | null = null;
- const txn = this.db.transaction("session_problems", "readwrite");
- const objectStore = txn.objectStore("session_problems");
- const index = objectStore.index("deviceKey");
- const req = index.getAll(deviceKey);
- req.onsuccess = (): void => {
- const problems = req.result;
- if (!problems.length) {
- result = null;
- return;
- }
- problems.sort((a, b) => {
- return a.time - b.time;
- });
- const lastProblem = problems[problems.length - 1];
- for (const problem of problems) {
- if (problem.time > timestamp) {
- result = Object.assign({}, problem, { fixed: lastProblem.fixed });
- return;
- }
- }
- if (lastProblem.fixed) {
- result = null;
- } else {
- result = lastProblem;
- }
- };
- await promiseifyTxn(txn);
- return result;
- }
-
- // FIXME: we should probably prune this when devices get deleted
- public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
- const txn = this.db.transaction("notified_error_devices", "readwrite");
- const objectStore = txn.objectStore("notified_error_devices");
-
- const ret: IOlmDevice[] = [];
-
- await Promise.all(
- devices.map((device) => {
- return new Promise<void>((resolve) => {
- const { userId, deviceInfo } = device;
- const getReq = objectStore.get([userId, deviceInfo.deviceId]);
- getReq.onsuccess = function (): void {
- if (!getReq.result) {
- objectStore.put({ userId, deviceId: deviceInfo.deviceId });
- ret.push(device);
- }
- resolve();
- };
- });
- }),
- );
-
- return ret;
- }
-
- // Inbound group sessions
-
- public getEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- txn: IDBTransaction,
- func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
- ): void {
- let session: InboundGroupSessionData | null | boolean = false;
- let withheld: IWithheld | null | boolean = false;
- const objectStore = txn.objectStore("inbound_group_sessions");
- const getReq = objectStore.get([senderCurve25519Key, sessionId]);
- getReq.onsuccess = function (): void {
- try {
- if (getReq.result) {
- session = getReq.result.session;
- } else {
- session = null;
- }
- if (withheld !== false) {
- func(session as InboundGroupSessionData, withheld as IWithheld);
- }
- } catch (e) {
- abortWithException(txn, <Error>e);
- }
- };
-
- const withheldObjectStore = txn.objectStore("inbound_group_sessions_withheld");
- const withheldGetReq = withheldObjectStore.get([senderCurve25519Key, sessionId]);
- withheldGetReq.onsuccess = function (): void {
- try {
- if (withheldGetReq.result) {
- withheld = withheldGetReq.result.session;
- } else {
- withheld = null;
- }
- if (session !== false) {
- func(session as InboundGroupSessionData, withheld as IWithheld);
- }
- } catch (e) {
- abortWithException(txn, <Error>e);
- }
- };
- }
-
- public getAllEndToEndInboundGroupSessions(txn: IDBTransaction, func: (session: ISession | null) => void): void {
- const objectStore = txn.objectStore("inbound_group_sessions");
- const getReq = objectStore.openCursor();
- getReq.onsuccess = function (): void {
- const cursor = getReq.result;
- if (cursor) {
- try {
- func({
- senderKey: cursor.value.senderCurve25519Key,
- sessionId: cursor.value.sessionId,
- sessionData: cursor.value.session,
- });
- } catch (e) {
- abortWithException(txn, <Error>e);
- }
- cursor.continue();
- } else {
- try {
- func(null);
- } catch (e) {
- abortWithException(txn, <Error>e);
- }
- }
- };
- }
-
- public addEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: InboundGroupSessionData,
- txn: IDBTransaction,
- ): void {
- const objectStore = txn.objectStore("inbound_group_sessions");
- const addReq = objectStore.add({
- senderCurve25519Key,
- sessionId,
- session: sessionData,
- });
- addReq.onerror = (ev): void => {
- if (addReq.error?.name === "ConstraintError") {
- // This stops the error from triggering the txn's onerror
- ev.stopPropagation();
- // ...and this stops it from aborting the transaction
- ev.preventDefault();
- logger.log("Ignoring duplicate inbound group session: " + senderCurve25519Key + " / " + sessionId);
- } else {
- abortWithException(txn, new Error("Failed to add inbound group session: " + addReq.error));
- }
- };
- }
-
- public storeEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: InboundGroupSessionData,
- txn: IDBTransaction,
- ): void {
- const objectStore = txn.objectStore("inbound_group_sessions");
- objectStore.put({
- senderCurve25519Key,
- sessionId,
- session: sessionData,
- });
- }
-
- public storeEndToEndInboundGroupSessionWithheld(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: IWithheld,
- txn: IDBTransaction,
- ): void {
- const objectStore = txn.objectStore("inbound_group_sessions_withheld");
- objectStore.put({
- senderCurve25519Key,
- sessionId,
- session: sessionData,
- });
- }
-
- public getEndToEndDeviceData(txn: IDBTransaction, func: (deviceData: IDeviceData | null) => void): void {
- const objectStore = txn.objectStore("device_data");
- const getReq = objectStore.get("-");
- getReq.onsuccess = function (): void {
- try {
- func(getReq.result || null);
- } catch (e) {
- abortWithException(txn, <Error>e);
- }
- };
- }
-
- public storeEndToEndDeviceData(deviceData: IDeviceData, txn: IDBTransaction): void {
- const objectStore = txn.objectStore("device_data");
- objectStore.put(deviceData, "-");
- }
-
- public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: IDBTransaction): void {
- const objectStore = txn.objectStore("rooms");
- objectStore.put(roomInfo, roomId);
- }
-
- public getEndToEndRooms(txn: IDBTransaction, func: (rooms: Record<string, IRoomEncryption>) => void): void {
- const rooms: Parameters<Parameters<Backend["getEndToEndRooms"]>[1]>[0] = {};
- const objectStore = txn.objectStore("rooms");
- const getReq = objectStore.openCursor();
- getReq.onsuccess = function (): void {
- const cursor = getReq.result;
- if (cursor) {
- rooms[cursor.key as string] = cursor.value;
- cursor.continue();
- } else {
- try {
- func(rooms);
- } catch (e) {
- abortWithException(txn, <Error>e);
- }
- }
- };
- }
-
- // session backups
-
- public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
- return new Promise((resolve, reject) => {
- const sessions: ISession[] = [];
-
- const txn = this.db.transaction(["sessions_needing_backup", "inbound_group_sessions"], "readonly");
- txn.onerror = reject;
- txn.oncomplete = function (): void {
- resolve(sessions);
- };
- const objectStore = txn.objectStore("sessions_needing_backup");
- const sessionStore = txn.objectStore("inbound_group_sessions");
- const getReq = objectStore.openCursor();
- getReq.onsuccess = function (): void {
- const cursor = getReq.result;
- if (cursor) {
- const sessionGetReq = sessionStore.get(cursor.key);
- sessionGetReq.onsuccess = function (): void {
- sessions.push({
- senderKey: sessionGetReq.result.senderCurve25519Key,
- sessionId: sessionGetReq.result.sessionId,
- sessionData: sessionGetReq.result.session,
- });
- };
- if (!limit || sessions.length < limit) {
- cursor.continue();
- }
- }
- };
- });
- }
-
- public countSessionsNeedingBackup(txn?: IDBTransaction): Promise<number> {
- if (!txn) {
- txn = this.db.transaction("sessions_needing_backup", "readonly");
- }
- const objectStore = txn.objectStore("sessions_needing_backup");
- return new Promise((resolve, reject) => {
- const req = objectStore.count();
- req.onerror = reject;
- req.onsuccess = (): void => resolve(req.result);
- });
- }
-
- public async unmarkSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
- if (!txn) {
- txn = this.db.transaction("sessions_needing_backup", "readwrite");
- }
- const objectStore = txn.objectStore("sessions_needing_backup");
- await Promise.all(
- sessions.map((session) => {
- return new Promise((resolve, reject) => {
- const req = objectStore.delete([session.senderKey, session.sessionId]);
- req.onsuccess = resolve;
- req.onerror = reject;
- });
- }),
- );
- }
-
- public async markSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
- if (!txn) {
- txn = this.db.transaction("sessions_needing_backup", "readwrite");
- }
- const objectStore = txn.objectStore("sessions_needing_backup");
- await Promise.all(
- sessions.map((session) => {
- return new Promise((resolve, reject) => {
- const req = objectStore.put({
- senderCurve25519Key: session.senderKey,
- sessionId: session.sessionId,
- });
- req.onsuccess = resolve;
- req.onerror = reject;
- });
- }),
- );
- }
-
- public addSharedHistoryInboundGroupSession(
- roomId: string,
- senderKey: string,
- sessionId: string,
- txn?: IDBTransaction,
- ): void {
- if (!txn) {
- txn = this.db.transaction("shared_history_inbound_group_sessions", "readwrite");
- }
- const objectStore = txn.objectStore("shared_history_inbound_group_sessions");
- const req = objectStore.get([roomId]);
- req.onsuccess = (): void => {
- const { sessions } = req.result || { sessions: [] };
- sessions.push([senderKey, sessionId]);
- objectStore.put({ roomId, sessions });
- };
- }
-
- public getSharedHistoryInboundGroupSessions(
- roomId: string,
- txn?: IDBTransaction,
- ): Promise<[senderKey: string, sessionId: string][]> {
- if (!txn) {
- txn = this.db.transaction("shared_history_inbound_group_sessions", "readonly");
- }
- const objectStore = txn.objectStore("shared_history_inbound_group_sessions");
- const req = objectStore.get([roomId]);
- return new Promise((resolve, reject) => {
- req.onsuccess = (): void => {
- const { sessions } = req.result || { sessions: [] };
- resolve(sessions);
- };
- req.onerror = reject;
- });
- }
-
- public addParkedSharedHistory(roomId: string, parkedData: ParkedSharedHistory, txn?: IDBTransaction): void {
- if (!txn) {
- txn = this.db.transaction("parked_shared_history", "readwrite");
- }
- const objectStore = txn.objectStore("parked_shared_history");
- const req = objectStore.get([roomId]);
- req.onsuccess = (): void => {
- const { parked } = req.result || { parked: [] };
- parked.push(parkedData);
- objectStore.put({ roomId, parked });
- };
- }
-
- public takeParkedSharedHistory(roomId: string, txn?: IDBTransaction): Promise<ParkedSharedHistory[]> {
- if (!txn) {
- txn = this.db.transaction("parked_shared_history", "readwrite");
- }
- const cursorReq = txn.objectStore("parked_shared_history").openCursor(roomId);
- return new Promise((resolve, reject) => {
- cursorReq.onsuccess = (): void => {
- const cursor = cursorReq.result;
- if (!cursor) {
- resolve([]);
- return;
- }
- const data = cursor.value;
- cursor.delete();
- resolve(data);
- };
- cursorReq.onerror = reject;
- });
- }
-
- public doTxn<T>(
- mode: Mode,
- stores: string | string[],
- func: (txn: IDBTransaction) => T,
- log: PrefixedLogger = logger,
- ): Promise<T> {
- let startTime: number;
- let description: string;
- if (PROFILE_TRANSACTIONS) {
- const txnId = this.nextTxnId++;
- startTime = Date.now();
- description = `${mode} crypto store transaction ${txnId} in ${stores}`;
- log.debug(`Starting ${description}`);
- }
- const txn = this.db.transaction(stores, mode);
- const promise = promiseifyTxn(txn);
- const result = func(txn);
- if (PROFILE_TRANSACTIONS) {
- promise.then(
- () => {
- const elapsedTime = Date.now() - startTime;
- log.debug(`Finished ${description}, took ${elapsedTime} ms`);
- },
- () => {
- const elapsedTime = Date.now() - startTime;
- log.error(`Failed ${description}, took ${elapsedTime} ms`);
- },
- );
- }
- return promise.then(() => {
- return result;
- });
- }
-}
-
-type DbMigration = (db: IDBDatabase) => void;
-const DB_MIGRATIONS: DbMigration[] = [
- (db): void => {
- createDatabase(db);
- },
- (db): void => {
- db.createObjectStore("account");
- },
- (db): void => {
- const sessionsStore = db.createObjectStore("sessions", {
- keyPath: ["deviceKey", "sessionId"],
- });
- sessionsStore.createIndex("deviceKey", "deviceKey");
- },
- (db): void => {
- db.createObjectStore("inbound_group_sessions", {
- keyPath: ["senderCurve25519Key", "sessionId"],
- });
- },
- (db): void => {
- db.createObjectStore("device_data");
- },
- (db): void => {
- db.createObjectStore("rooms");
- },
- (db): void => {
- db.createObjectStore("sessions_needing_backup", {
- keyPath: ["senderCurve25519Key", "sessionId"],
- });
- },
- (db): void => {
- db.createObjectStore("inbound_group_sessions_withheld", {
- keyPath: ["senderCurve25519Key", "sessionId"],
- });
- },
- (db): void => {
- const problemsStore = db.createObjectStore("session_problems", {
- keyPath: ["deviceKey", "time"],
- });
- problemsStore.createIndex("deviceKey", "deviceKey");
-
- db.createObjectStore("notified_error_devices", {
- keyPath: ["userId", "deviceId"],
- });
- },
- (db): void => {
- db.createObjectStore("shared_history_inbound_group_sessions", {
- keyPath: ["roomId"],
- });
- },
- (db): void => {
- db.createObjectStore("parked_shared_history", {
- keyPath: ["roomId"],
- });
- },
- // Expand as needed.
-];
-export const VERSION = DB_MIGRATIONS.length;
-
-export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
- logger.log(`Upgrading IndexedDBCryptoStore from version ${oldVersion}` + ` to ${VERSION}`);
- DB_MIGRATIONS.forEach((migration, index) => {
- if (oldVersion <= index) migration(db);
- });
-}
-
-function createDatabase(db: IDBDatabase): void {
- const outgoingRoomKeyRequestsStore = db.createObjectStore("outgoingRoomKeyRequests", { keyPath: "requestId" });
-
- // we assume that the RoomKeyRequestBody will have room_id and session_id
- // properties, to make the index efficient.
- outgoingRoomKeyRequestsStore.createIndex("session", ["requestBody.room_id", "requestBody.session_id"]);
-
- outgoingRoomKeyRequestsStore.createIndex("state", "state");
-}
-
-interface IWrappedIDBTransaction extends IDBTransaction {
- _mx_abortexception: Error; // eslint-disable-line camelcase
-}
-
-/*
- * Aborts a transaction with a given exception
- * The transaction promise will be rejected with this exception.
- */
-function abortWithException(txn: IDBTransaction, e: Error): void {
- // We cheekily stick our exception onto the transaction object here
- // We could alternatively make the thing we pass back to the app
- // an object containing the transaction and exception.
- (txn as IWrappedIDBTransaction)._mx_abortexception = e;
- try {
- txn.abort();
- } catch (e) {
- // sometimes we won't be able to abort the transaction
- // (ie. if it's aborted or completed)
- }
-}
-
-function promiseifyTxn<T>(txn: IDBTransaction): Promise<T | null> {
- return new Promise((resolve, reject) => {
- txn.oncomplete = (): void => {
- if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
- reject((txn as IWrappedIDBTransaction)._mx_abortexception);
- }
- resolve(null);
- };
- txn.onerror = (event): void => {
- if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
- reject((txn as IWrappedIDBTransaction)._mx_abortexception);
- } else {
- logger.log("Error performing indexeddb txn", event);
- reject(txn.error);
- }
- };
- txn.onabort = (event): void => {
- if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
- reject((txn as IWrappedIDBTransaction)._mx_abortexception);
- } else {
- logger.log("Error performing indexeddb txn", event);
- reject(txn.error);
- }
- };
- });
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store.ts
deleted file mode 100644
index 320235f..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store.ts
+++ /dev/null
@@ -1,708 +0,0 @@
-/*
-Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { logger, PrefixedLogger } from "../../logger";
-import { LocalStorageCryptoStore } from "./localStorage-crypto-store";
-import { MemoryCryptoStore } from "./memory-crypto-store";
-import * as IndexedDBCryptoStoreBackend from "./indexeddb-crypto-store-backend";
-import { InvalidCryptoStoreError, InvalidCryptoStoreState } from "../../errors";
-import * as IndexedDBHelpers from "../../indexeddb-helpers";
-import {
- CryptoStore,
- IDeviceData,
- IProblem,
- ISession,
- ISessionInfo,
- IWithheld,
- Mode,
- OutgoingRoomKeyRequest,
- ParkedSharedHistory,
- SecretStorePrivateKeys,
-} from "./base";
-import { IRoomKeyRequestBody } from "../index";
-import { ICrossSigningKey } from "../../client";
-import { IOlmDevice } from "../algorithms/megolm";
-import { IRoomEncryption } from "../RoomList";
-import { InboundGroupSessionData } from "../OlmDevice";
-
-/**
- * Internal module. indexeddb storage for e2e.
- */
-
-/**
- * An implementation of CryptoStore, which is normally backed by an indexeddb,
- * but with fallback to MemoryCryptoStore.
- */
-export class IndexedDBCryptoStore implements CryptoStore {
- public static STORE_ACCOUNT = "account";
- public static STORE_SESSIONS = "sessions";
- public static STORE_INBOUND_GROUP_SESSIONS = "inbound_group_sessions";
- public static STORE_INBOUND_GROUP_SESSIONS_WITHHELD = "inbound_group_sessions_withheld";
- public static STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS = "shared_history_inbound_group_sessions";
- public static STORE_PARKED_SHARED_HISTORY = "parked_shared_history";
- public static STORE_DEVICE_DATA = "device_data";
- public static STORE_ROOMS = "rooms";
- public static STORE_BACKUP = "sessions_needing_backup";
-
- public static exists(indexedDB: IDBFactory, dbName: string): Promise<boolean> {
- return IndexedDBHelpers.exists(indexedDB, dbName);
- }
-
- private backendPromise?: Promise<CryptoStore>;
- private backend?: CryptoStore;
-
- /**
- * Create a new IndexedDBCryptoStore
- *
- * @param indexedDB - global indexedDB instance
- * @param dbName - name of db to connect to
- */
- public constructor(private readonly indexedDB: IDBFactory, private readonly dbName: string) {}
-
- /**
- * Ensure the database exists and is up-to-date, or fall back to
- * a local storage or in-memory store.
- *
- * This must be called before the store can be used.
- *
- * @returns resolves to either an IndexedDBCryptoStoreBackend.Backend,
- * or a MemoryCryptoStore
- */
- public startup(): Promise<CryptoStore> {
- if (this.backendPromise) {
- return this.backendPromise;
- }
-
- this.backendPromise = new Promise<CryptoStore>((resolve, reject) => {
- if (!this.indexedDB) {
- reject(new Error("no indexeddb support available"));
- return;
- }
-
- logger.log(`connecting to indexeddb ${this.dbName}`);
-
- const req = this.indexedDB.open(this.dbName, IndexedDBCryptoStoreBackend.VERSION);
-
- req.onupgradeneeded = (ev): void => {
- const db = req.result;
- const oldVersion = ev.oldVersion;
- IndexedDBCryptoStoreBackend.upgradeDatabase(db, oldVersion);
- };
-
- req.onblocked = (): void => {
- logger.log(`can't yet open IndexedDBCryptoStore because it is open elsewhere`);
- };
-
- req.onerror = (ev): void => {
- logger.log("Error connecting to indexeddb", ev);
- reject(req.error);
- };
-
- req.onsuccess = (): void => {
- const db = req.result;
-
- logger.log(`connected to indexeddb ${this.dbName}`);
- resolve(new IndexedDBCryptoStoreBackend.Backend(db));
- };
- })
- .then((backend) => {
- // Edge has IndexedDB but doesn't support compund keys which we use fairly extensively.
- // Try a dummy query which will fail if the browser doesn't support compund keys, so
- // we can fall back to a different backend.
- return backend
- .doTxn(
- "readonly",
- [
- IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
- IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
- ],
- (txn) => {
- backend.getEndToEndInboundGroupSession("", "", txn, () => {});
- },
- )
- .then(() => backend);
- })
- .catch((e) => {
- if (e.name === "VersionError") {
- logger.warn("Crypto DB is too new for us to use!", e);
- // don't fall back to a different store: the user has crypto data
- // in this db so we should use it or nothing at all.
- throw new InvalidCryptoStoreError(InvalidCryptoStoreState.TooNew);
- }
- logger.warn(
- `unable to connect to indexeddb ${this.dbName}` + `: falling back to localStorage store: ${e}`,
- );
-
- try {
- return new LocalStorageCryptoStore(global.localStorage);
- } catch (e) {
- logger.warn(`unable to open localStorage: falling back to in-memory store: ${e}`);
- return new MemoryCryptoStore();
- }
- })
- .then((backend) => {
- this.backend = backend;
- return backend;
- });
-
- return this.backendPromise;
- }
-
- /**
- * Delete all data from this store.
- *
- * @returns resolves when the store has been cleared.
- */
- public deleteAllData(): Promise<void> {
- return new Promise<void>((resolve, reject) => {
- if (!this.indexedDB) {
- reject(new Error("no indexeddb support available"));
- return;
- }
-
- logger.log(`Removing indexeddb instance: ${this.dbName}`);
- const req = this.indexedDB.deleteDatabase(this.dbName);
-
- req.onblocked = (): void => {
- logger.log(`can't yet delete IndexedDBCryptoStore because it is open elsewhere`);
- };
-
- req.onerror = (ev): void => {
- logger.log("Error deleting data from indexeddb", ev);
- reject(req.error);
- };
-
- req.onsuccess = (): void => {
- logger.log(`Removed indexeddb instance: ${this.dbName}`);
- resolve();
- };
- }).catch((e) => {
- // in firefox, with indexedDB disabled, this fails with a
- // DOMError. We treat this as non-fatal, so that people can
- // still use the app.
- logger.warn(`unable to delete IndexedDBCryptoStore: ${e}`);
- });
- }
-
- /**
- * Look for an existing outgoing room key request, and if none is found,
- * add a new one
- *
- *
- * @returns resolves to
- * {@link OutgoingRoomKeyRequest}: either the
- * same instance as passed in, or the existing one.
- */
- public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> {
- return this.backend!.getOrAddOutgoingRoomKeyRequest(request);
- }
-
- /**
- * Look for an existing room key request
- *
- * @param requestBody - existing request to look for
- *
- * @returns resolves to the matching
- * {@link OutgoingRoomKeyRequest}, or null if
- * not found
- */
- public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null> {
- return this.backend!.getOutgoingRoomKeyRequest(requestBody);
- }
-
- /**
- * Look for room key requests by state
- *
- * @param wantedStates - list of acceptable states
- *
- * @returns resolves to the a
- * {@link OutgoingRoomKeyRequest}, or null if
- * there are no pending requests in those states. If there are multiple
- * requests in those states, an arbitrary one is chosen.
- */
- public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null> {
- return this.backend!.getOutgoingRoomKeyRequestByState(wantedStates);
- }
-
- /**
- * Look for room key requests by state –
- * unlike above, return a list of all entries in one state.
- *
- * @returns Returns an array of requests in the given state
- */
- public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]> {
- return this.backend!.getAllOutgoingRoomKeyRequestsByState(wantedState);
- }
-
- /**
- * Look for room key requests by target device and state
- *
- * @param userId - Target user ID
- * @param deviceId - Target device ID
- * @param wantedStates - list of acceptable states
- *
- * @returns resolves to a list of all the
- * {@link OutgoingRoomKeyRequest}
- */
- public getOutgoingRoomKeyRequestsByTarget(
- userId: string,
- deviceId: string,
- wantedStates: number[],
- ): Promise<OutgoingRoomKeyRequest[]> {
- return this.backend!.getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates);
- }
-
- /**
- * Look for an existing room key request by id and state, and update it if
- * found
- *
- * @param requestId - ID of request to update
- * @param expectedState - state we expect to find the request in
- * @param updates - name/value map of updates to apply
- *
- * @returns resolves to
- * {@link OutgoingRoomKeyRequest}
- * updated request, or null if no matching row was found
- */
- public updateOutgoingRoomKeyRequest(
- requestId: string,
- expectedState: number,
- updates: Partial<OutgoingRoomKeyRequest>,
- ): Promise<OutgoingRoomKeyRequest | null> {
- return this.backend!.updateOutgoingRoomKeyRequest(requestId, expectedState, updates);
- }
-
- /**
- * Look for an existing room key request by id and state, and delete it if
- * found
- *
- * @param requestId - ID of request to update
- * @param expectedState - state we expect to find the request in
- *
- * @returns resolves once the operation is completed
- */
- public deleteOutgoingRoomKeyRequest(
- requestId: string,
- expectedState: number,
- ): Promise<OutgoingRoomKeyRequest | null> {
- return this.backend!.deleteOutgoingRoomKeyRequest(requestId, expectedState);
- }
-
- // Olm Account
-
- /*
- * Get the account pickle from the store.
- * This requires an active transaction. See doTxn().
- *
- * @param txn - An active transaction. See doTxn().
- * @param func - Called with the account pickle
- */
- public getAccount(txn: IDBTransaction, func: (accountPickle: string | null) => void): void {
- this.backend!.getAccount(txn, func);
- }
-
- /**
- * Write the account pickle to the store.
- * This requires an active transaction. See doTxn().
- *
- * @param txn - An active transaction. See doTxn().
- * @param accountPickle - The new account pickle to store.
- */
- public storeAccount(txn: IDBTransaction, accountPickle: string): void {
- this.backend!.storeAccount(txn, accountPickle);
- }
-
- /**
- * Get the public part of the cross-signing keys (eg. self-signing key,
- * user signing key).
- *
- * @param txn - An active transaction. See doTxn().
- * @param func - Called with the account keys object:
- * `{ key_type: base64 encoded seed }` where key type = user_signing_key_seed or self_signing_key_seed
- */
- public getCrossSigningKeys(
- txn: IDBTransaction,
- func: (keys: Record<string, ICrossSigningKey> | null) => void,
- ): void {
- this.backend!.getCrossSigningKeys(txn, func);
- }
-
- /**
- * @param txn - An active transaction. See doTxn().
- * @param func - Called with the private key
- * @param type - A key type
- */
- public getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
- txn: IDBTransaction,
- func: (key: SecretStorePrivateKeys[K] | null) => void,
- type: K,
- ): void {
- this.backend!.getSecretStorePrivateKey(txn, func, type);
- }
-
- /**
- * Write the cross-signing keys back to the store
- *
- * @param txn - An active transaction. See doTxn().
- * @param keys - keys object as getCrossSigningKeys()
- */
- public storeCrossSigningKeys(txn: IDBTransaction, keys: Record<string, ICrossSigningKey>): void {
- this.backend!.storeCrossSigningKeys(txn, keys);
- }
-
- /**
- * Write the cross-signing private keys back to the store
- *
- * @param txn - An active transaction. See doTxn().
- * @param type - The type of cross-signing private key to store
- * @param key - keys object as getCrossSigningKeys()
- */
- public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
- txn: IDBTransaction,
- type: K,
- key: SecretStorePrivateKeys[K],
- ): void {
- this.backend!.storeSecretStorePrivateKey(txn, type, key);
- }
-
- // Olm sessions
-
- /**
- * Returns the number of end-to-end sessions in the store
- * @param txn - An active transaction. See doTxn().
- * @param func - Called with the count of sessions
- */
- public countEndToEndSessions(txn: IDBTransaction, func: (count: number) => void): void {
- this.backend!.countEndToEndSessions(txn, func);
- }
-
- /**
- * Retrieve a specific end-to-end session between the logged-in user
- * and another device.
- * @param deviceKey - The public key of the other device.
- * @param sessionId - The ID of the session to retrieve
- * @param txn - An active transaction. See doTxn().
- * @param func - Called with A map from sessionId
- * to session information object with 'session' key being the
- * Base64 end-to-end session and lastReceivedMessageTs being the
- * timestamp in milliseconds at which the session last received
- * a message.
- */
- public getEndToEndSession(
- deviceKey: string,
- sessionId: string,
- txn: IDBTransaction,
- func: (session: ISessionInfo | null) => void,
- ): void {
- this.backend!.getEndToEndSession(deviceKey, sessionId, txn, func);
- }
-
- /**
- * Retrieve the end-to-end sessions between the logged-in user and another
- * device.
- * @param deviceKey - The public key of the other device.
- * @param txn - An active transaction. See doTxn().
- * @param func - Called with A map from sessionId
- * to session information object with 'session' key being the
- * Base64 end-to-end session and lastReceivedMessageTs being the
- * timestamp in milliseconds at which the session last received
- * a message.
- */
- public getEndToEndSessions(
- deviceKey: string,
- txn: IDBTransaction,
- func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
- ): void {
- this.backend!.getEndToEndSessions(deviceKey, txn, func);
- }
-
- /**
- * Retrieve all end-to-end sessions
- * @param txn - An active transaction. See doTxn().
- * @param func - Called one for each session with
- * an object with, deviceKey, lastReceivedMessageTs, sessionId
- * and session keys.
- */
- public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo | null) => void): void {
- this.backend!.getAllEndToEndSessions(txn, func);
- }
-
- /**
- * Store a session between the logged-in user and another device
- * @param deviceKey - The public key of the other device.
- * @param sessionId - The ID for this end-to-end session.
- * @param sessionInfo - Session information object
- * @param txn - An active transaction. See doTxn().
- */
- public storeEndToEndSession(
- deviceKey: string,
- sessionId: string,
- sessionInfo: ISessionInfo,
- txn: IDBTransaction,
- ): void {
- this.backend!.storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn);
- }
-
- public storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
- return this.backend!.storeEndToEndSessionProblem(deviceKey, type, fixed);
- }
-
- public getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
- return this.backend!.getEndToEndSessionProblem(deviceKey, timestamp);
- }
-
- public filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
- return this.backend!.filterOutNotifiedErrorDevices(devices);
- }
-
- // Inbound group sessions
-
- /**
- * Retrieve the end-to-end inbound group session for a given
- * server key and session ID
- * @param senderCurve25519Key - The sender's curve 25519 key
- * @param sessionId - The ID of the session
- * @param txn - An active transaction. See doTxn().
- * @param func - Called with A map from sessionId
- * to Base64 end-to-end session.
- */
- public getEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- txn: IDBTransaction,
- func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
- ): void {
- this.backend!.getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func);
- }
-
- /**
- * Fetches all inbound group sessions in the store
- * @param txn - An active transaction. See doTxn().
- * @param func - Called once for each group session
- * in the store with an object having keys `{senderKey, sessionId, sessionData}`,
- * then once with null to indicate the end of the list.
- */
- public getAllEndToEndInboundGroupSessions(txn: IDBTransaction, func: (session: ISession | null) => void): void {
- this.backend!.getAllEndToEndInboundGroupSessions(txn, func);
- }
-
- /**
- * Adds an end-to-end inbound group session to the store.
- * If there already exists an inbound group session with the same
- * senderCurve25519Key and sessionID, the session will not be added.
- * @param senderCurve25519Key - The sender's curve 25519 key
- * @param sessionId - The ID of the session
- * @param sessionData - The session data structure
- * @param txn - An active transaction. See doTxn().
- */
- public addEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: InboundGroupSessionData,
- txn: IDBTransaction,
- ): void {
- this.backend!.addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
- }
-
- /**
- * Writes an end-to-end inbound group session to the store.
- * If there already exists an inbound group session with the same
- * senderCurve25519Key and sessionID, it will be overwritten.
- * @param senderCurve25519Key - The sender's curve 25519 key
- * @param sessionId - The ID of the session
- * @param sessionData - The session data structure
- * @param txn - An active transaction. See doTxn().
- */
- public storeEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: InboundGroupSessionData,
- txn: IDBTransaction,
- ): void {
- this.backend!.storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
- }
-
- public storeEndToEndInboundGroupSessionWithheld(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: IWithheld,
- txn: IDBTransaction,
- ): void {
- this.backend!.storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn);
- }
-
- // End-to-end device tracking
-
- /**
- * Store the state of all tracked devices
- * This contains devices for each user, a tracking state for each user
- * and a sync token matching the point in time the snapshot represents.
- * These all need to be written out in full each time such that the snapshot
- * is always consistent, so they are stored in one object.
- *
- * @param txn - An active transaction. See doTxn().
- */
- public storeEndToEndDeviceData(deviceData: IDeviceData, txn: IDBTransaction): void {
- this.backend!.storeEndToEndDeviceData(deviceData, txn);
- }
-
- /**
- * Get the state of all tracked devices
- *
- * @param txn - An active transaction. See doTxn().
- * @param func - Function called with the
- * device data
- */
- public getEndToEndDeviceData(txn: IDBTransaction, func: (deviceData: IDeviceData | null) => void): void {
- this.backend!.getEndToEndDeviceData(txn, func);
- }
-
- // End to End Rooms
-
- /**
- * Store the end-to-end state for a room.
- * @param roomId - The room's ID.
- * @param roomInfo - The end-to-end info for the room.
- * @param txn - An active transaction. See doTxn().
- */
- public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: IDBTransaction): void {
- this.backend!.storeEndToEndRoom(roomId, roomInfo, txn);
- }
-
- /**
- * Get an object of `roomId->roomInfo` for all e2e rooms in the store
- * @param txn - An active transaction. See doTxn().
- * @param func - Function called with the end-to-end encrypted rooms
- */
- public getEndToEndRooms(txn: IDBTransaction, func: (rooms: Record<string, IRoomEncryption>) => void): void {
- this.backend!.getEndToEndRooms(txn, func);
- }
-
- // session backups
-
- /**
- * Get the inbound group sessions that need to be backed up.
- * @param limit - The maximum number of sessions to retrieve. 0
- * for no limit.
- * @returns resolves to an array of inbound group sessions
- */
- public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
- return this.backend!.getSessionsNeedingBackup(limit);
- }
-
- /**
- * Count the inbound group sessions that need to be backed up.
- * @param txn - An active transaction. See doTxn(). (optional)
- * @returns resolves to the number of sessions
- */
- public countSessionsNeedingBackup(txn?: IDBTransaction): Promise<number> {
- return this.backend!.countSessionsNeedingBackup(txn);
- }
-
- /**
- * Unmark sessions as needing to be backed up.
- * @param sessions - The sessions that need to be backed up.
- * @param txn - An active transaction. See doTxn(). (optional)
- * @returns resolves when the sessions are unmarked
- */
- public unmarkSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
- return this.backend!.unmarkSessionsNeedingBackup(sessions, txn);
- }
-
- /**
- * Mark sessions as needing to be backed up.
- * @param sessions - The sessions that need to be backed up.
- * @param txn - An active transaction. See doTxn(). (optional)
- * @returns resolves when the sessions are marked
- */
- public markSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
- return this.backend!.markSessionsNeedingBackup(sessions, txn);
- }
-
- /**
- * Add a shared-history group session for a room.
- * @param roomId - The room that the key belongs to
- * @param senderKey - The sender's curve 25519 key
- * @param sessionId - The ID of the session
- * @param txn - An active transaction. See doTxn(). (optional)
- */
- public addSharedHistoryInboundGroupSession(
- roomId: string,
- senderKey: string,
- sessionId: string,
- txn?: IDBTransaction,
- ): void {
- this.backend!.addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn);
- }
-
- /**
- * Get the shared-history group session for a room.
- * @param roomId - The room that the key belongs to
- * @param txn - An active transaction. See doTxn(). (optional)
- * @returns Promise which resolves to an array of [senderKey, sessionId]
- */
- public getSharedHistoryInboundGroupSessions(
- roomId: string,
- txn?: IDBTransaction,
- ): Promise<[senderKey: string, sessionId: string][]> {
- return this.backend!.getSharedHistoryInboundGroupSessions(roomId, txn);
- }
-
- /**
- * Park a shared-history group session for a room we may be invited to later.
- */
- public addParkedSharedHistory(roomId: string, parkedData: ParkedSharedHistory, txn?: IDBTransaction): void {
- this.backend!.addParkedSharedHistory(roomId, parkedData, txn);
- }
-
- /**
- * Pop out all shared-history group sessions for a room.
- */
- public takeParkedSharedHistory(roomId: string, txn?: IDBTransaction): Promise<ParkedSharedHistory[]> {
- return this.backend!.takeParkedSharedHistory(roomId, txn);
- }
-
- /**
- * Perform a transaction on the crypto store. Any store methods
- * that require a transaction (txn) object to be passed in may
- * only be called within a callback of either this function or
- * one of the store functions operating on the same transaction.
- *
- * @param mode - 'readwrite' if you need to call setter
- * functions with this transaction. Otherwise, 'readonly'.
- * @param stores - List IndexedDBCryptoStore.STORE_*
- * options representing all types of object that will be
- * accessed or written to with this transaction.
- * @param func - Function called with the
- * transaction object: an opaque object that should be passed
- * to store functions.
- * @param log - A possibly customised log
- * @returns Promise that resolves with the result of the `func`
- * when the transaction is complete. If the backend is
- * async (ie. the indexeddb backend) any of the callback
- * functions throwing an exception will cause this promise to
- * reject with that exception. On synchronous backends, the
- * exception will propagate to the caller of the getFoo method.
- */
- public doTxn<T>(
- mode: Mode,
- stores: Iterable<string>,
- func: (txn: IDBTransaction) => T,
- log?: PrefixedLogger,
- ): Promise<T> {
- return this.backend!.doTxn<T>(mode, stores, func as (txn: unknown) => T, log);
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/localStorage-crypto-store.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/localStorage-crypto-store.ts
deleted file mode 100644
index 5552540..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/localStorage-crypto-store.ts
+++ /dev/null
@@ -1,403 +0,0 @@
-/*
-Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { logger } from "../../logger";
-import { MemoryCryptoStore } from "./memory-crypto-store";
-import { IDeviceData, IProblem, ISession, ISessionInfo, IWithheld, Mode, SecretStorePrivateKeys } from "./base";
-import { IOlmDevice } from "../algorithms/megolm";
-import { IRoomEncryption } from "../RoomList";
-import { ICrossSigningKey } from "../../client";
-import { InboundGroupSessionData } from "../OlmDevice";
-import { safeSet } from "../../utils";
-
-/**
- * Internal module. Partial localStorage backed storage for e2e.
- * This is not a full crypto store, just the in-memory store with
- * some things backed by localStorage. It exists because indexedDB
- * is broken in Firefox private mode or set to, "will not remember
- * history".
- */
-
-const E2E_PREFIX = "crypto.";
-const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
-const KEY_CROSS_SIGNING_KEYS = E2E_PREFIX + "cross_signing_keys";
-const KEY_NOTIFIED_ERROR_DEVICES = E2E_PREFIX + "notified_error_devices";
-const KEY_DEVICE_DATA = E2E_PREFIX + "device_data";
-const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/";
-const KEY_INBOUND_SESSION_WITHHELD_PREFIX = E2E_PREFIX + "inboundgroupsessions.withheld/";
-const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/";
-const KEY_SESSIONS_NEEDING_BACKUP = E2E_PREFIX + "sessionsneedingbackup";
-
-function keyEndToEndSessions(deviceKey: string): string {
- return E2E_PREFIX + "sessions/" + deviceKey;
-}
-
-function keyEndToEndSessionProblems(deviceKey: string): string {
- return E2E_PREFIX + "session.problems/" + deviceKey;
-}
-
-function keyEndToEndInboundGroupSession(senderKey: string, sessionId: string): string {
- return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId;
-}
-
-function keyEndToEndInboundGroupSessionWithheld(senderKey: string, sessionId: string): string {
- return KEY_INBOUND_SESSION_WITHHELD_PREFIX + senderKey + "/" + sessionId;
-}
-
-function keyEndToEndRoomsPrefix(roomId: string): string {
- return KEY_ROOMS_PREFIX + roomId;
-}
-
-export class LocalStorageCryptoStore extends MemoryCryptoStore {
- public static exists(store: Storage): boolean {
- const length = store.length;
- for (let i = 0; i < length; i++) {
- if (store.key(i)?.startsWith(E2E_PREFIX)) {
- return true;
- }
- }
- return false;
- }
-
- public constructor(private readonly store: Storage) {
- super();
- }
-
- // Olm Sessions
-
- public countEndToEndSessions(txn: unknown, func: (count: number) => void): void {
- let count = 0;
- for (let i = 0; i < this.store.length; ++i) {
- if (this.store.key(i)?.startsWith(keyEndToEndSessions(""))) ++count;
- }
- func(count);
- }
-
- // eslint-disable-next-line @typescript-eslint/naming-convention
- private _getEndToEndSessions(deviceKey: string): Record<string, ISessionInfo> {
- const sessions = getJsonItem(this.store, keyEndToEndSessions(deviceKey));
- const fixedSessions: Record<string, ISessionInfo> = {};
-
- // fix up any old sessions to be objects rather than just the base64 pickle
- for (const [sid, val] of Object.entries(sessions || {})) {
- if (typeof val === "string") {
- fixedSessions[sid] = {
- session: val,
- };
- } else {
- fixedSessions[sid] = val;
- }
- }
-
- return fixedSessions;
- }
-
- public getEndToEndSession(
- deviceKey: string,
- sessionId: string,
- txn: unknown,
- func: (session: ISessionInfo) => void,
- ): void {
- const sessions = this._getEndToEndSessions(deviceKey);
- func(sessions[sessionId] || {});
- }
-
- public getEndToEndSessions(
- deviceKey: string,
- txn: unknown,
- func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
- ): void {
- func(this._getEndToEndSessions(deviceKey) || {});
- }
-
- public getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void {
- for (let i = 0; i < this.store.length; ++i) {
- if (this.store.key(i)?.startsWith(keyEndToEndSessions(""))) {
- const deviceKey = this.store.key(i)!.split("/")[1];
- for (const sess of Object.values(this._getEndToEndSessions(deviceKey))) {
- func(sess);
- }
- }
- }
- }
-
- public storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void {
- const sessions = this._getEndToEndSessions(deviceKey) || {};
- sessions[sessionId] = sessionInfo;
- setJsonItem(this.store, keyEndToEndSessions(deviceKey), sessions);
- }
-
- public async storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
- const key = keyEndToEndSessionProblems(deviceKey);
- const problems = getJsonItem<IProblem[]>(this.store, key) || [];
- problems.push({ type, fixed, time: Date.now() });
- problems.sort((a, b) => {
- return a.time - b.time;
- });
- setJsonItem(this.store, key, problems);
- }
-
- public async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
- const key = keyEndToEndSessionProblems(deviceKey);
- const problems = getJsonItem<IProblem[]>(this.store, key) || [];
- if (!problems.length) {
- return null;
- }
- const lastProblem = problems[problems.length - 1];
- for (const problem of problems) {
- if (problem.time > timestamp) {
- return Object.assign({}, problem, { fixed: lastProblem.fixed });
- }
- }
- if (lastProblem.fixed) {
- return null;
- } else {
- return lastProblem;
- }
- }
-
- public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
- const notifiedErrorDevices =
- getJsonItem<MemoryCryptoStore["notifiedErrorDevices"]>(this.store, KEY_NOTIFIED_ERROR_DEVICES) || {};
- const ret: IOlmDevice[] = [];
-
- for (const device of devices) {
- const { userId, deviceInfo } = device;
- if (userId in notifiedErrorDevices) {
- if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) {
- ret.push(device);
- safeSet(notifiedErrorDevices[userId], deviceInfo.deviceId, true);
- }
- } else {
- ret.push(device);
- safeSet(notifiedErrorDevices, userId, { [deviceInfo.deviceId]: true });
- }
- }
-
- setJsonItem(this.store, KEY_NOTIFIED_ERROR_DEVICES, notifiedErrorDevices);
-
- return ret;
- }
-
- // Inbound Group Sessions
-
- public getEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- txn: unknown,
- func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
- ): void {
- func(
- getJsonItem(this.store, keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId)),
- getJsonItem(this.store, keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId)),
- );
- }
-
- public getAllEndToEndInboundGroupSessions(txn: unknown, func: (session: ISession | null) => void): void {
- for (let i = 0; i < this.store.length; ++i) {
- const key = this.store.key(i);
- if (key?.startsWith(KEY_INBOUND_SESSION_PREFIX)) {
- // we can't use split, as the components we are trying to split out
- // might themselves contain '/' characters. We rely on the
- // senderKey being a (32-byte) curve25519 key, base64-encoded
- // (hence 43 characters long).
-
- func({
- 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)!,
- });
- }
- }
- func(null);
- }
-
- public addEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: InboundGroupSessionData,
- txn: unknown,
- ): void {
- const existing = getJsonItem(this.store, keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId));
- if (!existing) {
- this.storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
- }
- }
-
- public storeEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: InboundGroupSessionData,
- txn: unknown,
- ): void {
- setJsonItem(this.store, keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId), sessionData);
- }
-
- public storeEndToEndInboundGroupSessionWithheld(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: IWithheld,
- txn: unknown,
- ): void {
- setJsonItem(this.store, keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId), sessionData);
- }
-
- public getEndToEndDeviceData(txn: unknown, func: (deviceData: IDeviceData | null) => void): void {
- func(getJsonItem(this.store, KEY_DEVICE_DATA));
- }
-
- public storeEndToEndDeviceData(deviceData: IDeviceData, txn: unknown): void {
- setJsonItem(this.store, KEY_DEVICE_DATA, deviceData);
- }
-
- public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: unknown): void {
- setJsonItem(this.store, keyEndToEndRoomsPrefix(roomId), roomInfo);
- }
-
- public getEndToEndRooms(txn: unknown, func: (rooms: Record<string, IRoomEncryption>) => void): void {
- const result: Record<string, IRoomEncryption> = {};
- const prefix = keyEndToEndRoomsPrefix("");
-
- for (let i = 0; i < this.store.length; ++i) {
- const key = this.store.key(i);
- if (key?.startsWith(prefix)) {
- const roomId = key.slice(prefix.length);
- result[roomId] = getJsonItem(this.store, key)!;
- }
- }
- func(result);
- }
-
- public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
- const sessionsNeedingBackup = getJsonItem<string[]>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
- const sessions: ISession[] = [];
-
- for (const session in sessionsNeedingBackup) {
- if (Object.prototype.hasOwnProperty.call(sessionsNeedingBackup, session)) {
- // see getAllEndToEndInboundGroupSessions for the magic number explanations
- const senderKey = session.slice(0, 43);
- const sessionId = session.slice(44);
- this.getEndToEndInboundGroupSession(senderKey, sessionId, null, (sessionData) => {
- sessions.push({
- senderKey: senderKey,
- sessionId: sessionId,
- sessionData: sessionData!,
- });
- });
- if (limit && sessions.length >= limit) {
- break;
- }
- }
- }
- return Promise.resolve(sessions);
- }
-
- public countSessionsNeedingBackup(): Promise<number> {
- const sessionsNeedingBackup = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
- return Promise.resolve(Object.keys(sessionsNeedingBackup).length);
- }
-
- public unmarkSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
- const sessionsNeedingBackup =
- getJsonItem<{
- [senderKeySessionId: string]: string;
- }>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
- for (const session of sessions) {
- delete sessionsNeedingBackup[session.senderKey + "/" + session.sessionId];
- }
- setJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup);
- return Promise.resolve();
- }
-
- public markSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
- const sessionsNeedingBackup =
- getJsonItem<{
- [senderKeySessionId: string]: boolean;
- }>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
- for (const session of sessions) {
- sessionsNeedingBackup[session.senderKey + "/" + session.sessionId] = true;
- }
- setJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup);
- return Promise.resolve();
- }
-
- /**
- * Delete all data from this store.
- *
- * @returns Promise which resolves when the store has been cleared.
- */
- public deleteAllData(): Promise<void> {
- this.store.removeItem(KEY_END_TO_END_ACCOUNT);
- return Promise.resolve();
- }
-
- // Olm account
-
- public getAccount(txn: unknown, func: (accountPickle: string | null) => void): void {
- const accountPickle = getJsonItem<string>(this.store, KEY_END_TO_END_ACCOUNT);
- func(accountPickle);
- }
-
- public storeAccount(txn: unknown, accountPickle: string): void {
- setJsonItem(this.store, KEY_END_TO_END_ACCOUNT, accountPickle);
- }
-
- public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void {
- const keys = getJsonItem<Record<string, ICrossSigningKey>>(this.store, KEY_CROSS_SIGNING_KEYS);
- func(keys);
- }
-
- public getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
- txn: unknown,
- func: (key: SecretStorePrivateKeys[K] | null) => void,
- type: K,
- ): void {
- const key = getJsonItem<SecretStorePrivateKeys[K]>(this.store, E2E_PREFIX + `ssss_cache.${type}`);
- func(key);
- }
-
- public storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void {
- setJsonItem(this.store, KEY_CROSS_SIGNING_KEYS, keys);
- }
-
- public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
- txn: unknown,
- type: K,
- key: SecretStorePrivateKeys[K],
- ): void {
- setJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`, key);
- }
-
- public doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T): Promise<T> {
- return Promise.resolve(func(null));
- }
-}
-
-function getJsonItem<T>(store: Storage, key: string): T | null {
- try {
- // if the key is absent, store.getItem() returns null, and
- // JSON.parse(null) === null, so this returns null.
- return JSON.parse(store.getItem(key)!);
- } catch (e) {
- logger.log("Error: Failed to get key %s: %s", key, (<Error>e).message);
- logger.log((<Error>e).stack);
- }
- return null;
-}
-
-function setJsonItem<T>(store: Storage, key: string, val: T): void {
- store.setItem(key, JSON.stringify(val));
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/memory-crypto-store.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/memory-crypto-store.ts
deleted file mode 100644
index 29ae81b..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/store/memory-crypto-store.ts
+++ /dev/null
@@ -1,533 +0,0 @@
-/*
-Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { logger } from "../../logger";
-import * as utils from "../../utils";
-import {
- CryptoStore,
- IDeviceData,
- IProblem,
- ISession,
- ISessionInfo,
- IWithheld,
- Mode,
- OutgoingRoomKeyRequest,
- ParkedSharedHistory,
- SecretStorePrivateKeys,
-} from "./base";
-import { IRoomKeyRequestBody } from "../index";
-import { ICrossSigningKey } from "../../client";
-import { IOlmDevice } from "../algorithms/megolm";
-import { IRoomEncryption } from "../RoomList";
-import { InboundGroupSessionData } from "../OlmDevice";
-import { safeSet } from "../../utils";
-
-/**
- * Internal module. in-memory storage for e2e.
- */
-
-export class MemoryCryptoStore implements CryptoStore {
- private outgoingRoomKeyRequests: OutgoingRoomKeyRequest[] = [];
- private account: string | null = null;
- private crossSigningKeys: Record<string, ICrossSigningKey> | null = null;
- private privateKeys: Partial<SecretStorePrivateKeys> = {};
-
- private sessions: { [deviceKey: string]: { [sessionId: string]: ISessionInfo } } = {};
- private sessionProblems: { [deviceKey: string]: IProblem[] } = {};
- private notifiedErrorDevices: { [userId: string]: { [deviceId: string]: boolean } } = {};
- private inboundGroupSessions: { [sessionKey: string]: InboundGroupSessionData } = {};
- private inboundGroupSessionsWithheld: Record<string, IWithheld> = {};
- // Opaque device data object
- private deviceData: IDeviceData | null = null;
- private rooms: { [roomId: string]: IRoomEncryption } = {};
- private sessionsNeedingBackup: { [sessionKey: string]: boolean } = {};
- private sharedHistoryInboundGroupSessions: { [roomId: string]: [senderKey: string, sessionId: string][] } = {};
- private parkedSharedHistory = new Map<string, ParkedSharedHistory[]>(); // keyed by room ID
-
- /**
- * Ensure the database exists and is up-to-date.
- *
- * This must be called before the store can be used.
- *
- * @returns resolves to the store.
- */
- public async startup(): Promise<CryptoStore> {
- // No startup work to do for the memory store.
- return this;
- }
-
- /**
- * Delete all data from this store.
- *
- * @returns Promise which resolves when the store has been cleared.
- */
- public deleteAllData(): Promise<void> {
- return Promise.resolve();
- }
-
- /**
- * Look for an existing outgoing room key request, and if none is found,
- * add a new one
- *
- *
- * @returns resolves to
- * {@link OutgoingRoomKeyRequest}: either the
- * same instance as passed in, or the existing one.
- */
- public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> {
- const requestBody = request.requestBody;
-
- return utils.promiseTry(() => {
- // first see if we already have an entry for this request.
- const existing = this._getOutgoingRoomKeyRequest(requestBody);
-
- if (existing) {
- // this entry matches the request - return it.
- logger.log(
- `already have key request outstanding for ` +
- `${requestBody.room_id} / ${requestBody.session_id}: ` +
- `not sending another`,
- );
- return existing;
- }
-
- // we got to the end of the list without finding a match
- // - add the new request.
- logger.log(`enqueueing key request for ${requestBody.room_id} / ` + requestBody.session_id);
- this.outgoingRoomKeyRequests.push(request);
- return request;
- });
- }
-
- /**
- * Look for an existing room key request
- *
- * @param requestBody - existing request to look for
- *
- * @returns resolves to the matching
- * {@link OutgoingRoomKeyRequest}, or null if
- * not found
- */
- public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null> {
- return Promise.resolve(this._getOutgoingRoomKeyRequest(requestBody));
- }
-
- /**
- * Looks for existing room key request, and returns the result synchronously.
- *
- * @internal
- *
- * @param requestBody - existing request to look for
- *
- * @returns
- * the matching request, or null if not found
- */
- // eslint-disable-next-line @typescript-eslint/naming-convention
- private _getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): OutgoingRoomKeyRequest | null {
- for (const existing of this.outgoingRoomKeyRequests) {
- if (utils.deepCompare(existing.requestBody, requestBody)) {
- return existing;
- }
- }
- return null;
- }
-
- /**
- * Look for room key requests by state
- *
- * @param wantedStates - list of acceptable states
- *
- * @returns resolves to the a
- * {@link OutgoingRoomKeyRequest}, or null if
- * there are no pending requests in those states
- */
- public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null> {
- for (const req of this.outgoingRoomKeyRequests) {
- for (const state of wantedStates) {
- if (req.state === state) {
- return Promise.resolve(req);
- }
- }
- }
- return Promise.resolve(null);
- }
-
- /**
- *
- * @returns All OutgoingRoomKeyRequests in state
- */
- public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]> {
- return Promise.resolve(this.outgoingRoomKeyRequests.filter((r) => r.state == wantedState));
- }
-
- public getOutgoingRoomKeyRequestsByTarget(
- userId: string,
- deviceId: string,
- wantedStates: number[],
- ): Promise<OutgoingRoomKeyRequest[]> {
- const results: OutgoingRoomKeyRequest[] = [];
-
- for (const req of this.outgoingRoomKeyRequests) {
- for (const state of wantedStates) {
- if (
- req.state === state &&
- req.recipients.some((recipient) => recipient.userId === userId && recipient.deviceId === deviceId)
- ) {
- results.push(req);
- }
- }
- }
- return Promise.resolve(results);
- }
-
- /**
- * Look for an existing room key request by id and state, and update it if
- * found
- *
- * @param requestId - ID of request to update
- * @param expectedState - state we expect to find the request in
- * @param updates - name/value map of updates to apply
- *
- * @returns resolves to
- * {@link OutgoingRoomKeyRequest}
- * updated request, or null if no matching row was found
- */
- public updateOutgoingRoomKeyRequest(
- requestId: string,
- expectedState: number,
- updates: Partial<OutgoingRoomKeyRequest>,
- ): Promise<OutgoingRoomKeyRequest | null> {
- for (const req of this.outgoingRoomKeyRequests) {
- if (req.requestId !== requestId) {
- continue;
- }
-
- if (req.state !== expectedState) {
- logger.warn(
- `Cannot update room key request from ${expectedState} ` +
- `as it was already updated to ${req.state}`,
- );
- return Promise.resolve(null);
- }
- Object.assign(req, updates);
- return Promise.resolve(req);
- }
-
- return Promise.resolve(null);
- }
-
- /**
- * Look for an existing room key request by id and state, and delete it if
- * found
- *
- * @param requestId - ID of request to update
- * @param expectedState - state we expect to find the request in
- *
- * @returns resolves once the operation is completed
- */
- public deleteOutgoingRoomKeyRequest(
- requestId: string,
- expectedState: number,
- ): Promise<OutgoingRoomKeyRequest | null> {
- for (let i = 0; i < this.outgoingRoomKeyRequests.length; i++) {
- const req = this.outgoingRoomKeyRequests[i];
-
- if (req.requestId !== requestId) {
- continue;
- }
-
- if (req.state != expectedState) {
- logger.warn(`Cannot delete room key request in state ${req.state} ` + `(expected ${expectedState})`);
- return Promise.resolve(null);
- }
-
- this.outgoingRoomKeyRequests.splice(i, 1);
- return Promise.resolve(req);
- }
-
- return Promise.resolve(null);
- }
-
- // Olm Account
-
- public getAccount(txn: unknown, func: (accountPickle: string | null) => void): void {
- func(this.account);
- }
-
- public storeAccount(txn: unknown, accountPickle: string): void {
- this.account = accountPickle;
- }
-
- public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void {
- func(this.crossSigningKeys);
- }
-
- public getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
- txn: unknown,
- func: (key: SecretStorePrivateKeys[K] | null) => void,
- type: K,
- ): void {
- const result = this.privateKeys[type] as SecretStorePrivateKeys[K] | undefined;
- func(result || null);
- }
-
- public storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void {
- this.crossSigningKeys = keys;
- }
-
- public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
- txn: unknown,
- type: K,
- key: SecretStorePrivateKeys[K],
- ): void {
- this.privateKeys[type] = key;
- }
-
- // Olm Sessions
-
- public countEndToEndSessions(txn: unknown, func: (count: number) => void): void {
- func(Object.keys(this.sessions).length);
- }
-
- public getEndToEndSession(
- deviceKey: string,
- sessionId: string,
- txn: unknown,
- func: (session: ISessionInfo) => void,
- ): void {
- const deviceSessions = this.sessions[deviceKey] || {};
- func(deviceSessions[sessionId] || null);
- }
-
- public getEndToEndSessions(
- deviceKey: string,
- txn: unknown,
- func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
- ): void {
- func(this.sessions[deviceKey] || {});
- }
-
- public getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void {
- Object.entries(this.sessions).forEach(([deviceKey, deviceSessions]) => {
- Object.entries(deviceSessions).forEach(([sessionId, session]) => {
- func({
- ...session,
- deviceKey,
- sessionId,
- });
- });
- });
- }
-
- public storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void {
- let deviceSessions = this.sessions[deviceKey];
- if (deviceSessions === undefined) {
- deviceSessions = {};
- this.sessions[deviceKey] = deviceSessions;
- }
- deviceSessions[sessionId] = sessionInfo;
- }
-
- public async storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
- const problems = (this.sessionProblems[deviceKey] = this.sessionProblems[deviceKey] || []);
- problems.push({ type, fixed, time: Date.now() });
- problems.sort((a, b) => {
- return a.time - b.time;
- });
- }
-
- public async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
- const problems = this.sessionProblems[deviceKey] || [];
- if (!problems.length) {
- return null;
- }
- const lastProblem = problems[problems.length - 1];
- for (const problem of problems) {
- if (problem.time > timestamp) {
- return Object.assign({}, problem, { fixed: lastProblem.fixed });
- }
- }
- if (lastProblem.fixed) {
- return null;
- } else {
- return lastProblem;
- }
- }
-
- public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
- const notifiedErrorDevices = this.notifiedErrorDevices;
- const ret: IOlmDevice[] = [];
-
- for (const device of devices) {
- const { userId, deviceInfo } = device;
- if (userId in notifiedErrorDevices) {
- if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) {
- ret.push(device);
- safeSet(notifiedErrorDevices[userId], deviceInfo.deviceId, true);
- }
- } else {
- ret.push(device);
- safeSet(notifiedErrorDevices, userId, { [deviceInfo.deviceId]: true });
- }
- }
-
- return ret;
- }
-
- // Inbound Group Sessions
-
- public getEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- txn: unknown,
- func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
- ): void {
- const k = senderCurve25519Key + "/" + sessionId;
- func(this.inboundGroupSessions[k] || null, this.inboundGroupSessionsWithheld[k] || null);
- }
-
- public getAllEndToEndInboundGroupSessions(txn: unknown, func: (session: ISession | null) => void): void {
- for (const key of Object.keys(this.inboundGroupSessions)) {
- // we can't use split, as the components we are trying to split out
- // might themselves contain '/' characters. We rely on the
- // senderKey being a (32-byte) curve25519 key, base64-encoded
- // (hence 43 characters long).
-
- func({
- senderKey: key.slice(0, 43),
- sessionId: key.slice(44),
- sessionData: this.inboundGroupSessions[key],
- });
- }
- func(null);
- }
-
- public addEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: InboundGroupSessionData,
- txn: unknown,
- ): void {
- const k = senderCurve25519Key + "/" + sessionId;
- if (this.inboundGroupSessions[k] === undefined) {
- this.inboundGroupSessions[k] = sessionData;
- }
- }
-
- public storeEndToEndInboundGroupSession(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: InboundGroupSessionData,
- txn: unknown,
- ): void {
- this.inboundGroupSessions[senderCurve25519Key + "/" + sessionId] = sessionData;
- }
-
- public storeEndToEndInboundGroupSessionWithheld(
- senderCurve25519Key: string,
- sessionId: string,
- sessionData: IWithheld,
- txn: unknown,
- ): void {
- const k = senderCurve25519Key + "/" + sessionId;
- this.inboundGroupSessionsWithheld[k] = sessionData;
- }
-
- // Device Data
-
- public getEndToEndDeviceData(txn: unknown, func: (deviceData: IDeviceData | null) => void): void {
- func(this.deviceData);
- }
-
- public storeEndToEndDeviceData(deviceData: IDeviceData, txn: unknown): void {
- this.deviceData = deviceData;
- }
-
- // E2E rooms
-
- public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: unknown): void {
- this.rooms[roomId] = roomInfo;
- }
-
- public getEndToEndRooms(txn: unknown, func: (rooms: Record<string, IRoomEncryption>) => void): void {
- func(this.rooms);
- }
-
- public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
- const sessions: ISession[] = [];
- for (const session in this.sessionsNeedingBackup) {
- if (this.inboundGroupSessions[session]) {
- sessions.push({
- senderKey: session.slice(0, 43),
- sessionId: session.slice(44),
- sessionData: this.inboundGroupSessions[session],
- });
- if (limit && session.length >= limit) {
- break;
- }
- }
- }
- return Promise.resolve(sessions);
- }
-
- public countSessionsNeedingBackup(): Promise<number> {
- return Promise.resolve(Object.keys(this.sessionsNeedingBackup).length);
- }
-
- public unmarkSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
- for (const session of sessions) {
- const sessionKey = session.senderKey + "/" + session.sessionId;
- delete this.sessionsNeedingBackup[sessionKey];
- }
- return Promise.resolve();
- }
-
- public markSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
- for (const session of sessions) {
- const sessionKey = session.senderKey + "/" + session.sessionId;
- this.sessionsNeedingBackup[sessionKey] = true;
- }
- return Promise.resolve();
- }
-
- public addSharedHistoryInboundGroupSession(roomId: string, senderKey: string, sessionId: string): void {
- const sessions = this.sharedHistoryInboundGroupSessions[roomId] || [];
- sessions.push([senderKey, sessionId]);
- this.sharedHistoryInboundGroupSessions[roomId] = sessions;
- }
-
- public getSharedHistoryInboundGroupSessions(roomId: string): Promise<[senderKey: string, sessionId: string][]> {
- return Promise.resolve(this.sharedHistoryInboundGroupSessions[roomId] || []);
- }
-
- public addParkedSharedHistory(roomId: string, parkedData: ParkedSharedHistory): void {
- const parked = this.parkedSharedHistory.get(roomId) ?? [];
- parked.push(parkedData);
- this.parkedSharedHistory.set(roomId, parked);
- }
-
- public takeParkedSharedHistory(roomId: string): Promise<ParkedSharedHistory[]> {
- const parked = this.parkedSharedHistory.get(roomId) ?? [];
- this.parkedSharedHistory.delete(roomId);
- return Promise.resolve(parked);
- }
-
- // Session key backups
-
- public doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn?: unknown) => T): Promise<T> {
- return Promise.resolve(func(null));
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/Base.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/Base.ts
deleted file mode 100644
index 89c700c..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/Base.ts
+++ /dev/null
@@ -1,369 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-Copyright 2020 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.
-*/
-
-/**
- * Base class for verification methods.
- */
-
-import { MatrixEvent } from "../../models/event";
-import { EventType } from "../../@types/event";
-import { logger } from "../../logger";
-import { DeviceInfo } from "../deviceinfo";
-import { newTimeoutError } from "./Error";
-import { KeysDuringVerification, requestKeysDuringVerification } from "../CrossSigning";
-import { IVerificationChannel } from "./request/Channel";
-import { MatrixClient } from "../../client";
-import { VerificationRequest } from "./request/VerificationRequest";
-import { ListenerMap, TypedEventEmitter } from "../../models/typed-event-emitter";
-
-const timeoutException = new Error("Verification timed out");
-
-export class SwitchStartEventError extends Error {
- public constructor(public readonly startEvent: MatrixEvent | null) {
- super();
- }
-}
-
-export type KeyVerifier = (keyId: string, device: DeviceInfo, keyInfo: string) => void;
-
-export enum VerificationEvent {
- Cancel = "cancel",
-}
-
-export type VerificationEventHandlerMap = {
- [VerificationEvent.Cancel]: (e: Error | MatrixEvent) => void;
-};
-
-export class VerificationBase<
- Events extends string,
- Arguments extends ListenerMap<Events | VerificationEvent>,
-> extends TypedEventEmitter<Events | VerificationEvent, Arguments, VerificationEventHandlerMap> {
- private cancelled = false;
- private _done = false;
- private promise: Promise<void> | null = null;
- private transactionTimeoutTimer: ReturnType<typeof setTimeout> | null = null;
- protected expectedEvent?: string;
- private resolve?: () => void;
- private reject?: (e: Error | MatrixEvent) => void;
- private resolveEvent?: (e: MatrixEvent) => void;
- private rejectEvent?: (e: Error) => void;
- private started?: boolean;
-
- /**
- * Base class for verification methods.
- *
- * <p>Once a verifier object is created, the verification can be started by
- * calling the verify() method, which will return a promise that will
- * resolve when the verification is completed, or reject if it could not
- * complete.</p>
- *
- * <p>Subclasses must have a NAME class property.</p>
- *
- * @param channel - the verification channel to send verification messages over.
- * TODO: Channel types
- *
- * @param baseApis - base matrix api interface
- *
- * @param userId - the user ID that is being verified
- *
- * @param deviceId - the device ID that is being verified
- *
- * @param startEvent - the m.key.verification.start event that
- * initiated this verification, if any
- *
- * @param request - the key verification request object related to
- * this verification, if any
- */
- public constructor(
- public readonly channel: IVerificationChannel,
- public readonly baseApis: MatrixClient,
- public readonly userId: string,
- public readonly deviceId: string,
- public startEvent: MatrixEvent | null,
- public readonly request: VerificationRequest,
- ) {
- super();
- }
-
- public get initiatedByMe(): boolean {
- // if there is no start event yet,
- // we probably want to send it,
- // which happens if we initiate
- if (!this.startEvent) {
- return true;
- }
- const sender = this.startEvent.getSender();
- const content = this.startEvent.getContent();
- return sender === this.baseApis.getUserId() && content.from_device === this.baseApis.getDeviceId();
- }
-
- public get hasBeenCancelled(): boolean {
- return this.cancelled;
- }
-
- private resetTimer(): void {
- logger.info("Refreshing/starting the verification transaction timeout timer");
- if (this.transactionTimeoutTimer !== null) {
- clearTimeout(this.transactionTimeoutTimer);
- }
- this.transactionTimeoutTimer = setTimeout(() => {
- if (!this._done && !this.cancelled) {
- logger.info("Triggering verification timeout");
- this.cancel(timeoutException);
- }
- }, 10 * 60 * 1000); // 10 minutes
- }
-
- private endTimer(): void {
- if (this.transactionTimeoutTimer !== null) {
- clearTimeout(this.transactionTimeoutTimer);
- this.transactionTimeoutTimer = null;
- }
- }
-
- protected send(type: string, uncompletedContent: Record<string, any>): Promise<void> {
- return this.channel.send(type, uncompletedContent);
- }
-
- protected waitForEvent(type: string): Promise<MatrixEvent> {
- if (this._done) {
- return Promise.reject(new Error("Verification is already done"));
- }
- const existingEvent = this.request.getEventFromOtherParty(type);
- if (existingEvent) {
- return Promise.resolve(existingEvent);
- }
-
- this.expectedEvent = type;
- return new Promise((resolve, reject) => {
- this.resolveEvent = resolve;
- this.rejectEvent = reject;
- });
- }
-
- public canSwitchStartEvent(event: MatrixEvent): boolean {
- return false;
- }
-
- public switchStartEvent(event: MatrixEvent): void {
- if (this.canSwitchStartEvent(event)) {
- logger.log("Verification Base: switching verification start event", { restartingFlow: !!this.rejectEvent });
- if (this.rejectEvent) {
- const reject = this.rejectEvent;
- this.rejectEvent = undefined;
- reject(new SwitchStartEventError(event));
- } else {
- this.startEvent = event;
- }
- }
- }
-
- public handleEvent(e: MatrixEvent): void {
- if (this._done) {
- return;
- } else if (e.getType() === this.expectedEvent) {
- // if we receive an expected m.key.verification.done, then just
- // ignore it, since we don't need to do anything about it
- if (this.expectedEvent !== EventType.KeyVerificationDone) {
- this.expectedEvent = undefined;
- this.rejectEvent = undefined;
- this.resetTimer();
- this.resolveEvent?.(e);
- }
- } else if (e.getType() === EventType.KeyVerificationCancel) {
- const reject = this.reject;
- this.reject = undefined;
- // there is only promise to reject if verify has been called
- if (reject) {
- const content = e.getContent();
- const { reason, code } = content;
- reject(new Error(`Other side cancelled verification ` + `because ${reason} (${code})`));
- }
- } else if (this.expectedEvent) {
- // only cancel if there is an event expected.
- // if there is no event expected, it means verify() wasn't called
- // and we're just replaying the timeline events when syncing
- // after a refresh when the events haven't been stored in the cache yet.
- const exception = new Error(
- "Unexpected message: expecting " + this.expectedEvent + " but got " + e.getType(),
- );
- this.expectedEvent = undefined;
- if (this.rejectEvent) {
- const reject = this.rejectEvent;
- this.rejectEvent = undefined;
- reject(exception);
- }
- this.cancel(exception);
- }
- }
-
- public async done(): Promise<KeysDuringVerification | void> {
- this.endTimer(); // always kill the activity timer
- if (!this._done) {
- this.request.onVerifierFinished();
- this.resolve?.();
- return requestKeysDuringVerification(this.baseApis, this.userId, this.deviceId);
- }
- }
-
- public cancel(e: Error | MatrixEvent): void {
- this.endTimer(); // always kill the activity timer
- if (!this._done) {
- this.cancelled = true;
- this.request.onVerifierCancelled();
- if (this.userId && this.deviceId) {
- // send a cancellation to the other user (if it wasn't
- // cancelled by the other user)
- if (e === timeoutException) {
- const timeoutEvent = newTimeoutError();
- this.send(timeoutEvent.getType(), timeoutEvent.getContent());
- } else if (e instanceof MatrixEvent) {
- const sender = e.getSender();
- if (sender !== this.userId) {
- const content = e.getContent();
- if (e.getType() === EventType.KeyVerificationCancel) {
- content.code = content.code || "m.unknown";
- content.reason = content.reason || content.body || "Unknown reason";
- this.send(EventType.KeyVerificationCancel, content);
- } else {
- this.send(EventType.KeyVerificationCancel, {
- code: "m.unknown",
- reason: content.body || "Unknown reason",
- });
- }
- }
- } else {
- this.send(EventType.KeyVerificationCancel, {
- code: "m.unknown",
- reason: e.toString(),
- });
- }
- }
- if (this.promise !== null) {
- // when we cancel without a promise, we end up with a promise
- // but no reject function. If cancel is called again, we'd error.
- if (this.reject) this.reject(e);
- } else {
- // FIXME: this causes an "Uncaught promise" console message
- // if nothing ends up chaining this promise.
- this.promise = Promise.reject(e);
- }
- // Also emit a 'cancel' event that the app can listen for to detect cancellation
- // before calling verify()
- this.emit(VerificationEvent.Cancel, e);
- }
- }
-
- /**
- * Begin the key verification
- *
- * @returns Promise which resolves when the verification has
- * completed.
- */
- public verify(): Promise<void> {
- if (this.promise) return this.promise;
-
- this.promise = new Promise((resolve, reject) => {
- this.resolve = (...args): void => {
- this._done = true;
- this.endTimer();
- resolve(...args);
- };
- this.reject = (e: Error | MatrixEvent): void => {
- this._done = true;
- this.endTimer();
- reject(e);
- };
- });
- if (this.doVerification && !this.started) {
- this.started = true;
- this.resetTimer(); // restart the timeout
- new Promise<void>((resolve, reject) => {
- const crossSignId = this.baseApis.crypto!.deviceList.getStoredCrossSigningForUser(this.userId)?.getId();
- if (crossSignId === this.deviceId) {
- reject(new Error("Device ID is the same as the cross-signing ID"));
- }
- resolve();
- })
- .then(() => this.doVerification!())
- .then(this.done.bind(this), this.cancel.bind(this));
- }
- return this.promise;
- }
-
- protected doVerification?: () => Promise<void>;
-
- protected async verifyKeys(userId: string, keys: Record<string, string>, verifier: KeyVerifier): Promise<void> {
- // we try to verify all the keys that we're told about, but we might
- // not know about all of them, so keep track of the keys that we know
- // about, and ignore the rest
- const verifiedDevices: [string, string, string][] = [];
-
- for (const [keyId, keyInfo] of Object.entries(keys)) {
- const deviceId = keyId.split(":", 2)[1];
- const device = this.baseApis.getStoredDevice(userId, deviceId);
- if (device) {
- verifier(keyId, device, keyInfo);
- verifiedDevices.push([deviceId, keyId, device.keys[keyId]]);
- } else {
- const crossSigningInfo = this.baseApis.crypto!.deviceList.getStoredCrossSigningForUser(userId);
- if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
- verifier(
- keyId,
- DeviceInfo.fromStorage(
- {
- keys: {
- [keyId]: deviceId,
- },
- },
- deviceId,
- ),
- keyInfo,
- );
- verifiedDevices.push([deviceId, keyId, deviceId]);
- } else {
- logger.warn(`verification: Could not find device ${deviceId} to verify`);
- }
- }
- }
-
- // if none of the keys could be verified, then error because the app
- // should be informed about that
- if (!verifiedDevices.length) {
- throw new Error("No devices could be verified");
- }
-
- logger.info("Verification completed! Marking devices verified: ", verifiedDevices);
- // TODO: There should probably be a batch version of this, otherwise it's going
- // to upload each signature in a separate API call which is silly because the
- // API supports as many signatures as you like.
- for (const [deviceId, keyId, key] of verifiedDevices) {
- await this.baseApis.crypto!.setDeviceVerification(userId, deviceId, true, null, null, { [keyId]: key });
- }
-
- // if one of the user's own devices is being marked as verified / unverified,
- // check the key backup status, since whether or not we use this depends on
- // whether it has a signature from a verified device
- if (userId == this.baseApis.credentials.userId) {
- await this.baseApis.checkKeyBackup();
- }
- }
-
- public get events(): string[] | undefined {
- return undefined;
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/Error.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/Error.ts
deleted file mode 100644
index da73ebb..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/Error.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
-Copyright 2018 - 2021 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.
-*/
-
-/**
- * Error messages.
- */
-
-import { MatrixEvent } from "../../models/event";
-import { EventType } from "../../@types/event";
-
-export function newVerificationError(code: string, reason: string, extraData?: Record<string, any>): MatrixEvent {
- const content = Object.assign({}, { code, reason }, extraData);
- return new MatrixEvent({
- type: EventType.KeyVerificationCancel,
- content,
- });
-}
-
-export function errorFactory(code: string, reason: string): (extraData?: Record<string, any>) => MatrixEvent {
- return function (extraData?: Record<string, any>) {
- return newVerificationError(code, reason, extraData);
- };
-}
-
-/**
- * The verification was cancelled by the user.
- */
-export const newUserCancelledError = errorFactory("m.user", "Cancelled by user");
-
-/**
- * The verification timed out.
- */
-export const newTimeoutError = errorFactory("m.timeout", "Timed out");
-
-/**
- * An unknown method was selected.
- */
-export const newUnknownMethodError = errorFactory("m.unknown_method", "Unknown method");
-
-/**
- * An unexpected message was sent.
- */
-export const newUnexpectedMessageError = errorFactory("m.unexpected_message", "Unexpected message");
-
-/**
- * The key does not match.
- */
-export const newKeyMismatchError = errorFactory("m.key_mismatch", "Key mismatch");
-
-/**
- * An invalid message was sent.
- */
-export const newInvalidMessageError = errorFactory("m.invalid_message", "Invalid message");
-
-export function errorFromEvent(event: MatrixEvent): { code: string; reason: string } {
- const content = event.getContent();
- if (content) {
- const { code, reason } = content;
- return { code, reason };
- } else {
- return { code: "Unknown error", reason: "m.unknown" };
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/IllegalMethod.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/IllegalMethod.ts
deleted file mode 100644
index c437e0c..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/IllegalMethod.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
-Copyright 2020 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.
-*/
-
-/**
- * Verification method that is illegal to have (cannot possibly
- * do verification with this method).
- */
-
-import { VerificationBase as Base, VerificationEvent, VerificationEventHandlerMap } from "./Base";
-import { IVerificationChannel } from "./request/Channel";
-import { MatrixClient } from "../../client";
-import { MatrixEvent } from "../../models/event";
-import { VerificationRequest } from "./request/VerificationRequest";
-
-export class IllegalMethod extends Base<VerificationEvent, VerificationEventHandlerMap> {
- public static factory(
- channel: IVerificationChannel,
- baseApis: MatrixClient,
- userId: string,
- deviceId: string,
- startEvent: MatrixEvent,
- request: VerificationRequest,
- ): IllegalMethod {
- return new IllegalMethod(channel, baseApis, userId, deviceId, startEvent, request);
- }
-
- // eslint-disable-next-line @typescript-eslint/naming-convention
- public static get NAME(): string {
- // Typically the name will be something else, but to complete
- // the contract we offer a default one here.
- return "org.matrix.illegal_method";
- }
-
- protected doVerification = async (): Promise<void> => {
- throw new Error("Verification is not possible with this method");
- };
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/QRCode.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/QRCode.ts
deleted file mode 100644
index bfb532e..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/QRCode.ts
+++ /dev/null
@@ -1,311 +0,0 @@
-/*
-Copyright 2018 - 2021 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.
-*/
-
-/**
- * QR code key verification.
- */
-
-import { VerificationBase as Base, VerificationEventHandlerMap } from "./Base";
-import { newKeyMismatchError, newUserCancelledError } from "./Error";
-import { decodeBase64, encodeUnpaddedBase64 } from "../olmlib";
-import { logger } from "../../logger";
-import { VerificationRequest } from "./request/VerificationRequest";
-import { MatrixClient } from "../../client";
-import { IVerificationChannel } from "./request/Channel";
-import { MatrixEvent } from "../../models/event";
-
-export const SHOW_QR_CODE_METHOD = "m.qr_code.show.v1";
-export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1";
-
-interface IReciprocateQr {
- confirm(): void;
- cancel(): void;
-}
-
-export enum QrCodeEvent {
- ShowReciprocateQr = "show_reciprocate_qr",
-}
-
-type EventHandlerMap = {
- [QrCodeEvent.ShowReciprocateQr]: (qr: IReciprocateQr) => void;
-} & VerificationEventHandlerMap;
-
-export class ReciprocateQRCode extends Base<QrCodeEvent, EventHandlerMap> {
- public reciprocateQREvent?: IReciprocateQr;
-
- public static factory(
- channel: IVerificationChannel,
- baseApis: MatrixClient,
- userId: string,
- deviceId: string,
- startEvent: MatrixEvent,
- request: VerificationRequest,
- ): ReciprocateQRCode {
- return new ReciprocateQRCode(channel, baseApis, userId, deviceId, startEvent, request);
- }
-
- // eslint-disable-next-line @typescript-eslint/naming-convention
- public static get NAME(): string {
- return "m.reciprocate.v1";
- }
-
- protected doVerification = async (): Promise<void> => {
- if (!this.startEvent) {
- // TODO: Support scanning QR codes
- throw new Error("It is not currently possible to start verification" + "with this method yet.");
- }
-
- const { qrCodeData } = this.request;
- // 1. check the secret
- if (this.startEvent.getContent()["secret"] !== qrCodeData?.encodedSharedSecret) {
- throw newKeyMismatchError();
- }
-
- // 2. ask if other user shows shield as well
- await new Promise<void>((resolve, reject) => {
- this.reciprocateQREvent = {
- confirm: resolve,
- cancel: () => reject(newUserCancelledError()),
- };
- this.emit(QrCodeEvent.ShowReciprocateQr, this.reciprocateQREvent);
- });
-
- // 3. determine key to sign / mark as trusted
- const keys: Record<string, string> = {};
-
- switch (qrCodeData?.mode) {
- case Mode.VerifyOtherUser: {
- // add master key to keys to be signed, only if we're not doing self-verification
- const masterKey = qrCodeData.otherUserMasterKey;
- keys[`ed25519:${masterKey}`] = masterKey!;
- break;
- }
- case Mode.VerifySelfTrusted: {
- const deviceId = this.request.targetDevice.deviceId;
- keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey!;
- break;
- }
- case Mode.VerifySelfUntrusted: {
- const masterKey = qrCodeData.myMasterKey;
- keys[`ed25519:${masterKey}`] = masterKey!;
- break;
- }
- }
-
- // 4. sign the key (or mark own MSK as verified in case of MODE_VERIFY_SELF_TRUSTED)
- await this.verifyKeys(this.userId, keys, (keyId, device, keyInfo) => {
- // make sure the device has the expected keys
- const targetKey = keys[keyId];
- if (!targetKey) throw newKeyMismatchError();
-
- if (keyInfo !== targetKey) {
- logger.error("key ID from key info does not match");
- throw newKeyMismatchError();
- }
- for (const deviceKeyId in device.keys) {
- if (!deviceKeyId.startsWith("ed25519")) continue;
- const deviceTargetKey = keys[deviceKeyId];
- if (!deviceTargetKey) throw newKeyMismatchError();
- if (device.keys[deviceKeyId] !== deviceTargetKey) {
- logger.error("master key does not match");
- throw newKeyMismatchError();
- }
- }
- });
- };
-}
-
-const CODE_VERSION = 0x02; // the version of binary QR codes we support
-const BINARY_PREFIX = "MATRIX"; // ASCII, used to prefix the binary format
-
-enum Mode {
- VerifyOtherUser = 0x00, // Verifying someone who isn't us
- VerifySelfTrusted = 0x01, // We trust the master key
- VerifySelfUntrusted = 0x02, // We do not trust the master key
-}
-
-interface IQrData {
- prefix: string;
- version: number;
- mode: Mode;
- transactionId?: string;
- firstKeyB64: string;
- secondKeyB64: string;
- secretB64: string;
-}
-
-export class QRCodeData {
- public constructor(
- public readonly mode: Mode,
- private readonly sharedSecret: string,
- // only set when mode is MODE_VERIFY_OTHER_USER, master key of other party at time of generating QR code
- public readonly otherUserMasterKey: string | null,
- // only set when mode is MODE_VERIFY_SELF_TRUSTED, device key of other party at time of generating QR code
- public readonly otherDeviceKey: string | null,
- // only set when mode is MODE_VERIFY_SELF_UNTRUSTED, own master key at time of generating QR code
- public readonly myMasterKey: string | null,
- private readonly buffer: Buffer,
- ) {}
-
- public static async create(request: VerificationRequest, client: MatrixClient): Promise<QRCodeData> {
- const sharedSecret = QRCodeData.generateSharedSecret();
- const mode = QRCodeData.determineMode(request, client);
- let otherUserMasterKey: string | null = null;
- let otherDeviceKey: string | null = null;
- let myMasterKey: string | null = null;
- if (mode === Mode.VerifyOtherUser) {
- const otherUserCrossSigningInfo = client.getStoredCrossSigningForUser(request.otherUserId);
- otherUserMasterKey = otherUserCrossSigningInfo!.getId("master");
- } else if (mode === Mode.VerifySelfTrusted) {
- otherDeviceKey = await QRCodeData.getOtherDeviceKey(request, client);
- } else if (mode === Mode.VerifySelfUntrusted) {
- const myUserId = client.getUserId()!;
- const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
- myMasterKey = myCrossSigningInfo!.getId("master");
- }
- const qrData = QRCodeData.generateQrData(
- request,
- client,
- mode,
- sharedSecret,
- otherUserMasterKey!,
- otherDeviceKey!,
- myMasterKey!,
- );
- const buffer = QRCodeData.generateBuffer(qrData);
- return new QRCodeData(mode, sharedSecret, otherUserMasterKey, otherDeviceKey, myMasterKey, buffer);
- }
-
- /**
- * The unpadded base64 encoded shared secret.
- */
- public get encodedSharedSecret(): string {
- return this.sharedSecret;
- }
-
- public getBuffer(): Buffer {
- return this.buffer;
- }
-
- private static generateSharedSecret(): string {
- const secretBytes = new Uint8Array(11);
- global.crypto.getRandomValues(secretBytes);
- return encodeUnpaddedBase64(secretBytes);
- }
-
- private static async getOtherDeviceKey(request: VerificationRequest, client: MatrixClient): Promise<string> {
- const myUserId = client.getUserId()!;
- const otherDevice = request.targetDevice;
- const device = otherDevice.deviceId ? client.getStoredDevice(myUserId, otherDevice.deviceId) : undefined;
- if (!device) {
- throw new Error("could not find device " + otherDevice?.deviceId);
- }
- return device.getFingerprint();
- }
-
- private static determineMode(request: VerificationRequest, client: MatrixClient): Mode {
- const myUserId = client.getUserId();
- const otherUserId = request.otherUserId;
-
- let mode = Mode.VerifyOtherUser;
- if (myUserId === otherUserId) {
- // Mode changes depending on whether or not we trust the master cross signing key
- const myTrust = client.checkUserTrust(myUserId);
- if (myTrust.isCrossSigningVerified()) {
- mode = Mode.VerifySelfTrusted;
- } else {
- mode = Mode.VerifySelfUntrusted;
- }
- }
- return mode;
- }
-
- private static generateQrData(
- request: VerificationRequest,
- client: MatrixClient,
- mode: Mode,
- encodedSharedSecret: string,
- otherUserMasterKey?: string,
- otherDeviceKey?: string,
- myMasterKey?: string,
- ): IQrData {
- const myUserId = client.getUserId()!;
- const transactionId = request.channel.transactionId;
- const qrData: IQrData = {
- prefix: BINARY_PREFIX,
- version: CODE_VERSION,
- mode,
- transactionId,
- firstKeyB64: "", // worked out shortly
- secondKeyB64: "", // worked out shortly
- secretB64: encodedSharedSecret,
- };
-
- const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
-
- if (mode === Mode.VerifyOtherUser) {
- // First key is our master cross signing key
- qrData.firstKeyB64 = myCrossSigningInfo!.getId("master")!;
- // Second key is the other user's master cross signing key
- qrData.secondKeyB64 = otherUserMasterKey!;
- } else if (mode === Mode.VerifySelfTrusted) {
- // First key is our master cross signing key
- qrData.firstKeyB64 = myCrossSigningInfo!.getId("master")!;
- qrData.secondKeyB64 = otherDeviceKey!;
- } else if (mode === Mode.VerifySelfUntrusted) {
- // First key is our device's key
- qrData.firstKeyB64 = client.getDeviceEd25519Key()!;
- // Second key is what we think our master cross signing key is
- qrData.secondKeyB64 = myMasterKey!;
- }
- return qrData;
- }
-
- private static generateBuffer(qrData: IQrData): Buffer {
- let buf = Buffer.alloc(0); // we'll concat our way through life
-
- const appendByte = (b: number): void => {
- const tmpBuf = Buffer.from([b]);
- buf = Buffer.concat([buf, tmpBuf]);
- };
- const appendInt = (i: number): void => {
- const tmpBuf = Buffer.alloc(2);
- tmpBuf.writeInt16BE(i, 0);
- buf = Buffer.concat([buf, tmpBuf]);
- };
- const appendStr = (s: string, enc: BufferEncoding, withLengthPrefix = true): void => {
- const tmpBuf = Buffer.from(s, enc);
- if (withLengthPrefix) appendInt(tmpBuf.byteLength);
- buf = Buffer.concat([buf, tmpBuf]);
- };
- const appendEncBase64 = (b64: string): void => {
- const b = decodeBase64(b64);
- const tmpBuf = Buffer.from(b);
- buf = Buffer.concat([buf, tmpBuf]);
- };
-
- // Actually build the buffer for the QR code
- appendStr(qrData.prefix, "ascii", false);
- appendByte(qrData.version);
- appendByte(qrData.mode);
- appendStr(qrData.transactionId!, "utf-8");
- appendEncBase64(qrData.firstKeyB64);
- appendEncBase64(qrData.secondKeyB64);
- appendEncBase64(qrData.secretB64);
-
- return buf;
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/SAS.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/SAS.ts
deleted file mode 100644
index a8d237d..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/SAS.ts
+++ /dev/null
@@ -1,492 +0,0 @@
-/*
-Copyright 2018 - 2021 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.
-*/
-
-/**
- * Short Authentication String (SAS) verification.
- */
-
-import anotherjson from "another-json";
-import { Utility, SAS as OlmSAS } from "@matrix-org/olm";
-
-import { VerificationBase as Base, SwitchStartEventError, VerificationEventHandlerMap } from "./Base";
-import {
- errorFactory,
- newInvalidMessageError,
- newKeyMismatchError,
- newUnknownMethodError,
- newUserCancelledError,
-} from "./Error";
-import { logger } from "../../logger";
-import { IContent, MatrixEvent } from "../../models/event";
-import { generateDecimalSas } from "./SASDecimal";
-import { EventType } from "../../@types/event";
-
-const START_TYPE = EventType.KeyVerificationStart;
-
-const EVENTS = [EventType.KeyVerificationAccept, EventType.KeyVerificationKey, EventType.KeyVerificationMac];
-
-let olmutil: Utility;
-
-const newMismatchedSASError = errorFactory("m.mismatched_sas", "Mismatched short authentication string");
-
-const newMismatchedCommitmentError = errorFactory("m.mismatched_commitment", "Mismatched commitment");
-
-type EmojiMapping = [emoji: string, name: string];
-
-const emojiMapping: EmojiMapping[] = [
- ["🐶", "dog"], // 0
- ["🐱", "cat"], // 1
- ["🦁", "lion"], // 2
- ["🐎", "horse"], // 3
- ["🦄", "unicorn"], // 4
- ["🐷", "pig"], // 5
- ["🐘", "elephant"], // 6
- ["🐰", "rabbit"], // 7
- ["🐼", "panda"], // 8
- ["🐓", "rooster"], // 9
- ["🐧", "penguin"], // 10
- ["🐢", "turtle"], // 11
- ["🐟", "fish"], // 12
- ["🐙", "octopus"], // 13
- ["🦋", "butterfly"], // 14
- ["🌷", "flower"], // 15
- ["🌳", "tree"], // 16
- ["🌵", "cactus"], // 17
- ["🍄", "mushroom"], // 18
- ["🌏", "globe"], // 19
- ["🌙", "moon"], // 20
- ["☁️", "cloud"], // 21
- ["🔥", "fire"], // 22
- ["🍌", "banana"], // 23
- ["🍎", "apple"], // 24
- ["🍓", "strawberry"], // 25
- ["🌽", "corn"], // 26
- ["🍕", "pizza"], // 27
- ["🎂", "cake"], // 28
- ["❤️", "heart"], // 29
- ["🙂", "smiley"], // 30
- ["🤖", "robot"], // 31
- ["🎩", "hat"], // 32
- ["👓", "glasses"], // 33
- ["🔧", "spanner"], // 34
- ["🎅", "santa"], // 35
- ["👍", "thumbs up"], // 36
- ["☂️", "umbrella"], // 37
- ["⌛", "hourglass"], // 38
- ["⏰", "clock"], // 39
- ["🎁", "gift"], // 40
- ["💡", "light bulb"], // 41
- ["📕", "book"], // 42
- ["✏️", "pencil"], // 43
- ["📎", "paperclip"], // 44
- ["✂️", "scissors"], // 45
- ["🔒", "lock"], // 46
- ["🔑", "key"], // 47
- ["🔨", "hammer"], // 48
- ["☎️", "telephone"], // 49
- ["🏁", "flag"], // 50
- ["🚂", "train"], // 51
- ["🚲", "bicycle"], // 52
- ["✈️", "aeroplane"], // 53
- ["🚀", "rocket"], // 54
- ["🏆", "trophy"], // 55
- ["⚽", "ball"], // 56
- ["🎸", "guitar"], // 57
- ["🎺", "trumpet"], // 58
- ["🔔", "bell"], // 59
- ["⚓️", "anchor"], // 60
- ["🎧", "headphones"], // 61
- ["📁", "folder"], // 62
- ["📌", "pin"], // 63
-];
-
-function generateEmojiSas(sasBytes: number[]): EmojiMapping[] {
- const emojis = [
- // just like base64 encoding
- sasBytes[0] >> 2,
- ((sasBytes[0] & 0x3) << 4) | (sasBytes[1] >> 4),
- ((sasBytes[1] & 0xf) << 2) | (sasBytes[2] >> 6),
- sasBytes[2] & 0x3f,
- sasBytes[3] >> 2,
- ((sasBytes[3] & 0x3) << 4) | (sasBytes[4] >> 4),
- ((sasBytes[4] & 0xf) << 2) | (sasBytes[5] >> 6),
- ];
-
- return emojis.map((num) => emojiMapping[num]);
-}
-
-const sasGenerators = {
- decimal: generateDecimalSas,
- emoji: generateEmojiSas,
-} as const;
-
-export interface IGeneratedSas {
- decimal?: [number, number, number];
- emoji?: EmojiMapping[];
-}
-
-export interface ISasEvent {
- sas: IGeneratedSas;
- confirm(): Promise<void>;
- cancel(): void;
- mismatch(): void;
-}
-
-function generateSas(sasBytes: Uint8Array, methods: string[]): IGeneratedSas {
- const sas: IGeneratedSas = {};
- for (const method of methods) {
- if (method in sasGenerators) {
- // @ts-ignore - ts doesn't like us mixing types like this
- sas[method] = sasGenerators[method](Array.from(sasBytes));
- }
- }
- return sas;
-}
-
-const macMethods = {
- "hkdf-hmac-sha256": "calculate_mac",
- "org.matrix.msc3783.hkdf-hmac-sha256": "calculate_mac_fixed_base64",
- "hkdf-hmac-sha256.v2": "calculate_mac_fixed_base64",
- "hmac-sha256": "calculate_mac_long_kdf",
-} as const;
-
-type MacMethod = keyof typeof macMethods;
-
-function calculateMAC(olmSAS: OlmSAS, method: MacMethod) {
- return function (input: string, info: string): string {
- const mac = olmSAS[macMethods[method]](input, info);
- logger.log("SAS calculateMAC:", method, [input, info], mac);
- return mac;
- };
-}
-
-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}|`;
- const theirInfo = `${sas.userId}|${sas.deviceId}|${sas.theirSASPubKey}|`;
- const sasInfo =
- "MATRIX_KEY_VERIFICATION_SAS|" +
- (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) +
- sas.channel.transactionId;
- return olmSAS.generate_bytes(sasInfo, bytes);
- },
- "curve25519": function (sas: SAS, olmSAS: OlmSAS, bytes: number): Uint8Array {
- const ourInfo = `${sas.baseApis.getUserId()}${sas.baseApis.deviceId}`;
- const theirInfo = `${sas.userId}${sas.deviceId}`;
- const sasInfo =
- "MATRIX_KEY_VERIFICATION_SAS" +
- (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) +
- sas.channel.transactionId;
- return olmSAS.generate_bytes(sasInfo, bytes);
- },
-} as const;
-
-type KeyAgreement = keyof typeof calculateKeyAgreement;
-
-/* lists of algorithms/methods that are supported. The key agreement, hashes,
- * and MAC lists should be sorted in order of preference (most preferred
- * first).
- */
-const KEY_AGREEMENT_LIST: KeyAgreement[] = ["curve25519-hkdf-sha256", "curve25519"];
-const HASHES_LIST = ["sha256"];
-const MAC_LIST: MacMethod[] = [
- "hkdf-hmac-sha256.v2",
- "org.matrix.msc3783.hkdf-hmac-sha256",
- "hkdf-hmac-sha256",
- "hmac-sha256",
-];
-const SAS_LIST = Object.keys(sasGenerators);
-
-const KEY_AGREEMENT_SET = new Set(KEY_AGREEMENT_LIST);
-const HASHES_SET = new Set(HASHES_LIST);
-const MAC_SET = new Set(MAC_LIST);
-const SAS_SET = new Set(SAS_LIST);
-
-function intersection<T>(anArray: T[], aSet: Set<T>): T[] {
- return Array.isArray(anArray) ? anArray.filter((x) => aSet.has(x)) : [];
-}
-
-export enum SasEvent {
- ShowSas = "show_sas",
-}
-
-type EventHandlerMap = {
- [SasEvent.ShowSas]: (sas: ISasEvent) => void;
-} & VerificationEventHandlerMap;
-
-export class SAS extends Base<SasEvent, EventHandlerMap> {
- private waitingForAccept?: boolean;
- public ourSASPubKey?: string;
- public theirSASPubKey?: string;
- public sasEvent?: ISasEvent;
-
- // eslint-disable-next-line @typescript-eslint/naming-convention
- public static get NAME(): string {
- return "m.sas.v1";
- }
-
- public get events(): string[] {
- return EVENTS;
- }
-
- protected doVerification = async (): Promise<void> => {
- await global.Olm.init();
- olmutil = olmutil || new global.Olm.Utility();
-
- // make sure user's keys are downloaded
- await this.baseApis.downloadKeys([this.userId]);
-
- let retry = false;
- do {
- try {
- if (this.initiatedByMe) {
- return await this.doSendVerification();
- } else {
- return await this.doRespondVerification();
- }
- } catch (err) {
- if (err instanceof SwitchStartEventError) {
- // this changes what initiatedByMe returns
- this.startEvent = err.startEvent;
- retry = true;
- } else {
- throw err;
- }
- }
- } while (retry);
- };
-
- public canSwitchStartEvent(event: MatrixEvent): boolean {
- if (event.getType() !== START_TYPE) {
- return false;
- }
- const content = event.getContent();
- return content?.method === SAS.NAME && !!this.waitingForAccept;
- }
-
- private async sendStart(): Promise<Record<string, any>> {
- const startContent = this.channel.completeContent(START_TYPE, {
- method: SAS.NAME,
- from_device: this.baseApis.deviceId,
- key_agreement_protocols: KEY_AGREEMENT_LIST,
- hashes: HASHES_LIST,
- message_authentication_codes: MAC_LIST,
- // FIXME: allow app to specify what SAS methods can be used
- short_authentication_string: SAS_LIST,
- });
- await this.channel.sendCompleted(START_TYPE, startContent);
- return startContent;
- }
-
- private async verifyAndCheckMAC(
- keyAgreement: KeyAgreement,
- sasMethods: string[],
- olmSAS: OlmSAS,
- macMethod: MacMethod,
- ): Promise<void> {
- const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
- const verifySAS = new Promise<void>((resolve, reject) => {
- this.sasEvent = {
- sas: generateSas(sasBytes, sasMethods),
- confirm: async (): Promise<void> => {
- try {
- await this.sendMAC(olmSAS, macMethod);
- resolve();
- } catch (err) {
- reject(err);
- }
- },
- cancel: () => reject(newUserCancelledError()),
- mismatch: () => reject(newMismatchedSASError()),
- };
- this.emit(SasEvent.ShowSas, this.sasEvent);
- });
-
- const [e] = await Promise.all([
- this.waitForEvent(EventType.KeyVerificationMac).then((e) => {
- // we don't expect any more messages from the other
- // party, and they may send a m.key.verification.done
- // when they're done on their end
- this.expectedEvent = EventType.KeyVerificationDone;
- return e;
- }),
- verifySAS,
- ]);
- const content = e.getContent();
- await this.checkMAC(olmSAS, content, macMethod);
- }
-
- private async doSendVerification(): Promise<void> {
- this.waitingForAccept = true;
- let startContent;
- if (this.startEvent) {
- startContent = this.channel.completedContentFromEvent(this.startEvent);
- } else {
- startContent = await this.sendStart();
- }
-
- // we might have switched to a different start event,
- // but was we didn't call _waitForEvent there was no
- // call that could throw yet. So check manually that
- // we're still on the initiator side
- if (!this.initiatedByMe) {
- throw new SwitchStartEventError(this.startEvent);
- }
-
- let e: MatrixEvent;
- try {
- e = await this.waitForEvent(EventType.KeyVerificationAccept);
- } finally {
- this.waitingForAccept = false;
- }
- let content = e.getContent();
- const sasMethods = intersection(content.short_authentication_string, SAS_SET);
- if (
- !(
- KEY_AGREEMENT_SET.has(content.key_agreement_protocol) &&
- HASHES_SET.has(content.hash) &&
- MAC_SET.has(content.message_authentication_code) &&
- sasMethods.length
- )
- ) {
- throw newUnknownMethodError();
- }
- if (typeof content.commitment !== "string") {
- throw newInvalidMessageError();
- }
- const keyAgreement = content.key_agreement_protocol;
- const macMethod = content.message_authentication_code;
- const hashCommitment = content.commitment;
- const olmSAS = new global.Olm.SAS();
- try {
- this.ourSASPubKey = olmSAS.get_pubkey();
- await this.send(EventType.KeyVerificationKey, {
- key: this.ourSASPubKey,
- });
-
- e = await this.waitForEvent(EventType.KeyVerificationKey);
- // FIXME: make sure event is properly formed
- content = e.getContent();
- const commitmentStr = content.key + anotherjson.stringify(startContent);
- // TODO: use selected hash function (when we support multiple)
- if (olmutil.sha256(commitmentStr) !== hashCommitment) {
- throw newMismatchedCommitmentError();
- }
- this.theirSASPubKey = content.key;
- olmSAS.set_their_key(content.key);
-
- await this.verifyAndCheckMAC(keyAgreement, sasMethods, olmSAS, macMethod);
- } finally {
- olmSAS.free();
- }
- }
-
- private async doRespondVerification(): Promise<void> {
- // as m.related_to is not included in the encrypted content in e2e rooms,
- // we need to make sure it is added
- let content = this.channel.completedContentFromEvent(this.startEvent!);
-
- // Note: we intersect using our pre-made lists, rather than the sets,
- // so that the result will be in our order of preference. Then
- // fetching the first element from the array will give our preferred
- // method out of the ones offered by the other party.
- const keyAgreement = intersection(KEY_AGREEMENT_LIST, new Set(content.key_agreement_protocols))[0];
- const hashMethod = intersection(HASHES_LIST, new Set(content.hashes))[0];
- const macMethod = intersection(MAC_LIST, new Set(content.message_authentication_codes))[0];
- // FIXME: allow app to specify what SAS methods can be used
- const sasMethods = intersection(content.short_authentication_string, SAS_SET);
- if (!(keyAgreement !== undefined && hashMethod !== undefined && macMethod !== undefined && sasMethods.length)) {
- throw newUnknownMethodError();
- }
-
- const olmSAS = new global.Olm.SAS();
- try {
- const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
- await this.send(EventType.KeyVerificationAccept, {
- key_agreement_protocol: keyAgreement,
- hash: hashMethod,
- message_authentication_code: macMethod,
- short_authentication_string: sasMethods,
- // TODO: use selected hash function (when we support multiple)
- commitment: olmutil.sha256(commitmentStr),
- });
-
- const e = await this.waitForEvent(EventType.KeyVerificationKey);
- // FIXME: make sure event is properly formed
- content = e.getContent();
- this.theirSASPubKey = content.key;
- olmSAS.set_their_key(content.key);
- this.ourSASPubKey = olmSAS.get_pubkey();
- await this.send(EventType.KeyVerificationKey, {
- key: this.ourSASPubKey,
- });
-
- await this.verifyAndCheckMAC(keyAgreement, sasMethods, olmSAS, macMethod);
- } finally {
- olmSAS.free();
- }
- }
-
- private sendMAC(olmSAS: OlmSAS, method: MacMethod): Promise<void> {
- const mac: Record<string, string> = {};
- const keyList: string[] = [];
- const baseInfo =
- "MATRIX_KEY_VERIFICATION_MAC" +
- this.baseApis.getUserId() +
- this.baseApis.deviceId +
- this.userId +
- this.deviceId +
- this.channel.transactionId;
-
- const deviceKeyId = `ed25519:${this.baseApis.deviceId}`;
- mac[deviceKeyId] = calculateMAC(olmSAS, method)(this.baseApis.getDeviceEd25519Key()!, baseInfo + deviceKeyId);
- keyList.push(deviceKeyId);
-
- const crossSigningId = this.baseApis.getCrossSigningId();
- if (crossSigningId) {
- const crossSigningKeyId = `ed25519:${crossSigningId}`;
- mac[crossSigningKeyId] = calculateMAC(olmSAS, method)(crossSigningId, baseInfo + crossSigningKeyId);
- keyList.push(crossSigningKeyId);
- }
-
- const keys = calculateMAC(olmSAS, method)(keyList.sort().join(","), baseInfo + "KEY_IDS");
- return this.send(EventType.KeyVerificationMac, { mac, keys });
- }
-
- private async checkMAC(olmSAS: OlmSAS, content: IContent, method: MacMethod): Promise<void> {
- const baseInfo =
- "MATRIX_KEY_VERIFICATION_MAC" +
- this.userId +
- this.deviceId +
- this.baseApis.getUserId() +
- this.baseApis.deviceId +
- this.channel.transactionId;
-
- if (
- content.keys !==
- calculateMAC(olmSAS, method)(Object.keys(content.mac).sort().join(","), baseInfo + "KEY_IDS")
- ) {
- throw newKeyMismatchError();
- }
-
- await this.verifyKeys(this.userId, content.mac, (keyId, device, keyInfo) => {
- if (keyInfo !== calculateMAC(olmSAS, method)(device.keys[keyId], baseInfo + keyId)) {
- throw newKeyMismatchError();
- }
- });
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/SASDecimal.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/SASDecimal.ts
deleted file mode 100644
index 0cb4630..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/SASDecimal.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
-Copyright 2018 - 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.
-*/
-
-/**
- * Implementation of decimal encoding of SAS as per:
- * https://spec.matrix.org/v1.4/client-server-api/#sas-method-decimal
- * @param sasBytes - the five bytes generated by HKDF
- * @returns the derived three numbers between 1000 and 9191 inclusive
- */
-export function generateDecimalSas(sasBytes: number[]): [number, number, number] {
- /*
- * +--------+--------+--------+--------+--------+
- * | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 |
- * +--------+--------+--------+--------+--------+
- * bits: 87654321 87654321 87654321 87654321 87654321
- * \____________/\_____________/\____________/
- * 1st number 2nd number 3rd number
- */
- return [
- ((sasBytes[0] << 5) | (sasBytes[1] >> 3)) + 1000,
- (((sasBytes[1] & 0x7) << 10) | (sasBytes[2] << 2) | (sasBytes[3] >> 6)) + 1000,
- (((sasBytes[3] & 0x3f) << 7) | (sasBytes[4] >> 1)) + 1000,
- ];
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/Channel.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/Channel.ts
deleted file mode 100644
index 48415f9..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/Channel.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
-Copyright 2021 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 { MatrixEvent } from "../../../models/event";
-import { VerificationRequest } from "./VerificationRequest";
-
-export interface IVerificationChannel {
- request?: VerificationRequest;
- readonly userId?: string;
- readonly roomId?: string;
- readonly deviceId?: string;
- readonly transactionId?: string;
- readonly receiveStartFromOtherDevices?: boolean;
- getTimestamp(event: MatrixEvent): number;
- send(type: string, uncompletedContent: Record<string, any>): Promise<void>;
- completeContent(type: string, content: Record<string, any>): Record<string, any>;
- sendCompleted(type: string, content: Record<string, any>): Promise<void>;
- completedContentFromEvent(event: MatrixEvent): Record<string, any>;
- canCreateRequest(type: string): boolean;
- handleEvent(event: MatrixEvent, request: VerificationRequest, isLiveEvent: boolean): Promise<void>;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/InRoomChannel.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/InRoomChannel.ts
deleted file mode 100644
index ff11bf1..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/InRoomChannel.ts
+++ /dev/null
@@ -1,356 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { VerificationRequest, REQUEST_TYPE, READY_TYPE, START_TYPE } from "./VerificationRequest";
-import { logger } from "../../../logger";
-import { IVerificationChannel } from "./Channel";
-import { EventType } from "../../../@types/event";
-import { MatrixClient } from "../../../client";
-import { MatrixEvent } from "../../../models/event";
-import { IRequestsMap } from "../..";
-
-const MESSAGE_TYPE = EventType.RoomMessage;
-const M_REFERENCE = "m.reference";
-const M_RELATES_TO = "m.relates_to";
-
-/**
- * A key verification channel that sends verification events in the timeline of a room.
- * Uses the event id of the initial m.key.verification.request event as a transaction id.
- */
-export class InRoomChannel implements IVerificationChannel {
- private requestEventId?: string;
-
- /**
- * @param client - the matrix client, to send messages with and get current user & device from.
- * @param roomId - id of the room where verification events should be posted in, should be a DM with the given user.
- * @param userId - id of user that the verification request is directed at, should be present in the room.
- */
- public constructor(private readonly client: MatrixClient, public readonly roomId: string, public userId?: string) {}
-
- public get receiveStartFromOtherDevices(): boolean {
- return true;
- }
-
- /** The transaction id generated/used by this verification channel */
- public get transactionId(): string | undefined {
- return this.requestEventId;
- }
-
- public static getOtherPartyUserId(event: MatrixEvent, client: MatrixClient): string | undefined {
- const type = InRoomChannel.getEventType(event);
- if (type !== REQUEST_TYPE) {
- return;
- }
- const ownUserId = client.getUserId();
- const sender = event.getSender();
- const content = event.getContent();
- const receiver = content.to;
-
- if (sender === ownUserId) {
- return receiver;
- } else if (receiver === ownUserId) {
- return sender;
- }
- }
-
- /**
- * @param event - the event to get the timestamp of
- * @returns the timestamp when the event was sent
- */
- public getTimestamp(event: MatrixEvent): number {
- return event.getTs();
- }
-
- /**
- * Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel
- * @param type - the event type to check
- * @returns boolean flag
- */
- public static canCreateRequest(type: string): boolean {
- return type === REQUEST_TYPE;
- }
-
- public canCreateRequest(type: string): boolean {
- return InRoomChannel.canCreateRequest(type);
- }
-
- /**
- * Extract the transaction id used by a given key verification event, if any
- * @param event - the event
- * @returns the transaction id
- */
- public static getTransactionId(event: MatrixEvent): string | undefined {
- if (InRoomChannel.getEventType(event) === REQUEST_TYPE) {
- return event.getId();
- } else {
- const relation = event.getRelation();
- if (relation?.rel_type === M_REFERENCE) {
- return relation.event_id;
- }
- }
- }
-
- /**
- * Checks whether this event is a well-formed key verification event.
- * This only does checks that don't rely on the current state of a potentially already channel
- * so we can prevent channels being created by invalid events.
- * `handleEvent` can do more checks and choose to ignore invalid events.
- * @param event - the event to validate
- * @param client - the client to get the current user and device id from
- * @returns whether the event is valid and should be passed to handleEvent
- */
- public static validateEvent(event: MatrixEvent, client: MatrixClient): boolean {
- const txnId = InRoomChannel.getTransactionId(event);
- if (typeof txnId !== "string" || txnId.length === 0) {
- return false;
- }
- const type = InRoomChannel.getEventType(event);
- const content = event.getContent();
-
- // from here on we're fairly sure that this is supposed to be
- // part of a verification request, so be noisy when rejecting something
- if (type === REQUEST_TYPE) {
- if (!content || typeof content.to !== "string" || !content.to.length) {
- logger.log("InRoomChannel: validateEvent: " + "no valid to " + (content && content.to));
- return false;
- }
-
- // ignore requests that are not direct to or sent by the syncing user
- if (!InRoomChannel.getOtherPartyUserId(event, client)) {
- logger.log(
- "InRoomChannel: validateEvent: " +
- `not directed to or sent by me: ${event.getSender()}` +
- `, ${content && content.to}`,
- );
- return false;
- }
- }
-
- return VerificationRequest.validateEvent(type, event, client);
- }
-
- /**
- * As m.key.verification.request events are as m.room.message events with the InRoomChannel
- * to have a fallback message in non-supporting clients, we map the real event type
- * to the symbolic one to keep things in unison with ToDeviceChannel
- * @param event - the event to get the type of
- * @returns the "symbolic" event type
- */
- public static getEventType(event: MatrixEvent): string {
- const type = event.getType();
- if (type === MESSAGE_TYPE) {
- const content = event.getContent();
- if (content) {
- const { msgtype } = content;
- if (msgtype === REQUEST_TYPE) {
- return REQUEST_TYPE;
- }
- }
- }
- if (type && type !== REQUEST_TYPE) {
- return type;
- } else {
- return "";
- }
- }
-
- /**
- * Changes the state of the channel, request, and verifier in response to a key verification event.
- * @param event - to handle
- * @param request - the request to forward handling to
- * @param isLiveEvent - whether this is an even received through sync or not
- * @returns 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> {
- // 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()!)) {
- return;
- }
- const type = InRoomChannel.getEventType(event);
- // do validations that need state (roomId, userId),
- // ignore if invalid
-
- if (event.getRoomId() !== this.roomId) {
- return;
- }
- // set userId if not set already
- if (!this.userId) {
- const userId = InRoomChannel.getOtherPartyUserId(event, this.client);
- if (userId) {
- this.userId = userId;
- }
- }
- // ignore events not sent by us or the other party
- const ownUserId = this.client.getUserId();
- const sender = event.getSender();
- if (this.userId) {
- if (sender !== ownUserId && sender !== this.userId) {
- logger.log(`InRoomChannel: ignoring verification event from non-participating sender ${sender}`);
- return;
- }
- }
- if (!this.requestEventId) {
- this.requestEventId = InRoomChannel.getTransactionId(event);
- }
-
- const isRemoteEcho = !!event.getUnsigned().transaction_id;
- const isSentByUs = event.getSender() === this.client.getUserId();
-
- return request.handleEvent(type, event, isLiveEvent, isRemoteEcho, isSentByUs);
- }
-
- /**
- * Adds the transaction id (relation) back to a received event
- * so it has the same format as returned by `completeContent` before sending.
- * The relation can not appear on the event content because of encryption,
- * relations are excluded from encryption.
- * @param event - the received event
- * @returns the content object with the relation added again
- */
- public completedContentFromEvent(event: MatrixEvent): Record<string, any> {
- // ensure m.related_to is included in e2ee rooms
- // as the field is excluded from encryption
- const content = Object.assign({}, event.getContent());
- content[M_RELATES_TO] = event.getRelation()!;
- return content;
- }
-
- /**
- * Add all the fields to content needed for sending it over this channel.
- * This is public so verification methods (SAS uses this) can get the exact
- * content that will be sent independent of the used channel,
- * as they need to calculate the hash of it.
- * @param type - the event type
- * @param content - the (incomplete) content
- * @returns the complete content, as it will be sent.
- */
- public completeContent(type: string, content: Record<string, any>): Record<string, any> {
- content = Object.assign({}, content);
- if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
- content.from_device = this.client.getDeviceId();
- }
- if (type === REQUEST_TYPE) {
- // type is mapped to m.room.message in the send method
- content = {
- body:
- this.client.getUserId() +
- " is requesting to verify " +
- "your key, but your client does not support in-chat key " +
- "verification. You will need to use legacy key " +
- "verification to verify keys.",
- msgtype: REQUEST_TYPE,
- to: this.userId,
- from_device: content.from_device,
- methods: content.methods,
- };
- } else {
- content[M_RELATES_TO] = {
- rel_type: M_REFERENCE,
- event_id: this.transactionId,
- };
- }
- return content;
- }
-
- /**
- * Send an event over the channel with the content not having gone through `completeContent`.
- * @param type - the event type
- * @param uncompletedContent - the (incomplete) content
- * @returns the promise of the request
- */
- public send(type: string, uncompletedContent: Record<string, any>): Promise<void> {
- const content = this.completeContent(type, uncompletedContent);
- return this.sendCompleted(type, content);
- }
-
- /**
- * Send an event over the channel with the content having gone through `completeContent` already.
- * @param type - the event type
- * @returns the promise of the request
- */
- public async sendCompleted(type: string, content: Record<string, any>): Promise<void> {
- let sendType = type;
- if (type === REQUEST_TYPE) {
- sendType = MESSAGE_TYPE;
- }
- const response = await this.client.sendEvent(this.roomId, sendType, content);
- if (type === REQUEST_TYPE) {
- this.requestEventId = response.event_id;
- }
- }
-}
-
-export class InRoomRequests implements IRequestsMap {
- private requestsByRoomId = new Map<string, Map<string, VerificationRequest>>();
-
- public getRequest(event: MatrixEvent): VerificationRequest | undefined {
- const roomId = event.getRoomId()!;
- const txnId = InRoomChannel.getTransactionId(event)!;
- return this.getRequestByTxnId(roomId, txnId);
- }
-
- public getRequestByChannel(channel: InRoomChannel): VerificationRequest | undefined {
- return this.getRequestByTxnId(channel.roomId, channel.transactionId!);
- }
-
- private getRequestByTxnId(roomId: string, txnId: string): VerificationRequest | undefined {
- const requestsByTxnId = this.requestsByRoomId.get(roomId);
- if (requestsByTxnId) {
- return requestsByTxnId.get(txnId);
- }
- }
-
- public setRequest(event: MatrixEvent, request: VerificationRequest): void {
- this.doSetRequest(event.getRoomId()!, InRoomChannel.getTransactionId(event)!, request);
- }
-
- public setRequestByChannel(channel: IVerificationChannel, request: VerificationRequest): void {
- this.doSetRequest(channel.roomId!, channel.transactionId!, request);
- }
-
- private doSetRequest(roomId: string, txnId: string, request: VerificationRequest): void {
- let requestsByTxnId = this.requestsByRoomId.get(roomId);
- if (!requestsByTxnId) {
- requestsByTxnId = new Map();
- this.requestsByRoomId.set(roomId, requestsByTxnId);
- }
- requestsByTxnId.set(txnId, request);
- }
-
- public removeRequest(event: MatrixEvent): void {
- const roomId = event.getRoomId()!;
- const requestsByTxnId = this.requestsByRoomId.get(roomId);
- if (requestsByTxnId) {
- requestsByTxnId.delete(InRoomChannel.getTransactionId(event)!);
- if (requestsByTxnId.size === 0) {
- this.requestsByRoomId.delete(roomId);
- }
- }
- }
-
- public findRequestInProgress(roomId: string): VerificationRequest | undefined {
- const requestsByTxnId = this.requestsByRoomId.get(roomId);
- if (requestsByTxnId) {
- for (const request of requestsByTxnId.values()) {
- if (request.pending) {
- return request;
- }
- }
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/ToDeviceChannel.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/ToDeviceChannel.ts
deleted file mode 100644
index d51b85a..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/ToDeviceChannel.ts
+++ /dev/null
@@ -1,354 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { randomString } from "../../../randomstring";
-import { logger } from "../../../logger";
-import {
- CANCEL_TYPE,
- PHASE_STARTED,
- PHASE_READY,
- REQUEST_TYPE,
- READY_TYPE,
- START_TYPE,
- VerificationRequest,
-} from "./VerificationRequest";
-import { errorFromEvent, newUnexpectedMessageError } from "../Error";
-import { MatrixEvent } from "../../../models/event";
-import { IVerificationChannel } from "./Channel";
-import { MatrixClient } from "../../../client";
-import { IRequestsMap } from "../..";
-
-export type Request = VerificationRequest<ToDeviceChannel>;
-
-/**
- * A key verification channel that sends verification events over to_device messages.
- * Generates its own transaction ids.
- */
-export class ToDeviceChannel implements IVerificationChannel {
- public request?: VerificationRequest;
-
- // userId and devices of user we're about to verify
- public constructor(
- private readonly client: MatrixClient,
- public readonly userId: string,
- private readonly devices: string[],
- public transactionId?: string,
- public deviceId?: string,
- ) {}
-
- public isToDevices(devices: string[]): boolean {
- if (devices.length === this.devices.length) {
- for (const device of devices) {
- if (!this.devices.includes(device)) {
- return false;
- }
- }
- return true;
- } else {
- return false;
- }
- }
-
- public static getEventType(event: MatrixEvent): string {
- return event.getType();
- }
-
- /**
- * Extract the transaction id used by a given key verification event, if any
- * @param event - the event
- * @returns the transaction id
- */
- public static getTransactionId(event: MatrixEvent): string {
- const content = event.getContent();
- return content && content.transaction_id;
- }
-
- /**
- * Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel
- * @param type - the event type to check
- * @returns boolean flag
- */
- public static canCreateRequest(type: string): boolean {
- return type === REQUEST_TYPE || type === START_TYPE;
- }
-
- public canCreateRequest(type: string): boolean {
- return ToDeviceChannel.canCreateRequest(type);
- }
-
- /**
- * Checks whether this event is a well-formed key verification event.
- * This only does checks that don't rely on the current state of a potentially already channel
- * so we can prevent channels being created by invalid events.
- * `handleEvent` can do more checks and choose to ignore invalid events.
- * @param event - the event to validate
- * @param client - the client to get the current user and device id from
- * @returns whether the event is valid and should be passed to handleEvent
- */
- public static validateEvent(event: MatrixEvent, client: MatrixClient): boolean {
- if (event.isCancelled()) {
- logger.warn("Ignoring flagged verification request from " + event.getSender());
- return false;
- }
- const content = event.getContent();
- if (!content) {
- logger.warn("ToDeviceChannel.validateEvent: invalid: no content");
- return false;
- }
-
- if (!content.transaction_id) {
- logger.warn("ToDeviceChannel.validateEvent: invalid: no transaction_id");
- return false;
- }
-
- const type = event.getType();
-
- if (type === REQUEST_TYPE) {
- if (!Number.isFinite(content.timestamp)) {
- logger.warn("ToDeviceChannel.validateEvent: invalid: no timestamp");
- return false;
- }
- if (event.getSender() === client.getUserId() && content.from_device == client.getDeviceId()) {
- // ignore requests from ourselves, because it doesn't make sense for a
- // device to verify itself
- logger.warn("ToDeviceChannel.validateEvent: invalid: from own device");
- return false;
- }
- }
-
- return VerificationRequest.validateEvent(type, event, client);
- }
-
- /**
- * @param event - the event to get the timestamp of
- * @returns the timestamp when the event was sent
- */
- public getTimestamp(event: MatrixEvent): number {
- const content = event.getContent();
- return content && content.timestamp;
- }
-
- /**
- * Changes the state of the channel, request, and verifier in response to a key verification event.
- * @param event - to handle
- * @param request - the request to forward handling to
- * @param isLiveEvent - whether this is an even received through sync or not
- * @returns a promise that resolves when any requests as an answer to the passed-in event are sent.
- */
- public async handleEvent(event: MatrixEvent, request: Request, isLiveEvent = false): Promise<void> {
- const type = event.getType();
- const content = event.getContent();
- if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
- if (!this.transactionId) {
- this.transactionId = content.transaction_id;
- }
- const deviceId = content.from_device;
- // adopt deviceId if not set before and valid
- if (!this.deviceId && this.devices.includes(deviceId)) {
- this.deviceId = deviceId;
- }
- // if no device id or different from adopted one, cancel with sender
- if (!this.deviceId || this.deviceId !== deviceId) {
- // also check that message came from the device we sent the request to earlier on
- // and do send a cancel message to that device
- // (but don't cancel the request for the device we should be talking to)
- const cancelContent = this.completeContent(CANCEL_TYPE, errorFromEvent(newUnexpectedMessageError()));
- return this.sendToDevices(CANCEL_TYPE, cancelContent, [deviceId]);
- }
- }
- const wasStarted = request.phase === PHASE_STARTED || request.phase === PHASE_READY;
-
- await request.handleEvent(event.getType(), event, isLiveEvent, false, false);
-
- const isStarted = request.phase === PHASE_STARTED || request.phase === PHASE_READY;
-
- const isAcceptingEvent = type === START_TYPE || type === READY_TYPE;
- // the request has picked a ready or start event, tell the other devices about it
- if (isAcceptingEvent && !wasStarted && isStarted && this.deviceId) {
- const nonChosenDevices = this.devices.filter((d) => d !== this.deviceId && d !== this.client.getDeviceId());
- if (nonChosenDevices.length) {
- const message = this.completeContent(CANCEL_TYPE, {
- code: "m.accepted",
- reason: "Verification request accepted by another device",
- });
- await this.sendToDevices(CANCEL_TYPE, message, nonChosenDevices);
- }
- }
- }
-
- /**
- * See {@link InRoomChannel#completedContentFromEvent} for why this is needed.
- * @param event - the received event
- * @returns the content object
- */
- public completedContentFromEvent(event: MatrixEvent): Record<string, any> {
- return event.getContent();
- }
-
- /**
- * Add all the fields to content needed for sending it over this channel.
- * This is public so verification methods (SAS uses this) can get the exact
- * content that will be sent independent of the used channel,
- * as they need to calculate the hash of it.
- * @param type - the event type
- * @param content - the (incomplete) content
- * @returns the complete content, as it will be sent.
- */
- public completeContent(type: string, content: Record<string, any>): Record<string, any> {
- // make a copy
- content = Object.assign({}, content);
- if (this.transactionId) {
- content.transaction_id = this.transactionId;
- }
- if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
- content.from_device = this.client.getDeviceId();
- }
- if (type === REQUEST_TYPE) {
- content.timestamp = Date.now();
- }
- return content;
- }
-
- /**
- * Send an event over the channel with the content not having gone through `completeContent`.
- * @param type - the event type
- * @param uncompletedContent - the (incomplete) content
- * @returns the promise of the request
- */
- public send(type: string, uncompletedContent: Record<string, any> = {}): Promise<void> {
- // create transaction id when sending request
- if ((type === REQUEST_TYPE || type === START_TYPE) && !this.transactionId) {
- this.transactionId = ToDeviceChannel.makeTransactionId();
- }
- const content = this.completeContent(type, uncompletedContent);
- return this.sendCompleted(type, content);
- }
-
- /**
- * Send an event over the channel with the content having gone through `completeContent` already.
- * @param type - the event type
- * @returns the promise of the request
- */
- public async sendCompleted(type: string, content: Record<string, any>): Promise<void> {
- let result;
- if (type === REQUEST_TYPE || (type === CANCEL_TYPE && !this.deviceId)) {
- result = await this.sendToDevices(type, content, this.devices);
- } else {
- result = await this.sendToDevices(type, content, [this.deviceId!]);
- }
- // the VerificationRequest state machine requires remote echos of the event
- // the client sends itself, so we fake this for to_device messages
- const remoteEchoEvent = new MatrixEvent({
- sender: this.client.getUserId()!,
- content,
- type,
- });
- await this.request!.handleEvent(
- type,
- remoteEchoEvent,
- /*isLiveEvent=*/ true,
- /*isRemoteEcho=*/ true,
- /*isSentByUs=*/ true,
- );
- return result;
- }
-
- private async sendToDevices(type: string, content: Record<string, any>, devices: string[]): Promise<void> {
- if (devices.length) {
- const deviceMessages: Map<string, Record<string, any>> = new Map();
- for (const deviceId of devices) {
- deviceMessages.set(deviceId, content);
- }
-
- await this.client.sendToDevice(type, new Map([[this.userId, deviceMessages]]));
- }
- }
-
- /**
- * Allow Crypto module to create and know the transaction id before the .start event gets sent.
- * @returns the transaction id
- */
- public static makeTransactionId(): string {
- return randomString(32);
- }
-}
-
-export class ToDeviceRequests implements IRequestsMap {
- private requestsByUserId = new Map<string, Map<string, Request>>();
-
- public getRequest(event: MatrixEvent): Request | undefined {
- return this.getRequestBySenderAndTxnId(event.getSender()!, ToDeviceChannel.getTransactionId(event));
- }
-
- public getRequestByChannel(channel: ToDeviceChannel): Request | undefined {
- return this.getRequestBySenderAndTxnId(channel.userId, channel.transactionId!);
- }
-
- public getRequestBySenderAndTxnId(sender: string, txnId: string): Request | undefined {
- const requestsByTxnId = this.requestsByUserId.get(sender);
- if (requestsByTxnId) {
- return requestsByTxnId.get(txnId);
- }
- }
-
- public setRequest(event: MatrixEvent, request: Request): void {
- this.setRequestBySenderAndTxnId(event.getSender()!, ToDeviceChannel.getTransactionId(event), request);
- }
-
- public setRequestByChannel(channel: ToDeviceChannel, request: Request): void {
- this.setRequestBySenderAndTxnId(channel.userId, channel.transactionId!, request);
- }
-
- public setRequestBySenderAndTxnId(sender: string, txnId: string, request: Request): void {
- let requestsByTxnId = this.requestsByUserId.get(sender);
- if (!requestsByTxnId) {
- requestsByTxnId = new Map();
- this.requestsByUserId.set(sender, requestsByTxnId);
- }
- requestsByTxnId.set(txnId, request);
- }
-
- public removeRequest(event: MatrixEvent): void {
- const userId = event.getSender()!;
- const requestsByTxnId = this.requestsByUserId.get(userId);
- if (requestsByTxnId) {
- requestsByTxnId.delete(ToDeviceChannel.getTransactionId(event));
- if (requestsByTxnId.size === 0) {
- this.requestsByUserId.delete(userId);
- }
- }
- }
-
- public findRequestInProgress(userId: string, devices: string[]): Request | undefined {
- const requestsByTxnId = this.requestsByUserId.get(userId);
- if (requestsByTxnId) {
- for (const request of requestsByTxnId.values()) {
- if (request.pending && request.channel.isToDevices(devices)) {
- return request;
- }
- }
- }
- }
-
- public getRequestsInProgress(userId: string): Request[] {
- const requestsByTxnId = this.requestsByUserId.get(userId);
- if (requestsByTxnId) {
- return Array.from(requestsByTxnId.values()).filter((r) => r.pending);
- }
- return [];
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/VerificationRequest.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/VerificationRequest.ts
deleted file mode 100644
index 617432e..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/VerificationRequest.ts
+++ /dev/null
@@ -1,926 +0,0 @@
-/*
-Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { logger } from "../../../logger";
-import { errorFactory, errorFromEvent, newUnexpectedMessageError, newUnknownMethodError } from "../Error";
-import { QRCodeData, SCAN_QR_CODE_METHOD } from "../QRCode";
-import { IVerificationChannel } from "./Channel";
-import { MatrixClient } from "../../../client";
-import { MatrixEvent } from "../../../models/event";
-import { EventType } from "../../../@types/event";
-import { VerificationBase } from "../Base";
-import { VerificationMethod } from "../../index";
-import { TypedEventEmitter } from "../../../models/typed-event-emitter";
-
-// How long after the event's timestamp that the request times out
-const TIMEOUT_FROM_EVENT_TS = 10 * 60 * 1000; // 10 minutes
-
-// How long after we receive the event that the request times out
-const TIMEOUT_FROM_EVENT_RECEIPT = 2 * 60 * 1000; // 2 minutes
-
-// to avoid almost expired verification notifications
-// from showing a notification and almost immediately
-// disappearing, also ignore verification requests that
-// are this amount of time away from expiring.
-const VERIFICATION_REQUEST_MARGIN = 3 * 1000; // 3 seconds
-
-export const EVENT_PREFIX = "m.key.verification.";
-export const REQUEST_TYPE = EVENT_PREFIX + "request";
-export const START_TYPE = EVENT_PREFIX + "start";
-export const CANCEL_TYPE = EVENT_PREFIX + "cancel";
-export const DONE_TYPE = EVENT_PREFIX + "done";
-export const READY_TYPE = EVENT_PREFIX + "ready";
-
-export enum Phase {
- Unsent = 1,
- Requested,
- Ready,
- Started,
- Cancelled,
- Done,
-}
-
-// Legacy export fields
-export const PHASE_UNSENT = Phase.Unsent;
-export const PHASE_REQUESTED = Phase.Requested;
-export const PHASE_READY = Phase.Ready;
-export const PHASE_STARTED = Phase.Started;
-export const PHASE_CANCELLED = Phase.Cancelled;
-export const PHASE_DONE = Phase.Done;
-
-interface ITargetDevice {
- userId?: string;
- deviceId?: string;
-}
-
-interface ITransition {
- phase: Phase;
- event?: MatrixEvent;
-}
-
-export enum VerificationRequestEvent {
- Change = "change",
-}
-
-type EventHandlerMap = {
- /**
- * Fires whenever the state of the request object has changed.
- */
- [VerificationRequestEvent.Change]: () => void;
-};
-
-/**
- * State machine for verification requests.
- * Things that differ based on what channel is used to
- * send and receive verification events are put in `InRoomChannel` or `ToDeviceChannel`.
- */
-export class VerificationRequest<C extends IVerificationChannel = IVerificationChannel> extends TypedEventEmitter<
- VerificationRequestEvent,
- EventHandlerMap
-> {
- private eventsByUs = new Map<string, MatrixEvent>();
- private eventsByThem = new Map<string, MatrixEvent>();
- private _observeOnly = false;
- private timeoutTimer: ReturnType<typeof setTimeout> | null = null;
- private _accepting = false;
- private _declining = false;
- private verifierHasFinished = false;
- private _cancelled = false;
- private _chosenMethod: VerificationMethod | null = null;
- // we keep a copy of the QR Code data (including other user master key) around
- // for QR reciprocate verification, to protect against
- // cross-signing identity reset between the .ready and .start event
- // and signing the wrong key after .start
- private _qrCodeData: QRCodeData | null = null;
-
- // The timestamp when we received the request event from the other side
- private requestReceivedAt: number | null = null;
-
- private commonMethods: VerificationMethod[] = [];
- private _phase!: Phase;
- public _cancellingUserId?: string; // Used in tests only
- private _verifier?: VerificationBase<any, any>;
-
- public constructor(
- public readonly channel: C,
- private readonly verificationMethods: Map<VerificationMethod, typeof VerificationBase>,
- private readonly client: MatrixClient,
- ) {
- super();
- this.channel.request = this;
- this.setPhase(PHASE_UNSENT, false);
- }
-
- /**
- * Stateless validation logic not specific to the channel.
- * Invoked by the same static method in either channel.
- * @param type - the "symbolic" event type, as returned by the `getEventType` function on the channel.
- * @param event - the event to validate. Don't call getType() on it but use the `type` parameter instead.
- * @param client - the client to get the current user and device id from
- * @returns whether the event is valid and should be passed to handleEvent
- */
- public static validateEvent(type: string, event: MatrixEvent, client: MatrixClient): boolean {
- const content = event.getContent();
-
- if (!type || !type.startsWith(EVENT_PREFIX)) {
- return false;
- }
-
- // from here on we're fairly sure that this is supposed to be
- // part of a verification request, so be noisy when rejecting something
- if (!content) {
- logger.log("VerificationRequest: validateEvent: no content");
- return false;
- }
-
- if (type === REQUEST_TYPE || type === READY_TYPE) {
- if (!Array.isArray(content.methods)) {
- logger.log("VerificationRequest: validateEvent: " + "fail because methods");
- return false;
- }
- }
-
- if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
- if (typeof content.from_device !== "string" || content.from_device.length === 0) {
- logger.log("VerificationRequest: validateEvent: " + "fail because from_device");
- return false;
- }
- }
-
- return true;
- }
-
- public get invalid(): boolean {
- return this.phase === PHASE_UNSENT;
- }
-
- /** returns whether the phase is PHASE_REQUESTED */
- public get requested(): boolean {
- return this.phase === PHASE_REQUESTED;
- }
-
- /** returns whether the phase is PHASE_CANCELLED */
- public get cancelled(): boolean {
- return this.phase === PHASE_CANCELLED;
- }
-
- /** returns whether the phase is PHASE_READY */
- public get ready(): boolean {
- return this.phase === PHASE_READY;
- }
-
- /** returns whether the phase is PHASE_STARTED */
- public get started(): boolean {
- return this.phase === PHASE_STARTED;
- }
-
- /** returns whether the phase is PHASE_DONE */
- public get done(): boolean {
- return this.phase === PHASE_DONE;
- }
-
- /** once the phase is PHASE_STARTED (and !initiatedByMe) or PHASE_READY: common methods supported by both sides */
- public get methods(): VerificationMethod[] {
- return this.commonMethods;
- }
-
- /** the method picked in the .start event */
- public get chosenMethod(): VerificationMethod | null {
- return this._chosenMethod;
- }
-
- public calculateEventTimeout(event: MatrixEvent): number {
- let effectiveExpiresAt = this.channel.getTimestamp(event) + TIMEOUT_FROM_EVENT_TS;
-
- if (this.requestReceivedAt && !this.initiatedByMe && this.phase <= PHASE_REQUESTED) {
- const expiresAtByReceipt = this.requestReceivedAt + TIMEOUT_FROM_EVENT_RECEIPT;
- effectiveExpiresAt = Math.min(effectiveExpiresAt, expiresAtByReceipt);
- }
-
- return Math.max(0, effectiveExpiresAt - Date.now());
- }
-
- /** The current remaining amount of ms before the request should be automatically cancelled */
- public get timeout(): number {
- const requestEvent = this.getEventByEither(REQUEST_TYPE);
- if (requestEvent) {
- return this.calculateEventTimeout(requestEvent);
- }
- return 0;
- }
-
- /**
- * The key verification request event.
- * @returns The request event, or falsey if not found.
- */
- public get requestEvent(): MatrixEvent | undefined {
- return this.getEventByEither(REQUEST_TYPE);
- }
-
- /** current phase of the request. Some properties might only be defined in a current phase. */
- public get phase(): Phase {
- return this._phase;
- }
-
- /** The verifier to do the actual verification, once the method has been established. Only defined when the `phase` is PHASE_STARTED. */
- public get verifier(): VerificationBase<any, any> | undefined {
- return this._verifier;
- }
-
- public get canAccept(): boolean {
- return this.phase < PHASE_READY && !this._accepting && !this._declining;
- }
-
- public get accepting(): boolean {
- return this._accepting;
- }
-
- public get declining(): boolean {
- return this._declining;
- }
-
- /** whether this request has sent it's initial event and needs more events to complete */
- public get pending(): boolean {
- return !this.observeOnly && this._phase !== PHASE_DONE && this._phase !== PHASE_CANCELLED;
- }
-
- /** Only set after a .ready if the other party can scan a QR code */
- public get qrCodeData(): QRCodeData | null {
- return this._qrCodeData;
- }
-
- /** Checks whether the other party supports a given verification method.
- * This is useful when setting up the QR code UI, as it is somewhat asymmetrical:
- * if the other party supports SCAN_QR, we should show a QR code in the UI, and vice versa.
- * For methods that need to be supported by both ends, use the `methods` property.
- * @param method - the method to check
- * @param force - to check even if the phase is not ready or started yet, internal usage
- * @returns whether or not the other party said the supported the method */
- public otherPartySupportsMethod(method: string, force = false): boolean {
- if (!force && !this.ready && !this.started) {
- return false;
- }
- const theirMethodEvent = this.eventsByThem.get(REQUEST_TYPE) || this.eventsByThem.get(READY_TYPE);
- if (!theirMethodEvent) {
- // if we started straight away with .start event,
- // we are assuming that the other side will support the
- // chosen method, so return true for that.
- if (this.started && this.initiatedByMe) {
- const myStartEvent = this.eventsByUs.get(START_TYPE);
- const content = myStartEvent && myStartEvent.getContent();
- const myStartMethod = content && content.method;
- return method == myStartMethod;
- }
- return false;
- }
- const content = theirMethodEvent.getContent();
- if (!content) {
- return false;
- }
- const { methods } = content;
- if (!Array.isArray(methods)) {
- return false;
- }
-
- return methods.includes(method);
- }
-
- /** Whether this request was initiated by the syncing user.
- * For InRoomChannel, this is who sent the .request event.
- * For ToDeviceChannel, this is who sent the .start event
- */
- public get initiatedByMe(): boolean {
- // event created by us but no remote echo has been received yet
- const noEventsYet = this.eventsByUs.size + this.eventsByThem.size === 0;
- if (this._phase === PHASE_UNSENT && noEventsYet) {
- return true;
- }
- const hasMyRequest = this.eventsByUs.has(REQUEST_TYPE);
- const hasTheirRequest = this.eventsByThem.has(REQUEST_TYPE);
- if (hasMyRequest && !hasTheirRequest) {
- return true;
- }
- if (!hasMyRequest && hasTheirRequest) {
- return false;
- }
- const hasMyStart = this.eventsByUs.has(START_TYPE);
- const hasTheirStart = this.eventsByThem.has(START_TYPE);
- if (hasMyStart && !hasTheirStart) {
- return true;
- }
- return false;
- }
-
- /** The id of the user that initiated the request */
- public get requestingUserId(): string {
- if (this.initiatedByMe) {
- return this.client.getUserId()!;
- } else {
- return this.otherUserId;
- }
- }
-
- /** The id of the user that (will) receive(d) the request */
- public get receivingUserId(): string {
- if (this.initiatedByMe) {
- return this.otherUserId;
- } else {
- return this.client.getUserId()!;
- }
- }
-
- /** The user id of the other party in this request */
- public get otherUserId(): string {
- return this.channel.userId!;
- }
-
- public get isSelfVerification(): boolean {
- return this.client.getUserId() === this.otherUserId;
- }
-
- /**
- * The id of the user that cancelled the request,
- * only defined when phase is PHASE_CANCELLED
- */
- public get cancellingUserId(): string | undefined {
- const myCancel = this.eventsByUs.get(CANCEL_TYPE);
- const theirCancel = this.eventsByThem.get(CANCEL_TYPE);
-
- if (myCancel && (!theirCancel || myCancel.getId()! < theirCancel.getId()!)) {
- return myCancel.getSender();
- }
- if (theirCancel) {
- return theirCancel.getSender();
- }
- return undefined;
- }
-
- /**
- * The cancellation code e.g m.user which is responsible for cancelling this verification
- */
- public get cancellationCode(): string {
- const ev = this.getEventByEither(CANCEL_TYPE);
- return ev ? ev.getContent().code : null;
- }
-
- public get observeOnly(): boolean {
- return this._observeOnly;
- }
-
- /**
- * Gets which device the verification should be started with
- * given the events sent so far in the verification. This is the
- * same algorithm used to determine which device to send the
- * verification to when no specific device is specified.
- * @returns The device information
- */
- public get targetDevice(): ITargetDevice {
- const theirFirstEvent =
- this.eventsByThem.get(REQUEST_TYPE) ||
- this.eventsByThem.get(READY_TYPE) ||
- this.eventsByThem.get(START_TYPE);
- const theirFirstContent = theirFirstEvent?.getContent();
- const fromDevice = theirFirstContent?.from_device;
- return {
- userId: this.otherUserId,
- deviceId: fromDevice,
- };
- }
-
- /* Start the key verification, creating a verifier and sending a .start event.
- * If no previous events have been sent, pass in `targetDevice` to set who to direct this request to.
- * @param method - the name of the verification method to use.
- * @param targetDevice.userId the id of the user to direct this request to
- * @param targetDevice.deviceId the id of the device to direct this request to
- * @returns the verifier of the given method
- */
- public beginKeyVerification(
- method: VerificationMethod,
- targetDevice: ITargetDevice | null = null,
- ): VerificationBase<any, any> {
- // need to allow also when unsent in case of to_device
- if (!this.observeOnly && !this._verifier) {
- const validStartPhase =
- this.phase === PHASE_REQUESTED ||
- this.phase === PHASE_READY ||
- (this.phase === PHASE_UNSENT && this.channel.canCreateRequest(START_TYPE));
- if (validStartPhase) {
- // when called on a request that was initiated with .request event
- // check the method is supported by both sides
- if (this.commonMethods.length && !this.commonMethods.includes(method)) {
- throw newUnknownMethodError();
- }
- this._verifier = this.createVerifier(method, null, targetDevice);
- if (!this._verifier) {
- throw newUnknownMethodError();
- }
- this._chosenMethod = method;
- }
- }
- return this._verifier!;
- }
-
- /**
- * sends the initial .request event.
- * @returns resolves when the event has been sent.
- */
- public async sendRequest(): Promise<void> {
- if (!this.observeOnly && this._phase === PHASE_UNSENT) {
- const methods = [...this.verificationMethods.keys()];
- await this.channel.send(REQUEST_TYPE, { methods });
- }
- }
-
- /**
- * Cancels the request, sending a cancellation to the other party
- * @param reason - the error reason to send the cancellation with
- * @param code - the error code to send the cancellation with
- * @returns resolves when the event has been sent.
- */
- public async cancel({ reason = "User declined", code = "m.user" } = {}): Promise<void> {
- if (!this.observeOnly && this._phase !== PHASE_CANCELLED) {
- this._declining = true;
- this.emit(VerificationRequestEvent.Change);
- if (this._verifier) {
- return this._verifier.cancel(errorFactory(code, reason)());
- } else {
- this._cancellingUserId = this.client.getUserId()!;
- await this.channel.send(CANCEL_TYPE, { code, reason });
- }
- }
- }
-
- /**
- * Accepts the request, sending a .ready event to the other party
- * @returns resolves when the event has been sent.
- */
- public async accept(): Promise<void> {
- if (!this.observeOnly && this.phase === PHASE_REQUESTED && !this.initiatedByMe) {
- const methods = [...this.verificationMethods.keys()];
- this._accepting = true;
- this.emit(VerificationRequestEvent.Change);
- await this.channel.send(READY_TYPE, { methods });
- }
- }
-
- /**
- * Can be used to listen for state changes until the callback returns true.
- * @param fn - callback to evaluate whether the request is in the desired state.
- * Takes the request as an argument.
- * @returns that resolves once the callback returns true
- * @throws Error when the request is cancelled
- */
- public waitFor(fn: (request: VerificationRequest) => boolean): Promise<VerificationRequest> {
- return new Promise((resolve, reject) => {
- const check = (): boolean => {
- let handled = false;
- if (fn(this)) {
- resolve(this);
- handled = true;
- } else if (this.cancelled) {
- reject(new Error("cancelled"));
- handled = true;
- }
- if (handled) {
- this.off(VerificationRequestEvent.Change, check);
- }
- return handled;
- };
- if (!check()) {
- this.on(VerificationRequestEvent.Change, check);
- }
- });
- }
-
- private setPhase(phase: Phase, notify = true): void {
- this._phase = phase;
- if (notify) {
- this.emit(VerificationRequestEvent.Change);
- }
- }
-
- private getEventByEither(type: string): MatrixEvent | undefined {
- return this.eventsByThem.get(type) || this.eventsByUs.get(type);
- }
-
- private getEventBy(type: string, byThem = false): MatrixEvent | undefined {
- if (byThem) {
- return this.eventsByThem.get(type);
- } else {
- return this.eventsByUs.get(type);
- }
- }
-
- private calculatePhaseTransitions(): ITransition[] {
- const transitions: ITransition[] = [{ phase: PHASE_UNSENT }];
- const phase = (): Phase => transitions[transitions.length - 1].phase;
-
- // always pass by .request first to be sure channel.userId has been set
- const hasRequestByThem = this.eventsByThem.has(REQUEST_TYPE);
- const requestEvent = this.getEventBy(REQUEST_TYPE, hasRequestByThem);
- if (requestEvent) {
- transitions.push({ phase: PHASE_REQUESTED, event: requestEvent });
- }
-
- const readyEvent = requestEvent && this.getEventBy(READY_TYPE, !hasRequestByThem);
- if (readyEvent && phase() === PHASE_REQUESTED) {
- transitions.push({ phase: PHASE_READY, event: readyEvent });
- }
-
- let startEvent: MatrixEvent | undefined;
- if (readyEvent || !requestEvent) {
- const theirStartEvent = this.eventsByThem.get(START_TYPE);
- const ourStartEvent = this.eventsByUs.get(START_TYPE);
- // any party can send .start after a .ready or unsent
- if (theirStartEvent && ourStartEvent) {
- startEvent =
- theirStartEvent.getSender()! < ourStartEvent.getSender()! ? theirStartEvent : ourStartEvent;
- } else {
- startEvent = theirStartEvent ? theirStartEvent : ourStartEvent;
- }
- } else {
- startEvent = this.getEventBy(START_TYPE, !hasRequestByThem);
- }
- if (startEvent) {
- const fromRequestPhase =
- phase() === PHASE_REQUESTED && requestEvent?.getSender() !== startEvent.getSender();
- const fromUnsentPhase = phase() === PHASE_UNSENT && this.channel.canCreateRequest(START_TYPE);
- if (fromRequestPhase || phase() === PHASE_READY || fromUnsentPhase) {
- transitions.push({ phase: PHASE_STARTED, event: startEvent });
- }
- }
-
- const ourDoneEvent = this.eventsByUs.get(DONE_TYPE);
- if (this.verifierHasFinished || (ourDoneEvent && phase() === PHASE_STARTED)) {
- transitions.push({ phase: PHASE_DONE });
- }
-
- const cancelEvent = this.getEventByEither(CANCEL_TYPE);
- if ((this._cancelled || cancelEvent) && phase() !== PHASE_DONE) {
- transitions.push({ phase: PHASE_CANCELLED, event: cancelEvent });
- return transitions;
- }
-
- return transitions;
- }
-
- private transitionToPhase(transition: ITransition): void {
- const { phase, event } = transition;
- // get common methods
- if (phase === PHASE_REQUESTED || phase === PHASE_READY) {
- if (!this.wasSentByOwnDevice(event)) {
- const content = event!.getContent<{
- methods: string[];
- }>();
- this.commonMethods = content.methods.filter((m) => this.verificationMethods.has(m));
- }
- }
- // detect if we're not a party in the request, and we should just observe
- if (!this.observeOnly) {
- // if requested or accepted by one of my other devices
- if (phase === PHASE_REQUESTED || phase === PHASE_STARTED || phase === PHASE_READY) {
- if (
- this.channel.receiveStartFromOtherDevices &&
- this.wasSentByOwnUser(event) &&
- !this.wasSentByOwnDevice(event)
- ) {
- this._observeOnly = true;
- }
- }
- }
- // create verifier
- if (phase === PHASE_STARTED) {
- const { method } = event!.getContent();
- if (!this._verifier && !this.observeOnly) {
- this._verifier = this.createVerifier(method, event);
- if (!this._verifier) {
- this.cancel({
- code: "m.unknown_method",
- reason: `Unknown method: ${method}`,
- });
- } else {
- this._chosenMethod = method;
- }
- }
- }
- }
-
- private applyPhaseTransitions(): ITransition[] {
- const transitions = this.calculatePhaseTransitions();
- const existingIdx = transitions.findIndex((t) => t.phase === this.phase);
- // trim off phases we already went through, if any
- const newTransitions = transitions.slice(existingIdx + 1);
- // transition to all new phases
- for (const transition of newTransitions) {
- this.transitionToPhase(transition);
- }
- return newTransitions;
- }
-
- private isWinningStartRace(newEvent: MatrixEvent): boolean {
- if (newEvent.getType() !== START_TYPE) {
- return false;
- }
- const oldEvent = this._verifier!.startEvent;
-
- let oldRaceIdentifier;
- if (this.isSelfVerification) {
- // if the verifier does not have a startEvent,
- // it is because it's still sending and we are on the initator side
- // we know we are sending a .start event because we already
- // have a verifier (checked in calling method)
- if (oldEvent) {
- const oldContent = oldEvent.getContent();
- oldRaceIdentifier = oldContent && oldContent.from_device;
- } else {
- oldRaceIdentifier = this.client.getDeviceId();
- }
- } else {
- if (oldEvent) {
- oldRaceIdentifier = oldEvent.getSender();
- } else {
- oldRaceIdentifier = this.client.getUserId();
- }
- }
-
- let newRaceIdentifier;
- if (this.isSelfVerification) {
- const newContent = newEvent.getContent();
- newRaceIdentifier = newContent && newContent.from_device;
- } else {
- newRaceIdentifier = newEvent.getSender();
- }
- return newRaceIdentifier < oldRaceIdentifier;
- }
-
- public hasEventId(eventId: string): boolean {
- for (const event of this.eventsByUs.values()) {
- if (event.getId() === eventId) {
- return true;
- }
- }
- for (const event of this.eventsByThem.values()) {
- if (event.getId() === eventId) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Changes the state of the request and verifier in response to a key verification event.
- * @param type - the "symbolic" event type, as returned by the `getEventType` function on the channel.
- * @param event - the event to handle. Don't call getType() on it but use the `type` parameter instead.
- * @param isLiveEvent - whether this is an even received through sync or not
- * @param isRemoteEcho - whether this is the remote echo of an event sent by the same device
- * @param isSentByUs - whether this event is sent by a party that can accept and/or observe the request like one of our peers.
- * For InRoomChannel this means any device for the syncing user. For ToDeviceChannel, just the syncing device.
- * @returns a promise that resolves when any requests as an answer to the passed-in event are sent.
- */
- public async handleEvent(
- type: string,
- event: MatrixEvent,
- isLiveEvent: boolean,
- isRemoteEcho: boolean,
- isSentByUs: boolean,
- ): Promise<void> {
- // if reached phase cancelled or done, ignore anything else that comes
- if (this.done || this.cancelled) {
- return;
- }
- const wasObserveOnly = this._observeOnly;
-
- this.adjustObserveOnly(event, isLiveEvent);
-
- if (!this.observeOnly && !isRemoteEcho) {
- if (await this.cancelOnError(type, event)) {
- return;
- }
- }
-
- // This assumes verification won't need to send an event with
- // the same type for the same party twice.
- // This is true for QR and SAS verification, and was
- // added here to prevent verification getting cancelled
- // when the server duplicates an event (https://github.com/matrix-org/synapse/issues/3365)
- const isDuplicateEvent = isSentByUs ? this.eventsByUs.has(type) : this.eventsByThem.has(type);
- if (isDuplicateEvent) {
- return;
- }
-
- const oldPhase = this.phase;
- this.addEvent(type, event, isSentByUs);
-
- // this will create if needed the verifier so needs to happen before calling it
- const newTransitions = this.applyPhaseTransitions();
- try {
- // only pass events from the other side to the verifier,
- // no remote echos of our own events
- if (this._verifier && !this.observeOnly) {
- const newEventWinsRace = this.isWinningStartRace(event);
- if (this._verifier.canSwitchStartEvent(event) && newEventWinsRace) {
- this._verifier.switchStartEvent(event);
- } else if (!isRemoteEcho) {
- if (type === CANCEL_TYPE || this._verifier.events?.includes(type)) {
- this._verifier.handleEvent(event);
- }
- }
- }
-
- if (newTransitions.length) {
- // create QRCodeData if the other side can scan
- // important this happens before emitting a phase change,
- // so listeners can rely on it being there already
- // We only do this for live events because it is important that
- // we sign the keys that were in the QR code, and not the keys
- // we happen to have at some later point in time.
- if (isLiveEvent && newTransitions.some((t) => t.phase === PHASE_READY)) {
- const shouldGenerateQrCode = this.otherPartySupportsMethod(SCAN_QR_CODE_METHOD, true);
- if (shouldGenerateQrCode) {
- this._qrCodeData = await QRCodeData.create(this, this.client);
- }
- }
-
- const lastTransition = newTransitions[newTransitions.length - 1];
- const { phase } = lastTransition;
-
- this.setupTimeout(phase);
- // set phase as last thing as this emits the "change" event
- this.setPhase(phase);
- } else if (this._observeOnly !== wasObserveOnly) {
- this.emit(VerificationRequestEvent.Change);
- }
- } finally {
- // log events we processed so we can see from rageshakes what events were added to a request
- logger.log(
- `Verification request ${this.channel.transactionId}: ` +
- `${type} event with id:${event.getId()}, ` +
- `content:${JSON.stringify(event.getContent())} ` +
- `deviceId:${this.channel.deviceId}, ` +
- `sender:${event.getSender()}, isSentByUs:${isSentByUs}, ` +
- `isLiveEvent:${isLiveEvent}, isRemoteEcho:${isRemoteEcho}, ` +
- `phase:${oldPhase}=>${this.phase}, ` +
- `observeOnly:${wasObserveOnly}=>${this._observeOnly}`,
- );
- }
- }
-
- private setupTimeout(phase: Phase): void {
- const shouldTimeout = !this.timeoutTimer && !this.observeOnly && phase === PHASE_REQUESTED;
-
- if (shouldTimeout) {
- this.timeoutTimer = setTimeout(this.cancelOnTimeout, this.timeout);
- }
- if (this.timeoutTimer) {
- const shouldClear =
- phase === PHASE_STARTED || phase === PHASE_READY || phase === PHASE_DONE || phase === PHASE_CANCELLED;
- if (shouldClear) {
- clearTimeout(this.timeoutTimer);
- this.timeoutTimer = null;
- }
- }
- }
-
- private cancelOnTimeout = async (): Promise<void> => {
- try {
- if (this.initiatedByMe) {
- await this.cancel({
- reason: "Other party didn't accept in time",
- code: "m.timeout",
- });
- } else {
- await this.cancel({
- reason: "User didn't accept in time",
- code: "m.timeout",
- });
- }
- } catch (err) {
- logger.error("Error while cancelling verification request", err);
- }
- };
-
- private async cancelOnError(type: string, event: MatrixEvent): Promise<boolean> {
- if (type === START_TYPE) {
- const method = event.getContent().method;
- if (!this.verificationMethods.has(method)) {
- await this.cancel(errorFromEvent(newUnknownMethodError()));
- return true;
- }
- }
-
- const isUnexpectedRequest = type === REQUEST_TYPE && this.phase !== PHASE_UNSENT;
- const isUnexpectedReady = type === READY_TYPE && this.phase !== PHASE_REQUESTED && this.phase !== PHASE_STARTED;
- // only if phase has passed from PHASE_UNSENT should we cancel, because events
- // are allowed to come in in any order (at least with InRoomChannel). So we only know
- // we're dealing with a valid request we should participate in once we've moved to PHASE_REQUESTED.
- // Before that, we could be looking at somebody else's verification request and we just
- // happen to be in the room
- if (this.phase !== PHASE_UNSENT && (isUnexpectedRequest || isUnexpectedReady)) {
- logger.warn(`Cancelling, unexpected ${type} verification ` + `event from ${event.getSender()}`);
- const reason = `Unexpected ${type} event in phase ${this.phase}`;
- await this.cancel(errorFromEvent(newUnexpectedMessageError({ reason })));
- return true;
- }
- return false;
- }
-
- private adjustObserveOnly(event: MatrixEvent, isLiveEvent = false): void {
- // don't send out events for historical requests
- if (!isLiveEvent) {
- this._observeOnly = true;
- }
- if (this.calculateEventTimeout(event) < VERIFICATION_REQUEST_MARGIN) {
- this._observeOnly = true;
- }
- }
-
- private addEvent(type: string, event: MatrixEvent, isSentByUs = false): void {
- if (isSentByUs) {
- this.eventsByUs.set(type, event);
- } else {
- this.eventsByThem.set(type, event);
- }
-
- // once we know the userId of the other party (from the .request event)
- // see if any event by anyone else crept into this.eventsByThem
- if (type === REQUEST_TYPE) {
- for (const [type, event] of this.eventsByThem.entries()) {
- if (event.getSender() !== this.otherUserId) {
- this.eventsByThem.delete(type);
- }
- }
- // also remember when we received the request event
- this.requestReceivedAt = Date.now();
- }
- }
-
- private createVerifier(
- method: VerificationMethod,
- startEvent: MatrixEvent | null = null,
- targetDevice: ITargetDevice | null = null,
- ): VerificationBase<any, any> | undefined {
- if (!targetDevice) {
- targetDevice = this.targetDevice;
- }
- const { userId, deviceId } = targetDevice;
-
- const VerifierCtor = this.verificationMethods.get(method);
- if (!VerifierCtor) {
- logger.warn("could not find verifier constructor for method", method);
- return;
- }
- return new VerifierCtor(this.channel, this.client, userId!, deviceId!, startEvent, this);
- }
-
- private wasSentByOwnUser(event?: MatrixEvent): boolean {
- return event?.getSender() === this.client.getUserId();
- }
-
- // only for .request, .ready or .start
- private wasSentByOwnDevice(event?: MatrixEvent): boolean {
- if (!this.wasSentByOwnUser(event)) {
- return false;
- }
- const content = event!.getContent();
- if (!content || content.from_device !== this.client.getDeviceId()) {
- return false;
- }
- return true;
- }
-
- public onVerifierCancelled(): void {
- this._cancelled = true;
- // move to cancelled phase
- const newTransitions = this.applyPhaseTransitions();
- if (newTransitions.length) {
- this.setPhase(newTransitions[newTransitions.length - 1].phase);
- }
- }
-
- public onVerifierFinished(): void {
- this.channel.send(EventType.KeyVerificationDone, {});
- this.verifierHasFinished = true;
- // move to .done phase
- const newTransitions = this.applyPhaseTransitions();
- if (newTransitions.length) {
- this.setPhase(newTransitions[newTransitions.length - 1].phase);
- }
- }
-
- public getEventFromOtherParty(type: string): MatrixEvent | undefined {
- return this.eventsByThem.get(type);
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/embedded.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/embedded.ts
deleted file mode 100644
index a08b79a..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/embedded.ts
+++ /dev/null
@@ -1,347 +0,0 @@
-/*
-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 {
- WidgetApi,
- WidgetApiToWidgetAction,
- MatrixCapabilities,
- IWidgetApiRequest,
- IWidgetApiAcknowledgeResponseData,
- ISendEventToWidgetActionRequest,
- ISendToDeviceToWidgetActionRequest,
- ISendEventFromWidgetResponseData,
-} from "matrix-widget-api";
-
-import { IEvent, IContent, EventStatus } from "./models/event";
-import { ISendEventResponse } from "./@types/requests";
-import { EventType } from "./@types/event";
-import { logger } from "./logger";
-import { MatrixClient, ClientEvent, IMatrixClientCreateOpts, IStartClientOpts, SendToDeviceContentMap } from "./client";
-import { SyncApi, SyncState } from "./sync";
-import { SlidingSyncSdk } from "./sliding-sync-sdk";
-import { MatrixEvent } from "./models/event";
-import { User } from "./models/user";
-import { Room } from "./models/room";
-import { ToDeviceBatch, ToDevicePayload } from "./models/ToDeviceMessage";
-import { DeviceInfo } from "./crypto/deviceinfo";
-import { IOlmDevice } from "./crypto/algorithms/megolm";
-import { MapWithDefault, recursiveMapToObject } from "./utils";
-
-interface IStateEventRequest {
- eventType: string;
- stateKey?: string;
-}
-
-export interface ICapabilities {
- /**
- * Event types that this client expects to send.
- */
- sendEvent?: string[];
- /**
- * Event types that this client expects to receive.
- */
- receiveEvent?: string[];
-
- /**
- * Message types that this client expects to send, or true for all message
- * types.
- */
- sendMessage?: string[] | true;
- /**
- * Message types that this client expects to receive, or true for all
- * message types.
- */
- receiveMessage?: string[] | true;
-
- /**
- * Types of state events that this client expects to send.
- */
- sendState?: IStateEventRequest[];
- /**
- * Types of state events that this client expects to receive.
- */
- receiveState?: IStateEventRequest[];
-
- /**
- * To-device event types that this client expects to send.
- */
- sendToDevice?: string[];
- /**
- * To-device event types that this client expects to receive.
- */
- receiveToDevice?: string[];
-
- /**
- * Whether this client needs access to TURN servers.
- * @defaultValue false
- */
- turnServers?: boolean;
-}
-
-/**
- * A MatrixClient that routes its requests through the widget API instead of the
- * real CS API.
- * @experimental This class is considered unstable!
- */
-export class RoomWidgetClient extends MatrixClient {
- private room?: Room;
- private widgetApiReady = new Promise<void>((resolve) => this.widgetApi.once("ready", resolve));
- private lifecycle?: AbortController;
- private syncState: SyncState | null = null;
-
- public constructor(
- private readonly widgetApi: WidgetApi,
- private readonly capabilities: ICapabilities,
- private readonly roomId: string,
- opts: IMatrixClientCreateOpts,
- ) {
- super(opts);
-
- // Request capabilities for the functionality this client needs to support
- if (
- capabilities.sendEvent?.length ||
- capabilities.receiveEvent?.length ||
- capabilities.sendMessage === true ||
- (Array.isArray(capabilities.sendMessage) && capabilities.sendMessage.length) ||
- capabilities.receiveMessage === true ||
- (Array.isArray(capabilities.receiveMessage) && capabilities.receiveMessage.length) ||
- capabilities.sendState?.length ||
- capabilities.receiveState?.length
- ) {
- widgetApi.requestCapabilityForRoomTimeline(roomId);
- }
- capabilities.sendEvent?.forEach((eventType) => widgetApi.requestCapabilityToSendEvent(eventType));
- capabilities.receiveEvent?.forEach((eventType) => widgetApi.requestCapabilityToReceiveEvent(eventType));
- if (capabilities.sendMessage === true) {
- widgetApi.requestCapabilityToSendMessage();
- } else if (Array.isArray(capabilities.sendMessage)) {
- capabilities.sendMessage.forEach((msgType) => widgetApi.requestCapabilityToSendMessage(msgType));
- }
- if (capabilities.receiveMessage === true) {
- widgetApi.requestCapabilityToReceiveMessage();
- } else if (Array.isArray(capabilities.receiveMessage)) {
- capabilities.receiveMessage.forEach((msgType) => widgetApi.requestCapabilityToReceiveMessage(msgType));
- }
- capabilities.sendState?.forEach(({ eventType, stateKey }) =>
- widgetApi.requestCapabilityToSendState(eventType, stateKey),
- );
- capabilities.receiveState?.forEach(({ eventType, stateKey }) =>
- widgetApi.requestCapabilityToReceiveState(eventType, stateKey),
- );
- capabilities.sendToDevice?.forEach((eventType) => widgetApi.requestCapabilityToSendToDevice(eventType));
- capabilities.receiveToDevice?.forEach((eventType) => widgetApi.requestCapabilityToReceiveToDevice(eventType));
- if (capabilities.turnServers) {
- widgetApi.requestCapability(MatrixCapabilities.MSC3846TurnServers);
- }
-
- widgetApi.on(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
- widgetApi.on(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
-
- // Open communication with the host
- widgetApi.start();
- }
-
- public async startClient(opts: IStartClientOpts = {}): Promise<void> {
- this.lifecycle = new AbortController();
-
- // Create our own user object artificially (instead of waiting for sync)
- // so it's always available, even if the user is not in any rooms etc.
- const userId = this.getUserId();
- if (userId) {
- this.store.storeUser(new User(userId));
- }
-
- // Even though we have no access token and cannot sync, the sync class
- // still has some valuable helper methods that we make use of, so we
- // instantiate it anyways
- if (opts.slidingSync) {
- this.syncApi = new SlidingSyncSdk(opts.slidingSync, this, opts, this.buildSyncApiOptions());
- } else {
- this.syncApi = new SyncApi(this, opts, this.buildSyncApiOptions());
- }
-
- this.room = this.syncApi.createRoom(this.roomId);
- this.store.storeRoom(this.room);
-
- await this.widgetApiReady;
-
- // Backfill the requested events
- // We only get the most recent event for every type + state key combo,
- // so it doesn't really matter what order we inject them in
- await Promise.all(
- this.capabilities.receiveState?.map(async ({ eventType, stateKey }) => {
- const rawEvents = await this.widgetApi.readStateEvents(eventType, undefined, stateKey, [this.roomId]);
- const events = rawEvents.map((rawEvent) => new MatrixEvent(rawEvent as Partial<IEvent>));
-
- await this.syncApi!.injectRoomEvents(this.room!, [], events);
- events.forEach((event) => {
- this.emit(ClientEvent.Event, event);
- logger.info(`Backfilled event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
- });
- }) ?? [],
- );
- this.setSyncState(SyncState.Syncing);
- logger.info("Finished backfilling events");
-
- // Watch for TURN servers, if requested
- if (this.capabilities.turnServers) this.watchTurnServers();
- }
-
- public stopClient(): void {
- this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
- this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
-
- super.stopClient();
- this.lifecycle!.abort(); // Signal to other async tasks that the client has stopped
- }
-
- public async joinRoom(roomIdOrAlias: string): Promise<Room> {
- if (roomIdOrAlias === this.roomId) return this.room!;
- throw new Error(`Unknown room: ${roomIdOrAlias}`);
- }
-
- protected async encryptAndSendEvent(room: Room, event: MatrixEvent): Promise<ISendEventResponse> {
- let response: ISendEventFromWidgetResponseData;
- try {
- response = await this.widgetApi.sendRoomEvent(event.getType(), event.getContent(), room.roomId);
- } catch (e) {
- this.updatePendingEventStatus(room, event, EventStatus.NOT_SENT);
- throw e;
- }
-
- room.updatePendingEvent(event, EventStatus.SENT, response.event_id);
- return { event_id: response.event_id };
- }
-
- public async sendStateEvent(
- roomId: string,
- eventType: string,
- content: any,
- stateKey = "",
- ): Promise<ISendEventResponse> {
- return await this.widgetApi.sendStateEvent(eventType, stateKey, content, roomId);
- }
-
- public async sendToDevice(eventType: string, contentMap: SendToDeviceContentMap): Promise<{}> {
- await this.widgetApi.sendToDevice(eventType, false, recursiveMapToObject(contentMap));
- return {};
- }
-
- public async queueToDevice({ eventType, batch }: ToDeviceBatch): Promise<void> {
- // map: user Id → device Id → payload
- const contentMap: MapWithDefault<string, Map<string, ToDevicePayload>> = new MapWithDefault(() => new Map());
- for (const { userId, deviceId, payload } of batch) {
- contentMap.getOrCreate(userId).set(deviceId, payload);
- }
-
- await this.widgetApi.sendToDevice(eventType, false, recursiveMapToObject(contentMap));
- }
-
- public async encryptAndSendToDevices(userDeviceInfoArr: IOlmDevice<DeviceInfo>[], payload: object): Promise<void> {
- // map: user Id → device Id → payload
- const contentMap: MapWithDefault<string, Map<string, object>> = new MapWithDefault(() => new Map());
- for (const {
- userId,
- deviceInfo: { deviceId },
- } of userDeviceInfoArr) {
- contentMap.getOrCreate(userId).set(deviceId, payload);
- }
-
- await this.widgetApi.sendToDevice((payload as { type: string }).type, true, recursiveMapToObject(contentMap));
- }
-
- // Overridden since we get TURN servers automatically over the widget API,
- // and this method would otherwise complain about missing an access token
- public async checkTurnServers(): Promise<boolean> {
- return this.turnServers.length > 0;
- }
-
- // Overridden since we 'sync' manually without the sync API
- public getSyncState(): SyncState | null {
- return this.syncState;
- }
-
- private setSyncState(state: SyncState): void {
- const oldState = this.syncState;
- this.syncState = state;
- this.emit(ClientEvent.Sync, state, oldState);
- }
-
- private async ack(ev: CustomEvent<IWidgetApiRequest>): Promise<void> {
- await this.widgetApi.transport.reply<IWidgetApiAcknowledgeResponseData>(ev.detail, {});
- }
-
- private onEvent = async (ev: CustomEvent<ISendEventToWidgetActionRequest>): Promise<void> => {
- ev.preventDefault();
-
- // Verify the room ID matches, since it's possible for the client to
- // send us events from other rooms if this widget is always on screen
- if (ev.detail.data.room_id === this.roomId) {
- const event = new MatrixEvent(ev.detail.data as Partial<IEvent>);
- await this.syncApi!.injectRoomEvents(this.room!, [], [event]);
- this.emit(ClientEvent.Event, event);
- this.setSyncState(SyncState.Syncing);
- logger.info(`Received event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
- } else {
- const { event_id: eventId, room_id: roomId } = ev.detail.data;
- logger.info(`Received event ${eventId} for a different room ${roomId}; discarding`);
- }
-
- await this.ack(ev);
- };
-
- private onToDevice = async (ev: CustomEvent<ISendToDeviceToWidgetActionRequest>): Promise<void> => {
- ev.preventDefault();
-
- const event = new MatrixEvent({
- type: ev.detail.data.type,
- sender: ev.detail.data.sender,
- content: ev.detail.data.content as IContent,
- });
- // Mark the event as encrypted if it was, using fake contents and keys since those are unknown to us
- if (ev.detail.data.encrypted) event.makeEncrypted(EventType.RoomMessageEncrypted, {}, "", "");
-
- this.emit(ClientEvent.ToDeviceEvent, event);
- this.setSyncState(SyncState.Syncing);
- await this.ack(ev);
- };
-
- private async watchTurnServers(): Promise<void> {
- const servers = this.widgetApi.getTurnServers();
- const onClientStopped = (): void => {
- servers.return(undefined);
- };
- this.lifecycle!.signal.addEventListener("abort", onClientStopped);
-
- try {
- for await (const server of servers) {
- this.turnServers = [
- {
- urls: server.uris,
- username: server.username,
- credential: server.password,
- },
- ];
- this.emit(ClientEvent.TurnServers, this.turnServers);
- logger.log(`Received TURN server: ${server.uris}`);
- }
- } catch (e) {
- logger.warn("Error watching TURN servers", e);
- } finally {
- this.lifecycle!.signal.removeEventListener("abort", onClientStopped);
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/errors.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/errors.ts
deleted file mode 100644
index 9d24651..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/errors.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
-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.
-*/
-
-export enum InvalidStoreState {
- ToggledLazyLoading,
-}
-
-export class InvalidStoreError extends Error {
- public static TOGGLED_LAZY_LOADING = InvalidStoreState.ToggledLazyLoading;
-
- public constructor(public readonly reason: InvalidStoreState, public readonly value: any) {
- const message =
- `Store is invalid because ${reason}, ` +
- `please stop the client, delete all data and start the client again`;
- super(message);
- this.name = "InvalidStoreError";
- }
-}
-
-export enum InvalidCryptoStoreState {
- TooNew = "TOO_NEW",
-}
-
-export class InvalidCryptoStoreError extends Error {
- public static TOO_NEW = InvalidCryptoStoreState.TooNew;
-
- public constructor(public readonly reason: InvalidCryptoStoreState) {
- const message =
- `Crypto store is invalid because ${reason}, ` +
- `please stop the client, delete all data and start the client again`;
- super(message);
- this.name = "InvalidCryptoStoreError";
- }
-}
-
-export class KeySignatureUploadError extends Error {
- public constructor(message: string, public readonly value: any) {
- super(message);
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/event-mapper.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/event-mapper.ts
deleted file mode 100644
index 828d87e..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/event-mapper.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
-Copyright 2021 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { MatrixClient } from "./client";
-import { IEvent, MatrixEvent, MatrixEventEvent } from "./models/event";
-import { RelationType } from "./@types/event";
-
-export type EventMapper = (obj: Partial<IEvent>) => MatrixEvent;
-
-export interface MapperOpts {
- // don't re-emit events emitted on an event mapped by this mapper on the client
- preventReEmit?: boolean;
- // decrypt event proactively
- decrypt?: boolean;
- // the event is a to_device event
- toDevice?: boolean;
-}
-
-export function eventMapperFor(client: MatrixClient, options: MapperOpts): EventMapper {
- let preventReEmit = Boolean(options.preventReEmit);
- const decrypt = options.decrypt !== false;
-
- function mapper(plainOldJsObject: Partial<IEvent>): MatrixEvent {
- if (options.toDevice) {
- delete plainOldJsObject.room_id;
- }
-
- const room = client.getRoom(plainOldJsObject.room_id);
-
- let event: MatrixEvent | undefined;
- // If the event is already known to the room, let's re-use the model rather than duplicating.
- // We avoid doing this to state events as they may be forward or backwards looking which tweaks behaviour.
- if (room && plainOldJsObject.state_key === undefined) {
- event = room.findEventById(plainOldJsObject.event_id!);
- }
-
- if (!event || event.status) {
- event = new MatrixEvent(plainOldJsObject);
- } else {
- // merge the latest unsigned data from the server
- event.setUnsigned({ ...event.getUnsigned(), ...plainOldJsObject.unsigned });
- // prevent doubling up re-emitters
- preventReEmit = true;
- }
-
- // if there is a complete edit bundled alongside the event, perform the replacement.
- // (prior to MSC3925, events were automatically replaced on the server-side. MSC3925 proposes that that doesn't
- // happen automatically but the server does provide us with the whole content of the edit event.)
- const bundledEdit = event.getServerAggregatedRelation<Partial<IEvent>>(RelationType.Replace);
- if (bundledEdit?.content) {
- const replacement = mapper(bundledEdit);
- // XXX: it's worth noting that the spec says we should only respect encrypted edits if, once decrypted, the
- // replacement has a `m.new_content` property. The problem is that we haven't yet decrypted the replacement
- // (it should be happening in the background), so we can't enforce this. Possibly we should for decryption
- // to complete, but that sounds a bit racy. For now, we just assume it's ok.
- event.makeReplaced(replacement);
- }
-
- const thread = room?.findThreadForEvent(event);
- if (thread) {
- event.setThread(thread);
- }
-
- // TODO: once we get rid of the old libolm-backed crypto, we can restrict this to room events (rather than
- // to-device events), because the rust implementation decrypts to-device messages at a higher level.
- // Generally we probably want to use a different eventMapper implementation for to-device events because
- if (event.isEncrypted()) {
- if (!preventReEmit) {
- client.reEmitter.reEmit(event, [MatrixEventEvent.Decrypted]);
- }
- if (decrypt) {
- client.decryptEventIfNeeded(event);
- }
- }
-
- if (!preventReEmit) {
- client.reEmitter.reEmit(event, [MatrixEventEvent.Replaced, MatrixEventEvent.VisibilityChange]);
- room?.reEmitter.reEmit(event, [MatrixEventEvent.BeforeRedaction]);
- }
- return event;
- }
-
- return mapper;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/ExtensibleEvent.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/ExtensibleEvent.ts
deleted file mode 100644
index 0496592..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/ExtensibleEvent.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
-Copyright 2021 - 2023 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 { ExtensibleEventType, IPartialEvent } from "../@types/extensible_events";
-
-/**
- * Represents an Extensible Event in Matrix.
- */
-export abstract class ExtensibleEvent<TContent extends object = object> {
- protected constructor(public readonly wireFormat: IPartialEvent<TContent>) {}
-
- /**
- * Shortcut to wireFormat.content
- */
- public get wireContent(): TContent {
- return this.wireFormat.content;
- }
-
- /**
- * Serializes the event into a format which can be used to send the
- * event to the room.
- * @returns The serialized event.
- */
- public abstract serialize(): IPartialEvent<object>;
-
- /**
- * Determines if this event is equivalent to the provided event type.
- * This is recommended over `instanceof` checks due to issues in the JS
- * runtime (and layering of dependencies in some projects).
- *
- * Implementations should pass this check off to their super classes
- * if their own checks fail. Some primary implementations do not extend
- * fallback classes given they support the primary type first. Thus,
- * those classes may return false if asked about their fallback
- * representation.
- *
- * Note that this only checks primary event types: legacy events, like
- * m.room.message, should/will fail this check.
- * @param primaryEventType - The (potentially namespaced) event
- * type.
- * @returns True if this event *could* be represented as the
- * given type.
- */
- public abstract isEquivalentTo(primaryEventType: ExtensibleEventType): boolean;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/InvalidEventError.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/InvalidEventError.ts
deleted file mode 100644
index 12e59ad..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/InvalidEventError.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
-Copyright 2022 - 2023 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.
-*/
-
-/**
- * Thrown when an event is unforgivably unparsable.
- */
-export class InvalidEventError extends Error {
- public constructor(message: string) {
- super(message);
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/MessageEvent.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/MessageEvent.ts
deleted file mode 100644
index 3d049f4..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/MessageEvent.ts
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
-Copyright 2022 - 2023 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 { Optional } from "matrix-events-sdk";
-
-import { ExtensibleEvent } from "./ExtensibleEvent";
-import {
- ExtensibleEventType,
- IMessageRendering,
- IPartialEvent,
- isEventTypeSame,
- M_HTML,
- M_MESSAGE,
- ExtensibleAnyMessageEventContent,
- M_TEXT,
-} from "../@types/extensible_events";
-import { isOptionalAString, isProvided } from "./utilities";
-import { InvalidEventError } from "./InvalidEventError";
-
-/**
- * Represents a message event. Message events are the simplest form of event with
- * just text (optionally of different mimetypes, like HTML).
- *
- * Message events can additionally be an Emote or Notice, though typically those
- * are represented as EmoteEvent and NoticeEvent respectively.
- */
-export class MessageEvent extends ExtensibleEvent<ExtensibleAnyMessageEventContent> {
- /**
- * The default text for the event.
- */
- public readonly text: string;
-
- /**
- * The default HTML for the event, if provided.
- */
- public readonly html: Optional<string>;
-
- /**
- * All the different renderings of the message. Note that this is the same
- * format as an m.message body but may contain elements not found directly
- * in the event content: this is because this is interpreted based off the
- * other information available in the event.
- */
- public readonly renderings: IMessageRendering[];
-
- /**
- * Creates a new MessageEvent from a pure format. Note that the event is
- * *not* parsed here: it will be treated as a literal m.message primary
- * typed event.
- * @param wireFormat - The event.
- */
- public constructor(wireFormat: IPartialEvent<ExtensibleAnyMessageEventContent>) {
- super(wireFormat);
-
- const mmessage = M_MESSAGE.findIn(this.wireContent);
- const mtext = M_TEXT.findIn<string>(this.wireContent);
- const mhtml = M_HTML.findIn<string>(this.wireContent);
- if (isProvided(mmessage)) {
- if (!Array.isArray(mmessage)) {
- throw new InvalidEventError("m.message contents must be an array");
- }
- const text = mmessage.find((r) => !isProvided(r.mimetype) || r.mimetype === "text/plain");
- const html = mmessage.find((r) => r.mimetype === "text/html");
-
- if (!text) throw new InvalidEventError("m.message is missing a plain text representation");
-
- this.text = text.body;
- this.html = html?.body;
- this.renderings = mmessage;
- } else if (isOptionalAString(mtext)) {
- this.text = mtext;
- this.html = mhtml;
- this.renderings = [{ body: mtext, mimetype: "text/plain" }];
- if (this.html) {
- this.renderings.push({ body: this.html, mimetype: "text/html" });
- }
- } else {
- throw new InvalidEventError("Missing textual representation for event");
- }
- }
-
- public isEquivalentTo(primaryEventType: ExtensibleEventType): boolean {
- return isEventTypeSame(primaryEventType, M_MESSAGE);
- }
-
- protected serializeMMessageOnly(): ExtensibleAnyMessageEventContent {
- let messageRendering: ExtensibleAnyMessageEventContent = {
- [M_MESSAGE.name]: this.renderings,
- };
-
- // Use the shorthand if it's just a simple text event
- if (this.renderings.length === 1) {
- const mime = this.renderings[0].mimetype;
- if (mime === undefined || mime === "text/plain") {
- messageRendering = {
- [M_TEXT.name]: this.renderings[0].body,
- };
- }
- }
-
- return messageRendering;
- }
-
- public serialize(): IPartialEvent<object> {
- return {
- type: "m.room.message",
- content: {
- ...this.serializeMMessageOnly(),
- body: this.text,
- msgtype: "m.text",
- format: this.html ? "org.matrix.custom.html" : undefined,
- formatted_body: this.html ?? undefined,
- },
- };
- }
-
- /**
- * Creates a new MessageEvent from text and HTML.
- * @param text - The text.
- * @param html - Optional HTML.
- * @returns The representative message event.
- */
- public static from(text: string, html?: string): MessageEvent {
- return new MessageEvent({
- type: M_MESSAGE.name,
- content: {
- [M_TEXT.name]: text,
- [M_HTML.name]: html,
- },
- });
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/PollEndEvent.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/PollEndEvent.ts
deleted file mode 100644
index 243f190..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/PollEndEvent.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
-Copyright 2022 - 2023 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 {
- ExtensibleEventType,
- IPartialEvent,
- isEventTypeSame,
- M_TEXT,
- REFERENCE_RELATION,
-} from "../@types/extensible_events";
-import { M_POLL_END, PollEndEventContent } from "../@types/polls";
-import { ExtensibleEvent } from "./ExtensibleEvent";
-import { InvalidEventError } from "./InvalidEventError";
-import { MessageEvent } from "./MessageEvent";
-
-/**
- * Represents a poll end/closure event.
- */
-export class PollEndEvent extends ExtensibleEvent<PollEndEventContent> {
- /**
- * The poll start event ID referenced by the response.
- */
- public readonly pollEventId: string;
-
- /**
- * The closing message for the event.
- */
- public readonly closingMessage: MessageEvent;
-
- /**
- * Creates a new PollEndEvent from a pure format. Note that the event is *not*
- * parsed here: it will be treated as a literal m.poll.response primary typed event.
- * @param wireFormat - The event.
- */
- public constructor(wireFormat: IPartialEvent<PollEndEventContent>) {
- super(wireFormat);
-
- const rel = this.wireContent["m.relates_to"];
- if (!REFERENCE_RELATION.matches(rel?.rel_type) || typeof rel?.event_id !== "string") {
- throw new InvalidEventError("Relationship must be a reference to an event");
- }
-
- this.pollEventId = rel.event_id;
- this.closingMessage = new MessageEvent(this.wireFormat);
- }
-
- public isEquivalentTo(primaryEventType: ExtensibleEventType): boolean {
- return isEventTypeSame(primaryEventType, M_POLL_END);
- }
-
- public serialize(): IPartialEvent<object> {
- return {
- type: M_POLL_END.name,
- content: {
- "m.relates_to": {
- rel_type: REFERENCE_RELATION.name,
- event_id: this.pollEventId,
- },
- [M_POLL_END.name]: {},
- ...this.closingMessage.serialize().content,
- },
- };
- }
-
- /**
- * Creates a new PollEndEvent from a poll event ID.
- * @param pollEventId - The poll start event ID.
- * @param message - A closing message, typically revealing the top answer.
- * @returns The representative poll closure event.
- */
- public static from(pollEventId: string, message: string): PollEndEvent {
- return new PollEndEvent({
- type: M_POLL_END.name,
- content: {
- "m.relates_to": {
- rel_type: REFERENCE_RELATION.name,
- event_id: pollEventId,
- },
- [M_POLL_END.name]: {},
- [M_TEXT.name]: message,
- },
- });
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/PollResponseEvent.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/PollResponseEvent.ts
deleted file mode 100644
index a61fc2e..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/PollResponseEvent.ts
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
-Copyright 2022 - 2023 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 { ExtensibleEvent } from "./ExtensibleEvent";
-import { M_POLL_RESPONSE, PollResponseEventContent, PollResponseSubtype } from "../@types/polls";
-import { ExtensibleEventType, IPartialEvent, isEventTypeSame, REFERENCE_RELATION } from "../@types/extensible_events";
-import { InvalidEventError } from "./InvalidEventError";
-import { PollStartEvent } from "./PollStartEvent";
-
-/**
- * Represents a poll response event.
- */
-export class PollResponseEvent extends ExtensibleEvent<PollResponseEventContent> {
- private internalAnswerIds: string[] = [];
- private internalSpoiled = false;
-
- /**
- * The provided answers for the poll. Note that this may be falsy/unpredictable if
- * the `spoiled` property is true.
- */
- public get answerIds(): string[] {
- return this.internalAnswerIds;
- }
-
- /**
- * The poll start event ID referenced by the response.
- */
- public readonly pollEventId: string;
-
- /**
- * Whether the vote is spoiled.
- */
- public get spoiled(): boolean {
- return this.internalSpoiled;
- }
-
- /**
- * Creates a new PollResponseEvent from a pure format. Note that the event is *not*
- * parsed here: it will be treated as a literal m.poll.response primary typed event.
- *
- * To validate the response against a poll, call `validateAgainst` after creation.
- * @param wireFormat - The event.
- */
- public constructor(wireFormat: IPartialEvent<PollResponseEventContent>) {
- super(wireFormat);
-
- const rel = this.wireContent["m.relates_to"];
- if (!REFERENCE_RELATION.matches(rel?.rel_type) || typeof rel?.event_id !== "string") {
- throw new InvalidEventError("Relationship must be a reference to an event");
- }
-
- this.pollEventId = rel.event_id;
- this.validateAgainst(null);
- }
-
- /**
- * Validates the poll response using the poll start event as a frame of reference. This
- * is used to determine if the vote is spoiled, whether the answers are valid, etc.
- * @param poll - The poll start event.
- */
- public validateAgainst(poll: PollStartEvent | null): void {
- const response = M_POLL_RESPONSE.findIn<PollResponseSubtype>(this.wireContent);
- if (!Array.isArray(response?.answers)) {
- this.internalSpoiled = true;
- this.internalAnswerIds = [];
- return;
- }
-
- let answers = response?.answers ?? [];
- if (answers.some((a) => typeof a !== "string") || answers.length === 0) {
- this.internalSpoiled = true;
- this.internalAnswerIds = [];
- return;
- }
-
- if (poll) {
- if (answers.some((a) => !poll.answers.some((pa) => pa.id === a))) {
- this.internalSpoiled = true;
- this.internalAnswerIds = [];
- return;
- }
-
- answers = answers.slice(0, poll.maxSelections);
- }
-
- this.internalAnswerIds = answers;
- this.internalSpoiled = false;
- }
-
- public isEquivalentTo(primaryEventType: ExtensibleEventType): boolean {
- return isEventTypeSame(primaryEventType, M_POLL_RESPONSE);
- }
-
- public serialize(): IPartialEvent<object> {
- return {
- type: M_POLL_RESPONSE.name,
- content: {
- "m.relates_to": {
- rel_type: REFERENCE_RELATION.name,
- event_id: this.pollEventId,
- },
- [M_POLL_RESPONSE.name]: {
- answers: this.spoiled ? undefined : this.answerIds,
- },
- },
- };
- }
-
- /**
- * Creates a new PollResponseEvent from a set of answers. To spoil the vote, pass an empty
- * answers array.
- * @param answers - The user's answers. Should be valid from a poll's answer IDs.
- * @param pollEventId - The poll start event ID.
- * @returns The representative poll response event.
- */
- public static from(answers: string[], pollEventId: string): PollResponseEvent {
- return new PollResponseEvent({
- type: M_POLL_RESPONSE.name,
- content: {
- "m.relates_to": {
- rel_type: REFERENCE_RELATION.name,
- event_id: pollEventId,
- },
- [M_POLL_RESPONSE.name]: {
- answers: answers,
- },
- },
- });
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/PollStartEvent.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/PollStartEvent.ts
deleted file mode 100644
index 8584bf9..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/PollStartEvent.ts
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
-Copyright 2022 - 2023 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 { NamespacedValue } from "matrix-events-sdk";
-
-import { MessageEvent } from "./MessageEvent";
-import { ExtensibleEventType, IPartialEvent, isEventTypeSame, M_TEXT } from "../@types/extensible_events";
-import {
- KnownPollKind,
- M_POLL_KIND_DISCLOSED,
- M_POLL_KIND_UNDISCLOSED,
- M_POLL_START,
- PollStartEventContent,
- PollStartSubtype,
- PollAnswer,
-} from "../@types/polls";
-import { InvalidEventError } from "./InvalidEventError";
-import { ExtensibleEvent } from "./ExtensibleEvent";
-
-/**
- * Represents a poll answer. Note that this is represented as a subtype and is
- * not registered as a parsable event - it is implied for usage exclusively
- * within the PollStartEvent parsing.
- */
-export class PollAnswerSubevent extends MessageEvent {
- /**
- * The answer ID.
- */
- public readonly id: string;
-
- public constructor(wireFormat: IPartialEvent<PollAnswer>) {
- super(wireFormat);
-
- const id = wireFormat.content.id;
- if (!id || typeof id !== "string") {
- throw new InvalidEventError("Answer ID must be a non-empty string");
- }
- this.id = id;
- }
-
- public serialize(): IPartialEvent<object> {
- return {
- type: "org.matrix.sdk.poll.answer",
- content: {
- id: this.id,
- ...this.serializeMMessageOnly(),
- },
- };
- }
-
- /**
- * Creates a new PollAnswerSubevent from ID and text.
- * @param id - The answer ID (unique within the poll).
- * @param text - The text.
- * @returns The representative answer.
- */
- public static from(id: string, text: string): PollAnswerSubevent {
- return new PollAnswerSubevent({
- type: "org.matrix.sdk.poll.answer",
- content: {
- id: id,
- [M_TEXT.name]: text,
- },
- });
- }
-}
-
-/**
- * Represents a poll start event.
- */
-export class PollStartEvent extends ExtensibleEvent<PollStartEventContent> {
- /**
- * The question being asked, as a MessageEvent node.
- */
- public readonly question: MessageEvent;
-
- /**
- * The interpreted kind of poll. Note that this will infer a value that is known to the
- * SDK rather than verbatim - this means unknown types will be represented as undisclosed
- * polls.
- *
- * To get the raw kind, use rawKind.
- */
- public readonly kind: KnownPollKind;
-
- /**
- * The true kind as provided by the event sender. Might not be valid.
- */
- public readonly rawKind: string;
-
- /**
- * The maximum number of selections a user is allowed to make.
- */
- public readonly maxSelections: number;
-
- /**
- * The possible answers for the poll.
- */
- public readonly answers: PollAnswerSubevent[];
-
- /**
- * Creates a new PollStartEvent from a pure format. Note that the event is *not*
- * parsed here: it will be treated as a literal m.poll.start primary typed event.
- * @param wireFormat - The event.
- */
- public constructor(wireFormat: IPartialEvent<PollStartEventContent>) {
- super(wireFormat);
-
- const poll = M_POLL_START.findIn<PollStartSubtype>(this.wireContent);
-
- if (!poll?.question) {
- throw new InvalidEventError("A question is required");
- }
-
- this.question = new MessageEvent({ type: "org.matrix.sdk.poll.question", content: poll.question });
-
- this.rawKind = poll.kind;
- if (M_POLL_KIND_DISCLOSED.matches(this.rawKind)) {
- this.kind = M_POLL_KIND_DISCLOSED;
- } else {
- this.kind = M_POLL_KIND_UNDISCLOSED; // default & assumed value
- }
-
- this.maxSelections =
- Number.isFinite(poll.max_selections) && poll.max_selections! > 0 ? poll.max_selections! : 1;
-
- if (!Array.isArray(poll.answers)) {
- throw new InvalidEventError("Poll answers must be an array");
- }
- const answers = poll.answers.slice(0, 20).map(
- (a) =>
- new PollAnswerSubevent({
- type: "org.matrix.sdk.poll.answer",
- content: a,
- }),
- );
- if (answers.length <= 0) {
- throw new InvalidEventError("No answers available");
- }
- this.answers = answers;
- }
-
- public isEquivalentTo(primaryEventType: ExtensibleEventType): boolean {
- return isEventTypeSame(primaryEventType, M_POLL_START);
- }
-
- public serialize(): IPartialEvent<object> {
- return {
- type: M_POLL_START.name,
- content: {
- [M_POLL_START.name]: {
- question: this.question.serialize().content,
- kind: this.rawKind,
- max_selections: this.maxSelections,
- answers: this.answers.map((a) => a.serialize().content),
- },
- [M_TEXT.name]: `${this.question.text}\n${this.answers.map((a, i) => `${i + 1}. ${a.text}`).join("\n")}`,
- },
- };
- }
-
- /**
- * Creates a new PollStartEvent from question, answers, and metadata.
- * @param question - The question to ask.
- * @param answers - The answers. Should be unique within each other.
- * @param kind - The kind of poll.
- * @param maxSelections - The maximum number of selections. Must be 1 or higher.
- * @returns The representative poll start event.
- */
- public static from(
- question: string,
- answers: string[],
- kind: KnownPollKind | string,
- maxSelections = 1,
- ): PollStartEvent {
- return new PollStartEvent({
- type: M_POLL_START.name,
- content: {
- [M_TEXT.name]: question, // unused by parsing
- [M_POLL_START.name]: {
- question: { [M_TEXT.name]: question },
- kind: kind instanceof NamespacedValue ? kind.name : kind,
- max_selections: maxSelections,
- answers: answers.map((a) => ({ id: makeId(), [M_TEXT.name]: a })),
- },
- },
- });
- }
-}
-
-const LETTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
-function makeId(): string {
- return [...Array(16)].map(() => LETTERS.charAt(Math.floor(Math.random() * LETTERS.length))).join("");
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/utilities.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/utilities.ts
deleted file mode 100644
index 0660442..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/extensible_events_v1/utilities.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
-Copyright 2021 - 2023 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 { Optional } from "matrix-events-sdk";
-
-/**
- * Determines if the given optional was provided a value.
- * @param s - The optional to test.
- * @returns True if the value is defined.
- */
-export function isProvided<T>(s: Optional<T>): boolean {
- return s !== null && s !== undefined;
-}
-
-/**
- * Determines if the given optional string is a defined string.
- * @param s - The input string.
- * @returns True if the input is a defined string.
- */
-export function isOptionalAString(s: Optional<string>): s is string {
- return isProvided(s) && typeof s === "string";
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/feature.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/feature.ts
deleted file mode 100644
index 9141e81..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/feature.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
-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 { IServerVersions } from "./client";
-
-export enum ServerSupport {
- Stable,
- Unstable,
- Unsupported,
-}
-
-export enum Feature {
- Thread = "Thread",
- ThreadUnreadNotifications = "ThreadUnreadNotifications",
- LoginTokenRequest = "LoginTokenRequest",
- RelationBasedRedactions = "RelationBasedRedactions",
- AccountDataDeletion = "AccountDataDeletion",
-}
-
-type FeatureSupportCondition = {
- unstablePrefixes?: string[];
- matrixVersion?: string;
-};
-
-const featureSupportResolver: Record<string, FeatureSupportCondition> = {
- [Feature.Thread]: {
- unstablePrefixes: ["org.matrix.msc3440"],
- matrixVersion: "v1.3",
- },
- [Feature.ThreadUnreadNotifications]: {
- unstablePrefixes: ["org.matrix.msc3771", "org.matrix.msc3773"],
- matrixVersion: "v1.4",
- },
- [Feature.LoginTokenRequest]: {
- unstablePrefixes: ["org.matrix.msc3882"],
- },
- [Feature.RelationBasedRedactions]: {
- unstablePrefixes: ["org.matrix.msc3912"],
- },
- [Feature.AccountDataDeletion]: {
- unstablePrefixes: ["org.matrix.msc3391"],
- },
-};
-
-export async function buildFeatureSupportMap(versions: IServerVersions): Promise<Map<Feature, ServerSupport>> {
- const supportMap = new Map<Feature, ServerSupport>();
- for (const [feature, supportCondition] of Object.entries(featureSupportResolver)) {
- const supportMatrixVersion = versions.versions?.includes(supportCondition.matrixVersion || "") ?? false;
- const supportUnstablePrefixes =
- supportCondition.unstablePrefixes?.every((unstablePrefix) => {
- return versions.unstable_features?.[unstablePrefix] === true;
- }) ?? false;
- if (supportMatrixVersion) {
- supportMap.set(feature as Feature, ServerSupport.Stable);
- } else if (supportUnstablePrefixes) {
- supportMap.set(feature as Feature, ServerSupport.Unstable);
- } else {
- supportMap.set(feature as Feature, ServerSupport.Unsupported);
- }
- }
- return supportMap;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/filter-component.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/filter-component.ts
deleted file mode 100644
index e28571d..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/filter-component.ts
+++ /dev/null
@@ -1,204 +0,0 @@
-/*
-Copyright 2016 - 2021 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 { RelationType } from "./@types/event";
-import { MatrixEvent } from "./models/event";
-import { FILTER_RELATED_BY_REL_TYPES, FILTER_RELATED_BY_SENDERS, THREAD_RELATION_TYPE } from "./models/thread";
-
-/**
- * Checks if a value matches a given field value, which may be a * terminated
- * wildcard pattern.
- * @param actualValue - The value to be compared
- * @param filterValue - The filter pattern to be compared
- * @returns true if the actualValue matches the filterValue
- */
-function matchesWildcard(actualValue: string, filterValue: string): boolean {
- if (filterValue.endsWith("*")) {
- const typePrefix = filterValue.slice(0, -1);
- return actualValue.slice(0, typePrefix.length) === typePrefix;
- } else {
- return actualValue === filterValue;
- }
-}
-
-/* eslint-disable camelcase */
-export interface IFilterComponent {
- "types"?: string[];
- "not_types"?: string[];
- "rooms"?: string[];
- "not_rooms"?: string[];
- "senders"?: string[];
- "not_senders"?: string[];
- "contains_url"?: boolean;
- "limit"?: number;
- "related_by_senders"?: Array<RelationType | string>;
- "related_by_rel_types"?: string[];
-
- // Unstable values
- "io.element.relation_senders"?: Array<RelationType | string>;
- "io.element.relation_types"?: string[];
-}
-/* eslint-enable camelcase */
-
-/**
- * FilterComponent is a section of a Filter definition which defines the
- * types, rooms, senders filters etc to be applied to a particular type of resource.
- * This is all ported over from synapse's Filter object.
- *
- * N.B. that synapse refers to these as 'Filters', and what js-sdk refers to as
- * 'Filters' are referred to as 'FilterCollections'.
- */
-export class FilterComponent {
- public constructor(private filterJson: IFilterComponent, public readonly userId?: string | undefined | null) {}
-
- /**
- * Checks with the filter component matches the given event
- * @param event - event to be checked against the filter
- * @returns true if the event matches the filter
- */
- public check(event: MatrixEvent): boolean {
- const bundledRelationships = event.getUnsigned()?.["m.relations"] || {};
- const relations: Array<string | RelationType> = Object.keys(bundledRelationships);
- // Relation senders allows in theory a look-up of any senders
- // however clients can only know about the current user participation status
- // as sending a whole list of participants could be proven problematic in terms
- // of performance
- // This should be improved when bundled relationships solve that problem
- const relationSenders: string[] = [];
- if (this.userId && bundledRelationships?.[THREAD_RELATION_TYPE.name]?.current_user_participated) {
- relationSenders.push(this.userId);
- }
-
- return this.checkFields(
- event.getRoomId(),
- event.getSender(),
- event.getType(),
- event.getContent() ? event.getContent().url !== undefined : false,
- relations,
- relationSenders,
- );
- }
-
- /**
- * Converts the filter component into the form expected over the wire
- */
- public toJSON(): object {
- return {
- types: this.filterJson.types || null,
- not_types: this.filterJson.not_types || [],
- rooms: this.filterJson.rooms || null,
- not_rooms: this.filterJson.not_rooms || [],
- senders: this.filterJson.senders || null,
- not_senders: this.filterJson.not_senders || [],
- contains_url: this.filterJson.contains_url || null,
- [FILTER_RELATED_BY_SENDERS.name]: this.filterJson[FILTER_RELATED_BY_SENDERS.name] || [],
- [FILTER_RELATED_BY_REL_TYPES.name]: this.filterJson[FILTER_RELATED_BY_REL_TYPES.name] || [],
- };
- }
-
- /**
- * Checks whether the filter component matches the given event fields.
- * @param roomId - the roomId for the event being checked
- * @param sender - the sender of the event being checked
- * @param eventType - the type of the event being checked
- * @param containsUrl - whether the event contains a content.url field
- * @param relationTypes - whether has aggregated relation of the given type
- * @param relationSenders - whether one of the relation is sent by the user listed
- * @returns true if the event fields match the filter
- */
- private checkFields(
- roomId: string | undefined,
- sender: string | undefined,
- eventType: string,
- containsUrl: boolean,
- relationTypes: Array<RelationType | string>,
- relationSenders: string[],
- ): boolean {
- const literalKeys = {
- rooms: function (v: string): boolean {
- return roomId === v;
- },
- senders: function (v: string): boolean {
- return sender === v;
- },
- types: function (v: string): boolean {
- return matchesWildcard(eventType, v);
- },
- } as const;
-
- for (const name in literalKeys) {
- const matchFunc = literalKeys[<keyof typeof literalKeys>name];
- const notName = "not_" + name;
- const disallowedValues = this.filterJson[<`not_${keyof typeof literalKeys}`>notName];
- if (disallowedValues?.some(matchFunc)) {
- return false;
- }
-
- const allowedValues = this.filterJson[name as keyof typeof literalKeys];
- if (allowedValues && !allowedValues.some(matchFunc)) {
- return false;
- }
- }
-
- const containsUrlFilter = this.filterJson.contains_url;
- if (containsUrlFilter !== undefined && containsUrlFilter !== containsUrl) {
- return false;
- }
-
- const relationTypesFilter = this.filterJson[FILTER_RELATED_BY_REL_TYPES.name];
- if (relationTypesFilter !== undefined) {
- if (!this.arrayMatchesFilter(relationTypesFilter, relationTypes)) {
- return false;
- }
- }
-
- const relationSendersFilter = this.filterJson[FILTER_RELATED_BY_SENDERS.name];
- if (relationSendersFilter !== undefined) {
- if (!this.arrayMatchesFilter(relationSendersFilter, relationSenders)) {
- return false;
- }
- }
-
- return true;
- }
-
- private arrayMatchesFilter(filter: any[], values: any[]): boolean {
- return (
- values.length > 0 &&
- filter.every((value) => {
- return values.includes(value);
- })
- );
- }
-
- /**
- * Filters a list of events down to those which match this filter component
- * @param events - Events to be checked against the filter component
- * @returns events which matched the filter component
- */
- public filter(events: MatrixEvent[]): MatrixEvent[] {
- return events.filter(this.check, this);
- }
-
- /**
- * Returns the limit field for a given filter component, providing a default of
- * 10 if none is otherwise specified. Cargo-culted from Synapse.
- * @returns the limit for this filter component.
- */
- public limit(): number {
- return this.filterJson.limit !== undefined ? this.filterJson.limit : 10;
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/filter.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/filter.ts
deleted file mode 100644
index 4d74c8c..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/filter.ts
+++ /dev/null
@@ -1,242 +0,0 @@
-/*
-Copyright 2015 - 2021 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 { EventType, RelationType } from "./@types/event";
-import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync";
-import { FilterComponent, IFilterComponent } from "./filter-component";
-import { MatrixEvent } from "./models/event";
-
-/**
- */
-function setProp(obj: Record<string, any>, keyNesting: string, val: any): void {
- const nestedKeys = keyNesting.split(".") as [keyof typeof obj];
- let currentObj = obj;
- for (let i = 0; i < nestedKeys.length - 1; i++) {
- if (!currentObj[nestedKeys[i]]) {
- currentObj[nestedKeys[i]] = {};
- }
- currentObj = currentObj[nestedKeys[i]];
- }
- currentObj[nestedKeys[nestedKeys.length - 1]] = val;
-}
-
-/* eslint-disable camelcase */
-export interface IFilterDefinition {
- event_fields?: string[];
- event_format?: "client" | "federation";
- presence?: IFilterComponent;
- account_data?: IFilterComponent;
- room?: IRoomFilter;
-}
-
-export interface IRoomEventFilter extends IFilterComponent {
- "lazy_load_members"?: boolean;
- "include_redundant_members"?: boolean;
- "types"?: Array<EventType | string>;
- "related_by_senders"?: Array<RelationType | string>;
- "related_by_rel_types"?: string[];
- "unread_thread_notifications"?: boolean;
- "org.matrix.msc3773.unread_thread_notifications"?: boolean;
-
- // Unstable values
- "io.element.relation_senders"?: Array<RelationType | string>;
- "io.element.relation_types"?: string[];
-}
-
-interface IStateFilter extends IRoomEventFilter {}
-
-interface IRoomFilter {
- not_rooms?: string[];
- rooms?: string[];
- ephemeral?: IRoomEventFilter;
- include_leave?: boolean;
- state?: IStateFilter;
- timeline?: IRoomEventFilter;
- account_data?: IRoomEventFilter;
-}
-/* eslint-enable camelcase */
-
-export class Filter {
- public static LAZY_LOADING_MESSAGES_FILTER = {
- lazy_load_members: true,
- };
-
- /**
- * Create a filter from existing data.
- */
- public static fromJson(userId: string | undefined | null, filterId: string, jsonObj: IFilterDefinition): Filter {
- const filter = new Filter(userId, filterId);
- filter.setDefinition(jsonObj);
- return filter;
- }
-
- private definition: IFilterDefinition = {};
- private roomFilter?: FilterComponent;
- private roomTimelineFilter?: FilterComponent;
-
- /**
- * Construct a new Filter.
- * @param userId - The user ID for this filter.
- * @param filterId - The filter ID if known.
- */
- public constructor(public readonly userId: string | undefined | null, public filterId?: string) {}
-
- /**
- * Get the ID of this filter on your homeserver (if known)
- * @returns The filter ID
- */
- public getFilterId(): string | undefined {
- return this.filterId;
- }
-
- /**
- * Get the JSON body of the filter.
- * @returns The filter definition
- */
- public getDefinition(): IFilterDefinition {
- return this.definition;
- }
-
- /**
- * Set the JSON body of the filter
- * @param definition - The filter definition
- */
- public setDefinition(definition: IFilterDefinition): void {
- this.definition = definition;
-
- // This is all ported from synapse's FilterCollection()
-
- // definitions look something like:
- // {
- // "room": {
- // "rooms": ["!abcde:example.com"],
- // "not_rooms": ["!123456:example.com"],
- // "state": {
- // "types": ["m.room.*"],
- // "not_rooms": ["!726s6s6q:example.com"],
- // "lazy_load_members": true,
- // },
- // "timeline": {
- // "limit": 10,
- // "types": ["m.room.message"],
- // "not_rooms": ["!726s6s6q:example.com"],
- // "not_senders": ["@spam:example.com"]
- // "contains_url": true
- // },
- // "ephemeral": {
- // "types": ["m.receipt", "m.typing"],
- // "not_rooms": ["!726s6s6q:example.com"],
- // "not_senders": ["@spam:example.com"]
- // }
- // },
- // "presence": {
- // "types": ["m.presence"],
- // "not_senders": ["@alice:example.com"]
- // },
- // "event_format": "client",
- // "event_fields": ["type", "content", "sender"]
- // }
-
- const roomFilterJson = definition.room;
-
- // consider the top level rooms/not_rooms filter
- const roomFilterFields: IRoomFilter = {};
- if (roomFilterJson) {
- if (roomFilterJson.rooms) {
- roomFilterFields.rooms = roomFilterJson.rooms;
- }
- if (roomFilterJson.rooms) {
- roomFilterFields.not_rooms = roomFilterJson.not_rooms;
- }
- }
-
- this.roomFilter = new FilterComponent(roomFilterFields, this.userId);
- this.roomTimelineFilter = new FilterComponent(roomFilterJson?.timeline || {}, this.userId);
-
- // don't bother porting this from synapse yet:
- // this._room_state_filter =
- // new FilterComponent(roomFilterJson.state || {});
- // this._room_ephemeral_filter =
- // new FilterComponent(roomFilterJson.ephemeral || {});
- // this._room_account_data_filter =
- // new FilterComponent(roomFilterJson.account_data || {});
- // this._presence_filter =
- // new FilterComponent(definition.presence || {});
- // this._account_data_filter =
- // new FilterComponent(definition.account_data || {});
- }
-
- /**
- * Get the room.timeline filter component of the filter
- * @returns room timeline filter component
- */
- public getRoomTimelineFilterComponent(): FilterComponent | undefined {
- return this.roomTimelineFilter;
- }
-
- /**
- * Filter the list of events based on whether they are allowed in a timeline
- * based on this filter
- * @param events - the list of events being filtered
- * @returns the list of events which match the filter
- */
- public filterRoomTimeline(events: MatrixEvent[]): MatrixEvent[] {
- if (this.roomFilter) {
- events = this.roomFilter.filter(events);
- }
- if (this.roomTimelineFilter) {
- events = this.roomTimelineFilter.filter(events);
- }
- return events;
- }
-
- /**
- * Set the max number of events to return for each room's timeline.
- * @param limit - The max number of events to return for each room.
- */
- public setTimelineLimit(limit: number): void {
- setProp(this.definition, "room.timeline.limit", limit);
- }
-
- /**
- * Enable threads unread notification
- */
- public setUnreadThreadNotifications(enabled: boolean): void {
- this.definition = {
- ...this.definition,
- room: {
- ...this.definition?.room,
- timeline: {
- ...this.definition?.room?.timeline,
- [UNREAD_THREAD_NOTIFICATIONS.name]: enabled,
- },
- },
- };
- }
-
- public setLazyLoadMembers(enabled: boolean): void {
- setProp(this.definition, "room.state.lazy_load_members", enabled);
- }
-
- /**
- * Control whether left rooms should be included in responses.
- * @param includeLeave - True to make rooms the user has left appear
- * in responses.
- */
- public setIncludeLeaveRooms(includeLeave: boolean): void {
- setProp(this.definition, "room.include_leave", includeLeave);
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/errors.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/errors.ts
deleted file mode 100644
index e48fc02..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/errors.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
-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 { IUsageLimit } from "../@types/partials";
-import { MatrixEvent } from "../models/event";
-
-interface IErrorJson extends Partial<IUsageLimit> {
- [key: string]: any; // extensible
- errcode?: string;
- error?: string;
-}
-
-/**
- * Construct a generic HTTP error. This is a JavaScript Error with additional information
- * specific to HTTP responses.
- * @param msg - The error message to include.
- * @param httpStatus - The HTTP response status code.
- */
-export class HTTPError extends Error {
- public constructor(msg: string, public readonly httpStatus?: number) {
- super(msg);
- }
-}
-
-export class MatrixError extends HTTPError {
- // The Matrix 'errcode' value, e.g. "M_FORBIDDEN".
- public readonly errcode?: string;
- // The raw Matrix error JSON used to construct this object.
- public data: IErrorJson;
-
- /**
- * Construct a Matrix error. This is a JavaScript Error with additional
- * information specific to the standard Matrix error response.
- * @param errorJson - The Matrix error JSON returned from the homeserver.
- * @param httpStatus - The numeric HTTP status code given
- */
- public constructor(
- errorJson: IErrorJson = {},
- public readonly httpStatus?: number,
- public url?: string,
- public event?: MatrixEvent,
- ) {
- let message = errorJson.error || "Unknown message";
- if (httpStatus) {
- message = `[${httpStatus}] ${message}`;
- }
- if (url) {
- message = `${message} (${url})`;
- }
- super(`MatrixError: ${message}`, httpStatus);
- this.errcode = errorJson.errcode;
- this.name = errorJson.errcode || "Unknown error code";
- this.data = errorJson;
- }
-}
-
-/**
- * Construct a ConnectionError. This is a JavaScript Error indicating
- * that a request failed because of some error with the connection, either
- * CORS was not correctly configured on the server, the server didn't response,
- * the request timed out, or the internet connection on the client side went down.
- */
-export class ConnectionError extends Error {
- public constructor(message: string, cause?: Error) {
- super(message + (cause ? `: ${cause.message}` : ""));
- }
-
- public get name(): string {
- return "ConnectionError";
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/fetch.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/fetch.ts
deleted file mode 100644
index ecb0908..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/fetch.ts
+++ /dev/null
@@ -1,311 +0,0 @@
-/*
-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.
-*/
-
-/**
- * This is an internal module. See {@link MatrixHttpApi} for the public class.
- */
-
-import * as utils from "../utils";
-import { TypedEventEmitter } from "../models/typed-event-emitter";
-import { Method } from "./method";
-import { ConnectionError, MatrixError } from "./errors";
-import { HttpApiEvent, HttpApiEventHandlerMap, IHttpOpts, IRequestOpts } from "./interface";
-import { anySignal, parseErrorResponse, timeoutSignal } from "./utils";
-import { QueryDict } from "../utils";
-
-type Body = Record<string, any> | BodyInit;
-
-interface TypedResponse<T> extends Response {
- json(): Promise<T>;
-}
-
-export type ResponseType<T, O extends IHttpOpts> = O extends undefined
- ? T
- : O extends { onlyData: true }
- ? T
- : TypedResponse<T>;
-
-export class FetchHttpApi<O extends IHttpOpts> {
- private abortController = new AbortController();
-
- public constructor(
- private eventEmitter: TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>,
- public readonly opts: O,
- ) {
- utils.checkObjectHasKeys(opts, ["baseUrl", "prefix"]);
- opts.onlyData = !!opts.onlyData;
- opts.useAuthorizationHeader = opts.useAuthorizationHeader ?? true;
- }
-
- public abort(): void {
- this.abortController.abort();
- this.abortController = new AbortController();
- }
-
- public fetch(resource: URL | string, options?: RequestInit): ReturnType<typeof global.fetch> {
- if (this.opts.fetchFn) {
- return this.opts.fetchFn(resource, options);
- }
- return global.fetch(resource, options);
- }
-
- /**
- * Sets the base URL for the identity server
- * @param url - The new base url
- */
- public setIdBaseUrl(url: string): void {
- this.opts.idBaseUrl = url;
- }
-
- public idServerRequest<T extends {} = Record<string, unknown>>(
- method: Method,
- path: string,
- params: Record<string, string | string[]> | undefined,
- prefix: string,
- accessToken?: string,
- ): Promise<ResponseType<T, O>> {
- if (!this.opts.idBaseUrl) {
- throw new Error("No identity server base URL set");
- }
-
- let queryParams: QueryDict | undefined = undefined;
- let body: Record<string, string | string[]> | undefined = undefined;
- if (method === Method.Get) {
- queryParams = params;
- } else {
- body = params;
- }
-
- const fullUri = this.getUrl(path, queryParams, prefix, this.opts.idBaseUrl);
-
- const opts: IRequestOpts = {
- json: true,
- headers: {},
- };
- if (accessToken) {
- opts.headers!.Authorization = `Bearer ${accessToken}`;
- }
-
- return this.requestOtherUrl(method, fullUri, body, opts);
- }
-
- /**
- * Perform an authorised request to the homeserver.
- * @param method - The HTTP method e.g. "GET".
- * @param path - The HTTP path <b>after</b> the supplied prefix e.g.
- * "/createRoom".
- *
- * @param queryParams - A dict of query params (these will NOT be
- * urlencoded). If unspecified, there will be no query params.
- *
- * @param body - The HTTP JSON body.
- *
- * @param opts - additional options. If a number is specified,
- * this is treated as `opts.localTimeoutMs`.
- *
- * @returns Promise which resolves to
- * ```
- * {
- * data: {Object},
- * headers: {Object},
- * code: {Number},
- * }
- * ```
- * If `onlyData` is set, this will resolve to the `data` object only.
- * @returns Rejects with an error if a problem occurred.
- * This includes network problems and Matrix-specific error JSON.
- */
- public authedRequest<T>(
- method: Method,
- path: string,
- queryParams?: QueryDict,
- body?: Body,
- opts: IRequestOpts = {},
- ): Promise<ResponseType<T, O>> {
- if (!queryParams) queryParams = {};
-
- if (this.opts.accessToken) {
- if (this.opts.useAuthorizationHeader) {
- if (!opts.headers) {
- opts.headers = {};
- }
- if (!opts.headers.Authorization) {
- opts.headers.Authorization = "Bearer " + this.opts.accessToken;
- }
- if (queryParams.access_token) {
- delete queryParams.access_token;
- }
- } else if (!queryParams.access_token) {
- queryParams.access_token = this.opts.accessToken;
- }
- }
-
- const requestPromise = this.request<T>(method, path, queryParams, body, opts);
-
- requestPromise.catch((err: MatrixError) => {
- if (err.errcode == "M_UNKNOWN_TOKEN" && !opts?.inhibitLogoutEmit) {
- this.eventEmitter.emit(HttpApiEvent.SessionLoggedOut, err);
- } else if (err.errcode == "M_CONSENT_NOT_GIVEN") {
- this.eventEmitter.emit(HttpApiEvent.NoConsent, err.message, err.data.consent_uri);
- }
- });
-
- // return the original promise, otherwise tests break due to it having to
- // go around the event loop one more time to process the result of the request
- return requestPromise;
- }
-
- /**
- * Perform a request to the homeserver without any credentials.
- * @param method - The HTTP method e.g. "GET".
- * @param path - The HTTP path <b>after</b> the supplied prefix e.g.
- * "/createRoom".
- *
- * @param queryParams - A dict of query params (these will NOT be
- * urlencoded). If unspecified, there will be no query params.
- *
- * @param body - The HTTP JSON body.
- *
- * @param opts - additional options
- *
- * @returns Promise which resolves to
- * ```
- * {
- * data: {Object},
- * headers: {Object},
- * code: {Number},
- * }
- * ```
- * If `onlyData</code> is set, this will resolve to the <code>data`
- * object only.
- * @returns Rejects with an error if a problem
- * occurred. This includes network problems and Matrix-specific error JSON.
- */
- public request<T>(
- method: Method,
- path: string,
- queryParams?: QueryDict,
- body?: Body,
- opts?: IRequestOpts,
- ): Promise<ResponseType<T, O>> {
- const fullUri = this.getUrl(path, queryParams, opts?.prefix, opts?.baseUrl);
- return this.requestOtherUrl<T>(method, fullUri, body, opts);
- }
-
- /**
- * Perform a request to an arbitrary URL.
- * @param method - The HTTP method e.g. "GET".
- * @param url - The HTTP URL object.
- *
- * @param body - The HTTP JSON body.
- *
- * @param opts - additional options
- *
- * @returns Promise which resolves to data unless `onlyData` is specified as false,
- * where the resolved value will be a fetch Response object.
- * @returns Rejects with an error if a problem
- * occurred. This includes network problems and Matrix-specific error JSON.
- */
- public async requestOtherUrl<T>(
- method: Method,
- url: URL | string,
- body?: Body,
- opts: Pick<IRequestOpts, "headers" | "json" | "localTimeoutMs" | "keepAlive" | "abortSignal"> = {},
- ): Promise<ResponseType<T, O>> {
- const headers = Object.assign({}, opts.headers || {});
- const json = opts.json ?? true;
- // We can't use getPrototypeOf here as objects made in other contexts e.g. over postMessage won't have same ref
- const jsonBody = json && body?.constructor?.name === Object.name;
-
- if (json) {
- if (jsonBody && !headers["Content-Type"]) {
- headers["Content-Type"] = "application/json";
- }
-
- if (!headers["Accept"]) {
- headers["Accept"] = "application/json";
- }
- }
-
- const timeout = opts.localTimeoutMs ?? this.opts.localTimeoutMs;
- const keepAlive = opts.keepAlive ?? false;
- const signals = [this.abortController.signal];
- if (timeout !== undefined) {
- signals.push(timeoutSignal(timeout));
- }
- if (opts.abortSignal) {
- signals.push(opts.abortSignal);
- }
-
- let data: BodyInit;
- if (jsonBody) {
- data = JSON.stringify(body);
- } else {
- data = body as BodyInit;
- }
-
- const { signal, cleanup } = anySignal(signals);
-
- let res: Response;
- try {
- res = await this.fetch(url, {
- signal,
- method,
- body: data,
- headers,
- mode: "cors",
- redirect: "follow",
- referrer: "",
- referrerPolicy: "no-referrer",
- cache: "no-cache",
- credentials: "omit", // we send credentials via headers
- keepalive: keepAlive,
- });
- } catch (e) {
- if ((<Error>e).name === "AbortError") {
- throw e;
- }
- throw new ConnectionError("fetch failed", <Error>e);
- } finally {
- cleanup();
- }
-
- if (!res.ok) {
- throw parseErrorResponse(res, await res.text());
- }
-
- if (this.opts.onlyData) {
- return json ? res.json() : res.text();
- }
- return res as ResponseType<T, O>;
- }
-
- /**
- * Form and return a homeserver request URL based on the given path params and prefix.
- * @param path - The HTTP path <b>after</b> the supplied prefix e.g. "/createRoom".
- * @param queryParams - A dict of query params (these will NOT be urlencoded).
- * @param prefix - The full prefix to use e.g. "/_matrix/client/v2_alpha", defaulting to this.opts.prefix.
- * @param baseUrl - The baseUrl to use e.g. "https://matrix.org/", defaulting to this.opts.baseUrl.
- * @returns URL
- */
- public getUrl(path: string, queryParams?: QueryDict, prefix?: string, baseUrl?: string): URL {
- const url = new URL((baseUrl ?? this.opts.baseUrl) + (prefix ?? this.opts.prefix) + path);
- if (queryParams) {
- utils.encodeParams(queryParams, url.searchParams);
- }
- return url;
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/index.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/index.ts
deleted file mode 100644
index c5e8e2a..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/index.ts
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
-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 { FetchHttpApi } from "./fetch";
-import { FileType, IContentUri, IHttpOpts, Upload, UploadOpts, UploadResponse } from "./interface";
-import { MediaPrefix } from "./prefix";
-import * as utils from "../utils";
-import * as callbacks from "../realtime-callbacks";
-import { Method } from "./method";
-import { ConnectionError } from "./errors";
-import { parseErrorResponse } from "./utils";
-
-export * from "./interface";
-export * from "./prefix";
-export * from "./errors";
-export * from "./method";
-export * from "./utils";
-
-export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
- private uploads: Upload[] = [];
-
- /**
- * Upload content to the homeserver
- *
- * @param file - The object to upload. On a browser, something that
- * can be sent to XMLHttpRequest.send (typically a File). Under node.js,
- * a Buffer, String or ReadStream.
- *
- * @param opts - options object
- *
- * @returns Promise which resolves to response object, as
- * determined by this.opts.onlyData, opts.rawResponse, and
- * opts.onlyContentUri. Rejects with an error (usually a MatrixError).
- */
- public uploadContent(file: FileType, opts: UploadOpts = {}): Promise<UploadResponse> {
- const includeFilename = opts.includeFilename ?? true;
- const abortController = opts.abortController ?? new AbortController();
-
- // If the file doesn't have a mime type, use a default since the HS errors if we don't supply one.
- const contentType = opts.type ?? (file as File).type ?? "application/octet-stream";
- const fileName = opts.name ?? (file as File).name;
-
- const upload = {
- loaded: 0,
- total: 0,
- abortController,
- } as Upload;
- const defer = utils.defer<UploadResponse>();
-
- if (global.XMLHttpRequest) {
- const xhr = new global.XMLHttpRequest();
-
- const timeoutFn = function (): void {
- xhr.abort();
- defer.reject(new Error("Timeout"));
- };
-
- // set an initial timeout of 30s; we'll advance it each time we get a progress notification
- let timeoutTimer = callbacks.setTimeout(timeoutFn, 30000);
-
- xhr.onreadystatechange = function (): void {
- switch (xhr.readyState) {
- case global.XMLHttpRequest.DONE:
- callbacks.clearTimeout(timeoutTimer);
- try {
- if (xhr.status === 0) {
- throw new DOMException(xhr.statusText, "AbortError"); // mimic fetch API
- }
- if (!xhr.responseText) {
- throw new Error("No response body.");
- }
-
- if (xhr.status >= 400) {
- defer.reject(parseErrorResponse(xhr, xhr.responseText));
- } else {
- defer.resolve(JSON.parse(xhr.responseText));
- }
- } catch (err) {
- if ((<Error>err).name === "AbortError") {
- defer.reject(err);
- return;
- }
- defer.reject(new ConnectionError("request failed", <Error>err));
- }
- break;
- }
- };
-
- xhr.upload.onprogress = (ev: ProgressEvent): void => {
- callbacks.clearTimeout(timeoutTimer);
- upload.loaded = ev.loaded;
- upload.total = ev.total;
- timeoutTimer = callbacks.setTimeout(timeoutFn, 30000);
- opts.progressHandler?.({
- loaded: ev.loaded,
- total: ev.total,
- });
- };
-
- const url = this.getUrl("/upload", undefined, MediaPrefix.R0);
-
- if (includeFilename && fileName) {
- url.searchParams.set("filename", encodeURIComponent(fileName));
- }
-
- if (!this.opts.useAuthorizationHeader && this.opts.accessToken) {
- url.searchParams.set("access_token", encodeURIComponent(this.opts.accessToken));
- }
-
- xhr.open(Method.Post, url.href);
- if (this.opts.useAuthorizationHeader && this.opts.accessToken) {
- xhr.setRequestHeader("Authorization", "Bearer " + this.opts.accessToken);
- }
- xhr.setRequestHeader("Content-Type", contentType);
- xhr.send(file);
-
- abortController.signal.addEventListener("abort", () => {
- xhr.abort();
- });
- } else {
- const queryParams: utils.QueryDict = {};
- if (includeFilename && fileName) {
- queryParams.filename = fileName;
- }
-
- const headers: Record<string, string> = { "Content-Type": contentType };
-
- this.authedRequest<UploadResponse>(Method.Post, "/upload", queryParams, file, {
- prefix: MediaPrefix.R0,
- headers,
- abortSignal: abortController.signal,
- })
- .then((response) => {
- return this.opts.onlyData ? <UploadResponse>response : response.json();
- })
- .then(defer.resolve, defer.reject);
- }
-
- // remove the upload from the list on completion
- upload.promise = defer.promise.finally(() => {
- utils.removeElement(this.uploads, (elem) => elem === upload);
- });
- abortController.signal.addEventListener("abort", () => {
- utils.removeElement(this.uploads, (elem) => elem === upload);
- defer.reject(new DOMException("Aborted", "AbortError"));
- });
- this.uploads.push(upload);
- return upload.promise;
- }
-
- public cancelUpload(promise: Promise<UploadResponse>): boolean {
- const upload = this.uploads.find((u) => u.promise === promise);
- if (upload) {
- upload.abortController.abort();
- return true;
- }
- return false;
- }
-
- public getCurrentUploads(): Upload[] {
- return this.uploads;
- }
-
- /**
- * Get the content repository url with query parameters.
- * @returns An object with a 'base', 'path' and 'params' for base URL,
- * path and query parameters respectively.
- */
- public getContentUri(): IContentUri {
- return {
- base: this.opts.baseUrl,
- path: MediaPrefix.R0 + "/upload",
- params: {
- access_token: this.opts.accessToken!,
- },
- };
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/interface.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/interface.ts
deleted file mode 100644
index 9946aa3..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/interface.ts
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
-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 { MatrixError } from "./errors";
-
-export interface IHttpOpts {
- fetchFn?: typeof global.fetch;
-
- baseUrl: string;
- idBaseUrl?: string;
- prefix: string;
- extraParams?: Record<string, string>;
-
- accessToken?: string;
- useAuthorizationHeader?: boolean; // defaults to true
-
- onlyData?: boolean;
- localTimeoutMs?: number;
-}
-
-export interface IRequestOpts {
- /**
- * The alternative base url to use.
- * If not specified, uses this.opts.baseUrl
- */
- baseUrl?: string;
- /**
- * The full prefix to use e.g.
- * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
- */
- prefix?: string;
- /**
- * map of additional request headers
- */
- headers?: Record<string, string>;
- abortSignal?: AbortSignal;
- /**
- * The maximum amount of time to wait before
- * timing out the request. If not specified, there is no timeout.
- */
- localTimeoutMs?: number;
- keepAlive?: boolean; // defaults to false
- json?: boolean; // defaults to true
-
- // Set to true to prevent the request function from emitting a Session.logged_out event.
- // This is intended for use on endpoints where M_UNKNOWN_TOKEN is a valid/notable error response,
- // such as with token refreshes.
- inhibitLogoutEmit?: boolean;
-}
-
-export interface IContentUri {
- base: string;
- path: string;
- params: {
- // eslint-disable-next-line camelcase
- access_token: string;
- };
-}
-
-export enum HttpApiEvent {
- SessionLoggedOut = "Session.logged_out",
- NoConsent = "no_consent",
-}
-
-export type HttpApiEventHandlerMap = {
- /**
- * Fires whenever the login session the JS SDK is using is no
- * longer valid and the user must log in again.
- * NB. This only fires when action is required from the user, not
- * when then login session can be renewed by using a refresh token.
- * @example
- * ```
- * matrixClient.on("Session.logged_out", function(errorObj){
- * // show the login screen
- * });
- * ```
- */
- [HttpApiEvent.SessionLoggedOut]: (err: MatrixError) => void;
- /**
- * Fires when the JS SDK receives a M_CONSENT_NOT_GIVEN error in response
- * to a HTTP request.
- * @example
- * ```
- * matrixClient.on("no_consent", function(message, contentUri) {
- * console.info(message + ' Go to ' + contentUri);
- * });
- * ```
- */
- [HttpApiEvent.NoConsent]: (message: string, consentUri: string) => void;
-};
-
-export interface UploadProgress {
- loaded: number;
- total: number;
-}
-
-export interface UploadOpts {
- /**
- * Name to give the file on the server. Defaults to <tt>file.name</tt>.
- */
- name?: string;
- /**
- * Content-type for the upload. Defaults to
- * <tt>file.type</tt>, or <tt>applicaton/octet-stream</tt>.
- */
- type?: string;
- /**
- * if false will not send the filename,
- * e.g for encrypted file uploads where filename leaks are undesirable.
- * Defaults to true.
- */
- includeFilename?: boolean;
- /**
- * Optional. Called when a chunk of
- * data has been uploaded, with an object containing the fields `loaded`
- * (number of bytes transferred) and `total` (total size, if known).
- */
- progressHandler?(progress: UploadProgress): void;
- abortController?: AbortController;
-}
-
-export interface Upload {
- loaded: number;
- total: number;
- promise: Promise<UploadResponse>;
- abortController: AbortController;
-}
-
-export interface UploadResponse {
- // eslint-disable-next-line camelcase
- content_uri: string;
-}
-
-export type FileType = XMLHttpRequestBodyInit;
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/method.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/method.ts
deleted file mode 100644
index 1914360..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/method.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
-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.
-*/
-
-export enum Method {
- Get = "GET",
- Put = "PUT",
- Post = "POST",
- Delete = "DELETE",
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/prefix.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/prefix.ts
deleted file mode 100644
index f15b1ac..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/prefix.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
-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.
-*/
-
-export enum ClientPrefix {
- /**
- * A constant representing the URI path for release 0 of the Client-Server HTTP API.
- */
- R0 = "/_matrix/client/r0",
- /**
- * A constant representing the URI path for the legacy release v1 of the Client-Server HTTP API.
- */
- V1 = "/_matrix/client/v1",
- /**
- * A constant representing the URI path for Client-Server API endpoints versioned at v3.
- */
- V3 = "/_matrix/client/v3",
- /**
- * A constant representing the URI path for as-yet unspecified Client-Server HTTP APIs.
- */
- Unstable = "/_matrix/client/unstable",
-}
-
-export enum IdentityPrefix {
- /**
- * URI path for the v2 identity API
- */
- V2 = "/_matrix/identity/v2",
-}
-
-export enum MediaPrefix {
- /**
- * URI path for the media repo API
- */
- R0 = "/_matrix/media/r0",
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/utils.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/utils.ts
deleted file mode 100644
index c49be74..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/http-api/utils.ts
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
-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 { parse as parseContentType, ParsedMediaType } from "content-type";
-
-import { logger } from "../logger";
-import { sleep } from "../utils";
-import { ConnectionError, HTTPError, MatrixError } from "./errors";
-
-// Ponyfill for https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout
-export function timeoutSignal(ms: number): AbortSignal {
- const controller = new AbortController();
- setTimeout(() => {
- controller.abort();
- }, ms);
-
- return controller.signal;
-}
-
-export function anySignal(signals: AbortSignal[]): {
- signal: AbortSignal;
- cleanup(): void;
-} {
- const controller = new AbortController();
-
- function cleanup(): void {
- for (const signal of signals) {
- signal.removeEventListener("abort", onAbort);
- }
- }
-
- function onAbort(): void {
- controller.abort();
- cleanup();
- }
-
- for (const signal of signals) {
- if (signal.aborted) {
- onAbort();
- break;
- }
- signal.addEventListener("abort", onAbort);
- }
-
- return {
- signal: controller.signal,
- cleanup,
- };
-}
-
-/**
- * Attempt to turn an HTTP error response into a Javascript Error.
- *
- * If it is a JSON response, we will parse it into a MatrixError. Otherwise
- * we return a generic Error.
- *
- * @param response - response object
- * @param body - raw body of the response
- * @returns
- */
-export function parseErrorResponse(response: XMLHttpRequest | Response, body?: string): Error {
- let contentType: ParsedMediaType | null;
- try {
- contentType = getResponseContentType(response);
- } catch (e) {
- return <Error>e;
- }
-
- if (contentType?.type === "application/json" && body) {
- return new MatrixError(
- JSON.parse(body),
- response.status,
- isXhr(response) ? response.responseURL : response.url,
- );
- }
- if (contentType?.type === "text/plain") {
- return new HTTPError(`Server returned ${response.status} error: ${body}`, response.status);
- }
- return new HTTPError(`Server returned ${response.status} error`, response.status);
-}
-
-function isXhr(response: XMLHttpRequest | Response): response is XMLHttpRequest {
- return "getResponseHeader" in response;
-}
-
-/**
- * extract the Content-Type header from the response object, and
- * parse it to a `{type, parameters}` object.
- *
- * returns null if no content-type header could be found.
- *
- * @param response - response object
- * @returns parsed content-type header, or null if not found
- */
-function getResponseContentType(response: XMLHttpRequest | Response): ParsedMediaType | null {
- let contentType: string | null;
- if (isXhr(response)) {
- contentType = response.getResponseHeader("Content-Type");
- } else {
- contentType = response.headers.get("Content-Type");
- }
-
- if (!contentType) return null;
-
- try {
- return parseContentType(contentType);
- } catch (e) {
- throw new Error(`Error parsing Content-Type '${contentType}': ${e}`);
- }
-}
-
-/**
- * Retries a network operation run in a callback.
- * @param maxAttempts - maximum attempts to try
- * @param callback - callback that returns a promise of the network operation. If rejected with ConnectionError, it will be retried by calling the callback again.
- * @returns the result of the network operation
- * @throws {@link ConnectionError} If after maxAttempts the callback still throws ConnectionError
- */
-export async function retryNetworkOperation<T>(maxAttempts: number, callback: () => Promise<T>): Promise<T> {
- let attempts = 0;
- let lastConnectionError: ConnectionError | null = null;
- while (attempts < maxAttempts) {
- try {
- if (attempts > 0) {
- const timeout = 1000 * Math.pow(2, attempts);
- logger.log(`network operation failed ${attempts} times, retrying in ${timeout}ms...`);
- await sleep(timeout);
- }
- return await callback();
- } catch (err) {
- if (err instanceof ConnectionError) {
- attempts += 1;
- lastConnectionError = err;
- } else {
- throw err;
- }
- }
- }
- throw lastConnectionError;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/index.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/index.ts
deleted file mode 100644
index c9a5dcf..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/index.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
-Copyright 2019 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import * as matrixcs from "./matrix";
-
-if (global.__js_sdk_entrypoint) {
- throw new Error("Multiple matrix-js-sdk entrypoints detected!");
-}
-global.__js_sdk_entrypoint = true;
-
-export * from "./matrix";
-export default matrixcs;
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/indexeddb-helpers.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/indexeddb-helpers.ts
deleted file mode 100644
index 6f99ae5..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/indexeddb-helpers.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
-Copyright 2019 New Vector Ltd
-
-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.
-*/
-
-/**
- * Check if an IndexedDB database exists. The only way to do so is to try opening it, so
- * we do that and then delete it did not exist before.
- *
- * @param indexedDB - The `indexedDB` interface
- * @param dbName - The database name to test for
- * @returns Whether the database exists
- */
-export function exists(indexedDB: IDBFactory, dbName: string): Promise<boolean> {
- return new Promise<boolean>((resolve, reject) => {
- let exists = true;
- const req = indexedDB.open(dbName);
- req.onupgradeneeded = (): void => {
- // Since we did not provide an explicit version when opening, this event
- // should only fire if the DB did not exist before at any version.
- exists = false;
- };
- req.onblocked = (): void => reject(req.error);
- req.onsuccess = (): void => {
- const db = req.result;
- db.close();
- if (!exists) {
- // The DB did not exist before, but has been created as part of this
- // existence check. Delete it now to restore previous state. Delete can
- // actually take a while to complete in some browsers, so don't wait for
- // it. This won't block future open calls that a store might issue next to
- // properly set up the DB.
- indexedDB.deleteDatabase(dbName);
- }
- resolve(exists);
- };
- req.onerror = (): void => reject(req.error);
- });
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/indexeddb-worker.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/indexeddb-worker.ts
deleted file mode 100644
index 68dcf0f..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/indexeddb-worker.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
-Copyright 2017 Vector Creations Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-/**
- * Separate exports file for the indexeddb web worker, which is designed
- * to be used separately
- */
-
-/** The {@link IndexedDBStoreWorker} class. */
-export { IndexedDBStoreWorker } from "./store/indexeddb-store-worker";
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/interactive-auth.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/interactive-auth.ts
deleted file mode 100644
index 7d9c183..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/interactive-auth.ts
+++ /dev/null
@@ -1,617 +0,0 @@
-/*
-Copyright 2016 OpenMarket Ltd
-Copyright 2017 Vector Creations Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { logger } from "./logger";
-import { MatrixClient } from "./client";
-import { defer, IDeferred } from "./utils";
-import { MatrixError } from "./http-api";
-
-const EMAIL_STAGE_TYPE = "m.login.email.identity";
-const MSISDN_STAGE_TYPE = "m.login.msisdn";
-
-export interface UIAFlow {
- stages: AuthType[];
-}
-
-export interface IInputs {
- // An email address. If supplied, a flow using email verification will be chosen.
- emailAddress?: string;
- // An ISO two letter country code. Gives the country that opts.phoneNumber should be resolved relative to.
- phoneCountry?: string;
- // A phone number. If supplied, a flow using phone number validation will be chosen.
- phoneNumber?: string;
- registrationToken?: string;
-}
-
-export interface IStageStatus {
- emailSid?: string;
- errcode?: string;
- error?: string;
-}
-
-export interface IAuthData {
- session?: string;
- type?: string;
- completed?: string[];
- flows?: UIAFlow[];
- available_flows?: UIAFlow[];
- stages?: string[];
- required_stages?: AuthType[];
- params?: Record<string, Record<string, any>>;
- data?: Record<string, string>;
- errcode?: string;
- error?: string;
- user_id?: string;
- device_id?: string;
- access_token?: string;
-}
-
-export enum AuthType {
- Password = "m.login.password",
- Recaptcha = "m.login.recaptcha",
- Terms = "m.login.terms",
- Email = "m.login.email.identity",
- Msisdn = "m.login.msisdn",
- Sso = "m.login.sso",
- SsoUnstable = "org.matrix.login.sso",
- Dummy = "m.login.dummy",
- 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 {
- // [key: string]: any;
- type?: string;
- session?: string;
- // TODO: Remove `user` once servers support proper UIA
- // See https://github.com/vector-im/element-web/issues/10312
- user?: string;
- identifier?: any;
- password?: string;
- response?: string;
- // TODO: Remove `threepid_creds` once servers support proper UIA
- // See https://github.com/vector-im/element-web/issues/10312
- // See https://github.com/matrix-org/matrix-doc/issues/2220
- // eslint-disable-next-line camelcase
- threepid_creds?: any;
- threepidCreds?: any;
- // For m.login.registration_token type
- token?: string;
-}
-
-class NoAuthFlowFoundError extends Error {
- public name = "NoAuthFlowFoundError";
-
- // eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
- public constructor(m: string, public readonly required_stages: string[], public readonly flows: UIAFlow[]) {
- super(m);
- }
-}
-
-interface IOpts {
- /**
- * A matrix client to use for the auth process
- */
- matrixClient: MatrixClient;
- /**
- * Error response from the last request. If null, a request will be made with no auth before starting.
- */
- authData?: IAuthData;
- /**
- * Inputs provided by the user and used by different stages of the auto process.
- * The inputs provided will affect what flow is chosen.
- */
- inputs?: IInputs;
- /**
- * If resuming an existing interactive auth session, the sessionId of that session.
- */
- sessionId?: string;
- /**
- * If resuming an existing interactive auth session, the client secret for that session
- */
- clientSecret?: string;
- /**
- * If returning from having completed m.login.email.identity auth, the sid for the email verification session.
- */
- emailSid?: string;
-
- /**
- * Called with the new auth dict to submit the request.
- * Also passes a second deprecated arg which is a flag set to true if this request is a background request.
- * The busyChanged callback should be used instead of the background flag.
- * Should return a promise which resolves to the successful response or rejects with a MatrixError.
- */
- doRequest(auth: IAuthData | null, background: boolean): Promise<IAuthData>;
- /**
- * Called when the status of the UI auth changes,
- * ie. when the state of an auth stage changes of when the auth flow moves to a new stage.
- * The arguments are: the login type (eg m.login.password); and an object which is either an error or an
- * informational object specific to the login type.
- * If the 'errcode' key is defined, the object is an error, and has keys:
- * errcode: string, the textual error code, eg. M_UNKNOWN
- * error: string, human readable string describing the error
- *
- * The login type specific objects are as follows:
- * m.login.email.identity:
- * * emailSid: string, the sid of the active email auth session
- */
- stateUpdated(nextStage: AuthType, status: IStageStatus): void;
-
- /**
- * A function that takes the email address (string), clientSecret (string), attempt number (int) and
- * sessionId (string) and calls the relevant requestToken function and returns the promise returned by that
- * function.
- * If the resulting promise rejects, the rejection will propagate through to the attemptAuth promise.
- */
- requestEmailToken(email: string, secret: string, attempt: number, session: string): Promise<{ sid: string }>;
- /**
- * Called whenever the interactive auth logic becomes busy submitting information provided by the user or finishes.
- * After this has been called with true the UI should indicate that a request is in progress
- * until it is called again with false.
- */
- busyChanged?(busy: boolean): void;
- startAuthStage?(nextStage: string): Promise<void>; // LEGACY
-}
-
-/**
- * Abstracts the logic used to drive the interactive auth process.
- *
- * <p>Components implementing an interactive auth flow should instantiate one of
- * these, passing in the necessary callbacks to the constructor. They should
- * then call attemptAuth, which will return a promise which will resolve or
- * reject when the interactive-auth process completes.
- *
- * <p>Meanwhile, calls will be made to the startAuthStage and doRequest
- * callbacks, and information gathered from the user can be submitted with
- * submitAuthDict.
- *
- * @param opts - options object
- */
-export class InteractiveAuth {
- private readonly matrixClient: MatrixClient;
- private readonly inputs: IInputs;
- private readonly clientSecret: string;
- private readonly requestCallback: IOpts["doRequest"];
- private readonly busyChangedCallback?: IOpts["busyChanged"];
- private readonly stateUpdatedCallback: IOpts["stateUpdated"];
- private readonly requestEmailTokenCallback: IOpts["requestEmailToken"];
-
- private data: IAuthData;
- private emailSid?: string;
- private requestingEmailToken = false;
- private attemptAuthDeferred: IDeferred<IAuthData> | null = null;
- private chosenFlow: UIAFlow | null = null;
- private currentStage: string | null = 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 = null;
-
- public constructor(opts: IOpts) {
- this.matrixClient = opts.matrixClient;
- this.data = opts.authData || {};
- this.requestCallback = opts.doRequest;
- this.busyChangedCallback = opts.busyChanged;
- // startAuthStage included for backwards compat
- this.stateUpdatedCallback = opts.stateUpdated || opts.startAuthStage;
- this.requestEmailTokenCallback = opts.requestEmailToken;
- this.inputs = opts.inputs || {};
-
- if (opts.sessionId) this.data.session = opts.sessionId;
- this.clientSecret = opts.clientSecret || this.matrixClient.generateClientSecret();
- this.emailSid = opts.emailSid;
- }
-
- /**
- * begin the authentication process.
- *
- * @returns which resolves to the response on success,
- * or rejects with the error on failure. Rejects with NoAuthFlowFoundError if
- * no suitable authentication flow can be found
- */
- public attemptAuth(): Promise<IAuthData> {
- // This promise will be quite long-lived and will resolve when the
- // request is authenticated and completes successfully.
- this.attemptAuthDeferred = defer();
- // pluck the promise out now, as doRequest may clear before we return
- const promise = this.attemptAuthDeferred.promise;
-
- // if we have no flows, try a request to acquire the flows
- if (!this.data?.flows) {
- this.busyChangedCallback?.(true);
- // use the existing sessionId, if one is present.
- const auth = this.data.session ? { session: this.data.session } : null;
- this.doRequest(auth).finally(() => {
- this.busyChangedCallback?.(false);
- });
- } else {
- this.startNextAuthStage();
- }
-
- return promise;
- }
-
- /**
- * Poll to check if the auth session or current stage has been
- * completed out-of-band. If so, the attemptAuth promise will
- * be resolved.
- */
- public async poll(): Promise<void> {
- if (!this.data.session) return;
- // likewise don't poll if there is no auth session in progress
- if (!this.attemptAuthDeferred) return;
- // if we currently have a request in flight, there's no point making
- // another just to check what the status is
- if (this.submitPromise) return;
-
- let authDict: IAuthDict = {};
- if (this.currentStage == EMAIL_STAGE_TYPE) {
- // The email can be validated out-of-band, but we need to provide the
- // creds so the HS can go & check it.
- if (this.emailSid) {
- const creds: Record<string, string> = {
- sid: this.emailSid,
- client_secret: this.clientSecret,
- };
- if (await this.matrixClient.doesServerRequireIdServerParam()) {
- const idServerParsedUrl = new URL(this.matrixClient.getIdentityServerUrl()!);
- creds.id_server = idServerParsedUrl.host;
- }
- authDict = {
- type: EMAIL_STAGE_TYPE,
- // TODO: Remove `threepid_creds` once servers support proper UIA
- // See https://github.com/matrix-org/synapse/issues/5665
- // See https://github.com/matrix-org/matrix-doc/issues/2220
- threepid_creds: creds,
- threepidCreds: creds,
- };
- }
- }
-
- this.submitAuthDict(authDict, true);
- }
-
- /**
- * get the auth session ID
- *
- * @returns session id
- */
- public getSessionId(): string | undefined {
- return this.data?.session;
- }
-
- /**
- * get the client secret used for validation sessions
- * with the identity server.
- *
- * @returns client secret
- */
- public getClientSecret(): string {
- return this.clientSecret;
- }
-
- /**
- * get the server params for a given stage
- *
- * @param loginType - login type for the stage
- * @returns any parameters from the server for this stage
- */
- public getStageParams(loginType: string): Record<string, any> | undefined {
- return this.data.params?.[loginType];
- }
-
- public getChosenFlow(): UIAFlow | null {
- return this.chosenFlow;
- }
-
- /**
- * submit a new auth dict and fire off the request. This will either
- * make attemptAuth resolve/reject, or cause the startAuthStage callback
- * to be called for a new stage.
- *
- * @param authData - new auth dict to send to the server. Should
- * include a `type` property denoting the login type, as well as any
- * other params for that stage.
- * @param background - If true, this request failing will not result
- * in the attemptAuth promise being rejected. This can be set to true
- * for requests that just poll to see if auth has been completed elsewhere.
- */
- public async submitAuthDict(authData: IAuthDict, background = false): Promise<void> {
- if (!this.attemptAuthDeferred) {
- throw new Error("submitAuthDict() called before attemptAuth()");
- }
-
- if (!background) {
- this.busyChangedCallback?.(true);
- }
-
- // if we're currently trying a request, wait for it to finish
- // as otherwise we can get multiple 200 responses which can mean
- // things like multiple logins for register requests.
- // (but discard any exceptions as we only care when its done,
- // not whether it worked or not)
- while (this.submitPromise) {
- try {
- await this.submitPromise;
- } catch (e) {}
- }
-
- // use the sessionid from the last request, if one is present.
- let auth: IAuthDict;
- if (this.data.session) {
- auth = {
- session: this.data.session,
- };
- Object.assign(auth, authData);
- } else {
- auth = authData;
- }
-
- try {
- // NB. the 'background' flag is deprecated by the busyChanged
- // callback and is here for backwards compat
- this.submitPromise = this.doRequest(auth, background);
- await this.submitPromise;
- } finally {
- this.submitPromise = null;
- if (!background) {
- this.busyChangedCallback?.(false);
- }
- }
- }
-
- /**
- * Gets the sid for the email validation session
- * Specific to m.login.email.identity
- *
- * @returns The sid of the email auth session
- */
- public getEmailSid(): string | undefined {
- return this.emailSid;
- }
-
- /**
- * Sets the sid for the email validation session
- * This must be set in order to successfully poll for completion
- * of the email validation.
- * Specific to m.login.email.identity
- *
- * @param sid - The sid for the email validation session
- */
- public setEmailSid(sid: string): void {
- this.emailSid = sid;
- }
-
- /**
- * Requests a new email token and sets the email sid for the validation session
- */
- public requestEmailToken = async (): Promise<void> => {
- 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.
- *
- * @internal
- * @param auth - new auth dict, including session id
- * @param background - If true, this request is a background poll, so it
- * failing will not result in the attemptAuth promise being rejected.
- * This can be set to true for requests that just poll to see if auth has
- * been completed elsewhere.
- */
- private async doRequest(auth: IAuthData | null, background = false): Promise<void> {
- try {
- const result = await this.requestCallback(auth, background);
- this.attemptAuthDeferred!.resolve(result);
- this.attemptAuthDeferred = null;
- } catch (error) {
- // sometimes UI auth errors don't come with flows
- const errorFlows = (<MatrixError>error).data?.flows ?? null;
- const haveFlows = this.data.flows || Boolean(errorFlows);
- if ((<MatrixError>error).httpStatus !== 401 || !(<MatrixError>error).data || !haveFlows) {
- // doesn't look like an interactive-auth failure.
- if (!background) {
- this.attemptAuthDeferred?.reject(error);
- } else {
- // We ignore all failures here (even non-UI auth related ones)
- // since we don't want to suddenly fail if the internet connection
- // had a blip whilst we were polling
- logger.log("Background poll request failed doing UI auth: ignoring", error);
- }
- }
- if (!(<MatrixError>error).data) {
- (<MatrixError>error).data = {};
- }
- // if the error didn't come with flows, completed flows or session ID,
- // copy over the ones we have. Synapse sometimes sends responses without
- // any UI auth data (eg. when polling for email validation, if the email
- // has not yet been validated). This appears to be a Synapse bug, which
- // we workaround here.
- if (
- !(<MatrixError>error).data.flows &&
- !(<MatrixError>error).data.completed &&
- !(<MatrixError>error).data.session
- ) {
- (<MatrixError>error).data.flows = this.data.flows;
- (<MatrixError>error).data.completed = this.data.completed;
- (<MatrixError>error).data.session = this.data.session;
- }
- this.data = (<MatrixError>error).data;
- try {
- this.startNextAuthStage();
- } catch (e) {
- this.attemptAuthDeferred!.reject(e);
- this.attemptAuthDeferred = null;
- return;
- }
-
- if (!this.emailSid && this.chosenFlow?.stages.includes(AuthType.Email)) {
- try {
- 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
- // will succeed.
- // Also, we should expose the fact that this request has compledted
- // so clients can know that the email has actually been sent.
- } catch (e) {
- // we failed to request an email token, so fail the request.
- // This could be due to the email already beeing registered
- // (or not being registered, depending on what we're trying
- // to do) or it could be a network failure. Either way, pass
- // the failure up as the user can't complete auth if we can't
- // send the email, for whatever reason.
- this.attemptAuthDeferred!.reject(e);
- this.attemptAuthDeferred = null;
- }
- }
- }
- }
-
- /**
- * Pick the next stage and call the callback
- *
- * @internal
- * @throws {@link NoAuthFlowFoundError} If no suitable authentication flow can be found
- */
- private startNextAuthStage(): void {
- const nextStage = this.chooseStage();
- if (!nextStage) {
- throw new Error("No incomplete flows from the server");
- }
- this.currentStage = nextStage;
-
- if (nextStage === AuthType.Dummy) {
- this.submitAuthDict({
- type: "m.login.dummy",
- });
- return;
- }
-
- if (this.data?.errcode || this.data?.error) {
- this.stateUpdatedCallback(nextStage, {
- errcode: this.data?.errcode || "",
- error: this.data?.error || "",
- });
- return;
- }
-
- this.stateUpdatedCallback(nextStage, nextStage === EMAIL_STAGE_TYPE ? { emailSid: this.emailSid } : {});
- }
-
- /**
- * Pick the next auth stage
- *
- * @internal
- * @returns login type
- * @throws {@link NoAuthFlowFoundError} If no suitable authentication flow can be found
- */
- private chooseStage(): AuthType | undefined {
- if (this.chosenFlow === null) {
- this.chosenFlow = this.chooseFlow();
- }
- logger.log("Active flow => %s", JSON.stringify(this.chosenFlow));
- const nextStage = this.firstUncompletedStage(this.chosenFlow);
- logger.log("Next stage: %s", nextStage);
- return nextStage;
- }
-
- /**
- * Pick one of the flows from the returned list
- * If a flow using all of the inputs is found, it will
- * be returned, otherwise, null will be returned.
- *
- * Only flows using all given inputs are chosen because it
- * is likely to be surprising if the user provides a
- * credential and it is not used. For example, for registration,
- * this could result in the email not being used which would leave
- * the account with no means to reset a password.
- *
- * @internal
- * @returns flow
- * @throws {@link NoAuthFlowFoundError} If no suitable authentication flow can be found
- */
- private chooseFlow(): UIAFlow {
- const flows = this.data.flows || [];
-
- // we've been given an email or we've already done an email part
- const haveEmail = Boolean(this.inputs.emailAddress) || Boolean(this.emailSid);
- const haveMsisdn = Boolean(this.inputs.phoneCountry) && Boolean(this.inputs.phoneNumber);
-
- for (const flow of flows) {
- let flowHasEmail = false;
- let flowHasMsisdn = false;
- for (const stage of flow.stages) {
- if (stage === EMAIL_STAGE_TYPE) {
- flowHasEmail = true;
- } else if (stage == MSISDN_STAGE_TYPE) {
- flowHasMsisdn = true;
- }
- }
-
- if (flowHasEmail == haveEmail && flowHasMsisdn == haveMsisdn) {
- return flow;
- }
- }
-
- const requiredStages: string[] = [];
- if (haveEmail) requiredStages.push(EMAIL_STAGE_TYPE);
- if (haveMsisdn) requiredStages.push(MSISDN_STAGE_TYPE);
- // Throw an error with a fairly generic description, but with more
- // information such that the app can give a better one if so desired.
- throw new NoAuthFlowFoundError("No appropriate authentication flow found", requiredStages, flows);
- }
-
- /**
- * Get the first uncompleted stage in the given flow
- *
- * @internal
- * @returns login type
- */
- private firstUncompletedStage(flow: UIAFlow): AuthType | undefined {
- const completed = this.data.completed || [];
- return flow.stages.find((stageType) => !completed.includes(stageType));
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/logger.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/logger.ts
deleted file mode 100644
index ba7f742..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/logger.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
-Copyright 2018 André Jaenisch
-Copyright 2019, 2021 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 log, { Logger } from "loglevel";
-
-// This is to demonstrate, that you can use any namespace you want.
-// Namespaces allow you to turn on/off the logging for specific parts of the
-// application.
-// An idea would be to control this via an environment variable (on Node.js).
-// See https://www.npmjs.com/package/debug to see how this could be implemented
-// Part of #332 is introducing a logging library in the first place.
-const DEFAULT_NAMESPACE = "matrix";
-
-// because rageshakes in react-sdk hijack the console log, also at module load time,
-// initializing the logger here races with the initialization of rageshakes.
-// to avoid the issue, we override the methodFactory of loglevel that binds to the
-// console methods at initialization time by a factory that looks up the console methods
-// when logging so we always get the current value of console methods.
-log.methodFactory = function (methodName, logLevel, loggerName) {
- return function (this: PrefixedLogger, ...args): void {
- /* eslint-disable @typescript-eslint/no-invalid-this */
- if (this.prefix) {
- args.unshift(this.prefix);
- }
- /* eslint-enable @typescript-eslint/no-invalid-this */
- const supportedByConsole =
- methodName === "error" || methodName === "warn" || methodName === "trace" || methodName === "info";
- /* eslint-disable no-console */
- if (supportedByConsole) {
- return console[methodName](...args);
- } else {
- return console.log(...args);
- }
- /* eslint-enable no-console */
- };
-};
-
-/**
- * Drop-in replacement for `console` using {@link https://www.npmjs.com/package/loglevel|loglevel}.
- * Can be tailored down to specific use cases if needed.
- */
-export const logger = log.getLogger(DEFAULT_NAMESPACE) as PrefixedLogger;
-logger.setLevel(log.levels.DEBUG, false);
-
-export interface PrefixedLogger extends Logger {
- withPrefix: (prefix: string) => PrefixedLogger;
- prefix: string;
-}
-
-function extendLogger(logger: Logger): void {
- (<PrefixedLogger>logger).withPrefix = function (prefix: string): PrefixedLogger {
- const existingPrefix = this.prefix || "";
- return getPrefixedLogger(existingPrefix + prefix);
- };
-}
-
-extendLogger(logger);
-
-function getPrefixedLogger(prefix: string): PrefixedLogger {
- const prefixLogger = log.getLogger(`${DEFAULT_NAMESPACE}-${prefix}`) as PrefixedLogger;
- if (prefixLogger.prefix !== prefix) {
- // Only do this setup work the first time through, as loggers are saved by name.
- extendLogger(prefixLogger);
- prefixLogger.prefix = prefix;
- prefixLogger.setLevel(log.levels.DEBUG, false);
- }
- return prefixLogger;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/matrix.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/matrix.ts
deleted file mode 100644
index 591c5e3..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/matrix.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
-Copyright 2015-2022 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-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 { WidgetApi } from "matrix-widget-api";
-
-import { MemoryCryptoStore } from "./crypto/store/memory-crypto-store";
-import { MemoryStore } from "./store/memory";
-import { MatrixScheduler } from "./scheduler";
-import { MatrixClient, ICreateClientOpts } from "./client";
-import { RoomWidgetClient, ICapabilities } from "./embedded";
-import { CryptoStore } from "./crypto/store/base";
-
-export * from "./client";
-export * from "./embedded";
-export * from "./http-api";
-export * from "./autodiscovery";
-export * from "./sync-accumulator";
-export * from "./errors";
-export * from "./models/beacon";
-export * from "./models/event";
-export * from "./models/room";
-export * from "./models/event-timeline";
-export * from "./models/event-timeline-set";
-export * from "./models/poll";
-export * from "./models/room-member";
-export * from "./models/room-state";
-export * from "./models/user";
-export * from "./scheduler";
-export * from "./filter";
-export * from "./timeline-window";
-export * from "./interactive-auth";
-export * from "./service-types";
-export * from "./store/memory";
-export * from "./store/indexeddb";
-export * from "./crypto/store/memory-crypto-store";
-export * from "./crypto/store/indexeddb-crypto-store";
-export * from "./content-repo";
-export * from "./@types/event";
-export * from "./@types/PushRules";
-export * from "./@types/partials";
-export * from "./@types/requests";
-export * from "./@types/search";
-export * from "./models/room-summary";
-export * as ContentHelpers from "./content-helpers";
-export * as SecretStorage from "./secret-storage";
-export type { ICryptoCallbacks } from "./crypto"; // used to be located here
-export { createNewMatrixCall } from "./webrtc/call";
-export type { MatrixCall } from "./webrtc/call";
-export { GroupCallEvent, GroupCallIntent, GroupCallState, GroupCallType } from "./webrtc/groupCall";
-export type { GroupCall } from "./webrtc/groupCall";
-export type { CryptoApi } from "./crypto-api";
-
-let cryptoStoreFactory = (): CryptoStore => new MemoryCryptoStore();
-
-/**
- * Configure a different factory to be used for creating crypto stores
- *
- * @param fac - a function which will return a new {@link CryptoStore}
- */
-export function setCryptoStoreFactory(fac: () => CryptoStore): void {
- cryptoStoreFactory = fac;
-}
-
-function amendClientOpts(opts: ICreateClientOpts): ICreateClientOpts {
- opts.store =
- opts.store ??
- new MemoryStore({
- localStorage: global.localStorage,
- });
- opts.scheduler = opts.scheduler ?? new MatrixScheduler();
- opts.cryptoStore = opts.cryptoStore ?? cryptoStoreFactory();
-
- return opts;
-}
-
-/**
- * Construct a Matrix Client. Similar to {@link MatrixClient}
- * except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
- * @param opts - The configuration options for this client. These configuration
- * options will be passed directly to {@link MatrixClient}.
- *
- * @returns A new matrix client.
- * @see {@link MatrixClient} for the full list of options for
- * `opts`.
- */
-export function createClient(opts: ICreateClientOpts): MatrixClient {
- return new MatrixClient(amendClientOpts(opts));
-}
-
-export function createRoomWidgetClient(
- widgetApi: WidgetApi,
- capabilities: ICapabilities,
- roomId: string,
- opts: ICreateClientOpts,
-): MatrixClient {
- return new RoomWidgetClient(widgetApi, capabilities, roomId, amendClientOpts(opts));
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/MSC3089Branch.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/MSC3089Branch.ts
deleted file mode 100644
index 27be4b8..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/MSC3089Branch.ts
+++ /dev/null
@@ -1,258 +0,0 @@
-/*
-Copyright 2021 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { MatrixClient } from "../client";
-import { IEncryptedFile, RelationType, UNSTABLE_MSC3089_BRANCH } from "../@types/event";
-import { IContent, MatrixEvent } from "./event";
-import { MSC3089TreeSpace } from "./MSC3089TreeSpace";
-import { EventTimeline } from "./event-timeline";
-import { FileType } from "../http-api";
-import type { ISendEventResponse } from "../@types/requests";
-
-/**
- * Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) branch - a reference
- * to a file (leaf) in the tree. Note that this is UNSTABLE and subject to breaking changes
- * without notice.
- */
-export class MSC3089Branch {
- public constructor(
- private client: MatrixClient,
- public readonly indexEvent: MatrixEvent,
- public readonly directory: MSC3089TreeSpace,
- ) {
- // Nothing to do
- }
-
- /**
- * The file ID.
- */
- public get id(): string {
- const stateKey = this.indexEvent.getStateKey();
- if (!stateKey) {
- throw new Error("State key not found for branch");
- }
- return stateKey;
- }
-
- /**
- * Whether this branch is active/valid.
- */
- public get isActive(): boolean {
- return this.indexEvent.getContent()["active"] === true;
- }
-
- /**
- * Version for the file, one-indexed.
- */
- public get version(): number {
- return this.indexEvent.getContent()["version"] ?? 1;
- }
-
- private get roomId(): string {
- return this.indexEvent.getRoomId()!;
- }
-
- /**
- * Deletes the file from the tree, including all prior edits/versions.
- * @returns Promise which resolves when complete.
- */
- public async delete(): Promise<void> {
- await this.client.sendStateEvent(this.roomId, UNSTABLE_MSC3089_BRANCH.name, {}, this.id);
- await this.client.redactEvent(this.roomId, this.id);
-
- const nextVersion = (await this.getVersionHistory())[1]; // [0] will be us
- if (nextVersion) await nextVersion.delete(); // implicit recursion
- }
-
- /**
- * Gets the name for this file.
- * @returns The name, or "Unnamed File" if unknown.
- */
- public getName(): string {
- return this.indexEvent.getContent()["name"] || "Unnamed File";
- }
-
- /**
- * Sets the name for this file.
- * @param name - The new name for this file.
- * @returns Promise which resolves when complete.
- */
- public async setName(name: string): Promise<void> {
- await this.client.sendStateEvent(
- this.roomId,
- UNSTABLE_MSC3089_BRANCH.name,
- {
- ...this.indexEvent.getContent(),
- name: name,
- },
- this.id,
- );
- }
-
- /**
- * Gets whether or not a file is locked.
- * @returns True if locked, false otherwise.
- */
- public isLocked(): boolean {
- return this.indexEvent.getContent()["locked"] || false;
- }
-
- /**
- * Sets a file as locked or unlocked.
- * @param locked - True to lock the file, false otherwise.
- * @returns Promise which resolves when complete.
- */
- public async setLocked(locked: boolean): Promise<void> {
- await this.client.sendStateEvent(
- this.roomId,
- UNSTABLE_MSC3089_BRANCH.name,
- {
- ...this.indexEvent.getContent(),
- locked: locked,
- },
- this.id,
- );
- }
-
- /**
- * Gets information about the file needed to download it.
- * @returns Information about the file.
- */
- public async getFileInfo(): Promise<{ info: IEncryptedFile; httpUrl: string }> {
- const event = await this.getFileEvent();
-
- const file = event.getOriginalContent()["file"];
- const httpUrl = this.client.mxcUrlToHttp(file["url"]);
-
- if (!httpUrl) {
- throw new Error(`No HTTP URL available for ${file["url"]}`);
- }
-
- return { info: file, httpUrl: httpUrl };
- }
-
- /**
- * Gets the event the file points to.
- * @returns Promise which resolves to the file's event.
- */
- public async getFileEvent(): Promise<MatrixEvent> {
- const room = this.client.getRoom(this.roomId);
- if (!room) throw new Error("Unknown room");
-
- let event: MatrixEvent | undefined = room.getUnfilteredTimelineSet().findEventById(this.id);
-
- // keep scrolling back if needed until we find the event or reach the start of the room:
- while (!event && room.getLiveTimeline().getState(EventTimeline.BACKWARDS)!.paginationToken) {
- await this.client.scrollback(room, 100);
- event = room.getUnfilteredTimelineSet().findEventById(this.id);
- }
-
- if (!event) throw new Error("Failed to find event");
-
- // Sometimes the event isn't decrypted for us, so do that. We specifically set `emit: true`
- // to ensure that the relations system in the sdk will function.
- await this.client.decryptEventIfNeeded(event, { emit: true, isRetry: true });
-
- return event;
- }
-
- /**
- * Creates a new version of this file with contents in a type that is compatible with MatrixClient.uploadContent().
- * @param name - The name of the file.
- * @param encryptedContents - The encrypted contents.
- * @param info - The encrypted file information.
- * @param additionalContent - Optional event content fields to include in the message.
- * @returns Promise which resolves to the file event's sent response.
- */
- public async createNewVersion(
- name: string,
- encryptedContents: FileType,
- info: Partial<IEncryptedFile>,
- additionalContent?: IContent,
- ): Promise<ISendEventResponse> {
- const fileEventResponse = await this.directory.createFile(name, encryptedContents, info, {
- ...(additionalContent ?? {}),
- "m.new_content": true,
- "m.relates_to": {
- rel_type: RelationType.Replace,
- event_id: this.id,
- },
- });
-
- // Update the version of the new event
- await this.client.sendStateEvent(
- this.roomId,
- UNSTABLE_MSC3089_BRANCH.name,
- {
- active: true,
- name: name,
- version: this.version + 1,
- },
- fileEventResponse["event_id"],
- );
-
- // Deprecate ourselves
- await this.client.sendStateEvent(
- this.roomId,
- UNSTABLE_MSC3089_BRANCH.name,
- {
- ...this.indexEvent.getContent(),
- active: false,
- },
- this.id,
- );
-
- return fileEventResponse;
- }
-
- /**
- * Gets the file's version history, starting at this file.
- * @returns Promise which resolves to the file's version history, with the
- * first element being the current version and the last element being the first version.
- */
- public async getVersionHistory(): Promise<MSC3089Branch[]> {
- const fileHistory: MSC3089Branch[] = [];
- fileHistory.push(this); // start with ourselves
-
- const room = this.client.getRoom(this.roomId);
- if (!room) throw new Error("Invalid or unknown room");
-
- // Clone the timeline to reverse it, getting most-recent-first ordering, hopefully
- // shortening the awful loop below. Without the clone, we can unintentionally mutate
- // the timeline.
- const timelineEvents = [...room.getLiveTimeline().getEvents()].reverse();
-
- // XXX: This is a very inefficient search, but it's the best we can do with the
- // relations structure we have in the SDK. As of writing, it is not worth the
- // investment in improving the structure.
- let childEvent: MatrixEvent | undefined;
- let parentEvent = await this.getFileEvent();
- do {
- childEvent = timelineEvents.find((e) => e.replacingEventId() === parentEvent.getId());
- if (childEvent) {
- const branch = this.directory.getFile(childEvent.getId()!);
- if (branch) {
- fileHistory.push(branch);
- parentEvent = childEvent;
- } else {
- break; // prevent infinite loop
- }
- }
- } while (childEvent);
-
- return fileHistory;
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/MSC3089TreeSpace.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/MSC3089TreeSpace.ts
deleted file mode 100644
index b0e71d9..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/MSC3089TreeSpace.ts
+++ /dev/null
@@ -1,566 +0,0 @@
-/*
-Copyright 2021 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 promiseRetry from "p-retry";
-
-import { MatrixClient } from "../client";
-import { EventType, IEncryptedFile, MsgType, UNSTABLE_MSC3089_BRANCH, UNSTABLE_MSC3089_LEAF } from "../@types/event";
-import { Room } from "./room";
-import { logger } from "../logger";
-import { IContent, MatrixEvent } from "./event";
-import {
- averageBetweenStrings,
- DEFAULT_ALPHABET,
- lexicographicCompare,
- nextString,
- prevString,
- simpleRetryOperation,
-} from "../utils";
-import { MSC3089Branch } from "./MSC3089Branch";
-import { isRoomSharedHistory } from "../crypto/algorithms/megolm";
-import { ISendEventResponse } from "../@types/requests";
-import { FileType } from "../http-api";
-
-/**
- * The recommended defaults for a tree space's power levels. Note that this
- * is UNSTABLE and subject to breaking changes without notice.
- */
-export const DEFAULT_TREE_POWER_LEVELS_TEMPLATE = {
- // Owner
- invite: 100,
- kick: 100,
- ban: 100,
-
- // Editor
- redact: 50,
- state_default: 50,
- events_default: 50,
-
- // Viewer
- users_default: 0,
-
- // Mixed
- events: {
- [EventType.RoomPowerLevels]: 100,
- [EventType.RoomHistoryVisibility]: 100,
- [EventType.RoomTombstone]: 100,
- [EventType.RoomEncryption]: 100,
- [EventType.RoomName]: 50,
- [EventType.RoomMessage]: 50,
- [EventType.RoomMessageEncrypted]: 50,
- [EventType.Sticker]: 50,
- },
-
- users: {}, // defined by calling code
-};
-
-/**
- * Ease-of-use representation for power levels represented as simple roles.
- * Note that this is UNSTABLE and subject to breaking changes without notice.
- */
-export enum TreePermissions {
- Viewer = "viewer", // Default
- Editor = "editor", // "Moderator" or ~PL50
- Owner = "owner", // "Admin" or PL100
-}
-
-/**
- * Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089)
- * file tree Space. Note that this is UNSTABLE and subject to breaking changes
- * without notice.
- */
-export class MSC3089TreeSpace {
- public readonly room: Room;
-
- public constructor(private client: MatrixClient, public readonly roomId: string) {
- this.room = this.client.getRoom(this.roomId)!;
-
- if (!this.room) throw new Error("Unknown room");
- }
-
- /**
- * Syntactic sugar for room ID of the Space.
- */
- public get id(): string {
- return this.roomId;
- }
-
- /**
- * Whether or not this is a top level space.
- */
- public get isTopLevel(): boolean {
- // XXX: This is absolutely not how you find out if the space is top level
- // but is safe for a managed usecase like we offer in the SDK.
- const parentEvents = this.room.currentState.getStateEvents(EventType.SpaceParent);
- if (!parentEvents?.length) return true;
- return parentEvents.every((e) => !e.getContent()?.["via"]);
- }
-
- /**
- * Sets the name of the tree space.
- * @param name - The new name for the space.
- * @returns Promise which resolves when complete.
- */
- public async setName(name: string): Promise<void> {
- await this.client.sendStateEvent(this.roomId, EventType.RoomName, { name }, "");
- }
-
- /**
- * Invites a user to the tree space. They will be given the default Viewer
- * permission level unless specified elsewhere.
- * @param userId - The user ID to invite.
- * @param andSubspaces - True (default) to invite the user to all
- * directories/subspaces too, recursively.
- * @param shareHistoryKeys - True (default) to share encryption keys
- * with the invited user. This will allow them to decrypt the events (files)
- * in the tree. Keys will not be shared if the room is lacking appropriate
- * history visibility (by default, history visibility is "shared" in trees,
- * which is an appropriate visibility for these purposes).
- * @returns Promise which resolves when complete.
- */
- public async invite(userId: string, andSubspaces = true, shareHistoryKeys = true): Promise<void> {
- const promises: Promise<void>[] = [this.retryInvite(userId)];
- if (andSubspaces) {
- promises.push(...this.getDirectories().map((d) => d.invite(userId, andSubspaces, shareHistoryKeys)));
- }
- return Promise.all(promises).then(() => {
- // Note: key sharing is default on because for file trees it is relatively important that the invite
- // target can actually decrypt the files. The implied use case is that by inviting a user to the tree
- // it means the sender would like the receiver to view/download the files contained within, much like
- // sharing a folder in other circles.
- if (shareHistoryKeys && isRoomSharedHistory(this.room)) {
- // noinspection JSIgnoredPromiseFromCall - we aren't concerned as much if this fails.
- this.client.sendSharedHistoryKeys(this.roomId, [userId]);
- }
- });
- }
-
- private retryInvite(userId: string): Promise<void> {
- return simpleRetryOperation(async () => {
- await this.client.invite(this.roomId, userId).catch((e) => {
- // We don't want to retry permission errors forever...
- if (e?.errcode === "M_FORBIDDEN") {
- throw new promiseRetry.AbortError(e);
- }
- throw e;
- });
- });
- }
-
- /**
- * Sets the permissions of a user to the given role. Note that if setting a user
- * to Owner then they will NOT be able to be demoted. If the user does not have
- * permission to change the power level of the target, an error will be thrown.
- * @param userId - The user ID to change the role of.
- * @param role - The role to assign.
- * @returns Promise which resolves when complete.
- */
- public async setPermissions(userId: string, role: TreePermissions): Promise<void> {
- const currentPls = this.room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
- if (Array.isArray(currentPls)) throw new Error("Unexpected return type for power levels");
-
- const pls = currentPls?.getContent() || {};
- const viewLevel = pls["users_default"] || 0;
- const editLevel = pls["events_default"] || 50;
- const adminLevel = pls["events"]?.[EventType.RoomPowerLevels] || 100;
-
- const users = pls["users"] || {};
- switch (role) {
- case TreePermissions.Viewer:
- users[userId] = viewLevel;
- break;
- case TreePermissions.Editor:
- users[userId] = editLevel;
- break;
- case TreePermissions.Owner:
- users[userId] = adminLevel;
- break;
- default:
- throw new Error("Invalid role: " + role);
- }
- pls["users"] = users;
-
- await this.client.sendStateEvent(this.roomId, EventType.RoomPowerLevels, pls, "");
- }
-
- /**
- * Gets the current permissions of a user. Note that any users missing explicit permissions (or not
- * in the space) will be considered Viewers. Appropriate membership checks need to be performed
- * elsewhere.
- * @param userId - The user ID to check permissions of.
- * @returns The permissions for the user, defaulting to Viewer.
- */
- public getPermissions(userId: string): TreePermissions {
- const currentPls = this.room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
- if (Array.isArray(currentPls)) throw new Error("Unexpected return type for power levels");
-
- const pls = currentPls?.getContent() || {};
- const viewLevel = pls["users_default"] || 0;
- const editLevel = pls["events_default"] || 50;
- const adminLevel = pls["events"]?.[EventType.RoomPowerLevels] || 100;
-
- const userLevel = pls["users"]?.[userId] || viewLevel;
- if (userLevel >= adminLevel) return TreePermissions.Owner;
- if (userLevel >= editLevel) return TreePermissions.Editor;
- return TreePermissions.Viewer;
- }
-
- /**
- * Creates a directory under this tree space, represented as another tree space.
- * @param name - The name for the directory.
- * @returns Promise which resolves to the created directory.
- */
- public async createDirectory(name: string): Promise<MSC3089TreeSpace> {
- const directory = await this.client.unstableCreateFileTree(name);
-
- await this.client.sendStateEvent(
- this.roomId,
- EventType.SpaceChild,
- {
- via: [this.client.getDomain()],
- },
- directory.roomId,
- );
-
- await this.client.sendStateEvent(
- directory.roomId,
- EventType.SpaceParent,
- {
- via: [this.client.getDomain()],
- },
- this.roomId,
- );
-
- return directory;
- }
-
- /**
- * Gets a list of all known immediate subdirectories to this tree space.
- * @returns The tree spaces (directories). May be empty, but not null.
- */
- public getDirectories(): MSC3089TreeSpace[] {
- const trees: MSC3089TreeSpace[] = [];
- const children = this.room.currentState.getStateEvents(EventType.SpaceChild);
- for (const child of children) {
- try {
- const stateKey = child.getStateKey();
- if (stateKey) {
- const tree = this.client.unstableGetFileTreeSpace(stateKey);
- if (tree) trees.push(tree);
- }
- } catch (e) {
- logger.warn("Unable to create tree space instance for listing. Are we joined?", e);
- }
- }
- return trees;
- }
-
- /**
- * Gets a subdirectory of a given ID under this tree space. Note that this will not recurse
- * into children and instead only look one level deep.
- * @param roomId - The room ID (directory ID) to find.
- * @returns The directory, or undefined if not found.
- */
- public getDirectory(roomId: string): MSC3089TreeSpace | undefined {
- return this.getDirectories().find((r) => r.roomId === roomId);
- }
-
- /**
- * Deletes the tree, kicking all members and deleting **all subdirectories**.
- * @returns Promise which resolves when complete.
- */
- public async delete(): Promise<void> {
- const subdirectories = this.getDirectories();
- for (const dir of subdirectories) {
- await dir.delete();
- }
-
- const kickMemberships = ["invite", "knock", "join"];
- const members = this.room.currentState.getStateEvents(EventType.RoomMember);
- for (const member of members) {
- const isNotUs = member.getStateKey() !== this.client.getUserId();
- if (isNotUs && kickMemberships.includes(member.getContent().membership!)) {
- const stateKey = member.getStateKey();
- if (!stateKey) {
- throw new Error("State key not found for branch");
- }
- await this.client.kick(this.roomId, stateKey, "Room deleted");
- }
- }
-
- await this.client.leave(this.roomId);
- }
-
- private getOrderedChildren(children: MatrixEvent[]): { roomId: string; order: string }[] {
- const ordered: { roomId: string; order: string }[] = children
- .map((c) => ({ roomId: c.getStateKey(), order: c.getContent()["order"] }))
- .filter((c) => c.roomId) as { roomId: string; order: string }[];
- ordered.sort((a, b) => {
- if (a.order && !b.order) {
- return -1;
- } else if (!a.order && b.order) {
- return 1;
- } else if (!a.order && !b.order) {
- const roomA = this.client.getRoom(a.roomId);
- const roomB = this.client.getRoom(b.roomId);
- if (!roomA || !roomB) {
- // just don't bother trying to do more partial sorting
- return lexicographicCompare(a.roomId, b.roomId);
- }
-
- const createTsA = roomA.currentState.getStateEvents(EventType.RoomCreate, "")?.getTs() ?? 0;
- const createTsB = roomB.currentState.getStateEvents(EventType.RoomCreate, "")?.getTs() ?? 0;
- if (createTsA === createTsB) {
- return lexicographicCompare(a.roomId, b.roomId);
- }
- return createTsA - createTsB;
- } else {
- // both not-null orders
- return lexicographicCompare(a.order, b.order);
- }
- });
- return ordered;
- }
-
- private getParentRoom(): Room {
- const parents = this.room.currentState.getStateEvents(EventType.SpaceParent);
- const parent = parents[0]; // XXX: Wild assumption
- if (!parent) throw new Error("Expected to have a parent in a non-top level space");
-
- // XXX: We are assuming the parent is a valid tree space.
- // We probably don't need to validate the parent room state for this usecase though.
- const stateKey = parent.getStateKey();
- if (!stateKey) throw new Error("No state key found for parent");
- const parentRoom = this.client.getRoom(stateKey);
- if (!parentRoom) throw new Error("Unable to locate room for parent");
-
- return parentRoom;
- }
-
- /**
- * Gets the current order index for this directory. Note that if this is the top level space
- * then -1 will be returned.
- * @returns The order index of this space.
- */
- public getOrder(): number {
- if (this.isTopLevel) return -1;
-
- const parentRoom = this.getParentRoom();
- const children = parentRoom.currentState.getStateEvents(EventType.SpaceChild);
- const ordered = this.getOrderedChildren(children);
-
- return ordered.findIndex((c) => c.roomId === this.roomId);
- }
-
- /**
- * Sets the order index for this directory within its parent. Note that if this is a top level
- * space then an error will be thrown. -1 can be used to move the child to the start, and numbers
- * larger than the number of children can be used to move the child to the end.
- * @param index - The new order index for this space.
- * @returns Promise which resolves when complete.
- * @throws Throws if this is a top level space.
- */
- public async setOrder(index: number): Promise<void> {
- if (this.isTopLevel) throw new Error("Cannot set order of top level spaces currently");
-
- const parentRoom = this.getParentRoom();
- const children = parentRoom.currentState.getStateEvents(EventType.SpaceChild);
- const ordered = this.getOrderedChildren(children);
- index = Math.max(Math.min(index, ordered.length - 1), 0);
-
- const currentIndex = this.getOrder();
- const movingUp = currentIndex < index;
- if (movingUp && index === ordered.length - 1) {
- index--;
- } else if (!movingUp && index === 0) {
- index++;
- }
-
- const prev = ordered[movingUp ? index : index - 1];
- const next = ordered[movingUp ? index + 1 : index];
-
- let newOrder = DEFAULT_ALPHABET[0];
- let ensureBeforeIsSane = false;
- if (!prev) {
- // Move to front
- if (next?.order) {
- newOrder = prevString(next.order);
- }
- } else if (index === ordered.length - 1) {
- // Move to back
- if (next?.order) {
- newOrder = nextString(next.order);
- }
- } else {
- // Move somewhere in the middle
- const startOrder = prev?.order;
- const endOrder = next?.order;
- if (startOrder && endOrder) {
- if (startOrder === endOrder) {
- // Error case: just move +1 to break out of awful math
- newOrder = nextString(startOrder);
- } else {
- newOrder = averageBetweenStrings(startOrder, endOrder);
- }
- } else {
- if (startOrder) {
- // We're at the end (endOrder is null, so no explicit order)
- newOrder = nextString(startOrder);
- } else if (endOrder) {
- // We're at the start (startOrder is null, so nothing before us)
- newOrder = prevString(endOrder);
- } else {
- // Both points are unknown. We're likely in a range where all the children
- // don't have particular order values, so we may need to update them too.
- // The other possibility is there's only us as a child, but we should have
- // shown up in the other states.
- ensureBeforeIsSane = true;
- }
- }
- }
-
- if (ensureBeforeIsSane) {
- // We were asked by the order algorithm to prepare the moving space for a landing
- // in the undefined order part of the order array, which means we need to update the
- // spaces that come before it with a stable order value.
- let lastOrder: string | undefined;
- for (let i = 0; i <= index; i++) {
- const target = ordered[i];
- if (i === 0) {
- lastOrder = target.order;
- }
- if (!target.order) {
- // XXX: We should be creating gaps to avoid conflicts
- lastOrder = lastOrder ? nextString(lastOrder) : DEFAULT_ALPHABET[0];
- const currentChild = parentRoom.currentState.getStateEvents(EventType.SpaceChild, target.roomId);
- const content = currentChild?.getContent() ?? { via: [this.client.getDomain()] };
- await this.client.sendStateEvent(
- parentRoom.roomId,
- EventType.SpaceChild,
- {
- ...content,
- order: lastOrder,
- },
- target.roomId,
- );
- } else {
- lastOrder = target.order;
- }
- }
- if (lastOrder) {
- newOrder = nextString(lastOrder);
- }
- }
-
- // TODO: Deal with order conflicts by reordering
-
- // Now we can finally update our own order state
- const currentChild = parentRoom.currentState.getStateEvents(EventType.SpaceChild, this.roomId);
- const content = currentChild?.getContent() ?? { via: [this.client.getDomain()] };
- await this.client.sendStateEvent(
- parentRoom.roomId,
- EventType.SpaceChild,
- {
- ...content,
-
- // TODO: Safely constrain to 50 character limit required by spaces.
- order: newOrder,
- },
- this.roomId,
- );
- }
-
- /**
- * Creates (uploads) a new file to this tree. The file must have already been encrypted for the room.
- * The file contents are in a type that is compatible with MatrixClient.uploadContent().
- * @param name - The name of the file.
- * @param encryptedContents - The encrypted contents.
- * @param info - The encrypted file information.
- * @param additionalContent - Optional event content fields to include in the message.
- * @returns Promise which resolves to the file event's sent response.
- */
- public async createFile(
- name: string,
- encryptedContents: FileType,
- info: Partial<IEncryptedFile>,
- additionalContent?: IContent,
- ): Promise<ISendEventResponse> {
- const { content_uri: mxc } = await this.client.uploadContent(encryptedContents, {
- includeFilename: false,
- });
- info.url = mxc;
-
- const fileContent = {
- msgtype: MsgType.File,
- body: name,
- url: mxc,
- file: info,
- };
-
- additionalContent = additionalContent ?? {};
- if (additionalContent["m.new_content"]) {
- // We do the right thing according to the spec, but due to how relations are
- // handled we also end up duplicating this information to the regular `content`
- // as well.
- additionalContent["m.new_content"] = fileContent;
- }
-
- const res = await this.client.sendMessage(this.roomId, {
- ...additionalContent,
- ...fileContent,
- [UNSTABLE_MSC3089_LEAF.name]: {},
- });
-
- await this.client.sendStateEvent(
- this.roomId,
- UNSTABLE_MSC3089_BRANCH.name,
- {
- active: true,
- name: name,
- },
- res["event_id"],
- );
-
- return res;
- }
-
- /**
- * Retrieves a file from the tree.
- * @param fileEventId - The event ID of the file.
- * @returns The file, or null if not found.
- */
- public getFile(fileEventId: string): MSC3089Branch | null {
- const branch = this.room.currentState.getStateEvents(UNSTABLE_MSC3089_BRANCH.name, fileEventId);
- return branch ? new MSC3089Branch(this.client, branch, this) : null;
- }
-
- /**
- * Gets an array of all known files for the tree.
- * @returns The known files. May be empty, but not null.
- */
- public listFiles(): MSC3089Branch[] {
- return this.listAllFiles().filter((b) => b.isActive);
- }
-
- /**
- * Gets an array of all known files for the tree, including inactive/invalid ones.
- * @returns The known files. May be empty, but not null.
- */
- public listAllFiles(): MSC3089Branch[] {
- const branches = this.room.currentState.getStateEvents(UNSTABLE_MSC3089_BRANCH.name) ?? [];
- return branches.map((e) => new MSC3089Branch(this.client, e, this));
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/ToDeviceMessage.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/ToDeviceMessage.ts
deleted file mode 100644
index 8efc3ed..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/ToDeviceMessage.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
-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.
-*/
-
-export type ToDevicePayload = Record<string, any>;
-
-export interface ToDeviceMessage {
- userId: string;
- deviceId: string;
- payload: ToDevicePayload;
-}
-
-export interface ToDeviceBatch {
- eventType: string;
- batch: ToDeviceMessage[];
-}
-
-// Only used internally
-export interface ToDeviceBatchWithTxnId extends ToDeviceBatch {
- txnId: string;
-}
-
-// Only used internally
-export interface IndexedToDeviceBatch extends ToDeviceBatchWithTxnId {
- id: number;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/beacon.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/beacon.ts
deleted file mode 100644
index 3801831..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/beacon.ts
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
-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 { MBeaconEventContent } from "../@types/beacon";
-import { BeaconInfoState, BeaconLocationState, parseBeaconContent, parseBeaconInfoContent } from "../content-helpers";
-import { MatrixEvent } from "./event";
-import { sortEventsByLatestContentTimestamp } from "../utils";
-import { TypedEventEmitter } from "./typed-event-emitter";
-
-export enum BeaconEvent {
- New = "Beacon.new",
- Update = "Beacon.update",
- LivenessChange = "Beacon.LivenessChange",
- 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 = (startTimestamp: number, durationMs: number, timestamp: number): boolean =>
- timestamp >= startTimestamp && startTimestamp + durationMs >= timestamp;
-
-// 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/3672
-export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.New>, BeaconEventHandlerMap> {
- public readonly roomId: string;
- // beaconInfo is assigned by setBeaconInfo in the constructor
- // ! to make tsc believe it is definitely assigned
- private _beaconInfo!: BeaconInfoState;
- private _isLive?: boolean;
- private livenessWatchTimeout?: ReturnType<typeof setTimeout>;
- private _latestLocationEvent?: MatrixEvent;
-
- public constructor(private rootEvent: MatrixEvent) {
- super();
- this.roomId = this.rootEvent.getRoomId()!;
- this.setBeaconInfo(this.rootEvent);
- }
-
- public get isLive(): boolean {
- return !!this._isLive;
- }
-
- public get identifier(): BeaconIdentifier {
- return getBeaconInfoIdentifier(this.rootEvent);
- }
-
- public get beaconInfoId(): string {
- return this.rootEvent.getId()!;
- }
-
- public get beaconInfoOwner(): string {
- return this.rootEvent.getStateKey()!;
- }
-
- public get beaconInfoEventType(): string {
- return this.rootEvent.getType();
- }
-
- public get beaconInfo(): BeaconInfoState {
- return this._beaconInfo;
- }
-
- public get latestLocationState(): BeaconLocationState | undefined {
- return this._latestLocationEvent && parseBeaconContent(this._latestLocationEvent.getContent());
- }
-
- public get latestLocationEvent(): MatrixEvent | undefined {
- return this._latestLocationEvent;
- }
-
- public update(beaconInfoEvent: MatrixEvent): void {
- if (getBeaconInfoIdentifier(beaconInfoEvent) !== this.identifier) {
- throw new Error("Invalid updating event");
- }
- // don't update beacon with an older event
- if (beaconInfoEvent.getTs() < this.rootEvent.getTs()) {
- return;
- }
- this.rootEvent = beaconInfoEvent;
- this.setBeaconInfo(this.rootEvent);
-
- this.emit(BeaconEvent.Update, beaconInfoEvent, this);
- this.clearLatestLocation();
- }
-
- public destroy(): void {
- if (this.livenessWatchTimeout) {
- clearTimeout(this.livenessWatchTimeout);
- }
-
- this._isLive = false;
- this.emit(BeaconEvent.Destroy, this.identifier);
- }
-
- /**
- * Monitor liveness of a beacon
- * Emits BeaconEvent.LivenessChange when beacon expires
- */
- public monitorLiveness(): void {
- if (this.livenessWatchTimeout) {
- clearTimeout(this.livenessWatchTimeout);
- }
-
- this.checkLiveness();
- if (!this.beaconInfo) return;
- if (this.isLive) {
- const expiryInMs = this.beaconInfo.timestamp! + this.beaconInfo.timeout - Date.now();
- if (expiryInMs > 1) {
- this.livenessWatchTimeout = setTimeout(() => {
- this.monitorLiveness();
- }, expiryInMs);
- }
- } else if (this.beaconInfo.timestamp! > Date.now()) {
- // beacon start timestamp is in the future
- // check liveness again then
- this.livenessWatchTimeout = setTimeout(() => {
- this.monitorLiveness();
- }, this.beaconInfo.timestamp! - Date.now());
- }
- }
-
- /**
- * 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 parsed = parseBeaconContent(content);
- if (!parsed.uri || !parsed.timestamp) return false; // we won't be able to process these
- const { timestamp } = parsed;
- return (
- this._beaconInfo.timestamp &&
- // 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._latestLocationEvent = latestLocationEvent;
- this.emit(BeaconEvent.LocationUpdate, this.latestLocationState!);
- }
- }
-
- private clearLatestLocation = (): void => {
- this._latestLocationEvent = undefined;
- this.emit(BeaconEvent.LocationUpdate, this.latestLocationState!);
- };
-
- private setBeaconInfo(event: MatrixEvent): void {
- this._beaconInfo = parseBeaconInfoContent(event.getContent());
- this.checkLiveness();
- }
-
- private checkLiveness(): void {
- const prevLiveness = this.isLive;
-
- // element web sets a beacon's start timestamp to the senders local current time
- // when Alice's system clock deviates slightly from Bob's a beacon Alice intended to be live
- // may have a start timestamp in the future from Bob's POV
- // handle this by adding 6min of leniency to the start timestamp when it is in the future
- if (!this.beaconInfo) return;
- const startTimestamp =
- this.beaconInfo.timestamp! > Date.now()
- ? this.beaconInfo.timestamp! - 360000 /* 6min */
- : this.beaconInfo.timestamp;
- this._isLive =
- !!this._beaconInfo.live &&
- !!startTimestamp &&
- isTimestampInDuration(startTimestamp, this._beaconInfo.timeout, Date.now());
-
- if (prevLiveness !== this.isLive) {
- this.emit(BeaconEvent.LivenessChange, this.isLive, this);
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-context.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-context.ts
deleted file mode 100644
index 0401cd5..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-context.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
-Copyright 2015 - 2021 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 { MatrixEvent } from "./event";
-import { Direction } from "./event-timeline";
-
-export class EventContext {
- private timeline: MatrixEvent[];
- private ourEventIndex = 0;
- private paginateTokens: Record<Direction, string | null> = {
- [Direction.Backward]: null,
- [Direction.Forward]: null,
- };
-
- /**
- * Construct a new EventContext
- *
- * An eventcontext is used for circumstances such as search results, when we
- * have a particular event of interest, and a bunch of events before and after
- * it.
- *
- * It also stores pagination tokens for going backwards and forwards in the
- * timeline.
- *
- * @param ourEvent - the event at the centre of this context
- */
- public constructor(public readonly ourEvent: MatrixEvent) {
- this.timeline = [ourEvent];
- }
-
- /**
- * Get the main event of interest
- *
- * This is a convenience function for getTimeline()[getOurEventIndex()].
- *
- * @returns The event at the centre of this context.
- */
- public getEvent(): MatrixEvent {
- return this.timeline[this.ourEventIndex];
- }
-
- /**
- * Get the list of events in this context
- *
- * @returns An array of MatrixEvents
- */
- public getTimeline(): MatrixEvent[] {
- return this.timeline;
- }
-
- /**
- * Get the index in the timeline of our event
- */
- public getOurEventIndex(): number {
- return this.ourEventIndex;
- }
-
- /**
- * Get a pagination token.
- *
- * @param backwards - true to get the pagination token for going
- */
- public getPaginateToken(backwards = false): string | null {
- return this.paginateTokens[backwards ? Direction.Backward : Direction.Forward];
- }
-
- /**
- * Set a pagination token.
- *
- * Generally this will be used only by the matrix js sdk.
- *
- * @param token - pagination token
- * @param backwards - true to set the pagination token for going
- * backwards in time
- */
- public setPaginateToken(token?: string, backwards = false): void {
- this.paginateTokens[backwards ? Direction.Backward : Direction.Forward] = token ?? null;
- }
-
- /**
- * Add more events to the timeline
- *
- * @param events - new events, in timeline order
- * @param atStart - true to insert new events at the start
- */
- public addEvents(events: MatrixEvent[], atStart = false): void {
- // TODO: should we share logic with Room.addEventsToTimeline?
- // Should Room even use EventContext?
-
- if (atStart) {
- this.timeline = events.concat(this.timeline);
- this.ourEventIndex += events.length;
- } else {
- this.timeline = this.timeline.concat(events);
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-status.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-status.ts
deleted file mode 100644
index a5113e0..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-status.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
-Copyright 2015 - 2022 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-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.
-*/
-
-/**
- * Enum for event statuses.
- * @readonly
- */
-export enum EventStatus {
- /** The event was not sent and will no longer be retried. */
- NOT_SENT = "not_sent",
-
- /** The message is being encrypted */
- ENCRYPTING = "encrypting",
-
- /** The event is in the process of being sent. */
- SENDING = "sending",
-
- /** The event is in a queue waiting to be sent. */
- QUEUED = "queued",
-
- /** The event has been sent to the server, but we have not yet received the echo. */
- SENT = "sent",
-
- /** The event was cancelled before it was successfully sent. */
- CANCELLED = "cancelled",
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline-set.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline-set.ts
deleted file mode 100644
index 5cb0499..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline-set.ts
+++ /dev/null
@@ -1,906 +0,0 @@
-/*
-Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { EventTimeline, IAddEventOptions } from "./event-timeline";
-import { MatrixEvent } from "./event";
-import { logger } from "../logger";
-import { Room, RoomEvent } from "./room";
-import { Filter } from "../filter";
-import { RoomState } from "./room-state";
-import { TypedEventEmitter } from "./typed-event-emitter";
-import { RelationsContainer } from "./relations-container";
-import { MatrixClient } from "../client";
-import { Thread, ThreadFilterType } from "./thread";
-
-const DEBUG = true;
-
-/* istanbul ignore next */
-let debuglog: (...args: any[]) => void;
-if (DEBUG) {
- // using bind means that we get to keep useful line numbers in the console
- debuglog = logger.log.bind(logger);
-} else {
- /* istanbul ignore next */
- debuglog = function (): void {};
-}
-
-interface IOpts {
- // Set to true to enable improved timeline support.
- timelineSupport?: boolean;
- // The filter object, if any, for this timelineSet.
- filter?: Filter;
- pendingEvents?: boolean;
-}
-
-export enum DuplicateStrategy {
- Ignore = "ignore",
- Replace = "replace",
-}
-
-export interface IRoomTimelineData {
- // the timeline the event was added to/removed from
- timeline: EventTimeline;
- // true if the event was a real-time event added to the end of the live timeline
- liveEvent?: boolean;
-}
-
-export interface IAddEventToTimelineOptions
- extends Pick<IAddEventOptions, "toStartOfTimeline" | "roomState" | "timelineWasEmpty"> {
- /** Whether the sync response came from cache */
- fromCache?: boolean;
-}
-
-export interface IAddLiveEventOptions
- extends Pick<IAddEventToTimelineOptions, "fromCache" | "roomState" | "timelineWasEmpty"> {
- /** Applies to events in the timeline only. If this is 'replace' then if a
- * duplicate is encountered, the event passed to this function will replace
- * the existing event in the timeline. If this is not specified, or is
- * 'ignore', then the event passed to this function will be ignored
- * entirely, preserving the existing event in the timeline. Events are
- * identical based on their event ID <b>only</b>. */
- duplicateStrategy?: DuplicateStrategy;
-}
-
-type EmittedEvents = RoomEvent.Timeline | RoomEvent.TimelineReset;
-
-export type EventTimelineSetHandlerMap = {
- /**
- * Fires whenever the timeline in a room is updated.
- * @param event - The matrix event which caused this event to fire.
- * @param room - The room, if any, whose timeline was updated.
- * @param toStartOfTimeline - True if this event was added to the start
- * @param removed - True if this event has just been removed from the timeline
- * (beginning; oldest) of the timeline e.g. due to pagination.
- *
- * @param data - more data about the event
- *
- * @example
- * ```
- * matrixClient.on("Room.timeline",
- * function(event, room, toStartOfTimeline, removed, data) {
- * if (!toStartOfTimeline && data.liveEvent) {
- * var messageToAppend = room.timeline.[room.timeline.length - 1];
- * }
- * });
- * ```
- */
- [RoomEvent.Timeline]: (
- event: MatrixEvent,
- room: Room | undefined,
- toStartOfTimeline: boolean | undefined,
- removed: boolean,
- data: IRoomTimelineData,
- ) => void;
- /**
- * Fires whenever the live timeline in a room is reset.
- *
- * When we get a 'limited' sync (for example, after a network outage), we reset
- * the live timeline to be empty before adding the recent events to the new
- * timeline. This event is fired after the timeline is reset, and before the
- * new events are added.
- *
- * @param room - The room whose live timeline was reset, if any
- * @param timelineSet - timelineSet room whose live timeline was reset
- * @param resetAllTimelines - True if all timelines were reset.
- */
- [RoomEvent.TimelineReset]: (
- room: Room | undefined,
- eventTimelineSet: EventTimelineSet,
- resetAllTimelines: boolean,
- ) => void;
-};
-
-export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTimelineSetHandlerMap> {
- public readonly relations: RelationsContainer;
- private readonly timelineSupport: boolean;
- private readonly displayPendingEvents: boolean;
- private liveTimeline: EventTimeline;
- private timelines: EventTimeline[];
- private _eventIdToTimeline = new Map<string, EventTimeline>();
- private filter?: Filter;
-
- /**
- * Construct a set of EventTimeline objects, typically on behalf of a given
- * room. A room may have multiple EventTimelineSets for different levels
- * of filtering. The global notification list is also an EventTimelineSet, but
- * lacks a room.
- *
- * <p>This is an ordered sequence of timelines, which may or may not
- * be continuous. Each timeline lists a series of events, as well as tracking
- * the room state at the start and the end of the timeline (if appropriate).
- * It also tracks forward and backward pagination tokens, as well as containing
- * links to the next timeline in the sequence.
- *
- * <p>There is one special timeline - the 'live' timeline, which represents the
- * timeline to which events are being added in real-time as they are received
- * from the /sync API. Note that you should not retain references to this
- * timeline - even if it is the current timeline right now, it may not remain
- * so if the server gives us a timeline gap in /sync.
- *
- * <p>In order that we can find events from their ids later, we also maintain a
- * map from event_id to timeline and index.
- *
- * @param room - Room for this timelineSet. May be null for non-room cases, such as the
- * notification timeline.
- * @param opts - Options inherited from Room.
- * @param client - the Matrix client which owns this EventTimelineSet,
- * can be omitted if room is specified.
- * @param thread - the thread to which this timeline set relates.
- * @param isThreadTimeline - Whether this timeline set relates to a thread list timeline
- * (e.g., All threads or My threads)
- */
- public constructor(
- public readonly room: Room | undefined,
- opts: IOpts = {},
- client?: MatrixClient,
- public readonly thread?: Thread,
- public readonly threadListType: ThreadFilterType | null = null,
- ) {
- super();
-
- this.timelineSupport = Boolean(opts.timelineSupport);
- this.liveTimeline = new EventTimeline(this);
- this.displayPendingEvents = opts.pendingEvents !== false;
-
- // just a list - *not* ordered.
- this.timelines = [this.liveTimeline];
- this._eventIdToTimeline = new Map<string, EventTimeline>();
-
- this.filter = opts.filter;
-
- this.relations = this.room?.relations ?? new RelationsContainer(room?.client ?? client!);
- }
-
- /**
- * Get all the timelines in this set
- * @returns the timelines in this set
- */
- public getTimelines(): EventTimeline[] {
- return this.timelines;
- }
-
- /**
- * Get the filter object this timeline set is filtered on, if any
- * @returns the optional filter for this timelineSet
- */
- public getFilter(): Filter | undefined {
- return this.filter;
- }
-
- /**
- * Set the filter object this timeline set is filtered on
- * (passed to the server when paginating via /messages).
- * @param filter - the filter for this timelineSet
- */
- public setFilter(filter?: Filter): void {
- this.filter = filter;
- }
-
- /**
- * Get the list of pending sent events for this timelineSet's room, filtered
- * by the timelineSet's filter if appropriate.
- *
- * @returns A list of the sent events
- * waiting for remote echo.
- *
- * @throws If `opts.pendingEventOrdering` was not 'detached'
- */
- public getPendingEvents(): MatrixEvent[] {
- if (!this.room || !this.displayPendingEvents) {
- return [];
- }
-
- return this.room.getPendingEvents();
- }
- /**
- * Get the live timeline for this room.
- *
- * @returns live timeline
- */
- public getLiveTimeline(): EventTimeline {
- return this.liveTimeline;
- }
-
- /**
- * Set the live timeline for this room.
- *
- * @returns live timeline
- */
- public setLiveTimeline(timeline: EventTimeline): void {
- this.liveTimeline = timeline;
- }
-
- /**
- * Return the timeline (if any) this event is in.
- * @param eventId - the eventId being sought
- * @returns timeline
- */
- public eventIdToTimeline(eventId: string): EventTimeline | undefined {
- return this._eventIdToTimeline.get(eventId);
- }
-
- /**
- * Track a new event as if it were in the same timeline as an old event,
- * replacing it.
- * @param oldEventId - event ID of the original event
- * @param newEventId - event ID of the replacement event
- */
- public replaceEventId(oldEventId: string, newEventId: string): void {
- const existingTimeline = this._eventIdToTimeline.get(oldEventId);
- if (existingTimeline) {
- this._eventIdToTimeline.delete(oldEventId);
- this._eventIdToTimeline.set(newEventId, existingTimeline);
- }
- }
-
- /**
- * Reset the live timeline, and start a new one.
- *
- * <p>This is used when /sync returns a 'limited' timeline.
- *
- * @param backPaginationToken - token for back-paginating the new timeline
- * @param forwardPaginationToken - token for forward-paginating the old live timeline,
- * if absent or null, all timelines are reset.
- *
- * @remarks
- * Fires {@link RoomEvent.TimelineReset}
- */
- public resetLiveTimeline(backPaginationToken?: string, forwardPaginationToken?: string): void {
- // Each EventTimeline has RoomState objects tracking the state at the start
- // and end of that timeline. The copies at the end of the live timeline are
- // special because they will have listeners attached to monitor changes to
- // the current room state, so we move this RoomState from the end of the
- // current live timeline to the end of the new one and, if necessary,
- // replace it with a newly created one. We also make a copy for the start
- // of the new timeline.
-
- // if timeline support is disabled, forget about the old timelines
- const resetAllTimelines = !this.timelineSupport || !forwardPaginationToken;
-
- const oldTimeline = this.liveTimeline;
- const newTimeline = resetAllTimelines
- ? oldTimeline.forkLive(EventTimeline.FORWARDS)
- : oldTimeline.fork(EventTimeline.FORWARDS);
-
- if (resetAllTimelines) {
- this.timelines = [newTimeline];
- this._eventIdToTimeline = new Map<string, EventTimeline>();
- } else {
- this.timelines.push(newTimeline);
- }
-
- if (forwardPaginationToken) {
- // Now set the forward pagination token on the old live timeline
- // so it can be forward-paginated.
- oldTimeline.setPaginationToken(forwardPaginationToken, EventTimeline.FORWARDS);
- }
-
- // make sure we set the pagination token before firing timelineReset,
- // otherwise clients which start back-paginating will fail, and then get
- // stuck without realising that they *can* back-paginate.
- newTimeline.setPaginationToken(backPaginationToken ?? null, EventTimeline.BACKWARDS);
-
- // Now we can swap the live timeline to the new one.
- this.liveTimeline = newTimeline;
- this.emit(RoomEvent.TimelineReset, this.room, this, resetAllTimelines);
- }
-
- /**
- * Get the timeline which contains the given event, if any
- *
- * @param eventId - event ID to look for
- * @returns timeline containing
- * the given event, or null if unknown
- */
- public getTimelineForEvent(eventId?: string): EventTimeline | null {
- if (eventId === null || eventId === undefined) {
- return null;
- }
- const res = this._eventIdToTimeline.get(eventId);
- return res === undefined ? null : res;
- }
-
- /**
- * Get an event which is stored in our timelines
- *
- * @param eventId - event ID to look for
- * @returns the given event, or undefined if unknown
- */
- public findEventById(eventId: string): MatrixEvent | undefined {
- const tl = this.getTimelineForEvent(eventId);
- if (!tl) {
- return undefined;
- }
- return tl.getEvents().find(function (ev) {
- return ev.getId() == eventId;
- });
- }
-
- /**
- * Add a new timeline to this timeline list
- *
- * @returns newly-created timeline
- */
- public addTimeline(): EventTimeline {
- if (!this.timelineSupport) {
- throw new Error(
- "timeline support is disabled. Set the 'timelineSupport'" +
- " parameter to true when creating MatrixClient to enable" +
- " it.",
- );
- }
-
- const timeline = new EventTimeline(this);
- this.timelines.push(timeline);
- return timeline;
- }
-
- /**
- * Add events to a timeline
- *
- * <p>Will fire "Room.timeline" for each event added.
- *
- * @param events - A list of events to add.
- *
- * @param toStartOfTimeline - True to add these events to the start
- * (oldest) instead of the end (newest) of the timeline. If true, the oldest
- * event will be the <b>last</b> element of 'events'.
- *
- * @param timeline - timeline to
- * add events to.
- *
- * @param paginationToken - token for the next batch of events
- *
- * @remarks
- * Fires {@link RoomEvent.Timeline}
- *
- */
- public addEventsToTimeline(
- events: MatrixEvent[],
- toStartOfTimeline: boolean,
- timeline: EventTimeline,
- paginationToken?: string | null,
- ): void {
- if (!timeline) {
- throw new Error("'timeline' not specified for EventTimelineSet.addEventsToTimeline");
- }
-
- if (!toStartOfTimeline && timeline == this.liveTimeline) {
- throw new Error(
- "EventTimelineSet.addEventsToTimeline cannot be used for adding events to " +
- "the live timeline - use Room.addLiveEvents instead",
- );
- }
-
- if (this.filter) {
- events = this.filter.filterRoomTimeline(events);
- if (!events.length) {
- return;
- }
- }
-
- const direction = toStartOfTimeline ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
- const inverseDirection = toStartOfTimeline ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS;
-
- // Adding events to timelines can be quite complicated. The following
- // illustrates some of the corner-cases.
- //
- // Let's say we start by knowing about four timelines. timeline3 and
- // timeline4 are neighbours:
- //
- // timeline1 timeline2 timeline3 timeline4
- // [M] [P] [S] <------> [T]
- //
- // Now we paginate timeline1, and get the following events from the server:
- // [M, N, P, R, S, T, U].
- //
- // 1. First, we ignore event M, since we already know about it.
- //
- // 2. Next, we append N to timeline 1.
- //
- // 3. Next, we don't add event P, since we already know about it,
- // but we do link together the timelines. We now have:
- //
- // timeline1 timeline2 timeline3 timeline4
- // [M, N] <---> [P] [S] <------> [T]
- //
- // 4. Now we add event R to timeline2:
- //
- // timeline1 timeline2 timeline3 timeline4
- // [M, N] <---> [P, R] [S] <------> [T]
- //
- // Note that we have switched the timeline we are working on from
- // timeline1 to timeline2.
- //
- // 5. We ignore event S, but again join the timelines:
- //
- // timeline1 timeline2 timeline3 timeline4
- // [M, N] <---> [P, R] <---> [S] <------> [T]
- //
- // 6. We ignore event T, and the timelines are already joined, so there
- // is nothing to do.
- //
- // 7. Finally, we add event U to timeline4:
- //
- // timeline1 timeline2 timeline3 timeline4
- // [M, N] <---> [P, R] <---> [S] <------> [T, U]
- //
- // The important thing to note in the above is what happened when we
- // already knew about a given event:
- //
- // - if it was appropriate, we joined up the timelines (steps 3, 5).
- // - in any case, we started adding further events to the timeline which
- // contained the event we knew about (steps 3, 5, 6).
- //
- //
- // So much for adding events to the timeline. But what do we want to do
- // with the pagination token?
- //
- // In the case above, we will be given a pagination token which tells us how to
- // get events beyond 'U' - in this case, it makes sense to store this
- // against timeline4. But what if timeline4 already had 'U' and beyond? in
- // that case, our best bet is to throw away the pagination token we were
- // given and stick with whatever token timeline4 had previously. In short,
- // we want to only store the pagination token if the last event we receive
- // is one we didn't previously know about.
- //
- // We make an exception for this if it turns out that we already knew about
- // *all* of the events, and we weren't able to join up any timelines. When
- // that happens, it means our existing pagination token is faulty, since it
- // is only telling us what we already know. Rather than repeatedly
- // paginating with the same token, we might as well use the new pagination
- // token in the hope that we eventually work our way out of the mess.
-
- let didUpdate = false;
- let lastEventWasNew = false;
- for (const event of events) {
- const eventId = event.getId()!;
-
- const existingTimeline = this._eventIdToTimeline.get(eventId);
-
- if (!existingTimeline) {
- // we don't know about this event yet. Just add it to the timeline.
- this.addEventToTimeline(event, timeline, {
- toStartOfTimeline,
- });
- lastEventWasNew = true;
- didUpdate = true;
- continue;
- }
-
- lastEventWasNew = false;
-
- if (existingTimeline == timeline) {
- debuglog("Event " + eventId + " already in timeline " + timeline);
- continue;
- }
-
- const neighbour = timeline.getNeighbouringTimeline(direction);
- if (neighbour) {
- // this timeline already has a neighbour in the relevant direction;
- // let's assume the timelines are already correctly linked up, and
- // skip over to it.
- //
- // there's probably some edge-case here where we end up with an
- // event which is in a timeline a way down the chain, and there is
- // a break in the chain somewhere. But I can't really imagine how
- // that would happen, so I'm going to ignore it for now.
- //
- if (existingTimeline == neighbour) {
- debuglog("Event " + eventId + " in neighbouring timeline - " + "switching to " + existingTimeline);
- } else {
- debuglog("Event " + eventId + " already in a different " + "timeline " + existingTimeline);
- }
- timeline = existingTimeline;
- continue;
- }
-
- // time to join the timelines.
- logger.info(
- "Already have timeline for " + eventId + " - joining timeline " + timeline + " to " + existingTimeline,
- );
-
- // Variables to keep the line length limited below.
- const existingIsLive = existingTimeline === this.liveTimeline;
- const timelineIsLive = timeline === this.liveTimeline;
-
- const backwardsIsLive = direction === EventTimeline.BACKWARDS && existingIsLive;
- const forwardsIsLive = direction === EventTimeline.FORWARDS && timelineIsLive;
-
- if (backwardsIsLive || forwardsIsLive) {
- // The live timeline should never be spliced into a non-live position.
- // We use independent logging to better discover the problem at a glance.
- if (backwardsIsLive) {
- logger.warn(
- "Refusing to set a preceding existingTimeLine on our " +
- "timeline as the existingTimeLine is live (" +
- existingTimeline +
- ")",
- );
- }
- if (forwardsIsLive) {
- logger.warn(
- "Refusing to set our preceding timeline on a existingTimeLine " +
- "as our timeline is live (" +
- timeline +
- ")",
- );
- }
- continue; // abort splicing - try next event
- }
-
- timeline.setNeighbouringTimeline(existingTimeline, direction);
- existingTimeline.setNeighbouringTimeline(timeline, inverseDirection);
-
- timeline = existingTimeline;
- didUpdate = true;
- }
-
- // see above - if the last event was new to us, or if we didn't find any
- // new information, we update the pagination token for whatever
- // timeline we ended up on.
- if (lastEventWasNew || !didUpdate) {
- if (direction === EventTimeline.FORWARDS && timeline === this.liveTimeline) {
- logger.warn({ lastEventWasNew, didUpdate }); // for debugging
- logger.warn(
- `Refusing to set forwards pagination token of live timeline ` + `${timeline} to ${paginationToken}`,
- );
- return;
- }
- timeline.setPaginationToken(paginationToken ?? null, direction);
- }
- }
-
- /**
- * Add an event to the end of this live timeline.
- *
- * @param event - Event to be added
- * @param options - addLiveEvent options
- */
- public addLiveEvent(
- event: MatrixEvent,
- { duplicateStrategy, fromCache, roomState, timelineWasEmpty }: IAddLiveEventOptions,
- ): void;
- /**
- * @deprecated In favor of the overload with `IAddLiveEventOptions`
- */
- public addLiveEvent(
- event: MatrixEvent,
- duplicateStrategy?: DuplicateStrategy,
- fromCache?: boolean,
- roomState?: RoomState,
- ): void;
- public addLiveEvent(
- event: MatrixEvent,
- duplicateStrategyOrOpts?: DuplicateStrategy | IAddLiveEventOptions,
- fromCache = false,
- roomState?: RoomState,
- ): void {
- let duplicateStrategy = (duplicateStrategyOrOpts as DuplicateStrategy) || DuplicateStrategy.Ignore;
- let timelineWasEmpty: boolean | undefined;
- if (typeof duplicateStrategyOrOpts === "object") {
- ({
- duplicateStrategy = DuplicateStrategy.Ignore,
- fromCache = false,
- roomState,
- timelineWasEmpty,
- } = duplicateStrategyOrOpts);
- } else if (duplicateStrategyOrOpts !== undefined) {
- // Deprecation warning
- // FIXME: Remove after 2023-06-01 (technical debt)
- logger.warn(
- "Overload deprecated: " +
- "`EventTimelineSet.addLiveEvent(event, duplicateStrategy?, fromCache?, roomState?)` " +
- "is deprecated in favor of the overload with " +
- "`EventTimelineSet.addLiveEvent(event, IAddLiveEventOptions)`",
- );
- }
-
- if (this.filter) {
- const events = this.filter.filterRoomTimeline([event]);
- if (!events.length) {
- return;
- }
- }
-
- const timeline = this._eventIdToTimeline.get(event.getId()!);
- if (timeline) {
- if (duplicateStrategy === DuplicateStrategy.Replace) {
- debuglog("EventTimelineSet.addLiveEvent: replacing duplicate event " + event.getId());
- const tlEvents = timeline.getEvents();
- for (let j = 0; j < tlEvents.length; j++) {
- if (tlEvents[j].getId() === event.getId()) {
- // still need to set the right metadata on this event
- if (!roomState) {
- roomState = timeline.getState(EventTimeline.FORWARDS);
- }
- EventTimeline.setEventMetadata(event, roomState!, false);
- tlEvents[j] = event;
-
- // XXX: we need to fire an event when this happens.
- break;
- }
- }
- } else {
- debuglog("EventTimelineSet.addLiveEvent: ignoring duplicate event " + event.getId());
- }
- return;
- }
-
- this.addEventToTimeline(event, this.liveTimeline, {
- toStartOfTimeline: false,
- fromCache,
- roomState,
- timelineWasEmpty,
- });
- }
-
- /**
- * Add event to the given timeline, and emit Room.timeline. Assumes
- * we have already checked we don't know about this event.
- *
- * Will fire "Room.timeline" for each event added.
- *
- * @param options - addEventToTimeline options
- *
- * @remarks
- * Fires {@link RoomEvent.Timeline}
- */
- public addEventToTimeline(
- event: MatrixEvent,
- timeline: EventTimeline,
- { toStartOfTimeline, fromCache, roomState, timelineWasEmpty }: IAddEventToTimelineOptions,
- ): void;
- /**
- * @deprecated In favor of the overload with `IAddEventToTimelineOptions`
- */
- public addEventToTimeline(
- event: MatrixEvent,
- timeline: EventTimeline,
- toStartOfTimeline: boolean,
- fromCache?: boolean,
- roomState?: RoomState,
- ): void;
- public addEventToTimeline(
- event: MatrixEvent,
- timeline: EventTimeline,
- toStartOfTimelineOrOpts: boolean | IAddEventToTimelineOptions,
- fromCache = false,
- roomState?: RoomState,
- ): void {
- let toStartOfTimeline = !!toStartOfTimelineOrOpts;
- let timelineWasEmpty: boolean | undefined;
- if (typeof toStartOfTimelineOrOpts === "object") {
- ({ toStartOfTimeline, fromCache = false, roomState, timelineWasEmpty } = toStartOfTimelineOrOpts);
- } else if (toStartOfTimelineOrOpts !== undefined) {
- // Deprecation warning
- // FIXME: Remove after 2023-06-01 (technical debt)
- logger.warn(
- "Overload deprecated: " +
- "`EventTimelineSet.addEventToTimeline(event, timeline, toStartOfTimeline, fromCache?, roomState?)` " +
- "is deprecated in favor of the overload with " +
- "`EventTimelineSet.addEventToTimeline(event, timeline, IAddEventToTimelineOptions)`",
- );
- }
-
- if (timeline.getTimelineSet() !== this) {
- throw new Error(`EventTimelineSet.addEventToTimeline: Timeline=${timeline.toString()} does not belong " +
- "in timelineSet(threadId=${this.thread?.id})`);
- }
-
- // Make sure events don't get mixed in timelines they shouldn't be in (e.g. a
- // threaded message should not be in the main timeline).
- //
- // We can only run this check for timelines with a `room` because `canContain`
- // requires it
- if (this.room && !this.canContain(event)) {
- let eventDebugString = `event=${event.getId()}`;
- if (event.threadRootId) {
- eventDebugString += `(belongs to thread=${event.threadRootId})`;
- }
- logger.warn(
- `EventTimelineSet.addEventToTimeline: Ignoring ${eventDebugString} that does not belong ` +
- `in timeline=${timeline.toString()} timelineSet(threadId=${this.thread?.id})`,
- );
- return;
- }
-
- const eventId = event.getId()!;
- timeline.addEvent(event, {
- toStartOfTimeline,
- roomState,
- timelineWasEmpty,
- });
- this._eventIdToTimeline.set(eventId, timeline);
-
- this.relations.aggregateParentEvent(event);
- this.relations.aggregateChildEvent(event, this);
-
- const data: IRoomTimelineData = {
- timeline: timeline,
- liveEvent: !toStartOfTimeline && timeline == this.liveTimeline && !fromCache,
- };
- this.emit(RoomEvent.Timeline, event, this.room, Boolean(toStartOfTimeline), false, data);
- }
-
- /**
- * Replaces event with ID oldEventId with one with newEventId, if oldEventId is
- * recognised. Otherwise, add to the live timeline. Used to handle remote echos.
- *
- * @param localEvent - the new event to be added to the timeline
- * @param oldEventId - the ID of the original event
- * @param newEventId - the ID of the replacement event
- *
- * @remarks
- * Fires {@link RoomEvent.Timeline}
- */
- public handleRemoteEcho(localEvent: MatrixEvent, oldEventId: string, newEventId: string): void {
- // XXX: why don't we infer newEventId from localEvent?
- const existingTimeline = this._eventIdToTimeline.get(oldEventId);
- if (existingTimeline) {
- this._eventIdToTimeline.delete(oldEventId);
- this._eventIdToTimeline.set(newEventId, existingTimeline);
- } else if (!this.filter || this.filter.filterRoomTimeline([localEvent]).length) {
- this.addEventToTimeline(localEvent, this.liveTimeline, {
- toStartOfTimeline: false,
- });
- }
- }
-
- /**
- * Removes a single event from this room.
- *
- * @param eventId - The id of the event to remove
- *
- * @returns the removed event, or null if the event was not found
- * in this room.
- */
- public removeEvent(eventId: string): MatrixEvent | null {
- const timeline = this._eventIdToTimeline.get(eventId);
- if (!timeline) {
- return null;
- }
-
- const removed = timeline.removeEvent(eventId);
- if (removed) {
- this._eventIdToTimeline.delete(eventId);
- const data = {
- timeline: timeline,
- };
- this.emit(RoomEvent.Timeline, removed, this.room, undefined, true, data);
- }
- return removed;
- }
-
- /**
- * Determine where two events appear in the timeline relative to one another
- *
- * @param eventId1 - The id of the first event
- * @param eventId2 - The id of the second event
-
- * @returns a number less than zero if eventId1 precedes eventId2, and
- * greater than zero if eventId1 succeeds eventId2. zero if they are the
- * same event; null if we can't tell (either because we don't know about one
- * of the events, or because they are in separate timelines which don't join
- * up).
- */
- public compareEventOrdering(eventId1: string, eventId2: string): number | null {
- if (eventId1 == eventId2) {
- // optimise this case
- return 0;
- }
-
- const timeline1 = this._eventIdToTimeline.get(eventId1);
- const timeline2 = this._eventIdToTimeline.get(eventId2);
-
- if (timeline1 === undefined) {
- return null;
- }
- if (timeline2 === undefined) {
- return null;
- }
-
- if (timeline1 === timeline2) {
- // both events are in the same timeline - figure out their relative indices
- let idx1: number | undefined = undefined;
- let idx2: number | undefined = undefined;
- const events = timeline1.getEvents();
- for (let idx = 0; idx < events.length && (idx1 === undefined || idx2 === undefined); idx++) {
- const evId = events[idx].getId();
- if (evId == eventId1) {
- idx1 = idx;
- }
- if (evId == eventId2) {
- idx2 = idx;
- }
- }
- return idx1! - idx2!;
- }
-
- // the events are in different timelines. Iterate through the
- // linkedlist to see which comes first.
-
- // first work forwards from timeline1
- let tl: EventTimeline | null = timeline1;
- while (tl) {
- if (tl === timeline2) {
- // timeline1 is before timeline2
- return -1;
- }
- tl = tl.getNeighbouringTimeline(EventTimeline.FORWARDS);
- }
-
- // now try backwards from timeline1
- tl = timeline1;
- while (tl) {
- if (tl === timeline2) {
- // timeline2 is before timeline1
- return 1;
- }
- tl = tl.getNeighbouringTimeline(EventTimeline.BACKWARDS);
- }
-
- // the timelines are not contiguous.
- return null;
- }
-
- /**
- * Determine whether a given event can sanely be added to this event timeline set,
- * for timeline sets relating to a thread, only return true for events in the same
- * thread timeline, for timeline sets not relating to a thread only return true
- * for events which should be shown in the main room timeline.
- * Requires the `room` property to have been set at EventTimelineSet construction time.
- *
- * @param event - the event to check whether it belongs to this timeline set.
- * @throws Error if `room` was not set when constructing this timeline set.
- * @returns whether the event belongs to this timeline set.
- */
- public canContain(event: MatrixEvent): boolean {
- if (!this.room) {
- throw new Error(
- "Cannot call `EventTimelineSet::canContain without a `room` set. " +
- "Set the room when creating the EventTimelineSet to call this method.",
- );
- }
-
- const { threadId, shouldLiveInRoom } = this.room.eventShouldLiveIn(event);
-
- if (this.thread) {
- return this.thread.id === threadId;
- }
- return shouldLiveInRoom;
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline.ts
deleted file mode 100644
index d1ba321..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline.ts
+++ /dev/null
@@ -1,458 +0,0 @@
-/*
-Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { logger } from "../logger";
-import { IMarkerFoundOptions, RoomState } from "./room-state";
-import { EventTimelineSet } from "./event-timeline-set";
-import { MatrixEvent } from "./event";
-import { Filter } from "../filter";
-import { EventType } from "../@types/event";
-
-export interface IInitialiseStateOptions extends Pick<IMarkerFoundOptions, "timelineWasEmpty"> {
- // This is a separate interface without any extra stuff currently added on
- // top of `IMarkerFoundOptions` just because it feels like they have
- // different concerns. One shouldn't necessarily look to add to
- // `IMarkerFoundOptions` just because they want to add an extra option to
- // `initialiseState`.
-}
-
-export interface IAddEventOptions extends Pick<IMarkerFoundOptions, "timelineWasEmpty"> {
- /** Whether to insert the new event at the start of the timeline where the
- * oldest events are (timeline is in chronological order, oldest to most
- * recent) */
- toStartOfTimeline: boolean;
- /** The state events to reconcile metadata from */
- roomState?: RoomState;
-}
-
-export enum Direction {
- Backward = "b",
- Forward = "f",
-}
-
-export class EventTimeline {
- /**
- * Symbolic constant for methods which take a 'direction' argument:
- * refers to the start of the timeline, or backwards in time.
- */
- public static readonly BACKWARDS = Direction.Backward;
-
- /**
- * Symbolic constant for methods which take a 'direction' argument:
- * refers to the end of the timeline, or forwards in time.
- */
- public static readonly FORWARDS = Direction.Forward;
-
- /**
- * Static helper method to set sender and target properties
- *
- * @param event - the event whose metadata is to be set
- * @param stateContext - the room state to be queried
- * @param toStartOfTimeline - if true the event's forwardLooking flag is set false
- */
- public static setEventMetadata(event: MatrixEvent, stateContext: RoomState, toStartOfTimeline: boolean): void {
- // When we try to generate a sentinel member before we have that member
- // in the members object, we still generate a sentinel but it doesn't
- // have a membership event, so test to see if events.member is set. We
- // check this to avoid overriding non-sentinel members by sentinel ones
- // when adding the event to a filtered timeline
- if (!event.sender?.events?.member) {
- event.sender = stateContext.getSentinelMember(event.getSender()!);
- }
- if (!event.target?.events?.member && event.getType() === EventType.RoomMember) {
- event.target = stateContext.getSentinelMember(event.getStateKey()!);
- }
-
- if (event.isState()) {
- // room state has no concept of 'old' or 'current', but we want the
- // room state to regress back to previous values if toStartOfTimeline
- // is set, which means inspecting prev_content if it exists. This
- // is done by toggling the forwardLooking flag.
- if (toStartOfTimeline) {
- event.forwardLooking = false;
- }
- }
- }
-
- private readonly roomId: string | null;
- private readonly name: string;
- private events: MatrixEvent[] = [];
- private baseIndex = 0;
-
- private startState?: RoomState;
- private endState?: RoomState;
- // If we have a roomId then we delegate pagination token storage to the room state objects `startState` and
- // `endState`, but for things like the notification timeline which mix multiple rooms we store the tokens ourselves.
- private startToken: string | null = null;
- private endToken: string | null = null;
-
- private prevTimeline: EventTimeline | null = null;
- private nextTimeline: EventTimeline | null = null;
- public paginationRequests: Record<Direction, Promise<boolean> | null> = {
- [Direction.Backward]: null,
- [Direction.Forward]: null,
- };
-
- /**
- * Construct a new EventTimeline
- *
- * <p>An EventTimeline represents a contiguous sequence of events in a room.
- *
- * <p>As well as keeping track of the events themselves, it stores the state of
- * the room at the beginning and end of the timeline, and pagination tokens for
- * going backwards and forwards in the timeline.
- *
- * <p>In order that clients can meaningfully maintain an index into a timeline,
- * the EventTimeline object tracks a 'baseIndex'. This starts at zero, but is
- * incremented when events are prepended to the timeline. The index of an event
- * relative to baseIndex therefore remains constant.
- *
- * <p>Once a timeline joins up with its neighbour, they are linked together into a
- * doubly-linked list.
- *
- * @param eventTimelineSet - the set of timelines this is part of
- */
- public constructor(private readonly eventTimelineSet: EventTimelineSet) {
- this.roomId = eventTimelineSet.room?.roomId ?? null;
- if (this.roomId) {
- this.startState = new RoomState(this.roomId);
- this.endState = new RoomState(this.roomId);
- }
-
- // this is used by client.js
- this.paginationRequests = { b: null, f: null };
-
- this.name = this.roomId + ":" + new Date().toISOString();
- }
-
- /**
- * Initialise the start and end state with the given events
- *
- * <p>This can only be called before any events are added.
- *
- * @param stateEvents - list of state events to initialise the
- * state with.
- * @throws Error if an attempt is made to call this after addEvent is called.
- */
- public initialiseState(stateEvents: MatrixEvent[], { timelineWasEmpty }: IInitialiseStateOptions = {}): void {
- if (this.events.length > 0) {
- throw new Error("Cannot initialise state after events are added");
- }
-
- this.startState?.setStateEvents(stateEvents, { timelineWasEmpty });
- this.endState?.setStateEvents(stateEvents, { timelineWasEmpty });
- }
-
- /**
- * Forks the (live) timeline, taking ownership of the existing directional state of this timeline.
- * All attached listeners will keep receiving state updates from the new live timeline state.
- * The end state of this timeline gets replaced with an independent copy of the current RoomState,
- * and will need a new pagination token if it ever needs to paginate forwards.
-
- * @param direction - EventTimeline.BACKWARDS to get the state at the
- * start of the timeline; EventTimeline.FORWARDS to get the state at the end
- * of the timeline.
- *
- * @returns the new timeline
- */
- public forkLive(direction: Direction): EventTimeline {
- const forkState = this.getState(direction);
- const timeline = new EventTimeline(this.eventTimelineSet);
- timeline.startState = forkState?.clone();
- // Now clobber the end state of the new live timeline with that from the
- // previous live timeline. It will be identical except that we'll keep
- // using the same RoomMember objects for the 'live' set of members with any
- // listeners still attached
- timeline.endState = forkState;
- // Firstly, we just stole the current timeline's end state, so it needs a new one.
- // Make an immutable copy of the state so back pagination will get the correct sentinels.
- this.endState = forkState?.clone();
- return timeline;
- }
-
- /**
- * Creates an independent timeline, inheriting the directional state from this timeline.
- *
- * @param direction - EventTimeline.BACKWARDS to get the state at the
- * start of the timeline; EventTimeline.FORWARDS to get the state at the end
- * of the timeline.
- *
- * @returns the new timeline
- */
- public fork(direction: Direction): EventTimeline {
- const forkState = this.getState(direction);
- const timeline = new EventTimeline(this.eventTimelineSet);
- timeline.startState = forkState?.clone();
- timeline.endState = forkState?.clone();
- return timeline;
- }
-
- /**
- * Get the ID of the room for this timeline
- * @returns room ID
- */
- public getRoomId(): string | null {
- return this.roomId;
- }
-
- /**
- * Get the filter for this timeline's timelineSet (if any)
- * @returns filter
- */
- public getFilter(): Filter | undefined {
- return this.eventTimelineSet.getFilter();
- }
-
- /**
- * Get the timelineSet for this timeline
- * @returns timelineSet
- */
- public getTimelineSet(): EventTimelineSet {
- return this.eventTimelineSet;
- }
-
- /**
- * Get the base index.
- *
- * <p>This is an index which is incremented when events are prepended to the
- * timeline. An individual event therefore stays at the same index in the array
- * relative to the base index (although note that a given event's index may
- * well be less than the base index, thus giving that event a negative relative
- * index).
- */
- public getBaseIndex(): number {
- return this.baseIndex;
- }
-
- /**
- * Get the list of events in this context
- *
- * @returns An array of MatrixEvents
- */
- public getEvents(): MatrixEvent[] {
- return this.events;
- }
-
- /**
- * Get the room state at the start/end of the timeline
- *
- * @param direction - EventTimeline.BACKWARDS to get the state at the
- * start of the timeline; EventTimeline.FORWARDS to get the state at the end
- * of the timeline.
- *
- * @returns state at the start/end of the timeline
- */
- public getState(direction: Direction): RoomState | undefined {
- if (direction == EventTimeline.BACKWARDS) {
- return this.startState;
- } else if (direction == EventTimeline.FORWARDS) {
- return this.endState;
- } else {
- throw new Error("Invalid direction '" + direction + "'");
- }
- }
-
- /**
- * Get a pagination token
- *
- * @param direction - EventTimeline.BACKWARDS to get the pagination
- * token for going backwards in time; EventTimeline.FORWARDS to get the
- * pagination token for going forwards in time.
- *
- * @returns pagination token
- */
- public getPaginationToken(direction: Direction): string | null {
- if (this.roomId) {
- return this.getState(direction)!.paginationToken;
- } else if (direction === Direction.Backward) {
- return this.startToken;
- } else {
- return this.endToken;
- }
- }
-
- /**
- * Set a pagination token
- *
- * @param token - pagination token
- *
- * @param direction - EventTimeline.BACKWARDS to set the pagination
- * token for going backwards in time; EventTimeline.FORWARDS to set the
- * pagination token for going forwards in time.
- */
- public setPaginationToken(token: string | null, direction: Direction): void {
- if (this.roomId) {
- this.getState(direction)!.paginationToken = token;
- } else if (direction === Direction.Backward) {
- this.startToken = token;
- } else {
- this.endToken = token;
- }
- }
-
- /**
- * Get the next timeline in the series
- *
- * @param direction - EventTimeline.BACKWARDS to get the previous
- * timeline; EventTimeline.FORWARDS to get the next timeline.
- *
- * @returns previous or following timeline, if they have been
- * joined up.
- */
- public getNeighbouringTimeline(direction: Direction): EventTimeline | null {
- if (direction == EventTimeline.BACKWARDS) {
- return this.prevTimeline;
- } else if (direction == EventTimeline.FORWARDS) {
- return this.nextTimeline;
- } else {
- throw new Error("Invalid direction '" + direction + "'");
- }
- }
-
- /**
- * Set the next timeline in the series
- *
- * @param neighbour - previous/following timeline
- *
- * @param direction - EventTimeline.BACKWARDS to set the previous
- * timeline; EventTimeline.FORWARDS to set the next timeline.
- *
- * @throws Error if an attempt is made to set the neighbouring timeline when
- * it is already set.
- */
- public setNeighbouringTimeline(neighbour: EventTimeline, direction: Direction): void {
- if (this.getNeighbouringTimeline(direction)) {
- throw new Error(
- "timeline already has a neighbouring timeline - " +
- "cannot reset neighbour (direction: " +
- direction +
- ")",
- );
- }
-
- if (direction == EventTimeline.BACKWARDS) {
- this.prevTimeline = neighbour;
- } else if (direction == EventTimeline.FORWARDS) {
- this.nextTimeline = neighbour;
- } else {
- throw new Error("Invalid direction '" + direction + "'");
- }
-
- // make sure we don't try to paginate this timeline
- this.setPaginationToken(null, direction);
- }
-
- /**
- * Add a new event to the timeline, and update the state
- *
- * @param event - new event
- * @param options - addEvent options
- */
- public addEvent(event: MatrixEvent, { toStartOfTimeline, roomState, timelineWasEmpty }: IAddEventOptions): void;
- /**
- * @deprecated In favor of the overload with `IAddEventOptions`
- */
- public addEvent(event: MatrixEvent, toStartOfTimeline: boolean, roomState?: RoomState): void;
- public addEvent(
- event: MatrixEvent,
- toStartOfTimelineOrOpts: boolean | IAddEventOptions,
- roomState?: RoomState,
- ): void {
- let toStartOfTimeline = !!toStartOfTimelineOrOpts;
- let timelineWasEmpty: boolean | undefined;
- if (typeof toStartOfTimelineOrOpts === "object") {
- ({ toStartOfTimeline, roomState, timelineWasEmpty } = toStartOfTimelineOrOpts);
- } else if (toStartOfTimelineOrOpts !== undefined) {
- // Deprecation warning
- // FIXME: Remove after 2023-06-01 (technical debt)
- logger.warn(
- "Overload deprecated: " +
- "`EventTimeline.addEvent(event, toStartOfTimeline, roomState?)` " +
- "is deprecated in favor of the overload with `EventTimeline.addEvent(event, IAddEventOptions)`",
- );
- }
-
- if (!roomState) {
- roomState = toStartOfTimeline ? this.startState : this.endState;
- }
-
- const timelineSet = this.getTimelineSet();
-
- if (timelineSet.room) {
- EventTimeline.setEventMetadata(event, roomState!, toStartOfTimeline);
-
- // modify state but only on unfiltered timelineSets
- if (event.isState() && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
- roomState?.setStateEvents([event], { timelineWasEmpty });
- // it is possible that the act of setting the state event means we
- // can set more metadata (specifically sender/target props), so try
- // it again if the prop wasn't previously set. It may also mean that
- // the sender/target is updated (if the event set was a room member event)
- // so we want to use the *updated* member (new avatar/name) instead.
- //
- // However, we do NOT want to do this on member events if we're going
- // back in time, else we'll set the .sender value for BEFORE the given
- // member event, whereas we want to set the .sender value for the ACTUAL
- // member event itself.
- if (!event.sender || (event.getType() === EventType.RoomMember && !toStartOfTimeline)) {
- EventTimeline.setEventMetadata(event, roomState!, toStartOfTimeline);
- }
- }
- }
-
- let insertIndex: number;
-
- if (toStartOfTimeline) {
- insertIndex = 0;
- } else {
- insertIndex = this.events.length;
- }
-
- this.events.splice(insertIndex, 0, event); // insert element
- if (toStartOfTimeline) {
- this.baseIndex++;
- }
- }
-
- /**
- * Remove an event from the timeline
- *
- * @param eventId - ID of event to be removed
- * @returns removed event, or null if not found
- */
- public removeEvent(eventId: string): MatrixEvent | null {
- for (let i = this.events.length - 1; i >= 0; i--) {
- const ev = this.events[i];
- if (ev.getId() == eventId) {
- this.events.splice(i, 1);
- if (i < this.baseIndex) {
- this.baseIndex--;
- }
- return ev;
- }
- }
- return null;
- }
-
- /**
- * Return a string to identify this timeline, for debugging
- *
- * @returns name for this timeline
- */
- public toString(): string {
- return this.name;
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event.ts
deleted file mode 100644
index 2db3479..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event.ts
+++ /dev/null
@@ -1,1631 +0,0 @@
-/*
-Copyright 2015 - 2023 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.
-*/
-
-/**
- * This is an internal module. See {@link MatrixEvent} and {@link RoomEvent} for
- * the public classes.
- */
-
-import { ExtensibleEvent, ExtensibleEvents, Optional } from "matrix-events-sdk";
-
-import type { IEventDecryptionResult } from "../@types/crypto";
-import { logger } from "../logger";
-import { VerificationRequest } from "../crypto/verification/request/VerificationRequest";
-import { EVENT_VISIBILITY_CHANGE_TYPE, EventType, MsgType, RelationType } from "../@types/event";
-import { Crypto } from "../crypto";
-import { deepSortedObjectEntries, internaliseString } from "../utils";
-import { RoomMember } from "./room-member";
-import { Thread, ThreadEvent, EventHandlerMap as ThreadEventHandlerMap, THREAD_RELATION_TYPE } from "./thread";
-import { IActionsObject } from "../pushprocessor";
-import { TypedReEmitter } from "../ReEmitter";
-import { MatrixError } from "../http-api";
-import { TypedEventEmitter } from "./typed-event-emitter";
-import { EventStatus } from "./event-status";
-import { DecryptionError } from "../crypto/algorithms";
-import { CryptoBackend } from "../common-crypto/CryptoBackend";
-import { WITHHELD_MESSAGES } from "../crypto/OlmDevice";
-
-export { EventStatus } from "./event-status";
-
-/* eslint-disable camelcase */
-export interface IContent {
- [key: string]: any;
- "msgtype"?: MsgType | string;
- "membership"?: string;
- "avatar_url"?: string;
- "displayname"?: string;
- "m.relates_to"?: IEventRelation;
-
- "org.matrix.msc3952.mentions"?: IMentions;
-}
-
-type StrippedState = Required<Pick<IEvent, "content" | "state_key" | "type" | "sender">>;
-
-export interface IUnsigned {
- "age"?: number;
- "prev_sender"?: string;
- "prev_content"?: IContent;
- "redacted_because"?: IEvent;
- "transaction_id"?: string;
- "invite_room_state"?: StrippedState[];
- "m.relations"?: Record<RelationType | string, any>; // No common pattern for aggregated relations
-}
-
-export interface IThreadBundledRelationship {
- latest_event: IEvent;
- count: number;
- current_user_participated?: boolean;
-}
-
-export interface IEvent {
- event_id: string;
- type: string;
- content: IContent;
- sender: string;
- room_id?: string;
- origin_server_ts: number;
- txn_id?: string;
- state_key?: string;
- membership?: string;
- unsigned: IUnsigned;
- redacts?: string;
-
- /**
- * @deprecated in favour of `sender`
- */
- user_id?: string;
- /**
- * @deprecated in favour of `unsigned.prev_content`
- */
- prev_content?: IContent;
- /**
- * @deprecated in favour of `origin_server_ts`
- */
- age?: number;
-}
-
-export interface IAggregatedRelation {
- origin_server_ts: number;
- event_id?: string;
- sender?: string;
- type?: string;
- count?: number;
- key?: string;
-}
-
-export interface IEventRelation {
- "rel_type"?: RelationType | string;
- "event_id"?: string;
- "is_falling_back"?: boolean;
- "m.in_reply_to"?: {
- event_id?: string;
- };
- "key"?: string;
-}
-
-export interface IMentions {
- user_ids?: string[];
- room?: boolean;
-}
-
-/**
- * When an event is a visibility change event, as per MSC3531,
- * the visibility change implied by the event.
- */
-export interface IVisibilityChange {
- /**
- * If `true`, the target event should be made visible.
- * Otherwise, it should be hidden.
- */
- visible: boolean;
-
- /**
- * The event id affected.
- */
- eventId: string;
-
- /**
- * Optionally, a human-readable reason explaining why
- * the event was hidden. Ignored if the event was made
- * visible.
- */
- reason: string | null;
-}
-
-export interface IClearEvent {
- room_id?: string;
- type: string;
- content: Omit<IContent, "membership" | "avatar_url" | "displayname" | "m.relates_to">;
- unsigned?: IUnsigned;
-}
-/* eslint-enable camelcase */
-
-interface IKeyRequestRecipient {
- userId: string;
- deviceId: "*" | string;
-}
-
-export interface IDecryptOptions {
- // Emits "event.decrypted" if set to true
- emit?: boolean;
- // True if this is a retry (enables more logging)
- isRetry?: boolean;
- // whether the message should be re-decrypted if it was previously successfully decrypted with an untrusted key
- forceRedecryptIfUntrusted?: boolean;
-}
-
-/**
- * Message hiding, as specified by https://github.com/matrix-org/matrix-doc/pull/3531.
- */
-export type MessageVisibility = IMessageVisibilityHidden | IMessageVisibilityVisible;
-/**
- * Variant of `MessageVisibility` for the case in which the message should be displayed.
- */
-export interface IMessageVisibilityVisible {
- readonly visible: true;
-}
-/**
- * Variant of `MessageVisibility` for the case in which the message should be hidden.
- */
-export interface IMessageVisibilityHidden {
- readonly visible: false;
- /**
- * Optionally, a human-readable reason to show to the user indicating why the
- * message has been hidden (e.g. "Message Pending Moderation").
- */
- readonly reason: string | null;
-}
-// A singleton implementing `IMessageVisibilityVisible`.
-const MESSAGE_VISIBLE: IMessageVisibilityVisible = Object.freeze({ visible: true });
-
-export enum MatrixEventEvent {
- Decrypted = "Event.decrypted",
- BeforeRedaction = "Event.beforeRedaction",
- VisibilityChange = "Event.visibilityChange",
- LocalEventIdReplaced = "Event.localEventIdReplaced",
- Status = "Event.status",
- Replaced = "Event.replaced",
- RelationsCreated = "Event.relationsCreated",
-}
-
-export type MatrixEventEmittedEvents = MatrixEventEvent | ThreadEvent.Update;
-
-export type MatrixEventHandlerMap = {
- /**
- * Fires when an event is decrypted
- *
- * @param event - The matrix event which has been decrypted
- * @param err - The error that occurred during decryption, or `undefined` if no error occurred.
- */
- [MatrixEventEvent.Decrypted]: (event: MatrixEvent, err?: Error) => void;
- [MatrixEventEvent.BeforeRedaction]: (event: MatrixEvent, redactionEvent: MatrixEvent) => void;
- [MatrixEventEvent.VisibilityChange]: (event: MatrixEvent, visible: boolean) => void;
- [MatrixEventEvent.LocalEventIdReplaced]: (event: MatrixEvent) => void;
- [MatrixEventEvent.Status]: (event: MatrixEvent, status: EventStatus | null) => void;
- [MatrixEventEvent.Replaced]: (event: MatrixEvent) => void;
- [MatrixEventEvent.RelationsCreated]: (relationType: string, eventType: string) => void;
-} & Pick<ThreadEventHandlerMap, ThreadEvent.Update>;
-
-export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, MatrixEventHandlerMap> {
- private pushActions: IActionsObject | null = null;
- private _replacingEvent: MatrixEvent | null = null;
- private _localRedactionEvent: MatrixEvent | null = null;
- private _isCancelled = false;
- private clearEvent?: IClearEvent;
-
- /* Message hiding, as specified by https://github.com/matrix-org/matrix-doc/pull/3531.
-
- Note: We're returning this object, so any value stored here MUST be frozen.
- */
- private visibility: MessageVisibility = MESSAGE_VISIBLE;
-
- // Not all events will be extensible-event compatible, so cache a flag in
- // addition to a falsy cached event value. We check the flag later on in
- // a public getter to decide if the cache is valid.
- private _hasCachedExtEv = false;
- private _cachedExtEv: Optional<ExtensibleEvent> = undefined;
-
- /* curve25519 key which we believe belongs to the sender of the event. See
- * getSenderKey()
- */
- private senderCurve25519Key: string | null = null;
-
- /* ed25519 key which the sender of this event (for olm) or the creator of
- * the megolm session (for megolm) claims to own. See getClaimedEd25519Key()
- */
- private claimedEd25519Key: string | null = null;
-
- /* curve25519 keys of devices involved in telling us about the
- * senderCurve25519Key and claimedEd25519Key.
- * See getForwardingCurve25519KeyChain().
- */
- private forwardingCurve25519KeyChain: string[] = [];
-
- /* where the decryption key is untrusted
- */
- private untrusted: boolean | null = null;
-
- /* if we have a process decrypting this event, a Promise which resolves
- * when it is finished. Normally null.
- */
- private decryptionPromise: Promise<void> | null = null;
-
- /* flag to indicate if we should retry decrypting this event after the
- * first attempt (eg, we have received new data which means that a second
- * attempt may succeed)
- */
- private retryDecryption = false;
-
- /* The txnId with which this event was sent if it was during this session,
- * allows for a unique ID which does not change when the event comes back down sync.
- */
- private txnId?: string;
-
- /**
- * A reference to the thread this event belongs to
- */
- private thread?: Thread;
- private threadId?: string;
-
- /*
- * True if this event is an encrypted event which we failed to decrypt, the receiver's device is unverified and
- * the sender has disabled encrypting to unverified devices.
- */
- private encryptedDisabledForUnverifiedDevices = false;
-
- /* Set an approximate timestamp for the event relative the local clock.
- * This will inherently be approximate because it doesn't take into account
- * the time between the server putting the 'age' field on the event as it sent
- * it to us and the time we're now constructing this event, but that's better
- * than assuming the local clock is in sync with the origin HS's clock.
- */
- public localTimestamp: number;
-
- /**
- * The room member who sent this event, or null e.g.
- * this is a presence event. This is only guaranteed to be set for events that
- * appear in a timeline, ie. do not guarantee that it will be set on state
- * events.
- * @privateRemarks
- * Should be read-only
- */
- public sender: RoomMember | null = null;
- /**
- * The room member who is the target of this event, e.g.
- * the invitee, the person being banned, etc.
- * @privateRemarks
- * Should be read-only
- */
- public target: RoomMember | null = null;
- /**
- * The sending status of the event.
- * @privateRemarks
- * Should be read-only
- */
- public status: EventStatus | null = null;
- /**
- * most recent error associated with sending the event, if any
- * @privateRemarks
- * Should be read-only
- */
- public error: MatrixError | null = null;
- /**
- * True if this event is 'forward looking', meaning
- * that getDirectionalContent() will return event.content and not event.prev_content.
- * Only state events may be backwards looking
- * Default: true. <strong>This property is experimental and may change.</strong>
- * @privateRemarks
- * Should be read-only
- */
- public forwardLooking = true;
-
- /* If the event is a `m.key.verification.request` (or to_device `m.key.verification.start`) event,
- * `Crypto` will set this the `VerificationRequest` for the event
- * so it can be easily accessed from the timeline.
- */
- public verificationRequest?: VerificationRequest;
-
- private readonly reEmitter: TypedReEmitter<MatrixEventEmittedEvents, MatrixEventHandlerMap>;
-
- /**
- * Construct a Matrix Event object
- *
- * @param event - The raw (possibly encrypted) event. <b>Do not access
- * this property</b> directly unless you absolutely have to. Prefer the getter
- * methods defined on this class. Using the getter methods shields your app
- * from changes to event JSON between Matrix versions.
- */
- public constructor(public event: Partial<IEvent> = {}) {
- super();
-
- // intern the values of matrix events to force share strings and reduce the
- // amount of needless string duplication. This can save moderate amounts of
- // memory (~10% on a 350MB heap).
- // 'membership' at the event level (rather than the content level) is a legacy
- // field that Element never otherwise looks at, but it will still take up a lot
- // of space if we don't intern it.
- (["state_key", "type", "sender", "room_id", "membership"] as const).forEach((prop) => {
- if (typeof event[prop] !== "string") return;
- event[prop] = internaliseString(event[prop]!);
- });
-
- (["membership", "avatar_url", "displayname"] as const).forEach((prop) => {
- if (typeof event.content?.[prop] !== "string") return;
- event.content[prop] = internaliseString(event.content[prop]!);
- });
-
- (["rel_type"] as const).forEach((prop) => {
- if (typeof event.content?.["m.relates_to"]?.[prop] !== "string") return;
- event.content["m.relates_to"][prop] = internaliseString(event.content["m.relates_to"][prop]!);
- });
-
- this.txnId = event.txn_id;
- this.localTimestamp = Date.now() - (this.getAge() ?? 0);
- this.reEmitter = new TypedReEmitter(this);
- }
-
- /**
- * Unstable getter to try and get an extensible event. Note that this might
- * return a falsy value if the event could not be parsed as an extensible
- * event.
- *
- * @deprecated Use stable functions where possible.
- */
- public get unstableExtensibleEvent(): Optional<ExtensibleEvent> {
- if (!this._hasCachedExtEv) {
- this._cachedExtEv = ExtensibleEvents.parse(this.getEffectiveEvent());
- }
- return this._cachedExtEv;
- }
-
- private invalidateExtensibleEvent(): void {
- // just reset the flag - that'll trick the getter into parsing a new event
- this._hasCachedExtEv = false;
- }
-
- /**
- * Gets the event as though it would appear unencrypted. If the event is already not
- * encrypted, it is simply returned as-is.
- * @returns The event in wire format.
- */
- public getEffectiveEvent(): IEvent {
- const content = Object.assign({}, this.getContent()); // clone for mutation
-
- if (this.getWireType() === EventType.RoomMessageEncrypted) {
- // Encrypted events sometimes aren't symmetrical on the `content` so we'll copy
- // that over too, but only for missing properties. We don't copy over mismatches
- // between the plain and decrypted copies of `content` because we assume that the
- // app is relying on the decrypted version, so we want to expose that as a source
- // of truth here too.
- for (const [key, value] of Object.entries(this.getWireContent())) {
- // Skip fields from the encrypted event schema though - we don't want to leak
- // these.
- if (["algorithm", "ciphertext", "device_id", "sender_key", "session_id"].includes(key)) {
- continue;
- }
-
- if (content[key] === undefined) content[key] = value;
- }
- }
-
- // clearEvent doesn't have all the fields, so we'll copy what we can from this.event.
- // We also copy over our "fixed" content key.
- return Object.assign({}, this.event, this.clearEvent, { content }) as IEvent;
- }
-
- /**
- * Get the event_id for this event.
- * @returns The event ID, e.g. <code>$143350589368169JsLZx:localhost
- * </code>
- */
- public getId(): string | undefined {
- return this.event.event_id;
- }
-
- /**
- * Get the user_id for this event.
- * @returns The user ID, e.g. `@alice:matrix.org`
- */
- public getSender(): string | undefined {
- return this.event.sender || this.event.user_id; // v2 / v1
- }
-
- /**
- * Get the (decrypted, if necessary) type of event.
- *
- * @returns The event type, e.g. `m.room.message`
- */
- public getType(): EventType | string {
- if (this.clearEvent) {
- return this.clearEvent.type;
- }
- return this.event.type!;
- }
-
- /**
- * Get the (possibly encrypted) type of the event that will be sent to the
- * homeserver.
- *
- * @returns The event type.
- */
- public getWireType(): EventType | string {
- return this.event.type!;
- }
-
- /**
- * Get the room_id for this event. This will return `undefined`
- * for `m.presence` events.
- * @returns The room ID, e.g. <code>!cURbafjkfsMDVwdRDQ:matrix.org
- * </code>
- */
- public getRoomId(): string | undefined {
- return this.event.room_id;
- }
-
- /**
- * Get the timestamp of this event.
- * @returns The event timestamp, e.g. `1433502692297`
- */
- public getTs(): number {
- return this.event.origin_server_ts!;
- }
-
- /**
- * Get the timestamp of this event, as a Date object.
- * @returns The event date, e.g. `new Date(1433502692297)`
- */
- public getDate(): Date | null {
- return this.event.origin_server_ts ? new Date(this.event.origin_server_ts) : null;
- }
-
- /**
- * Get a string containing details of this event
- *
- * This is intended for logging, to help trace errors. Example output:
- *
- * @example
- * ```
- * id=$HjnOHV646n0SjLDAqFrgIjim7RCpB7cdMXFrekWYAn type=m.room.encrypted
- * sender=@user:example.com room=!room:example.com ts=2022-10-25T17:30:28.404Z
- * ```
- */
- public getDetails(): string {
- let details = `id=${this.getId()} type=${this.getWireType()} sender=${this.getSender()}`;
- const room = this.getRoomId();
- if (room) {
- details += ` room=${room}`;
- }
- const date = this.getDate();
- if (date) {
- details += ` ts=${date.toISOString()}`;
- }
- return details;
- }
-
- /**
- * Get the (decrypted, if necessary) event content JSON, even if the event
- * was replaced by another event.
- *
- * @returns The event content JSON, or an empty object.
- */
- public getOriginalContent<T = IContent>(): T {
- if (this._localRedactionEvent) {
- return {} as T;
- }
- if (this.clearEvent) {
- return (this.clearEvent.content || {}) as T;
- }
- return (this.event.content || {}) as T;
- }
-
- /**
- * Get the (decrypted, if necessary) event content JSON,
- * or the content from the replacing event, if any.
- * See `makeReplaced`.
- *
- * @returns The event content JSON, or an empty object.
- */
- public getContent<T extends IContent = IContent>(): T {
- if (this._localRedactionEvent) {
- return {} as T;
- } else if (this._replacingEvent) {
- return this._replacingEvent.getContent()["m.new_content"] || {};
- } else {
- return this.getOriginalContent();
- }
- }
-
- /**
- * Get the (possibly encrypted) event content JSON that will be sent to the
- * homeserver.
- *
- * @returns The event content JSON, or an empty object.
- */
- public getWireContent(): IContent {
- return this.event.content || {};
- }
-
- /**
- * Get the event ID of the thread head
- */
- public get threadRootId(): string | undefined {
- const relatesTo = this.getWireContent()?.["m.relates_to"];
- if (relatesTo?.rel_type === THREAD_RELATION_TYPE.name) {
- return relatesTo.event_id;
- } else {
- return this.getThread()?.id || this.threadId;
- }
- }
-
- /**
- * A helper to check if an event is a thread's head or not
- */
- public get isThreadRoot(): boolean {
- const threadDetails = this.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
-
- // Bundled relationships only returned when the sync response is limited
- // hence us having to check both bundled relation and inspect the thread
- // model
- return !!threadDetails || this.getThread()?.id === this.getId();
- }
-
- public get replyEventId(): string | undefined {
- return this.getWireContent()["m.relates_to"]?.["m.in_reply_to"]?.event_id;
- }
-
- public get relationEventId(): string | undefined {
- return this.getWireContent()?.["m.relates_to"]?.event_id;
- }
-
- /**
- * Get the previous event content JSON. This will only return something for
- * state events which exist in the timeline.
- * @returns The previous event content JSON, or an empty object.
- */
- public getPrevContent(): IContent {
- // v2 then v1 then default
- return this.getUnsigned().prev_content || this.event.prev_content || {};
- }
-
- /**
- * Get either 'content' or 'prev_content' depending on if this event is
- * 'forward-looking' or not. This can be modified via event.forwardLooking.
- * In practice, this means we get the chronologically earlier content value
- * for this event (this method should surely be called getEarlierContent)
- * <strong>This method is experimental and may change.</strong>
- * @returns event.content if this event is forward-looking, else
- * event.prev_content.
- */
- public getDirectionalContent(): IContent {
- return this.forwardLooking ? this.getContent() : this.getPrevContent();
- }
-
- /**
- * Get the age of this event. This represents the age of the event when the
- * event arrived at the device, and not the age of the event when this
- * function was called.
- * Can only be returned once the server has echo'ed back
- * @returns The age of this event in milliseconds.
- */
- public getAge(): number | undefined {
- return this.getUnsigned().age || this.event.age; // v2 / v1
- }
-
- /**
- * Get the age of the event when this function was called.
- * This is the 'age' field adjusted according to how long this client has
- * had the event.
- * @returns The age of this event in milliseconds.
- */
- public getLocalAge(): number {
- return Date.now() - this.localTimestamp;
- }
-
- /**
- * Get the event state_key if it has one. This will return <code>undefined
- * </code> for message events.
- * @returns The event's `state_key`.
- */
- public getStateKey(): string | undefined {
- return this.event.state_key;
- }
-
- /**
- * Check if this event is a state event.
- * @returns True if this is a state event.
- */
- public isState(): boolean {
- return this.event.state_key !== undefined;
- }
-
- /**
- * Replace the content of this event with encrypted versions.
- * (This is used when sending an event; it should not be used by applications).
- *
- * @internal
- *
- * @param cryptoType - type of the encrypted event - typically
- * <tt>"m.room.encrypted"</tt>
- *
- * @param cryptoContent - raw 'content' for the encrypted event.
- *
- * @param senderCurve25519Key - curve25519 key to record for the
- * sender of this event.
- * See {@link MatrixEvent#getSenderKey}.
- *
- * @param claimedEd25519Key - claimed ed25519 key to record for the
- * sender if this event.
- * See {@link MatrixEvent#getClaimedEd25519Key}
- */
- public makeEncrypted(
- cryptoType: string,
- cryptoContent: object,
- senderCurve25519Key: string,
- claimedEd25519Key: string,
- ): void {
- // keep the plain-text data for 'view source'
- this.clearEvent = {
- type: this.event.type!,
- content: this.event.content!,
- };
- this.event.type = cryptoType;
- this.event.content = cryptoContent;
- this.senderCurve25519Key = senderCurve25519Key;
- this.claimedEd25519Key = claimedEd25519Key;
- }
-
- /**
- * Check if this event is currently being decrypted.
- *
- * @returns True if this event is currently being decrypted, else false.
- */
- public isBeingDecrypted(): boolean {
- return this.decryptionPromise != null;
- }
-
- public getDecryptionPromise(): Promise<void> | null {
- return this.decryptionPromise;
- }
-
- /**
- * Check if this event is an encrypted event which we failed to decrypt
- *
- * (This implies that we might retry decryption at some point in the future)
- *
- * @returns True if this event is an encrypted event which we
- * couldn't decrypt.
- */
- public isDecryptionFailure(): boolean {
- return this.clearEvent?.content?.msgtype === "m.bad.encrypted";
- }
-
- /*
- * True if this event is an encrypted event which we failed to decrypt, the receiver's device is unverified and
- * the sender has disabled encrypting to unverified devices.
- */
- public get isEncryptedDisabledForUnverifiedDevices(): boolean {
- return this.isDecryptionFailure() && this.encryptedDisabledForUnverifiedDevices;
- }
-
- public shouldAttemptDecryption(): boolean {
- if (this.isRedacted()) return false;
- if (this.isBeingDecrypted()) return false;
- if (this.clearEvent) return false;
- if (!this.isEncrypted()) return false;
-
- return true;
- }
-
- /**
- * Start the process of trying to decrypt this event.
- *
- * (This is used within the SDK: it isn't intended for use by applications)
- *
- * @internal
- *
- * @param crypto - crypto module
- *
- * @returns promise which resolves (to undefined) when the decryption
- * attempt is completed.
- */
- public async attemptDecryption(crypto: CryptoBackend, options: IDecryptOptions = {}): Promise<void> {
- // start with a couple of sanity checks.
- if (!this.isEncrypted()) {
- throw new Error("Attempt to decrypt event which isn't encrypted");
- }
-
- const alreadyDecrypted = this.clearEvent && !this.isDecryptionFailure();
- const forceRedecrypt = options.forceRedecryptIfUntrusted && this.isKeySourceUntrusted();
- if (alreadyDecrypted && !forceRedecrypt) {
- // we may want to just ignore this? let's start with rejecting it.
- throw new Error("Attempt to decrypt event which has already been decrypted");
- }
-
- // if we already have a decryption attempt in progress, then it may
- // fail because it was using outdated info. We now have reason to
- // succeed where it failed before, but we don't want to have multiple
- // attempts going at the same time, so just set a flag that says we have
- // new info.
- //
- if (this.decryptionPromise) {
- logger.log(`Event ${this.getId()} already being decrypted; queueing a retry`);
- this.retryDecryption = true;
- return this.decryptionPromise;
- }
-
- this.decryptionPromise = this.decryptionLoop(crypto, options);
- return this.decryptionPromise;
- }
-
- /**
- * Cancel any room key request for this event and resend another.
- *
- * @param crypto - crypto module
- * @param userId - the user who received this event
- *
- * @returns a promise that resolves when the request is queued
- */
- public cancelAndResendKeyRequest(crypto: Crypto, userId: string): Promise<void> {
- const wireContent = this.getWireContent();
- return crypto.requestRoomKey(
- {
- algorithm: wireContent.algorithm,
- room_id: this.getRoomId()!,
- session_id: wireContent.session_id,
- sender_key: wireContent.sender_key,
- },
- this.getKeyRequestRecipients(userId),
- true,
- );
- }
-
- /**
- * Calculate the recipients for keyshare requests.
- *
- * @param userId - the user who received this event.
- *
- * @returns array of recipients
- */
- public getKeyRequestRecipients(userId: string): IKeyRequestRecipient[] {
- // send the request to all of our own devices
- const recipients = [
- {
- userId,
- deviceId: "*",
- },
- ];
-
- return recipients;
- }
-
- private async decryptionLoop(crypto: CryptoBackend, options: IDecryptOptions = {}): Promise<void> {
- // make sure that this method never runs completely synchronously.
- // (doing so would mean that we would clear decryptionPromise *before*
- // it is set in attemptDecryption - and hence end up with a stuck
- // `decryptionPromise`).
- await Promise.resolve();
-
- // eslint-disable-next-line no-constant-condition
- while (true) {
- this.retryDecryption = false;
-
- let res: IEventDecryptionResult;
- let err: Error | undefined = undefined;
- try {
- if (!crypto) {
- res = this.badEncryptedMessage("Encryption not enabled");
- } else {
- res = await crypto.decryptEvent(this);
- if (options.isRetry === true) {
- logger.info(`Decrypted event on retry (${this.getDetails()})`);
- }
- }
- } catch (e) {
- const detailedError = e instanceof DecryptionError ? (<DecryptionError>e).detailedString : String(e);
-
- err = e as Error;
-
- // see if we have a retry queued.
- //
- // NB: make sure to keep this check in the same tick of the
- // event loop as `decryptionPromise = null` below - otherwise we
- // risk a race:
- //
- // * A: we check retryDecryption here and see that it is
- // false
- // * B: we get a second call to attemptDecryption, which sees
- // that decryptionPromise is set so sets
- // retryDecryption
- // * A: we continue below, clear decryptionPromise, and
- // never do the retry.
- //
- if (this.retryDecryption) {
- // decryption error, but we have a retry queued.
- logger.log(`Error decrypting event (${this.getDetails()}), but retrying: ${detailedError}`);
- continue;
- }
-
- // decryption error, no retries queued. Warn about the error and
- // set it to m.bad.encrypted.
- //
- // the detailedString already includes the name and message of the error, and the stack isn't much use,
- // so we don't bother to log `e` separately.
- logger.warn(`Error decrypting event (${this.getDetails()}): ${detailedError}`);
-
- res = this.badEncryptedMessage(String(e));
- }
-
- // at this point, we've either successfully decrypted the event, or have given up
- // (and set res to a 'badEncryptedMessage'). Either way, we can now set the
- // cleartext of the event and raise Event.decrypted.
- //
- // make sure we clear 'decryptionPromise' before sending the 'Event.decrypted' event,
- // otherwise the app will be confused to see `isBeingDecrypted` still set when
- // there isn't an `Event.decrypted` on the way.
- //
- // see also notes on retryDecryption above.
- //
- this.decryptionPromise = null;
- this.retryDecryption = false;
- this.setClearData(res);
-
- // Before we emit the event, clear the push actions so that they can be recalculated
- // by relevant code. We do this because the clear event has now changed, making it
- // so that existing rules can be re-run over the applicable properties. Stuff like
- // highlighting when the user's name is mentioned rely on this happening. We also want
- // to set the push actions before emitting so that any notification listeners don't
- // pick up the wrong contents.
- this.setPushActions(null);
-
- if (options.emit !== false) {
- this.emit(MatrixEventEvent.Decrypted, this, err);
- }
-
- return;
- }
- }
-
- private badEncryptedMessage(reason: string): IEventDecryptionResult {
- return {
- clearEvent: {
- type: EventType.RoomMessage,
- content: {
- msgtype: "m.bad.encrypted",
- body: "** Unable to decrypt: " + reason + " **",
- },
- },
- encryptedDisabledForUnverifiedDevices: reason === `DecryptionError: ${WITHHELD_MESSAGES["m.unverified"]}`,
- };
- }
-
- /**
- * Update the cleartext data on this event.
- *
- * (This is used after decrypting an event; it should not be used by applications).
- *
- * @internal
- *
- * @param decryptionResult - the decryption result, including the plaintext and some key info
- *
- * @remarks
- * Fires {@link MatrixEventEvent.Decrypted}
- */
- private setClearData(decryptionResult: IEventDecryptionResult): void {
- this.clearEvent = decryptionResult.clearEvent;
- this.senderCurve25519Key = decryptionResult.senderCurve25519Key ?? null;
- this.claimedEd25519Key = decryptionResult.claimedEd25519Key ?? null;
- this.forwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain || [];
- this.untrusted = decryptionResult.untrusted || false;
- this.encryptedDisabledForUnverifiedDevices = decryptionResult.encryptedDisabledForUnverifiedDevices || false;
- this.invalidateExtensibleEvent();
- }
-
- /**
- * Gets the cleartext content for this event. If the event is not encrypted,
- * or encryption has not been completed, this will return null.
- *
- * @returns The cleartext (decrypted) content for the event
- */
- public getClearContent(): IContent | null {
- return this.clearEvent ? this.clearEvent.content : null;
- }
-
- /**
- * Check if the event is encrypted.
- * @returns True if this event is encrypted.
- */
- public isEncrypted(): boolean {
- return !this.isState() && this.event.type === EventType.RoomMessageEncrypted;
- }
-
- /**
- * The curve25519 key for the device that we think sent this event
- *
- * For an Olm-encrypted event, this is inferred directly from the DH
- * exchange at the start of the session: the curve25519 key is involved in
- * the DH exchange, so only a device which holds the private part of that
- * key can establish such a session.
- *
- * For a megolm-encrypted event, it is inferred from the Olm message which
- * established the megolm session
- */
- public getSenderKey(): string | null {
- return this.senderCurve25519Key;
- }
-
- /**
- * The additional keys the sender of this encrypted event claims to possess.
- *
- * Just a wrapper for #getClaimedEd25519Key (q.v.)
- */
- public getKeysClaimed(): Partial<Record<"ed25519", string>> {
- if (!this.claimedEd25519Key) return {};
-
- return {
- ed25519: this.claimedEd25519Key,
- };
- }
-
- /**
- * Get the ed25519 the sender of this event claims to own.
- *
- * For Olm messages, this claim is encoded directly in the plaintext of the
- * event itself. For megolm messages, it is implied by the m.room_key event
- * which established the megolm session.
- *
- * Until we download the device list of the sender, it's just a claim: the
- * device list gives a proof that the owner of the curve25519 key used for
- * this event (and returned by #getSenderKey) also owns the ed25519 key by
- * signing the public curve25519 key with the ed25519 key.
- *
- * In general, applications should not use this method directly, but should
- * instead use MatrixClient.getEventSenderDeviceInfo.
- */
- public getClaimedEd25519Key(): string | null {
- return this.claimedEd25519Key;
- }
-
- /**
- * Get the curve25519 keys of the devices which were involved in telling us
- * about the claimedEd25519Key and sender curve25519 key.
- *
- * Normally this will be empty, but in the case of a forwarded megolm
- * session, the sender keys are sent to us by another device (the forwarding
- * device), which we need to trust to do this. In that case, the result will
- * be a list consisting of one entry.
- *
- * If the device that sent us the key (A) got it from another device which
- * it wasn't prepared to vouch for (B), the result will be [A, B]. And so on.
- *
- * @returns base64-encoded curve25519 keys, from oldest to newest.
- */
- public getForwardingCurve25519KeyChain(): string[] {
- return this.forwardingCurve25519KeyChain;
- }
-
- /**
- * Whether the decryption key was obtained from an untrusted source. If so,
- * we cannot verify the authenticity of the message.
- */
- public isKeySourceUntrusted(): boolean | undefined {
- return !!this.untrusted;
- }
-
- public getUnsigned(): IUnsigned {
- return this.event.unsigned || {};
- }
-
- public setUnsigned(unsigned: IUnsigned): void {
- this.event.unsigned = unsigned;
- }
-
- public unmarkLocallyRedacted(): boolean {
- const value = this._localRedactionEvent;
- this._localRedactionEvent = null;
- if (this.event.unsigned) {
- this.event.unsigned.redacted_because = undefined;
- }
- return !!value;
- }
-
- public markLocallyRedacted(redactionEvent: MatrixEvent): void {
- if (this._localRedactionEvent) return;
- this.emit(MatrixEventEvent.BeforeRedaction, this, redactionEvent);
- this._localRedactionEvent = redactionEvent;
- if (!this.event.unsigned) {
- this.event.unsigned = {};
- }
- this.event.unsigned.redacted_because = redactionEvent.event as IEvent;
- }
-
- /**
- * Change the visibility of an event, as per https://github.com/matrix-org/matrix-doc/pull/3531 .
- *
- * @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).
- *
- * @remarks
- * Fires {@link MatrixEventEvent.VisibilityChange} if `visibilityEvent`
- * 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).
- */
- public applyVisibilityEvent(visibilityChange?: IVisibilityChange): void {
- const visible = visibilityChange?.visible ?? true;
- const reason = visibilityChange?.reason ?? null;
- let change = false;
- if (this.visibility.visible !== visible) {
- change = true;
- } else if (!this.visibility.visible && this.visibility["reason"] !== reason) {
- change = true;
- }
- if (change) {
- if (visible) {
- this.visibility = MESSAGE_VISIBLE;
- } else {
- this.visibility = Object.freeze({
- visible: false,
- reason,
- });
- }
- this.emit(MatrixEventEvent.VisibilityChange, this, visible);
- }
- }
-
- /**
- * Return instructions to display or hide the message.
- *
- * @returns Instructions determining whether the message
- * should be displayed.
- */
- public messageVisibility(): MessageVisibility {
- // Note: We may return `this.visibility` without fear, as
- // this is a shallow frozen object.
- return this.visibility;
- }
-
- /**
- * Update the content of an event in the same way it would be by the server
- * if it were redacted before it was sent to us
- *
- * @param redactionEvent - event causing the redaction
- */
- public makeRedacted(redactionEvent: MatrixEvent): void {
- // quick sanity-check
- if (!redactionEvent.event) {
- throw new Error("invalid redactionEvent in makeRedacted");
- }
-
- this._localRedactionEvent = null;
-
- this.emit(MatrixEventEvent.BeforeRedaction, this, redactionEvent);
-
- this._replacingEvent = null;
- // we attempt to replicate what we would see from the server if
- // the event had been redacted before we saw it.
- //
- // The server removes (most of) the content of the event, and adds a
- // "redacted_because" key to the unsigned section containing the
- // redacted event.
- if (!this.event.unsigned) {
- this.event.unsigned = {};
- }
- this.event.unsigned.redacted_because = redactionEvent.event as IEvent;
-
- for (const key in this.event) {
- if (this.event.hasOwnProperty(key) && !REDACT_KEEP_KEYS.has(key)) {
- delete this.event[key as keyof IEvent];
- }
- }
-
- // If the event is encrypted prune the decrypted bits
- if (this.isEncrypted()) {
- this.clearEvent = undefined;
- }
-
- const keeps =
- this.getType() in REDACT_KEEP_CONTENT_MAP
- ? REDACT_KEEP_CONTENT_MAP[this.getType() as keyof typeof REDACT_KEEP_CONTENT_MAP]
- : {};
- const content = this.getContent();
- for (const key in content) {
- if (content.hasOwnProperty(key) && !keeps[key]) {
- delete content[key];
- }
- }
-
- this.invalidateExtensibleEvent();
- }
-
- /**
- * Check if this event has been redacted
- *
- * @returns True if this event has been redacted
- */
- public isRedacted(): boolean {
- return Boolean(this.getUnsigned().redacted_because);
- }
-
- /**
- * Check if this event is a redaction of another event
- *
- * @returns True if this event is a redaction
- */
- public isRedaction(): boolean {
- return this.getType() === EventType.RoomRedaction;
- }
-
- /**
- * Return the visibility change caused by this event,
- * as per https://github.com/matrix-org/matrix-doc/pull/3531.
- *
- * @returns If the event is a well-formed visibility change event,
- * an instance of `IVisibilityChange`, otherwise `null`.
- */
- public asVisibilityChange(): IVisibilityChange | null {
- if (!EVENT_VISIBILITY_CHANGE_TYPE.matches(this.getType())) {
- // Not a visibility change event.
- return null;
- }
- const relation = this.getRelation();
- if (!relation || relation.rel_type != "m.reference") {
- // Ill-formed, ignore this event.
- return null;
- }
- const eventId = relation.event_id;
- if (!eventId) {
- // Ill-formed, ignore this event.
- return null;
- }
- const content = this.getWireContent();
- const visible = !!content.visible;
- const reason = content.reason;
- if (reason && typeof reason != "string") {
- // Ill-formed, ignore this event.
- return null;
- }
- // Well-formed visibility change event.
- return {
- visible,
- reason,
- eventId,
- };
- }
-
- /**
- * Check if this event alters the visibility of another event,
- * as per https://github.com/matrix-org/matrix-doc/pull/3531.
- *
- * @returns True if this event alters the visibility
- * of another event.
- */
- public isVisibilityEvent(): boolean {
- return EVENT_VISIBILITY_CHANGE_TYPE.matches(this.getType());
- }
-
- /**
- * Get the (decrypted, if necessary) redaction event JSON
- * if event was redacted
- *
- * @returns The redaction event JSON, or an empty object
- */
- public getRedactionEvent(): IEvent | {} | null {
- if (!this.isRedacted()) return null;
-
- if (this.clearEvent?.unsigned) {
- return this.clearEvent?.unsigned.redacted_because ?? null;
- } else if (this.event.unsigned?.redacted_because) {
- return this.event.unsigned.redacted_because;
- } else {
- return {};
- }
- }
-
- /**
- * Get the push actions, if known, for this event
- *
- * @returns push actions
- */
- public getPushActions(): IActionsObject | null {
- return this.pushActions;
- }
-
- /**
- * Set the push actions for this event.
- *
- * @param pushActions - push actions
- */
- public setPushActions(pushActions: IActionsObject | null): void {
- this.pushActions = pushActions;
- }
-
- /**
- * Replace the `event` property and recalculate any properties based on it.
- * @param event - the object to assign to the `event` property
- */
- public handleRemoteEcho(event: object): void {
- const oldUnsigned = this.getUnsigned();
- const oldId = this.getId();
- this.event = event;
- // if this event was redacted before it was sent, it's locally marked as redacted.
- // At this point, we've received the remote echo for the event, but not yet for
- // the redaction that we are sending ourselves. Preserve the locally redacted
- // state by copying over redacted_because so we don't get a flash of
- // redacted, not-redacted, redacted as remote echos come in
- if (oldUnsigned.redacted_because) {
- if (!this.event.unsigned) {
- this.event.unsigned = {};
- }
- this.event.unsigned.redacted_because = oldUnsigned.redacted_because;
- }
- // successfully sent.
- this.setStatus(null);
- if (this.getId() !== oldId) {
- // emit the event if it changed
- this.emit(MatrixEventEvent.LocalEventIdReplaced, this);
- }
-
- this.localTimestamp = Date.now() - this.getAge()!;
- }
-
- /**
- * Whether the event is in any phase of sending, send failure, waiting for
- * remote echo, etc.
- */
- public isSending(): boolean {
- return !!this.status;
- }
-
- /**
- * Update the event's sending status and emit an event as well.
- *
- * @param status - The new status
- */
- public setStatus(status: EventStatus | null): void {
- this.status = status;
- this.emit(MatrixEventEvent.Status, this, status);
- }
-
- public replaceLocalEventId(eventId: string): void {
- this.event.event_id = eventId;
- this.emit(MatrixEventEvent.LocalEventIdReplaced, this);
- }
-
- /**
- * Get whether the event is a relation event, and of a given type if
- * `relType` is passed in. State events cannot be relation events
- *
- * @param relType - if given, checks that the relation is of the
- * given type
- */
- public isRelation(relType?: string): boolean {
- // Relation info is lifted out of the encrypted content when sent to
- // encrypted rooms, so we have to check `getWireContent` for this.
- const relation = this.getWireContent()?.["m.relates_to"];
- if (this.isState() && relation?.rel_type === RelationType.Replace) {
- // State events cannot be m.replace relations
- return false;
- }
- return !!(relation?.rel_type && relation.event_id && (relType ? relation.rel_type === relType : true));
- }
-
- /**
- * Get relation info for the event, if any.
- */
- public getRelation(): IEventRelation | null {
- if (!this.isRelation()) {
- return null;
- }
- return this.getWireContent()["m.relates_to"] ?? null;
- }
-
- /**
- * Set an event that replaces the content of this event, through an m.replace relation.
- *
- * @param newEvent - the event with the replacing content, if any.
- *
- * @remarks
- * Fires {@link MatrixEventEvent.Replaced}
- */
- public makeReplaced(newEvent?: MatrixEvent): void {
- // don't allow redacted events to be replaced.
- // if newEvent is null we allow to go through though,
- // as with local redaction, the replacing event might get
- // cancelled, which should be reflected on the target event.
- 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 ?? null;
- this.emit(MatrixEventEvent.Replaced, this);
- this.invalidateExtensibleEvent();
- }
- }
-
- /**
- * Returns the status of any associated edit or redaction
- * (not for reactions/annotations as their local echo doesn't affect the original event),
- * or else the status of the event.
- */
- public getAssociatedStatus(): EventStatus | null {
- if (this._replacingEvent) {
- return this._replacingEvent.status;
- } else if (this._localRedactionEvent) {
- return this._localRedactionEvent.status;
- }
- return this.status;
- }
-
- public getServerAggregatedRelation<T>(relType: RelationType | string): T | undefined {
- return this.getUnsigned()["m.relations"]?.[relType];
- }
-
- /**
- * Returns the event ID of the event replacing the content of this event, if any.
- */
- public replacingEventId(): string | undefined {
- const replaceRelation = this.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
- if (replaceRelation) {
- return replaceRelation.event_id;
- } else if (this._replacingEvent) {
- return this._replacingEvent.getId();
- }
- }
-
- /**
- * Returns the event replacing the content of this event, if any.
- * Replacements are aggregated on the server, so this would only
- * return an event in case it came down the sync, or for local echo of edits.
- */
- public replacingEvent(): MatrixEvent | null {
- return this._replacingEvent;
- }
-
- /**
- * Returns the origin_server_ts of the event replacing the content of this event, if any.
- */
- public replacingEventDate(): Date | undefined {
- const replaceRelation = this.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
- if (replaceRelation) {
- const ts = replaceRelation.origin_server_ts;
- if (Number.isFinite(ts)) {
- return new Date(ts);
- }
- } else if (this._replacingEvent) {
- return this._replacingEvent.getDate() ?? undefined;
- }
- }
-
- /**
- * Returns the event that wants to redact this event, but hasn't been sent yet.
- * @returns the event
- */
- public localRedactionEvent(): MatrixEvent | null {
- return this._localRedactionEvent;
- }
-
- /**
- * For relations and redactions, returns the event_id this event is referring to.
- */
- public getAssociatedId(): string | undefined {
- const relation = this.getRelation();
- if (this.replyEventId) {
- return this.replyEventId;
- } else if (relation) {
- return relation.event_id;
- } else if (this.isRedaction()) {
- return this.event.redacts;
- }
- }
-
- /**
- * Checks if this event is associated with another event. See `getAssociatedId`.
- * @deprecated use hasAssociation instead.
- */
- public hasAssocation(): boolean {
- return !!this.getAssociatedId();
- }
-
- /**
- * Checks if this event is associated with another event. See `getAssociatedId`.
- */
- public hasAssociation(): boolean {
- return !!this.getAssociatedId();
- }
-
- /**
- * Update the related id with a new one.
- *
- * Used to replace a local id with remote one before sending
- * an event with a related id.
- *
- * @param eventId - the new event id
- */
- public updateAssociatedId(eventId: string): void {
- const relation = this.getRelation();
- if (relation) {
- relation.event_id = eventId;
- } else if (this.isRedaction()) {
- this.event.redacts = eventId;
- }
- }
-
- /**
- * Flags an event as cancelled due to future conditions. For example, a verification
- * request event in the same sync transaction may be flagged as cancelled to warn
- * listeners that a cancellation event is coming down the same pipe shortly.
- * @param cancelled - Whether the event is to be cancelled or not.
- */
- public flagCancelled(cancelled = true): void {
- this._isCancelled = cancelled;
- }
-
- /**
- * Gets whether or not the event is flagged as cancelled. See flagCancelled() for
- * more information.
- * @returns True if the event is cancelled, false otherwise.
- */
- public isCancelled(): boolean {
- return this._isCancelled;
- }
-
- /**
- * Get a copy/snapshot of this event. The returned copy will be loosely linked
- * back to this instance, though will have "frozen" event information. Other
- * properties of this MatrixEvent instance will be copied verbatim, which can
- * mean they are in reference to this instance despite being on the copy too.
- * The reference the snapshot uses does not change, however members aside from
- * the underlying event will not be deeply cloned, thus may be mutated internally.
- * For example, the sender profile will be copied over at snapshot time, and
- * the sender profile internally may mutate without notice to the consumer.
- *
- * This is meant to be used to snapshot the event details themselves, not the
- * features (such as sender) surrounding the event.
- * @returns A snapshot of this event.
- */
- public toSnapshot(): MatrixEvent {
- const ev = new MatrixEvent(JSON.parse(JSON.stringify(this.event)));
- for (const [p, v] of Object.entries(this)) {
- if (p !== "event") {
- // exclude the thing we just cloned
- // @ts-ignore - XXX: this is just nasty
- ev[p as keyof MatrixEvent] = v;
- }
- }
- return ev;
- }
-
- /**
- * Determines if this event is equivalent to the given event. This only checks
- * the event object itself, not the other properties of the event. Intended for
- * use with toSnapshot() to identify events changing.
- * @param otherEvent - The other event to check against.
- * @returns True if the events are the same, false otherwise.
- */
- public isEquivalentTo(otherEvent: MatrixEvent): boolean {
- if (!otherEvent) return false;
- if (otherEvent === this) return true;
- const myProps = deepSortedObjectEntries(this.event);
- const theirProps = deepSortedObjectEntries(otherEvent.event);
- return JSON.stringify(myProps) === JSON.stringify(theirProps);
- }
-
- /**
- * Summarise the event as JSON. This is currently used by React SDK's view
- * event source feature and Seshat's event indexing, so take care when
- * adjusting the output here.
- *
- * If encrypted, include both the decrypted and encrypted view of the event.
- *
- * This is named `toJSON` for use with `JSON.stringify` which checks objects
- * for functions named `toJSON` and will call them to customise the output
- * if they are defined.
- */
- public toJSON(): object {
- const event = this.getEffectiveEvent();
-
- if (!this.isEncrypted()) {
- return event;
- }
-
- return {
- decrypted: event,
- encrypted: this.event,
- };
- }
-
- public setVerificationRequest(request: VerificationRequest): void {
- this.verificationRequest = request;
- }
-
- public setTxnId(txnId: string): void {
- this.txnId = txnId;
- }
-
- public getTxnId(): string | undefined {
- return this.txnId;
- }
-
- /**
- * Set the instance of a thread associated with the current event
- * @param thread - the thread
- */
- public setThread(thread?: Thread): void {
- if (this.thread) {
- this.reEmitter.stopReEmitting(this.thread, [ThreadEvent.Update]);
- }
- this.thread = thread;
- this.setThreadId(thread?.id);
- if (thread) {
- this.reEmitter.reEmit(thread, [ThreadEvent.Update]);
- }
- }
-
- /**
- * Get the instance of the thread associated with the current event
- */
- public getThread(): Thread | undefined {
- return this.thread;
- }
-
- public setThreadId(threadId?: string): void {
- this.threadId = threadId;
- }
-}
-
-/* REDACT_KEEP_KEYS gives the keys we keep when an event is redacted
- *
- * This is specified here:
- * http://matrix.org/speculator/spec/HEAD/client_server/latest.html#redactions
- *
- * Also:
- * - We keep 'unsigned' since that is created by the local server
- * - We keep user_id for backwards-compat with v1
- */
-const REDACT_KEEP_KEYS = new Set([
- "event_id",
- "type",
- "room_id",
- "user_id",
- "sender",
- "state_key",
- "prev_state",
- "content",
- "unsigned",
- "origin_server_ts",
-]);
-
-// a map from state event type to the .content keys we keep when an event is redacted
-const REDACT_KEEP_CONTENT_MAP: Record<string, Record<string, 1>> = {
- [EventType.RoomMember]: { membership: 1 },
- [EventType.RoomCreate]: { creator: 1 },
- [EventType.RoomJoinRules]: { join_rule: 1 },
- [EventType.RoomPowerLevels]: {
- ban: 1,
- events: 1,
- events_default: 1,
- kick: 1,
- redact: 1,
- state_default: 1,
- users: 1,
- users_default: 1,
- },
-} as const;
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/invites-ignorer.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/invites-ignorer.ts
deleted file mode 100644
index 173ba62..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/invites-ignorer.ts
+++ /dev/null
@@ -1,368 +0,0 @@
-/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { UnstableValue } from "matrix-events-sdk";
-
-import { MatrixClient } from "../client";
-import { IContent, MatrixEvent } from "./event";
-import { EventTimeline } from "./event-timeline";
-import { Preset } from "../@types/partials";
-import { globToRegexp } from "../utils";
-import { Room } from "./room";
-
-/// The event type storing the user's individual policies.
-///
-/// Exported for testing purposes.
-export const POLICIES_ACCOUNT_EVENT_TYPE = new UnstableValue("m.policies", "org.matrix.msc3847.policies");
-
-/// The key within the user's individual policies storing the user's ignored invites.
-///
-/// Exported for testing purposes.
-export const IGNORE_INVITES_ACCOUNT_EVENT_KEY = new UnstableValue(
- "m.ignore.invites",
- "org.matrix.msc3847.ignore.invites",
-);
-
-/// The types of recommendations understood.
-enum PolicyRecommendation {
- Ban = "m.ban",
-}
-
-/**
- * The various scopes for policies.
- */
-export enum PolicyScope {
- /**
- * The policy deals with an individual user, e.g. reject invites
- * from this user.
- */
- User = "m.policy.user",
-
- /**
- * The policy deals with a room, e.g. reject invites towards
- * a specific room.
- */
- Room = "m.policy.room",
-
- /**
- * The policy deals with a server, e.g. reject invites from
- * this server.
- */
- Server = "m.policy.server",
-}
-
-/**
- * A container for ignored invites.
- *
- * # Performance
- *
- * This implementation is extremely naive. It expects that we are dealing
- * with a very short list of sources (e.g. only one). If real-world
- * applications turn out to require longer lists, we may need to rework
- * our data structures.
- */
-export class IgnoredInvites {
- public constructor(private readonly client: MatrixClient) {}
-
- /**
- * Add a new rule.
- *
- * @param scope - The scope for this rule.
- * @param entity - The entity covered by this rule. Globs are supported.
- * @param reason - A human-readable reason for introducing this new rule.
- * @returns The event id for the new rule.
- */
- public async addRule(scope: PolicyScope, entity: string, reason: string): Promise<string> {
- const target = await this.getOrCreateTargetRoom();
- const response = await this.client.sendStateEvent(target.roomId, scope, {
- entity,
- reason,
- recommendation: PolicyRecommendation.Ban,
- });
- return response.event_id;
- }
-
- /**
- * Remove a rule.
- */
- public async removeRule(event: MatrixEvent): Promise<void> {
- await this.client.redactEvent(event.getRoomId()!, event.getId()!);
- }
-
- /**
- * Add a new room to the list of sources. If the user isn't a member of the
- * room, attempt to join it.
- *
- * @param roomId - A valid room id. If this room is already in the list
- * of sources, it will not be duplicated.
- * @returns `true` if the source was added, `false` if it was already present.
- * @throws If `roomId` isn't the id of a room that the current user is already
- * member of or can join.
- *
- * # Safety
- *
- * This method will rewrite the `Policies` object in the user's account data.
- * This rewrite is inherently racy and could overwrite or be overwritten by
- * other concurrent rewrites of the same object.
- */
- public async addSource(roomId: string): Promise<boolean> {
- // We attempt to join the room *before* calling
- // `await this.getOrCreateSourceRooms()` to decrease the duration
- // of the racy section.
- await this.client.joinRoom(roomId);
- // Race starts.
- const sources = (await this.getOrCreateSourceRooms()).map((room) => room.roomId);
- if (sources.includes(roomId)) {
- return false;
- }
- sources.push(roomId);
- await this.withIgnoreInvitesPolicies((ignoreInvitesPolicies) => {
- ignoreInvitesPolicies.sources = sources;
- });
-
- // Race ends.
- return true;
- }
-
- /**
- * Find out whether an invite should be ignored.
- *
- * @param sender - The user id for the user who issued the invite.
- * @param roomId - The room to which the user is invited.
- * @returns A rule matching the entity, if any was found, `null` otherwise.
- */
- public async getRuleForInvite({
- sender,
- roomId,
- }: {
- sender: string;
- roomId: string;
- }): Promise<Readonly<MatrixEvent | null>> {
- // In this implementation, we perform a very naive lookup:
- // - search in each policy room;
- // - turn each (potentially glob) rule entity into a regexp.
- //
- // Real-world testing will tell us whether this is performant enough.
- // In the (unfortunately likely) case it isn't, there are several manners
- // in which we could optimize this:
- // - match several entities per go;
- // - pre-compile each rule entity into a regexp;
- // - pre-compile entire rooms into a single regexp.
- const policyRooms = await this.getOrCreateSourceRooms();
- const senderServer = sender.split(":")[1];
- const roomServer = roomId.split(":")[1];
- for (const room of policyRooms) {
- const state = room.getUnfilteredTimelineSet().getLiveTimeline().getState(EventTimeline.FORWARDS)!;
-
- for (const { scope, entities } of [
- { scope: PolicyScope.Room, entities: [roomId] },
- { scope: PolicyScope.User, entities: [sender] },
- { scope: PolicyScope.Server, entities: [senderServer, roomServer] },
- ]) {
- const events = state.getStateEvents(scope);
- for (const event of events) {
- const content = event.getContent();
- if (content?.recommendation != PolicyRecommendation.Ban) {
- // Ignoring invites only looks at `m.ban` recommendations.
- continue;
- }
- const glob = content?.entity;
- if (!glob) {
- // Invalid event.
- continue;
- }
- let regexp: RegExp;
- try {
- regexp = new RegExp(globToRegexp(glob, false));
- } catch (ex) {
- // Assume invalid event.
- continue;
- }
- for (const entity of entities) {
- if (entity && regexp.test(entity)) {
- return event;
- }
- }
- // No match.
- }
- }
- }
- return null;
- }
-
- /**
- * Get the target room, i.e. the room in which any new rule should be written.
- *
- * If there is no target room setup, a target room is created.
- *
- * Note: This method is public for testing reasons. Most clients should not need
- * to call it directly.
- *
- * # Safety
- *
- * This method will rewrite the `Policies` object in the user's account data.
- * This rewrite is inherently racy and could overwrite or be overwritten by
- * other concurrent rewrites of the same object.
- */
- public async getOrCreateTargetRoom(): Promise<Room> {
- const ignoreInvitesPolicies = this.getIgnoreInvitesPolicies();
- let target = ignoreInvitesPolicies.target;
- // Validate `target`. If it is invalid, trash out the current `target`
- // and create a new room.
- if (typeof target !== "string") {
- target = null;
- }
- if (target) {
- // Check that the room exists and is valid.
- const room = this.client.getRoom(target);
- if (room) {
- return room;
- } else {
- target = null;
- }
- }
- // We need to create our own policy room for ignoring invites.
- target = (
- await this.client.createRoom({
- name: "Individual Policy Room",
- preset: Preset.PrivateChat,
- })
- ).room_id;
- await this.withIgnoreInvitesPolicies((ignoreInvitesPolicies) => {
- ignoreInvitesPolicies.target = target;
- });
-
- // Since we have just called `createRoom`, `getRoom` should not be `null`.
- return this.client.getRoom(target)!;
- }
-
- /**
- * Get the list of source rooms, i.e. the rooms from which rules need to be read.
- *
- * If no source rooms are setup, the target room is used as sole source room.
- *
- * Note: This method is public for testing reasons. Most clients should not need
- * to call it directly.
- *
- * # Safety
- *
- * This method will rewrite the `Policies` object in the user's account data.
- * This rewrite is inherently racy and could overwrite or be overwritten by
- * other concurrent rewrites of the same object.
- */
- public async getOrCreateSourceRooms(): Promise<Room[]> {
- const ignoreInvitesPolicies = this.getIgnoreInvitesPolicies();
- let sources: string[] = ignoreInvitesPolicies.sources;
-
- // Validate `sources`. If it is invalid, trash out the current `sources`
- // and create a new list of sources from `target`.
- let hasChanges = false;
- if (!Array.isArray(sources)) {
- // `sources` could not be an array.
- hasChanges = true;
- sources = [];
- }
- let sourceRooms = sources
- // `sources` could contain non-string / invalid room ids
- .filter((roomId) => typeof roomId === "string")
- .map((roomId) => this.client.getRoom(roomId))
- .filter((room) => !!room) as Room[];
- if (sourceRooms.length != sources.length) {
- hasChanges = true;
- }
- if (sourceRooms.length == 0) {
- // `sources` could be empty (possibly because we've removed
- // invalid content)
- const target = await this.getOrCreateTargetRoom();
- hasChanges = true;
- sourceRooms = [target];
- }
- if (hasChanges) {
- // Reload `policies`/`ignoreInvitesPolicies` in case it has been changed
- // during or by our call to `this.getTargetRoom()`.
- await this.withIgnoreInvitesPolicies((ignoreInvitesPolicies) => {
- ignoreInvitesPolicies.sources = sources;
- });
- }
- return sourceRooms;
- }
-
- /**
- * Fetch the `IGNORE_INVITES_POLICIES` object from account data.
- *
- * If both an unstable prefix version and a stable prefix version are available,
- * it will return the stable prefix version preferentially.
- *
- * The result is *not* validated but is guaranteed to be a non-null object.
- *
- * @returns A non-null object.
- */
- private getIgnoreInvitesPolicies(): { [key: string]: any } {
- return this.getPoliciesAndIgnoreInvitesPolicies().ignoreInvitesPolicies;
- }
-
- /**
- * Modify in place the `IGNORE_INVITES_POLICIES` object from account data.
- */
- private async withIgnoreInvitesPolicies(
- cb: (ignoreInvitesPolicies: { [key: string]: any }) => void,
- ): Promise<void> {
- const { policies, ignoreInvitesPolicies } = this.getPoliciesAndIgnoreInvitesPolicies();
- cb(ignoreInvitesPolicies);
- policies[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name] = ignoreInvitesPolicies;
- await this.client.setAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name, policies);
- }
-
- /**
- * As `getIgnoreInvitesPolicies` but also return the `POLICIES_ACCOUNT_EVENT_TYPE`
- * object.
- */
- private getPoliciesAndIgnoreInvitesPolicies(): {
- policies: { [key: string]: any };
- ignoreInvitesPolicies: { [key: string]: any };
- } {
- let policies: IContent = {};
- for (const key of [POLICIES_ACCOUNT_EVENT_TYPE.name, POLICIES_ACCOUNT_EVENT_TYPE.altName]) {
- if (!key) {
- continue;
- }
- const value = this.client.getAccountData(key)?.getContent();
- if (value) {
- policies = value;
- break;
- }
- }
-
- let ignoreInvitesPolicies = {};
- let hasIgnoreInvitesPolicies = false;
- for (const key of [IGNORE_INVITES_ACCOUNT_EVENT_KEY.name, IGNORE_INVITES_ACCOUNT_EVENT_KEY.altName]) {
- if (!key) {
- continue;
- }
- const value = policies[key];
- if (value && typeof value == "object") {
- ignoreInvitesPolicies = value;
- hasIgnoreInvitesPolicies = true;
- break;
- }
- }
- if (!hasIgnoreInvitesPolicies) {
- policies[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name] = ignoreInvitesPolicies;
- }
-
- return { policies, ignoreInvitesPolicies };
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/poll.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/poll.ts
deleted file mode 100644
index 1d4344a..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/poll.ts
+++ /dev/null
@@ -1,268 +0,0 @@
-/*
-Copyright 2023 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 { M_POLL_END, M_POLL_RESPONSE } from "../@types/polls";
-import { MatrixClient } from "../client";
-import { PollStartEvent } from "../extensible_events_v1/PollStartEvent";
-import { MatrixEvent } from "./event";
-import { Relations } from "./relations";
-import { Room } from "./room";
-import { TypedEventEmitter } from "./typed-event-emitter";
-
-export enum PollEvent {
- New = "Poll.new",
- End = "Poll.end",
- Update = "Poll.update",
- Responses = "Poll.Responses",
- Destroy = "Poll.Destroy",
- UndecryptableRelations = "Poll.UndecryptableRelations",
-}
-
-export type PollEventHandlerMap = {
- [PollEvent.Update]: (event: MatrixEvent, poll: Poll) => void;
- [PollEvent.Destroy]: (pollIdentifier: string) => void;
- [PollEvent.End]: () => void;
- [PollEvent.Responses]: (responses: Relations) => void;
- [PollEvent.UndecryptableRelations]: (count: number) => void;
-};
-
-const filterResponseRelations = (
- relationEvents: MatrixEvent[],
- pollEndTimestamp: number,
-): {
- responseEvents: MatrixEvent[];
-} => {
- const responseEvents = relationEvents.filter((event) => {
- if (event.isDecryptionFailure()) {
- return;
- }
- return (
- M_POLL_RESPONSE.matches(event.getType()) &&
- // From MSC3381:
- // "Votes sent on or before the end event's timestamp are valid votes"
- event.getTs() <= pollEndTimestamp
- );
- });
-
- return { responseEvents };
-};
-
-export class Poll extends TypedEventEmitter<Exclude<PollEvent, PollEvent.New>, PollEventHandlerMap> {
- public readonly roomId: string;
- public readonly pollEvent: PollStartEvent;
- private _isFetchingResponses = false;
- private relationsNextBatch: string | undefined;
- private responses: null | Relations = null;
- private endEvent: MatrixEvent | undefined;
- /**
- * Keep track of undecryptable relations
- * As incomplete result sets affect poll results
- */
- private undecryptableRelationEventIds = new Set<string>();
-
- public constructor(public readonly rootEvent: MatrixEvent, private matrixClient: MatrixClient, private room: Room) {
- super();
- if (!this.rootEvent.getRoomId() || !this.rootEvent.getId()) {
- throw new Error("Invalid poll start event.");
- }
- this.roomId = this.rootEvent.getRoomId()!;
- this.pollEvent = this.rootEvent.unstableExtensibleEvent as unknown as PollStartEvent;
- }
-
- public get pollId(): string {
- return this.rootEvent.getId()!;
- }
-
- public get endEventId(): string | undefined {
- return this.endEvent?.getId();
- }
-
- public get isEnded(): boolean {
- return !!this.endEvent;
- }
-
- public get isFetchingResponses(): boolean {
- return this._isFetchingResponses;
- }
-
- public get undecryptableRelationsCount(): number {
- return this.undecryptableRelationEventIds.size;
- }
-
- public async getResponses(): Promise<Relations> {
- // if we have already fetched some responses
- // just return them
- if (this.responses) {
- return this.responses;
- }
-
- // if there is no fetching in progress
- // start fetching
- if (!this.isFetchingResponses) {
- await this.fetchResponses();
- }
- // return whatever responses we got from the first page
- return this.responses!;
- }
-
- /**
- *
- * @param event - event with a relation to the rootEvent
- * @returns void
- */
- public onNewRelation(event: MatrixEvent): void {
- if (M_POLL_END.matches(event.getType()) && this.validateEndEvent(event)) {
- this.endEvent = event;
- this.refilterResponsesOnEnd();
- this.emit(PollEvent.End);
- }
-
- // wait for poll responses to be initialised
- if (!this.responses) {
- return;
- }
-
- const pollEndTimestamp = this.endEvent?.getTs() || Number.MAX_SAFE_INTEGER;
- const { responseEvents } = filterResponseRelations([event], pollEndTimestamp);
-
- this.countUndecryptableEvents([event]);
-
- if (responseEvents.length) {
- responseEvents.forEach((event) => {
- this.responses!.addEvent(event);
- });
-
- this.emit(PollEvent.Responses, this.responses);
- }
- }
-
- private async fetchResponses(): Promise<void> {
- this._isFetchingResponses = true;
-
- // we want:
- // - stable and unstable M_POLL_RESPONSE
- // - stable and unstable M_POLL_END
- // so make one api call and filter by event type client side
- const allRelations = await this.matrixClient.relations(
- this.roomId,
- this.rootEvent.getId()!,
- "m.reference",
- undefined,
- {
- from: this.relationsNextBatch || undefined,
- },
- );
-
- await Promise.all(allRelations.events.map((event) => this.matrixClient.decryptEventIfNeeded(event)));
-
- const responses =
- this.responses ||
- new Relations("m.reference", M_POLL_RESPONSE.name, this.matrixClient, [M_POLL_RESPONSE.altName!]);
-
- const pollEndEvent = allRelations.events.find((event) => M_POLL_END.matches(event.getType()));
-
- if (this.validateEndEvent(pollEndEvent)) {
- this.endEvent = pollEndEvent;
- this.refilterResponsesOnEnd();
- this.emit(PollEvent.End);
- }
-
- const pollCloseTimestamp = this.endEvent?.getTs() || Number.MAX_SAFE_INTEGER;
-
- const { responseEvents } = filterResponseRelations(allRelations.events, pollCloseTimestamp);
-
- responseEvents.forEach((event) => {
- responses.addEvent(event);
- });
-
- this.relationsNextBatch = allRelations.nextBatch ?? undefined;
- this.responses = responses;
- this.countUndecryptableEvents(allRelations.events);
-
- // while there are more pages of relations
- // fetch them
- if (this.relationsNextBatch) {
- // don't await
- // we want to return the first page as soon as possible
- this.fetchResponses();
- } else {
- // no more pages
- this._isFetchingResponses = false;
- }
-
- // emit after updating _isFetchingResponses state
- this.emit(PollEvent.Responses, this.responses);
- }
-
- /**
- * Only responses made before the poll ended are valid
- * Refilter after an end event is recieved
- * To ensure responses are valid
- */
- private refilterResponsesOnEnd(): void {
- if (!this.responses) {
- return;
- }
-
- const pollEndTimestamp = this.endEvent?.getTs() || Number.MAX_SAFE_INTEGER;
- this.responses.getRelations().forEach((event) => {
- if (event.getTs() > pollEndTimestamp) {
- this.responses?.removeEvent(event);
- }
- });
-
- this.emit(PollEvent.Responses, this.responses);
- }
-
- private countUndecryptableEvents = (events: MatrixEvent[]): void => {
- const undecryptableEventIds = events
- .filter((event) => event.isDecryptionFailure())
- .map((event) => event.getId()!);
-
- const previousCount = this.undecryptableRelationsCount;
- this.undecryptableRelationEventIds = new Set([...this.undecryptableRelationEventIds, ...undecryptableEventIds]);
-
- if (this.undecryptableRelationsCount !== previousCount) {
- this.emit(PollEvent.UndecryptableRelations, this.undecryptableRelationsCount);
- }
- };
-
- private validateEndEvent(endEvent?: MatrixEvent): boolean {
- if (!endEvent) {
- return false;
- }
- /**
- * Repeated end events are ignored -
- * only the first (valid) closure event by origin_server_ts is counted.
- */
- if (this.endEvent && this.endEvent.getTs() < endEvent.getTs()) {
- return false;
- }
-
- /**
- * MSC3381
- * If a m.poll.end event is received from someone other than the poll creator or user with permission to redact
- * others' messages in the room, the event must be ignored by clients due to being invalid.
- */
- const roomCurrentState = this.room.currentState;
- const endEventSender = endEvent.getSender();
- return (
- !!endEventSender &&
- (endEventSender === this.rootEvent.getSender() ||
- roomCurrentState.maySendRedactionForEvent(this.rootEvent, endEventSender))
- );
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/read-receipt.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/read-receipt.ts
deleted file mode 100644
index 5858fe5..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/read-receipt.ts
+++ /dev/null
@@ -1,312 +0,0 @@
-/*
-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 {
- CachedReceipt,
- MAIN_ROOM_TIMELINE,
- Receipt,
- ReceiptCache,
- ReceiptType,
- WrappedReceipt,
-} from "../@types/read_receipts";
-import { ListenerMap, TypedEventEmitter } from "./typed-event-emitter";
-import * as utils from "../utils";
-import { MatrixEvent } from "./event";
-import { EventType } from "../@types/event";
-import { EventTimelineSet } from "./event-timeline-set";
-import { MapWithDefault } from "../utils";
-import { NotificationCountType } from "./room";
-
-export function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: ReceiptType): MatrixEvent {
- return new MatrixEvent({
- content: {
- [event.getId()!]: {
- [receiptType]: {
- [userId]: {
- ts: event.getTs(),
- thread_id: event.threadRootId ?? MAIN_ROOM_TIMELINE,
- },
- },
- },
- },
- type: EventType.Receipt,
- room_id: event.getRoomId(),
- });
-}
-
-const ReceiptPairRealIndex = 0;
-const ReceiptPairSyntheticIndex = 1;
-
-export abstract class ReadReceipt<
- Events extends string,
- Arguments extends ListenerMap<Events>,
- SuperclassArguments extends ListenerMap<any> = Arguments,
-> extends TypedEventEmitter<Events, Arguments, SuperclassArguments> {
- // receipts should clobber based on receipt_type and user_id pairs hence
- // the form of this structure. This is sub-optimal for the exposed APIs
- // which pass in an event ID and get back some receipts, so we also store
- // a pre-cached list for this purpose.
- // Map: receipt type → user Id → receipt
- private receipts = new MapWithDefault<string, Map<string, [WrappedReceipt | null, WrappedReceipt | null]>>(
- () => new Map(),
- );
- private receiptCacheByEventId: ReceiptCache = new Map();
-
- public abstract getUnfilteredTimelineSet(): EventTimelineSet;
- public abstract timeline: MatrixEvent[];
-
- /**
- * 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,
- ): WrappedReceipt | null {
- const [realReceipt, syntheticReceipt] = this.receipts.get(receiptType)?.get(userId) ?? [null, null];
- if (ignoreSynthesized) {
- return realReceipt;
- }
-
- return syntheticReceipt ?? realReceipt;
- }
-
- /**
- * Get the ID of the event that a given user has read up to, or null if we
- * have received no read receipts from them.
- * @param userId - The user ID to get read receipt event ID for
- * @param ignoreSynthesized - If true, return only receipts that have been
- * sent by the server, not implicit ones generated
- * by the JS SDK.
- * @returns ID of the latest event that the given user has read, or null.
- */
- public getEventReadUpTo(userId: string, ignoreSynthesized = false): string | null {
- // XXX: This is very very ugly and I hope I won't have to ever add a new
- // receipt type here again. IMHO this should be done by the server in
- // some more intelligent manner or the client should just use timestamps
-
- const timelineSet = this.getUnfilteredTimelineSet();
- const publicReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.Read);
- const privateReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.ReadPrivate);
-
- // If we have both, compare them
- let comparison: number | null | 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 && publicReadReceipt?.data?.ts && privateReadReceipt?.data?.ts) {
- 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) ?? null;
- }
-
- public addReceiptToStructure(
- eventId: string,
- receiptType: ReceiptType,
- userId: string,
- receipt: Receipt,
- synthetic: boolean,
- ): void {
- const receiptTypesMap = this.receipts.getOrCreate(receiptType);
- let pair = receiptTypesMap.get(userId);
-
- if (!pair) {
- pair = [null, null];
- receiptTypesMap.set(userId, pair);
- }
-
- let existingReceipt = pair[ReceiptPairRealIndex];
- if (synthetic) {
- existingReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
- }
-
- if (existingReceipt) {
- // we only want to add this receipt if we think it is later than the one we already have.
- // This is managed server-side, but because we synthesize RRs locally we have to do it here too.
- const ordering = this.getUnfilteredTimelineSet().compareEventOrdering(existingReceipt.eventId, eventId);
- if (ordering !== null && ordering >= 0) {
- return;
- }
- }
-
- const wrappedReceipt: WrappedReceipt = {
- eventId,
- data: receipt,
- };
-
- const realReceipt = synthetic ? pair[ReceiptPairRealIndex] : wrappedReceipt;
- const syntheticReceipt = synthetic ? wrappedReceipt : pair[ReceiptPairSyntheticIndex];
-
- let ordering: number | null = null;
- if (realReceipt && syntheticReceipt) {
- ordering = this.getUnfilteredTimelineSet().compareEventOrdering(
- realReceipt.eventId,
- syntheticReceipt.eventId,
- );
- }
-
- const preferSynthetic = ordering === null || ordering < 0;
-
- // we don't bother caching just real receipts by event ID as there's nothing that would read it.
- // Take the current cached receipt before we overwrite the pair elements.
- const cachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
-
- if (synthetic && preferSynthetic) {
- pair[ReceiptPairSyntheticIndex] = wrappedReceipt;
- } else if (!synthetic) {
- pair[ReceiptPairRealIndex] = wrappedReceipt;
-
- if (!preferSynthetic) {
- pair[ReceiptPairSyntheticIndex] = null;
- }
- }
-
- const newCachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
- if (cachedReceipt === newCachedReceipt) return;
-
- // clean up any previous cache entry
- if (cachedReceipt && this.receiptCacheByEventId.get(cachedReceipt.eventId)) {
- const previousEventId = cachedReceipt.eventId;
- // Remove the receipt we're about to clobber out of existence from the cache
- this.receiptCacheByEventId.set(
- previousEventId,
- this.receiptCacheByEventId.get(previousEventId)!.filter((r) => {
- return r.type !== receiptType || r.userId !== userId;
- }),
- );
-
- if (this.receiptCacheByEventId.get(previousEventId)!.length < 1) {
- this.receiptCacheByEventId.delete(previousEventId); // clean up the cache keys
- }
- }
-
- // cache the new one
- if (!this.receiptCacheByEventId.get(eventId)) {
- this.receiptCacheByEventId.set(eventId, []);
- }
- this.receiptCacheByEventId.get(eventId)!.push({
- userId: userId,
- type: receiptType as ReceiptType,
- data: receipt,
- });
- }
-
- /**
- * Get a list of receipts for the given event.
- * @param event - the event to get receipts for
- * @returns A list of receipts with a userId, type and data keys or
- * an empty list.
- */
- public getReceiptsForEvent(event: MatrixEvent): CachedReceipt[] {
- return this.receiptCacheByEventId.get(event.getId()!) || [];
- }
-
- public abstract addReceipt(event: MatrixEvent, synthetic: boolean): void;
-
- public abstract setUnread(type: NotificationCountType, count: number): void;
-
- /**
- * This issue should also be addressed on synapse's side and is tracked as part
- * of https://github.com/matrix-org/synapse/issues/14837
- *
- * Retrieves the read receipt for the logged in user and checks if it matches
- * the last event in the room and whether that event originated from the logged
- * in user.
- * Under those conditions we can consider the context as read. This is useful
- * because we never send read receipts against our own events
- * @param userId - the logged in user
- */
- public fixupNotifications(userId: string): void {
- const receipt = this.getReadReceiptForUserId(userId, false);
-
- const lastEvent = this.timeline[this.timeline.length - 1];
- if (lastEvent && receipt?.eventId === lastEvent.getId() && userId === lastEvent.getSender()) {
- this.setUnread(NotificationCountType.Total, 0);
- this.setUnread(NotificationCountType.Highlight, 0);
- }
- }
-
- /**
- * Add a temporary local-echo receipt to the room to reflect in the
- * client the fact that we've sent one.
- * @param userId - The user ID if the receipt sender
- * @param e - The event that is to be acknowledged
- * @param receiptType - The type of receipt
- */
- public addLocalEchoReceipt(userId: string, e: MatrixEvent, receiptType: ReceiptType): void {
- this.addReceipt(synthesizeReceipt(userId, e, receiptType), true);
- }
-
- /**
- * Get a list of user IDs who have <b>read up to</b> the given event.
- * @param event - the event to get read receipts for.
- * @returns A list of user IDs.
- */
- public getUsersReadUpTo(event: MatrixEvent): string[] {
- return this.getReceiptsForEvent(event)
- .filter(function (receipt) {
- return utils.isSupportedReceiptType(receipt.type);
- })
- .map(function (receipt) {
- return receipt.userId;
- });
- }
-
- /**
- * Determines if the given user has read a particular event ID with the known
- * history of the room. This is not a definitive check as it relies only on
- * what is available to the room at the time of execution.
- * @param userId - The user ID to check the read state of.
- * @param eventId - The event ID to check if the user read.
- * @returns True if the user has read the event, false otherwise.
- */
- public hasUserReadEvent(userId: string, eventId: string): boolean {
- const readUpToId = this.getEventReadUpTo(userId, false);
- if (readUpToId === eventId) return true;
-
- if (
- this.timeline?.length &&
- this.timeline[this.timeline.length - 1].getSender() &&
- this.timeline[this.timeline.length - 1].getSender() === userId
- ) {
- // It doesn't matter where the event is in the timeline, the user has read
- // it because they've sent the latest event.
- return true;
- }
-
- for (let i = this.timeline?.length - 1; i >= 0; --i) {
- const ev = this.timeline[i];
-
- // If we encounter the target event first, the user hasn't read it
- // however if we encounter the readUpToId first then the user has read
- // it. These rules apply because we're iterating bottom-up.
- if (ev.getId() === eventId) return false;
- if (ev.getId() === readUpToId) return true;
- }
-
- // We don't know if the user has read it, so assume not.
- return false;
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/related-relations.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/related-relations.ts
deleted file mode 100644
index a005169..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/related-relations.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
-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 { Relations, RelationsEvent, EventHandlerMap } from "./relations";
-import { MatrixEvent } from "./event";
-import { Listener } from "./typed-event-emitter";
-
-export class RelatedRelations {
- private relations: Relations[];
-
- public constructor(relations: Relations[]) {
- this.relations = relations.filter((r) => !!r);
- }
-
- public getRelations(): MatrixEvent[] {
- return this.relations.reduce<MatrixEvent[]>((c, p) => [...c, ...p.getRelations()], []);
- }
-
- public on<T extends RelationsEvent>(ev: T, fn: Listener<RelationsEvent, EventHandlerMap, T>): void {
- this.relations.forEach((r) => r.on(ev, fn));
- }
-
- public off<T extends RelationsEvent>(ev: T, fn: Listener<RelationsEvent, EventHandlerMap, T>): void {
- this.relations.forEach((r) => r.off(ev, fn));
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/relations-container.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/relations-container.ts
deleted file mode 100644
index d328b1c..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/relations-container.ts
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
-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 { Relations } from "./relations";
-import { EventType, RelationType } from "../@types/event";
-import { EventStatus, MatrixEvent, MatrixEventEvent } from "./event";
-import { EventTimelineSet } from "./event-timeline-set";
-import { MatrixClient } from "../client";
-import { Room } from "./room";
-
-export class RelationsContainer {
- // A tree of objects to access a set of related children for an event, as in:
- // this.relations.get(parentEventId).get(relationType).get(relationEventType)
- private relations = new Map<string, Map<RelationType | string, Map<EventType | string, Relations>>>();
-
- public constructor(private readonly client: MatrixClient, private readonly room?: Room) {}
-
- /**
- * Get a collection of child events to a given event in this timeline set.
- *
- * @param eventId - The ID of the event that you'd like to access child events for.
- * For example, with annotations, this would be the ID of the event being annotated.
- * @param relationType - The type of relationship involved, such as "m.annotation", "m.reference", "m.replace", etc.
- * @param eventType - The relation event's type, such as "m.reaction", etc.
- * @throws If `eventId</code>, <code>relationType</code> or <code>eventType`
- * are not valid.
- *
- * @returns
- * A container for relation events or undefined if there are no relation events for
- * the relationType.
- */
- public getChildEventsForEvent(
- eventId: string,
- relationType: RelationType | string,
- eventType: EventType | string,
- ): Relations | undefined {
- return this.relations.get(eventId)?.get(relationType)?.get(eventType);
- }
-
- public getAllChildEventsForEvent(parentEventId: string): MatrixEvent[] {
- const relationsForEvent =
- this.relations.get(parentEventId) ?? new Map<RelationType | string, Map<EventType | string, Relations>>();
- const events: MatrixEvent[] = [];
- for (const relationsRecord of relationsForEvent.values()) {
- for (const relations of relationsRecord.values()) {
- events.push(...relations.getRelations());
- }
- }
- return events;
- }
-
- /**
- * Set an event as the target event if any Relations exist for it already.
- * Child events can point to other child events as their parent, so this method may be
- * called for events which are also logically child events.
- *
- * @param event - The event to check as relation target.
- */
- public aggregateParentEvent(event: MatrixEvent): void {
- const relationsForEvent = this.relations.get(event.getId()!);
- if (!relationsForEvent) return;
-
- for (const relationsWithRelType of relationsForEvent.values()) {
- for (const relationsWithEventType of relationsWithRelType.values()) {
- relationsWithEventType.setTargetEvent(event);
- }
- }
- }
-
- /**
- * Add relation events to the relevant relation collection.
- *
- * @param event - The new child event to be aggregated.
- * @param timelineSet - The event timeline set within which to search for the related event if any.
- */
- public aggregateChildEvent(event: MatrixEvent, timelineSet?: EventTimelineSet): void {
- if (event.isRedacted() || event.status === EventStatus.CANCELLED) {
- return;
- }
-
- const relation = event.getRelation();
- if (!relation) return;
-
- const onEventDecrypted = (): void => {
- if (event.isDecryptionFailure()) {
- // This could for example happen if the encryption keys are not yet available.
- // The event may still be decrypted later. Register the listener again.
- event.once(MatrixEventEvent.Decrypted, onEventDecrypted);
- return;
- }
-
- this.aggregateChildEvent(event, timelineSet);
- };
-
- // If the event is currently encrypted, wait until it has been decrypted.
- if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) {
- event.once(MatrixEventEvent.Decrypted, onEventDecrypted);
- return;
- }
-
- const { event_id: relatesToEventId, rel_type: relationType } = relation;
- const eventType = event.getType();
-
- let relationsForEvent = this.relations.get(relatesToEventId!);
- if (!relationsForEvent) {
- relationsForEvent = new Map<RelationType | string, Map<EventType | string, Relations>>();
- this.relations.set(relatesToEventId!, relationsForEvent);
- }
-
- let relationsWithRelType = relationsForEvent.get(relationType!);
- if (!relationsWithRelType) {
- relationsWithRelType = new Map<EventType | string, Relations>();
- relationsForEvent.set(relationType!, relationsWithRelType);
- }
-
- let relationsWithEventType = relationsWithRelType.get(eventType);
- if (!relationsWithEventType) {
- relationsWithEventType = new Relations(relationType!, eventType, this.client);
- relationsWithRelType.set(eventType, relationsWithEventType);
-
- const room = this.room ?? timelineSet?.room;
- const relatesToEvent =
- timelineSet?.findEventById(relatesToEventId!) ??
- room?.findEventById(relatesToEventId!) ??
- room?.getPendingEvent(relatesToEventId!);
- if (relatesToEvent) {
- relationsWithEventType.setTargetEvent(relatesToEvent);
- }
- }
-
- relationsWithEventType.addEvent(event);
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/relations.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/relations.ts
deleted file mode 100644
index d2b637c..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/relations.ts
+++ /dev/null
@@ -1,368 +0,0 @@
-/*
-Copyright 2019, 2021, 2023 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 { EventStatus, IAggregatedRelation, MatrixEvent, MatrixEventEvent } from "./event";
-import { logger } from "../logger";
-import { RelationType } from "../@types/event";
-import { TypedEventEmitter } from "./typed-event-emitter";
-import { MatrixClient } from "../client";
-import { Room } from "./room";
-
-export enum RelationsEvent {
- Add = "Relations.add",
- Remove = "Relations.remove",
- Redaction = "Relations.redaction",
-}
-
-export type EventHandlerMap = {
- [RelationsEvent.Add]: (event: MatrixEvent) => void;
- [RelationsEvent.Remove]: (event: MatrixEvent) => void;
- [RelationsEvent.Redaction]: (event: MatrixEvent) => void;
-};
-
-const matchesEventType = (eventType: string, targetEventType: string, altTargetEventTypes: string[] = []): boolean =>
- [targetEventType, ...altTargetEventTypes].includes(eventType);
-
-/**
- * A container for relation events that supports easy access to common ways of
- * aggregating such events. Each instance holds events that of a single relation
- * type and event type. All of the events also relate to the same original event.
- *
- * The typical way to get one of these containers is via
- * EventTimelineSet#getRelationsForEvent.
- */
-export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap> {
- private relationEventIds = new Set<string>();
- private relations = new Set<MatrixEvent>();
- private annotationsByKey: Record<string, Set<MatrixEvent>> = {};
- private annotationsBySender: Record<string, Set<MatrixEvent>> = {};
- private sortedAnnotationsByKey: [string, Set<MatrixEvent>][] = [];
- private targetEvent: MatrixEvent | null = null;
- private creationEmitted = false;
- private readonly client: MatrixClient;
-
- /**
- * @param relationType - The type of relation involved, such as "m.annotation", "m.reference", "m.replace", etc.
- * @param eventType - The relation event's type, such as "m.reaction", etc.
- * @param client - The client which created this instance. For backwards compatibility also accepts a Room.
- * @param altEventTypes - alt event types for relation events, for example to support unstable prefixed event types
- */
- public constructor(
- public readonly relationType: RelationType | string,
- public readonly eventType: string,
- client: MatrixClient | Room,
- public readonly altEventTypes?: string[],
- ) {
- super();
- this.client = client instanceof Room ? client.client : client;
- }
-
- /**
- * Add relation events to this collection.
- *
- * @param event - The new relation event to be added.
- */
- public async addEvent(event: MatrixEvent): Promise<void> {
- if (this.relationEventIds.has(event.getId()!)) {
- return;
- }
-
- const relation = event.getRelation();
- if (!relation) {
- logger.error("Event must have relation info");
- return;
- }
-
- const relationType = relation.rel_type;
- const eventType = event.getType();
-
- if (this.relationType !== relationType || !matchesEventType(eventType, this.eventType, this.altEventTypes)) {
- logger.error("Event relation info doesn't match this container");
- return;
- }
-
- // If the event is in the process of being sent, listen for cancellation
- // so we can remove the event from the collection.
- if (event.isSending()) {
- event.on(MatrixEventEvent.Status, this.onEventStatus);
- }
-
- this.relations.add(event);
- this.relationEventIds.add(event.getId()!);
-
- if (this.relationType === RelationType.Annotation) {
- this.addAnnotationToAggregation(event);
- } else if (this.relationType === RelationType.Replace && this.targetEvent && !this.targetEvent.isState()) {
- const lastReplacement = await this.getLastReplacement();
- this.targetEvent.makeReplaced(lastReplacement!);
- }
-
- event.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
-
- this.emit(RelationsEvent.Add, event);
-
- this.maybeEmitCreated();
- }
-
- /**
- * Remove relation event from this collection.
- *
- * @param event - The relation event to remove.
- */
- public async removeEvent(event: MatrixEvent): Promise<void> {
- if (!this.relations.has(event)) {
- return;
- }
-
- this.relations.delete(event);
-
- if (this.relationType === RelationType.Annotation) {
- this.removeAnnotationFromAggregation(event);
- } else if (this.relationType === RelationType.Replace && this.targetEvent && !this.targetEvent.isState()) {
- const lastReplacement = await this.getLastReplacement();
- this.targetEvent.makeReplaced(lastReplacement!);
- }
-
- this.emit(RelationsEvent.Remove, event);
- }
-
- /**
- * Listens for event status changes to remove cancelled events.
- *
- * @param event - The event whose status has changed
- * @param status - The new status
- */
- private onEventStatus = (event: MatrixEvent, status: EventStatus | null): void => {
- if (!event.isSending()) {
- // Sending is done, so we don't need to listen anymore
- event.removeListener(MatrixEventEvent.Status, this.onEventStatus);
- return;
- }
- if (status !== EventStatus.CANCELLED) {
- return;
- }
- // Event was cancelled, remove from the collection
- event.removeListener(MatrixEventEvent.Status, this.onEventStatus);
- this.removeEvent(event);
- };
-
- /**
- * Get all relation events in this collection.
- *
- * These are currently in the order of insertion to this collection, which
- * won't match timeline order in the case of scrollback.
- * TODO: Tweak `addEvent` to insert correctly for scrollback.
- *
- * Relation events in insertion order.
- */
- public getRelations(): MatrixEvent[] {
- return [...this.relations];
- }
-
- private addAnnotationToAggregation(event: MatrixEvent): void {
- const { key } = event.getRelation() ?? {};
- if (!key) return;
-
- let eventsForKey = this.annotationsByKey[key];
- if (!eventsForKey) {
- eventsForKey = this.annotationsByKey[key] = new Set();
- this.sortedAnnotationsByKey.push([key, eventsForKey]);
- }
- // Add the new event to the set for this key
- eventsForKey.add(event);
- // Re-sort the [key, events] pairs in descending order of event count
- this.sortedAnnotationsByKey.sort((a, b) => {
- const aEvents = a[1];
- const bEvents = b[1];
- return bEvents.size - aEvents.size;
- });
-
- const sender = event.getSender()!;
- let eventsFromSender = this.annotationsBySender[sender];
- if (!eventsFromSender) {
- eventsFromSender = this.annotationsBySender[sender] = new Set();
- }
- // Add the new event to the set for this sender
- eventsFromSender.add(event);
- }
-
- private removeAnnotationFromAggregation(event: MatrixEvent): void {
- const { key } = event.getRelation() ?? {};
- if (!key) return;
-
- const eventsForKey = this.annotationsByKey[key];
- if (eventsForKey) {
- eventsForKey.delete(event);
-
- // Re-sort the [key, events] pairs in descending order of event count
- this.sortedAnnotationsByKey.sort((a, b) => {
- const aEvents = a[1];
- const bEvents = b[1];
- return bEvents.size - aEvents.size;
- });
- }
-
- const sender = event.getSender()!;
- const eventsFromSender = this.annotationsBySender[sender];
- if (eventsFromSender) {
- eventsFromSender.delete(event);
- }
- }
-
- /**
- * For relations that have been redacted, we want to remove them from
- * aggregation data sets and emit an update event.
- *
- * To do so, we listen for `Event.beforeRedaction`, which happens:
- * - after the server accepted the redaction and remote echoed back to us
- * - before the original event has been marked redacted in the client
- *
- * @param redactedEvent - The original relation event that is about to be redacted.
- */
- private onBeforeRedaction = async (redactedEvent: MatrixEvent): Promise<void> => {
- if (!this.relations.has(redactedEvent)) {
- return;
- }
-
- this.relations.delete(redactedEvent);
-
- if (this.relationType === RelationType.Annotation) {
- // Remove the redacted annotation from aggregation by key
- this.removeAnnotationFromAggregation(redactedEvent);
- } else if (this.relationType === RelationType.Replace && this.targetEvent && !this.targetEvent.isState()) {
- const lastReplacement = await this.getLastReplacement();
- this.targetEvent.makeReplaced(lastReplacement!);
- }
-
- redactedEvent.removeListener(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
-
- this.emit(RelationsEvent.Redaction, redactedEvent);
- };
-
- /**
- * Get all events in this collection grouped by key and sorted by descending
- * event count in each group.
- *
- * This is currently only supported for the annotation relation type.
- *
- * An array of [key, events] pairs sorted by descending event count.
- * The events are stored in a Set (which preserves insertion order).
- */
- public getSortedAnnotationsByKey(): [string, Set<MatrixEvent>][] | null {
- if (this.relationType !== RelationType.Annotation) {
- // Other relation types are not grouped currently.
- return null;
- }
-
- return this.sortedAnnotationsByKey;
- }
-
- /**
- * Get all events in this collection grouped by sender.
- *
- * This is currently only supported for the annotation relation type.
- *
- * An object with each relation sender as a key and the matching Set of
- * events for that sender as a value.
- */
- public getAnnotationsBySender(): Record<string, Set<MatrixEvent>> | null {
- if (this.relationType !== RelationType.Annotation) {
- // Other relation types are not grouped currently.
- return null;
- }
-
- return this.annotationsBySender;
- }
-
- /**
- * Returns the most recent (and allowed) m.replace relation, if any.
- *
- * This is currently only supported for the m.replace relation type,
- * once the target event is known, see `addEvent`.
- */
- public async getLastReplacement(): Promise<MatrixEvent | null> {
- if (this.relationType !== RelationType.Replace) {
- // Aggregating on last only makes sense for this relation type
- return null;
- }
- if (!this.targetEvent) {
- // Don't know which replacements to accept yet.
- // This method shouldn't be called before the original
- // event is known anyway.
- return null;
- }
-
- // the all-knowning server tells us that the event at some point had
- // this timestamp for its replacement, so any following replacement should definitely not be less
- const replaceRelation = this.targetEvent.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
- const minTs = replaceRelation?.origin_server_ts;
-
- const lastReplacement = this.getRelations().reduce<MatrixEvent | null>((last, event) => {
- if (event.getSender() !== this.targetEvent!.getSender()) {
- return last;
- }
- if (minTs && minTs > event.getTs()) {
- return last;
- }
- if (last && last.getTs() > event.getTs()) {
- return last;
- }
- return event;
- }, null);
-
- if (lastReplacement?.shouldAttemptDecryption() && this.client.isCryptoEnabled()) {
- await lastReplacement.attemptDecryption(this.client.crypto!);
- } else if (lastReplacement?.isBeingDecrypted()) {
- await lastReplacement.getDecryptionPromise();
- }
-
- return lastReplacement;
- }
-
- /*
- * @param targetEvent - the event the relations are related to.
- */
- public async setTargetEvent(event: MatrixEvent): Promise<void> {
- if (this.targetEvent) {
- return;
- }
- this.targetEvent = event;
-
- if (this.relationType === RelationType.Replace && !this.targetEvent.isState()) {
- const replacement = await this.getLastReplacement();
- // this is the initial update, so only call it if we already have something
- // to not emit Event.replaced needlessly
- if (replacement) {
- this.targetEvent.makeReplaced(replacement);
- }
- }
-
- this.maybeEmitCreated();
- }
-
- private maybeEmitCreated(): void {
- if (this.creationEmitted) {
- return;
- }
- // Only emit we're "created" once we have a target event instance _and_
- // at least one related event.
- if (!this.targetEvent || !this.relations.size) {
- return;
- }
- this.creationEmitted = true;
- this.targetEvent.emit(MatrixEventEvent.RelationsCreated, this.relationType, this.eventType);
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-member.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-member.ts
deleted file mode 100644
index 116a93b..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-member.ts
+++ /dev/null
@@ -1,453 +0,0 @@
-/*
-Copyright 2015 - 2021 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 { getHttpUriForMxc } from "../content-repo";
-import * as utils from "../utils";
-import { User } from "./user";
-import { MatrixEvent } from "./event";
-import { RoomState } from "./room-state";
-import { logger } from "../logger";
-import { TypedEventEmitter } from "./typed-event-emitter";
-import { EventType } from "../@types/event";
-
-export enum RoomMemberEvent {
- Membership = "RoomMember.membership",
- Name = "RoomMember.name",
- PowerLevel = "RoomMember.powerLevel",
- Typing = "RoomMember.typing",
-}
-
-export type RoomMemberEventHandlerMap = {
- /**
- * Fires whenever any room member's membership state changes.
- * @param event - The matrix event which caused this event to fire.
- * @param member - The member whose RoomMember.membership changed.
- * @param oldMembership - The previous membership state. Null if it's a new member.
- * @example
- * ```
- * matrixClient.on("RoomMember.membership", function(event, member, oldMembership){
- * var newState = member.membership;
- * });
- * ```
- */
- [RoomMemberEvent.Membership]: (event: MatrixEvent, member: RoomMember, oldMembership?: string) => void;
- /**
- * Fires whenever any room member's name changes.
- * @param event - The matrix event which caused this event to fire.
- * @param member - The member whose RoomMember.name changed.
- * @param oldName - The previous name. Null if the member didn't have a name previously.
- * @example
- * ```
- * matrixClient.on("RoomMember.name", function(event, member){
- * var newName = member.name;
- * });
- * ```
- */
- [RoomMemberEvent.Name]: (event: MatrixEvent, member: RoomMember, oldName: string | null) => void;
- /**
- * Fires whenever any room member's power level changes.
- * @param event - The matrix event which caused this event to fire.
- * @param member - The member whose RoomMember.powerLevel changed.
- * @example
- * ```
- * matrixClient.on("RoomMember.powerLevel", function(event, member){
- * var newPowerLevel = member.powerLevel;
- * var newNormPowerLevel = member.powerLevelNorm;
- * });
- * ```
- */
- [RoomMemberEvent.PowerLevel]: (event: MatrixEvent, member: RoomMember) => void;
- /**
- * Fires whenever any room member's typing state changes.
- * @param event - The matrix event which caused this event to fire.
- * @param member - The member whose RoomMember.typing changed.
- * @example
- * ```
- * matrixClient.on("RoomMember.typing", function(event, member){
- * var isTyping = member.typing;
- * });
- * ```
- */
- [RoomMemberEvent.Typing]: (event: MatrixEvent, member: RoomMember) => void;
-};
-
-export class RoomMember extends TypedEventEmitter<RoomMemberEvent, RoomMemberEventHandlerMap> {
- private _isOutOfBand = false;
- private modified = -1;
- public requestedProfileInfo = false; // used by sync.ts
-
- // XXX these should be read-only
- /**
- * True if the room member is currently typing.
- */
- public typing = false;
- /**
- * The human-readable name for this room member. This will be
- * disambiguated with a suffix of " (\@user_id:matrix.org)" if another member shares the
- * same displayname.
- */
- public name: string;
- /**
- * The ambiguous displayname of this room member.
- */
- public rawDisplayName: string;
- /**
- * The power level for this room member.
- */
- public powerLevel = 0;
- /**
- * The normalised power level (0-100) for this room member.
- */
- public powerLevelNorm = 0;
- /**
- * The User object for this room member, if one exists.
- */
- public user?: User;
- /**
- * The membership state for this room member e.g. 'join'.
- */
- public membership?: string;
- /**
- * True if the member's name is disambiguated.
- */
- public disambiguate = false;
- /**
- * The events describing this RoomMember.
- */
- public events: {
- /**
- * The m.room.member event for this RoomMember.
- */
- member?: MatrixEvent;
- } = {};
-
- /**
- * Construct a new room member.
- *
- * @param roomId - The room ID of the member.
- * @param userId - The user ID of the member.
- */
- public constructor(public readonly roomId: string, public readonly userId: string) {
- super();
-
- this.name = userId;
- this.rawDisplayName = userId;
- this.updateModifiedTime();
- }
-
- /**
- * Mark the member as coming from a channel that is not sync
- */
- public markOutOfBand(): void {
- this._isOutOfBand = true;
- }
-
- /**
- * @returns does the member come from a channel that is not sync?
- * This is used to store the member seperately
- * from the sync state so it available across browser sessions.
- */
- public isOutOfBand(): boolean {
- return this._isOutOfBand;
- }
-
- /**
- * Update this room member's membership event. May fire "RoomMember.name" if
- * this event updates this member's name.
- * @param event - The `m.room.member` event
- * @param roomState - Optional. The room state to take into account
- * when calculating (e.g. for disambiguating users with the same name).
- *
- * @remarks
- * Fires {@link RoomMemberEvent.Name}
- * Fires {@link RoomMemberEvent.Membership}
- */
- public setMembershipEvent(event: MatrixEvent, roomState?: RoomState): void {
- const displayName = event.getDirectionalContent().displayname ?? "";
-
- if (event.getType() !== EventType.RoomMember) {
- return;
- }
-
- this._isOutOfBand = false;
-
- this.events.member = event;
-
- const oldMembership = this.membership;
- this.membership = event.getDirectionalContent().membership;
- if (this.membership === undefined) {
- // logging to diagnose https://github.com/vector-im/element-web/issues/20962
- // (logs event content, although only of membership events)
- logger.trace(
- `membership event with membership undefined (forwardLooking: ${event.forwardLooking})!`,
- event.getContent(),
- `prevcontent is `,
- event.getPrevContent(),
- );
- }
-
- this.disambiguate = shouldDisambiguate(this.userId, displayName, roomState);
-
- const oldName = this.name;
- this.name = calculateDisplayName(this.userId, displayName, this.disambiguate);
-
- // not quite raw: we strip direction override chars so it can safely be inserted into
- // blocks of text without breaking the text direction
- this.rawDisplayName = utils.removeDirectionOverrideChars(event.getDirectionalContent().displayname ?? "");
- if (!this.rawDisplayName || !utils.removeHiddenChars(this.rawDisplayName)) {
- this.rawDisplayName = this.userId;
- }
-
- if (oldMembership !== this.membership) {
- this.updateModifiedTime();
- this.emit(RoomMemberEvent.Membership, event, this, oldMembership);
- }
- if (oldName !== this.name) {
- this.updateModifiedTime();
- this.emit(RoomMemberEvent.Name, event, this, oldName);
- }
- }
-
- /**
- * Update this room member's power level event. May fire
- * "RoomMember.powerLevel" if this event updates this member's power levels.
- * @param powerLevelEvent - The `m.room.power_levels` event
- *
- * @remarks
- * Fires {@link RoomMemberEvent.PowerLevel}
- */
- public setPowerLevelEvent(powerLevelEvent: MatrixEvent): void {
- if (powerLevelEvent.getType() !== EventType.RoomPowerLevels || powerLevelEvent.getStateKey() !== "") {
- return;
- }
-
- const evContent = powerLevelEvent.getDirectionalContent();
-
- let maxLevel = evContent.users_default || 0;
- const users: { [userId: string]: number } = evContent.users || {};
- Object.values(users).forEach((lvl: number) => {
- maxLevel = Math.max(maxLevel, lvl);
- });
- const oldPowerLevel = this.powerLevel;
- const oldPowerLevelNorm = this.powerLevelNorm;
-
- if (users[this.userId] !== undefined && Number.isInteger(users[this.userId])) {
- this.powerLevel = users[this.userId];
- } else if (evContent.users_default !== undefined) {
- this.powerLevel = evContent.users_default;
- } else {
- this.powerLevel = 0;
- }
- this.powerLevelNorm = 0;
- if (maxLevel > 0) {
- this.powerLevelNorm = (this.powerLevel * 100) / maxLevel;
- }
-
- // emit for changes in powerLevelNorm as well (since the app will need to
- // redraw everyone's level if the max has changed)
- if (oldPowerLevel !== this.powerLevel || oldPowerLevelNorm !== this.powerLevelNorm) {
- this.updateModifiedTime();
- this.emit(RoomMemberEvent.PowerLevel, powerLevelEvent, this);
- }
- }
-
- /**
- * Update this room member's typing event. May fire "RoomMember.typing" if
- * this event changes this member's typing state.
- * @param event - The typing event
- *
- * @remarks
- * Fires {@link RoomMemberEvent.Typing}
- */
- public setTypingEvent(event: MatrixEvent): void {
- if (event.getType() !== "m.typing") {
- return;
- }
- const oldTyping = this.typing;
- this.typing = false;
- const typingList = event.getContent().user_ids;
- if (!Array.isArray(typingList)) {
- // malformed event :/ bail early. TODO: whine?
- return;
- }
- if (typingList.indexOf(this.userId) !== -1) {
- this.typing = true;
- }
- if (oldTyping !== this.typing) {
- this.updateModifiedTime();
- this.emit(RoomMemberEvent.Typing, event, this);
- }
- }
-
- /**
- * Update the last modified time to the current time.
- */
- private updateModifiedTime(): void {
- this.modified = Date.now();
- }
-
- /**
- * Get the timestamp when this RoomMember was last updated. This timestamp is
- * updated when properties on this RoomMember are updated.
- * It is updated <i>before</i> firing events.
- * @returns The timestamp
- */
- public getLastModifiedTime(): number {
- return this.modified;
- }
-
- public isKicked(): boolean {
- return (
- this.membership === "leave" &&
- this.events.member !== undefined &&
- this.events.member.getSender() !== this.events.member.getStateKey()
- );
- }
-
- /**
- * If this member was invited with the is_direct flag set, return
- * the user that invited this member
- * @returns user id of the inviter
- */
- public getDMInviter(): string | undefined {
- // when not available because that room state hasn't been loaded in,
- // we don't really know, but more likely to not be a direct chat
- if (this.events.member) {
- // TODO: persist the is_direct flag on the member as more member events
- // come in caused by displayName changes.
-
- // the is_direct flag is set on the invite member event.
- // This is copied on the prev_content section of the join member event
- // when the invite is accepted.
-
- const memberEvent = this.events.member;
- let memberContent = memberEvent.getContent();
- let inviteSender: string | undefined = memberEvent.getSender();
-
- if (memberContent.membership === "join") {
- memberContent = memberEvent.getPrevContent();
- inviteSender = memberEvent.getUnsigned().prev_sender;
- }
-
- if (memberContent.membership === "invite" && memberContent.is_direct) {
- return inviteSender;
- }
- }
- }
-
- /**
- * Get the avatar URL for a room member.
- * @param baseUrl - The base homeserver URL See
- * {@link MatrixClient#getHomeserverUrl}.
- * @param width - The desired width of the thumbnail.
- * @param height - The desired height of the thumbnail.
- * @param resizeMethod - The thumbnail resize method to use, either
- * "crop" or "scale".
- * @param allowDefault - (optional) Passing false causes this method to
- * return null if the user has no avatar image. Otherwise, a default image URL
- * will be returned. Default: true. (Deprecated)
- * @param allowDirectLinks - (optional) If true, the avatar URL will be
- * returned even if it is a direct hyperlink rather than a matrix content URL.
- * If false, any non-matrix content URLs will be ignored. Setting this option to
- * true will expose URLs that, if fetched, will leak information about the user
- * to anyone who they share a room with.
- * @returns the avatar URL or null.
- */
- public getAvatarUrl(
- baseUrl: string,
- width: number,
- height: number,
- resizeMethod: string,
- allowDefault = true,
- allowDirectLinks: boolean,
- ): string | null {
- const rawUrl = this.getMxcAvatarUrl();
-
- if (!rawUrl && !allowDefault) {
- return null;
- }
- const httpUrl = getHttpUriForMxc(baseUrl, rawUrl, width, height, resizeMethod, allowDirectLinks);
- if (httpUrl) {
- return httpUrl;
- }
- return null;
- }
-
- /**
- * get the mxc avatar url, either from a state event, or from a lazily loaded member
- * @returns the mxc avatar url
- */
- public getMxcAvatarUrl(): string | undefined {
- if (this.events.member) {
- return this.events.member.getDirectionalContent().avatar_url;
- } else if (this.user) {
- return this.user.avatarUrl;
- }
- }
-}
-
-const MXID_PATTERN = /@.+:.+/;
-const LTR_RTL_PATTERN = /[\u200E\u200F\u202A-\u202F]/;
-
-function shouldDisambiguate(selfUserId: string, displayName?: string, roomState?: RoomState): boolean {
- if (!displayName || displayName === selfUserId) return false;
-
- // First check if the displayname is something we consider truthy
- // after stripping it of zero width characters and padding spaces
- if (!utils.removeHiddenChars(displayName)) return false;
-
- if (!roomState) return false;
-
- // Next check if the name contains something that look like a mxid
- // If it does, it may be someone trying to impersonate someone else
- // Show full mxid in this case
- if (MXID_PATTERN.test(displayName)) return true;
-
- // Also show mxid if the display name contains any LTR/RTL characters as these
- // make it very difficult for us to find similar *looking* display names
- // E.g "Mark" could be cloned by writing "kraM" but in RTL.
- if (LTR_RTL_PATTERN.test(displayName)) return true;
-
- // Also show mxid if there are other people with the same or similar
- // displayname, after hidden character removal.
- const userIds = roomState.getUserIdsWithDisplayName(displayName);
- if (userIds.some((u) => u !== selfUserId)) return true;
-
- return false;
-}
-
-function calculateDisplayName(selfUserId: string, displayName: string | undefined, disambiguate: boolean): string {
- if (!displayName || displayName === selfUserId) return selfUserId;
-
- if (disambiguate) return utils.removeDirectionOverrideChars(displayName) + " (" + selfUserId + ")";
-
- // First check if the displayname is something we consider truthy
- // after stripping it of zero width characters and padding spaces
- if (!utils.removeHiddenChars(displayName)) return selfUserId;
-
- // We always strip the direction override characters (LRO and RLO).
- // These override the text direction for all subsequent characters
- // in the paragraph so if display names contained these, they'd
- // need to be wrapped in something to prevent this from leaking out
- // (which we can do in HTML but not text) or we'd need to add
- // control characters to the string to reset any overrides (eg.
- // adding PDF characters at the end). As far as we can see,
- // there should be no reason these would be necessary - rtl display
- // names should flip into the correct direction automatically based on
- // the characters, and you can still embed rtl in ltr or vice versa
- // with the embed chars or marker chars.
- return utils.removeDirectionOverrideChars(displayName);
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-state.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-state.ts
deleted file mode 100644
index f975b9c..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-state.ts
+++ /dev/null
@@ -1,1081 +0,0 @@
-/*
-Copyright 2015 - 2021 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 { RoomMember } from "./room-member";
-import { logger } from "../logger";
-import * as utils from "../utils";
-import { EventType, UNSTABLE_MSC2716_MARKER } from "../@types/event";
-import { IEvent, MatrixEvent, MatrixEventEvent } from "./event";
-import { MatrixClient } from "../client";
-import { GuestAccess, HistoryVisibility, IJoinRuleEventContent, JoinRule } from "../@types/partials";
-import { TypedEventEmitter } from "./typed-event-emitter";
-import { Beacon, BeaconEvent, BeaconEventHandlerMap, getBeaconInfoIdentifier, BeaconIdentifier } from "./beacon";
-import { TypedReEmitter } from "../ReEmitter";
-import { M_BEACON, M_BEACON_INFO } from "../@types/beacon";
-
-export interface IMarkerFoundOptions {
- /** Whether the timeline was empty before the marker event arrived in the
- * room. This could be happen in a variety of cases:
- * 1. From the initial sync
- * 2. It's the first state we're seeing after joining the room
- * 3. Or whether it's coming from `syncFromCache`
- *
- * A marker event refers to `UNSTABLE_MSC2716_MARKER` and indicates that
- * history was imported somewhere back in time. It specifically points to an
- * MSC2716 insertion event where the history was imported at. Marker events
- * are sent as state events so they are easily discoverable by clients and
- * homeservers and don't get lost in timeline gaps.
- */
- timelineWasEmpty?: boolean;
-}
-
-// possible statuses for out-of-band member loading
-enum OobStatus {
- NotStarted,
- InProgress,
- Finished,
-}
-
-export interface IPowerLevelsContent {
- users?: Record<string, number>;
- events?: Record<string, number>;
- // eslint-disable-next-line camelcase
- users_default?: number;
- // eslint-disable-next-line camelcase
- events_default?: number;
- // eslint-disable-next-line camelcase
- state_default?: number;
- ban?: number;
- kick?: number;
- redact?: number;
-}
-
-export enum RoomStateEvent {
- Events = "RoomState.events",
- Members = "RoomState.members",
- NewMember = "RoomState.newMember",
- Update = "RoomState.update", // signals batches of updates without specificity
- BeaconLiveness = "RoomState.BeaconLiveness",
- Marker = "RoomState.Marker",
-}
-
-export type RoomStateEventHandlerMap = {
- /**
- * Fires whenever the event dictionary in room state is updated.
- * @param event - The matrix event which caused this event to fire.
- * @param state - The room state whose RoomState.events dictionary
- * was updated.
- * @param prevEvent - The event being replaced by the new state, if
- * known. Note that this can differ from `getPrevContent()` on the new state event
- * as this is the store's view of the last state, not the previous state provided
- * by the server.
- * @example
- * ```
- * matrixClient.on("RoomState.events", function(event, state, prevEvent){
- * var newStateEvent = event;
- * });
- * ```
- */
- [RoomStateEvent.Events]: (event: MatrixEvent, state: RoomState, lastStateEvent: MatrixEvent | null) => void;
- /**
- * Fires whenever a member in the members dictionary is updated in any way.
- * @param event - The matrix event which caused this event to fire.
- * @param state - The room state whose RoomState.members dictionary
- * was updated.
- * @param member - The room member that was updated.
- * @example
- * ```
- * matrixClient.on("RoomState.members", function(event, state, member){
- * var newMembershipState = member.membership;
- * });
- * ```
- */
- [RoomStateEvent.Members]: (event: MatrixEvent, state: RoomState, member: RoomMember) => void;
- /**
- * Fires whenever a member is added to the members dictionary. The RoomMember
- * will not be fully populated yet (e.g. no membership state) but will already
- * be available in the members dictionary.
- * @param event - The matrix event which caused this event to fire.
- * @param state - The room state whose RoomState.members dictionary
- * was updated with a new entry.
- * @param member - The room member that was added.
- * @example
- * ```
- * matrixClient.on("RoomState.newMember", function(event, state, member){
- * // add event listeners on 'member'
- * });
- * ```
- */
- [RoomStateEvent.NewMember]: (event: MatrixEvent, state: RoomState, member: RoomMember) => void;
- [RoomStateEvent.Update]: (state: RoomState) => void;
- [RoomStateEvent.BeaconLiveness]: (state: RoomState, hasLiveBeacons: boolean) => void;
- [RoomStateEvent.Marker]: (event: MatrixEvent, setStateOptions?: IMarkerFoundOptions) => void;
- [BeaconEvent.New]: (event: MatrixEvent, beacon: Beacon) => void;
-};
-
-type EmittedEvents = RoomStateEvent | BeaconEvent;
-type EventHandlerMap = RoomStateEventHandlerMap & BeaconEventHandlerMap;
-
-export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
- public readonly reEmitter = new TypedReEmitter<EmittedEvents, EventHandlerMap>(this);
- private sentinels: Record<string, RoomMember> = {}; // userId: RoomMember
- // stores fuzzy matches to a list of userIDs (applies utils.removeHiddenChars to keys)
- private displayNameToUserIds = new Map<string, string[]>();
- private userIdsToDisplayNames: Record<string, string> = {};
- private tokenToInvite: Record<string, MatrixEvent> = {}; // 3pid invite state_key to m.room.member invite
- private joinedMemberCount: number | null = null; // cache of the number of joined members
- // joined members count from summary api
- // once set, we know the server supports the summary api
- // and we should only trust that
- // we could also only trust that before OOB members
- // are loaded but doesn't seem worth the hassle atm
- private summaryJoinedMemberCount: number | null = null;
- // same for invited member count
- private invitedMemberCount: number | null = null;
- private summaryInvitedMemberCount: number | null = null;
- private modified = -1;
-
- // XXX: Should be read-only
- // The room member dictionary, keyed on the user's ID.
- public members: Record<string, RoomMember> = {}; // userId: RoomMember
- // The state events dictionary, keyed on the event type and then the state_key value.
- public events = new Map<string, Map<string, MatrixEvent>>(); // Map<eventType, Map<stateKey, MatrixEvent>>
- // The pagination token for this state.
- public paginationToken: string | null = null;
-
- public readonly beacons = new Map<BeaconIdentifier, Beacon>();
- private _liveBeaconIds: BeaconIdentifier[] = [];
-
- /**
- * Construct room state.
- *
- * Room State represents the state of the room at a given point.
- * It can be mutated by adding state events to it.
- * There are two types of room member associated with a state event:
- * normal member objects (accessed via getMember/getMembers) which mutate
- * with the state to represent the current state of that room/user, e.g.
- * the object returned by `getMember('@bob:example.com')` will mutate to
- * get a different display name if Bob later changes his display name
- * in the room.
- * There are also 'sentinel' members (accessed via getSentinelMember).
- * These also represent the state of room members at the point in time
- * represented by the RoomState object, but unlike objects from getMember,
- * sentinel objects will always represent the room state as at the time
- * getSentinelMember was called, so if Bob subsequently changes his display
- * name, a room member object previously acquired with getSentinelMember
- * will still have his old display name. Calling getSentinelMember again
- * after the display name change will return a new RoomMember object
- * with Bob's new display name.
- *
- * @param roomId - Optional. The ID of the room which has this state.
- * If none is specified it just tracks paginationTokens, useful for notifTimelineSet
- * @param oobMemberFlags - Optional. The state of loading out of bound members.
- * As the timeline might get reset while they are loading, this state needs to be inherited
- * and shared when the room state is cloned for the new timeline.
- * This should only be passed from clone.
- */
- public constructor(public readonly roomId: string, private oobMemberFlags = { status: OobStatus.NotStarted }) {
- super();
- this.updateModifiedTime();
- }
-
- /**
- * Returns the number of joined members in this room
- * This method caches the result.
- * @returns The number of members in this room whose membership is 'join'
- */
- public getJoinedMemberCount(): number {
- if (this.summaryJoinedMemberCount !== null) {
- return this.summaryJoinedMemberCount;
- }
- if (this.joinedMemberCount === null) {
- this.joinedMemberCount = this.getMembers().reduce((count, m) => {
- return m.membership === "join" ? count + 1 : count;
- }, 0);
- }
- return this.joinedMemberCount;
- }
-
- /**
- * Set the joined member count explicitly (like from summary part of the sync response)
- * @param count - the amount of joined members
- */
- public setJoinedMemberCount(count: number): void {
- this.summaryJoinedMemberCount = count;
- }
-
- /**
- * Returns the number of invited members in this room
- * @returns The number of members in this room whose membership is 'invite'
- */
- public getInvitedMemberCount(): number {
- if (this.summaryInvitedMemberCount !== null) {
- return this.summaryInvitedMemberCount;
- }
- if (this.invitedMemberCount === null) {
- this.invitedMemberCount = this.getMembers().reduce((count, m) => {
- return m.membership === "invite" ? count + 1 : count;
- }, 0);
- }
- return this.invitedMemberCount;
- }
-
- /**
- * Set the amount of invited members in this room
- * @param count - the amount of invited members
- */
- public setInvitedMemberCount(count: number): void {
- this.summaryInvitedMemberCount = count;
- }
-
- /**
- * Get all RoomMembers in this room.
- * @returns A list of RoomMembers.
- */
- public getMembers(): RoomMember[] {
- return Object.values(this.members);
- }
-
- /**
- * Get all RoomMembers in this room, excluding the user IDs provided.
- * @param excludedIds - The user IDs to exclude.
- * @returns A list of RoomMembers.
- */
- public getMembersExcept(excludedIds: string[]): RoomMember[] {
- return this.getMembers().filter((m) => !excludedIds.includes(m.userId));
- }
-
- /**
- * Get a room member by their user ID.
- * @param userId - The room member's user ID.
- * @returns The member or null if they do not exist.
- */
- public getMember(userId: string): RoomMember | null {
- return this.members[userId] || null;
- }
-
- /**
- * Get a room member whose properties will not change with this room state. You
- * typically want this if you want to attach a RoomMember to a MatrixEvent which
- * may no longer be represented correctly by Room.currentState or Room.oldState.
- * The term 'sentinel' refers to the fact that this RoomMember is an unchanging
- * guardian for state at this particular point in time.
- * @param userId - The room member's user ID.
- * @returns The member or null if they do not exist.
- */
- public getSentinelMember(userId: string): RoomMember | null {
- if (!userId) return null;
- let sentinel = this.sentinels[userId];
-
- if (sentinel === undefined) {
- sentinel = new RoomMember(this.roomId, userId);
- const member = this.members[userId];
- if (member?.events.member) {
- sentinel.setMembershipEvent(member.events.member, this);
- }
- this.sentinels[userId] = sentinel;
- }
- return sentinel;
- }
-
- /**
- * Get state events from the state of the room.
- * @param eventType - The event type of the state event.
- * @param stateKey - Optional. The state_key of the state event. If
- * this is `undefined` then all matching state events will be
- * returned.
- * @returns A list of events if state_key was
- * `undefined`, else a single event (or null if no match found).
- */
- public getStateEvents(eventType: EventType | string): MatrixEvent[];
- public getStateEvents(eventType: EventType | string, stateKey: string): MatrixEvent | null;
- public getStateEvents(eventType: EventType | string, stateKey?: string): MatrixEvent[] | MatrixEvent | null {
- if (!this.events.has(eventType)) {
- // no match
- return stateKey === undefined ? [] : null;
- }
- if (stateKey === undefined) {
- // return all values
- return Array.from(this.events.get(eventType)!.values());
- }
- const event = this.events.get(eventType)!.get(stateKey);
- return event ? event : null;
- }
-
- public get hasLiveBeacons(): boolean {
- 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.
- * @returns the copy of the room state
- */
- public clone(): RoomState {
- const copy = new RoomState(this.roomId, this.oobMemberFlags);
-
- // Ugly hack: because setStateEvents will mark
- // members as susperseding future out of bound members
- // if loading is in progress (through oobMemberFlags)
- // since these are not new members, we're merely copying them
- // set the status to not started
- // after copying, we set back the status
- const status = this.oobMemberFlags.status;
- this.oobMemberFlags.status = OobStatus.NotStarted;
-
- Array.from(this.events.values()).forEach((eventsByStateKey) => {
- copy.setStateEvents(Array.from(eventsByStateKey.values()));
- });
-
- // Ugly hack: see above
- this.oobMemberFlags.status = status;
-
- if (this.summaryInvitedMemberCount !== null) {
- copy.setInvitedMemberCount(this.getInvitedMemberCount());
- }
- if (this.summaryJoinedMemberCount !== null) {
- copy.setJoinedMemberCount(this.getJoinedMemberCount());
- }
-
- // copy out of band flags if needed
- if (this.oobMemberFlags.status == OobStatus.Finished) {
- // copy markOutOfBand flags
- this.getMembers().forEach((member) => {
- if (member.isOutOfBand()) {
- copy.getMember(member.userId)?.markOutOfBand();
- }
- });
- }
-
- return copy;
- }
-
- /**
- * Add previously unknown state events.
- * When lazy loading members while back-paginating,
- * the relevant room state for the timeline chunk at the end
- * of the chunk can be set with this method.
- * @param events - state events to prepend
- */
- public setUnknownStateEvents(events: MatrixEvent[]): void {
- const unknownStateEvents = events.filter((event) => {
- return !this.events.has(event.getType()) || !this.events.get(event.getType())!.has(event.getStateKey()!);
- });
-
- this.setStateEvents(unknownStateEvents);
- }
-
- /**
- * Add an array of one or more state MatrixEvents, overwriting any existing
- * state with the same `{type, stateKey}` tuple. Will fire "RoomState.events"
- * for every event added. May fire "RoomState.members" if there are
- * `m.room.member` events. May fire "RoomStateEvent.Marker" if there are
- * `UNSTABLE_MSC2716_MARKER` events.
- * @param stateEvents - a list of state events for this room.
- *
- * @remarks
- * Fires {@link RoomStateEvent.Members}
- * Fires {@link RoomStateEvent.NewMember}
- * Fires {@link RoomStateEvent.Events}
- * Fires {@link RoomStateEvent.Marker}
- */
- public setStateEvents(stateEvents: MatrixEvent[], markerFoundOptions?: IMarkerFoundOptions): void {
- this.updateModifiedTime();
-
- // update the core event dict
- stateEvents.forEach((event) => {
- if (event.getRoomId() !== this.roomId || !event.isState()) return;
-
- if (M_BEACON_INFO.matches(event.getType())) {
- this.setBeacon(event);
- }
-
- const lastStateEvent = this.getStateEventMatching(event);
- this.setStateEvent(event);
- if (event.getType() === EventType.RoomMember) {
- this.updateDisplayNameCache(event.getStateKey()!, event.getContent().displayname ?? "");
- this.updateThirdPartyTokenCache(event);
- }
- this.emit(RoomStateEvent.Events, event, this, lastStateEvent);
- });
-
- this.onBeaconLivenessChange();
- // update higher level data structures. This needs to be done AFTER the
- // core event dict as these structures may depend on other state events in
- // the given array (e.g. disambiguating display names in one go to do both
- // clashing names rather than progressively which only catches 1 of them).
- stateEvents.forEach((event) => {
- if (event.getRoomId() !== this.roomId || !event.isState()) return;
-
- if (event.getType() === EventType.RoomMember) {
- const userId = event.getStateKey()!;
-
- // leave events apparently elide the displayname or avatar_url,
- // so let's fake one up so that we don't leak user ids
- // into the timeline
- if (event.getContent().membership === "leave" || event.getContent().membership === "ban") {
- event.getContent().avatar_url = event.getContent().avatar_url || event.getPrevContent().avatar_url;
- event.getContent().displayname =
- event.getContent().displayname || event.getPrevContent().displayname;
- }
-
- const member = this.getOrCreateMember(userId, event);
- member.setMembershipEvent(event, this);
- this.updateMember(member);
- this.emit(RoomStateEvent.Members, event, this, member);
- } else if (event.getType() === EventType.RoomPowerLevels) {
- // events with unknown state keys should be ignored
- // and should not aggregate onto members power levels
- if (event.getStateKey() !== "") {
- return;
- }
- const members = Object.values(this.members);
- members.forEach((member) => {
- // We only propagate `RoomState.members` event if the
- // power levels has been changed
- // large room suffer from large re-rendering especially when not needed
- const oldLastModified = member.getLastModifiedTime();
- member.setPowerLevelEvent(event);
- if (oldLastModified !== member.getLastModifiedTime()) {
- this.emit(RoomStateEvent.Members, event, this, member);
- }
- });
-
- // assume all our sentinels are now out-of-date
- this.sentinels = {};
- } else if (UNSTABLE_MSC2716_MARKER.matches(event.getType())) {
- this.emit(RoomStateEvent.Marker, event, markerFoundOptions);
- }
- });
-
- this.emit(RoomStateEvent.Update, this);
- }
-
- public async processBeaconEvents(events: MatrixEvent[], matrixClient: MatrixClient): Promise<void> {
- if (
- !events.length ||
- // discard locations if we have no beacons
- !this.beacons.size
- ) {
- return;
- }
-
- const beaconByEventIdDict = [...this.beacons.values()].reduce<Record<string, Beacon>>((dict, beacon) => {
- dict[beacon.beaconInfoId] = beacon;
- return dict;
- }, {});
-
- const processBeaconRelation = (beaconInfoEventId: string, event: MatrixEvent): void => {
- if (!M_BEACON.matches(event.getType())) {
- return;
- }
-
- const beacon = beaconByEventIdDict[beaconInfoEventId];
-
- if (beacon) {
- beacon.addLocations([event]);
- }
- };
-
- for (const event of events) {
- const relatedToEventId = event.getRelation()?.event_id;
- // not related to a beacon we know about; discard
- if (!relatedToEventId || !beaconByEventIdDict[relatedToEventId]) return;
- if (!M_BEACON.matches(event.getType()) && !event.isEncrypted()) return;
-
- try {
- await matrixClient.decryptEventIfNeeded(event);
- processBeaconRelation(relatedToEventId, event);
- } catch {
- if (event.isDecryptionFailure()) {
- // add an event listener for once the event is decrypted.
- event.once(MatrixEventEvent.Decrypted, async () => {
- 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.
- * This method makes sure the member is added to the members dictionary
- * before emitting, as this is done from setStateEvents and setOutOfBandMember.
- * @param userId - the id of the user to look up
- * @param event - the membership event for the (new) member. Used to emit.
- * @returns the member, existing or newly created.
- *
- * @remarks
- * Fires {@link RoomStateEvent.NewMember}
- */
- private getOrCreateMember(userId: string, event: MatrixEvent): RoomMember {
- let member = this.members[userId];
- if (!member) {
- member = new RoomMember(this.roomId, userId);
- // add member to members before emitting any events,
- // as event handlers often lookup the member
- this.members[userId] = member;
- this.emit(RoomStateEvent.NewMember, event, this, member);
- }
- return member;
- }
-
- private setStateEvent(event: MatrixEvent): void {
- if (!this.events.has(event.getType())) {
- this.events.set(event.getType(), new Map());
- }
- this.events.get(event.getType())!.set(event.getStateKey()!, event);
- }
-
- /**
- * @experimental
- */
- private setBeacon(event: MatrixEvent): void {
- const beaconIdentifier = getBeaconInfoIdentifier(event);
-
- if (this.beacons.has(beaconIdentifier)) {
- const beacon = this.beacons.get(beaconIdentifier)!;
-
- if (event.isRedacted()) {
- if (beacon.beaconInfoId === (<IEvent>event.getRedactionEvent())?.redacts) {
- beacon.destroy();
- this.beacons.delete(beaconIdentifier);
- }
- return;
- }
-
- return beacon.update(event);
- }
-
- if (event.isRedacted()) {
- return;
- }
-
- const beacon = new Beacon(event);
-
- this.reEmitter.reEmit<BeaconEvent, BeaconEvent>(beacon, [
- BeaconEvent.New,
- BeaconEvent.Update,
- BeaconEvent.Destroy,
- BeaconEvent.LivenessChange,
- ]);
-
- this.emit(BeaconEvent.New, event, beacon);
- beacon.on(BeaconEvent.LivenessChange, this.onBeaconLivenessChange.bind(this));
- beacon.on(BeaconEvent.Destroy, this.onBeaconLivenessChange.bind(this));
-
- this.beacons.set(beacon.identifier, beacon);
- }
-
- /**
- * @experimental
- * Check liveness of room beacons
- * emit RoomStateEvent.BeaconLiveness event
- */
- private onBeaconLivenessChange(): void {
- this._liveBeaconIds = Array.from(this.beacons.values())
- .filter((beacon) => beacon.isLive)
- .map((beacon) => beacon.identifier);
-
- this.emit(RoomStateEvent.BeaconLiveness, this, this.hasLiveBeacons);
- }
-
- private getStateEventMatching(event: MatrixEvent): MatrixEvent | null {
- return this.events.get(event.getType())?.get(event.getStateKey()!) ?? null;
- }
-
- private updateMember(member: RoomMember): void {
- // this member may have a power level already, so set it.
- const pwrLvlEvent = this.getStateEvents(EventType.RoomPowerLevels, "");
- if (pwrLvlEvent) {
- member.setPowerLevelEvent(pwrLvlEvent);
- }
-
- // blow away the sentinel which is now outdated
- delete this.sentinels[member.userId];
-
- this.members[member.userId] = member;
- this.joinedMemberCount = null;
- this.invitedMemberCount = null;
- }
-
- /**
- * Get the out-of-band members loading state, whether loading is needed or not.
- * Note that loading might be in progress and hence isn't needed.
- * @returns whether or not the members of this room need to be loaded
- */
- public needsOutOfBandMembers(): boolean {
- return this.oobMemberFlags.status === OobStatus.NotStarted;
- }
-
- /**
- * Check if loading of out-of-band-members has completed
- *
- * @returns true if the full membership list of this room has been loaded. False if it is not started or is in
- * progress.
- */
- public outOfBandMembersReady(): boolean {
- return this.oobMemberFlags.status === OobStatus.Finished;
- }
-
- /**
- * Mark this room state as waiting for out-of-band members,
- * ensuring it doesn't ask for them to be requested again
- * through needsOutOfBandMembers
- */
- public markOutOfBandMembersStarted(): void {
- if (this.oobMemberFlags.status !== OobStatus.NotStarted) {
- return;
- }
- this.oobMemberFlags.status = OobStatus.InProgress;
- }
-
- /**
- * Mark this room state as having failed to fetch out-of-band members
- */
- public markOutOfBandMembersFailed(): void {
- if (this.oobMemberFlags.status !== OobStatus.InProgress) {
- return;
- }
- this.oobMemberFlags.status = OobStatus.NotStarted;
- }
-
- /**
- * Clears the loaded out-of-band members
- */
- public clearOutOfBandMembers(): void {
- let count = 0;
- Object.keys(this.members).forEach((userId) => {
- const member = this.members[userId];
- if (member.isOutOfBand()) {
- ++count;
- delete this.members[userId];
- }
- });
- logger.log(`LL: RoomState removed ${count} members...`);
- this.oobMemberFlags.status = OobStatus.NotStarted;
- }
-
- /**
- * Sets the loaded out-of-band members.
- * @param stateEvents - array of membership state events
- */
- public setOutOfBandMembers(stateEvents: MatrixEvent[]): void {
- logger.log(`LL: RoomState about to set ${stateEvents.length} OOB members ...`);
- if (this.oobMemberFlags.status !== OobStatus.InProgress) {
- return;
- }
- logger.log(`LL: RoomState put in finished state ...`);
- this.oobMemberFlags.status = OobStatus.Finished;
- stateEvents.forEach((e) => this.setOutOfBandMember(e));
- this.emit(RoomStateEvent.Update, this);
- }
-
- /**
- * Sets a single out of band member, used by both setOutOfBandMembers and clone
- * @param stateEvent - membership state event
- */
- private setOutOfBandMember(stateEvent: MatrixEvent): void {
- if (stateEvent.getType() !== EventType.RoomMember) {
- return;
- }
- const userId = stateEvent.getStateKey()!;
- const existingMember = this.getMember(userId);
- // never replace members received as part of the sync
- if (existingMember && !existingMember.isOutOfBand()) {
- return;
- }
-
- const member = this.getOrCreateMember(userId, stateEvent);
- member.setMembershipEvent(stateEvent, this);
- // needed to know which members need to be stored seperately
- // as they are not part of the sync accumulator
- // this is cleared by setMembershipEvent so when it's updated through /sync
- member.markOutOfBand();
-
- this.updateDisplayNameCache(member.userId, member.name);
-
- this.setStateEvent(stateEvent);
- this.updateMember(member);
- this.emit(RoomStateEvent.Members, stateEvent, this, member);
- }
-
- /**
- * Set the current typing event for this room.
- * @param event - The typing event
- */
- public setTypingEvent(event: MatrixEvent): void {
- Object.values(this.members).forEach(function (member) {
- member.setTypingEvent(event);
- });
- }
-
- /**
- * Get the m.room.member event which has the given third party invite token.
- *
- * @param token - The token
- * @returns The m.room.member event or null
- */
- public getInviteForThreePidToken(token: string): MatrixEvent | null {
- return this.tokenToInvite[token] || null;
- }
-
- /**
- * Update the last modified time to the current time.
- */
- private updateModifiedTime(): void {
- this.modified = Date.now();
- }
-
- /**
- * Get the timestamp when this room state was last updated. This timestamp is
- * updated when this object has received new state events.
- * @returns The timestamp
- */
- public getLastModifiedTime(): number {
- return this.modified;
- }
-
- /**
- * Get user IDs with the specified or similar display names.
- * @param displayName - The display name to get user IDs from.
- * @returns An array of user IDs or an empty array.
- */
- public getUserIdsWithDisplayName(displayName: string): string[] {
- return this.displayNameToUserIds.get(utils.removeHiddenChars(displayName)) ?? [];
- }
-
- /**
- * Returns true if userId is in room, event is not redacted and either sender of
- * mxEvent or has power level sufficient to redact events other than their own.
- * @param mxEvent - The event to test permission for
- * @param userId - The user ID of the user to test permission for
- * @returns true if the given used ID can redact given event
- */
- public maySendRedactionForEvent(mxEvent: MatrixEvent, userId: string): boolean {
- const member = this.getMember(userId);
- if (!member || member.membership === "leave") return false;
-
- if (mxEvent.status || mxEvent.isRedacted()) return false;
-
- // The user may have been the sender, but they can't redact their own message
- // if redactions are blocked.
- const canRedact = this.maySendEvent(EventType.RoomRedaction, userId);
- if (mxEvent.getSender() === userId) return canRedact;
-
- return this.hasSufficientPowerLevelFor("redact", member.powerLevel);
- }
-
- /**
- * Returns true if the given power level is sufficient for action
- * @param action - The type of power level to check
- * @param powerLevel - The power level of the member
- * @returns true if the given power level is sufficient
- */
- public hasSufficientPowerLevelFor(action: "ban" | "kick" | "redact", powerLevel: number): boolean {
- const powerLevelsEvent = this.getStateEvents(EventType.RoomPowerLevels, "");
-
- let powerLevels: IPowerLevelsContent = {};
- if (powerLevelsEvent) {
- powerLevels = powerLevelsEvent.getContent();
- }
-
- let requiredLevel = 50;
- if (utils.isNumber(powerLevels[action])) {
- requiredLevel = powerLevels[action]!;
- }
-
- return powerLevel >= requiredLevel;
- }
-
- /**
- * Short-form for maySendEvent('m.room.message', userId)
- * @param userId - The user ID of the user to test permission for
- * @returns true if the given user ID should be permitted to send
- * message events into the given room.
- */
- public maySendMessage(userId: string): boolean {
- return this.maySendEventOfType(EventType.RoomMessage, userId, false);
- }
-
- /**
- * Returns true if the given user ID has permission to send a normal
- * event of type `eventType` into this room.
- * @param eventType - The type of event to test
- * @param userId - The user ID of the user to test permission for
- * @returns true if the given user ID should be permitted to send
- * the given type of event into this room,
- * according to the room's state.
- */
- public maySendEvent(eventType: EventType | string, userId: string): boolean {
- return this.maySendEventOfType(eventType, userId, false);
- }
-
- /**
- * Returns true if the given MatrixClient has permission to send a state
- * event of type `stateEventType` into this room.
- * @param stateEventType - The type of state events to test
- * @param cli - The client to test permission for
- * @returns true if the given client should be permitted to send
- * the given type of state event into this room,
- * according to the room's state.
- */
- public mayClientSendStateEvent(stateEventType: EventType | string, cli: MatrixClient): boolean {
- if (cli.isGuest() || !cli.credentials.userId) {
- return false;
- }
- return this.maySendStateEvent(stateEventType, cli.credentials.userId);
- }
-
- /**
- * Returns true if the given user ID has permission to send a state
- * event of type `stateEventType` into this room.
- * @param stateEventType - The type of state events to test
- * @param userId - The user ID of the user to test permission for
- * @returns true if the given user ID should be permitted to send
- * the given type of state event into this room,
- * according to the room's state.
- */
- public maySendStateEvent(stateEventType: EventType | string, userId: string): boolean {
- return this.maySendEventOfType(stateEventType, userId, true);
- }
-
- /**
- * Returns true if the given user ID has permission to send a normal or state
- * event of type `eventType` into this room.
- * @param eventType - The type of event to test
- * @param userId - The user ID of the user to test permission for
- * @param state - If true, tests if the user may send a state
- event of this type. Otherwise tests whether
- they may send a regular event.
- * @returns true if the given user ID should be permitted to send
- * the given type of event into this room,
- * according to the room's state.
- */
- private maySendEventOfType(eventType: EventType | string, userId: string, state: boolean): boolean {
- const powerLevelsEvent = this.getStateEvents(EventType.RoomPowerLevels, "");
-
- let powerLevels: IPowerLevelsContent;
- let eventsLevels: Record<EventType | string, number> = {};
-
- let stateDefault = 0;
- let eventsDefault = 0;
- let powerLevel = 0;
- if (powerLevelsEvent) {
- powerLevels = powerLevelsEvent.getContent();
- eventsLevels = powerLevels.events || {};
-
- if (Number.isSafeInteger(powerLevels.state_default)) {
- stateDefault = powerLevels.state_default!;
- } else {
- stateDefault = 50;
- }
-
- const userPowerLevel = powerLevels.users && powerLevels.users[userId];
- if (Number.isSafeInteger(userPowerLevel)) {
- powerLevel = userPowerLevel!;
- } else if (Number.isSafeInteger(powerLevels.users_default)) {
- powerLevel = powerLevels.users_default!;
- }
-
- if (Number.isSafeInteger(powerLevels.events_default)) {
- eventsDefault = powerLevels.events_default!;
- }
- }
-
- let requiredLevel = state ? stateDefault : eventsDefault;
- if (Number.isSafeInteger(eventsLevels[eventType])) {
- requiredLevel = eventsLevels[eventType];
- }
- return powerLevel >= requiredLevel;
- }
-
- /**
- * Returns true if the given user ID has permission to trigger notification
- * of type `notifLevelKey`
- * @param notifLevelKey - The level of notification to test (eg. 'room')
- * @param userId - The user ID of the user to test permission for
- * @returns true if the given user ID has permission to trigger a
- * notification of this type.
- */
- public mayTriggerNotifOfType(notifLevelKey: string, userId: string): boolean {
- const member = this.getMember(userId);
- if (!member) {
- return false;
- }
-
- const powerLevelsEvent = this.getStateEvents(EventType.RoomPowerLevels, "");
-
- let notifLevel = 50;
- if (
- powerLevelsEvent &&
- powerLevelsEvent.getContent() &&
- powerLevelsEvent.getContent().notifications &&
- utils.isNumber(powerLevelsEvent.getContent().notifications[notifLevelKey])
- ) {
- notifLevel = powerLevelsEvent.getContent().notifications[notifLevelKey];
- }
-
- return member.powerLevel >= notifLevel;
- }
-
- /**
- * Returns the join rule based on the m.room.join_rule state event, defaulting to `invite`.
- * @returns the join_rule applied to this room
- */
- public getJoinRule(): JoinRule {
- const joinRuleEvent = this.getStateEvents(EventType.RoomJoinRules, "");
- const joinRuleContent: Partial<IJoinRuleEventContent> = joinRuleEvent?.getContent() ?? {};
- return joinRuleContent["join_rule"] || JoinRule.Invite;
- }
-
- /**
- * Returns the history visibility based on the m.room.history_visibility state event, defaulting to `shared`.
- * @returns the history_visibility applied to this room
- */
- public getHistoryVisibility(): HistoryVisibility {
- const historyVisibilityEvent = this.getStateEvents(EventType.RoomHistoryVisibility, "");
- const historyVisibilityContent = historyVisibilityEvent?.getContent() ?? {};
- return historyVisibilityContent["history_visibility"] || HistoryVisibility.Shared;
- }
-
- /**
- * Returns the guest access based on the m.room.guest_access state event, defaulting to `shared`.
- * @returns the guest_access applied to this room
- */
- public getGuestAccess(): GuestAccess {
- const guestAccessEvent = this.getStateEvents(EventType.RoomGuestAccess, "");
- const guestAccessContent = guestAccessEvent?.getContent() ?? {};
- return guestAccessContent["guest_access"] || GuestAccess.Forbidden;
- }
-
- /**
- * Find the predecessor room based on this room state.
- *
- * @param msc3946ProcessDynamicPredecessor - if true, look for an
- * m.room.predecessor state event and use it if found (MSC3946).
- * @returns null if this room has no predecessor. Otherwise, returns
- * the roomId, last eventId and viaServers of the predecessor room.
- *
- * If msc3946ProcessDynamicPredecessor is true, use m.predecessor events
- * as well as m.room.create events to find predecessors.
- *
- * Note: if an m.predecessor event is used, eventId may be undefined
- * since last_known_event_id is optional.
- *
- * Note: viaServers may be undefined, and will definitely be undefined if
- * this predecessor comes from a RoomCreate event (rather than a
- * RoomPredecessor, which has the optional via_servers property).
- */
- public findPredecessor(
- msc3946ProcessDynamicPredecessor = false,
- ): { roomId: string; eventId?: string; viaServers?: string[] } | null {
- // Note: the tests for this function are against Room.findPredecessor,
- // which just calls through to here.
-
- if (msc3946ProcessDynamicPredecessor) {
- const predecessorEvent = this.getStateEvents(EventType.RoomPredecessor, "");
- if (predecessorEvent) {
- const content = predecessorEvent.getContent<{
- predecessor_room_id: string;
- last_known_event_id?: string;
- via_servers?: string[];
- }>();
- const roomId = content.predecessor_room_id;
- let eventId = content.last_known_event_id;
- if (typeof eventId !== "string") {
- eventId = undefined;
- }
- let viaServers = content.via_servers;
- if (!Array.isArray(viaServers)) {
- viaServers = undefined;
- }
- if (typeof roomId === "string") {
- return { roomId, eventId, viaServers };
- }
- }
- }
-
- const createEvent = this.getStateEvents(EventType.RoomCreate, "");
- if (createEvent) {
- const predecessor = createEvent.getContent<{
- predecessor?: Partial<{
- room_id: string;
- event_id: string;
- }>;
- }>()["predecessor"];
- if (predecessor) {
- const roomId = predecessor["room_id"];
- if (typeof roomId === "string") {
- let eventId = predecessor["event_id"];
- if (typeof eventId !== "string" || eventId === "") {
- eventId = undefined;
- }
- return { roomId, eventId };
- }
- }
- }
- return null;
- }
-
- private updateThirdPartyTokenCache(memberEvent: MatrixEvent): void {
- if (!memberEvent.getContent().third_party_invite) {
- return;
- }
- const token = (memberEvent.getContent().third_party_invite.signed || {}).token;
- if (!token) {
- return;
- }
- const threePidInvite = this.getStateEvents(EventType.RoomThirdPartyInvite, token);
- if (!threePidInvite) {
- return;
- }
- this.tokenToInvite[token] = memberEvent;
- }
-
- private updateDisplayNameCache(userId: string, displayName: string): void {
- const oldName = this.userIdsToDisplayNames[userId];
- delete this.userIdsToDisplayNames[userId];
- if (oldName) {
- // Remove the old name from the cache.
- // We clobber the user_id > name lookup but the name -> [user_id] lookup
- // means we need to remove that user ID from that array rather than nuking
- // the lot.
- const strippedOldName = utils.removeHiddenChars(oldName);
-
- const existingUserIds = this.displayNameToUserIds.get(strippedOldName);
- if (existingUserIds) {
- // remove this user ID from this array
- const filteredUserIDs = existingUserIds.filter((id) => id !== userId);
- this.displayNameToUserIds.set(strippedOldName, filteredUserIDs);
- }
- }
-
- this.userIdsToDisplayNames[userId] = displayName;
-
- const strippedDisplayname = displayName && utils.removeHiddenChars(displayName);
- // an empty stripped displayname (undefined/'') will be set to MXID in room-member.js
- if (strippedDisplayname) {
- const arr = this.displayNameToUserIds.get(strippedDisplayname) ?? [];
- arr.push(userId);
- this.displayNameToUserIds.set(strippedDisplayname, arr);
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-summary.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-summary.ts
deleted file mode 100644
index 936ec1d..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-summary.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
-Copyright 2015 - 2021 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.
-*/
-
-export interface IRoomSummary {
- "m.heroes": string[];
- "m.joined_member_count"?: number;
- "m.invited_member_count"?: number;
-}
-
-interface IInfo {
- /** The title of the room (e.g. `m.room.name`) */
- title: string;
- /** The description of the room (e.g. `m.room.topic`) */
- desc?: string;
- /** The number of joined users. */
- numMembers?: number;
- /** The list of aliases for this room. */
- aliases?: string[];
- /** The timestamp for this room. */
- timestamp?: number;
-}
-
-/**
- * Construct a new Room Summary. A summary can be used for display on a recent
- * list, without having to load the entire room list into memory.
- * @param roomId - Required. The ID of this room.
- * @param info - Optional. The summary info. Additional keys are supported.
- */
-export class RoomSummary {
- public constructor(public readonly roomId: string, info?: IInfo) {}
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room.ts
deleted file mode 100644
index 133b210..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room.ts
+++ /dev/null
@@ -1,3487 +0,0 @@
-/*
-Copyright 2015 - 2023 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 { M_POLL_START, Optional } from "matrix-events-sdk";
-
-import {
- EventTimelineSet,
- DuplicateStrategy,
- IAddLiveEventOptions,
- EventTimelineSetHandlerMap,
-} from "./event-timeline-set";
-import { Direction, EventTimeline } from "./event-timeline";
-import { getHttpUriForMxc } from "../content-repo";
-import * as utils from "../utils";
-import { normalize, noUnsafeEventProps } from "../utils";
-import { IEvent, IThreadBundledRelationship, MatrixEvent, MatrixEventEvent, MatrixEventHandlerMap } from "./event";
-import { EventStatus } from "./event-status";
-import { RoomMember } from "./room-member";
-import { IRoomSummary, RoomSummary } from "./room-summary";
-import { logger } from "../logger";
-import { TypedReEmitter } from "../ReEmitter";
-import {
- EventType,
- RoomCreateTypeField,
- RoomType,
- UNSTABLE_ELEMENT_FUNCTIONAL_USERS,
- EVENT_VISIBILITY_CHANGE_TYPE,
- RelationType,
-} from "../@types/event";
-import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
-import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials";
-import { Filter, IFilterDefinition } from "../filter";
-import { RoomState, RoomStateEvent, RoomStateEventHandlerMap } from "./room-state";
-import { BeaconEvent, BeaconEventHandlerMap } from "./beacon";
-import {
- Thread,
- ThreadEvent,
- EventHandlerMap as ThreadHandlerMap,
- FILTER_RELATED_BY_REL_TYPES,
- THREAD_RELATION_TYPE,
- FILTER_RELATED_BY_SENDERS,
- ThreadFilterType,
-} from "./thread";
-import {
- CachedReceiptStructure,
- MAIN_ROOM_TIMELINE,
- Receipt,
- ReceiptContent,
- ReceiptType,
-} from "../@types/read_receipts";
-import { IStateEventWithRoomId } from "../@types/search";
-import { RelationsContainer } from "./relations-container";
-import { ReadReceipt, synthesizeReceipt } from "./read-receipt";
-import { Poll, PollEvent } from "./poll";
-
-// These constants are used as sane defaults when the homeserver doesn't support
-// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
-// the same as the common default room version whereas SAFE_ROOM_VERSIONS are the
-// 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.
-export const KNOWN_SAFE_ROOM_VERSION = "9";
-const SAFE_ROOM_VERSIONS = ["1", "2", "3", "4", "5", "6", "7", "8", "9"];
-
-interface IOpts {
- /**
- * Controls where pending messages appear in a room's timeline.
- * If "<b>chronological</b>", messages will appear in the timeline when the call to `sendEvent` was made.
- * If "<b>detached</b>", pending messages will appear in a separate list,
- * accessible via {@link Room#getPendingEvents}.
- * Default: "chronological".
- */
- pendingEventOrdering?: PendingEventOrdering;
- /**
- * Set to true to enable improved timeline support.
- */
- timelineSupport?: boolean;
- lazyLoadMembers?: boolean;
-}
-
-export interface IRecommendedVersion {
- version: string;
- needsUpgrade: boolean;
- urgent: boolean;
-}
-
-// When inserting a visibility event affecting event `eventId`, we
-// need to scan through existing visibility events for `eventId`.
-// In theory, this could take an unlimited amount of time if:
-//
-// - the visibility event was sent by a moderator; and
-// - `eventId` already has many visibility changes (usually, it should
-// be 2 or less); and
-// - for some reason, the visibility changes are received out of order
-// (usually, this shouldn't happen at all).
-//
-// For this reason, we limit the number of events to scan through,
-// expecting that a broken visibility change for a single event in
-// an extremely uncommon case (possibly a DoS) is a small
-// price to pay to keep matrix-js-sdk responsive.
-const MAX_NUMBER_OF_VISIBILITY_EVENTS_TO_SCAN_THROUGH = 30;
-
-export type NotificationCount = Partial<Record<NotificationCountType, number>>;
-
-export enum NotificationCountType {
- Highlight = "highlight",
- Total = "total",
-}
-
-export interface ICreateFilterOpts {
- // Populate the filtered timeline with already loaded events in the room
- // timeline. Useful to disable for some filters that can't be achieved by the
- // client in an efficient manner
- prepopulateTimeline?: boolean;
- useSyncEvents?: boolean;
- pendingEvents?: boolean;
-}
-
-export enum RoomEvent {
- MyMembership = "Room.myMembership",
- Tags = "Room.tags",
- AccountData = "Room.accountData",
- Receipt = "Room.receipt",
- Name = "Room.name",
- Redaction = "Room.redaction",
- RedactionCancelled = "Room.redactionCancelled",
- LocalEchoUpdated = "Room.localEchoUpdated",
- Timeline = "Room.timeline",
- TimelineReset = "Room.timelineReset",
- TimelineRefresh = "Room.TimelineRefresh",
- OldStateUpdated = "Room.OldStateUpdated",
- CurrentStateUpdated = "Room.CurrentStateUpdated",
- HistoryImportedWithinTimeline = "Room.historyImportedWithinTimeline",
- UnreadNotifications = "Room.UnreadNotifications",
-}
-
-export type RoomEmittedEvents =
- | RoomEvent
- | RoomStateEvent.Events
- | RoomStateEvent.Members
- | RoomStateEvent.NewMember
- | RoomStateEvent.Update
- | RoomStateEvent.Marker
- | ThreadEvent.New
- | ThreadEvent.Update
- | ThreadEvent.NewReply
- | ThreadEvent.Delete
- | MatrixEventEvent.BeforeRedaction
- | BeaconEvent.New
- | BeaconEvent.Update
- | BeaconEvent.Destroy
- | BeaconEvent.LivenessChange
- | PollEvent.New;
-
-export type RoomEventHandlerMap = {
- /**
- * Fires when the logged in user's membership in the room is updated.
- *
- * @param room - The room in which the membership has been updated
- * @param membership - The new membership value
- * @param prevMembership - The previous membership value
- */
- [RoomEvent.MyMembership]: (room: Room, membership: string, prevMembership?: string) => void;
- /**
- * Fires whenever a room's tags are updated.
- * @param event - The tags event
- * @param room - The room whose Room.tags was updated.
- * @example
- * ```
- * matrixClient.on("Room.tags", function(event, room){
- * var newTags = event.getContent().tags;
- * if (newTags["favourite"]) showStar(room);
- * });
- * ```
- */
- [RoomEvent.Tags]: (event: MatrixEvent, room: Room) => void;
- /**
- * Fires whenever a room's account_data is updated.
- * @param event - The account_data event
- * @param room - The room whose account_data was updated.
- * @param prevEvent - The event being replaced by
- * the new account data, if known.
- * @example
- * ```
- * matrixClient.on("Room.accountData", function(event, room, oldEvent){
- * if (event.getType() === "m.room.colorscheme") {
- * applyColorScheme(event.getContents());
- * }
- * });
- * ```
- */
- [RoomEvent.AccountData]: (event: MatrixEvent, room: Room, lastEvent?: MatrixEvent) => void;
- /**
- * Fires whenever a receipt is received for a room
- * @param event - The receipt event
- * @param room - The room whose receipts was updated.
- * @example
- * ```
- * matrixClient.on("Room.receipt", function(event, room){
- * var receiptContent = event.getContent();
- * });
- * ```
- */
- [RoomEvent.Receipt]: (event: MatrixEvent, room: Room) => void;
- /**
- * Fires whenever the name of a room is updated.
- * @param room - The room whose Room.name was updated.
- * @example
- * ```
- * matrixClient.on("Room.name", function(room){
- * var newName = room.name;
- * });
- * ```
- */
- [RoomEvent.Name]: (room: Room) => void;
- /**
- * Fires when an event we had previously received is redacted.
- *
- * (Note this is *not* fired when the redaction happens before we receive the
- * event).
- *
- * @param event - The matrix redaction event
- * @param room - The room containing the redacted event
- */
- [RoomEvent.Redaction]: (event: MatrixEvent, room: Room) => void;
- /**
- * Fires when an event that was previously redacted isn't anymore.
- * This happens when the redaction couldn't be sent and
- * was subsequently cancelled by the user. Redactions have a local echo
- * which is undone in this scenario.
- *
- * @param event - The matrix redaction event that was cancelled.
- * @param room - The room containing the unredacted event
- */
- [RoomEvent.RedactionCancelled]: (event: MatrixEvent, room: Room) => void;
- /**
- * Fires when the status of a transmitted event is updated.
- *
- * <p>When an event is first transmitted, a temporary copy of the event is
- * inserted into the timeline, with a temporary event id, and a status of
- * 'SENDING'.
- *
- * <p>Once the echo comes back from the server, the content of the event
- * (MatrixEvent.event) is replaced by the complete event from the homeserver,
- * thus updating its event id, as well as server-generated fields such as the
- * timestamp. Its status is set to null.
- *
- * <p>Once the /send request completes, if the remote echo has not already
- * arrived, the event is updated with a new event id and the status is set to
- * 'SENT'. The server-generated fields are of course not updated yet.
- *
- * <p>If the /send fails, In this case, the event's status is set to
- * 'NOT_SENT'. If it is later resent, the process starts again, setting the
- * status to 'SENDING'. Alternatively, the message may be cancelled, which
- * removes the event from the room, and sets the status to 'CANCELLED'.
- *
- * <p>This event is raised to reflect each of the transitions above.
- *
- * @param event - The matrix event which has been updated
- *
- * @param room - The room containing the redacted event
- *
- * @param oldEventId - The previous event id (the temporary event id,
- * except when updating a successfully-sent event when its echo arrives)
- *
- * @param oldStatus - The previous event status.
- */
- [RoomEvent.LocalEchoUpdated]: (
- event: MatrixEvent,
- room: Room,
- oldEventId?: string,
- oldStatus?: EventStatus | null,
- ) => void;
- [RoomEvent.OldStateUpdated]: (room: Room, previousRoomState: RoomState, roomState: RoomState) => void;
- [RoomEvent.CurrentStateUpdated]: (room: Room, previousRoomState: RoomState, roomState: RoomState) => void;
- [RoomEvent.HistoryImportedWithinTimeline]: (markerEvent: MatrixEvent, room: Room) => void;
- [RoomEvent.UnreadNotifications]: (unreadNotifications?: NotificationCount, threadId?: string) => void;
- [RoomEvent.TimelineRefresh]: (room: Room, eventTimelineSet: EventTimelineSet) => void;
- [ThreadEvent.New]: (thread: Thread, toStartOfTimeline: boolean) => void;
- /**
- * Fires when a new poll instance is added to the room state
- * @param poll - the new poll
- */
- [PollEvent.New]: (poll: Poll) => void;
-} & Pick<ThreadHandlerMap, ThreadEvent.Update | ThreadEvent.NewReply | ThreadEvent.Delete> &
- EventTimelineSetHandlerMap &
- Pick<MatrixEventHandlerMap, MatrixEventEvent.BeforeRedaction> &
- Pick<
- RoomStateEventHandlerMap,
- | RoomStateEvent.Events
- | RoomStateEvent.Members
- | RoomStateEvent.NewMember
- | RoomStateEvent.Update
- | RoomStateEvent.Marker
- | BeaconEvent.New
- > &
- Pick<BeaconEventHandlerMap, BeaconEvent.Update | BeaconEvent.Destroy | BeaconEvent.LivenessChange>;
-
-export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
- public readonly reEmitter: TypedReEmitter<RoomEmittedEvents, RoomEventHandlerMap>;
- private txnToEvent: Map<string, MatrixEvent> = new Map(); // Pending in-flight requests { string: MatrixEvent }
- private notificationCounts: NotificationCount = {};
- private readonly threadNotifications = new Map<string, NotificationCount>();
- public readonly cachedThreadReadReceipts = new Map<string, CachedReceiptStructure[]>();
- // Useful to know at what point the current user has started using threads in this room
- private oldestThreadedReceiptTs = Infinity;
- /**
- * A record of the latest unthread receipts per user
- * This is useful in determining whether a user has read a thread or not
- */
- private unthreadedReceipts = new Map<string, Receipt>();
- private readonly timelineSets: EventTimelineSet[];
- public readonly polls: Map<string, Poll> = new Map<string, Poll>();
- public readonly threadsTimelineSets: EventTimelineSet[] = [];
- // any filtered timeline sets we're maintaining for this room
- private readonly filteredTimelineSets: Record<string, EventTimelineSet> = {}; // filter_id: timelineSet
- private timelineNeedsRefresh = false;
- private readonly pendingEventList?: MatrixEvent[];
- // read by megolm via getter; boolean value - null indicates "use global value"
- private blacklistUnverifiedDevices?: boolean;
- private selfMembership?: string;
- private summaryHeroes: string[] | null = null;
- // flags to stop logspam about missing m.room.create events
- private getTypeWarning = false;
- private getVersionWarning = false;
- private membersPromise?: Promise<boolean>;
-
- // XXX: These should be read-only
- /**
- * The human-readable display name for this room.
- */
- public name: string;
- /**
- * The un-homoglyphed name for this room.
- */
- public normalizedName: string;
- /**
- * Dict of room tags; the keys are the tag name and the values
- * are any metadata associated with the tag - e.g. `{ "fav" : { order: 1 } }`
- */
- public tags: Record<string, Record<string, any>> = {}; // $tagName: { $metadata: $value }
- /**
- * accountData Dict of per-room account_data events; the keys are the
- * event type and the values are the events.
- */
- public accountData: Map<string, MatrixEvent> = new Map(); // $eventType: $event
- /**
- * The room summary.
- */
- public summary: RoomSummary | null = null;
- // legacy fields
- /**
- * The live event timeline for this room, with the oldest event at index 0.
- * Present for backwards compatibility - prefer getLiveTimeline().getEvents()
- */
- public timeline!: MatrixEvent[];
- /**
- * oldState The state of the room at the time of the oldest
- * event in the live timeline. Present for backwards compatibility -
- * prefer getLiveTimeline().getState(EventTimeline.BACKWARDS).
- */
- public oldState!: RoomState;
- /**
- * currentState The state of the room at the time of the
- * newest event in the timeline. Present for backwards compatibility -
- * prefer getLiveTimeline().getState(EventTimeline.FORWARDS).
- */
- public currentState!: RoomState;
- public readonly relations = new RelationsContainer(this.client, this);
-
- /**
- * A collection of events known by the client
- * This is not a comprehensive list of the threads that exist in this room
- */
- private threads = new Map<string, Thread>();
- public lastThread?: Thread;
-
- /**
- * A mapping of eventId to all visibility changes to apply
- * to the event, by chronological order, as per
- * https://github.com/matrix-org/matrix-doc/pull/3531
- *
- * # Invariants
- *
- * - within each list, all events are classed by
- * chronological order;
- * - all events are events such that
- * `asVisibilityEvent()` returns a non-null `IVisibilityChange`;
- * - within each list with key `eventId`, all events
- * are in relation to `eventId`.
- *
- * @experimental
- */
- private visibilityEvents = new Map<string, MatrixEvent[]>();
-
- /**
- * Construct a new Room.
- *
- * <p>For a room, we store an ordered sequence of timelines, which may or may not
- * be continuous. Each timeline lists a series of events, as well as tracking
- * the room state at the start and the end of the timeline. It also tracks
- * forward and backward pagination tokens, as well as containing links to the
- * next timeline in the sequence.
- *
- * <p>There is one special timeline - the 'live' timeline, which represents the
- * timeline to which events are being added in real-time as they are received
- * from the /sync API. Note that you should not retain references to this
- * timeline - even if it is the current timeline right now, it may not remain
- * so if the server gives us a timeline gap in /sync.
- *
- * <p>In order that we can find events from their ids later, we also maintain a
- * map from event_id to timeline and index.
- *
- * @param roomId - Required. The ID of this room.
- * @param client - Required. The client, used to lazy load members.
- * @param myUserId - Required. The ID of the syncing user.
- * @param opts - Configuration options
- */
- public constructor(
- public readonly roomId: string,
- public readonly client: MatrixClient,
- public readonly myUserId: string,
- private readonly opts: IOpts = {},
- ) {
- super();
- // In some cases, we add listeners for every displayed Matrix event, so it's
- // common to have quite a few more than the default limit.
- this.setMaxListeners(100);
- this.reEmitter = new TypedReEmitter(this);
-
- opts.pendingEventOrdering = opts.pendingEventOrdering || PendingEventOrdering.Chronological;
-
- this.name = roomId;
- this.normalizedName = roomId;
-
- // all our per-room timeline sets. the first one is the unfiltered ones;
- // the subsequent ones are the filtered ones in no particular order.
- this.timelineSets = [new EventTimelineSet(this, opts)];
- this.reEmitter.reEmit(this.getUnfilteredTimelineSet(), [RoomEvent.Timeline, RoomEvent.TimelineReset]);
-
- this.fixUpLegacyTimelineFields();
-
- if (this.opts.pendingEventOrdering === PendingEventOrdering.Detached) {
- this.pendingEventList = [];
- this.client.store.getPendingEvents(this.roomId).then((events) => {
- const mapper = this.client.getEventMapper({
- toDevice: false,
- decrypt: false,
- });
- events.forEach(async (serializedEvent: Partial<IEvent>) => {
- const event = mapper(serializedEvent);
- await client.decryptEventIfNeeded(event);
- event.setStatus(EventStatus.NOT_SENT);
- this.addPendingEvent(event, event.getTxnId()!);
- });
- });
- }
-
- // awaited by getEncryptionTargetMembers while room members are loading
- if (!this.opts.lazyLoadMembers) {
- this.membersPromise = Promise.resolve(false);
- } else {
- this.membersPromise = undefined;
- }
- }
-
- private threadTimelineSetsPromise: Promise<[EventTimelineSet, EventTimelineSet]> | null = null;
- public async createThreadsTimelineSets(): Promise<[EventTimelineSet, EventTimelineSet] | null> {
- if (this.threadTimelineSetsPromise) {
- return this.threadTimelineSetsPromise;
- }
-
- if (this.client?.supportsThreads()) {
- try {
- this.threadTimelineSetsPromise = Promise.all([
- this.createThreadTimelineSet(),
- this.createThreadTimelineSet(ThreadFilterType.My),
- ]);
- const timelineSets = await this.threadTimelineSetsPromise;
- this.threadsTimelineSets.push(...timelineSets);
- return timelineSets;
- } catch (e) {
- this.threadTimelineSetsPromise = null;
- return null;
- }
- }
- return null;
- }
-
- /**
- * Bulk decrypt critical events in a room
- *
- * Critical events represents the minimal set of events to decrypt
- * for a typical UI to function properly
- *
- * - Last event of every room (to generate likely message preview)
- * - All events up to the read receipt (to calculate an accurate notification count)
- *
- * @returns Signals when all events have been decrypted
- */
- public async decryptCriticalEvents(): Promise<void> {
- if (!this.client.isCryptoEnabled()) return;
-
- const readReceiptEventId = this.getEventReadUpTo(this.client.getUserId()!, true);
- const events = this.getLiveTimeline().getEvents();
- const readReceiptTimelineIndex = events.findIndex((matrixEvent) => {
- return matrixEvent.event.event_id === readReceiptEventId;
- });
-
- const decryptionPromises = events
- .slice(readReceiptTimelineIndex)
- .reverse()
- .map((event) => this.client.decryptEventIfNeeded(event, { isRetry: true }));
-
- await Promise.allSettled(decryptionPromises);
- }
-
- /**
- * Bulk decrypt events in a room
- *
- * @returns Signals when all events have been decrypted
- */
- public async decryptAllEvents(): Promise<void> {
- if (!this.client.isCryptoEnabled()) return;
-
- const decryptionPromises = this.getUnfilteredTimelineSet()
- .getLiveTimeline()
- .getEvents()
- .slice(0) // copy before reversing
- .reverse()
- .map((event) => this.client.decryptEventIfNeeded(event, { isRetry: true }));
-
- await Promise.allSettled(decryptionPromises);
- }
-
- /**
- * Gets the creator of the room
- * @returns The creator of the room, or null if it could not be determined
- */
- public getCreator(): string | null {
- const createEvent = this.currentState.getStateEvents(EventType.RoomCreate, "");
- return createEvent?.getContent()["creator"] ?? null;
- }
-
- /**
- * Gets the version of the room
- * @returns The version of the room, or null if it could not be determined
- */
- public getVersion(): string {
- const createEvent = this.currentState.getStateEvents(EventType.RoomCreate, "");
- if (!createEvent) {
- if (!this.getVersionWarning) {
- logger.warn("[getVersion] Room " + this.roomId + " does not have an m.room.create event");
- this.getVersionWarning = true;
- }
- return "1";
- }
- return createEvent.getContent()["room_version"] ?? "1";
- }
-
- /**
- * Determines whether this room needs to be upgraded to a new version
- * @returns What version the room should be upgraded to, or null if
- * the room does not require upgrading at this time.
- * @deprecated Use #getRecommendedVersion() instead
- */
- public shouldUpgradeToVersion(): string | null {
- // TODO: Remove this function.
- // This makes assumptions about which versions are safe, and can easily
- // be wrong. Instead, people are encouraged to use getRecommendedVersion
- // which determines a safer value. This function doesn't use that function
- // because this is not async-capable, and to avoid breaking the contract
- // we're deprecating this.
-
- if (!SAFE_ROOM_VERSIONS.includes(this.getVersion())) {
- return KNOWN_SAFE_ROOM_VERSION;
- }
-
- return null;
- }
-
- /**
- * Determines the recommended room version for the room. This returns an
- * object with 3 properties: `version` as the new version the
- * room should be upgraded to (may be the same as the current version);
- * `needsUpgrade` to indicate if the room actually can be
- * upgraded (ie: does the current version not match?); and `urgent`
- * to indicate if the new version patches a vulnerability in a previous
- * version.
- * @returns
- * Resolves to the version the room should be upgraded to.
- */
- public async getRecommendedVersion(): Promise<IRecommendedVersion> {
- const capabilities = await this.client.getCapabilities();
- let versionCap = capabilities["m.room_versions"];
- if (!versionCap) {
- versionCap = {
- default: KNOWN_SAFE_ROOM_VERSION,
- available: {},
- };
- for (const safeVer of SAFE_ROOM_VERSIONS) {
- versionCap.available[safeVer] = RoomVersionStability.Stable;
- }
- }
-
- let result = this.checkVersionAgainstCapability(versionCap);
- if (result.urgent && result.needsUpgrade) {
- // Something doesn't feel right: we shouldn't need to update
- // because the version we're on should be in the protocol's
- // namespace. This usually means that the server was updated
- // before the client was, making us think the newest possible
- // room version is not stable. As a solution, we'll refresh
- // the capability we're using to determine this.
- logger.warn(
- "Refreshing room version capability because the server looks " +
- "to be supporting a newer room version we don't know about.",
- );
-
- const caps = await this.client.getCapabilities(true);
- versionCap = caps["m.room_versions"];
- if (!versionCap) {
- logger.warn("No room version capability - assuming upgrade required.");
- return result;
- } else {
- result = this.checkVersionAgainstCapability(versionCap);
- }
- }
-
- return result;
- }
-
- private checkVersionAgainstCapability(versionCap: IRoomVersionsCapability): IRecommendedVersion {
- const currentVersion = this.getVersion();
- logger.log(`[${this.roomId}] Current version: ${currentVersion}`);
- logger.log(`[${this.roomId}] Version capability: `, versionCap);
-
- const result: IRecommendedVersion = {
- version: currentVersion,
- needsUpgrade: false,
- urgent: false,
- };
-
- // If the room is on the default version then nothing needs to change
- if (currentVersion === versionCap.default) return result;
-
- const stableVersions = Object.keys(versionCap.available).filter((v) => versionCap.available[v] === "stable");
-
- // Check if the room is on an unstable version. We determine urgency based
- // off the version being in the Matrix spec namespace or not (if the version
- // is in the current namespace and unstable, the room is probably vulnerable).
- if (!stableVersions.includes(currentVersion)) {
- result.version = versionCap.default;
- result.needsUpgrade = true;
- result.urgent = !!this.getVersion().match(/^[0-9]+[0-9.]*$/g);
- if (result.urgent) {
- logger.warn(`URGENT upgrade required on ${this.roomId}`);
- } else {
- logger.warn(`Non-urgent upgrade required on ${this.roomId}`);
- }
- return result;
- }
-
- // The room is on a stable, but non-default, version by this point.
- // No upgrade needed.
- return result;
- }
-
- /**
- * Determines whether the given user is permitted to perform a room upgrade
- * @param userId - The ID of the user to test against
- * @returns True if the given user is permitted to upgrade the room
- */
- public userMayUpgradeRoom(userId: string): boolean {
- return this.currentState.maySendStateEvent(EventType.RoomTombstone, userId);
- }
-
- /**
- * Get the list of pending sent events for this room
- *
- * @returns A list of the sent events
- * waiting for remote echo.
- *
- * @throws If `opts.pendingEventOrdering` was not 'detached'
- */
- public getPendingEvents(): MatrixEvent[] {
- if (!this.pendingEventList) {
- throw new Error(
- "Cannot call getPendingEvents with pendingEventOrdering == " + this.opts.pendingEventOrdering,
- );
- }
-
- return this.pendingEventList;
- }
-
- /**
- * Removes a pending event for this room
- *
- * @returns True if an element was removed.
- */
- public removePendingEvent(eventId: string): boolean {
- if (!this.pendingEventList) {
- throw new Error(
- "Cannot call removePendingEvent with pendingEventOrdering == " + this.opts.pendingEventOrdering,
- );
- }
-
- const removed = utils.removeElement(
- this.pendingEventList,
- function (ev) {
- return ev.getId() == eventId;
- },
- false,
- );
-
- this.savePendingEvents();
-
- return removed;
- }
-
- /**
- * Check whether the pending event list contains a given event by ID.
- * If pending event ordering is not "detached" then this returns false.
- *
- * @param eventId - The event ID to check for.
- */
- public hasPendingEvent(eventId: string): boolean {
- return this.pendingEventList?.some((event) => event.getId() === eventId) ?? false;
- }
-
- /**
- * Get a specific event from the pending event list, if configured, null otherwise.
- *
- * @param eventId - The event ID to check for.
- */
- public getPendingEvent(eventId: string): MatrixEvent | null {
- return this.pendingEventList?.find((event) => event.getId() === eventId) ?? null;
- }
-
- /**
- * Get the live unfiltered timeline for this room.
- *
- * @returns live timeline
- */
- public getLiveTimeline(): EventTimeline {
- return this.getUnfilteredTimelineSet().getLiveTimeline();
- }
-
- /**
- * Get the timestamp of the last message in the room
- *
- * @returns the timestamp of the last message in the room
- */
- public getLastActiveTimestamp(): number {
- const timeline = this.getLiveTimeline();
- const events = timeline.getEvents();
- if (events.length) {
- const lastEvent = events[events.length - 1];
- return lastEvent.getTs();
- } else {
- return Number.MIN_SAFE_INTEGER;
- }
- }
-
- /**
- * @returns the membership type (join | leave | invite) for the logged in user
- */
- public getMyMembership(): string {
- return this.selfMembership ?? "leave";
- }
-
- /**
- * If this room is a DM we're invited to,
- * try to find out who invited us
- * @returns user id of the inviter
- */
- public getDMInviter(): string | undefined {
- const me = this.getMember(this.myUserId);
- if (me) {
- return me.getDMInviter();
- }
-
- if (this.selfMembership === "invite") {
- // fall back to summary information
- const memberCount = this.getInvitedAndJoinedMemberCount();
- if (memberCount === 2) {
- return this.summaryHeroes?.[0];
- }
- }
- }
-
- /**
- * Assuming this room is a DM room, tries to guess with which user.
- * @returns user id of the other member (could be syncing user)
- */
- public guessDMUserId(): string {
- const me = this.getMember(this.myUserId);
- if (me) {
- const inviterId = me.getDMInviter();
- if (inviterId) {
- return inviterId;
- }
- }
- // Remember, we're assuming this room is a DM, so returning the first member we find should be fine
- if (Array.isArray(this.summaryHeroes) && this.summaryHeroes.length) {
- return this.summaryHeroes[0];
- }
- const members = this.currentState.getMembers();
- const anyMember = members.find((m) => m.userId !== this.myUserId);
- if (anyMember) {
- return anyMember.userId;
- }
- // it really seems like I'm the only user in the room
- // so I probably created a room with just me in it
- // and marked it as a DM. Ok then
- return this.myUserId;
- }
-
- public getAvatarFallbackMember(): RoomMember | undefined {
- const memberCount = this.getInvitedAndJoinedMemberCount();
- if (memberCount > 2) {
- return;
- }
- const hasHeroes = Array.isArray(this.summaryHeroes) && this.summaryHeroes.length;
- if (hasHeroes) {
- const availableMember = this.summaryHeroes!.map((userId) => {
- return this.getMember(userId);
- }).find((member) => !!member);
- if (availableMember) {
- return availableMember;
- }
- }
- const members = this.currentState.getMembers();
- // could be different than memberCount
- // as this includes left members
- if (members.length <= 2) {
- const availableMember = members.find((m) => {
- return m.userId !== this.myUserId;
- });
- if (availableMember) {
- return availableMember;
- }
- }
- // if all else fails, try falling back to a user,
- // and create a one-off member for it
- if (hasHeroes) {
- const availableUser = this.summaryHeroes!.map((userId) => {
- return this.client.getUser(userId);
- }).find((user) => !!user);
- if (availableUser) {
- const member = new RoomMember(this.roomId, availableUser.userId);
- member.user = availableUser;
- return member;
- }
- }
- }
-
- /**
- * Sets the membership this room was received as during sync
- * @param membership - join | leave | invite
- */
- public updateMyMembership(membership: string): void {
- const prevMembership = this.selfMembership;
- this.selfMembership = membership;
- if (prevMembership !== membership) {
- if (membership === "leave") {
- this.cleanupAfterLeaving();
- }
- this.emit(RoomEvent.MyMembership, this, membership, prevMembership);
- }
- }
-
- private async loadMembersFromServer(): Promise<IStateEventWithRoomId[]> {
- const lastSyncToken = this.client.store.getSyncToken();
- const response = await this.client.members(this.roomId, undefined, "leave", lastSyncToken ?? undefined);
- return response.chunk;
- }
-
- private async loadMembers(): Promise<{ memberEvents: MatrixEvent[]; fromServer: boolean }> {
- // were the members loaded from the server?
- 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
- // 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))) {
- fromServer = true;
- rawMembersEvents = await this.loadMembersFromServer();
- logger.log(`LL: got ${rawMembersEvents.length} ` + `members from server for room ${this.roomId}`);
- }
- const memberEvents = rawMembersEvents.filter(noUnsafeEventProps).map(this.client.getEventMapper());
- return { memberEvents, fromServer };
- }
-
- /**
- * Check if loading of out-of-band-members has completed
- *
- * @returns true if the full membership list of this room has been loaded (including if lazy-loading is disabled).
- * False if the load is not started or is in progress.
- */
- public membersLoaded(): boolean {
- if (!this.opts.lazyLoadMembers) {
- return true;
- }
-
- return this.currentState.outOfBandMembersReady();
- }
-
- /**
- * Preloads the member list in case lazy loading
- * of memberships is in use. Can be called multiple times,
- * it will only preload once.
- * @returns when preloading is done and
- * accessing the members on the room will take
- * all members in the room into account
- */
- public loadMembersIfNeeded(): Promise<boolean> {
- if (this.membersPromise) {
- return this.membersPromise;
- }
-
- // mark the state so that incoming messages while
- // the request is in flight get marked as superseding
- // the OOB members
- this.currentState.markOutOfBandMembersStarted();
-
- const inMemoryUpdate = this.loadMembers()
- .then((result) => {
- this.currentState.setOutOfBandMembers(result.memberEvents);
- return result.fromServer;
- })
- .catch((err) => {
- // allow retries on fail
- this.membersPromise = undefined;
- this.currentState.markOutOfBandMembersFailed();
- throw err;
- });
- // update members in storage, but don't wait for it
- inMemoryUpdate
- .then((fromServer) => {
- if (fromServer) {
- const oobMembers = this.currentState
- .getMembers()
- .filter((m) => m.isOutOfBand())
- .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;
- return (
- store
- .setOutOfBandMembers(this.roomId, oobMembers)
- // swallow any IDB error as we don't want to fail
- // because of this
- .catch((err) => {
- logger.log("LL: storing OOB room members failed, oh well", err);
- })
- );
- }
- })
- .catch((err) => {
- // as this is not awaited anywhere,
- // at least show the error in the console
- logger.error(err);
- });
-
- this.membersPromise = inMemoryUpdate;
-
- return this.membersPromise;
- }
-
- /**
- * Removes the lazily loaded members from storage if needed
- */
- public async clearLoadedMembersIfNeeded(): Promise<void> {
- if (this.opts.lazyLoadMembers && this.membersPromise) {
- await this.loadMembersIfNeeded();
- await this.client.store.clearOutOfBandMembers(this.roomId);
- this.currentState.clearOutOfBandMembers();
- this.membersPromise = undefined;
- }
- }
-
- /**
- * called when sync receives this room in the leave section
- * to do cleanup after leaving a room. Possibly called multiple times.
- */
- private cleanupAfterLeaving(): void {
- this.clearLoadedMembersIfNeeded().catch((err) => {
- logger.error(`error after clearing loaded members from ` + `room ${this.roomId} after leaving`);
- logger.log(err);
- });
- }
-
- /**
- * Empty out the current live timeline and re-request it. This is used when
- * historical messages are imported into the room via MSC2716 `/batch_send`
- * because the client may already have that section of the timeline loaded.
- * We need to force the client to throw away their current timeline so that
- * when they back paginate over the area again with the historical messages
- * in between, it grabs the newly imported messages. We can listen for
- * `UNSTABLE_MSC2716_MARKER`, in order to tell when historical messages are ready
- * to be discovered in the room and the timeline needs a refresh. The SDK
- * emits a `RoomEvent.HistoryImportedWithinTimeline` event when we detect a
- * valid marker and can check the needs refresh status via
- * `room.getTimelineNeedsRefresh()`.
- */
- public async refreshLiveTimeline(): Promise<void> {
- const liveTimelineBefore = this.getLiveTimeline();
- const forwardPaginationToken = liveTimelineBefore.getPaginationToken(EventTimeline.FORWARDS);
- const backwardPaginationToken = liveTimelineBefore.getPaginationToken(EventTimeline.BACKWARDS);
- const eventsBefore = liveTimelineBefore.getEvents();
- const mostRecentEventInTimeline = eventsBefore[eventsBefore.length - 1];
- logger.log(
- `[refreshLiveTimeline for ${this.roomId}] at ` +
- `mostRecentEventInTimeline=${mostRecentEventInTimeline && mostRecentEventInTimeline.getId()} ` +
- `liveTimelineBefore=${liveTimelineBefore.toString()} ` +
- `forwardPaginationToken=${forwardPaginationToken} ` +
- `backwardPaginationToken=${backwardPaginationToken}`,
- );
-
- // Get the main TimelineSet
- const timelineSet = this.getUnfilteredTimelineSet();
-
- let newTimeline: Optional<EventTimeline>;
- // If there isn't any event in the timeline, let's go fetch the latest
- // event and construct a timeline from it.
- //
- // This should only really happen if the user ran into an error
- // with refreshing the timeline before which left them in a blank
- // timeline from `resetLiveTimeline`.
- if (!mostRecentEventInTimeline) {
- newTimeline = await this.client.getLatestTimeline(timelineSet);
- } else {
- // Empty out all of `this.timelineSets`. But we also need to keep the
- // same `timelineSet` references around so the React code updates
- // properly and doesn't ignore the room events we emit because it checks
- // that the `timelineSet` references are the same. We need the
- // `timelineSet` empty so that the `client.getEventTimeline(...)` call
- // later, will call `/context` and create a new timeline instead of
- // returning the same one.
- this.resetLiveTimeline(null, null);
-
- // Make the UI timeline show the new blank live timeline we just
- // reset so that if the network fails below it's showing the
- // accurate state of what we're working with instead of the
- // disconnected one in the TimelineWindow which is just hanging
- // around by reference.
- this.emit(RoomEvent.TimelineRefresh, this, timelineSet);
-
- // Use `client.getEventTimeline(...)` to construct a new timeline from a
- // `/context` response state and events for the most recent event before
- // we reset everything. The `timelineSet` we pass in needs to be empty
- // in order for this function to call `/context` and generate a new
- // timeline.
- newTimeline = await this.client.getEventTimeline(timelineSet, mostRecentEventInTimeline.getId()!);
- }
-
- // If a racing `/sync` beat us to creating a new timeline, use that
- // instead because it's the latest in the room and any new messages in
- // the scrollback will include the history.
- const liveTimeline = timelineSet.getLiveTimeline();
- if (
- !liveTimeline ||
- (liveTimeline.getPaginationToken(Direction.Forward) === null &&
- liveTimeline.getPaginationToken(Direction.Backward) === null &&
- liveTimeline.getEvents().length === 0)
- ) {
- logger.log(`[refreshLiveTimeline for ${this.roomId}] using our new live timeline`);
- // Set the pagination token back to the live sync token (`null`) instead
- // of using the `/context` historical token (ex. `t12-13_0_0_0_0_0_0_0_0`)
- // so that it matches the next response from `/sync` and we can properly
- // continue the timeline.
- newTimeline!.setPaginationToken(forwardPaginationToken, EventTimeline.FORWARDS);
-
- // Set our new fresh timeline as the live timeline to continue syncing
- // forwards and back paginating from.
- timelineSet.setLiveTimeline(newTimeline!);
- // Fixup `this.oldstate` so that `scrollback` has the pagination tokens
- // available
- this.fixUpLegacyTimelineFields();
- } else {
- logger.log(
- `[refreshLiveTimeline for ${this.roomId}] \`/sync\` or some other request beat us to creating a new ` +
- `live timeline after we reset it. We'll use that instead since any events in the scrollback from ` +
- `this timeline will include the history.`,
- );
- }
-
- // The timeline has now been refreshed ✅
- this.setTimelineNeedsRefresh(false);
-
- // Emit an event which clients can react to and re-load the timeline
- // from the SDK
- this.emit(RoomEvent.TimelineRefresh, this, timelineSet);
- }
-
- /**
- * Reset the live timeline of all timelineSets, and start new ones.
- *
- * <p>This is used when /sync returns a 'limited' timeline.
- *
- * @param backPaginationToken - token for back-paginating the new timeline
- * @param forwardPaginationToken - token for forward-paginating the old live timeline,
- * if absent or null, all timelines are reset, removing old ones (including the previous live
- * timeline which would otherwise be unable to paginate forwards without this token).
- * Removing just the old live timeline whilst preserving previous ones is not supported.
- */
- public resetLiveTimeline(backPaginationToken?: string | null, forwardPaginationToken?: string | null): void {
- for (const timelineSet of this.timelineSets) {
- timelineSet.resetLiveTimeline(backPaginationToken ?? undefined, forwardPaginationToken ?? undefined);
- }
- for (const thread of this.threads.values()) {
- thread.resetLiveTimeline(backPaginationToken, forwardPaginationToken);
- }
-
- this.fixUpLegacyTimelineFields();
- }
-
- /**
- * Fix up this.timeline, this.oldState and this.currentState
- *
- * @internal
- */
- private fixUpLegacyTimelineFields(): void {
- const previousOldState = this.oldState;
- const previousCurrentState = this.currentState;
-
- // maintain this.timeline as a reference to the live timeline,
- // and this.oldState and this.currentState as references to the
- // state at the start and end of that timeline. These are more
- // for backwards-compatibility than anything else.
- this.timeline = this.getLiveTimeline().getEvents();
- this.oldState = this.getLiveTimeline().getState(EventTimeline.BACKWARDS)!;
- this.currentState = this.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
-
- // Let people know to register new listeners for the new state
- // references. The reference won't necessarily change every time so only
- // emit when we see a change.
- if (previousOldState !== this.oldState) {
- this.emit(RoomEvent.OldStateUpdated, this, previousOldState, this.oldState);
- }
-
- if (previousCurrentState !== this.currentState) {
- this.emit(RoomEvent.CurrentStateUpdated, this, previousCurrentState, this.currentState);
-
- // Re-emit various events on the current room state
- // TODO: If currentState really only exists for backwards
- // compatibility, shouldn't we be doing this some other way?
- this.reEmitter.stopReEmitting(previousCurrentState, [
- RoomStateEvent.Events,
- RoomStateEvent.Members,
- RoomStateEvent.NewMember,
- RoomStateEvent.Update,
- RoomStateEvent.Marker,
- BeaconEvent.New,
- BeaconEvent.Update,
- BeaconEvent.Destroy,
- BeaconEvent.LivenessChange,
- ]);
- this.reEmitter.reEmit(this.currentState, [
- RoomStateEvent.Events,
- RoomStateEvent.Members,
- RoomStateEvent.NewMember,
- RoomStateEvent.Update,
- RoomStateEvent.Marker,
- BeaconEvent.New,
- BeaconEvent.Update,
- BeaconEvent.Destroy,
- BeaconEvent.LivenessChange,
- ]);
- }
- }
-
- /**
- * Returns whether there are any devices in the room that are unverified
- *
- * Note: Callers should first check if crypto is enabled on this device. If it is
- * disabled, then we aren't tracking room devices at all, so we can't answer this, and an
- * error will be thrown.
- *
- * @returns the result
- */
- public async hasUnverifiedDevices(): Promise<boolean> {
- if (!this.client.isRoomEncrypted(this.roomId)) {
- return false;
- }
- const e2eMembers = await this.getEncryptionTargetMembers();
- for (const member of e2eMembers) {
- const devices = this.client.getStoredDevicesForUser(member.userId);
- if (devices.some((device) => device.isUnverified())) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Return the timeline sets for this room.
- * @returns array of timeline sets for this room
- */
- public getTimelineSets(): EventTimelineSet[] {
- return this.timelineSets;
- }
-
- /**
- * Helper to return the main unfiltered timeline set for this room
- * @returns room's unfiltered timeline set
- */
- public getUnfilteredTimelineSet(): EventTimelineSet {
- return this.timelineSets[0];
- }
-
- /**
- * Get the timeline which contains the given event from the unfiltered set, if any
- *
- * @param eventId - event ID to look for
- * @returns timeline containing
- * the given event, or null if unknown
- */
- public getTimelineForEvent(eventId: string): EventTimeline | null {
- const event = this.findEventById(eventId);
- const thread = this.findThreadForEvent(event);
- if (thread) {
- return thread.timelineSet.getTimelineForEvent(eventId);
- } else {
- return this.getUnfilteredTimelineSet().getTimelineForEvent(eventId);
- }
- }
-
- /**
- * Add a new timeline to this room's unfiltered timeline set
- *
- * @returns newly-created timeline
- */
- public addTimeline(): EventTimeline {
- return this.getUnfilteredTimelineSet().addTimeline();
- }
-
- /**
- * Whether the timeline needs to be refreshed in order to pull in new
- * historical messages that were imported.
- * @param value - The value to set
- */
- public setTimelineNeedsRefresh(value: boolean): void {
- this.timelineNeedsRefresh = value;
- }
-
- /**
- * Whether the timeline needs to be refreshed in order to pull in new
- * historical messages that were imported.
- * @returns .
- */
- public getTimelineNeedsRefresh(): boolean {
- return this.timelineNeedsRefresh;
- }
-
- /**
- * Get an event which is stored in our unfiltered timeline set, or in a thread
- *
- * @param eventId - event ID to look for
- * @returns the given event, or undefined if unknown
- */
- public findEventById(eventId: string): MatrixEvent | undefined {
- let event = this.getUnfilteredTimelineSet().findEventById(eventId);
-
- if (!event) {
- const threads = this.getThreads();
- for (let i = 0; i < threads.length; i++) {
- const thread = threads[i];
- event = thread.findEventById(eventId);
- if (event) {
- return event;
- }
- }
- }
-
- return event;
- }
-
- /**
- * Get one of the notification counts for this room
- * @param type - The type of notification count to get. default: 'total'
- * @returns The notification count, or undefined if there is no count
- * for this type.
- */
- public getUnreadNotificationCount(type = NotificationCountType.Total): number {
- let count = this.getRoomUnreadNotificationCount(type);
- for (const threadNotification of this.threadNotifications.values()) {
- count += threadNotification[type] ?? 0;
- }
- return count;
- }
-
- /**
- * Get the notification for the event context (room or thread timeline)
- */
- public getUnreadCountForEventContext(type = NotificationCountType.Total, event: MatrixEvent): number {
- const isThreadEvent = !!event.threadRootId && !event.isThreadRoot;
-
- return (
- (isThreadEvent
- ? this.getThreadUnreadNotificationCount(event.threadRootId, type)
- : this.getRoomUnreadNotificationCount(type)) ?? 0
- );
- }
-
- /**
- * Get one of the notification counts for this room
- * @param type - The type of notification count to get. default: 'total'
- * @returns The notification count, or undefined if there is no count
- * for this type.
- */
- public getRoomUnreadNotificationCount(type = NotificationCountType.Total): number {
- return this.notificationCounts[type] ?? 0;
- }
-
- /**
- * Get one of the notification counts for a thread
- * @param threadId - the root event ID
- * @param type - The type of notification count to get. default: 'total'
- * @returns The notification count, or undefined if there is no count
- * for this type.
- */
- public getThreadUnreadNotificationCount(threadId: string, type = NotificationCountType.Total): number {
- return this.threadNotifications.get(threadId)?.[type] ?? 0;
- }
-
- /**
- * Checks if the current room has unread thread notifications
- * @returns
- */
- public hasThreadUnreadNotification(): boolean {
- for (const notification of this.threadNotifications.values()) {
- if ((notification.highlight ?? 0) > 0 || (notification.total ?? 0) > 0) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Swet one of the notification count for a thread
- * @param threadId - the root event ID
- * @param type - The type of notification count to get. default: 'total'
- * @returns
- */
- public setThreadUnreadNotificationCount(threadId: string, type: NotificationCountType, count: number): void {
- const notification: NotificationCount = {
- highlight: this.threadNotifications.get(threadId)?.highlight,
- total: this.threadNotifications.get(threadId)?.total,
- ...{
- [type]: count,
- },
- };
-
- this.threadNotifications.set(threadId, notification);
-
- this.emit(RoomEvent.UnreadNotifications, notification, threadId);
- }
-
- /**
- * @returns the notification count type for all the threads in the room
- */
- public get threadsAggregateNotificationType(): NotificationCountType | null {
- let type: NotificationCountType | null = null;
- for (const threadNotification of this.threadNotifications.values()) {
- if ((threadNotification.highlight ?? 0) > 0) {
- return NotificationCountType.Highlight;
- } else if ((threadNotification.total ?? 0) > 0 && !type) {
- type = NotificationCountType.Total;
- }
- }
- return type;
- }
-
- /**
- * Resets the thread notifications for this room
- */
- public resetThreadUnreadNotificationCount(notificationsToKeep?: string[]): void {
- if (notificationsToKeep) {
- for (const [threadId] of this.threadNotifications) {
- if (!notificationsToKeep.includes(threadId)) {
- this.threadNotifications.delete(threadId);
- }
- }
- } else {
- this.threadNotifications.clear();
- }
- this.emit(RoomEvent.UnreadNotifications);
- }
-
- /**
- * Set one of the notification counts for this room
- * @param type - The type of notification count to set.
- * @param count - The new count
- */
- public setUnreadNotificationCount(type: NotificationCountType, count: number): void {
- this.notificationCounts[type] = count;
- this.emit(RoomEvent.UnreadNotifications, this.notificationCounts);
- }
-
- public setUnread(type: NotificationCountType, count: number): void {
- return this.setUnreadNotificationCount(type, count);
- }
-
- public setSummary(summary: IRoomSummary): void {
- const heroes = summary["m.heroes"];
- const joinedCount = summary["m.joined_member_count"];
- const invitedCount = summary["m.invited_member_count"];
- if (Number.isInteger(joinedCount)) {
- this.currentState.setJoinedMemberCount(joinedCount!);
- }
- if (Number.isInteger(invitedCount)) {
- this.currentState.setInvitedMemberCount(invitedCount!);
- }
- if (Array.isArray(heroes)) {
- // be cautious about trusting server values,
- // and make sure heroes doesn't contain our own id
- // just to be sure
- this.summaryHeroes = heroes.filter((userId) => {
- return userId !== this.myUserId;
- });
- }
- }
-
- /**
- * Whether to send encrypted messages to devices within this room.
- * @param value - true to blacklist unverified devices, null
- * to use the global value for this room.
- */
- public setBlacklistUnverifiedDevices(value: boolean): void {
- this.blacklistUnverifiedDevices = value;
- }
-
- /**
- * Whether to send encrypted messages to devices within this room.
- * @returns true if blacklisting unverified devices, null
- * if the global value should be used for this room.
- */
- public getBlacklistUnverifiedDevices(): boolean | null {
- if (this.blacklistUnverifiedDevices === undefined) return null;
- return this.blacklistUnverifiedDevices;
- }
-
- /**
- * Get the avatar URL for a room if one was set.
- * @param baseUrl - The homeserver base URL. See
- * {@link MatrixClient#getHomeserverUrl}.
- * @param width - The desired width of the thumbnail.
- * @param height - The desired height of the thumbnail.
- * @param resizeMethod - The thumbnail resize method to use, either
- * "crop" or "scale".
- * @param allowDefault - True to allow an identicon for this room if an
- * avatar URL wasn't explicitly set. Default: true. (Deprecated)
- * @returns the avatar URL or null.
- */
- public getAvatarUrl(
- baseUrl: string,
- width: number,
- height: number,
- resizeMethod: ResizeMethod,
- allowDefault = true,
- ): string | null {
- const roomAvatarEvent = this.currentState.getStateEvents(EventType.RoomAvatar, "");
- if (!roomAvatarEvent && !allowDefault) {
- return null;
- }
-
- const mainUrl = roomAvatarEvent ? roomAvatarEvent.getContent().url : null;
- if (mainUrl) {
- return getHttpUriForMxc(baseUrl, mainUrl, width, height, resizeMethod);
- }
-
- return null;
- }
-
- /**
- * Get the mxc avatar url for the room, if one was set.
- * @returns the mxc avatar url or falsy
- */
- public getMxcAvatarUrl(): string | null {
- return this.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url || null;
- }
-
- /**
- * Get this room's canonical alias
- * The alias returned by this function may not necessarily
- * still point to this room.
- * @returns The room's canonical alias, or null if there is none
- */
- public getCanonicalAlias(): string | null {
- const canonicalAlias = this.currentState.getStateEvents(EventType.RoomCanonicalAlias, "");
- if (canonicalAlias) {
- return canonicalAlias.getContent().alias || null;
- }
- return null;
- }
-
- /**
- * Get this room's alternative aliases
- * @returns The room's alternative aliases, or an empty array
- */
- public getAltAliases(): string[] {
- const canonicalAlias = this.currentState.getStateEvents(EventType.RoomCanonicalAlias, "");
- if (canonicalAlias) {
- return canonicalAlias.getContent().alt_aliases || [];
- }
- return [];
- }
-
- /**
- * Add events to a timeline
- *
- * <p>Will fire "Room.timeline" for each event added.
- *
- * @param events - A list of events to add.
- *
- * @param toStartOfTimeline - True to add these events to the start
- * (oldest) instead of the end (newest) of the timeline. If true, the oldest
- * event will be the <b>last</b> element of 'events'.
- *
- * @param timeline - timeline to
- * add events to.
- *
- * @param paginationToken - token for the next batch of events
- *
- * @remarks
- * Fires {@link RoomEvent.Timeline}
- */
- public addEventsToTimeline(
- events: MatrixEvent[],
- toStartOfTimeline: boolean,
- timeline: EventTimeline,
- paginationToken?: string,
- ): void {
- timeline.getTimelineSet().addEventsToTimeline(events, toStartOfTimeline, timeline, paginationToken);
- }
-
- /**
- * Get the instance of the thread associated with the current event
- * @param eventId - the ID of the current event
- * @returns a thread instance if known
- */
- public getThread(eventId: string): Thread | null {
- return this.threads.get(eventId) ?? null;
- }
-
- /**
- * Get all the known threads in the room
- */
- public getThreads(): Thread[] {
- return Array.from(this.threads.values());
- }
-
- /**
- * Get a member from the current room state.
- * @param userId - The user ID of the member.
- * @returns The member or `null`.
- */
- public getMember(userId: string): RoomMember | null {
- return this.currentState.getMember(userId);
- }
-
- /**
- * Get all currently loaded members from the current
- * room state.
- * @returns Room members
- */
- public getMembers(): RoomMember[] {
- return this.currentState.getMembers();
- }
-
- /**
- * Get a list of members whose membership state is "join".
- * @returns A list of currently joined members.
- */
- public getJoinedMembers(): RoomMember[] {
- return this.getMembersWithMembership("join");
- }
-
- /**
- * Returns the number of joined members in this room
- * This method caches the result.
- * This is a wrapper around the method of the same name in roomState, returning
- * its result for the room's current state.
- * @returns The number of members in this room whose membership is 'join'
- */
- public getJoinedMemberCount(): number {
- return this.currentState.getJoinedMemberCount();
- }
-
- /**
- * Returns the number of invited members in this room
- * @returns The number of members in this room whose membership is 'invite'
- */
- public getInvitedMemberCount(): number {
- return this.currentState.getInvitedMemberCount();
- }
-
- /**
- * Returns the number of invited + joined members in this room
- * @returns The number of members in this room whose membership is 'invite' or 'join'
- */
- public getInvitedAndJoinedMemberCount(): number {
- return this.getInvitedMemberCount() + this.getJoinedMemberCount();
- }
-
- /**
- * Get a list of members with given membership state.
- * @param membership - The membership state.
- * @returns A list of members with the given membership state.
- */
- public getMembersWithMembership(membership: string): RoomMember[] {
- return this.currentState.getMembers().filter(function (m) {
- return m.membership === membership;
- });
- }
-
- /**
- * Get a list of members we should be encrypting for in this room
- * @returns A list of members who
- * we should encrypt messages for in this room.
- */
- public async getEncryptionTargetMembers(): Promise<RoomMember[]> {
- await this.loadMembersIfNeeded();
- let members = this.getMembersWithMembership("join");
- if (this.shouldEncryptForInvitedMembers()) {
- members = members.concat(this.getMembersWithMembership("invite"));
- }
- return members;
- }
-
- /**
- * Determine whether we should encrypt messages for invited users in this room
- * @returns if we should encrypt messages for invited users
- */
- public shouldEncryptForInvitedMembers(): boolean {
- const ev = this.currentState.getStateEvents(EventType.RoomHistoryVisibility, "");
- return ev?.getContent()?.history_visibility !== "joined";
- }
-
- /**
- * Get the default room name (i.e. what a given user would see if the
- * room had no m.room.name)
- * @param userId - The userId from whose perspective we want
- * to calculate the default name
- * @returns The default room name
- */
- public getDefaultRoomName(userId: string): string {
- return this.calculateRoomName(userId, true);
- }
-
- /**
- * Check if the given user_id has the given membership state.
- * @param userId - The user ID to check.
- * @param membership - The membership e.g. `'join'`
- * @returns True if this user_id has the given membership state.
- */
- public hasMembershipState(userId: string, membership: string): boolean {
- const member = this.getMember(userId);
- if (!member) {
- return false;
- }
- return member.membership === membership;
- }
-
- /**
- * Add a timelineSet for this room with the given filter
- * @param filter - The filter to be applied to this timelineSet
- * @param opts - Configuration options
- * @returns The timelineSet
- */
- public getOrCreateFilteredTimelineSet(
- filter: Filter,
- { prepopulateTimeline = true, useSyncEvents = true, pendingEvents = true }: ICreateFilterOpts = {},
- ): EventTimelineSet {
- if (this.filteredTimelineSets[filter.filterId!]) {
- return this.filteredTimelineSets[filter.filterId!];
- }
- const opts = Object.assign({ filter, pendingEvents }, this.opts);
- const timelineSet = new EventTimelineSet(this, opts);
- this.reEmitter.reEmit(timelineSet, [RoomEvent.Timeline, RoomEvent.TimelineReset]);
- if (useSyncEvents) {
- this.filteredTimelineSets[filter.filterId!] = timelineSet;
- this.timelineSets.push(timelineSet);
- }
-
- const unfilteredLiveTimeline = this.getLiveTimeline();
- // Not all filter are possible to replicate client-side only
- // When that's the case we do not want to prepopulate from the live timeline
- // as we would get incorrect results compared to what the server would send back
- if (prepopulateTimeline) {
- // populate up the new timelineSet with filtered events from our live
- // unfiltered timeline.
- //
- // XXX: This is risky as our timeline
- // may have grown huge and so take a long time to filter.
- // see https://github.com/vector-im/vector-web/issues/2109
-
- unfilteredLiveTimeline.getEvents().forEach(function (event) {
- timelineSet.addLiveEvent(event);
- });
-
- // find the earliest unfiltered timeline
- let timeline = unfilteredLiveTimeline;
- while (timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)) {
- timeline = timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)!;
- }
-
- timelineSet
- .getLiveTimeline()
- .setPaginationToken(timeline.getPaginationToken(EventTimeline.BACKWARDS), EventTimeline.BACKWARDS);
- } else if (useSyncEvents) {
- const livePaginationToken = unfilteredLiveTimeline.getPaginationToken(Direction.Forward);
- timelineSet.getLiveTimeline().setPaginationToken(livePaginationToken, Direction.Backward);
- }
-
- // alternatively, we could try to do something like this to try and re-paginate
- // in the filtered events from nothing, but Mark says it's an abuse of the API
- // to do so:
- //
- // timelineSet.resetLiveTimeline(
- // unfilteredLiveTimeline.getPaginationToken(EventTimeline.FORWARDS)
- // );
-
- return timelineSet;
- }
-
- private async getThreadListFilter(filterType = ThreadFilterType.All): Promise<Filter> {
- const myUserId = this.client.getUserId()!;
- const filter = new Filter(myUserId);
-
- const definition: IFilterDefinition = {
- room: {
- timeline: {
- [FILTER_RELATED_BY_REL_TYPES.name]: [THREAD_RELATION_TYPE.name],
- },
- },
- };
-
- if (filterType === ThreadFilterType.My) {
- definition!.room!.timeline![FILTER_RELATED_BY_SENDERS.name] = [myUserId];
- }
-
- filter.setDefinition(definition);
- const filterId = await this.client.getOrCreateFilter(`THREAD_PANEL_${this.roomId}_${filterType}`, filter);
-
- filter.filterId = filterId;
-
- return filter;
- }
-
- private async createThreadTimelineSet(filterType?: ThreadFilterType): Promise<EventTimelineSet> {
- let timelineSet: EventTimelineSet;
- if (Thread.hasServerSideListSupport) {
- timelineSet = new EventTimelineSet(
- this,
- {
- ...this.opts,
- pendingEvents: false,
- },
- undefined,
- undefined,
- filterType ?? ThreadFilterType.All,
- );
- this.reEmitter.reEmit(timelineSet, [RoomEvent.Timeline, RoomEvent.TimelineReset]);
- } else if (Thread.hasServerSideSupport) {
- const filter = await this.getThreadListFilter(filterType);
-
- timelineSet = this.getOrCreateFilteredTimelineSet(filter, {
- prepopulateTimeline: false,
- useSyncEvents: false,
- pendingEvents: false,
- });
- } else {
- timelineSet = new EventTimelineSet(this, {
- pendingEvents: false,
- });
-
- Array.from(this.threads).forEach(([, thread]) => {
- if (thread.length === 0) return;
- const currentUserParticipated = thread.timeline.some((event) => {
- return event.getSender() === this.client.getUserId();
- });
- if (filterType !== ThreadFilterType.My || currentUserParticipated) {
- timelineSet.getLiveTimeline().addEvent(thread.rootEvent!, {
- toStartOfTimeline: false,
- });
- }
- });
- }
-
- return timelineSet;
- }
-
- private threadsReady = false;
-
- /**
- * Takes the given thread root events and creates threads for them.
- */
- public processThreadRoots(events: MatrixEvent[], toStartOfTimeline: boolean): void {
- for (const rootEvent of events) {
- EventTimeline.setEventMetadata(rootEvent, this.currentState, toStartOfTimeline);
- if (!this.getThread(rootEvent.getId()!)) {
- this.createThread(rootEvent.getId()!, rootEvent, [], toStartOfTimeline);
- }
- }
- }
-
- /**
- * Fetch the bare minimum of room threads required for the thread list to work reliably.
- * With server support that means fetching one page.
- * Without server support that means fetching as much at once as the server allows us to.
- */
- public async fetchRoomThreads(): Promise<void> {
- if (this.threadsReady || !this.client.supportsThreads()) {
- return;
- }
-
- if (Thread.hasServerSideListSupport) {
- await Promise.all([
- this.fetchRoomThreadList(ThreadFilterType.All),
- this.fetchRoomThreadList(ThreadFilterType.My),
- ]);
- } else {
- const allThreadsFilter = await this.getThreadListFilter();
-
- const { chunk: events } = await this.client.createMessagesRequest(
- this.roomId,
- "",
- Number.MAX_SAFE_INTEGER,
- Direction.Backward,
- allThreadsFilter,
- );
-
- if (!events.length) return;
-
- // Sorted by last_reply origin_server_ts
- const threadRoots = events.map(this.client.getEventMapper()).sort((eventA, eventB) => {
- /**
- * `origin_server_ts` in a decentralised world is far from ideal
- * but for lack of any better, we will have to use this
- * Long term the sorting should be handled by homeservers and this
- * is only meant as a short term patch
- */
- const threadAMetadata = eventA.getServerAggregatedRelation<IThreadBundledRelationship>(
- THREAD_RELATION_TYPE.name,
- )!;
- const threadBMetadata = eventB.getServerAggregatedRelation<IThreadBundledRelationship>(
- THREAD_RELATION_TYPE.name,
- )!;
- return threadAMetadata.latest_event.origin_server_ts - threadBMetadata.latest_event.origin_server_ts;
- });
-
- let latestMyThreadsRootEvent: MatrixEvent | undefined;
- const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS);
- for (const rootEvent of threadRoots) {
- const opts = {
- duplicateStrategy: DuplicateStrategy.Ignore,
- fromCache: false,
- roomState,
- };
- this.threadsTimelineSets[0]?.addLiveEvent(rootEvent, opts);
-
- const threadRelationship = rootEvent.getServerAggregatedRelation<IThreadBundledRelationship>(
- THREAD_RELATION_TYPE.name,
- );
- if (threadRelationship?.current_user_participated) {
- this.threadsTimelineSets[1]?.addLiveEvent(rootEvent, opts);
- latestMyThreadsRootEvent = rootEvent;
- }
- }
-
- this.processThreadRoots(threadRoots, true);
-
- this.client.decryptEventIfNeeded(threadRoots[threadRoots.length - 1]);
- if (latestMyThreadsRootEvent) {
- this.client.decryptEventIfNeeded(latestMyThreadsRootEvent);
- }
- }
-
- this.on(ThreadEvent.NewReply, this.onThreadNewReply);
- this.on(ThreadEvent.Delete, this.onThreadDelete);
- this.threadsReady = true;
- }
-
- public async processPollEvents(events: MatrixEvent[]): Promise<void> {
- const processPollStartEvent = (event: MatrixEvent): void => {
- if (!M_POLL_START.matches(event.getType())) return;
- try {
- const poll = new Poll(event, this.client, this);
- this.polls.set(event.getId()!, poll);
- this.emit(PollEvent.New, poll);
- } catch {}
- // poll creation can fail for malformed poll start events
- };
-
- const processPollRelationEvent = (event: MatrixEvent): void => {
- const relationEventId = event.relationEventId;
- if (relationEventId && this.polls.has(relationEventId)) {
- const poll = this.polls.get(relationEventId);
- poll?.onNewRelation(event);
- }
- };
-
- const processPollEvent = (event: MatrixEvent): void => {
- processPollStartEvent(event);
- processPollRelationEvent(event);
- };
-
- for (const event of events) {
- try {
- await this.client.decryptEventIfNeeded(event);
- processPollEvent(event);
- } catch {}
- }
- }
-
- /**
- * Fetch a single page of threadlist messages for the specific thread filter
- * @internal
- */
- private async fetchRoomThreadList(filter?: ThreadFilterType): Promise<void> {
- const timelineSet = filter === ThreadFilterType.My ? this.threadsTimelineSets[1] : this.threadsTimelineSets[0];
-
- const { chunk: events, end } = await this.client.createThreadListMessagesRequest(
- this.roomId,
- null,
- undefined,
- Direction.Backward,
- timelineSet.threadListType,
- timelineSet.getFilter(),
- );
-
- timelineSet.getLiveTimeline().setPaginationToken(end ?? null, Direction.Backward);
-
- if (!events.length) return;
-
- const matrixEvents = events.map(this.client.getEventMapper());
- this.processThreadRoots(matrixEvents, true);
- const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS);
- for (const rootEvent of matrixEvents) {
- timelineSet.addLiveEvent(rootEvent, {
- duplicateStrategy: DuplicateStrategy.Replace,
- fromCache: false,
- roomState,
- });
- }
- }
-
- private onThreadNewReply(thread: Thread): void {
- this.updateThreadRootEvents(thread, false, true);
- }
-
- private onThreadDelete(thread: Thread): void {
- this.threads.delete(thread.id);
-
- const timeline = this.getTimelineForEvent(thread.id);
- const roomEvent = timeline?.getEvents()?.find((it) => it.getId() === thread.id);
- if (roomEvent) {
- thread.clearEventMetadata(roomEvent);
- } else {
- logger.debug("onThreadDelete: Could not find root event in room timeline");
- }
- for (const timelineSet of this.threadsTimelineSets) {
- timelineSet.removeEvent(thread.id);
- }
- }
-
- /**
- * Forget the timelineSet for this room with the given filter
- *
- * @param filter - the filter whose timelineSet is to be forgotten
- */
- public removeFilteredTimelineSet(filter: Filter): void {
- const timelineSet = this.filteredTimelineSets[filter.filterId!];
- delete this.filteredTimelineSets[filter.filterId!];
- const i = this.timelineSets.indexOf(timelineSet);
- if (i > -1) {
- this.timelineSets.splice(i, 1);
- }
- }
-
- public eventShouldLiveIn(
- event: MatrixEvent,
- events?: MatrixEvent[],
- roots?: Set<string>,
- ): {
- shouldLiveInRoom: boolean;
- shouldLiveInThread: boolean;
- threadId?: string;
- } {
- if (!this.client?.supportsThreads()) {
- return {
- shouldLiveInRoom: true,
- shouldLiveInThread: false,
- };
- }
-
- // A thread root is always shown in both timelines
- if (event.isThreadRoot || roots?.has(event.getId()!)) {
- return {
- shouldLiveInRoom: true,
- shouldLiveInThread: true,
- threadId: event.getId(),
- };
- }
-
- // A thread relation is always only shown in a thread
- if (event.isRelation(THREAD_RELATION_TYPE.name)) {
- return {
- shouldLiveInRoom: false,
- shouldLiveInThread: true,
- threadId: event.threadRootId,
- };
- }
-
- const parentEventId = event.getAssociatedId();
- let parentEvent: MatrixEvent | undefined;
- if (parentEventId) {
- parentEvent = this.findEventById(parentEventId) ?? events?.find((e) => e.getId() === parentEventId);
- }
-
- // Treat relations and redactions as extensions of their parents so evaluate parentEvent instead
- if (parentEvent && (event.isRelation() || event.isRedaction())) {
- return this.eventShouldLiveIn(parentEvent, events, roots);
- }
-
- // Edge case where we know the event is a relation but don't have the parentEvent
- if (roots?.has(event.relationEventId!)) {
- return {
- shouldLiveInRoom: true,
- shouldLiveInThread: true,
- threadId: event.relationEventId,
- };
- }
-
- // We've exhausted all scenarios, can safely assume that this event should live in the room timeline only
- return {
- shouldLiveInRoom: true,
- shouldLiveInThread: false,
- };
- }
-
- public findThreadForEvent(event?: MatrixEvent): Thread | null {
- if (!event) return null;
-
- const { threadId } = this.eventShouldLiveIn(event);
- return threadId ? this.getThread(threadId) : null;
- }
-
- private addThreadedEvents(threadId: string, events: MatrixEvent[], toStartOfTimeline = false): void {
- let thread = this.getThread(threadId);
-
- if (!thread) {
- const rootEvent = this.findEventById(threadId) ?? events.find((e) => e.getId() === threadId);
- thread = this.createThread(threadId, rootEvent, events, toStartOfTimeline);
- }
-
- thread.addEvents(events, toStartOfTimeline);
- }
-
- /**
- * Adds events to a thread's timeline. Will fire "Thread.update"
- */
- public processThreadedEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void {
- events.forEach(this.applyRedaction);
-
- const eventsByThread: { [threadId: string]: MatrixEvent[] } = {};
- for (const event of events) {
- const { threadId, shouldLiveInThread } = this.eventShouldLiveIn(event);
- if (shouldLiveInThread && !eventsByThread[threadId!]) {
- eventsByThread[threadId!] = [];
- }
- eventsByThread[threadId!]?.push(event);
- }
-
- Object.entries(eventsByThread).map(([threadId, threadEvents]) =>
- this.addThreadedEvents(threadId, threadEvents, toStartOfTimeline),
- );
- }
-
- private updateThreadRootEvents = (thread: Thread, toStartOfTimeline: boolean, recreateEvent: boolean): void => {
- if (thread.length) {
- this.updateThreadRootEvent(this.threadsTimelineSets?.[0], thread, toStartOfTimeline, recreateEvent);
- if (thread.hasCurrentUserParticipated) {
- this.updateThreadRootEvent(this.threadsTimelineSets?.[1], thread, toStartOfTimeline, recreateEvent);
- }
- }
- };
-
- private updateThreadRootEvent = (
- timelineSet: Optional<EventTimelineSet>,
- thread: Thread,
- toStartOfTimeline: boolean,
- recreateEvent: boolean,
- ): void => {
- if (timelineSet && thread.rootEvent) {
- if (recreateEvent) {
- timelineSet.removeEvent(thread.id);
- }
- if (Thread.hasServerSideSupport) {
- timelineSet.addLiveEvent(thread.rootEvent, {
- duplicateStrategy: DuplicateStrategy.Replace,
- fromCache: false,
- roomState: this.currentState,
- });
- } else {
- timelineSet.addEventToTimeline(thread.rootEvent, timelineSet.getLiveTimeline(), { toStartOfTimeline });
- }
- }
- };
-
- public createThread(
- threadId: string,
- rootEvent: MatrixEvent | undefined,
- events: MatrixEvent[] = [],
- toStartOfTimeline: boolean,
- ): Thread {
- if (this.threads.has(threadId)) {
- return this.threads.get(threadId)!;
- }
-
- if (rootEvent) {
- const relatedEvents = this.relations.getAllChildEventsForEvent(rootEvent.getId()!);
- 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(threadId, rootEvent, {
- room: this,
- client: this.client,
- pendingEventOrdering: this.opts.pendingEventOrdering,
- receipts: this.cachedThreadReadReceipts.get(threadId) ?? [],
- });
-
- // All read receipts should now come down from sync, we do not need to keep
- // a reference to the cached receipts anymore.
- this.cachedThreadReadReceipts.delete(threadId);
-
- // If we managed to create a thread and figure out its `id` then we can use it
- // This has to happen before thread.addEvents, because that adds events to the eventtimeline, and the
- // eventtimeline sometimes looks up thread information via the room.
- this.threads.set(thread.id, thread);
-
- // This is necessary to be able to jump to events in threads:
- // If we jump to an event in a thread where neither the event, nor the root,
- // nor any thread event are loaded yet, we'll load the event as well as the thread root, create the thread,
- // and pass the event through this.
- thread.addEvents(events, false);
-
- this.reEmitter.reEmit(thread, [
- ThreadEvent.Delete,
- ThreadEvent.Update,
- ThreadEvent.NewReply,
- RoomEvent.Timeline,
- RoomEvent.TimelineReset,
- ]);
- const isNewer =
- this.lastThread?.rootEvent &&
- rootEvent?.localTimestamp &&
- this.lastThread.rootEvent?.localTimestamp < rootEvent?.localTimestamp;
-
- if (!this.lastThread || isNewer) {
- this.lastThread = thread;
- }
-
- if (this.threadsReady) {
- this.updateThreadRootEvents(thread, toStartOfTimeline, false);
- }
- this.emit(ThreadEvent.New, thread, toStartOfTimeline);
-
- return thread;
- }
-
- private applyRedaction = (event: MatrixEvent): void => {
- if (event.isRedaction()) {
- const redactId = event.event.redacts;
-
- // if we know about this event, redact its contents now.
- const redactedEvent = redactId ? this.findEventById(redactId) : undefined;
- if (redactedEvent) {
- redactedEvent.makeRedacted(event);
-
- // If this is in the current state, replace it with the redacted version
- if (redactedEvent.isState()) {
- const currentStateEvent = this.currentState.getStateEvents(
- redactedEvent.getType(),
- redactedEvent.getStateKey()!,
- );
- if (currentStateEvent?.getId() === redactedEvent.getId()) {
- this.currentState.setStateEvents([redactedEvent]);
- }
- }
-
- this.emit(RoomEvent.Redaction, event, this);
-
- // TODO: we stash user displaynames (among other things) in
- // RoomMember objects which are then attached to other events
- // (in the sender and target fields). We should get those
- // RoomMember objects to update themselves when the events that
- // they are based on are changed.
-
- // Remove any visibility change on this event.
- this.visibilityEvents.delete(redactId!);
-
- // If this event is a visibility change event, remove it from the
- // list of visibility changes and update any event affected by it.
- if (redactedEvent.isVisibilityEvent()) {
- this.redactVisibilityChangeEvent(event);
- }
- }
-
- // FIXME: apply redactions to notification list
-
- // NB: We continue to add the redaction event to the timeline so
- // clients can say "so and so redacted an event" if they wish to. Also
- // this may be needed to trigger an update.
- }
- };
-
- private processLiveEvent(event: MatrixEvent): void {
- this.applyRedaction(event);
-
- // Implement MSC3531: hiding messages.
- if (event.isVisibilityEvent()) {
- // This event changes the visibility of another event, record
- // the visibility change, inform clients if necessary.
- this.applyNewVisibilityEvent(event);
- }
- // If any pending visibility change is waiting for this (older) event,
- this.applyPendingVisibilityEvents(event);
-
- // Sliding Sync modifications:
- // The proxy cannot guarantee every sent event will have a transaction_id field, so we need
- // to check the event ID against the list of pending events if there is no transaction ID
- // field. Only do this for events sent by us though as it's potentially expensive to loop
- // the pending events map.
- const txnId = event.getUnsigned().transaction_id;
- if (!txnId && event.getSender() === this.myUserId) {
- // check the txn map for a matching event ID
- for (const [tid, localEvent] of this.txnToEvent) {
- if (localEvent.getId() === event.getId()) {
- logger.debug("processLiveEvent: found sent event without txn ID: ", tid, event.getId());
- // update the unsigned field so we can re-use the same codepaths
- const unsigned = event.getUnsigned();
- unsigned.transaction_id = tid;
- event.setUnsigned(unsigned);
- break;
- }
- }
- }
- }
-
- /**
- * Add an event to the end of this room's live timelines. Will fire
- * "Room.timeline".
- *
- * @param event - Event to be added
- * @param addLiveEventOptions - addLiveEvent options
- * @internal
- *
- * @remarks
- * Fires {@link RoomEvent.Timeline}
- */
- private addLiveEvent(event: MatrixEvent, addLiveEventOptions: IAddLiveEventOptions): void {
- const { duplicateStrategy, timelineWasEmpty, fromCache } = addLiveEventOptions;
-
- // add to our timeline sets
- for (const timelineSet of this.timelineSets) {
- timelineSet.addLiveEvent(event, {
- duplicateStrategy,
- fromCache,
- timelineWasEmpty,
- });
- }
-
- // synthesize and inject implicit read receipts
- // Done after adding the event because otherwise the app would get a read receipt
- // pointing to an event that wasn't yet in the timeline
- // 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, ReceiptType.Read), true);
-
- // Any live events from a user could be taken as implicit
- // presence information: evidence that they are currently active.
- // ...except in a world where we use 'user.currentlyActive' to reduce
- // presence spam, this isn't very useful - we'll get a transition when
- // they are no longer currently active anyway. So don't bother to
- // reset the lastActiveAgo and lastPresenceTs from the RoomState's user.
- }
- }
-
- /**
- * Add a pending outgoing event to this room.
- *
- * <p>The event is added to either the pendingEventList, or the live timeline,
- * depending on the setting of opts.pendingEventOrdering.
- *
- * <p>This is an internal method, intended for use by MatrixClient.
- *
- * @param event - The event to add.
- *
- * @param txnId - Transaction id for this outgoing event
- *
- * @throws if the event doesn't have status SENDING, or we aren't given a
- * unique transaction id.
- *
- * @remarks
- * Fires {@link RoomEvent.LocalEchoUpdated}
- */
- public addPendingEvent(event: MatrixEvent, txnId: string): void {
- if (event.status !== EventStatus.SENDING && event.status !== EventStatus.NOT_SENT) {
- throw new Error("addPendingEvent called on an event with status " + event.status);
- }
-
- if (this.txnToEvent.get(txnId)) {
- throw new Error("addPendingEvent called on an event with known txnId " + txnId);
- }
-
- // call setEventMetadata to set up event.sender etc
- // as event is shared over all timelineSets, we set up its metadata based
- // on the unfiltered timelineSet.
- EventTimeline.setEventMetadata(event, this.getLiveTimeline().getState(EventTimeline.FORWARDS)!, false);
-
- this.txnToEvent.set(txnId, event);
- if (this.pendingEventList) {
- if (this.pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) {
- logger.warn("Setting event as NOT_SENT due to messages in the same state");
- event.setStatus(EventStatus.NOT_SENT);
- }
- this.pendingEventList.push(event);
- this.savePendingEvents();
- if (event.isRelation()) {
- // For pending events, add them to the relations collection immediately.
- // (The alternate case below already covers this as part of adding to
- // the timeline set.)
- this.aggregateNonLiveRelation(event);
- }
-
- if (event.isRedaction()) {
- const redactId = event.event.redacts;
- let redactedEvent = this.pendingEventList.find((e) => e.getId() === redactId);
- if (!redactedEvent && redactId) {
- redactedEvent = this.findEventById(redactId);
- }
- if (redactedEvent) {
- redactedEvent.markLocallyRedacted(event);
- this.emit(RoomEvent.Redaction, event, this);
- }
- }
- } else {
- for (const timelineSet of this.timelineSets) {
- if (timelineSet.getFilter()) {
- if (timelineSet.getFilter()!.filterRoomTimeline([event]).length) {
- timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), {
- toStartOfTimeline: false,
- });
- }
- } else {
- timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), {
- toStartOfTimeline: false,
- });
- }
- }
- }
-
- this.emit(RoomEvent.LocalEchoUpdated, event, this);
- }
-
- /**
- * Persists all pending events to local storage
- *
- * If the current room is encrypted only encrypted events will be persisted
- * all messages that are not yet encrypted will be discarded
- *
- * This is because the flow of EVENT_STATUS transition is
- * `queued => sending => encrypting => sending => sent`
- *
- * Steps 3 and 4 are skipped for unencrypted room.
- * It is better to discard an unencrypted message rather than persisting
- * it locally for everyone to read
- */
- private savePendingEvents(): void {
- if (this.pendingEventList) {
- const pendingEvents = this.pendingEventList
- .map((event) => {
- return {
- ...event.event,
- txn_id: event.getTxnId(),
- };
- })
- .filter((event) => {
- // Filter out the unencrypted messages if the room is encrypted
- const isEventEncrypted = event.type === EventType.RoomMessageEncrypted;
- const isRoomEncrypted = this.client.isRoomEncrypted(this.roomId);
- return isEventEncrypted || !isRoomEncrypted;
- });
-
- this.client.store.setPendingEvents(this.roomId, pendingEvents);
- }
- }
-
- /**
- * Used to aggregate the local echo for a relation, and also
- * for re-applying a relation after it's redaction has been cancelled,
- * as the local echo for the redaction of the relation would have
- * un-aggregated the relation. Note that this is different from regular messages,
- * which are just kept detached for their local echo.
- *
- * Also note that live events are aggregated in the live EventTimelineSet.
- * @param event - the relation event that needs to be aggregated.
- */
- private aggregateNonLiveRelation(event: MatrixEvent): void {
- this.relations.aggregateChildEvent(event);
- }
-
- public getEventForTxnId(txnId: string): MatrixEvent | undefined {
- return this.txnToEvent.get(txnId);
- }
-
- /**
- * Deal with the echo of a message we sent.
- *
- * <p>We move the event to the live timeline if it isn't there already, and
- * update it.
- *
- * @param remoteEvent - The event received from
- * /sync
- * @param localEvent - The local echo, which
- * should be either in the pendingEventList or the timeline.
- *
- * @internal
- *
- * @remarks
- * Fires {@link RoomEvent.LocalEchoUpdated}
- */
- public handleRemoteEcho(remoteEvent: MatrixEvent, localEvent: MatrixEvent): void {
- const oldEventId = localEvent.getId()!;
- const newEventId = remoteEvent.getId()!;
- const oldStatus = localEvent.status;
-
- logger.debug(`Got remote echo for event ${oldEventId} -> ${newEventId} old status ${oldStatus}`);
-
- // no longer pending
- this.txnToEvent.delete(remoteEvent.getUnsigned().transaction_id!);
-
- // if it's in the pending list, remove it
- if (this.pendingEventList) {
- this.removePendingEvent(oldEventId);
- }
-
- // replace the event source (this will preserve the plaintext payload if
- // any, which is good, because we don't want to try decoding it again).
- localEvent.handleRemoteEcho(remoteEvent.event);
-
- const { shouldLiveInRoom, threadId } = this.eventShouldLiveIn(remoteEvent);
- const thread = threadId ? this.getThread(threadId) : null;
- thread?.setEventMetadata(localEvent);
- thread?.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
-
- if (shouldLiveInRoom) {
- for (const timelineSet of this.timelineSets) {
- // if it's already in the timeline, update the timeline map. If it's not, add it.
- timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
- }
- }
-
- this.emit(RoomEvent.LocalEchoUpdated, localEvent, this, oldEventId, oldStatus);
- }
-
- /**
- * Update the status / event id on a pending event, to reflect its transmission
- * progress.
- *
- * <p>This is an internal method.
- *
- * @param event - local echo event
- * @param newStatus - status to assign
- * @param newEventId - new event id to assign. Ignored unless newStatus == EventStatus.SENT.
- *
- * @remarks
- * Fires {@link RoomEvent.LocalEchoUpdated}
- */
- public updatePendingEvent(event: MatrixEvent, newStatus: EventStatus, newEventId?: string): void {
- logger.log(
- `setting pendingEvent status to ${newStatus} in ${event.getRoomId()} ` +
- `event ID ${event.getId()} -> ${newEventId}`,
- );
-
- // if the message was sent, we expect an event id
- if (newStatus == EventStatus.SENT && !newEventId) {
- throw new Error("updatePendingEvent called with status=SENT, but no new event id");
- }
-
- // SENT races against /sync, so we have to special-case it.
- if (newStatus == EventStatus.SENT) {
- const timeline = this.getTimelineForEvent(newEventId!);
- if (timeline) {
- // we've already received the event via the event stream.
- // nothing more to do here, assuming the transaction ID was correctly matched.
- // Let's check that.
- const remoteEvent = this.findEventById(newEventId!);
- const remoteTxnId = remoteEvent?.getUnsigned().transaction_id;
- if (!remoteTxnId && remoteEvent) {
- // This code path is mostly relevant for the Sliding Sync proxy.
- // The remote event did not contain a transaction ID, so we did not handle
- // the remote echo yet. Handle it now.
- const unsigned = remoteEvent.getUnsigned();
- unsigned.transaction_id = event.getTxnId();
- remoteEvent.setUnsigned(unsigned);
- // the remote event is _already_ in the timeline, so we need to remove it so
- // we can convert the local event into the final event.
- this.removeEvent(remoteEvent.getId()!);
- this.handleRemoteEcho(remoteEvent, event);
- }
- return;
- }
- }
-
- const oldStatus = event.status;
- const oldEventId = event.getId()!;
-
- if (!oldStatus) {
- throw new Error("updatePendingEventStatus called on an event which is not a local echo.");
- }
-
- const allowed = ALLOWED_TRANSITIONS[oldStatus];
- if (!allowed?.includes(newStatus)) {
- throw new Error(`Invalid EventStatus transition ${oldStatus}->${newStatus}`);
- }
-
- event.setStatus(newStatus);
-
- if (newStatus == EventStatus.SENT) {
- // update the event id
- event.replaceLocalEventId(newEventId!);
-
- const { shouldLiveInRoom, threadId } = this.eventShouldLiveIn(event);
- const thread = threadId ? this.getThread(threadId) : undefined;
- thread?.setEventMetadata(event);
- thread?.timelineSet.replaceEventId(oldEventId, newEventId!);
-
- if (shouldLiveInRoom) {
- // if the event was already in the timeline (which will be the case if
- // opts.pendingEventOrdering==chronological), we need to update the
- // timeline map.
- for (const timelineSet of this.timelineSets) {
- timelineSet.replaceEventId(oldEventId, newEventId!);
- }
- }
- } else if (newStatus == EventStatus.CANCELLED) {
- // remove it from the pending event list, or the timeline.
- if (this.pendingEventList) {
- const removedEvent = this.getPendingEvent(oldEventId);
- this.removePendingEvent(oldEventId);
- if (removedEvent?.isRedaction()) {
- this.revertRedactionLocalEcho(removedEvent);
- }
- }
- this.removeEvent(oldEventId);
- }
- this.savePendingEvents();
-
- this.emit(RoomEvent.LocalEchoUpdated, event, this, oldEventId, oldStatus);
- }
-
- private revertRedactionLocalEcho(redactionEvent: MatrixEvent): void {
- const redactId = redactionEvent.event.redacts;
- if (!redactId) {
- return;
- }
- const redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId);
- if (redactedEvent) {
- redactedEvent.unmarkLocallyRedacted();
- // re-render after undoing redaction
- this.emit(RoomEvent.RedactionCancelled, redactionEvent, this);
- // reapply relation now redaction failed
- if (redactedEvent.isRelation()) {
- this.aggregateNonLiveRelation(redactedEvent);
- }
- }
- }
-
- /**
- * Add some events to this room. This can include state events, message
- * events and typing notifications. These events are treated as "live" so
- * they will go to the end of the timeline.
- *
- * @param events - A list of events to add.
- * @param addLiveEventOptions - addLiveEvent options
- * @throws If `duplicateStrategy` is not falsey, 'replace' or 'ignore'.
- */
- public addLiveEvents(events: MatrixEvent[], addLiveEventOptions?: IAddLiveEventOptions): void;
- /**
- * @deprecated In favor of the overload with `IAddLiveEventOptions`
- */
- public addLiveEvents(events: MatrixEvent[], duplicateStrategy?: DuplicateStrategy, fromCache?: boolean): void;
- public addLiveEvents(
- events: MatrixEvent[],
- duplicateStrategyOrOpts?: DuplicateStrategy | IAddLiveEventOptions,
- fromCache = false,
- ): void {
- let duplicateStrategy: DuplicateStrategy | undefined = duplicateStrategyOrOpts as DuplicateStrategy;
- let timelineWasEmpty: boolean | undefined = false;
- if (typeof duplicateStrategyOrOpts === "object") {
- ({
- duplicateStrategy,
- fromCache = false,
- /* roomState, (not used here) */
- timelineWasEmpty,
- } = duplicateStrategyOrOpts);
- } else if (duplicateStrategyOrOpts !== undefined) {
- // Deprecation warning
- // FIXME: Remove after 2023-06-01 (technical debt)
- logger.warn(
- "Overload deprecated: " +
- "`Room.addLiveEvents(events, duplicateStrategy?, fromCache?)` " +
- "is deprecated in favor of the overload with `Room.addLiveEvents(events, IAddLiveEventOptions)`",
- );
- }
-
- if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
- throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
- }
-
- // sanity check that the live timeline is still live
- for (let i = 0; i < this.timelineSets.length; i++) {
- const liveTimeline = this.timelineSets[i].getLiveTimeline();
- if (liveTimeline.getPaginationToken(EventTimeline.FORWARDS)) {
- throw new Error(
- "live timeline " +
- i +
- " is no longer live - it has a pagination token " +
- "(" +
- liveTimeline.getPaginationToken(EventTimeline.FORWARDS) +
- ")",
- );
- }
- if (liveTimeline.getNeighbouringTimeline(EventTimeline.FORWARDS)) {
- throw new Error(`live timeline ${i} is no longer live - it has a neighbouring timeline`);
- }
- }
-
- const threadRoots = this.findThreadRoots(events);
- const eventsByThread: { [threadId: string]: MatrixEvent[] } = {};
-
- const options: IAddLiveEventOptions = {
- duplicateStrategy,
- fromCache,
- timelineWasEmpty,
- };
-
- for (const event of events) {
- // TODO: We should have a filter to say "only add state event types X Y Z to the timeline".
- this.processLiveEvent(event);
-
- if (event.getUnsigned().transaction_id) {
- const existingEvent = this.txnToEvent.get(event.getUnsigned().transaction_id!);
- if (existingEvent) {
- // remote echo of an event we sent earlier
- this.handleRemoteEcho(event, existingEvent);
- continue; // we can skip adding the event to the timeline sets, it is already there
- }
- }
-
- const { shouldLiveInRoom, shouldLiveInThread, threadId } = this.eventShouldLiveIn(
- event,
- events,
- threadRoots,
- );
-
- if (shouldLiveInThread && !eventsByThread[threadId ?? ""]) {
- eventsByThread[threadId ?? ""] = [];
- }
- eventsByThread[threadId ?? ""]?.push(event);
-
- if (shouldLiveInRoom) {
- this.addLiveEvent(event, options);
- }
- }
-
- Object.entries(eventsByThread).forEach(([threadId, threadEvents]) => {
- this.addThreadedEvents(threadId, threadEvents, false);
- });
- }
-
- public partitionThreadedEvents(
- events: MatrixEvent[],
- ): [timelineEvents: MatrixEvent[], threadedEvents: MatrixEvent[]] {
- // Indices to the events array, for readability
- const ROOM = 0;
- const THREAD = 1;
- if (this.client.supportsThreads()) {
- const threadRoots = this.findThreadRoots(events);
- return events.reduce(
- (memo, event: MatrixEvent) => {
- const { shouldLiveInRoom, shouldLiveInThread, threadId } = this.eventShouldLiveIn(
- event,
- events,
- threadRoots,
- );
-
- if (shouldLiveInRoom) {
- memo[ROOM].push(event);
- }
-
- if (shouldLiveInThread) {
- event.setThreadId(threadId ?? "");
- memo[THREAD].push(event);
- }
-
- return memo;
- },
- [[] as MatrixEvent[], [] as MatrixEvent[]],
- );
- } else {
- // When `experimentalThreadSupport` is disabled treat all events as timelineEvents
- return [events as MatrixEvent[], [] as MatrixEvent[]];
- }
- }
-
- /**
- * Given some events, find the IDs of all the thread roots that are referred to by them.
- */
- private findThreadRoots(events: MatrixEvent[]): Set<string> {
- const threadRoots = new Set<string>();
- for (const event of events) {
- if (event.isRelation(THREAD_RELATION_TYPE.name)) {
- threadRoots.add(event.relationEventId ?? "");
- }
- }
- return threadRoots;
- }
-
- /**
- * Add a receipt event to the room.
- * @param event - The m.receipt event.
- * @param synthetic - True if this event is implicit.
- */
- public addReceipt(event: MatrixEvent, synthetic = false): void {
- const content = event.getContent<ReceiptContent>();
- Object.keys(content).forEach((eventId: string) => {
- Object.keys(content[eventId]).forEach((receiptType: ReceiptType | string) => {
- Object.keys(content[eventId][receiptType]).forEach((userId: string) => {
- const receipt = content[eventId][receiptType][userId] as Receipt;
- const receiptForMainTimeline = !receipt.thread_id || receipt.thread_id === MAIN_ROOM_TIMELINE;
- const receiptDestination: Thread | this | undefined = receiptForMainTimeline
- ? this
- : this.threads.get(receipt.thread_id ?? "");
-
- if (receiptDestination) {
- receiptDestination.addReceiptToStructure(
- eventId,
- receiptType as ReceiptType,
- userId,
- receipt,
- synthetic,
- );
-
- // If the read receipt sent for the logged in user matches
- // the last event of the live timeline, then we know for a fact
- // that the user has read that message.
- // We can mark the room as read and not wait for the local echo
- // from synapse
- // This needs to be done after the initial sync as we do not want this
- // logic to run whilst the room is being initialised
- if (this.client.isInitialSyncComplete() && userId === this.client.getUserId()) {
- const lastEvent = receiptDestination.timeline[receiptDestination.timeline.length - 1];
- if (lastEvent && eventId === lastEvent.getId() && userId === lastEvent.getSender()) {
- receiptDestination.setUnread(NotificationCountType.Total, 0);
- receiptDestination.setUnread(NotificationCountType.Highlight, 0);
- }
- }
- } else {
- // The thread does not exist locally, keep the read receipt
- // in a cache locally, and re-apply the `addReceipt` logic
- // when the thread is created
- this.cachedThreadReadReceipts.set(receipt.thread_id!, [
- ...(this.cachedThreadReadReceipts.get(receipt.thread_id!) ?? []),
- { eventId, receiptType, userId, receipt, synthetic },
- ]);
- }
-
- const me = this.client.getUserId();
- // Track the time of the current user's oldest threaded receipt in the room.
- if (userId === me && !receiptForMainTimeline && receipt.ts < this.oldestThreadedReceiptTs) {
- this.oldestThreadedReceiptTs = receipt.ts;
- }
-
- // Track each user's unthreaded read receipt.
- if (!receipt.thread_id && receipt.ts > (this.unthreadedReceipts.get(userId)?.ts ?? 0)) {
- this.unthreadedReceipts.set(userId, receipt);
- }
- });
- });
- });
-
- // send events after we've regenerated the structure & cache, otherwise things that
- // listened for the event would read stale data.
- this.emit(RoomEvent.Receipt, event, this);
- }
-
- /**
- * Adds/handles ephemeral events such as typing notifications and read receipts.
- * @param events - A list of events to process
- */
- public addEphemeralEvents(events: MatrixEvent[]): void {
- for (const event of events) {
- if (event.getType() === EventType.Typing) {
- this.currentState.setTypingEvent(event);
- } else if (event.getType() === EventType.Receipt) {
- this.addReceipt(event);
- } // else ignore - life is too short for us to care about these events
- }
- }
-
- /**
- * Removes events from this room.
- * @param eventIds - A list of eventIds to remove.
- */
- public removeEvents(eventIds: string[]): void {
- for (const eventId of eventIds) {
- this.removeEvent(eventId);
- }
- }
-
- /**
- * Removes a single event from this room.
- *
- * @param eventId - The id of the event to remove
- *
- * @returns true if the event was removed from any of the room's timeline sets
- */
- public removeEvent(eventId: string): boolean {
- let removedAny = false;
- for (const timelineSet of this.timelineSets) {
- const removed = timelineSet.removeEvent(eventId);
- if (removed) {
- if (removed.isRedaction()) {
- this.revertRedactionLocalEcho(removed);
- }
- removedAny = true;
- }
- }
- return removedAny;
- }
-
- /**
- * Recalculate various aspects of the room, including the room name and
- * room summary. Call this any time the room's current state is modified.
- * May fire "Room.name" if the room name is updated.
- *
- * @remarks
- * Fires {@link RoomEvent.Name}
- */
- public recalculate(): void {
- // 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) {
- 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;
- this.name = this.calculateRoomName(this.myUserId);
- this.normalizedName = normalize(this.name);
- this.summary = new RoomSummary(this.roomId, {
- title: this.name,
- });
-
- if (oldName !== this.name) {
- this.emit(RoomEvent.Name, this);
- }
- }
-
- /**
- * Update the room-tag event for the room. The previous one is overwritten.
- * @param event - the m.tag event
- */
- public addTags(event: MatrixEvent): void {
- // event content looks like:
- // content: {
- // tags: {
- // $tagName: { $metadata: $value },
- // $tagName: { $metadata: $value },
- // }
- // }
-
- // XXX: do we need to deep copy here?
- this.tags = event.getContent().tags || {};
-
- // XXX: we could do a deep-comparison to see if the tags have really
- // changed - but do we want to bother?
- this.emit(RoomEvent.Tags, event, this);
- }
-
- /**
- * Update the account_data events for this room, overwriting events of the same type.
- * @param events - an array of account_data events to add
- */
- public addAccountData(events: MatrixEvent[]): void {
- for (const event of events) {
- if (event.getType() === "m.tag") {
- this.addTags(event);
- }
- const eventType = event.getType();
- const lastEvent = this.accountData.get(eventType);
- this.accountData.set(eventType, event);
- this.emit(RoomEvent.AccountData, event, this, lastEvent);
- }
- }
-
- /**
- * Access account_data event of given event type for this room
- * @param type - the type of account_data event to be accessed
- * @returns the account_data event in question
- */
- public getAccountData(type: EventType | string): MatrixEvent | undefined {
- return this.accountData.get(type);
- }
-
- /**
- * Returns whether the syncing user has permission to send a message in the room
- * @returns true if the user should be permitted to send
- * message events into the room.
- */
- public maySendMessage(): boolean {
- return (
- this.getMyMembership() === "join" &&
- (this.client.isRoomEncrypted(this.roomId)
- ? this.currentState.maySendEvent(EventType.RoomMessageEncrypted, this.myUserId)
- : this.currentState.maySendEvent(EventType.RoomMessage, this.myUserId))
- );
- }
-
- /**
- * Returns whether the given user has permissions to issue an invite for this room.
- * @param userId - the ID of the Matrix user to check permissions for
- * @returns true if the user should be permitted to issue invites for this room.
- */
- public canInvite(userId: string): boolean {
- let canInvite = this.getMyMembership() === "join";
- const powerLevelsEvent = this.currentState.getStateEvents(EventType.RoomPowerLevels, "");
- const powerLevels = powerLevelsEvent && powerLevelsEvent.getContent();
- const me = this.getMember(userId);
- if (powerLevels && me && powerLevels.invite > me.powerLevel) {
- canInvite = false;
- }
- return canInvite;
- }
-
- /**
- * Returns the join rule based on the m.room.join_rule state event, defaulting to `invite`.
- * @returns the join_rule applied to this room
- */
- public getJoinRule(): JoinRule {
- return this.currentState.getJoinRule();
- }
-
- /**
- * Returns the history visibility based on the m.room.history_visibility state event, defaulting to `shared`.
- * @returns the history_visibility applied to this room
- */
- public getHistoryVisibility(): HistoryVisibility {
- return this.currentState.getHistoryVisibility();
- }
-
- /**
- * Returns the history visibility based on the m.room.history_visibility state event, defaulting to `shared`.
- * @returns the history_visibility applied to this room
- */
- public getGuestAccess(): GuestAccess {
- return this.currentState.getGuestAccess();
- }
-
- /**
- * Returns the type of the room from the `m.room.create` event content or undefined if none is set
- * @returns the type of the room.
- */
- public getType(): RoomType | string | undefined {
- const createEvent = this.currentState.getStateEvents(EventType.RoomCreate, "");
- if (!createEvent) {
- if (!this.getTypeWarning) {
- logger.warn("[getType] Room " + this.roomId + " does not have an m.room.create event");
- this.getTypeWarning = true;
- }
- return undefined;
- }
- return createEvent.getContent()[RoomCreateTypeField];
- }
-
- /**
- * Returns whether the room is a space-room as defined by MSC1772.
- * @returns true if the room's type is RoomType.Space
- */
- public isSpaceRoom(): boolean {
- return this.getType() === RoomType.Space;
- }
-
- /**
- * Returns whether the room is a call-room as defined by MSC3417.
- * @returns true if the room's type is RoomType.UnstableCall
- */
- public isCallRoom(): boolean {
- return this.getType() === RoomType.UnstableCall;
- }
-
- /**
- * Returns whether the room is a video room.
- * @returns true if the room's type is RoomType.ElementVideo
- */
- public isElementVideoRoom(): boolean {
- return this.getType() === RoomType.ElementVideo;
- }
-
- /**
- * Find the predecessor of this room.
- *
- * @param msc3946ProcessDynamicPredecessor - if true, look for an
- * m.room.predecessor state event and use it if found (MSC3946).
- * @returns null if this room has no predecessor. Otherwise, returns
- * the roomId, last eventId and viaServers of the predecessor room.
- *
- * If msc3946ProcessDynamicPredecessor is true, use m.predecessor events
- * as well as m.room.create events to find predecessors.
- *
- * Note: if an m.predecessor event is used, eventId may be undefined
- * since last_known_event_id is optional.
- *
- * Note: viaServers may be undefined, and will definitely be undefined if
- * this predecessor comes from a RoomCreate event (rather than a
- * RoomPredecessor, which has the optional via_servers property).
- */
- public findPredecessor(
- msc3946ProcessDynamicPredecessor = false,
- ): { roomId: string; eventId?: string; viaServers?: string[] } | null {
- const currentState = this.getLiveTimeline().getState(EventTimeline.FORWARDS);
- if (!currentState) {
- return null;
- }
- return currentState.findPredecessor(msc3946ProcessDynamicPredecessor);
- }
-
- private roomNameGenerator(state: RoomNameState): string {
- if (this.client.roomNameGenerator) {
- const name = this.client.roomNameGenerator(this.roomId, state);
- if (name !== null) {
- return name;
- }
- }
-
- switch (state.type) {
- case RoomNameType.Actual:
- return state.name;
- case RoomNameType.Generated:
- switch (state.subtype) {
- case "Inviting":
- return `Inviting ${memberNamesToRoomName(state.names, state.count)}`;
- default:
- return memberNamesToRoomName(state.names, state.count);
- }
- case RoomNameType.EmptyRoom:
- if (state.oldName) {
- return `Empty room (was ${state.oldName})`;
- } else {
- return "Empty room";
- }
- }
- }
-
- /**
- * This is an internal method. Calculates the name of the room from the current
- * room state.
- * @param userId - The client's user ID. Used to filter room members
- * correctly.
- * @param ignoreRoomNameEvent - Return the implicit room name that we'd see if there
- * was no m.room.name event.
- * @returns The calculated room name.
- */
- private calculateRoomName(userId: string, ignoreRoomNameEvent = false): string {
- if (!ignoreRoomNameEvent) {
- // check for an alias, if any. for now, assume first alias is the
- // official one.
- const mRoomName = this.currentState.getStateEvents(EventType.RoomName, "");
- if (mRoomName?.getContent().name) {
- return this.roomNameGenerator({
- type: RoomNameType.Actual,
- name: mRoomName.getContent().name,
- });
- }
- }
-
- const alias = this.getCanonicalAlias();
- if (alias) {
- return this.roomNameGenerator({
- type: RoomNameType.Actual,
- name: alias,
- });
- }
-
- const joinedMemberCount = this.currentState.getJoinedMemberCount();
- const invitedMemberCount = this.currentState.getInvitedMemberCount();
- // -1 because these numbers include the syncing user
- let inviteJoinCount = joinedMemberCount + invitedMemberCount - 1;
-
- // get service members (e.g. helper bots) for exclusion
- let excludedUserIds: string[] = [];
- const mFunctionalMembers = this.currentState.getStateEvents(UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, "");
- if (Array.isArray(mFunctionalMembers?.getContent().service_members)) {
- excludedUserIds = mFunctionalMembers!.getContent().service_members;
- }
-
- // get members that are NOT ourselves and are actually in the room.
- let otherNames: string[] = [];
- if (this.summaryHeroes) {
- // if we have a summary, the member state events should be in the room state
- this.summaryHeroes.forEach((userId) => {
- // filter service members
- if (excludedUserIds.includes(userId)) {
- inviteJoinCount--;
- return;
- }
- const member = this.getMember(userId);
- otherNames.push(member ? member.name : userId);
- });
- } else {
- let otherMembers = this.currentState.getMembers().filter((m) => {
- return m.userId !== userId && (m.membership === "invite" || m.membership === "join");
- });
- otherMembers = otherMembers.filter(({ userId }) => {
- // filter service members
- if (excludedUserIds.includes(userId)) {
- inviteJoinCount--;
- return false;
- }
- return true;
- });
- // make sure members have stable order
- otherMembers.sort((a, b) => utils.compare(a.userId, b.userId));
- // only 5 first members, immitate summaryHeroes
- otherMembers = otherMembers.slice(0, 5);
- otherNames = otherMembers.map((m) => m.name);
- }
-
- if (inviteJoinCount) {
- return this.roomNameGenerator({
- type: RoomNameType.Generated,
- names: otherNames,
- count: inviteJoinCount,
- });
- }
-
- const myMembership = this.getMyMembership();
- // if I have created a room and invited people through
- // 3rd party invites
- if (myMembership == "join") {
- const thirdPartyInvites = this.currentState.getStateEvents(EventType.RoomThirdPartyInvite);
-
- if (thirdPartyInvites?.length) {
- const thirdPartyNames = thirdPartyInvites.map((i) => {
- return i.getContent().display_name;
- });
-
- return this.roomNameGenerator({
- type: RoomNameType.Generated,
- subtype: "Inviting",
- names: thirdPartyNames,
- count: thirdPartyNames.length + 1,
- });
- }
- }
-
- // let's try to figure out who was here before
- let leftNames = otherNames;
- // if we didn't have heroes, try finding them in the room state
- if (!leftNames.length) {
- leftNames = this.currentState
- .getMembers()
- .filter((m) => {
- return m.userId !== userId && m.membership !== "invite" && m.membership !== "join";
- })
- .map((m) => m.name);
- }
-
- let oldName: string | undefined;
- if (leftNames.length) {
- oldName = this.roomNameGenerator({
- type: RoomNameType.Generated,
- names: leftNames,
- count: leftNames.length + 1,
- });
- }
-
- return this.roomNameGenerator({
- type: RoomNameType.EmptyRoom,
- oldName,
- });
- }
-
- /**
- * When we receive a new visibility change event:
- *
- * - store this visibility change alongside the timeline, in case we
- * later need to apply it to an event that we haven't received yet;
- * - if we have already received the event whose visibility has changed,
- * patch it to reflect the visibility change and inform listeners.
- */
- private applyNewVisibilityEvent(event: MatrixEvent): void {
- const visibilityChange = event.asVisibilityChange();
- if (!visibilityChange) {
- // The event is ill-formed.
- return;
- }
-
- // Ignore visibility change events that are not emitted by moderators.
- const userId = event.getSender();
- if (!userId) {
- return;
- }
- const isPowerSufficient =
- (EVENT_VISIBILITY_CHANGE_TYPE.name &&
- this.currentState.maySendStateEvent(EVENT_VISIBILITY_CHANGE_TYPE.name, userId)) ||
- (EVENT_VISIBILITY_CHANGE_TYPE.altName &&
- this.currentState.maySendStateEvent(EVENT_VISIBILITY_CHANGE_TYPE.altName, userId));
- if (!isPowerSufficient) {
- // Powerlevel is insufficient.
- return;
- }
-
- // Record this change in visibility.
- // If the event is not in our timeline and we only receive it later,
- // we may need to apply the visibility change at a later date.
-
- const visibilityEventsOnOriginalEvent = this.visibilityEvents.get(visibilityChange.eventId);
- if (visibilityEventsOnOriginalEvent) {
- // It would be tempting to simply erase the latest visibility change
- // but we need to record all of the changes in case the latest change
- // is ever redacted.
- //
- // In practice, linear scans through `visibilityEvents` should be fast.
- // However, to protect against a potential DoS attack, we limit the
- // number of iterations in this loop.
- let index = visibilityEventsOnOriginalEvent.length - 1;
- const min = Math.max(
- 0,
- visibilityEventsOnOriginalEvent.length - MAX_NUMBER_OF_VISIBILITY_EVENTS_TO_SCAN_THROUGH,
- );
- for (; index >= min; --index) {
- const target = visibilityEventsOnOriginalEvent[index];
- if (target.getTs() < event.getTs()) {
- break;
- }
- }
- if (index === -1) {
- visibilityEventsOnOriginalEvent.unshift(event);
- } else {
- visibilityEventsOnOriginalEvent.splice(index + 1, 0, event);
- }
- } else {
- this.visibilityEvents.set(visibilityChange.eventId, [event]);
- }
-
- // Finally, let's check if the event is already in our timeline.
- // If so, we need to patch it and inform listeners.
-
- const originalEvent = this.findEventById(visibilityChange.eventId);
- if (!originalEvent) {
- return;
- }
- originalEvent.applyVisibilityEvent(visibilityChange);
- }
-
- private redactVisibilityChangeEvent(event: MatrixEvent): void {
- // Sanity checks.
- if (!event.isVisibilityEvent) {
- throw new Error("expected a visibility change event");
- }
- const relation = event.getRelation();
- const originalEventId = relation?.event_id;
- const visibilityEventsOnOriginalEvent = this.visibilityEvents.get(originalEventId!);
- if (!visibilityEventsOnOriginalEvent) {
- // No visibility changes on the original event.
- // In particular, this change event was not recorded,
- // most likely because it was ill-formed.
- return;
- }
- const index = visibilityEventsOnOriginalEvent.findIndex((change) => change.getId() === event.getId());
- if (index === -1) {
- // This change event was not recorded, most likely because
- // it was ill-formed.
- return;
- }
- // Remove visibility change.
- visibilityEventsOnOriginalEvent.splice(index, 1);
-
- // If we removed the latest visibility change event, propagate changes.
- if (index === visibilityEventsOnOriginalEvent.length) {
- const originalEvent = this.findEventById(originalEventId!);
- if (!originalEvent) {
- return;
- }
- if (index === 0) {
- // We have just removed the only visibility change event.
- this.visibilityEvents.delete(originalEventId!);
- originalEvent.applyVisibilityEvent();
- } else {
- const newEvent = visibilityEventsOnOriginalEvent[visibilityEventsOnOriginalEvent.length - 1];
- const newVisibility = newEvent.asVisibilityChange();
- if (!newVisibility) {
- // Event is ill-formed.
- // This breaks our invariant.
- throw new Error("at this stage, visibility changes should be well-formed");
- }
- originalEvent.applyVisibilityEvent(newVisibility);
- }
- }
- }
-
- /**
- * When we receive an event whose visibility has been altered by
- * a (more recent) visibility change event, patch the event in
- * place so that clients now not to display it.
- *
- * @param event - Any matrix event. If this event has at least one a
- * pending visibility change event, apply the latest visibility
- * change event.
- */
- private applyPendingVisibilityEvents(event: MatrixEvent): void {
- const visibilityEvents = this.visibilityEvents.get(event.getId()!);
- if (!visibilityEvents || visibilityEvents.length == 0) {
- // No pending visibility change in store.
- return;
- }
- const visibilityEvent = visibilityEvents[visibilityEvents.length - 1];
- const visibilityChange = visibilityEvent.asVisibilityChange();
- if (!visibilityChange) {
- return;
- }
- if (visibilityChange.visible) {
- // Events are visible by default, no need to apply a visibility change.
- // Note that we need to keep the visibility changes in `visibilityEvents`,
- // in case we later fetch an older visibility change event that is superseded
- // by `visibilityChange`.
- }
- if (visibilityEvent.getTs() < event.getTs()) {
- // Something is wrong, the visibility change cannot happen before the
- // event. Presumably an ill-formed event.
- return;
- }
- event.applyVisibilityEvent(visibilityChange);
- }
-
- /**
- * Find when a client has gained thread capabilities by inspecting the oldest
- * threaded receipt
- * @returns the timestamp of the oldest threaded receipt
- */
- public getOldestThreadedReceiptTs(): number {
- return this.oldestThreadedReceiptTs;
- }
-
- /**
- * Returns the most recent unthreaded receipt for a given user
- * @param userId - the MxID of the User
- * @returns an unthreaded Receipt. Can be undefined if receipts have been disabled
- * or a user chooses to use private read receipts (or we have simply not received
- * a receipt from this user yet).
- */
- public getLastUnthreadedReceiptFor(userId: string): Receipt | undefined {
- return this.unthreadedReceipts.get(userId);
- }
-
- /**
- * This issue should also be addressed on synapse's side and is tracked as part
- * of https://github.com/matrix-org/synapse/issues/14837
- *
- *
- * We consider a room fully read if the current user has sent
- * the last event in the live timeline of that context and if the read receipt
- * we have on record matches.
- * This also detects all unread threads and applies the same logic to those
- * contexts
- */
- public fixupNotifications(userId: string): void {
- super.fixupNotifications(userId);
-
- const unreadThreads = this.getThreads().filter(
- (thread) => this.getThreadUnreadNotificationCount(thread.id, NotificationCountType.Total) > 0,
- );
-
- for (const thread of unreadThreads) {
- thread.fixupNotifications(userId);
- }
- }
-}
-
-// a map from current event status to a list of allowed next statuses
-const ALLOWED_TRANSITIONS: Record<EventStatus, EventStatus[]> = {
- [EventStatus.ENCRYPTING]: [EventStatus.SENDING, EventStatus.NOT_SENT, EventStatus.CANCELLED],
- [EventStatus.SENDING]: [EventStatus.ENCRYPTING, EventStatus.QUEUED, EventStatus.NOT_SENT, EventStatus.SENT],
- [EventStatus.QUEUED]: [EventStatus.SENDING, EventStatus.NOT_SENT, EventStatus.CANCELLED],
- [EventStatus.SENT]: [],
- [EventStatus.NOT_SENT]: [EventStatus.SENDING, EventStatus.QUEUED, EventStatus.CANCELLED],
- [EventStatus.CANCELLED]: [],
-};
-
-export enum RoomNameType {
- EmptyRoom,
- Generated,
- Actual,
-}
-
-export interface EmptyRoomNameState {
- type: RoomNameType.EmptyRoom;
- oldName?: string;
-}
-
-export interface GeneratedRoomNameState {
- type: RoomNameType.Generated;
- subtype?: "Inviting";
- names: string[];
- count: number;
-}
-
-export interface ActualRoomNameState {
- type: RoomNameType.Actual;
- name: string;
-}
-
-export type RoomNameState = EmptyRoomNameState | GeneratedRoomNameState | ActualRoomNameState;
-
-// Can be overriden by IMatrixClientCreateOpts::memberNamesToRoomNameFn
-function memberNamesToRoomName(names: string[], count: number): string {
- const countWithoutMe = count - 1;
- if (!names.length) {
- return "Empty room";
- } else if (names.length === 1 && countWithoutMe <= 1) {
- return names[0];
- } else if (names.length === 2 && countWithoutMe <= 2) {
- return `${names[0]} and ${names[1]}`;
- } else {
- const plural = countWithoutMe > 1;
- if (plural) {
- return `${names[0]} and ${countWithoutMe} others`;
- } else {
- return `${names[0]} and 1 other`;
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/search-result.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/search-result.ts
deleted file mode 100644
index 21192a6..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/search-result.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
-Copyright 2015 - 2021 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 { EventContext } from "./event-context";
-import { EventMapper } from "../event-mapper";
-import { IResultContext, ISearchResult } from "../@types/search";
-
-export class SearchResult {
- /**
- * Create a SearchResponse from the response to /search
- */
-
- public static fromJson(jsonObj: ISearchResult, eventMapper: EventMapper): SearchResult {
- const jsonContext = jsonObj.context || ({} as IResultContext);
- let eventsBefore = (jsonContext.events_before || []).map(eventMapper);
- let eventsAfter = (jsonContext.events_after || []).map(eventMapper);
-
- const context = new EventContext(eventMapper(jsonObj.result));
-
- // Filter out any contextual events which do not correspond to the same timeline (thread or room)
- const threadRootId = context.ourEvent.threadRootId;
- eventsBefore = eventsBefore.filter((e) => e.threadRootId === threadRootId);
- eventsAfter = eventsAfter.filter((e) => e.threadRootId === threadRootId);
-
- context.setPaginateToken(jsonContext.start, true);
- context.addEvents(eventsBefore, true);
- context.addEvents(eventsAfter, false);
- context.setPaginateToken(jsonContext.end, false);
-
- return new SearchResult(jsonObj.rank, context);
- }
-
- /**
- * Construct a new SearchResult
- *
- * @param rank - where this SearchResult ranks in the results
- * @param context - the matching event and its
- * context
- */
- public constructor(public readonly rank: number, public readonly context: EventContext) {}
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/thread.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/thread.ts
deleted file mode 100644
index 9a4ead3..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/thread.ts
+++ /dev/null
@@ -1,669 +0,0 @@
-/*
-Copyright 2021 - 2023 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 { Optional } from "matrix-events-sdk";
-
-import { MatrixClient, PendingEventOrdering } from "../client";
-import { TypedReEmitter } from "../ReEmitter";
-import { RelationType } from "../@types/event";
-import { IThreadBundledRelationship, MatrixEvent, MatrixEventEvent } from "./event";
-import { Direction, EventTimeline } from "./event-timeline";
-import { EventTimelineSet, EventTimelineSetHandlerMap } from "./event-timeline-set";
-import { NotificationCountType, Room, RoomEvent } from "./room";
-import { RoomState } from "./room-state";
-import { ServerControlledNamespacedValue } from "../NamespacedValue";
-import { logger } from "../logger";
-import { ReadReceipt } from "./read-receipt";
-import { CachedReceiptStructure, ReceiptType } from "../@types/read_receipts";
-
-export enum ThreadEvent {
- New = "Thread.new",
- Update = "Thread.update",
- NewReply = "Thread.newReply",
- ViewThread = "Thread.viewThread",
- Delete = "Thread.delete",
-}
-
-type EmittedEvents = Exclude<ThreadEvent, ThreadEvent.New> | RoomEvent.Timeline | RoomEvent.TimelineReset;
-
-export type EventHandlerMap = {
- [ThreadEvent.Update]: (thread: Thread) => void;
- [ThreadEvent.NewReply]: (thread: Thread, event: MatrixEvent) => void;
- [ThreadEvent.ViewThread]: () => void;
- [ThreadEvent.Delete]: (thread: Thread) => void;
-} & EventTimelineSetHandlerMap;
-
-interface IThreadOpts {
- room: Room;
- client: MatrixClient;
- pendingEventOrdering?: PendingEventOrdering;
- receipts?: CachedReceiptStructure[];
-}
-
-export enum FeatureSupport {
- None = 0,
- Experimental = 1,
- Stable = 2,
-}
-
-export function determineFeatureSupport(stable: boolean, unstable: boolean): FeatureSupport {
- if (stable) {
- return FeatureSupport.Stable;
- } else if (unstable) {
- return FeatureSupport.Experimental;
- } else {
- return FeatureSupport.None;
- }
-}
-
-export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
- public static hasServerSideSupport = FeatureSupport.None;
- public static hasServerSideListSupport = FeatureSupport.None;
- public static hasServerSideFwdPaginationSupport = FeatureSupport.None;
-
- /**
- * A reference to all the events ID at the bottom of the threads
- */
- public readonly timelineSet: EventTimelineSet;
- public timeline: MatrixEvent[] = [];
-
- private _currentUserParticipated = false;
-
- private reEmitter: TypedReEmitter<EmittedEvents, EventHandlerMap>;
-
- private lastEvent: MatrixEvent | undefined;
- private replyCount = 0;
- private lastPendingEvent: MatrixEvent | undefined;
- private pendingReplyCount = 0;
-
- public readonly room: Room;
- public readonly client: MatrixClient;
- private readonly pendingEventOrdering: PendingEventOrdering;
-
- public initialEventsFetched = !Thread.hasServerSideSupport;
- /**
- * An array of events to add to the timeline once the thread has been initialised
- * with server suppport.
- */
- public replayEvents: MatrixEvent[] | null = [];
-
- public constructor(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.pendingEventOrdering = opts.pendingEventOrdering ?? PendingEventOrdering.Chronological;
- this.timelineSet = new EventTimelineSet(
- this.room,
- {
- timelineSupport: true,
- pendingEvents: true,
- },
- this.client,
- this,
- );
- this.reEmitter = new TypedReEmitter(this);
-
- this.reEmitter.reEmit(this.timelineSet, [RoomEvent.Timeline, RoomEvent.TimelineReset]);
-
- this.room.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
- this.room.on(RoomEvent.Redaction, this.onRedaction);
- this.room.on(RoomEvent.LocalEchoUpdated, this.onLocalEcho);
- this.timelineSet.on(RoomEvent.Timeline, this.onTimelineEvent);
-
- this.processReceipts(opts.receipts);
-
- // 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.updateThreadMetadata();
- this.setEventMetadata(this.rootEvent);
- }
-
- 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);
- }
- await this.processEvent(this.rootEvent);
- }
-
- public static setServerSideSupport(status: FeatureSupport): void {
- Thread.hasServerSideSupport = status;
- if (status !== FeatureSupport.Stable) {
- FILTER_RELATED_BY_SENDERS.setPreferUnstable(true);
- FILTER_RELATED_BY_REL_TYPES.setPreferUnstable(true);
- THREAD_RELATION_TYPE.setPreferUnstable(true);
- }
- }
-
- public static setServerSideListSupport(status: FeatureSupport): void {
- Thread.hasServerSideListSupport = status;
- }
-
- public static setServerSideFwdPaginationSupport(status: FeatureSupport): void {
- Thread.hasServerSideFwdPaginationSupport = status;
- }
-
- private onBeforeRedaction = (event: MatrixEvent, redaction: MatrixEvent): void => {
- if (
- event?.isRelation(THREAD_RELATION_TYPE.name) &&
- this.room.eventShouldLiveIn(event).threadId === this.id &&
- event.getId() !== this.id && // the root event isn't counted in the length so ignore this redaction
- !redaction.status // only respect it when it succeeds
- ) {
- this.replyCount--;
- this.updatePendingReplyCount();
- this.emit(ThreadEvent.Update, this);
- }
- };
-
- private onRedaction = async (event: MatrixEvent): Promise<void> => {
- if (event.threadRootId !== this.id) return; // ignore redactions for other timelines
- if (this.replyCount <= 0) {
- for (const threadEvent of this.timeline) {
- this.clearEventMetadata(threadEvent);
- }
- this.lastEvent = this.rootEvent;
- this._currentUserParticipated = false;
- this.emit(ThreadEvent.Delete, this);
- } else {
- await this.updateThreadMetadata();
- }
- };
-
- private onTimelineEvent = (
- event: MatrixEvent,
- room: Room | undefined,
- toStartOfTimeline: boolean | undefined,
- ): void => {
- // Add a synthesized receipt when paginating forward in the timeline
- if (!toStartOfTimeline) {
- room!.addLocalEchoReceipt(event.getSender()!, event, ReceiptType.Read);
- }
- this.onEcho(event, toStartOfTimeline ?? false);
- };
-
- private onLocalEcho = (event: MatrixEvent): void => {
- this.onEcho(event, false);
- };
-
- private onEcho = async (event: MatrixEvent, toStartOfTimeline: boolean): Promise<void> => {
- if (event.threadRootId !== this.id) return; // ignore echoes for other timelines
- if (this.lastEvent === event) return; // ignore duplicate events
- await this.updateThreadMetadata();
- if (!event.isRelation(THREAD_RELATION_TYPE.name)) return; // don't send a new reply event for reactions or edits
- if (toStartOfTimeline) return; // ignore messages added to the start of the timeline
- this.emit(ThreadEvent.NewReply, this, event);
- };
-
- public get roomState(): RoomState {
- return this.room.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
- }
-
- private addEventToTimeline(event: MatrixEvent, toStartOfTimeline: boolean): void {
- if (!this.findEventById(event.getId()!)) {
- this.timelineSet.addEventToTimeline(event, this.liveTimeline, {
- toStartOfTimeline,
- fromCache: false,
- roomState: this.roomState,
- });
- this.timeline = this.events;
- }
- }
-
- public addEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void {
- events.forEach((ev) => this.addEvent(ev, toStartOfTimeline, false));
- this.updateThreadMetadata();
- }
-
- /**
- * Add an event to the thread and updates
- * the tail/root references if needed
- * Will fire "Thread.update"
- * @param event - The event to add
- * @param toStartOfTimeline - whether the event is being added
- * to the start (and not the end) of the timeline.
- * @param emit - whether to emit the Update event if the thread was updated or not.
- */
- public async addEvent(event: MatrixEvent, toStartOfTimeline: boolean, emit = true): Promise<void> {
- this.setEventMetadata(event);
-
- const lastReply = this.lastReply();
- const isNewestReply = !lastReply || event.localTimestamp >= lastReply!.localTimestamp;
-
- // 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
- this.addEventToTimeline(event, toStartOfTimeline);
-
- this.client.decryptEventIfNeeded(event, {});
- } else if (!toStartOfTimeline && this.initialEventsFetched && isNewestReply) {
- this.addEventToTimeline(event, false);
- this.fetchEditsWhereNeeded(event);
- } else if (event.isRelation(RelationType.Annotation) || event.isRelation(RelationType.Replace)) {
- if (!this.initialEventsFetched) {
- /**
- * A thread can be fully discovered via a single sync response
- * And when that's the case we still ask the server to do an initialisation
- * as it's the safest to ensure we have everything.
- * However when we are in that scenario we might loose annotation or edits
- *
- * This fix keeps a reference to those events and replay them once the thread
- * has been initialised properly.
- */
- this.replayEvents?.push(event);
- } else {
- this.addEventToTimeline(event, toStartOfTimeline);
- }
- // Apply annotations and replace relations to the relations of the timeline only
- this.timelineSet.relations?.aggregateParentEvent(event);
- this.timelineSet.relations?.aggregateChildEvent(event, this.timelineSet);
- return;
- }
-
- // 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 || !this.rootEvent) && event.isRelation(THREAD_RELATION_TYPE.name)) {
- this.replyCount++;
- }
-
- if (emit) {
- this.emit(ThreadEvent.NewReply, this, event);
- this.updateThreadMetadata();
- }
- }
-
- public async processEvent(event: Optional<MatrixEvent>): Promise<void> {
- if (event) {
- this.setEventMetadata(event);
- await this.fetchEditsWhereNeeded(event);
- }
- this.timeline = this.events;
- }
-
- /**
- * Processes the receipts that were caught during initial sync
- * When clients become aware of a thread, they try to retrieve those read receipts
- * and apply them to the current thread
- * @param receipts - A collection of the receipts cached from initial sync
- */
- private processReceipts(receipts: CachedReceiptStructure[] = []): void {
- for (const { eventId, receiptType, userId, receipt, synthetic } of receipts) {
- this.addReceiptToStructure(eventId, receiptType as ReceiptType, userId, receipt, synthetic);
- }
- }
-
- private getRootEventBundledRelationship(rootEvent = this.rootEvent): IThreadBundledRelationship | undefined {
- return rootEvent?.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
- }
-
- private async processRootEvent(): Promise<void> {
- const bundledRelationship = this.getRootEventBundledRelationship();
- if (Thread.hasServerSideSupport && bundledRelationship) {
- this.replyCount = bundledRelationship.count;
- this._currentUserParticipated = !!bundledRelationship.current_user_participated;
-
- const mapper = this.client.getEventMapper();
- // re-insert roomId
- this.lastEvent = mapper({
- ...bundledRelationship.latest_event,
- room_id: this.roomId,
- });
- this.updatePendingReplyCount();
- await this.processEvent(this.lastEvent);
- }
- }
-
- private updatePendingReplyCount(): void {
- const unfilteredPendingEvents =
- this.pendingEventOrdering === PendingEventOrdering.Detached ? this.room.getPendingEvents() : this.events;
- const pendingEvents = unfilteredPendingEvents.filter(
- (ev) =>
- ev.threadRootId === this.id &&
- ev.isRelation(THREAD_RELATION_TYPE.name) &&
- ev.status !== null &&
- ev.getId() !== this.lastEvent?.getId(),
- );
- this.lastPendingEvent = pendingEvents.length ? pendingEvents[pendingEvents.length - 1] : undefined;
- this.pendingReplyCount = pendingEvents.length;
- }
-
- /**
- * Reset the live timeline of all timelineSets, and start new ones.
- *
- * <p>This is used when /sync returns a 'limited' timeline. 'Limited' means that there's a gap between the messages
- * /sync returned, and the last known message in our timeline. In such a case, our live timeline isn't live anymore
- * and has to be replaced by a new one. To make sure we can continue paginating our timelines correctly, we have to
- * set new pagination tokens on the old and the new timeline.
- *
- * @param backPaginationToken - token for back-paginating the new timeline
- * @param forwardPaginationToken - token for forward-paginating the old live timeline,
- * if absent or null, all timelines are reset, removing old ones (including the previous live
- * timeline which would otherwise be unable to paginate forwards without this token).
- * Removing just the old live timeline whilst preserving previous ones is not supported.
- */
- public async resetLiveTimeline(
- backPaginationToken?: string | null,
- forwardPaginationToken?: string | null,
- ): Promise<void> {
- const oldLive = this.liveTimeline;
- this.timelineSet.resetLiveTimeline(backPaginationToken ?? undefined, forwardPaginationToken ?? undefined);
- const newLive = this.liveTimeline;
-
- // FIXME: Remove the following as soon as https://github.com/matrix-org/synapse/issues/14830 is resolved.
- //
- // The pagination API for thread timelines currently can't handle the type of pagination tokens returned by sync
- //
- // To make this work anyway, we'll have to transform them into one of the types that the API can handle.
- // One option is passing the tokens to /messages, which can handle sync tokens, and returns the right format.
- // /messages does not return new tokens on requests with a limit of 0.
- // This means our timelines might overlap a slight bit, but that's not an issue, as we deduplicate messages
- // anyway.
-
- let newBackward: string | undefined;
- let oldForward: string | undefined;
- if (backPaginationToken) {
- const res = await this.client.createMessagesRequest(this.roomId, backPaginationToken, 1, Direction.Forward);
- newBackward = res.end;
- }
- if (forwardPaginationToken) {
- const res = await this.client.createMessagesRequest(
- this.roomId,
- forwardPaginationToken,
- 1,
- Direction.Backward,
- );
- oldForward = res.start;
- }
- // Only replace the token if we don't have paginated away from this position already. This situation doesn't
- // occur today, but if the above issue is resolved, we'd have to go down this path.
- if (forwardPaginationToken && oldLive.getPaginationToken(Direction.Forward) === forwardPaginationToken) {
- oldLive.setPaginationToken(oldForward ?? null, Direction.Forward);
- }
- if (backPaginationToken && newLive.getPaginationToken(Direction.Backward) === backPaginationToken) {
- newLive.setPaginationToken(newBackward ?? null, Direction.Backward);
- }
- }
-
- private async updateThreadMetadata(): Promise<void> {
- this.updatePendingReplyCount();
-
- if (Thread.hasServerSideSupport) {
- // Ensure we show *something* as soon as possible, we'll update it as soon as we get better data, but we
- // don't want the thread preview to be empty if we can avoid it
- if (!this.initialEventsFetched) {
- await this.processRootEvent();
- }
- await this.fetchRootEvent();
- }
- await this.processRootEvent();
-
- if (!this.initialEventsFetched) {
- this.initialEventsFetched = true;
- // fetch initial event to allow proper pagination
- try {
- // if the thread has regular events, this will just load the last reply.
- // if the thread is newly created, this will load the root event.
- if (this.replyCount === 0 && this.rootEvent) {
- this.timelineSet.addEventsToTimeline([this.rootEvent], true, this.liveTimeline, null);
- this.liveTimeline.setPaginationToken(null, Direction.Backward);
- } else {
- await this.client.paginateEventTimeline(this.liveTimeline, {
- backwards: true,
- limit: Math.max(1, this.length),
- });
- }
- for (const event of this.replayEvents!) {
- this.addEvent(event, false);
- }
- this.replayEvents = null;
- // just to make sure that, if we've created a timeline window for this thread before the thread itself
- // existed (e.g. when creating a new thread), we'll make sure the panel is force refreshed correctly.
- this.emit(RoomEvent.TimelineReset, this.room, this.timelineSet, true);
- } catch (e) {
- logger.error("Failed to load start of newly created thread: ", e);
- this.initialEventsFetched = false;
- }
- }
-
- this.emit(ThreadEvent.Update, this);
- }
-
- // XXX: Workaround for https://github.com/matrix-org/matrix-spec-proposals/pull/2676/files#r827240084
- private async fetchEditsWhereNeeded(...events: MatrixEvent[]): Promise<unknown> {
- return Promise.all(
- events
- .filter((e) => e.isEncrypted())
- .map((event: MatrixEvent) => {
- if (event.isRelation()) return; // skip - relations don't get edits
- return this.client
- .relations(this.roomId, event.getId()!, RelationType.Replace, event.getType(), {
- limit: 1,
- })
- .then((relations) => {
- if (relations.events.length) {
- event.makeReplaced(relations.events[0]);
- }
- })
- .catch((e) => {
- logger.error("Failed to load edits for encrypted thread event", e);
- });
- }),
- );
- }
-
- public setEventMetadata(event: Optional<MatrixEvent>): void {
- if (event) {
- EventTimeline.setEventMetadata(event, this.roomState, false);
- event.setThread(this);
- }
- }
-
- public clearEventMetadata(event: Optional<MatrixEvent>): void {
- if (event) {
- event.setThread(undefined);
- delete event.event?.unsigned?.["m.relations"]?.[THREAD_RELATION_TYPE.name];
- }
- }
-
- /**
- * Finds an event by ID in the current thread
- */
- public findEventById(eventId: string): MatrixEvent | undefined {
- return this.timelineSet.findEventById(eventId);
- }
-
- /**
- * Return last reply to the thread, if known.
- */
- public lastReply(matches: (ev: MatrixEvent) => boolean = (): boolean => true): MatrixEvent | null {
- for (let i = this.timeline.length - 1; i >= 0; i--) {
- const event = this.timeline[i];
- if (matches(event)) {
- return event;
- }
- }
- return null;
- }
-
- public get roomId(): string {
- return this.room.roomId;
- }
-
- /**
- * The number of messages in the thread
- * Only count rel_type=m.thread as we want to
- * exclude annotations from that number
- */
- public get length(): number {
- return this.replyCount + this.pendingReplyCount;
- }
-
- /**
- * A getter for the last event of the thread.
- * This might be a synthesized event, if so, it will not emit any events to listeners.
- */
- public get replyToEvent(): Optional<MatrixEvent> {
- return this.lastPendingEvent ?? this.lastEvent ?? this.lastReply();
- }
-
- public get events(): MatrixEvent[] {
- return this.liveTimeline.getEvents();
- }
-
- public has(eventId: string): boolean {
- return this.timelineSet.findEventById(eventId) instanceof MatrixEvent;
- }
-
- public get hasCurrentUserParticipated(): boolean {
- return this._currentUserParticipated;
- }
-
- public get liveTimeline(): EventTimeline {
- return this.timelineSet.getLiveTimeline();
- }
-
- public getUnfilteredTimelineSet(): EventTimelineSet {
- return this.timelineSet;
- }
-
- public addReceipt(event: MatrixEvent, synthetic: boolean): void {
- throw new Error("Unsupported function on the thread model");
- }
-
- /**
- * Get the ID of the event that a given user has read up to within this thread,
- * or null if we have received no read receipt (at all) from them.
- * @param userId - The user ID to get read receipt event ID for
- * @param ignoreSynthesized - If true, return only receipts that have been
- * sent by the server, not implicit ones generated
- * by the JS SDK.
- * @returns ID of the latest event that the given user has read, or null.
- */
- public getEventReadUpTo(userId: string, ignoreSynthesized?: boolean): string | null {
- const isCurrentUser = userId === this.client.getUserId();
- const lastReply = this.timeline[this.timeline.length - 1];
- if (isCurrentUser && lastReply) {
- // If the last activity in a thread is prior to the first threaded read receipt
- // sent in the room (suggesting that it was sent before the user started
- // using a client that supported threaded read receipts), we want to
- // consider this thread as read.
- const beforeFirstThreadedReceipt = lastReply.getTs() < this.room.getOldestThreadedReceiptTs();
- const lastReplyId = lastReply.getId();
- // Some unsent events do not have an ID, we do not want to consider them read
- if (beforeFirstThreadedReceipt && lastReplyId) {
- return lastReplyId;
- }
- }
-
- const readUpToId = super.getEventReadUpTo(userId, ignoreSynthesized);
-
- // Check whether the unthreaded read receipt for that user is more recent
- // than the read receipt inside that thread.
- if (lastReply) {
- const unthreadedReceipt = this.room.getLastUnthreadedReceiptFor(userId);
- if (!unthreadedReceipt) {
- return readUpToId;
- }
-
- for (let i = this.timeline?.length - 1; i >= 0; --i) {
- const ev = this.timeline[i];
- // If we encounter the `readUpToId` we do not need to look further
- // there is no "more recent" unthreaded read receipt
- if (ev.getId() === readUpToId) return readUpToId;
-
- // Inspecting events from most recent to oldest, we're checking
- // whether an unthreaded read receipt is more recent that the current event.
- // We usually prefer relying on the order of the DAG but in this scenario
- // it is not possible and we have to rely on timestamp
- if (ev.getTs() < unthreadedReceipt.ts) return ev.getId() ?? readUpToId;
- }
- }
-
- return readUpToId;
- }
-
- /**
- * Determine if the given user has read a particular event.
- *
- * It is invalid to call this method with an event that is not part of this thread.
- *
- * This is not a definitive check as it only checks the events that have been
- * loaded client-side at the time of execution.
- * @param userId - The user ID to check the read state of.
- * @param eventId - The event ID to check if the user read.
- * @returns True if the user has read the event, false otherwise.
- */
- public hasUserReadEvent(userId: string, eventId: string): boolean {
- if (userId === this.client.getUserId()) {
- // Consider an event read if it's part of a thread that is before the
- // first threaded receipt sent in that room. It is likely that it is
- // part of a thread that was created before MSC3771 was implemented.
- // Or before the last unthreaded receipt for the logged in user
- const beforeFirstThreadedReceipt =
- (this.lastReply()?.getTs() ?? 0) < this.room.getOldestThreadedReceiptTs();
- const unthreadedReceiptTs = this.room.getLastUnthreadedReceiptFor(userId)?.ts ?? 0;
- const beforeLastUnthreadedReceipt = (this?.lastReply()?.getTs() ?? 0) < unthreadedReceiptTs;
- if (beforeFirstThreadedReceipt || beforeLastUnthreadedReceipt) {
- return true;
- }
- }
-
- return super.hasUserReadEvent(userId, eventId);
- }
-
- public setUnread(type: NotificationCountType, count: number): void {
- return this.room.setThreadUnreadNotificationCount(this.id, type, count);
- }
-}
-
-export const FILTER_RELATED_BY_SENDERS = new ServerControlledNamespacedValue(
- "related_by_senders",
- "io.element.relation_senders",
-);
-export const FILTER_RELATED_BY_REL_TYPES = new ServerControlledNamespacedValue(
- "related_by_rel_types",
- "io.element.relation_types",
-);
-export const THREAD_RELATION_TYPE = new ServerControlledNamespacedValue("m.thread", "io.element.thread");
-
-export enum ThreadFilterType {
- "My",
- "All",
-}
-
-export function threadFilterTypeToFilter(type: ThreadFilterType | null): "all" | "participated" {
- switch (type) {
- case ThreadFilterType.My:
- return "participated";
- default:
- return "all";
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/typed-event-emitter.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/typed-event-emitter.ts
deleted file mode 100644
index 3cfe602..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/typed-event-emitter.ts
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
-Copyright 2021 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.
-*/
-
-// eslint-disable-next-line no-restricted-imports
-import { EventEmitter } from "events";
-
-export enum EventEmitterEvents {
- NewListener = "newListener",
- RemoveListener = "removeListener",
- Error = "error",
-}
-
-type AnyListener = (...args: any) => any;
-export type ListenerMap<E extends string> = { [eventName in E]: AnyListener };
-type EventEmitterEventListener = (eventName: string, listener: AnyListener) => void;
-type EventEmitterErrorListener = (error: Error) => void;
-
-export type Listener<E extends string, A extends ListenerMap<E>, T extends E | EventEmitterEvents> = T extends E
- ? A[T]
- : T extends EventEmitterEvents
- ? EventEmitterErrorListener
- : EventEmitterEventListener;
-
-/**
- * Typed Event Emitter class which can act as a Base Model for all our model
- * and communication events.
- * This makes it much easier for us to distinguish between events, as we now need
- * to properly type this, so that our events are not stringly-based and prone
- * to silly typos.
- */
-export class TypedEventEmitter<
- Events extends string,
- Arguments extends ListenerMap<Events>,
- SuperclassArguments extends ListenerMap<any> = Arguments,
-> extends EventEmitter {
- public addListener<T extends Events | EventEmitterEvents>(
- event: T,
- listener: Listener<Events, Arguments, T>,
- ): this {
- return super.addListener(event, listener);
- }
-
- public emit<T extends Events>(event: T, ...args: Parameters<SuperclassArguments[T]>): boolean;
- public emit<T extends Events>(event: T, ...args: Parameters<Arguments[T]>): boolean;
- public emit<T extends Events>(event: T, ...args: any[]): boolean {
- return super.emit(event, ...args);
- }
-
- public eventNames(): (Events | EventEmitterEvents)[] {
- return super.eventNames() as Array<Events | EventEmitterEvents>;
- }
-
- public listenerCount(event: Events | EventEmitterEvents): number {
- return super.listenerCount(event);
- }
-
- public listeners(event: Events | EventEmitterEvents): ReturnType<EventEmitter["listeners"]> {
- return super.listeners(event);
- }
-
- public off<T extends Events | EventEmitterEvents>(event: T, listener: Listener<Events, Arguments, T>): this {
- return super.off(event, listener);
- }
-
- public on<T extends Events | EventEmitterEvents>(event: T, listener: Listener<Events, Arguments, T>): this {
- return super.on(event, listener);
- }
-
- public once<T extends Events | EventEmitterEvents>(event: T, listener: Listener<Events, Arguments, T>): this {
- return super.once(event, listener);
- }
-
- public prependListener<T extends Events | EventEmitterEvents>(
- event: T,
- listener: Listener<Events, Arguments, T>,
- ): this {
- return super.prependListener(event, listener);
- }
-
- public prependOnceListener<T extends Events | EventEmitterEvents>(
- event: T,
- listener: Listener<Events, Arguments, T>,
- ): this {
- return super.prependOnceListener(event, listener);
- }
-
- public removeAllListeners(event?: Events | EventEmitterEvents): this {
- return super.removeAllListeners(event);
- }
-
- public removeListener<T extends Events | EventEmitterEvents>(
- event: T,
- listener: Listener<Events, Arguments, T>,
- ): this {
- return super.removeListener(event, listener);
- }
-
- public rawListeners(event: Events | EventEmitterEvents): ReturnType<EventEmitter["rawListeners"]> {
- return super.rawListeners(event);
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/user.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/user.ts
deleted file mode 100644
index 054a174..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/user.ts
+++ /dev/null
@@ -1,281 +0,0 @@
-/*
-Copyright 2015 - 2021 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 { MatrixEvent } from "./event";
-import { TypedEventEmitter } from "./typed-event-emitter";
-
-export enum UserEvent {
- DisplayName = "User.displayName",
- AvatarUrl = "User.avatarUrl",
- Presence = "User.presence",
- CurrentlyActive = "User.currentlyActive",
- LastPresenceTs = "User.lastPresenceTs",
-}
-
-export type UserEventHandlerMap = {
- /**
- * Fires whenever any user's display name changes.
- * @param event - The matrix event which caused this event to fire.
- * @param user - The user whose User.displayName changed.
- * @example
- * ```
- * matrixClient.on("User.displayName", function(event, user){
- * var newName = user.displayName;
- * });
- * ```
- */
- [UserEvent.DisplayName]: (event: MatrixEvent | undefined, user: User) => void;
- /**
- * Fires whenever any user's avatar URL changes.
- * @param event - The matrix event which caused this event to fire.
- * @param user - The user whose User.avatarUrl changed.
- * @example
- * ```
- * matrixClient.on("User.avatarUrl", function(event, user){
- * var newUrl = user.avatarUrl;
- * });
- * ```
- */
- [UserEvent.AvatarUrl]: (event: MatrixEvent | undefined, user: User) => void;
- /**
- * Fires whenever any user's presence changes.
- * @param event - The matrix event which caused this event to fire.
- * @param user - The user whose User.presence changed.
- * @example
- * ```
- * matrixClient.on("User.presence", function(event, user){
- * var newPresence = user.presence;
- * });
- * ```
- */
- [UserEvent.Presence]: (event: MatrixEvent | undefined, user: User) => void;
- /**
- * Fires whenever any user's currentlyActive changes.
- * @param event - The matrix event which caused this event to fire.
- * @param user - The user whose User.currentlyActive changed.
- * @example
- * ```
- * matrixClient.on("User.currentlyActive", function(event, user){
- * var newCurrentlyActive = user.currentlyActive;
- * });
- * ```
- */
- [UserEvent.CurrentlyActive]: (event: MatrixEvent | undefined, user: User) => void;
- /**
- * Fires whenever any user's lastPresenceTs changes,
- * ie. whenever any presence event is received for a user.
- * @param event - The matrix event which caused this event to fire.
- * @param user - The user whose User.lastPresenceTs changed.
- * @example
- * ```
- * matrixClient.on("User.lastPresenceTs", function(event, user){
- * var newlastPresenceTs = user.lastPresenceTs;
- * });
- * ```
- */
- [UserEvent.LastPresenceTs]: (event: MatrixEvent | undefined, user: User) => void;
-};
-
-export class User extends TypedEventEmitter<UserEvent, UserEventHandlerMap> {
- private modified = -1;
-
- /**
- * The 'displayname' of the user if known.
- * @privateRemarks
- * Should be read-only
- */
- public displayName?: string;
- public rawDisplayName?: string;
- /**
- * The 'avatar_url' of the user if known.
- * @privateRemarks
- * Should be read-only
- */
- public avatarUrl?: string;
- /**
- * The presence status message if known.
- * @privateRemarks
- * Should be read-only
- */
- public presenceStatusMsg?: string;
- /**
- * The presence enum if known.
- * @privateRemarks
- * Should be read-only
- */
- public presence = "offline";
- /**
- * Timestamp (ms since the epoch) for when we last received presence data for this user.
- * We can subtract lastActiveAgo from this to approximate an absolute value for when a user was last active.
- * @privateRemarks
- * Should be read-only
- */
- public lastActiveAgo = 0;
- /**
- * The time elapsed in ms since the user interacted proactively with the server,
- * or we saw a message from the user
- * @privateRemarks
- * Should be read-only
- */
- public lastPresenceTs = 0;
- /**
- * Whether we should consider lastActiveAgo to be an approximation
- * and that the user should be seen as active 'now'
- * @privateRemarks
- * Should be read-only
- */
- public currentlyActive = false;
- /**
- * The events describing this user.
- * @privateRemarks
- * Should be read-only
- */
- public events: {
- /** The m.presence event for this user. */
- presence?: MatrixEvent;
- profile?: MatrixEvent;
- } = {};
-
- /**
- * Construct a new User. A User must have an ID and can optionally have extra information associated with it.
- * @param userId - Required. The ID of this user.
- */
- public constructor(public readonly userId: string) {
- super();
- this.displayName = userId;
- this.rawDisplayName = userId;
- this.updateModifiedTime();
- }
-
- /**
- * Update this User with the given presence event. May fire "User.presence",
- * "User.avatarUrl" and/or "User.displayName" if this event updates this user's
- * properties.
- * @param event - The `m.presence` event.
- *
- * @remarks
- * Fires {@link UserEvent.Presence}
- * Fires {@link UserEvent.DisplayName}
- * Fires {@link UserEvent.AvatarUrl}
- */
- public setPresenceEvent(event: MatrixEvent): void {
- if (event.getType() !== "m.presence") {
- return;
- }
- const firstFire = this.events.presence === null;
- this.events.presence = event;
-
- const eventsToFire: UserEvent[] = [];
- if (event.getContent().presence !== this.presence || firstFire) {
- eventsToFire.push(UserEvent.Presence);
- }
- if (event.getContent().avatar_url && event.getContent().avatar_url !== this.avatarUrl) {
- eventsToFire.push(UserEvent.AvatarUrl);
- }
- if (event.getContent().displayname && event.getContent().displayname !== this.displayName) {
- eventsToFire.push(UserEvent.DisplayName);
- }
- if (
- event.getContent().currently_active !== undefined &&
- event.getContent().currently_active !== this.currentlyActive
- ) {
- eventsToFire.push(UserEvent.CurrentlyActive);
- }
-
- this.presence = event.getContent().presence;
- eventsToFire.push(UserEvent.LastPresenceTs);
-
- if (event.getContent().status_msg) {
- this.presenceStatusMsg = event.getContent().status_msg;
- }
- if (event.getContent().displayname) {
- this.displayName = event.getContent().displayname;
- }
- if (event.getContent().avatar_url) {
- this.avatarUrl = event.getContent().avatar_url;
- }
- this.lastActiveAgo = event.getContent().last_active_ago;
- this.lastPresenceTs = Date.now();
- this.currentlyActive = event.getContent().currently_active;
-
- this.updateModifiedTime();
-
- for (const eventToFire of eventsToFire) {
- this.emit(eventToFire, event, this);
- }
- }
-
- /**
- * Manually set this user's display name. No event is emitted in response to this
- * as there is no underlying MatrixEvent to emit with.
- * @param name - The new display name.
- */
- public setDisplayName(name: string): void {
- const oldName = this.displayName;
- this.displayName = name;
- if (name !== oldName) {
- this.updateModifiedTime();
- }
- }
-
- /**
- * Manually set this user's non-disambiguated display name. No event is emitted
- * in response to this as there is no underlying MatrixEvent to emit with.
- * @param name - The new display name.
- */
- public setRawDisplayName(name?: string): void {
- this.rawDisplayName = name;
- }
-
- /**
- * Manually set this user's avatar URL. No event is emitted in response to this
- * as there is no underlying MatrixEvent to emit with.
- * @param url - The new avatar URL.
- */
- public setAvatarUrl(url?: string): void {
- const oldUrl = this.avatarUrl;
- this.avatarUrl = url;
- if (url !== oldUrl) {
- this.updateModifiedTime();
- }
- }
-
- /**
- * Update the last modified time to the current time.
- */
- private updateModifiedTime(): void {
- this.modified = Date.now();
- }
-
- /**
- * Get the timestamp when this User was last updated. This timestamp is
- * updated when this User receives a new Presence event which has updated a
- * property on this object. It is updated <i>before</i> firing events.
- * @returns The timestamp
- */
- public getLastModifiedTime(): number {
- return this.modified;
- }
-
- /**
- * Get the absolute timestamp when this User was last known active on the server.
- * It is *NOT* accurate if this.currentlyActive is true.
- * @returns The timestamp
- */
- public getLastActiveTs(): number {
- return this.lastPresenceTs - this.lastActiveAgo;
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/pushprocessor.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/pushprocessor.ts
deleted file mode 100644
index 78d26fe..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/pushprocessor.ts
+++ /dev/null
@@ -1,770 +0,0 @@
-/*
-Copyright 2015 - 2021 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 { deepCompare, escapeRegExp, globToRegexp, isNullOrUndefined } from "./utils";
-import { logger } from "./logger";
-import { MatrixClient } from "./client";
-import { MatrixEvent } from "./models/event";
-import {
- ConditionKind,
- IAnnotatedPushRule,
- ICallStartedCondition,
- ICallStartedPrefixCondition,
- IContainsDisplayNameCondition,
- IEventMatchCondition,
- IEventPropertyIsCondition,
- IEventPropertyContainsCondition,
- IPushRule,
- IPushRules,
- IRoomMemberCountCondition,
- ISenderNotificationPermissionCondition,
- PushRuleAction,
- PushRuleActionName,
- PushRuleCondition,
- PushRuleKind,
- PushRuleSet,
- RuleId,
- TweakName,
-} from "./@types/PushRules";
-import { EventType } from "./@types/event";
-
-const RULEKINDS_IN_ORDER = [
- PushRuleKind.Override,
- PushRuleKind.ContentSpecific,
- PushRuleKind.RoomSpecific,
- PushRuleKind.SenderSpecific,
- PushRuleKind.Underride,
-];
-
-// The default override rules to apply to the push rules that arrive from the server.
-// We do this for two reasons:
-// 1. Synapse is unlikely to send us the push rule in an incremental sync - see
-// https://github.com/matrix-org/synapse/pull/4867#issuecomment-481446072 for
-// more details.
-// 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 MSC2153 yet
- rule_id: ".m.rule.reaction",
- default: true,
- enabled: true,
- conditions: [
- {
- kind: ConditionKind.EventMatch,
- key: "type",
- pattern: "m.reaction",
- },
- ],
- actions: [PushRuleActionName.DontNotify],
- },
- {
- rule_id: RuleId.IsUserMention,
- default: true,
- enabled: true,
- conditions: [
- {
- kind: ConditionKind.EventPropertyContains,
- key: "content.org\\.matrix\\.msc3952\\.mentions.user_ids",
- value: "", // The user ID is dynamically added in rewriteDefaultRules.
- },
- ],
- actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight }],
- },
- {
- rule_id: RuleId.IsRoomMention,
- default: true,
- enabled: true,
- conditions: [
- {
- kind: ConditionKind.EventPropertyIs,
- key: "content.org\\.matrix\\.msc3952\\.mentions.room",
- value: true,
- },
- {
- kind: ConditionKind.SenderNotificationPermission,
- key: "room",
- },
- ],
- actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight }],
- },
- {
- // 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,
- },
- {
- kind: ConditionKind.EventMatch,
- key: "state_key",
- pattern: "",
- },
- ],
- actions: [],
- },
-];
-
-const DEFAULT_UNDERRIDE_RULES: IPushRule[] = [
- {
- // For homeservers which don't support MSC3914 yet
- rule_id: ".org.matrix.msc3914.rule.room.call",
- default: true,
- enabled: true,
- conditions: [
- {
- kind: ConditionKind.EventMatch,
- key: "type",
- pattern: "org.matrix.msc3401.call",
- },
- {
- kind: ConditionKind.CallStarted,
- },
- ],
- actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Sound, value: "default" }],
- },
-];
-
-export interface IActionsObject {
- /** Whether this event should notify the user or not. */
- notify: boolean;
- /** How this event should be notified. */
- tweaks: Partial<Record<TweakName, any>>;
-}
-
-export class PushProcessor {
- /**
- * Construct a Push Processor.
- * @param client - The Matrix client object to use
- */
- public constructor(private readonly client: MatrixClient) {}
-
- /**
- * Maps the original key from the push rules to a list of property names
- * after unescaping.
- */
- private readonly parsedKeys = new Map<string, string[]>();
-
- /**
- * Convert a list of actions into a object with the actions as keys and their values
- * @example
- * eg. `[ 'notify', { set_tweak: 'sound', value: 'default' } ]`
- * becomes `{ notify: true, tweaks: { sound: 'default' } }`
- * @param actionList - The actions list
- *
- * @returns A object with key 'notify' (true or false) and an object of actions
- */
- public static actionListToActionsObject(actionList: PushRuleAction[]): IActionsObject {
- const actionObj: IActionsObject = { notify: false, tweaks: {} };
- for (const action of actionList) {
- if (action === PushRuleActionName.Notify) {
- actionObj.notify = true;
- } else if (typeof action === "object") {
- if (action.value === undefined) {
- action.value = true;
- }
- actionObj.tweaks[action.set_tweak] = action.value;
- }
- }
- return actionObj;
- }
-
- /**
- * Rewrites conditions on a client's push rules to match the defaults
- * where applicable. Useful for upgrading push rules to more strict
- * conditions when the server is falling behind on defaults.
- * @param incomingRules - The client's existing push rules
- * @param userId - The Matrix ID of the client.
- * @returns The rewritten rules
- */
- public static rewriteDefaultRules(incomingRules: IPushRules, userId: string | undefined = undefined): IPushRules {
- let newRules: IPushRules = JSON.parse(JSON.stringify(incomingRules)); // deep clone
-
- // These lines are mostly to make the tests happy. We shouldn't run into these
- // properties missing in practice.
- if (!newRules) newRules = {} as IPushRules;
- if (!newRules.global) newRules.global = {} as PushRuleSet;
- if (!newRules.global.override) newRules.global.override = [];
- if (!newRules.global.underride) newRules.global.underride = [];
-
- // Merge the client-level defaults with the ones from the server
- const globalOverrides = newRules.global.override;
- for (const originalOverride of DEFAULT_OVERRIDE_RULES) {
- const existingRule = globalOverrides.find((r) => r.rule_id === originalOverride.rule_id);
-
- // Dynamically add the user ID as the value for the is_user_mention rule.
- let override: IPushRule;
- if (originalOverride.rule_id === RuleId.IsUserMention) {
- // If the user ID wasn't provided, skip the rule.
- if (!userId) {
- continue;
- }
-
- override = JSON.parse(JSON.stringify(originalOverride)); // deep clone
- override.conditions![0].value = userId;
- } else {
- override = originalOverride;
- }
-
- if (existingRule) {
- // Copy over the actions, default, and conditions. Don't touch the user's preference.
- existingRule.default = override.default;
- existingRule.conditions = override.conditions;
- existingRule.actions = override.actions;
- } else {
- // Add the rule
- const ruleId = override.rule_id;
- logger.warn(`Adding default global override for ${ruleId}`);
- globalOverrides.push(override);
- }
- }
-
- const globalUnderrides = newRules.global.underride ?? [];
- for (const underride of DEFAULT_UNDERRIDE_RULES) {
- const existingRule = globalUnderrides.find((r) => r.rule_id === underride.rule_id);
-
- if (existingRule) {
- // Copy over the actions, default, and conditions. Don't touch the user's preference.
- existingRule.default = underride.default;
- existingRule.conditions = underride.conditions;
- existingRule.actions = underride.actions;
- } else {
- // Add the rule
- const ruleId = underride.rule_id;
- logger.warn(`Adding default global underride for ${ruleId}`);
- globalUnderrides.push(underride);
- }
- }
-
- return newRules;
- }
-
- /**
- * Pre-caches the parsed keys for push rules and cleans out any obsolete cache
- * entries. Should be called after push rules are updated.
- * @param newRules - The new push rules.
- */
- public updateCachedPushRuleKeys(newRules: IPushRules): void {
- // These lines are mostly to make the tests happy. We shouldn't run into these
- // properties missing in practice.
- if (!newRules) newRules = {} as IPushRules;
- if (!newRules.global) newRules.global = {} as PushRuleSet;
- if (!newRules.global.override) newRules.global.override = [];
- if (!newRules.global.room) newRules.global.room = [];
- if (!newRules.global.sender) newRules.global.sender = [];
- if (!newRules.global.underride) newRules.global.underride = [];
-
- // Process the 'key' property on event_match conditions pre-cache the
- // values and clean-out any unused values.
- const toRemoveKeys = new Set(this.parsedKeys.keys());
- for (const ruleset of [
- newRules.global.override,
- newRules.global.room,
- newRules.global.sender,
- newRules.global.underride,
- ]) {
- for (const rule of ruleset) {
- if (!rule.conditions) {
- continue;
- }
-
- for (const condition of rule.conditions) {
- if (condition.kind !== ConditionKind.EventMatch) {
- continue;
- }
-
- // Ensure we keep this key.
- toRemoveKeys.delete(condition.key);
-
- // Pre-process the key.
- this.parsedKeys.set(condition.key, PushProcessor.partsForDottedKey(condition.key));
- }
- }
- }
- // Any keys that were previously cached, but are no longer needed should
- // be removed.
- toRemoveKeys.forEach((k) => this.parsedKeys.delete(k));
- }
-
- private static cachedGlobToRegex: Record<string, RegExp> = {}; // $glob: RegExp
-
- private matchingRuleFromKindSet(ev: MatrixEvent, kindset: PushRuleSet): IAnnotatedPushRule | null {
- for (const kind of RULEKINDS_IN_ORDER) {
- const ruleset = kindset[kind];
- if (!ruleset) {
- continue;
- }
-
- for (const rule of ruleset) {
- if (!rule.enabled) {
- continue;
- }
-
- const rawrule = this.templateRuleToRaw(kind, rule);
- if (!rawrule) {
- continue;
- }
-
- if (this.ruleMatchesEvent(rawrule, ev)) {
- return {
- ...rule,
- kind,
- };
- }
- }
- }
- return null;
- }
-
- private templateRuleToRaw(
- kind: PushRuleKind,
- tprule: IPushRule,
- ): Pick<IPushRule, "rule_id" | "actions" | "conditions"> | null {
- const rawrule: Pick<IPushRule, "rule_id" | "actions" | "conditions"> = {
- rule_id: tprule.rule_id,
- actions: tprule.actions,
- conditions: [],
- };
- switch (kind) {
- case PushRuleKind.Underride:
- case PushRuleKind.Override:
- rawrule.conditions = tprule.conditions;
- break;
- case PushRuleKind.RoomSpecific:
- if (!tprule.rule_id) {
- return null;
- }
- rawrule.conditions!.push({
- kind: ConditionKind.EventMatch,
- key: "room_id",
- value: tprule.rule_id,
- });
- break;
- case PushRuleKind.SenderSpecific:
- if (!tprule.rule_id) {
- return null;
- }
- rawrule.conditions!.push({
- kind: ConditionKind.EventMatch,
- key: "user_id",
- value: tprule.rule_id,
- });
- break;
- case PushRuleKind.ContentSpecific:
- if (!tprule.pattern) {
- return null;
- }
- rawrule.conditions!.push({
- kind: ConditionKind.EventMatch,
- key: "content.body",
- pattern: tprule.pattern,
- });
- break;
- }
- return rawrule;
- }
-
- private eventFulfillsCondition(cond: PushRuleCondition, ev: MatrixEvent): boolean {
- switch (cond.kind) {
- case ConditionKind.EventMatch:
- return this.eventFulfillsEventMatchCondition(cond, ev);
- case ConditionKind.EventPropertyIs:
- return this.eventFulfillsEventPropertyIsCondition(cond, ev);
- case ConditionKind.EventPropertyContains:
- return this.eventFulfillsEventPropertyContains(cond, ev);
- case ConditionKind.ContainsDisplayName:
- return this.eventFulfillsDisplayNameCondition(cond, ev);
- case ConditionKind.RoomMemberCount:
- return this.eventFulfillsRoomMemberCountCondition(cond, ev);
- case ConditionKind.SenderNotificationPermission:
- return this.eventFulfillsSenderNotifPermCondition(cond, ev);
- case ConditionKind.CallStarted:
- case ConditionKind.CallStartedPrefix:
- return this.eventFulfillsCallStartedCondition(cond, ev);
- }
-
- // unknown conditions: we previously matched all unknown conditions,
- // but given that rules can be added to the base rules on a server,
- // it's probably better to not match unknown conditions.
- return false;
- }
-
- private eventFulfillsSenderNotifPermCondition(
- cond: ISenderNotificationPermissionCondition,
- ev: MatrixEvent,
- ): boolean {
- const notifLevelKey = cond["key"];
- if (!notifLevelKey) {
- return false;
- }
-
- const room = this.client.getRoom(ev.getRoomId());
- if (!room?.currentState) {
- return false;
- }
-
- // Note that this should not be the current state of the room but the state at
- // the point the event is in the DAG. Unfortunately the js-sdk does not store
- // this.
- return room.currentState.mayTriggerNotifOfType(notifLevelKey, ev.getSender()!);
- }
-
- private eventFulfillsRoomMemberCountCondition(cond: IRoomMemberCountCondition, ev: MatrixEvent): boolean {
- if (!cond.is) {
- return false;
- }
-
- const room = this.client.getRoom(ev.getRoomId());
- if (!room || !room.currentState || !room.currentState.members) {
- return false;
- }
-
- const memberCount = room.currentState.getJoinedMemberCount();
-
- const m = cond.is.match(/^([=<>]*)(\d*)$/);
- if (!m) {
- return false;
- }
- const ineq = m[1];
- const rhs = parseInt(m[2]);
- if (isNaN(rhs)) {
- return false;
- }
- switch (ineq) {
- case "":
- case "==":
- return memberCount == rhs;
- case "<":
- return memberCount < rhs;
- case ">":
- return memberCount > rhs;
- case "<=":
- return memberCount <= rhs;
- case ">=":
- return memberCount >= rhs;
- default:
- return false;
- }
- }
-
- private eventFulfillsDisplayNameCondition(cond: IContainsDisplayNameCondition, ev: MatrixEvent): boolean {
- let content = ev.getContent();
- if (ev.isEncrypted() && ev.getClearContent()) {
- content = ev.getClearContent()!;
- }
- if (!content || !content.body || typeof content.body != "string") {
- return false;
- }
-
- const room = this.client.getRoom(ev.getRoomId());
- const member = room?.currentState?.getMember(this.client.credentials.userId!);
- if (!member) {
- return false;
- }
-
- const displayName = member.name;
-
- // N.B. we can't use \b as it chokes on unicode. however \W seems to be okay
- // as shorthand for [^0-9A-Za-z_].
- const pat = new RegExp("(^|\\W)" + escapeRegExp(displayName) + "(\\W|$)", "i");
- return content.body.search(pat) > -1;
- }
-
- /**
- * Check whether the given event matches the push rule condition by fetching
- * the property from the event and comparing against the condition's glob-based
- * pattern.
- * @param cond - The push rule condition to check for a match.
- * @param ev - The event to check for a match.
- */
- private eventFulfillsEventMatchCondition(cond: IEventMatchCondition, ev: MatrixEvent): boolean {
- if (!cond.key) {
- return false;
- }
-
- const val = this.valueForDottedKey(cond.key, ev);
- if (typeof val !== "string") {
- return false;
- }
-
- // XXX This does not match in a case-insensitive manner.
- //
- // See https://spec.matrix.org/v1.5/client-server-api/#conditions-1
- if (cond.value) {
- return cond.value === val;
- }
-
- if (typeof cond.pattern !== "string") {
- return false;
- }
-
- const regex =
- cond.key === "content.body"
- ? this.createCachedRegex("(^|\\W)", cond.pattern, "(\\W|$)")
- : this.createCachedRegex("^", cond.pattern, "$");
-
- return !!val.match(regex);
- }
-
- /**
- * Check whether the given event matches the push rule condition by fetching
- * the property from the event and comparing exactly against the condition's
- * value.
- * @param cond - The push rule condition to check for a match.
- * @param ev - The event to check for a match.
- */
- private eventFulfillsEventPropertyIsCondition(cond: IEventPropertyIsCondition, ev: MatrixEvent): boolean {
- if (!cond.key || cond.value === undefined) {
- return false;
- }
- return cond.value === this.valueForDottedKey(cond.key, ev);
- }
-
- /**
- * Check whether the given event matches the push rule condition by fetching
- * the property from the event and comparing exactly against the condition's
- * value.
- * @param cond - The push rule condition to check for a match.
- * @param ev - The event to check for a match.
- */
- private eventFulfillsEventPropertyContains(cond: IEventPropertyContainsCondition, ev: MatrixEvent): boolean {
- if (!cond.key || cond.value === undefined) {
- return false;
- }
- const val = this.valueForDottedKey(cond.key, ev);
- if (!Array.isArray(val)) {
- return false;
- }
- return val.includes(cond.value);
- }
-
- private eventFulfillsCallStartedCondition(
- _cond: ICallStartedCondition | ICallStartedPrefixCondition,
- ev: MatrixEvent,
- ): boolean {
- // Since servers don't support properly sending push notification
- // about MSC3401 call events, we do the handling ourselves
- return (
- ["m.ring", "m.prompt"].includes(ev.getContent()["m.intent"]) &&
- !("m.terminated" in ev.getContent()) &&
- (ev.getPrevContent()["m.terminated"] !== ev.getContent()["m.terminated"] ||
- deepCompare(ev.getPrevContent(), {}))
- );
- }
-
- private createCachedRegex(prefix: string, glob: string, suffix: string): RegExp {
- if (PushProcessor.cachedGlobToRegex[glob]) {
- return PushProcessor.cachedGlobToRegex[glob];
- }
- PushProcessor.cachedGlobToRegex[glob] = new RegExp(
- prefix + globToRegexp(glob) + suffix,
- "i", // Case insensitive
- );
- return PushProcessor.cachedGlobToRegex[glob];
- }
-
- /**
- * Parse the key into the separate fields to search by splitting on
- * unescaped ".", and then removing any escape characters.
- *
- * @param str - The key of the push rule condition: a dotted field.
- * @returns The unescaped parts to fetch.
- * @internal
- */
- public static partsForDottedKey(str: string): string[] {
- const result = [];
-
- // The current field and whether the previous character was the escape
- // character (a backslash).
- let part = "";
- let escaped = false;
-
- // Iterate over each character, and decide whether to append to the current
- // part (following the escape rules) or to start a new part (based on the
- // field separator).
- for (const c of str) {
- // If the previous character was the escape character (a backslash)
- // then decide what to append to the current part.
- if (escaped) {
- if (c === "\\" || c === ".") {
- // An escaped backslash or dot just gets added.
- part += c;
- } else {
- // A character that shouldn't be escaped gets the backslash prepended.
- part += "\\" + c;
- }
- // This always resets being escaped.
- escaped = false;
- continue;
- }
-
- if (c == ".") {
- // The field separator creates a new part.
- result.push(part);
- part = "";
- } else if (c == "\\") {
- // A backslash adds no characters, but starts an escape sequence.
- escaped = true;
- } else {
- // Otherwise, just add the current character.
- part += c;
- }
- }
-
- // Ensure the final part is included. If there's an open escape sequence
- // it should be included.
- if (escaped) {
- part += "\\";
- }
- result.push(part);
-
- return result;
- }
-
- /**
- * For a dotted field and event, fetch the value at that position, if one
- * exists.
- *
- * @param key - The key of the push rule condition: a dotted field to fetch.
- * @param ev - The matrix event to fetch the field from.
- * @returns The value at the dotted path given by key.
- */
- private valueForDottedKey(key: string, ev: MatrixEvent): any {
- // The key should already have been parsed via updateCachedPushRuleKeys,
- // but if it hasn't (maybe via an old consumer of the SDK which hasn't
- // been updated?) then lazily calculate it here.
- let parts = this.parsedKeys.get(key);
- if (parts === undefined) {
- parts = PushProcessor.partsForDottedKey(key);
- this.parsedKeys.set(key, parts);
- }
- let val: any;
-
- // special-case the first component to deal with encrypted messages
- const firstPart = parts[0];
- let currentIndex = 0;
- if (firstPart === "content") {
- val = ev.getContent();
- ++currentIndex;
- } else if (firstPart === "type") {
- val = ev.getType();
- ++currentIndex;
- } else {
- // use the raw event for any other fields
- val = ev.event;
- }
-
- for (; currentIndex < parts.length; ++currentIndex) {
- // The previous iteration resulted in null or undefined, bail (and
- // avoid the type error of attempting to retrieve a property).
- if (isNullOrUndefined(val)) {
- return undefined;
- }
-
- const thisPart = parts[currentIndex];
- val = val[thisPart];
- }
- return val;
- }
-
- private matchingRuleForEventWithRulesets(ev: MatrixEvent, rulesets?: IPushRules): IAnnotatedPushRule | null {
- if (!rulesets) {
- return null;
- }
- if (ev.getSender() === this.client.credentials.userId) {
- return null;
- }
-
- return this.matchingRuleFromKindSet(ev, rulesets.global);
- }
-
- private pushActionsForEventAndRulesets(ev: MatrixEvent, rulesets?: IPushRules): IActionsObject {
- const rule = this.matchingRuleForEventWithRulesets(ev, rulesets);
- if (!rule) {
- return {} as IActionsObject;
- }
-
- const actionObj = PushProcessor.actionListToActionsObject(rule.actions);
-
- // Some actions are implicit in some situations: we add those here
- if (actionObj.tweaks.highlight === undefined) {
- // if it isn't specified, highlight if it's a content
- // rule but otherwise not
- actionObj.tweaks.highlight = rule.kind == PushRuleKind.ContentSpecific;
- }
-
- return actionObj;
- }
-
- public ruleMatchesEvent(rule: Partial<IPushRule> & Pick<IPushRule, "conditions">, ev: MatrixEvent): boolean {
- // Disable the deprecated mentions push rules if the new mentions property exists.
- if (
- this.client.supportsIntentionalMentions() &&
- ev.getContent()["org.matrix.msc3952.mentions"] !== undefined &&
- (rule.rule_id === RuleId.ContainsUserName ||
- rule.rule_id === RuleId.ContainsDisplayName ||
- rule.rule_id === RuleId.AtRoomNotification)
- ) {
- return false;
- }
-
- return !rule.conditions?.some((cond) => !this.eventFulfillsCondition(cond, ev));
- }
-
- /**
- * Get the user's push actions for the given event
- */
- public actionsForEvent(ev: MatrixEvent): IActionsObject {
- return this.pushActionsForEventAndRulesets(ev, this.client.pushRules);
- }
-
- /**
- * Get one of the users push rules by its ID
- *
- * @param ruleId - The ID of the rule to search for
- * @returns The push rule, or null if no such rule was found
- */
- public getPushRuleById(ruleId: string): IPushRule | null {
- const result = this.getPushRuleAndKindById(ruleId);
- return result?.rule ?? null;
- }
-
- /**
- * Get one of the users push rules by its ID
- *
- * @param ruleId - The ID of the rule to search for
- * @returns rule The push rule, or null if no such rule was found
- * @returns kind - The PushRuleKind of the rule to search for
- */
- public getPushRuleAndKindById(ruleId: string): { rule: IPushRule; kind: PushRuleKind } | null {
- for (const scope of ["global"] as const) {
- if (this.client.pushRules?.[scope] === undefined) continue;
-
- for (const kind of RULEKINDS_IN_ORDER) {
- if (this.client.pushRules[scope][kind] === undefined) continue;
-
- for (const rule of this.client.pushRules[scope][kind]!) {
- if (rule.rule_id === ruleId) return { rule, kind };
- }
- }
- }
- return null;
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/randomstring.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/randomstring.ts
deleted file mode 100644
index 0ed46fb..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/randomstring.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-const LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
-const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
-const DIGITS = "0123456789";
-
-export function randomString(len: number): string {
- return randomStringFrom(len, UPPERCASE + LOWERCASE + DIGITS);
-}
-
-export function randomLowercaseString(len: number): string {
- return randomStringFrom(len, LOWERCASE);
-}
-
-export function randomUppercaseString(len: number): string {
- return randomStringFrom(len, UPPERCASE);
-}
-
-function randomStringFrom(len: number, chars: string): string {
- let ret = "";
-
- for (let i = 0; i < len; ++i) {
- ret += chars.charAt(Math.floor(Math.random() * chars.length));
- }
-
- return ret;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/realtime-callbacks.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/realtime-callbacks.ts
deleted file mode 100644
index 1b03a57..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/realtime-callbacks.ts
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
-Copyright 2016 OpenMarket Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-/* A re-implementation of the javascript callback functions (setTimeout,
- * clearTimeout; setInterval and clearInterval are not yet implemented) which
- * try to improve handling of large clock jumps (as seen when
- * suspending/resuming the system).
- *
- * In particular, if a timeout would have fired while the system was suspended,
- * it will instead fire as soon as possible after resume.
- */
-
-import { logger } from "./logger";
-
-// we schedule a callback at least this often, to check if we've missed out on
-// some wall-clock time due to being suspended.
-const TIMER_CHECK_PERIOD_MS = 1000;
-
-// counter, for making up ids to return from setTimeout
-let count = 0;
-
-// the key for our callback with the real global.setTimeout
-let realCallbackKey: NodeJS.Timeout | number;
-
-type Callback = {
- runAt: number;
- func: (...params: any[]) => void;
- params: any[];
- key: number;
-};
-
-// a sorted list of the callbacks to be run.
-// each is an object with keys [runAt, func, params, key].
-const callbackList: Callback[] = [];
-
-// var debuglog = logger.log.bind(logger);
-/* istanbul ignore next */
-const debuglog = function (...params: any[]): void {};
-
-/**
- * reimplementation of window.setTimeout, which will call the callback if
- * the wallclock time goes past the deadline.
- *
- * @param func - callback to be called after a delay
- * @param delayMs - number of milliseconds to delay by
- *
- * @returns an identifier for this callback, which may be passed into
- * clearTimeout later.
- */
-export function setTimeout(func: (...params: any[]) => void, delayMs: number, ...params: any[]): number {
- delayMs = delayMs || 0;
- if (delayMs < 0) {
- delayMs = 0;
- }
-
- const runAt = Date.now() + delayMs;
- const key = count++;
- debuglog("setTimeout: scheduling cb", key, "at", runAt, "(delay", delayMs, ")");
- const data = {
- runAt: runAt,
- func: func,
- params: params,
- key: key,
- };
-
- // figure out where it goes in the list
- const idx = binarySearch(callbackList, function (el) {
- return el.runAt - runAt;
- });
-
- callbackList.splice(idx, 0, data);
- scheduleRealCallback();
-
- return key;
-}
-
-/**
- * reimplementation of window.clearTimeout, which mirrors setTimeout
- *
- * @param key - result from an earlier setTimeout call
- */
-export function clearTimeout(key: number): void {
- if (callbackList.length === 0) {
- return;
- }
-
- // remove the element from the list
- let i: number;
- for (i = 0; i < callbackList.length; i++) {
- const cb = callbackList[i];
- if (cb.key == key) {
- callbackList.splice(i, 1);
- break;
- }
- }
-
- // iff it was the first one in the list, reschedule our callback.
- if (i === 0) {
- scheduleRealCallback();
- }
-}
-
-// use the real global.setTimeout to schedule a callback to runCallbacks.
-function scheduleRealCallback(): void {
- if (realCallbackKey) {
- global.clearTimeout(realCallbackKey as NodeJS.Timeout);
- }
-
- const first = callbackList[0];
-
- if (!first) {
- debuglog("scheduleRealCallback: no more callbacks, not rescheduling");
- return;
- }
-
- const timestamp = Date.now();
- const delayMs = Math.min(first.runAt - timestamp, TIMER_CHECK_PERIOD_MS);
-
- debuglog("scheduleRealCallback: now:", timestamp, "delay:", delayMs);
- realCallbackKey = global.setTimeout(runCallbacks, delayMs);
-}
-
-function runCallbacks(): void {
- const timestamp = Date.now();
- debuglog("runCallbacks: now:", timestamp);
-
- // get the list of things to call
- const callbacksToRun: Callback[] = [];
- // eslint-disable-next-line
- while (true) {
- const first = callbackList[0];
- if (!first || first.runAt > timestamp) {
- break;
- }
- const cb = callbackList.shift()!;
- debuglog("runCallbacks: popping", cb.key);
- callbacksToRun.push(cb);
- }
-
- // reschedule the real callback before running our functions, to
- // keep the codepaths the same whether or not our functions
- // register their own setTimeouts.
- scheduleRealCallback();
-
- for (const cb of callbacksToRun) {
- try {
- cb.func.apply(global, cb.params);
- } catch (e) {
- logger.error("Uncaught exception in callback function", e);
- }
- }
-}
-
-/* search in a sorted array.
- *
- * returns the index of the last element for which func returns
- * greater than zero, or array.length if no such element exists.
- */
-function binarySearch<T>(array: T[], func: (v: T) => number): number {
- // min is inclusive, max exclusive.
- let min = 0;
- let max = array.length;
-
- while (min < max) {
- const mid = (min + max) >> 1;
- const res = func(array[mid]);
- if (res > 0) {
- // the element at 'mid' is too big; set it as the new max.
- max = mid;
- } else {
- // the element at 'mid' is too small. 'min' is inclusive, so +1.
- min = mid + 1;
- }
- }
- // presumably, min==max now.
- return min;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/MSC3906Rendezvous.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/MSC3906Rendezvous.ts
deleted file mode 100644
index f431c83..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/MSC3906Rendezvous.ts
+++ /dev/null
@@ -1,264 +0,0 @@
-/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { UnstableValue } from "matrix-events-sdk";
-
-import { RendezvousChannel, RendezvousFailureListener, RendezvousFailureReason, RendezvousIntent } from ".";
-import { MatrixClient } from "../client";
-import { CrossSigningInfo } from "../crypto/CrossSigning";
-import { DeviceInfo } from "../crypto/deviceinfo";
-import { buildFeatureSupportMap, Feature, ServerSupport } from "../feature";
-import { logger } from "../logger";
-import { sleep } from "../utils";
-
-enum PayloadType {
- Start = "m.login.start",
- Finish = "m.login.finish",
- Progress = "m.login.progress",
-}
-
-enum Outcome {
- Success = "success",
- Failure = "failure",
- Verified = "verified",
- Declined = "declined",
- Unsupported = "unsupported",
-}
-
-export interface MSC3906RendezvousPayload {
- type: PayloadType;
- intent?: RendezvousIntent;
- outcome?: Outcome;
- device_id?: string;
- device_key?: string;
- verifying_device_id?: string;
- verifying_device_key?: string;
- master_key?: string;
- protocols?: string[];
- protocol?: string;
- login_token?: string;
- homeserver?: string;
-}
-
-const LOGIN_TOKEN_PROTOCOL = new UnstableValue("login_token", "org.matrix.msc3906.login_token");
-
-/**
- * Implements MSC3906 to allow a user to sign in on a new device using QR code.
- * This implementation only supports generating a QR code on a device that is already signed in.
- * Note that this is UNSTABLE and may have breaking changes without notice.
- */
-export class MSC3906Rendezvous {
- private newDeviceId?: string;
- private newDeviceKey?: string;
- private ourIntent: RendezvousIntent = RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE;
- private _code?: string;
-
- /**
- * @param channel - The secure channel used for communication
- * @param client - The Matrix client in used on the device already logged in
- * @param onFailure - Callback for when the rendezvous fails
- */
- public constructor(
- private channel: RendezvousChannel<MSC3906RendezvousPayload>,
- private client: MatrixClient,
- public onFailure?: RendezvousFailureListener,
- ) {}
-
- /**
- * Returns the code representing the rendezvous suitable for rendering in a QR code or undefined if not generated yet.
- */
- public get code(): string | undefined {
- return this._code;
- }
-
- /**
- * Generate the code including doing partial set up of the channel where required.
- */
- public async generateCode(): Promise<void> {
- if (this._code) {
- return;
- }
-
- this._code = JSON.stringify(await this.channel.generateCode(this.ourIntent));
- }
-
- public async startAfterShowingCode(): Promise<string | undefined> {
- const checksum = await this.channel.connect();
-
- logger.info(`Connected to secure channel with checksum: ${checksum} our intent is ${this.ourIntent}`);
-
- const features = await buildFeatureSupportMap(await this.client.getVersions());
- // determine available protocols
- if (features.get(Feature.LoginTokenRequest) === ServerSupport.Unsupported) {
- logger.info("Server doesn't support MSC3882");
- await this.send({ type: PayloadType.Finish, outcome: Outcome.Unsupported });
- await this.cancel(RendezvousFailureReason.HomeserverLacksSupport);
- return undefined;
- }
-
- await this.send({ type: PayloadType.Progress, protocols: [LOGIN_TOKEN_PROTOCOL.name] });
-
- logger.info("Waiting for other device to chose protocol");
- const { type, protocol, outcome } = await this.receive();
-
- if (type === PayloadType.Finish) {
- // new device decided not to complete
- switch (outcome ?? "") {
- case "unsupported":
- await this.cancel(RendezvousFailureReason.UnsupportedAlgorithm);
- break;
- default:
- await this.cancel(RendezvousFailureReason.Unknown);
- }
- return undefined;
- }
-
- if (type !== PayloadType.Progress) {
- await this.cancel(RendezvousFailureReason.Unknown);
- return undefined;
- }
-
- if (!protocol || !LOGIN_TOKEN_PROTOCOL.matches(protocol)) {
- await this.cancel(RendezvousFailureReason.UnsupportedAlgorithm);
- return undefined;
- }
-
- return checksum;
- }
-
- private async receive(): Promise<MSC3906RendezvousPayload> {
- return (await this.channel.receive()) as MSC3906RendezvousPayload;
- }
-
- private async send(payload: MSC3906RendezvousPayload): Promise<void> {
- await this.channel.send(payload);
- }
-
- public async declineLoginOnExistingDevice(): Promise<void> {
- logger.info("User declined sign in");
- await this.send({ type: PayloadType.Finish, outcome: Outcome.Declined });
- }
-
- public async approveLoginOnExistingDevice(loginToken: string): Promise<string | undefined> {
- // eslint-disable-next-line camelcase
- await this.send({ type: PayloadType.Progress, login_token: loginToken, homeserver: this.client.baseUrl });
-
- logger.info("Waiting for outcome");
- const res = await this.receive();
- if (!res) {
- return undefined;
- }
- const { outcome, device_id: deviceId, device_key: deviceKey } = res;
-
- if (outcome !== "success") {
- throw new Error("Linking failed");
- }
-
- this.newDeviceId = deviceId;
- this.newDeviceKey = deviceKey;
-
- return deviceId;
- }
-
- private async verifyAndCrossSignDevice(deviceInfo: DeviceInfo): Promise<CrossSigningInfo | DeviceInfo> {
- if (!this.client.crypto) {
- throw new Error("Crypto not available on client");
- }
-
- if (!this.newDeviceId) {
- throw new Error("No new device ID set");
- }
-
- // check that keys received from the server for the new device match those received from the device itself
- if (deviceInfo.getFingerprint() !== this.newDeviceKey) {
- throw new Error(
- `New device has different keys than expected: ${this.newDeviceKey} vs ${deviceInfo.getFingerprint()}`,
- );
- }
-
- const userId = this.client.getUserId();
-
- if (!userId) {
- throw new Error("No user ID set");
- }
- // mark the device as verified locally + cross sign
- logger.info(`Marking device ${this.newDeviceId} as verified`);
- const info = await this.client.crypto.setDeviceVerification(userId, this.newDeviceId, true, false, true);
-
- const masterPublicKey = this.client.crypto.crossSigningInfo.getId("master")!;
-
- await this.send({
- type: PayloadType.Finish,
- outcome: Outcome.Verified,
- verifying_device_id: this.client.getDeviceId()!,
- verifying_device_key: this.client.getDeviceEd25519Key()!,
- master_key: masterPublicKey,
- });
-
- return info;
- }
-
- /**
- * Verify the device and cross-sign it.
- * @param timeout - time in milliseconds to wait for device to come online
- * @returns the new device info if the device was verified
- */
- public async verifyNewDeviceOnExistingDevice(
- timeout = 10 * 1000,
- ): Promise<DeviceInfo | CrossSigningInfo | undefined> {
- if (!this.newDeviceId) {
- throw new Error("No new device to sign");
- }
-
- if (!this.newDeviceKey) {
- logger.info("No new device key to sign");
- return undefined;
- }
-
- if (!this.client.crypto) {
- throw new Error("Crypto not available on client");
- }
-
- const userId = this.client.getUserId();
-
- if (!userId) {
- throw new Error("No user ID set");
- }
-
- let deviceInfo = this.client.crypto.getStoredDevice(userId, this.newDeviceId);
-
- if (!deviceInfo) {
- logger.info("Going to wait for new device to be online");
- await sleep(timeout);
- deviceInfo = this.client.crypto.getStoredDevice(userId, this.newDeviceId);
- }
-
- if (deviceInfo) {
- return await this.verifyAndCrossSignDevice(deviceInfo);
- }
-
- throw new Error("Device not online within timeout");
- }
-
- public async cancel(reason: RendezvousFailureReason): Promise<void> {
- this.onFailure?.(reason);
- await this.channel.cancel(reason);
- }
-
- public async close(): Promise<void> {
- await this.channel.close();
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousChannel.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousChannel.ts
deleted file mode 100644
index 549ebc8..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousChannel.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
-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 { RendezvousCode, RendezvousIntent, RendezvousFailureReason } from ".";
-
-export interface RendezvousChannel<T> {
- /**
- * @returns the checksum/confirmation digits to be shown to the user
- */
- connect(): Promise<string>;
-
- /**
- * Send a payload via the channel.
- * @param data - payload to send
- */
- send(data: T): Promise<void>;
-
- /**
- * Receive a payload from the channel.
- * @returns the received payload
- */
- receive(): Promise<Partial<T> | undefined>;
-
- /**
- * Close the channel and clear up any resources.
- */
- close(): Promise<void>;
-
- /**
- * @returns a representation of the channel that can be encoded in a QR or similar
- */
- generateCode(intent: RendezvousIntent): Promise<RendezvousCode>;
-
- cancel(reason: RendezvousFailureReason): Promise<void>;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousCode.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousCode.ts
deleted file mode 100644
index 86608aa..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousCode.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
-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 { RendezvousTransportDetails, RendezvousIntent } from ".";
-
-export interface RendezvousCode {
- intent: RendezvousIntent;
- rendezvous?: {
- transport: RendezvousTransportDetails;
- algorithm: string;
- };
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousError.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousError.ts
deleted file mode 100644
index 8b76fc1..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousError.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
-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 { RendezvousFailureReason } from ".";
-
-export class RendezvousError extends Error {
- public constructor(message: string, public readonly code: RendezvousFailureReason) {
- super(message);
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousFailureReason.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousFailureReason.ts
deleted file mode 100644
index b19a91c..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousFailureReason.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
-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.
-*/
-
-export type RendezvousFailureListener = (reason: RendezvousFailureReason) => void;
-
-export enum RendezvousFailureReason {
- UserDeclined = "user_declined",
- OtherDeviceNotSignedIn = "other_device_not_signed_in",
- OtherDeviceAlreadySignedIn = "other_device_already_signed_in",
- Unknown = "unknown",
- Expired = "expired",
- UserCancelled = "user_cancelled",
- InvalidCode = "invalid_code",
- UnsupportedAlgorithm = "unsupported_algorithm",
- DataMismatch = "data_mismatch",
- UnsupportedTransport = "unsupported_transport",
- HomeserverLacksSupport = "homeserver_lacks_support",
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousIntent.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousIntent.ts
deleted file mode 100644
index db53ef9..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousIntent.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
-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.
-*/
-
-export enum RendezvousIntent {
- LOGIN_ON_NEW_DEVICE = "login.start",
- RECIPROCATE_LOGIN_ON_EXISTING_DEVICE = "login.reciprocate",
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousTransport.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousTransport.ts
deleted file mode 100644
index 08905be..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/RendezvousTransport.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
-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 { RendezvousFailureListener, RendezvousFailureReason } from ".";
-
-export interface RendezvousTransportDetails {
- type: string;
-}
-
-/**
- * Interface representing a generic rendezvous transport.
- */
-export interface RendezvousTransport<T> {
- /**
- * Ready state of the transport. This is set to true when the transport is ready to be used.
- */
- readonly ready: boolean;
-
- /**
- * Listener for cancellation events. This is called when the rendezvous is cancelled or fails.
- */
- onFailure?: RendezvousFailureListener;
-
- /**
- * @returns the transport details that can be encoded in a QR or similar
- */
- details(): Promise<RendezvousTransportDetails>;
-
- /**
- * Send data via the transport.
- * @param data - the data itself
- */
- send(data: T): Promise<void>;
-
- /**
- * Receive data from the transport.
- */
- receive(): Promise<Partial<T> | undefined>;
-
- /**
- * Cancel the rendezvous. This will call `onCancelled()` if it is set.
- * @param reason - the reason for the cancellation/failure
- */
- cancel(reason: RendezvousFailureReason): Promise<void>;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts
deleted file mode 100644
index be60ee5..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.ts
+++ /dev/null
@@ -1,259 +0,0 @@
-/*
-Copyright 2023 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 { SAS } from "@matrix-org/olm";
-
-import {
- RendezvousError,
- RendezvousCode,
- RendezvousIntent,
- RendezvousChannel,
- RendezvousTransportDetails,
- RendezvousTransport,
- RendezvousFailureReason,
-} from "..";
-import { encodeUnpaddedBase64, decodeBase64 } from "../../crypto/olmlib";
-import { crypto, subtleCrypto, TextEncoder } from "../../crypto/crypto";
-import { generateDecimalSas } from "../../crypto/verification/SASDecimal";
-import { UnstableValue } from "../../NamespacedValue";
-
-const ECDH_V2 = new UnstableValue(
- "m.rendezvous.v2.curve25519-aes-sha256",
- "org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256",
-);
-
-export interface ECDHv2RendezvousCode extends RendezvousCode {
- rendezvous: {
- transport: RendezvousTransportDetails;
- algorithm: typeof ECDH_V2.name | typeof ECDH_V2.altName;
- key: string;
- };
-}
-
-export type MSC3903ECDHPayload = PlainTextPayload | EncryptedPayload;
-
-export interface PlainTextPayload {
- algorithm: typeof ECDH_V2.name | typeof ECDH_V2.altName;
- key?: string;
-}
-
-export interface EncryptedPayload {
- iv: string;
- ciphertext: string;
-}
-
-async function importKey(key: Uint8Array): Promise<CryptoKey> {
- if (!subtleCrypto) {
- throw new Error("Web Crypto is not available");
- }
-
- const imported = subtleCrypto.importKey("raw", key, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
-
- return imported;
-}
-
-/**
- * Implementation of the unstable [MSC3903](https://github.com/matrix-org/matrix-spec-proposals/pull/3903)
- * X25519/ECDH key agreement based secure rendezvous channel.
- * Note that this is UNSTABLE and may have breaking changes without notice.
- */
-export class MSC3903ECDHv2RendezvousChannel<T> implements RendezvousChannel<T> {
- private olmSAS?: SAS;
- private ourPublicKey: Uint8Array;
- private aesKey?: CryptoKey;
- private connected = false;
-
- public constructor(
- private transport: RendezvousTransport<MSC3903ECDHPayload>,
- private theirPublicKey?: Uint8Array,
- public onFailure?: (reason: RendezvousFailureReason) => void,
- ) {
- this.olmSAS = new global.Olm.SAS();
- this.ourPublicKey = decodeBase64(this.olmSAS.get_pubkey());
- }
-
- public async generateCode(intent: RendezvousIntent): Promise<ECDHv2RendezvousCode> {
- if (this.transport.ready) {
- throw new Error("Code already generated");
- }
-
- await this.transport.send({ algorithm: ECDH_V2.name });
-
- const rendezvous: ECDHv2RendezvousCode = {
- rendezvous: {
- algorithm: ECDH_V2.name,
- key: encodeUnpaddedBase64(this.ourPublicKey),
- transport: await this.transport.details(),
- },
- intent,
- };
-
- return rendezvous;
- }
-
- public async connect(): Promise<string> {
- if (this.connected) {
- throw new Error("Channel already connected");
- }
-
- if (!this.olmSAS) {
- throw new Error("Channel closed");
- }
-
- const isInitiator = !this.theirPublicKey;
-
- if (isInitiator) {
- // wait for the other side to send us their public key
- const rawRes = await this.transport.receive();
- if (!rawRes) {
- throw new Error("No response from other device");
- }
- const res = rawRes as Partial<PlainTextPayload>;
- const { key, algorithm } = res;
- if (!algorithm || !ECDH_V2.matches(algorithm) || !key) {
- throw new RendezvousError(
- "Unsupported algorithm: " + algorithm,
- RendezvousFailureReason.UnsupportedAlgorithm,
- );
- }
-
- this.theirPublicKey = decodeBase64(key);
- } else {
- // send our public key unencrypted
- await this.transport.send({
- algorithm: ECDH_V2.name,
- key: encodeUnpaddedBase64(this.ourPublicKey),
- });
- }
-
- this.connected = true;
-
- this.olmSAS.set_their_key(encodeUnpaddedBase64(this.theirPublicKey!));
-
- const initiatorKey = isInitiator ? this.ourPublicKey : this.theirPublicKey!;
- const recipientKey = isInitiator ? this.theirPublicKey! : this.ourPublicKey;
- let aesInfo = ECDH_V2.name;
- aesInfo += `|${encodeUnpaddedBase64(initiatorKey)}`;
- aesInfo += `|${encodeUnpaddedBase64(recipientKey)}`;
-
- const aesKeyBytes = this.olmSAS.generate_bytes(aesInfo, 32);
-
- this.aesKey = await importKey(aesKeyBytes);
-
- // blank the bytes out to make sure not kept in memory
- aesKeyBytes.fill(0);
-
- const rawChecksum = this.olmSAS.generate_bytes(aesInfo, 5);
- return generateDecimalSas(Array.from(rawChecksum)).join("-");
- }
-
- private async encrypt(data: T): Promise<MSC3903ECDHPayload> {
- if (!subtleCrypto) {
- throw new Error("Web Crypto is not available");
- }
-
- const iv = new Uint8Array(32);
- crypto.getRandomValues(iv);
-
- const encodedData = new TextEncoder().encode(JSON.stringify(data));
-
- const ciphertext = await subtleCrypto.encrypt(
- {
- name: "AES-GCM",
- iv,
- tagLength: 128,
- },
- this.aesKey as CryptoKey,
- encodedData,
- );
-
- return {
- iv: encodeUnpaddedBase64(iv),
- ciphertext: encodeUnpaddedBase64(ciphertext),
- };
- }
-
- public async send(payload: T): Promise<void> {
- if (!this.olmSAS) {
- throw new Error("Channel closed");
- }
-
- if (!this.aesKey) {
- throw new Error("Shared secret not set up");
- }
-
- return this.transport.send(await this.encrypt(payload));
- }
-
- private async decrypt({ iv, ciphertext }: EncryptedPayload): Promise<Partial<T>> {
- if (!ciphertext || !iv) {
- throw new Error("Missing ciphertext and/or iv");
- }
-
- const ciphertextBytes = decodeBase64(ciphertext);
-
- if (!subtleCrypto) {
- throw new Error("Web Crypto is not available");
- }
-
- const plaintext = await subtleCrypto.decrypt(
- {
- name: "AES-GCM",
- iv: decodeBase64(iv),
- tagLength: 128,
- },
- this.aesKey as CryptoKey,
- ciphertextBytes,
- );
-
- return JSON.parse(new TextDecoder().decode(new Uint8Array(plaintext)));
- }
-
- public async receive(): Promise<Partial<T> | undefined> {
- if (!this.olmSAS) {
- throw new Error("Channel closed");
- }
- if (!this.aesKey) {
- throw new Error("Shared secret not set up");
- }
-
- const rawData = await this.transport.receive();
- if (!rawData) {
- return undefined;
- }
- const data = rawData as Partial<EncryptedPayload>;
- if (data.ciphertext && data.iv) {
- return this.decrypt(data as EncryptedPayload);
- }
-
- throw new Error("Data received but no ciphertext");
- }
-
- public async close(): Promise<void> {
- if (this.olmSAS) {
- this.olmSAS.free();
- this.olmSAS = undefined;
- }
- }
-
- public async cancel(reason: RendezvousFailureReason): Promise<void> {
- try {
- await this.transport.cancel(reason);
- } finally {
- await this.close();
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/channels/index.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/channels/index.ts
deleted file mode 100644
index f157bbe..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/channels/index.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
-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.
-*/
-
-export * from "./MSC3903ECDHv2RendezvousChannel";
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/index.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/index.ts
deleted file mode 100644
index 379b133..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/index.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
-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.
-*/
-
-export * from "./MSC3906Rendezvous";
-export * from "./RendezvousChannel";
-export * from "./RendezvousCode";
-export * from "./RendezvousError";
-export * from "./RendezvousFailureReason";
-export * from "./RendezvousIntent";
-export * from "./RendezvousTransport";
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts
deleted file mode 100644
index 430ee92..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.ts
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { UnstableValue } from "matrix-events-sdk";
-
-import { logger } from "../../logger";
-import { sleep } from "../../utils";
-import {
- RendezvousFailureListener,
- RendezvousFailureReason,
- RendezvousTransport,
- RendezvousTransportDetails,
-} from "..";
-import { MatrixClient } from "../../matrix";
-import { ClientPrefix } from "../../http-api";
-
-const TYPE = new UnstableValue("http.v1", "org.matrix.msc3886.http.v1");
-
-export interface MSC3886SimpleHttpRendezvousTransportDetails extends RendezvousTransportDetails {
- uri: string;
-}
-
-/**
- * Implementation of the unstable [MSC3886](https://github.com/matrix-org/matrix-spec-proposals/pull/3886)
- * simple HTTP rendezvous protocol.
- * Note that this is UNSTABLE and may have breaking changes without notice.
- */
-export class MSC3886SimpleHttpRendezvousTransport<T extends {}> implements RendezvousTransport<T> {
- private uri?: string;
- private etag?: string;
- private expiresAt?: Date;
- private client: MatrixClient;
- private fallbackRzServer?: string;
- private fetchFn?: typeof global.fetch;
- private cancelled = false;
- private _ready = false;
- public onFailure?: RendezvousFailureListener;
-
- public constructor({
- onFailure,
- client,
- fallbackRzServer,
- fetchFn,
- }: {
- fetchFn?: typeof global.fetch;
- onFailure?: RendezvousFailureListener;
- client: MatrixClient;
- fallbackRzServer?: string;
- }) {
- this.fetchFn = fetchFn;
- this.onFailure = onFailure;
- this.client = client;
- this.fallbackRzServer = fallbackRzServer;
- }
-
- public get ready(): boolean {
- return this._ready;
- }
-
- public async details(): Promise<MSC3886SimpleHttpRendezvousTransportDetails> {
- if (!this.uri) {
- throw new Error("Rendezvous not set up");
- }
-
- return {
- type: TYPE.name,
- uri: this.uri,
- };
- }
-
- private fetch(resource: URL | string, options?: RequestInit): ReturnType<typeof global.fetch> {
- if (this.fetchFn) {
- return this.fetchFn(resource, options);
- }
- return global.fetch(resource, options);
- }
-
- private async getPostEndpoint(): Promise<string | undefined> {
- try {
- if (await this.client.doesServerSupportUnstableFeature("org.matrix.msc3886")) {
- return `${this.client.baseUrl}${ClientPrefix.Unstable}/org.matrix.msc3886/rendezvous`;
- }
- } catch (err) {
- logger.warn("Failed to get unstable features", err);
- }
-
- return this.fallbackRzServer;
- }
-
- public async send(data: T): Promise<void> {
- if (this.cancelled) {
- return;
- }
- const method = this.uri ? "PUT" : "POST";
- const uri = this.uri ?? (await this.getPostEndpoint());
-
- if (!uri) {
- throw new Error("Invalid rendezvous URI");
- }
-
- const headers: Record<string, string> = { "content-type": "application/json" };
- if (this.etag) {
- headers["if-match"] = this.etag;
- }
-
- const res = await this.fetch(uri, { method, headers, body: JSON.stringify(data) });
- if (res.status === 404) {
- return this.cancel(RendezvousFailureReason.Unknown);
- }
- this.etag = res.headers.get("etag") ?? undefined;
-
- if (method === "POST") {
- const location = res.headers.get("location");
- if (!location) {
- throw new Error("No rendezvous URI given");
- }
- const expires = res.headers.get("expires");
- if (expires) {
- this.expiresAt = new Date(expires);
- }
- // we would usually expect the final `url` to be set by a proper fetch implementation.
- // however, if a polyfill based on XHR is used it won't be set, we we use existing URI as fallback
- const baseUrl = res.url ?? uri;
- // resolve location header which could be relative or absolute
- this.uri = new URL(location, `${baseUrl}${baseUrl.endsWith("/") ? "" : "/"}`).href;
- this._ready = true;
- }
- }
-
- public async receive(): Promise<Partial<T> | undefined> {
- if (!this.uri) {
- throw new Error("Rendezvous not set up");
- }
- // eslint-disable-next-line no-constant-condition
- while (true) {
- if (this.cancelled) {
- return undefined;
- }
-
- const headers: Record<string, string> = {};
- if (this.etag) {
- headers["if-none-match"] = this.etag;
- }
- const poll = await this.fetch(this.uri, { method: "GET", headers });
-
- if (poll.status === 404) {
- this.cancel(RendezvousFailureReason.Unknown);
- return undefined;
- }
-
- // rely on server expiring the channel rather than checking ourselves
-
- if (poll.headers.get("content-type") !== "application/json") {
- this.etag = poll.headers.get("etag") ?? undefined;
- } else if (poll.status === 200) {
- this.etag = poll.headers.get("etag") ?? undefined;
- return poll.json();
- }
- await sleep(1000);
- }
- }
-
- public async cancel(reason: RendezvousFailureReason): Promise<void> {
- if (reason === RendezvousFailureReason.Unknown && this.expiresAt && this.expiresAt.getTime() < Date.now()) {
- reason = RendezvousFailureReason.Expired;
- }
-
- this.cancelled = true;
- this._ready = false;
- this.onFailure?.(reason);
-
- if (this.uri && reason === RendezvousFailureReason.UserDeclined) {
- try {
- await this.fetch(this.uri, { method: "DELETE" });
- } catch (e) {
- logger.warn(e);
- }
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/transports/index.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/transports/index.ts
deleted file mode 100644
index 6d8d642..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/rendezvous/transports/index.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
-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.
-*/
-
-export * from "./MSC3886SimpleHttpRendezvousTransport";
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/room-hierarchy.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/room-hierarchy.ts
deleted file mode 100644
index 5c0b61d..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/room-hierarchy.ts
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
-Copyright 2021 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 { Room } from "./models/room";
-import { IHierarchyRoom, IHierarchyRelation } from "./@types/spaces";
-import { MatrixClient } from "./client";
-import { EventType } from "./@types/event";
-import { MatrixError } from "./http-api";
-
-export class RoomHierarchy {
- // Map from room id to list of servers which are listed as a via somewhere in the loaded hierarchy
- public readonly viaMap = new Map<string, Set<string>>();
- // Map from room id to list of rooms which claim this room as their child
- public readonly backRefs = new Map<string, string[]>();
- // Map from room id to object
- public readonly roomMap = new Map<string, IHierarchyRoom>();
- private loadRequest?: ReturnType<MatrixClient["getRoomHierarchy"]>;
- private nextBatch?: string;
- private _rooms?: IHierarchyRoom[];
- private serverSupportError?: Error;
-
- /**
- * Construct a new RoomHierarchy
- *
- * A RoomHierarchy instance allows you to easily make use of the /hierarchy API and paginate it.
- *
- * @param root - the root of this hierarchy
- * @param pageSize - the maximum number of rooms to return per page, can be overridden per load request.
- * @param maxDepth - the maximum depth to traverse the hierarchy to
- * @param suggestedOnly - whether to only return rooms with suggested=true.
- */
- public constructor(
- public readonly root: Room,
- private readonly pageSize?: number,
- private readonly maxDepth?: number,
- private readonly suggestedOnly = false,
- ) {}
-
- public get noSupport(): boolean {
- return !!this.serverSupportError;
- }
-
- public get canLoadMore(): boolean {
- return !!this.serverSupportError || !!this.nextBatch || !this._rooms;
- }
-
- public get loading(): boolean {
- return !!this.loadRequest;
- }
-
- public get rooms(): IHierarchyRoom[] | undefined {
- return this._rooms;
- }
-
- public async load(pageSize = this.pageSize): Promise<IHierarchyRoom[]> {
- if (this.loadRequest) return this.loadRequest.then((r) => r.rooms);
-
- this.loadRequest = this.root.client.getRoomHierarchy(
- this.root.roomId,
- pageSize,
- this.maxDepth,
- this.suggestedOnly,
- this.nextBatch,
- );
-
- let rooms: IHierarchyRoom[];
- try {
- ({ rooms, next_batch: this.nextBatch } = await this.loadRequest);
- } catch (e) {
- if ((<MatrixError>e).errcode === "M_UNRECOGNIZED") {
- this.serverSupportError = <MatrixError>e;
- } else {
- throw e;
- }
-
- return [];
- } finally {
- this.loadRequest = undefined;
- }
-
- if (this._rooms) {
- this._rooms = this._rooms.concat(rooms);
- } else {
- this._rooms = rooms;
- }
-
- rooms.forEach((room) => {
- this.roomMap.set(room.room_id, room);
-
- room.children_state.forEach((ev) => {
- if (ev.type !== EventType.SpaceChild) return;
- const childRoomId = ev.state_key;
-
- // track backrefs for quicker hierarchy navigation
- if (!this.backRefs.has(childRoomId)) {
- this.backRefs.set(childRoomId, []);
- }
- this.backRefs.get(childRoomId)!.push(room.room_id);
-
- // fill viaMap
- if (Array.isArray(ev.content.via)) {
- if (!this.viaMap.has(childRoomId)) {
- this.viaMap.set(childRoomId, new Set());
- }
- const vias = this.viaMap.get(childRoomId)!;
- ev.content.via.forEach((via) => vias.add(via));
- }
- });
- });
-
- return rooms;
- }
-
- public getRelation(parentId: string, childId: string): IHierarchyRelation | undefined {
- return this.roomMap.get(parentId)?.children_state.find((e) => e.state_key === childId);
- }
-
- public isSuggested(parentId: string, childId: string): boolean | undefined {
- return this.getRelation(parentId, childId)?.content.suggested;
- }
-
- // locally remove a relation as a form of local echo
- public removeRelation(parentId: string, childId: string): void {
- const backRefs = this.backRefs.get(childId);
- if (backRefs?.length === 1) {
- this.backRefs.delete(childId);
- } else if (backRefs?.length) {
- this.backRefs.set(
- childId,
- backRefs.filter((ref) => ref !== parentId),
- );
- }
-
- const room = this.roomMap.get(parentId);
- if (room) {
- room.children_state = room.children_state.filter((ev) => ev.state_key !== childId);
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/KeyClaimManager.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/KeyClaimManager.ts
deleted file mode 100644
index 9df8f89..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/KeyClaimManager.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
-Copyright 2023 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 { OlmMachine, UserId } from "@matrix-org/matrix-sdk-crypto-js";
-
-import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
-
-/**
- * KeyClaimManager: linearises calls to OlmMachine.getMissingSessions to avoid races
- *
- * We have one of these per `RustCrypto` (and hence per `MatrixClient`).
- */
-export class KeyClaimManager {
- private currentClaimPromise: Promise<void>;
- private stopped = false;
-
- public constructor(
- private readonly olmMachine: OlmMachine,
- private readonly outgoingRequestProcessor: OutgoingRequestProcessor,
- ) {
- this.currentClaimPromise = Promise.resolve();
- }
-
- /**
- * Tell the KeyClaimManager to immediately stop processing requests.
- *
- * Any further calls, and any still in the queue, will fail with an error.
- */
- public stop(): void {
- this.stopped = true;
- }
-
- /**
- * Given a list of users, attempt to ensure that we have Olm Sessions active with each of their devices
- *
- * If we don't have an active olm session, we will claim a one-time key and start one.
- *
- * @param userList - list of userIDs to claim
- */
- public ensureSessionsForUsers(userList: Array<UserId>): Promise<void> {
- // The Rust-SDK requires that we only have one getMissingSessions process in flight at once. This little dance
- // ensures that, by only having one call to ensureSessionsForUsersInner active at once (and making them
- // queue up in order).
- const prom = this.currentClaimPromise
- .catch(() => {
- // any errors in the previous claim will have been reported already, so there is nothing to do here.
- // we just throw away the error and start anew.
- })
- .then(() => this.ensureSessionsForUsersInner(userList));
- this.currentClaimPromise = prom;
- return prom;
- }
-
- private async ensureSessionsForUsersInner(userList: Array<UserId>): Promise<void> {
- // bail out quickly if we've been stopped.
- if (this.stopped) {
- throw new Error(`Cannot ensure Olm sessions: shutting down`);
- }
- const claimRequest = await this.olmMachine.getMissingSessions(userList);
- if (claimRequest) {
- await this.outgoingRequestProcessor.makeOutgoingRequest(claimRequest);
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/OutgoingRequestProcessor.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/OutgoingRequestProcessor.ts
deleted file mode 100644
index 7ac9a21..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/OutgoingRequestProcessor.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
-Copyright 2023 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 {
- OlmMachine,
- KeysBackupRequest,
- KeysClaimRequest,
- KeysQueryRequest,
- KeysUploadRequest,
- RoomMessageRequest,
- SignatureUploadRequest,
- ToDeviceRequest,
-} from "@matrix-org/matrix-sdk-crypto-js";
-
-import { logger } from "../logger";
-import { IHttpOpts, MatrixHttpApi, Method } from "../http-api";
-import { QueryDict } from "../utils";
-
-/**
- * Common interface for all the request types returned by `OlmMachine.outgoingRequests`.
- */
-export interface OutgoingRequest {
- readonly id: string | undefined;
- readonly type: number;
-}
-
-/**
- * OutgoingRequestManager: turns `OutgoingRequest`s from the rust sdk into HTTP requests
- *
- * We have one of these per `RustCrypto` (and hence per `MatrixClient`), not that it does anything terribly complicated.
- * It's responsible for:
- *
- * * holding the reference to the `MatrixHttpApi`
- * * turning `OutgoingRequest`s from the rust backend into HTTP requests, and sending them
- * * sending the results of such requests back to the rust backend.
- */
-export class OutgoingRequestProcessor {
- public constructor(
- private readonly olmMachine: OlmMachine,
- private readonly http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
- ) {}
-
- public async makeOutgoingRequest(msg: OutgoingRequest): Promise<void> {
- let resp: string;
-
- /* refer https://docs.rs/matrix-sdk-crypto/0.6.0/matrix_sdk_crypto/requests/enum.OutgoingRequests.html
- * for the complete list of request types
- */
- if (msg instanceof KeysUploadRequest) {
- resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/upload", {}, msg.body);
- } else if (msg instanceof KeysQueryRequest) {
- resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/query", {}, msg.body);
- } else if (msg instanceof KeysClaimRequest) {
- resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/claim", {}, msg.body);
- } else if (msg instanceof SignatureUploadRequest) {
- resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/signatures/upload", {}, msg.body);
- } else if (msg instanceof KeysBackupRequest) {
- resp = await this.rawJsonRequest(Method.Put, "/_matrix/client/v3/room_keys/keys", {}, msg.body);
- } else if (msg instanceof ToDeviceRequest) {
- const path =
- `/_matrix/client/v3/sendToDevice/${encodeURIComponent(msg.event_type)}/` +
- encodeURIComponent(msg.txn_id);
- resp = await this.rawJsonRequest(Method.Put, path, {}, msg.body);
- } else if (msg instanceof RoomMessageRequest) {
- const path =
- `/_matrix/client/v3/room/${encodeURIComponent(msg.room_id)}/send/` +
- `${encodeURIComponent(msg.event_type)}/${encodeURIComponent(msg.txn_id)}`;
- resp = await this.rawJsonRequest(Method.Put, path, {}, msg.body);
- } else {
- logger.warn("Unsupported outgoing message", Object.getPrototypeOf(msg));
- resp = "";
- }
-
- if (msg.id) {
- await this.olmMachine.markRequestAsSent(msg.id, msg.type, resp);
- }
- }
-
- private async rawJsonRequest(method: Method, path: string, queryParams: QueryDict, body: string): Promise<string> {
- const opts = {
- // inhibit the JSON stringification and parsing within HttpApi.
- json: false,
-
- // nevertheless, we are sending, and accept, JSON.
- headers: {
- "Content-Type": "application/json",
- "Accept": "application/json",
- },
-
- // we use the full prefix
- prefix: "",
- };
-
- try {
- const response = await this.http.authedRequest<string>(method, path, queryParams, body, opts);
- logger.info(`rust-crypto: successfully made HTTP request: ${method} ${path}`);
- return response;
- } catch (e) {
- logger.warn(`rust-crypto: error making HTTP request: ${method} ${path}: ${e}`);
- throw e;
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/RoomEncryptor.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/RoomEncryptor.ts
deleted file mode 100644
index 1649a69..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/RoomEncryptor.ts
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
-Copyright 2023 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 { EncryptionSettings, OlmMachine, RoomId, UserId } from "@matrix-org/matrix-sdk-crypto-js";
-
-import { EventType } from "../@types/event";
-import { IContent, MatrixEvent } from "../models/event";
-import { Room } from "../models/room";
-import { logger, PrefixedLogger } from "../logger";
-import { KeyClaimManager } from "./KeyClaimManager";
-import { RoomMember } from "../models/room-member";
-import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
-
-/**
- * RoomEncryptor: responsible for encrypting messages to a given room
- */
-export class RoomEncryptor {
- private readonly prefixedLogger: PrefixedLogger;
-
- /**
- * @param olmMachine - The rust-sdk's OlmMachine
- * @param keyClaimManager - Our KeyClaimManager, which manages the queue of one-time-key claim requests
- * @param room - The room we want to encrypt for
- * @param encryptionSettings - body of the m.room.encryption event currently in force in this room
- */
- public constructor(
- private readonly olmMachine: OlmMachine,
- private readonly keyClaimManager: KeyClaimManager,
- private readonly outgoingRequestProcessor: OutgoingRequestProcessor,
- private readonly room: Room,
- private encryptionSettings: IContent,
- ) {
- this.prefixedLogger = logger.withPrefix(`[${room.roomId} encryption]`);
- }
-
- /**
- * Handle a new `m.room.encryption` event in this room
- *
- * @param config - The content of the encryption event
- */
- public onCryptoEvent(config: IContent): void {
- if (JSON.stringify(this.encryptionSettings) != JSON.stringify(config)) {
- this.prefixedLogger.error(`Ignoring m.room.encryption event which requests a change of config`);
- }
- }
-
- /**
- * Handle a new `m.room.member` event in this room
- *
- * @param member - new membership state
- */
- public onRoomMembership(member: RoomMember): void {
- this.prefixedLogger.debug(`${member.membership} event for ${member.userId}`);
-
- if (
- member.membership == "join" ||
- (member.membership == "invite" && this.room.shouldEncryptForInvitedMembers())
- ) {
- // make sure we are tracking the deviceList for this user
- this.prefixedLogger.debug(`starting to track devices for: ${member.userId}`);
- this.olmMachine.updateTrackedUsers([new UserId(member.userId)]);
- }
-
- // TODO: handle leaves (including our own)
- }
-
- /**
- * Prepare to encrypt events in this room.
- *
- * This ensures that we have a megolm session ready to use and that we have shared its key with all the devices
- * in the room.
- */
- public async ensureEncryptionSession(): Promise<void> {
- if (this.encryptionSettings.algorithm !== "m.megolm.v1.aes-sha2") {
- throw new Error(
- `Cannot encrypt in ${this.room.roomId} for unsupported algorithm '${this.encryptionSettings.algorithm}'`,
- );
- }
-
- const members = await this.room.getEncryptionTargetMembers();
- this.prefixedLogger.debug(
- `Encrypting for users (shouldEncryptForInvitedMembers: ${this.room.shouldEncryptForInvitedMembers()}):`,
- members.map((u) => `${u.userId} (${u.membership})`),
- );
-
- const userList = members.map((u) => new UserId(u.userId));
- await this.keyClaimManager.ensureSessionsForUsers(userList);
-
- this.prefixedLogger.debug("Sessions for users are ready; now sharing room key");
-
- const rustEncryptionSettings = new EncryptionSettings();
- /* FIXME historyVisibility, rotation, etc */
-
- const shareMessages = await this.olmMachine.shareRoomKey(
- new RoomId(this.room.roomId),
- userList,
- rustEncryptionSettings,
- );
- if (shareMessages) {
- for (const m of shareMessages) {
- await this.outgoingRequestProcessor.makeOutgoingRequest(m);
- }
- }
- }
-
- /**
- * Discard any existing group session for this room
- */
- public async forceDiscardSession(): Promise<void> {
- const r = await this.olmMachine.invalidateGroupSession(new RoomId(this.room.roomId));
- if (r) {
- this.prefixedLogger.info("Discarded existing group session");
- }
- }
-
- /**
- * Encrypt an event for this room
- *
- * This will ensure that we have a megolm session for this room, share it with the devices in the room, and
- * then encrypt the event using the session.
- *
- * @param event - Event to be encrypted.
- */
- public async encryptEvent(event: MatrixEvent): Promise<void> {
- await this.ensureEncryptionSession();
-
- const encryptedContent = await this.olmMachine.encryptRoomEvent(
- new RoomId(this.room.roomId),
- event.getType(),
- JSON.stringify(event.getContent()),
- );
-
- event.makeEncrypted(
- EventType.RoomMessageEncrypted,
- JSON.parse(encryptedContent),
- this.olmMachine.identityKeys.curve25519.toBase64(),
- this.olmMachine.identityKeys.ed25519.toBase64(),
- );
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/browserify-index.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/browserify-index.ts
deleted file mode 100644
index 7f91e90..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/browserify-index.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
-Copyright 2023 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.
-*/
-
-/* This file replaces rust-crypto/index.ts when the js-sdk is being built for browserify.
- *
- * It is a stub, so that we do not import the whole of the base64'ed wasm artifact into the browserify bundle.
- * It deliberately does nothing except raise an exception.
- */
-
-import { IHttpOpts, MatrixHttpApi } from "../http-api";
-
-export async function initRustCrypto(
- _http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
- _userId: string,
- _deviceId: string,
-): Promise<Crypto> {
- throw new Error("Rust crypto is not supported under browserify.");
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/constants.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/constants.ts
deleted file mode 100644
index 9d72060..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/constants.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
-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.
-*/
-
-/** The prefix used on indexeddbs created by rust-crypto */
-export const RUST_SDK_STORE_PREFIX = "matrix-js-sdk";
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/index.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/index.ts
deleted file mode 100644
index e2c541f..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/index.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
-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 RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
-
-import { RustCrypto } from "./rust-crypto";
-import { logger } from "../logger";
-import { RUST_SDK_STORE_PREFIX } from "./constants";
-import { IHttpOpts, MatrixHttpApi } from "../http-api";
-
-export async function initRustCrypto(
- http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
- userId: string,
- deviceId: string,
-): Promise<RustCrypto> {
- // initialise the rust matrix-sdk-crypto-js, if it hasn't already been done
- await RustSdkCryptoJs.initAsync();
-
- // enable tracing in the rust-sdk
- new RustSdkCryptoJs.Tracing(RustSdkCryptoJs.LoggerLevel.Trace).turnOn();
-
- const u = new RustSdkCryptoJs.UserId(userId);
- const d = new RustSdkCryptoJs.DeviceId(deviceId);
- logger.info("Init OlmMachine");
-
- // TODO: use the pickle key for the passphrase
- const olmMachine = await RustSdkCryptoJs.OlmMachine.initialize(u, d, RUST_SDK_STORE_PREFIX, "test pass");
- const rustCrypto = new RustCrypto(olmMachine, http, userId, deviceId);
-
- logger.info("Completed rust crypto-sdk setup");
- return rustCrypto;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/rust-crypto.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/rust-crypto.ts
deleted file mode 100644
index 4a0b1f8..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/rust-crypto/rust-crypto.ts
+++ /dev/null
@@ -1,334 +0,0 @@
-/*
-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 RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
-
-import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
-import type { IToDeviceEvent } from "../sync-accumulator";
-import type { IEncryptedEventInfo } from "../crypto/api";
-import { MatrixEvent } from "../models/event";
-import { Room } from "../models/room";
-import { RoomMember } from "../models/room-member";
-import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
-import { logger } from "../logger";
-import { IHttpOpts, MatrixHttpApi } from "../http-api";
-import { DeviceTrustLevel, UserTrustLevel } from "../crypto/CrossSigning";
-import { RoomEncryptor } from "./RoomEncryptor";
-import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
-import { KeyClaimManager } from "./KeyClaimManager";
-
-/**
- * An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
- */
-export class RustCrypto implements CryptoBackend {
- public globalErrorOnUnknownDevices = false;
-
- /** whether {@link stop} has been called */
- private stopped = false;
-
- /** whether {@link outgoingRequestLoop} is currently running */
- private outgoingRequestLoopRunning = false;
-
- /** mapping of roomId → encryptor class */
- private roomEncryptors: Record<string, RoomEncryptor> = {};
-
- private keyClaimManager: KeyClaimManager;
- private outgoingRequestProcessor: OutgoingRequestProcessor;
-
- public constructor(
- private readonly olmMachine: RustSdkCryptoJs.OlmMachine,
- http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
- _userId: string,
- _deviceId: string,
- ) {
- this.outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, http);
- this.keyClaimManager = new KeyClaimManager(olmMachine, this.outgoingRequestProcessor);
- }
-
- ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- //
- // CryptoBackend implementation
- //
- ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-
- public stop(): void {
- // stop() may be called multiple times, but attempting to close() the OlmMachine twice
- // will cause an error.
- if (this.stopped) {
- return;
- }
- this.stopped = true;
-
- this.keyClaimManager.stop();
-
- // make sure we close() the OlmMachine; doing so means that all the Rust objects will be
- // cleaned up; in particular, the indexeddb connections will be closed, which means they
- // can then be deleted.
- this.olmMachine.close();
- }
-
- public async encryptEvent(event: MatrixEvent, _room: Room): Promise<void> {
- const roomId = event.getRoomId()!;
- const encryptor = this.roomEncryptors[roomId];
-
- if (!encryptor) {
- throw new Error(`Cannot encrypt event in unconfigured room ${roomId}`);
- }
-
- await encryptor.encryptEvent(event);
- }
-
- public async decryptEvent(event: MatrixEvent): Promise<IEventDecryptionResult> {
- const roomId = event.getRoomId();
- if (!roomId) {
- // presumably, a to-device message. These are normally decrypted in preprocessToDeviceMessages
- // so the fact it has come back here suggests that decryption failed.
- //
- // once we drop support for the libolm crypto implementation, we can stop passing to-device messages
- // through decryptEvent and hence get rid of this case.
- throw new Error("to-device event was not decrypted in preprocessToDeviceMessages");
- }
- const res = (await this.olmMachine.decryptRoomEvent(
- JSON.stringify({
- event_id: event.getId(),
- type: event.getWireType(),
- sender: event.getSender(),
- state_key: event.getStateKey(),
- content: event.getWireContent(),
- origin_server_ts: event.getTs(),
- }),
- new RustSdkCryptoJs.RoomId(event.getRoomId()!),
- )) as RustSdkCryptoJs.DecryptedRoomEvent;
- return {
- clearEvent: JSON.parse(res.event),
- claimedEd25519Key: res.senderClaimedEd25519Key,
- senderCurve25519Key: res.senderCurve25519Key,
- forwardingCurve25519KeyChain: res.forwardingCurve25519KeyChain,
- };
- }
-
- public getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo {
- // TODO: make this work properly. Or better, replace it.
-
- const ret: Partial<IEncryptedEventInfo> = {};
-
- ret.senderKey = event.getSenderKey() ?? undefined;
- ret.algorithm = event.getWireContent().algorithm;
-
- if (!ret.senderKey || !ret.algorithm) {
- ret.encrypted = false;
- return ret as IEncryptedEventInfo;
- }
- ret.encrypted = true;
- ret.authenticated = true;
- ret.mismatchedSender = true;
- return ret as IEncryptedEventInfo;
- }
-
- public checkUserTrust(userId: string): UserTrustLevel {
- // TODO
- return new UserTrustLevel(false, false, false);
- }
-
- public checkDeviceTrust(userId: string, deviceId: string): DeviceTrustLevel {
- // TODO
- return new DeviceTrustLevel(false, false, false, false);
- }
-
- ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- //
- // CryptoApi implementation
- //
- ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-
- public globalBlacklistUnverifiedDevices = false;
-
- public async userHasCrossSigningKeys(): Promise<boolean> {
- // TODO
- return false;
- }
-
- public prepareToEncrypt(room: Room): void {
- const encryptor = this.roomEncryptors[room.roomId];
-
- if (encryptor) {
- encryptor.ensureEncryptionSession();
- }
- }
-
- public forceDiscardSession(roomId: string): Promise<void> {
- return this.roomEncryptors[roomId]?.forceDiscardSession();
- }
-
- public async exportRoomKeys(): Promise<IMegolmSessionData[]> {
- // TODO
- return [];
- }
-
- ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- //
- // SyncCryptoCallbacks implementation
- //
- ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-
- /**
- * Apply sync changes to the olm machine
- * @param events - the received to-device messages
- * @param oneTimeKeysCounts - the received one time key counts
- * @param unusedFallbackKeys - the received unused fallback keys
- * @returns A list of preprocessed to-device messages.
- */
- private async receiveSyncChanges({
- events,
- oneTimeKeysCounts = new Map<string, number>(),
- unusedFallbackKeys = new Set<string>(),
- }: {
- events?: IToDeviceEvent[];
- oneTimeKeysCounts?: Map<string, number>;
- unusedFallbackKeys?: Set<string>;
- }): Promise<IToDeviceEvent[]> {
- const result = await this.olmMachine.receiveSyncChanges(
- events ? JSON.stringify(events) : "[]",
- new RustSdkCryptoJs.DeviceLists(),
- oneTimeKeysCounts,
- unusedFallbackKeys,
- );
-
- // receiveSyncChanges returns a JSON-encoded list of decrypted to-device messages.
- return JSON.parse(result);
- }
-
- /** called by the sync loop to preprocess incoming to-device messages
- *
- * @param events - the received to-device messages
- * @returns A list of preprocessed to-device messages.
- */
- public preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]> {
- // send the received to-device messages into receiveSyncChanges. We have no info on device-list changes,
- // one-time-keys, or fallback keys, so just pass empty data.
- return this.receiveSyncChanges({ events });
- }
-
- /** called by the sync loop to preprocess one time key counts
- *
- * @param oneTimeKeysCounts - the received one time key counts
- * @returns A list of preprocessed to-device messages.
- */
- public async preprocessOneTimeKeyCounts(oneTimeKeysCounts: Map<string, number>): Promise<void> {
- await this.receiveSyncChanges({ oneTimeKeysCounts });
- }
-
- /** called by the sync loop to preprocess unused fallback keys
- *
- * @param unusedFallbackKeys - the received unused fallback keys
- * @returns A list of preprocessed to-device messages.
- */
- public async preprocessUnusedFallbackKeys(unusedFallbackKeys: Set<string>): Promise<void> {
- await this.receiveSyncChanges({ unusedFallbackKeys });
- }
-
- /** called by the sync loop on m.room.encrypted events
- *
- * @param room - in which the event was received
- * @param event - encryption event to be processed
- */
- public async onCryptoEvent(room: Room, event: MatrixEvent): Promise<void> {
- const config = event.getContent();
-
- const existingEncryptor = this.roomEncryptors[room.roomId];
- if (existingEncryptor) {
- existingEncryptor.onCryptoEvent(config);
- } else {
- this.roomEncryptors[room.roomId] = new RoomEncryptor(
- this.olmMachine,
- this.keyClaimManager,
- this.outgoingRequestProcessor,
- room,
- config,
- );
- }
-
- // start tracking devices for any users already known to be in this room.
- const members = await room.getEncryptionTargetMembers();
- logger.debug(
- `[${room.roomId} encryption] starting to track devices for: `,
- members.map((u) => `${u.userId} (${u.membership})`),
- );
- await this.olmMachine.updateTrackedUsers(members.map((u) => new RustSdkCryptoJs.UserId(u.userId)));
- }
-
- /** called by the sync loop after processing each sync.
- *
- * TODO: figure out something equivalent for sliding sync.
- *
- * @param syncState - information on the completed sync.
- */
- public onSyncCompleted(syncState: OnSyncCompletedData): void {
- // Processing the /sync may have produced new outgoing requests which need sending, so kick off the outgoing
- // request loop, if it's not already running.
- this.outgoingRequestLoop();
- }
-
- ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- //
- // Other public functions
- //
- ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-
- /** called by the MatrixClient on a room membership event
- *
- * @param event - The matrix event which caused this event to fire.
- * @param member - The member whose RoomMember.membership changed.
- * @param oldMembership - The previous membership state. Null if it's a new member.
- */
- public onRoomMembership(event: MatrixEvent, member: RoomMember, oldMembership?: string): void {
- const enc = this.roomEncryptors[event.getRoomId()!];
- if (!enc) {
- // not encrypting in this room
- return;
- }
- enc.onRoomMembership(member);
- }
-
- ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- //
- // Outgoing requests
- //
- ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-
- private async outgoingRequestLoop(): Promise<void> {
- if (this.outgoingRequestLoopRunning) {
- return;
- }
- this.outgoingRequestLoopRunning = true;
- try {
- while (!this.stopped) {
- const outgoingRequests: Object[] = await this.olmMachine.outgoingRequests();
- if (outgoingRequests.length == 0 || this.stopped) {
- // no more messages to send (or we have been told to stop): exit the loop
- return;
- }
- for (const msg of outgoingRequests) {
- await this.outgoingRequestProcessor.makeOutgoingRequest(msg as OutgoingRequest);
- }
- }
- } catch (e) {
- logger.error("Error processing outgoing-message requests from rust crypto-sdk", e);
- } finally {
- this.outgoingRequestLoopRunning = false;
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/scheduler.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/scheduler.ts
deleted file mode 100644
index 6b6bae1..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/scheduler.ts
+++ /dev/null
@@ -1,335 +0,0 @@
-/*
-Copyright 2015 - 2021 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.
-*/
-
-/**
- * This is an internal module which manages queuing, scheduling and retrying
- * of requests.
- */
-import * as utils from "./utils";
-import { logger } from "./logger";
-import { MatrixEvent } from "./models/event";
-import { EventType } from "./@types/event";
-import { IDeferred } from "./utils";
-import { ConnectionError, MatrixError } from "./http-api";
-import { ISendEventResponse } from "./@types/requests";
-
-const DEBUG = false; // set true to enable console logging.
-
-interface IQueueEntry<T> {
- event: MatrixEvent;
- defer: IDeferred<T>;
- attempts: number;
-}
-
-/**
- * The function to invoke to process (send) events in the queue.
- * @param event - The event to send.
- * @returns Resolved/rejected depending on the outcome of the request.
- */
-type ProcessFunction<T> = (event: MatrixEvent) => Promise<T>;
-
-// eslint-disable-next-line camelcase
-export class MatrixScheduler<T = ISendEventResponse> {
- /**
- * Retries events up to 4 times using exponential backoff. This produces wait
- * times of 2, 4, 8, and 16 seconds (30s total) after which we give up. If the
- * failure was due to a rate limited request, the time specified in the error is
- * waited before being retried.
- * @param attempts - Number of attempts that have been made, including the one that just failed (ie. starting at 1)
- * @see retryAlgorithm
- */
- // eslint-disable-next-line @typescript-eslint/naming-convention
- public static RETRY_BACKOFF_RATELIMIT(event: MatrixEvent | null, attempts: number, err: MatrixError): number {
- if (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401) {
- // client error; no amount of retrying with save you now.
- return -1;
- }
- if (err instanceof ConnectionError) {
- return -1;
- }
-
- // if event that we are trying to send is too large in any way then retrying won't help
- if (err.name === "M_TOO_LARGE") {
- return -1;
- }
-
- if (err.name === "M_LIMIT_EXCEEDED") {
- const waitTime = err.data.retry_after_ms;
- if (waitTime > 0) {
- return waitTime;
- }
- }
- if (attempts > 4) {
- return -1; // give up
- }
- return 1000 * Math.pow(2, attempts);
- }
-
- /**
- * Queues `m.room.message` events and lets other events continue
- * concurrently.
- * @see queueAlgorithm
- */
- // eslint-disable-next-line @typescript-eslint/naming-convention
- public static QUEUE_MESSAGES(event: MatrixEvent): string | null {
- // enqueue messages or events that associate with another event (redactions and relations)
- if (event.getType() === EventType.RoomMessage || event.hasAssociation()) {
- // put these events in the 'message' queue.
- return "message";
- }
- // allow all other events continue concurrently.
- return null;
- }
-
- // queueName: [{
- // event: MatrixEvent, // event to send
- // defer: Deferred, // defer to resolve/reject at the END of the retries
- // attempts: Number // number of times we've called processFn
- // }, ...]
- private readonly queues: Record<string, IQueueEntry<T>[]> = {};
- private activeQueues: string[] = [];
- private procFn: ProcessFunction<T> | null = null;
-
- /**
- * Construct a scheduler for Matrix. Requires
- * {@link MatrixScheduler#setProcessFunction} to be provided
- * with a way of processing events.
- * @param retryAlgorithm - Optional. The retry
- * algorithm to apply when determining when to try to send an event again.
- * Defaults to {@link MatrixScheduler.RETRY_BACKOFF_RATELIMIT}.
- * @param queueAlgorithm - Optional. The queuing
- * algorithm to apply when determining which events should be sent before the
- * given event. Defaults to {@link MatrixScheduler.QUEUE_MESSAGES}.
- */
- public constructor(
- /**
- * The retry algorithm to apply when retrying events. To stop retrying, return
- * `-1`. If this event was part of a queue, it will be removed from
- * the queue.
- * @param event - The event being retried.
- * @param attempts - The number of failed attempts. This will always be \>= 1.
- * @param err - The most recent error message received when trying
- * to send this event.
- * @returns The number of milliseconds to wait before trying again. If
- * this is 0, the request will be immediately retried. If this is
- * `-1`, the event will be marked as
- * {@link EventStatus.NOT_SENT} and will not be retried.
- */
- public readonly retryAlgorithm = MatrixScheduler.RETRY_BACKOFF_RATELIMIT,
- /**
- * The queuing algorithm to apply to events. This function must be idempotent as
- * it may be called multiple times with the same event. All queues created are
- * serviced in a FIFO manner. To send the event ASAP, return `null`
- * which will not put this event in a queue. Events that fail to send that form
- * part of a queue will be removed from the queue and the next event in the
- * queue will be sent.
- * @param event - The event to be sent.
- * @returns The name of the queue to put the event into. If a queue with
- * this name does not exist, it will be created. If this is `null`,
- * the event is not put into a queue and will be sent concurrently.
- */
- public readonly queueAlgorithm = MatrixScheduler.QUEUE_MESSAGES,
- ) {}
-
- /**
- * Retrieve a queue based on an event. The event provided does not need to be in
- * the queue.
- * @param event - An event to get the queue for.
- * @returns A shallow copy of events in the queue or null.
- * Modifying this array will not modify the list itself. Modifying events in
- * this array <i>will</i> modify the underlying event in the queue.
- * @see MatrixScheduler.removeEventFromQueue To remove an event from the queue.
- */
- public getQueueForEvent(event: MatrixEvent): MatrixEvent[] | null {
- const name = this.queueAlgorithm(event);
- if (!name || !this.queues[name]) {
- return null;
- }
- return this.queues[name].map(function (obj) {
- return obj.event;
- });
- }
-
- /**
- * Remove this event from the queue. The event is equal to another event if they
- * have the same ID returned from event.getId().
- * @param event - The event to remove.
- * @returns True if this event was removed.
- */
- public removeEventFromQueue(event: MatrixEvent): boolean {
- const name = this.queueAlgorithm(event);
- if (!name || !this.queues[name]) {
- return false;
- }
- let removed = false;
- utils.removeElement(this.queues[name], (element) => {
- if (element.event.getId() === event.getId()) {
- // XXX we should probably reject the promise?
- // https://github.com/matrix-org/matrix-js-sdk/issues/496
- removed = true;
- return true;
- }
- return false;
- });
- return removed;
- }
-
- /**
- * Set the process function. Required for events in the queue to be processed.
- * If set after events have been added to the queue, this will immediately start
- * processing them.
- * @param fn - The function that can process events
- * in the queue.
- */
- public setProcessFunction(fn: ProcessFunction<T>): void {
- this.procFn = fn;
- this.startProcessingQueues();
- }
-
- /**
- * Queue an event if it is required and start processing queues.
- * @param event - The event that may be queued.
- * @returns A promise if the event was queued, which will be
- * resolved or rejected in due time, else null.
- */
- public queueEvent(event: MatrixEvent): Promise<T> | null {
- const queueName = this.queueAlgorithm(event);
- if (!queueName) {
- return null;
- }
- // add the event to the queue and make a deferred for it.
- if (!this.queues[queueName]) {
- this.queues[queueName] = [];
- }
- const defer = utils.defer<T>();
- this.queues[queueName].push({
- event: event,
- defer: defer,
- attempts: 0,
- });
- debuglog("Queue algorithm dumped event %s into queue '%s'", event.getId(), queueName);
- this.startProcessingQueues();
- return defer.promise;
- }
-
- private startProcessingQueues(): void {
- if (!this.procFn) return;
- // for each inactive queue with events in them
- Object.keys(this.queues)
- .filter((queueName) => {
- return this.activeQueues.indexOf(queueName) === -1 && this.queues[queueName].length > 0;
- })
- .forEach((queueName) => {
- // mark the queue as active
- this.activeQueues.push(queueName);
- // begin processing the head of the queue
- debuglog("Spinning up queue: '%s'", queueName);
- this.processQueue(queueName);
- });
- }
-
- private processQueue = (queueName: string): void => {
- // get head of queue
- const obj = this.peekNextEvent(queueName);
- if (!obj) {
- this.disableQueue(queueName);
- return;
- }
- debuglog("Queue '%s' has %s pending events", queueName, this.queues[queueName].length);
- // fire the process function and if it resolves, resolve the deferred. Else
- // invoke the retry algorithm.
-
- // First wait for a resolved promise, so the resolve handlers for
- // the deferred of the previously sent event can run.
- // This way enqueued relations/redactions to enqueued events can receive
- // the remove id of their target before being sent.
- Promise.resolve()
- .then(() => {
- return this.procFn!(obj.event);
- })
- .then(
- (res) => {
- // remove this from the queue
- this.removeNextEvent(queueName);
- debuglog("Queue '%s' sent event %s", queueName, obj.event.getId());
- obj.defer.resolve(res);
- // keep processing
- this.processQueue(queueName);
- },
- (err) => {
- obj.attempts += 1;
- // ask the retry algorithm when/if we should try again
- const waitTimeMs = this.retryAlgorithm(obj.event, obj.attempts, err);
- debuglog(
- "retry(%s) err=%s event_id=%s waitTime=%s",
- obj.attempts,
- err,
- obj.event.getId(),
- waitTimeMs,
- );
- if (waitTimeMs === -1) {
- // give up (you quitter!)
- debuglog("Queue '%s' giving up on event %s", queueName, obj.event.getId());
- // remove this from the queue
- this.clearQueue(queueName, err);
- } else {
- setTimeout(this.processQueue, waitTimeMs, queueName);
- }
- },
- );
- };
-
- private disableQueue(queueName: string): void {
- // queue is empty. Mark as inactive and stop recursing.
- const index = this.activeQueues.indexOf(queueName);
- if (index >= 0) {
- this.activeQueues.splice(index, 1);
- }
- debuglog("Stopping queue '%s' as it is now empty", queueName);
- }
-
- private clearQueue(queueName: string, err: unknown): void {
- debuglog("clearing queue '%s'", queueName);
- let obj: IQueueEntry<T> | undefined;
- while ((obj = this.removeNextEvent(queueName))) {
- obj.defer.reject(err);
- }
- this.disableQueue(queueName);
- }
-
- private peekNextEvent(queueName: string): IQueueEntry<T> | undefined {
- const queue = this.queues[queueName];
- if (!Array.isArray(queue)) {
- return undefined;
- }
- return queue[0];
- }
-
- private removeNextEvent(queueName: string): IQueueEntry<T> | undefined {
- const queue = this.queues[queueName];
- if (!Array.isArray(queue)) {
- return undefined;
- }
- return queue.shift();
- }
-}
-
-/* istanbul ignore next */
-function debuglog(...args: any[]): void {
- if (DEBUG) {
- logger.log(...args);
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/secret-storage.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/secret-storage.ts
deleted file mode 100644
index f0c19c4..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/secret-storage.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
-Copyright 2021-2023 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.
-*/
-
-/**
- * Implementation of server-side secret storage
- *
- * @see https://spec.matrix.org/v1.6/client-server-api/#storage
- */
-
-/**
- * Common base interface for Secret Storage Keys.
- *
- * The common properties for all encryption keys used in server-side secret storage.
- *
- * @see https://spec.matrix.org/v1.6/client-server-api/#key-storage
- */
-export interface SecretStorageKeyDescriptionCommon {
- /** A human-readable name for this key. */
- // XXX: according to the spec, this is optional
- name: string;
-
- /** The encryption algorithm used with this key. */
- algorithm: string;
-
- /** Information for deriving this key from a passphrase. */
- // XXX: according to the spec, this is optional
- passphrase: PassphraseInfo;
-}
-
-/**
- * Properties for a SSSS key using the `m.secret_storage.v1.aes-hmac-sha2` algorithm.
- *
- * Corresponds to `AesHmacSha2KeyDescription` in the specification.
- *
- * @see https://spec.matrix.org/v1.6/client-server-api/#msecret_storagev1aes-hmac-sha2
- */
-export interface SecretStorageKeyDescriptionAesV1 extends SecretStorageKeyDescriptionCommon {
- // XXX: strictly speaking, we should be able to enforce the algorithm here. But
- // this interface ends up being incorrectly used where other algorithms are in use (notably
- // in device-dehydration support), and unpicking that is too much like hard work
- // at the moment.
- // algorithm: "m.secret_storage.v1.aes-hmac-sha2";
-
- /** The 16-byte AES initialization vector, encoded as base64. */
- iv: string;
-
- /** The MAC of the result of encrypting 32 bytes of 0, encoded as base64. */
- mac: string;
-}
-
-/**
- * Union type for secret storage keys.
- *
- * For now, this is only {@link SecretStorageKeyDescriptionAesV1}, but other interfaces may be added in future.
- */
-export type SecretStorageKeyDescription = SecretStorageKeyDescriptionAesV1;
-
-/**
- * Information on how to generate the key from a passphrase.
- *
- * @see https://spec.matrix.org/v1.6/client-server-api/#deriving-keys-from-passphrases
- */
-export interface PassphraseInfo {
- /** The algorithm to be used to derive the key. */
- algorithm: "m.pbkdf2";
-
- /** The number of PBKDF2 iterations to use. */
- iterations: number;
-
- /** The salt to be used for PBKDF2. */
- salt: string;
-
- /** The number of bits to generate. Defaults to 256. */
- bits?: number;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/service-types.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/service-types.ts
deleted file mode 100644
index 3ed08bb..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/service-types.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
-Copyright 2019 - 2021 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.
-*/
-
-export enum SERVICE_TYPES {
- IS = "SERVICE_TYPE_IS", // An identity server
- IM = "SERVICE_TYPE_IM", // An integration manager
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/sliding-sync-sdk.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/sliding-sync-sdk.ts
deleted file mode 100644
index 93e29e0..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/sliding-sync-sdk.ts
+++ /dev/null
@@ -1,1027 +0,0 @@
-/*
-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 type { SyncCryptoCallbacks } from "./common-crypto/CryptoBackend";
-import { NotificationCountType, Room, RoomEvent } from "./models/room";
-import { logger } from "./logger";
-import * as utils from "./utils";
-import { EventTimeline } from "./models/event-timeline";
-import { ClientEvent, IStoredClientOpts, MatrixClient } from "./client";
-import {
- ISyncStateData,
- SyncState,
- _createAndReEmitRoom,
- SyncApiOptions,
- defaultClientOpts,
- defaultSyncApiOpts,
-} from "./sync";
-import { MatrixEvent } from "./models/event";
-import { Crypto } from "./crypto";
-import { IMinimalEvent, IRoomEvent, IStateEvent, IStrippedState, ISyncResponse } from "./sync-accumulator";
-import { MatrixError } from "./http-api";
-import {
- Extension,
- ExtensionState,
- MSC3575RoomData,
- MSC3575SlidingSyncResponse,
- SlidingSync,
- SlidingSyncEvent,
- SlidingSyncState,
-} from "./sliding-sync";
-import { EventType } from "./@types/event";
-import { IPushRules } from "./@types/PushRules";
-import { RoomStateEvent } from "./models/room-state";
-import { RoomMemberEvent } from "./models/room-member";
-
-// Number of consecutive failed syncs that will lead to a syncState of ERROR as opposed
-// to RECONNECTING. This is needed to inform the client of server issues when the
-// keepAlive is successful but the server /sync fails.
-const FAILED_SYNC_ERROR_THRESHOLD = 3;
-
-type ExtensionE2EERequest = {
- enabled: boolean;
-};
-
-type ExtensionE2EEResponse = Pick<
- ISyncResponse,
- | "device_lists"
- | "device_one_time_keys_count"
- | "device_unused_fallback_key_types"
- | "org.matrix.msc2732.device_unused_fallback_key_types"
->;
-
-class ExtensionE2EE implements Extension<ExtensionE2EERequest, ExtensionE2EEResponse> {
- public constructor(private readonly crypto: Crypto) {}
-
- public name(): string {
- return "e2ee";
- }
-
- public when(): ExtensionState {
- return ExtensionState.PreProcess;
- }
-
- public onRequest(isInitial: boolean): ExtensionE2EERequest | undefined {
- if (!isInitial) {
- return undefined;
- }
- return {
- enabled: true, // this is sticky so only send it on the initial request
- };
- }
-
- public async onResponse(data: ExtensionE2EEResponse): Promise<void> {
- // Handle device list updates
- if (data["device_lists"]) {
- await this.crypto.handleDeviceListChanges(
- {
- oldSyncToken: "yep", // XXX need to do this so the device list changes get processed :(
- },
- data["device_lists"],
- );
- }
-
- // Handle one_time_keys_count
- if (data["device_one_time_keys_count"]) {
- const currentCount = data["device_one_time_keys_count"].signed_curve25519 || 0;
- this.crypto.updateOneTimeKeyCount(currentCount);
- }
- if (data["device_unused_fallback_key_types"] || data["org.matrix.msc2732.device_unused_fallback_key_types"]) {
- // The presence of device_unused_fallback_key_types indicates that the
- // server supports fallback keys. If there's no unused
- // signed_curve25519 fallback key we need a new one.
- const unusedFallbackKeys =
- data["device_unused_fallback_key_types"] || data["org.matrix.msc2732.device_unused_fallback_key_types"];
- this.crypto.setNeedsNewFallback(
- Array.isArray(unusedFallbackKeys) && !unusedFallbackKeys.includes("signed_curve25519"),
- );
- }
- this.crypto.onSyncCompleted({});
- }
-}
-
-type ExtensionToDeviceRequest = {
- since?: string;
- limit?: number;
- enabled?: boolean;
-};
-
-type ExtensionToDeviceResponse = {
- events: Required<ISyncResponse>["to_device"]["events"];
- next_batch: string | null;
-};
-
-class ExtensionToDevice implements Extension<ExtensionToDeviceRequest, ExtensionToDeviceResponse> {
- private nextBatch: string | null = null;
-
- public constructor(private readonly client: MatrixClient, private readonly cryptoCallbacks?: SyncCryptoCallbacks) {}
-
- public name(): string {
- return "to_device";
- }
-
- public when(): ExtensionState {
- return ExtensionState.PreProcess;
- }
-
- public onRequest(isInitial: boolean): ExtensionToDeviceRequest {
- const extReq: ExtensionToDeviceRequest = {
- since: this.nextBatch !== null ? this.nextBatch : undefined,
- };
- if (isInitial) {
- extReq["limit"] = 100;
- extReq["enabled"] = true;
- }
- return extReq;
- }
-
- public async onResponse(data: ExtensionToDeviceResponse): Promise<void> {
- const cancelledKeyVerificationTxns: string[] = [];
- let events = data["events"] || [];
- if (events.length > 0 && this.cryptoCallbacks) {
- events = await this.cryptoCallbacks.preprocessToDeviceMessages(events);
- }
- events
- .map(this.client.getEventMapper())
- .map((toDeviceEvent) => {
- // map is a cheap inline forEach
- // We want to flag m.key.verification.start events as cancelled
- // if there's an accompanying m.key.verification.cancel event, so
- // we pull out the transaction IDs from the cancellation events
- // so we can flag the verification events as cancelled in the loop
- // below.
- if (toDeviceEvent.getType() === "m.key.verification.cancel") {
- const txnId: string | undefined = toDeviceEvent.getContent()["transaction_id"];
- if (txnId) {
- cancelledKeyVerificationTxns.push(txnId);
- }
- }
-
- // as mentioned above, .map is a cheap inline forEach, so return
- // the unmodified event.
- return toDeviceEvent;
- })
- .forEach((toDeviceEvent) => {
- const content = toDeviceEvent.getContent();
- if (toDeviceEvent.getType() == "m.room.message" && content.msgtype == "m.bad.encrypted") {
- // the mapper already logged a warning.
- logger.log("Ignoring undecryptable to-device event from " + toDeviceEvent.getSender());
- return;
- }
-
- if (
- toDeviceEvent.getType() === "m.key.verification.start" ||
- toDeviceEvent.getType() === "m.key.verification.request"
- ) {
- const txnId = content["transaction_id"];
- if (cancelledKeyVerificationTxns.includes(txnId)) {
- toDeviceEvent.flagCancelled();
- }
- }
-
- this.client.emit(ClientEvent.ToDeviceEvent, toDeviceEvent);
- });
-
- this.nextBatch = data.next_batch;
- }
-}
-
-type ExtensionAccountDataRequest = {
- enabled: boolean;
-};
-
-type ExtensionAccountDataResponse = {
- global: IMinimalEvent[];
- rooms: Record<string, IMinimalEvent[]>;
-};
-
-class ExtensionAccountData implements Extension<ExtensionAccountDataRequest, ExtensionAccountDataResponse> {
- public constructor(private readonly client: MatrixClient) {}
-
- public name(): string {
- return "account_data";
- }
-
- public when(): ExtensionState {
- return ExtensionState.PostProcess;
- }
-
- public onRequest(isInitial: boolean): ExtensionAccountDataRequest | undefined {
- if (!isInitial) {
- return undefined;
- }
- return {
- enabled: true,
- };
- }
-
- public onResponse(data: ExtensionAccountDataResponse): void {
- if (data.global && data.global.length > 0) {
- this.processGlobalAccountData(data.global);
- }
-
- for (const roomId in data.rooms) {
- const accountDataEvents = mapEvents(this.client, roomId, data.rooms[roomId]);
- const room = this.client.getRoom(roomId);
- if (!room) {
- logger.warn("got account data for room but room doesn't exist on client:", roomId);
- continue;
- }
- room.addAccountData(accountDataEvents);
- accountDataEvents.forEach((e) => {
- this.client.emit(ClientEvent.Event, e);
- });
- }
- }
-
- private processGlobalAccountData(globalAccountData: IMinimalEvent[]): void {
- const events = mapEvents(this.client, undefined, globalAccountData);
- const prevEventsMap = events.reduce<Record<string, MatrixEvent | undefined>>((m, c) => {
- m[c.getType()] = this.client.store.getAccountData(c.getType());
- return m;
- }, {});
- this.client.store.storeAccountDataEvents(events);
- events.forEach((accountDataEvent) => {
- // Honour push rules that come down the sync stream but also
- // honour push rules that were previously cached. Base rules
- // will be updated when we receive push rules via getPushRules
- // (see sync) before syncing over the network.
- if (accountDataEvent.getType() === EventType.PushRules) {
- const rules = accountDataEvent.getContent<IPushRules>();
- this.client.setPushRules(rules);
- }
- const prevEvent = prevEventsMap[accountDataEvent.getType()];
- this.client.emit(ClientEvent.AccountData, accountDataEvent, prevEvent);
- return accountDataEvent;
- });
- }
-}
-
-type ExtensionTypingRequest = {
- enabled: boolean;
-};
-
-type ExtensionTypingResponse = {
- rooms: Record<string, IMinimalEvent>;
-};
-
-class ExtensionTyping implements Extension<ExtensionTypingRequest, ExtensionTypingResponse> {
- public constructor(private readonly client: MatrixClient) {}
-
- public name(): string {
- return "typing";
- }
-
- public when(): ExtensionState {
- return ExtensionState.PostProcess;
- }
-
- public onRequest(isInitial: boolean): ExtensionTypingRequest | undefined {
- if (!isInitial) {
- return undefined; // don't send a JSON object for subsequent requests, we don't need to.
- }
- return {
- enabled: true,
- };
- }
-
- public onResponse(data: ExtensionTypingResponse): void {
- if (!data?.rooms) {
- return;
- }
-
- for (const roomId in data.rooms) {
- processEphemeralEvents(this.client, roomId, [data.rooms[roomId]]);
- }
- }
-}
-
-type ExtensionReceiptsRequest = {
- enabled: boolean;
-};
-
-type ExtensionReceiptsResponse = {
- rooms: Record<string, IMinimalEvent>;
-};
-
-class ExtensionReceipts implements Extension<ExtensionReceiptsRequest, ExtensionReceiptsResponse> {
- public constructor(private readonly client: MatrixClient) {}
-
- public name(): string {
- return "receipts";
- }
-
- public when(): ExtensionState {
- return ExtensionState.PostProcess;
- }
-
- public onRequest(isInitial: boolean): ExtensionReceiptsRequest | undefined {
- if (isInitial) {
- return {
- enabled: true,
- };
- }
- return undefined; // don't send a JSON object for subsequent requests, we don't need to.
- }
-
- public onResponse(data: ExtensionReceiptsResponse): void {
- if (!data?.rooms) {
- return;
- }
-
- for (const roomId in data.rooms) {
- processEphemeralEvents(this.client, roomId, [data.rooms[roomId]]);
- }
- }
-}
-
-/**
- * A copy of SyncApi such that it can be used as a drop-in replacement for sync v2. For the actual
- * sliding sync API, see sliding-sync.ts or the class SlidingSync.
- */
-export class SlidingSyncSdk {
- private readonly opts: IStoredClientOpts;
- private readonly syncOpts: SyncApiOptions;
- private syncState: SyncState | null = null;
- private syncStateData?: ISyncStateData;
- private lastPos: string | null = null;
- private failCount = 0;
- private notifEvents: MatrixEvent[] = []; // accumulator of sync events in the current sync response
-
- public constructor(
- private readonly slidingSync: SlidingSync,
- private readonly client: MatrixClient,
- opts?: IStoredClientOpts,
- syncOpts?: SyncApiOptions,
- ) {
- this.opts = defaultClientOpts(opts);
- this.syncOpts = defaultSyncApiOpts(syncOpts);
-
- if (client.getNotifTimelineSet()) {
- client.reEmitter.reEmit(client.getNotifTimelineSet()!, [RoomEvent.Timeline, RoomEvent.TimelineReset]);
- }
-
- this.slidingSync.on(SlidingSyncEvent.Lifecycle, this.onLifecycle.bind(this));
- this.slidingSync.on(SlidingSyncEvent.RoomData, this.onRoomData.bind(this));
- const extensions: Extension<any, any>[] = [
- new ExtensionToDevice(this.client, this.syncOpts.cryptoCallbacks),
- new ExtensionAccountData(this.client),
- new ExtensionTyping(this.client),
- new ExtensionReceipts(this.client),
- ];
- if (this.syncOpts.crypto) {
- extensions.push(new ExtensionE2EE(this.syncOpts.crypto));
- }
- extensions.forEach((ext) => {
- this.slidingSync.registerExtension(ext);
- });
- }
-
- private onRoomData(roomId: string, roomData: MSC3575RoomData): void {
- let room = this.client.store.getRoom(roomId);
- if (!room) {
- if (!roomData.initial) {
- logger.debug("initial flag not set but no stored room exists for room ", roomId, roomData);
- return;
- }
- room = _createAndReEmitRoom(this.client, roomId, this.opts);
- }
- this.processRoomData(this.client, room, roomData);
- }
-
- private onLifecycle(state: SlidingSyncState, resp: MSC3575SlidingSyncResponse | null, err?: Error): void {
- if (err) {
- logger.debug("onLifecycle", state, err);
- }
- switch (state) {
- case SlidingSyncState.Complete:
- this.purgeNotifications();
- if (!resp) {
- break;
- }
- // Element won't stop showing the initial loading spinner unless we fire SyncState.Prepared
- if (!this.lastPos) {
- this.updateSyncState(SyncState.Prepared, {
- oldSyncToken: undefined,
- nextSyncToken: resp.pos,
- catchingUp: false,
- fromCache: false,
- });
- }
- // Conversely, Element won't show the room list unless there is at least 1x SyncState.Syncing
- // so hence for the very first sync we will fire prepared then immediately syncing.
- this.updateSyncState(SyncState.Syncing, {
- oldSyncToken: this.lastPos!,
- nextSyncToken: resp.pos,
- catchingUp: false,
- fromCache: false,
- });
- this.lastPos = resp.pos;
- break;
- case SlidingSyncState.RequestFinished:
- if (err) {
- this.failCount += 1;
- this.updateSyncState(
- this.failCount > FAILED_SYNC_ERROR_THRESHOLD ? SyncState.Error : SyncState.Reconnecting,
- {
- error: new MatrixError(err),
- },
- );
- if (this.shouldAbortSync(new MatrixError(err))) {
- return; // shouldAbortSync actually stops syncing too so we don't need to do anything.
- }
- } else {
- this.failCount = 0;
- }
- break;
- }
- }
-
- /**
- * Sync rooms the user has left.
- * @returns Resolved when they've been added to the store.
- */
- public async syncLeftRooms(): Promise<Room[]> {
- return []; // TODO
- }
-
- /**
- * Peek into a room. This will result in the room in question being synced so it
- * is accessible via getRooms(). Live updates for the room will be provided.
- * @param roomId - The room ID to peek into.
- * @returns A promise which resolves once the room has been added to the
- * store.
- */
- public async peek(_roomId: string): Promise<Room> {
- return null!; // TODO
- }
-
- /**
- * Stop polling for updates in the peeked room. NOPs if there is no room being
- * peeked.
- */
- public stopPeeking(): void {
- // TODO
- }
-
- /**
- * Returns the current state of this sync object
- * @see MatrixClient#event:"sync"
- */
- public getSyncState(): SyncState | null {
- return this.syncState;
- }
-
- /**
- * Returns the additional data object associated with
- * the current sync state, or null if there is no
- * such data.
- * Sync errors, if available, are put in the 'error' key of
- * this object.
- */
- public getSyncStateData(): ISyncStateData | null {
- return this.syncStateData ?? null;
- }
-
- // Helper functions which set up JS SDK structs are below and are identical to the sync v2 counterparts
-
- public createRoom(roomId: string): Room {
- // XXX cargoculted from sync.ts
- const { timelineSupport } = this.client;
- const room = new Room(roomId, this.client, this.client.getUserId()!, {
- lazyLoadMembers: this.opts.lazyLoadMembers,
- pendingEventOrdering: this.opts.pendingEventOrdering,
- timelineSupport,
- });
- this.client.reEmitter.reEmit(room, [
- RoomEvent.Name,
- RoomEvent.Redaction,
- RoomEvent.RedactionCancelled,
- RoomEvent.Receipt,
- RoomEvent.Tags,
- RoomEvent.LocalEchoUpdated,
- RoomEvent.AccountData,
- RoomEvent.MyMembership,
- RoomEvent.Timeline,
- RoomEvent.TimelineReset,
- ]);
- this.registerStateListeners(room);
- return room;
- }
-
- private registerStateListeners(room: Room): void {
- // XXX cargoculted from sync.ts
- // we need to also re-emit room state and room member events, so hook it up
- // to the client now. We need to add a listener for RoomState.members in
- // order to hook them correctly.
- this.client.reEmitter.reEmit(room.currentState, [
- RoomStateEvent.Events,
- RoomStateEvent.Members,
- RoomStateEvent.NewMember,
- RoomStateEvent.Update,
- ]);
- room.currentState.on(RoomStateEvent.NewMember, (event, state, member) => {
- member.user = this.client.getUser(member.userId) ?? undefined;
- this.client.reEmitter.reEmit(member, [
- RoomMemberEvent.Name,
- RoomMemberEvent.Typing,
- RoomMemberEvent.PowerLevel,
- RoomMemberEvent.Membership,
- ]);
- });
- }
-
- /*
- private deregisterStateListeners(room: Room): void { // XXX cargoculted from sync.ts
- // could do with a better way of achieving this.
- room.currentState.removeAllListeners(RoomStateEvent.Events);
- room.currentState.removeAllListeners(RoomStateEvent.Members);
- room.currentState.removeAllListeners(RoomStateEvent.NewMember);
- } */
-
- private shouldAbortSync(error: MatrixError): boolean {
- if (error.errcode === "M_UNKNOWN_TOKEN") {
- // The logout already happened, we just need to stop.
- logger.warn("Token no longer valid - assuming logout");
- this.stop();
- this.updateSyncState(SyncState.Error, { error });
- return true;
- }
- return false;
- }
-
- private async processRoomData(client: MatrixClient, room: Room, roomData: MSC3575RoomData): Promise<void> {
- roomData = ensureNameEvent(client, room.roomId, roomData);
- const stateEvents = mapEvents(this.client, room.roomId, roomData.required_state);
- // Prevent events from being decrypted ahead of time
- // this helps large account to speed up faster
- // room::decryptCriticalEvent is in charge of decrypting all the events
- // required for a client to function properly
- let timelineEvents = mapEvents(this.client, room.roomId, roomData.timeline, false);
- const ephemeralEvents: MatrixEvent[] = []; // TODO this.mapSyncEventsFormat(joinObj.ephemeral);
-
- // TODO: handle threaded / beacon events
-
- if (roomData.initial) {
- // we should not know about any of these timeline entries if this is a genuinely new room.
- // If we do, then we've effectively done scrollback (e.g requesting timeline_limit: 1 for
- // this room, then timeline_limit: 50).
- const knownEvents = new Set<string>();
- room.getLiveTimeline()
- .getEvents()
- .forEach((e) => {
- knownEvents.add(e.getId()!);
- });
- // all unknown events BEFORE a known event must be scrollback e.g:
- // D E <-- what we know
- // A B C D E F <-- what we just received
- // means:
- // A B C <-- scrollback
- // D E <-- dupes
- // F <-- new event
- // We bucket events based on if we have seen a known event yet.
- const oldEvents: MatrixEvent[] = [];
- const newEvents: MatrixEvent[] = [];
- let seenKnownEvent = false;
- for (let i = timelineEvents.length - 1; i >= 0; i--) {
- const recvEvent = timelineEvents[i];
- if (knownEvents.has(recvEvent.getId()!)) {
- seenKnownEvent = true;
- continue; // don't include this event, it's a dupe
- }
- if (seenKnownEvent) {
- // old -> new
- oldEvents.push(recvEvent);
- } else {
- // old -> new
- newEvents.unshift(recvEvent);
- }
- }
- timelineEvents = newEvents;
- if (oldEvents.length > 0) {
- // old events are scrollback, insert them now
- room.addEventsToTimeline(oldEvents, true, room.getLiveTimeline(), roomData.prev_batch);
- }
- }
-
- const encrypted = this.client.isRoomEncrypted(room.roomId);
- // we do this first so it's correct when any of the events fire
- if (roomData.notification_count != null) {
- room.setUnreadNotificationCount(NotificationCountType.Total, roomData.notification_count);
- }
-
- if (roomData.highlight_count != null) {
- // We track unread notifications ourselves in encrypted rooms, so don't
- // bother setting it here. We trust our calculations better than the
- // server's for this case, and therefore will assume that our non-zero
- // count is accurate.
- if (!encrypted || (encrypted && room.getUnreadNotificationCount(NotificationCountType.Highlight) <= 0)) {
- room.setUnreadNotificationCount(NotificationCountType.Highlight, roomData.highlight_count);
- }
- }
-
- if (Number.isInteger(roomData.invited_count)) {
- room.currentState.setInvitedMemberCount(roomData.invited_count!);
- }
- if (Number.isInteger(roomData.joined_count)) {
- room.currentState.setJoinedMemberCount(roomData.joined_count!);
- }
-
- if (roomData.invite_state) {
- const inviteStateEvents = mapEvents(this.client, room.roomId, roomData.invite_state);
- this.injectRoomEvents(room, inviteStateEvents);
- if (roomData.initial) {
- room.recalculate();
- this.client.store.storeRoom(room);
- this.client.emit(ClientEvent.Room, room);
- }
- inviteStateEvents.forEach((e) => {
- this.client.emit(ClientEvent.Event, e);
- });
- room.updateMyMembership("invite");
- return;
- }
-
- if (roomData.initial) {
- // set the back-pagination token. Do this *before* adding any
- // events so that clients can start back-paginating.
- room.getLiveTimeline().setPaginationToken(roomData.prev_batch ?? null, EventTimeline.BACKWARDS);
- }
-
- /* TODO
- else if (roomData.limited) {
-
- let limited = true;
-
- // we've got a limited sync, so we *probably* have a gap in the
- // timeline, so should reset. But we might have been peeking or
- // paginating and already have some of the events, in which
- // case we just want to append any subsequent events to the end
- // of the existing timeline.
- //
- // This is particularly important in the case that we already have
- // *all* of the events in the timeline - in that case, if we reset
- // the timeline, we'll end up with an entirely empty timeline,
- // which we'll try to paginate but not get any new events (which
- // will stop us linking the empty timeline into the chain).
- //
- for (let i = timelineEvents.length - 1; i >= 0; i--) {
- const eventId = timelineEvents[i].getId();
- if (room.getTimelineForEvent(eventId)) {
- logger.debug("Already have event " + eventId + " in limited " +
- "sync - not resetting");
- limited = false;
-
- // we might still be missing some of the events before i;
- // we don't want to be adding them to the end of the
- // timeline because that would put them out of order.
- timelineEvents.splice(0, i);
-
- // XXX: there's a problem here if the skipped part of the
- // timeline modifies the state set in stateEvents, because
- // we'll end up using the state from stateEvents rather
- // than the later state from timelineEvents. We probably
- // need to wind stateEvents forward over the events we're
- // skipping.
- break;
- }
- }
-
- if (limited) {
- room.resetLiveTimeline(
- roomData.prev_batch,
- null, // TODO this.syncOpts.canResetEntireTimeline(room.roomId) ? null : syncEventData.oldSyncToken,
- );
-
- // We have to assume any gap in any timeline is
- // reason to stop incrementally tracking notifications and
- // reset the timeline.
- this.client.resetNotifTimelineSet();
- this.registerStateListeners(room);
- }
- } */
-
- this.injectRoomEvents(room, stateEvents, timelineEvents, roomData.num_live);
-
- // we deliberately don't add ephemeral events to the timeline
- room.addEphemeralEvents(ephemeralEvents);
-
- // local fields must be set before any async calls because call site assumes
- // synchronous execution prior to emitting SlidingSyncState.Complete
- room.updateMyMembership("join");
-
- room.recalculate();
- if (roomData.initial) {
- client.store.storeRoom(room);
- client.emit(ClientEvent.Room, room);
- }
-
- // check if any timeline events should bing and add them to the notifEvents array:
- // we'll purge this once we've fully processed the sync response
- this.addNotifications(timelineEvents);
-
- const processRoomEvent = async (e: MatrixEvent): Promise<void> => {
- client.emit(ClientEvent.Event, e);
- if (e.isState() && e.getType() == EventType.RoomEncryption && this.syncOpts.cryptoCallbacks) {
- await this.syncOpts.cryptoCallbacks.onCryptoEvent(room, e);
- }
- };
-
- await utils.promiseMapSeries(stateEvents, processRoomEvent);
- await utils.promiseMapSeries(timelineEvents, processRoomEvent);
- ephemeralEvents.forEach(function (e) {
- client.emit(ClientEvent.Event, e);
- });
-
- // 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
- room.decryptCriticalEvents();
- }
-
- /**
- * Injects events into a room's model.
- * @param stateEventList - A list of state events. This is the state
- * at the *START* of the timeline list if it is supplied.
- * @param timelineEventList - A list of timeline events. Lower index
- * is earlier in time. Higher index is later.
- * @param numLive - the number of events in timelineEventList which just happened,
- * supplied from the server.
- */
- public injectRoomEvents(
- room: Room,
- stateEventList: MatrixEvent[],
- timelineEventList?: MatrixEvent[],
- numLive?: number,
- ): void {
- timelineEventList = timelineEventList || [];
- stateEventList = stateEventList || [];
- numLive = numLive || 0;
-
- // If there are no events in the timeline yet, initialise it with
- // the given state events
- const liveTimeline = room.getLiveTimeline();
- const timelineWasEmpty = liveTimeline.getEvents().length == 0;
- if (timelineWasEmpty) {
- // Passing these events into initialiseState will freeze them, so we need
- // to compute and cache the push actions for them now, otherwise sync dies
- // with an attempt to assign to read only property.
- // XXX: This is pretty horrible and is assuming all sorts of behaviour from
- // these functions that it shouldn't be. We should probably either store the
- // push actions cache elsewhere so we can freeze MatrixEvents, or otherwise
- // find some solution where MatrixEvents are immutable but allow for a cache
- // field.
- for (const ev of stateEventList) {
- this.client.getPushActionsForEvent(ev);
- }
- liveTimeline.initialiseState(stateEventList);
- }
-
- // If the timeline wasn't empty, we process the state events here: they're
- // defined as updates to the state before the start of the timeline, so this
- // starts to roll the state forward.
- // XXX: That's what we *should* do, but this can happen if we were previously
- // peeking in a room, in which case we obviously do *not* want to add the
- // state events here onto the end of the timeline. Historically, the js-sdk
- // has just set these new state events on the old and new state. This seems
- // very wrong because there could be events in the timeline that diverge the
- // state, in which case this is going to leave things out of sync. However,
- // for now I think it;s best to behave the same as the code has done previously.
- if (!timelineWasEmpty) {
- // XXX: As above, don't do this...
- //room.addLiveEvents(stateEventList || []);
- // Do this instead...
- room.oldState.setStateEvents(stateEventList);
- room.currentState.setStateEvents(stateEventList);
- }
-
- // the timeline is broken into 'live' events which just happened and normal timeline events
- // which are still to be appended to the end of the live timeline but happened a while ago.
- // The live events are marked as fromCache=false to ensure that downstream components know
- // this is a live event, not historical (from a remote server cache).
-
- let liveTimelineEvents: MatrixEvent[] = [];
- if (numLive > 0) {
- // last numLive events are live
- liveTimelineEvents = timelineEventList.slice(-1 * numLive);
- // everything else is not live
- timelineEventList = timelineEventList.slice(0, -1 * liveTimelineEvents.length);
- }
-
- // execute the timeline events. This will continue to diverge the current state
- // if the timeline has any state events in it.
- // 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, {
- fromCache: true,
- });
- if (liveTimelineEvents.length > 0) {
- room.addLiveEvents(liveTimelineEvents, {
- fromCache: false,
- });
- }
-
- room.recalculate();
-
- // resolve invites now we have set the latest state
- this.resolveInvites(room);
- }
-
- private resolveInvites(room: Room): void {
- if (!room || !this.opts.resolveInvitesToProfiles) {
- return;
- }
- const client = this.client;
- // For each invited room member we want to give them a displayname/avatar url
- // if they have one (the m.room.member invites don't contain this).
- room.getMembersWithMembership("invite").forEach(function (member) {
- if (member.requestedProfileInfo) return;
- member.requestedProfileInfo = true;
- // try to get a cached copy first.
- const user = client.getUser(member.userId);
- let promise: ReturnType<MatrixClient["getProfileInfo"]>;
- if (user) {
- promise = Promise.resolve({
- avatar_url: user.avatarUrl,
- displayname: user.displayName,
- });
- } else {
- promise = client.getProfileInfo(member.userId);
- }
- promise.then(
- function (info) {
- // slightly naughty by doctoring the invite event but this means all
- // the code paths remain the same between invite/join display name stuff
- // which is a worthy trade-off for some minor pollution.
- const inviteEvent = member.events.member!;
- if (inviteEvent.getContent().membership !== "invite") {
- // between resolving and now they have since joined, so don't clobber
- return;
- }
- inviteEvent.getContent().avatar_url = info.avatar_url;
- inviteEvent.getContent().displayname = info.displayname;
- // fire listeners
- member.setMembershipEvent(inviteEvent, room.currentState);
- },
- function (_err) {
- // OH WELL.
- },
- );
- });
- }
-
- public retryImmediately(): boolean {
- return true;
- }
-
- /**
- * Main entry point. Blocks until stop() is called.
- */
- public async sync(): Promise<void> {
- logger.debug("Sliding sync init loop");
-
- // 1) We need to get push rules so we can check if events should bing as we get
- // them from /sync.
- while (!this.client.isGuest()) {
- try {
- logger.debug("Getting push rules...");
- const result = await this.client.getPushRules();
- logger.debug("Got push rules");
- this.client.pushRules = result;
- break;
- } catch (err) {
- logger.error("Getting push rules failed", err);
- if (this.shouldAbortSync(<MatrixError>err)) {
- return;
- }
- }
- }
-
- // start syncing
- await this.slidingSync.start();
- }
-
- /**
- * Stops the sync object from syncing.
- */
- public stop(): void {
- logger.debug("SyncApi.stop");
- this.slidingSync.stop();
- }
-
- /**
- * Sets the sync state and emits an event to say so
- * @param newState - The new state string
- * @param data - Object of additional data to emit in the event
- */
- private updateSyncState(newState: SyncState, data?: ISyncStateData): void {
- const old = this.syncState;
- this.syncState = newState;
- this.syncStateData = data;
- this.client.emit(ClientEvent.Sync, this.syncState, old, data);
- }
-
- /**
- * Takes a list of timelineEvents and adds and adds to notifEvents
- * as appropriate.
- * This must be called after the room the events belong to has been stored.
- *
- * @param timelineEventList - A list of timeline events. Lower index
- * is earlier in time. Higher index is later.
- */
- private addNotifications(timelineEventList: MatrixEvent[]): void {
- // gather our notifications into this.notifEvents
- if (!this.client.getNotifTimelineSet()) {
- return;
- }
- for (const timelineEvent of timelineEventList) {
- const pushActions = this.client.getPushActionsForEvent(timelineEvent);
- if (pushActions && pushActions.notify && pushActions.tweaks && pushActions.tweaks.highlight) {
- this.notifEvents.push(timelineEvent);
- }
- }
- }
-
- /**
- * Purge any events in the notifEvents array. Used after a /sync has been complete.
- * This should not be called at a per-room scope (e.g in onRoomData) because otherwise the ordering
- * will be messed up e.g room A gets a bing, room B gets a newer bing, but both in the same /sync
- * response. If we purge at a per-room scope then we could process room B before room A leading to
- * room B appearing earlier in the notifications timeline, even though it has the higher origin_server_ts.
- */
- private purgeNotifications(): void {
- this.notifEvents.sort(function (a, b) {
- return a.getTs() - b.getTs();
- });
- this.notifEvents.forEach((event) => {
- this.client.getNotifTimelineSet()?.addLiveEvent(event);
- });
- this.notifEvents = [];
- }
-}
-
-function ensureNameEvent(client: MatrixClient, roomId: string, roomData: MSC3575RoomData): MSC3575RoomData {
- // make sure m.room.name is in required_state if there is a name, replacing anything previously
- // there if need be. This ensures clients transparently 'calculate' the right room name. Native
- // sliding sync clients should just read the "name" field.
- if (!roomData.name) {
- return roomData;
- }
- for (const stateEvent of roomData.required_state) {
- if (stateEvent.type === EventType.RoomName && stateEvent.state_key === "") {
- stateEvent.content = {
- name: roomData.name,
- };
- return roomData;
- }
- }
- roomData.required_state.push({
- event_id: "$fake-sliding-sync-name-event-" + roomId,
- state_key: "",
- type: EventType.RoomName,
- content: {
- name: roomData.name,
- },
- sender: client.getUserId()!,
- origin_server_ts: new Date().getTime(),
- });
- return roomData;
-}
-
-type TaggedEvent = (IStrippedState | IRoomEvent | IStateEvent | IMinimalEvent) & { room_id?: string };
-
-// Helper functions which set up JS SDK structs are below and are identical to the sync v2 counterparts,
-// just outside the class.
-function mapEvents(client: MatrixClient, roomId: string | undefined, events: object[], decrypt = true): MatrixEvent[] {
- const mapper = client.getEventMapper({ decrypt });
- return (events as TaggedEvent[]).map(function (e) {
- e.room_id = roomId;
- return mapper(e);
- });
-}
-
-function processEphemeralEvents(client: MatrixClient, roomId: string, ephEvents: IMinimalEvent[]): void {
- const ephemeralEvents = mapEvents(client, roomId, ephEvents);
- const room = client.getRoom(roomId);
- if (!room) {
- logger.warn("got ephemeral events for room but room doesn't exist on client:", roomId);
- return;
- }
- room.addEphemeralEvents(ephemeralEvents);
- ephemeralEvents.forEach((e) => {
- client.emit(ClientEvent.Event, e);
- });
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/sliding-sync.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/sliding-sync.ts
deleted file mode 100644
index dde5f1b..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/sliding-sync.ts
+++ /dev/null
@@ -1,961 +0,0 @@
-/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { logger } from "./logger";
-import { MatrixClient } from "./client";
-import { IRoomEvent, IStateEvent } from "./sync-accumulator";
-import { TypedEventEmitter } from "./models/typed-event-emitter";
-import { sleep, IDeferred, defer } from "./utils";
-import { HTTPError } from "./http-api";
-
-// /sync requests allow you to set a timeout= but the request may continue
-// beyond that and wedge forever, so we need to track how long we are willing
-// to keep open the connection. This constant is *ADDED* to the timeout= value
-// to determine the max time we're willing to wait.
-const BUFFER_PERIOD_MS = 10 * 1000;
-
-export const MSC3575_WILDCARD = "*";
-export const MSC3575_STATE_KEY_ME = "$ME";
-export const MSC3575_STATE_KEY_LAZY = "$LAZY";
-
-/**
- * Represents a subscription to a room or set of rooms. Controls which events are returned.
- */
-export interface MSC3575RoomSubscription {
- required_state?: string[][];
- timeline_limit?: number;
- include_old_rooms?: MSC3575RoomSubscription;
-}
-
-/**
- * Controls which rooms are returned in a given list.
- */
-export interface MSC3575Filter {
- is_dm?: boolean;
- is_encrypted?: boolean;
- is_invite?: boolean;
- room_name_like?: string;
- room_types?: string[];
- not_room_types?: string[];
- spaces?: string[];
- tags?: string[];
- not_tags?: string[];
-}
-
-/**
- * Represents a list subscription.
- */
-export interface MSC3575List extends MSC3575RoomSubscription {
- ranges: number[][];
- sort?: string[];
- filters?: MSC3575Filter;
- slow_get_all_rooms?: boolean;
-}
-
-/**
- * A complete Sliding Sync request.
- */
-export interface MSC3575SlidingSyncRequest {
- // json body params
- lists?: Record<string, MSC3575List>;
- unsubscribe_rooms?: string[];
- room_subscriptions?: Record<string, MSC3575RoomSubscription>;
- extensions?: object;
- txn_id?: string;
-
- // query params
- pos?: string;
- timeout?: number;
- clientTimeout?: number;
-}
-
-export interface MSC3575RoomData {
- name: string;
- required_state: IStateEvent[];
- timeline: (IRoomEvent | IStateEvent)[];
- notification_count?: number;
- highlight_count?: number;
- joined_count?: number;
- invited_count?: number;
- invite_state?: IStateEvent[];
- initial?: boolean;
- limited?: boolean;
- is_dm?: boolean;
- prev_batch?: string;
- num_live?: number;
-}
-
-interface ListResponse {
- count: number;
- ops: Operation[];
-}
-
-interface BaseOperation {
- op: string;
-}
-
-interface DeleteOperation extends BaseOperation {
- op: "DELETE";
- index: number;
-}
-
-interface InsertOperation extends BaseOperation {
- op: "INSERT";
- index: number;
- room_id: string;
-}
-
-interface InvalidateOperation extends BaseOperation {
- op: "INVALIDATE";
- range: [number, number];
-}
-
-interface SyncOperation extends BaseOperation {
- op: "SYNC";
- range: [number, number];
- room_ids: string[];
-}
-
-type Operation = DeleteOperation | InsertOperation | InvalidateOperation | SyncOperation;
-
-/**
- * A complete Sliding Sync response
- */
-export interface MSC3575SlidingSyncResponse {
- pos: string;
- txn_id?: string;
- lists: Record<string, ListResponse>;
- rooms: Record<string, MSC3575RoomData>;
- extensions: Record<string, object>;
-}
-
-export enum SlidingSyncState {
- /**
- * Fired by SlidingSyncEvent.Lifecycle event immediately before processing the response.
- */
- RequestFinished = "FINISHED",
- /**
- * Fired by SlidingSyncEvent.Lifecycle event immediately after all room data listeners have been
- * invoked, but before list listeners.
- */
- Complete = "COMPLETE",
-}
-
-/**
- * Internal Class. SlidingList represents a single list in sliding sync. The list can have filters,
- * multiple sliding windows, and maintains the index-\>room_id mapping.
- */
-class SlidingList {
- private list!: MSC3575List;
- private isModified?: boolean;
-
- // returned data
- public roomIndexToRoomId: Record<number, string> = {};
- public joinedCount = 0;
-
- /**
- * Construct a new sliding list.
- * @param list - The range, sort and filter values to use for this list.
- */
- public constructor(list: MSC3575List) {
- this.replaceList(list);
- }
-
- /**
- * Mark this list as modified or not. Modified lists will return sticky params with calls to getList.
- * This is useful for the first time the list is sent, or if the list has changed in some way.
- * @param modified - True to mark this list as modified so all sticky parameters will be re-sent.
- */
- public setModified(modified: boolean): void {
- this.isModified = modified;
- }
-
- /**
- * Update the list range for this list. Does not affect modified status as list ranges are non-sticky.
- * @param newRanges - The new ranges for the list
- */
- public updateListRange(newRanges: number[][]): void {
- this.list.ranges = JSON.parse(JSON.stringify(newRanges));
- }
-
- /**
- * Replace list parameters. All fields will be replaced with the new list parameters.
- * @param list - The new list parameters
- */
- public replaceList(list: MSC3575List): void {
- list.filters = list.filters || {};
- list.ranges = list.ranges || [];
- this.list = JSON.parse(JSON.stringify(list));
- this.isModified = true;
-
- // reset values as the join count may be very different (if filters changed) including the rooms
- // (e.g. sort orders or sliding window ranges changed)
-
- // the constantly changing sliding window ranges. Not an array for performance reasons
- // E.g. tracking ranges 0-99, 500-599, we don't want to have a 600 element array
- this.roomIndexToRoomId = {};
- // the total number of joined rooms according to the server, always >= len(roomIndexToRoomId)
- this.joinedCount = 0;
- }
-
- /**
- * Return a copy of the list suitable for a request body.
- * @param forceIncludeAllParams - True to forcibly include all params even if the list
- * hasn't been modified. Callers may want to do this if they are modifying the list prior to calling
- * updateList.
- */
- public getList(forceIncludeAllParams: boolean): MSC3575List {
- let list = {
- ranges: JSON.parse(JSON.stringify(this.list.ranges)),
- };
- if (this.isModified || forceIncludeAllParams) {
- list = JSON.parse(JSON.stringify(this.list));
- }
- return list;
- }
-
- /**
- * Check if a given index is within the list range. This is required even though the /sync API
- * provides explicit updates with index positions because of the following situation:
- * 0 1 2 3 4 5 6 7 8 indexes
- * a b c d e f COMMANDS: SYNC 0 2 a b c; SYNC 6 8 d e f;
- * a b c d _ f COMMAND: DELETE 7;
- * e a b c d f COMMAND: INSERT 0 e;
- * c=3 is wrong as we are not tracking it, ergo we need to see if `i` is in range else drop it
- * @param i - The index to check
- * @returns True if the index is within a sliding window
- */
- public isIndexInRange(i: number): boolean {
- for (const r of this.list.ranges) {
- if (r[0] <= i && i <= r[1]) {
- return true;
- }
- }
- return false;
- }
-}
-
-/**
- * When onResponse extensions should be invoked: before or after processing the main response.
- */
-export enum ExtensionState {
- // Call onResponse before processing the response body. This is useful when your extension is
- // preparing the ground for the response body e.g. processing to-device messages before the
- // encrypted event arrives.
- PreProcess = "ExtState.PreProcess",
- // Call onResponse after processing the response body. This is useful when your extension is
- // decorating data from the client, and you rely on MatrixClient.getRoom returning the Room object
- // e.g. room account data.
- PostProcess = "ExtState.PostProcess",
-}
-
-/**
- * An interface that must be satisfied to register extensions
- */
-export interface Extension<Req extends {}, Res extends {}> {
- /**
- * The extension name to go under 'extensions' in the request body.
- * @returns The JSON key.
- */
- name(): string;
- /**
- * A function which is called when the request JSON is being formed.
- * Returns the data to insert under this key.
- * @param isInitial - True when this is part of the initial request (send sticky params)
- * @returns The request JSON to send.
- */
- onRequest(isInitial: boolean): Req | undefined;
- /**
- * A function which is called when there is response JSON under this extension.
- * @param data - The response JSON under the extension name.
- */
- onResponse(data: Res): void;
- /**
- * Controls when onResponse should be called.
- * @returns The state when it should be called.
- */
- when(): ExtensionState;
-}
-
-/**
- * Events which can be fired by the SlidingSync class. These are designed to provide different levels
- * of information when processing sync responses.
- * - RoomData: concerns rooms, useful for SlidingSyncSdk to update its knowledge of rooms.
- * - Lifecycle: concerns callbacks at various well-defined points in the sync process.
- * - List: concerns lists, useful for UI layers to re-render room lists.
- * Specifically, the order of event invocation is:
- * - Lifecycle (state=RequestFinished)
- * - RoomData (N times)
- * - Lifecycle (state=Complete)
- * - List (at most once per list)
- */
-export enum SlidingSyncEvent {
- /**
- * This event fires when there are updates for a room. Fired as and when rooms are encountered
- * in the response.
- */
- RoomData = "SlidingSync.RoomData",
- /**
- * This event fires at various points in the /sync loop lifecycle.
- * - SlidingSyncState.RequestFinished: Fires after we receive a valid response but before the
- * response has been processed. Perform any pre-process steps here. If there was a problem syncing,
- * `err` will be set (e.g network errors).
- * - SlidingSyncState.Complete: Fires after all SlidingSyncEvent.RoomData have been fired but before
- * SlidingSyncEvent.List.
- */
- Lifecycle = "SlidingSync.Lifecycle",
- /**
- * This event fires whenever there has been a change to this list index. It fires exactly once
- * per list, even if there were multiple operations for the list.
- * It fires AFTER Lifecycle and RoomData events.
- */
- List = "SlidingSync.List",
-}
-
-export type SlidingSyncEventHandlerMap = {
- [SlidingSyncEvent.RoomData]: (roomId: string, roomData: MSC3575RoomData) => void;
- [SlidingSyncEvent.Lifecycle]: (
- state: SlidingSyncState,
- resp: MSC3575SlidingSyncResponse | null,
- err?: Error,
- ) => void;
- [SlidingSyncEvent.List]: (listKey: string, joinedCount: number, roomIndexToRoomId: Record<number, string>) => void;
-};
-
-/**
- * SlidingSync is a high-level data structure which controls the majority of sliding sync.
- * It has no hooks into JS SDK except for needing a MatrixClient to perform the HTTP request.
- * This means this class (and everything it uses) can be used in isolation from JS SDK if needed.
- * To hook this up with the JS SDK, you need to use SlidingSyncSdk.
- */
-export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSyncEventHandlerMap> {
- private lists: Map<string, SlidingList>;
- private listModifiedCount = 0;
- private terminated = false;
- // flag set when resend() is called because we cannot rely on detecting AbortError in JS SDK :(
- private needsResend = false;
- // the txn_id to send with the next request.
- private txnId: string | null = null;
- // a list (in chronological order of when they were sent) of objects containing the txn ID and
- // a defer to resolve/reject depending on whether they were successfully sent or not.
- private txnIdDefers: (IDeferred<string> & { txnId: string })[] = [];
- // map of extension name to req/resp handler
- private extensions: Record<string, Extension<any, any>> = {};
-
- private desiredRoomSubscriptions = new Set<string>(); // the *desired* room subscriptions
- private confirmedRoomSubscriptions = new Set<string>();
-
- // map of custom subscription name to the subscription
- private customSubscriptions: Map<string, MSC3575RoomSubscription> = new Map();
- // map of room ID to custom subscription name
- private roomIdToCustomSubscription: Map<string, string> = new Map();
-
- private pendingReq?: Promise<MSC3575SlidingSyncResponse>;
- private abortController?: AbortController;
-
- /**
- * Create a new sliding sync instance
- * @param proxyBaseUrl - The base URL of the sliding sync proxy
- * @param lists - The lists to use for sliding sync.
- * @param roomSubscriptionInfo - The params to use for room subscriptions.
- * @param client - The client to use for /sync calls.
- * @param timeoutMS - The number of milliseconds to wait for a response.
- */
- public constructor(
- private readonly proxyBaseUrl: string,
- lists: Map<string, MSC3575List>,
- private roomSubscriptionInfo: MSC3575RoomSubscription,
- private readonly client: MatrixClient,
- private readonly timeoutMS: number,
- ) {
- super();
- this.lists = new Map<string, SlidingList>();
- lists.forEach((list, key) => {
- this.lists.set(key, new SlidingList(list));
- });
- }
-
- /**
- * Add a custom room subscription, referred to by an arbitrary name. If a subscription with this
- * name already exists, it is replaced. No requests are sent by calling this method.
- * @param name - The name of the subscription. Only used to reference this subscription in
- * useCustomSubscription.
- * @param sub - The subscription information.
- */
- public addCustomSubscription(name: string, sub: MSC3575RoomSubscription): void {
- if (this.customSubscriptions.has(name)) {
- logger.warn(`addCustomSubscription: ${name} already exists as a custom subscription, ignoring.`);
- return;
- }
- this.customSubscriptions.set(name, sub);
- }
-
- /**
- * Use a custom subscription previously added via addCustomSubscription. No requests are sent
- * by calling this method. Use modifyRoomSubscriptions to resend subscription information.
- * @param roomId - The room to use the subscription in.
- * @param name - The name of the subscription. If this name is unknown, the default subscription
- * will be used.
- */
- public useCustomSubscription(roomId: string, name: string): void {
- // We already know about this custom subscription, as it is immutable,
- // we don't need to unconfirm the subscription.
- if (this.roomIdToCustomSubscription.get(roomId) === name) {
- return;
- }
- this.roomIdToCustomSubscription.set(roomId, name);
- // unconfirm this subscription so a resend() will send it up afresh.
- this.confirmedRoomSubscriptions.delete(roomId);
- }
-
- /**
- * Get the room index data for a list.
- * @param key - The list key
- * @returns The list data which contains the rooms in this list
- */
- public getListData(key: string): { joinedCount: number; roomIndexToRoomId: Record<number, string> } | null {
- const data = this.lists.get(key);
- if (!data) {
- return null;
- }
- return {
- joinedCount: data.joinedCount,
- roomIndexToRoomId: Object.assign({}, data.roomIndexToRoomId),
- };
- }
-
- /**
- * Get the full request list parameters for a list index. This function is provided for callers to use
- * in conjunction with setList to update fields on an existing list.
- * @param key - The list key to get the params for.
- * @returns A copy of the list params or undefined.
- */
- public getListParams(key: string): MSC3575List | null {
- const params = this.lists.get(key);
- if (!params) {
- return null;
- }
- return params.getList(true);
- }
-
- /**
- * Set new ranges for an existing list. Calling this function when _only_ the ranges have changed
- * is more efficient than calling setList(index,list) as this function won't resend sticky params,
- * whereas setList always will.
- * @param key - The list key to modify
- * @param ranges - The new ranges to apply.
- * @returns A promise which resolves to the transaction ID when it has been received down sync
- * (or rejects with the transaction ID if the action was not applied e.g the request was cancelled
- * immediately after sending, in which case the action will be applied in the subsequent request)
- */
- public setListRanges(key: string, ranges: number[][]): Promise<string> {
- const list = this.lists.get(key);
- if (!list) {
- return Promise.reject(new Error("no list with key " + key));
- }
- list.updateListRange(ranges);
- return this.resend();
- }
-
- /**
- * Add or replace a list. Calling this function will interrupt the /sync request to resend new
- * lists.
- * @param key - The key to modify
- * @param list - The new list parameters.
- * @returns A promise which resolves to the transaction ID when it has been received down sync
- * (or rejects with the transaction ID if the action was not applied e.g the request was cancelled
- * immediately after sending, in which case the action will be applied in the subsequent request)
- */
- public setList(key: string, list: MSC3575List): Promise<string> {
- const existingList = this.lists.get(key);
- if (existingList) {
- existingList.replaceList(list);
- this.lists.set(key, existingList);
- } else {
- this.lists.set(key, new SlidingList(list));
- }
- this.listModifiedCount += 1;
- return this.resend();
- }
-
- /**
- * Get the room subscriptions for the sync API.
- * @returns A copy of the desired room subscriptions.
- */
- public getRoomSubscriptions(): Set<string> {
- return new Set(Array.from(this.desiredRoomSubscriptions));
- }
-
- /**
- * Modify the room subscriptions for the sync API. Calling this function will interrupt the
- * /sync request to resend new subscriptions. If the /sync stream has not started, this will
- * prepare the room subscriptions for when start() is called.
- * @param s - The new desired room subscriptions.
- * @returns A promise which resolves to the transaction ID when it has been received down sync
- * (or rejects with the transaction ID if the action was not applied e.g the request was cancelled
- * immediately after sending, in which case the action will be applied in the subsequent request)
- */
- public modifyRoomSubscriptions(s: Set<string>): Promise<string> {
- this.desiredRoomSubscriptions = s;
- return this.resend();
- }
-
- /**
- * Modify which events to retrieve for room subscriptions. Invalidates all room subscriptions
- * such that they will be sent up afresh.
- * @param rs - The new room subscription fields to fetch.
- * @returns A promise which resolves to the transaction ID when it has been received down sync
- * (or rejects with the transaction ID if the action was not applied e.g the request was cancelled
- * immediately after sending, in which case the action will be applied in the subsequent request)
- */
- public modifyRoomSubscriptionInfo(rs: MSC3575RoomSubscription): Promise<string> {
- this.roomSubscriptionInfo = rs;
- this.confirmedRoomSubscriptions = new Set<string>();
- return this.resend();
- }
-
- /**
- * Register an extension to send with the /sync request.
- * @param ext - The extension to register.
- */
- public registerExtension(ext: Extension<any, any>): void {
- if (this.extensions[ext.name()]) {
- throw new Error(`registerExtension: ${ext.name()} already exists as an extension`);
- }
- this.extensions[ext.name()] = ext;
- }
-
- private getExtensionRequest(isInitial: boolean): Record<string, object | undefined> {
- const ext: Record<string, object | undefined> = {};
- Object.keys(this.extensions).forEach((extName) => {
- ext[extName] = this.extensions[extName].onRequest(isInitial);
- });
- return ext;
- }
-
- private onPreExtensionsResponse(ext: Record<string, object>): void {
- Object.keys(ext).forEach((extName) => {
- if (this.extensions[extName].when() == ExtensionState.PreProcess) {
- this.extensions[extName].onResponse(ext[extName]);
- }
- });
- }
-
- private onPostExtensionsResponse(ext: Record<string, object>): void {
- Object.keys(ext).forEach((extName) => {
- if (this.extensions[extName].when() == ExtensionState.PostProcess) {
- this.extensions[extName].onResponse(ext[extName]);
- }
- });
- }
-
- /**
- * Invoke all attached room data listeners.
- * @param roomId - The room which received some data.
- * @param roomData - The raw sliding sync response JSON.
- */
- private invokeRoomDataListeners(roomId: string, roomData: MSC3575RoomData): void {
- if (!roomData.required_state) {
- roomData.required_state = [];
- }
- if (!roomData.timeline) {
- roomData.timeline = [];
- }
- this.emit(SlidingSyncEvent.RoomData, roomId, roomData);
- }
-
- /**
- * Invoke all attached lifecycle listeners.
- * @param state - The Lifecycle state
- * @param resp - The raw sync response JSON
- * @param err - Any error that occurred when making the request e.g. network errors.
- */
- private invokeLifecycleListeners(
- state: SlidingSyncState,
- resp: MSC3575SlidingSyncResponse | null,
- err?: Error,
- ): void {
- this.emit(SlidingSyncEvent.Lifecycle, state, resp, err);
- }
-
- private shiftRight(listKey: string, hi: number, low: number): void {
- const list = this.lists.get(listKey);
- if (!list) {
- return;
- }
- // l h
- // 0,1,2,3,4 <- before
- // 0,1,2,2,3 <- after, hi is deleted and low is duplicated
- for (let i = hi; i > low; i--) {
- if (list.isIndexInRange(i)) {
- list.roomIndexToRoomId[i] = list.roomIndexToRoomId[i - 1];
- }
- }
- }
-
- private shiftLeft(listKey: string, hi: number, low: number): void {
- const list = this.lists.get(listKey);
- if (!list) {
- return;
- }
- // l h
- // 0,1,2,3,4 <- before
- // 0,1,3,4,4 <- after, low is deleted and hi is duplicated
- for (let i = low; i < hi; i++) {
- if (list.isIndexInRange(i)) {
- list.roomIndexToRoomId[i] = list.roomIndexToRoomId[i + 1];
- }
- }
- }
-
- private removeEntry(listKey: string, index: number): void {
- const list = this.lists.get(listKey);
- if (!list) {
- return;
- }
- // work out the max index
- let max = -1;
- for (const n in list.roomIndexToRoomId) {
- if (Number(n) > max) {
- max = Number(n);
- }
- }
- if (max < 0 || index > max) {
- return;
- }
- // Everything higher than the gap needs to be shifted left.
- this.shiftLeft(listKey, max, index);
- delete list.roomIndexToRoomId[max];
- }
-
- private addEntry(listKey: string, index: number): void {
- const list = this.lists.get(listKey);
- if (!list) {
- return;
- }
- // work out the max index
- let max = -1;
- for (const n in list.roomIndexToRoomId) {
- if (Number(n) > max) {
- max = Number(n);
- }
- }
- if (max < 0 || index > max) {
- return;
- }
- // Everything higher than the gap needs to be shifted right, +1 so we don't delete the highest element
- this.shiftRight(listKey, max + 1, index);
- }
-
- private processListOps(list: ListResponse, listKey: string): void {
- let gapIndex = -1;
- const listData = this.lists.get(listKey);
- if (!listData) {
- return;
- }
- list.ops.forEach((op: Operation) => {
- if (!listData) {
- return;
- }
- switch (op.op) {
- case "DELETE": {
- logger.debug("DELETE", listKey, op.index, ";");
- delete listData.roomIndexToRoomId[op.index];
- if (gapIndex !== -1) {
- // we already have a DELETE operation to process, so process it.
- this.removeEntry(listKey, gapIndex);
- }
- gapIndex = op.index;
- break;
- }
- case "INSERT": {
- logger.debug("INSERT", listKey, op.index, op.room_id, ";");
- if (listData.roomIndexToRoomId[op.index]) {
- // something is in this space, shift items out of the way
- if (gapIndex < 0) {
- // we haven't been told where to shift from, so make way for a new room entry.
- this.addEntry(listKey, op.index);
- } else if (gapIndex > op.index) {
- // the gap is further down the list, shift every element to the right
- // starting at the gap so we can just shift each element in turn:
- // [A,B,C,_] gapIndex=3, op.index=0
- // [A,B,C,C] i=3
- // [A,B,B,C] i=2
- // [A,A,B,C] i=1
- // Terminate. We'll assign into op.index next.
- this.shiftRight(listKey, gapIndex, op.index);
- } else if (gapIndex < op.index) {
- // the gap is further up the list, shift every element to the left
- // starting at the gap so we can just shift each element in turn
- this.shiftLeft(listKey, op.index, gapIndex);
- }
- }
- // forget the gap, we don't need it anymore. This is outside the check for
- // a room being present in this index position because INSERTs always universally
- // forget the gap, not conditionally based on the presence of a room in the INSERT
- // position. Without this, DELETE 0; INSERT 0; would do the wrong thing.
- gapIndex = -1;
- listData.roomIndexToRoomId[op.index] = op.room_id;
- break;
- }
- case "INVALIDATE": {
- const startIndex = op.range[0];
- for (let i = startIndex; i <= op.range[1]; i++) {
- delete listData.roomIndexToRoomId[i];
- }
- logger.debug("INVALIDATE", listKey, op.range[0], op.range[1], ";");
- break;
- }
- case "SYNC": {
- const startIndex = op.range[0];
- for (let i = startIndex; i <= op.range[1]; i++) {
- const roomId = op.room_ids[i - startIndex];
- if (!roomId) {
- break; // we are at the end of list
- }
- listData.roomIndexToRoomId[i] = roomId;
- }
- logger.debug("SYNC", listKey, op.range[0], op.range[1], (op.room_ids || []).join(" "), ";");
- break;
- }
- }
- });
- if (gapIndex !== -1) {
- // we already have a DELETE operation to process, so process it
- // Everything higher than the gap needs to be shifted left.
- this.removeEntry(listKey, gapIndex);
- }
- }
-
- /**
- * Resend a Sliding Sync request. Used when something has changed in the request. Resolves with
- * the transaction ID of this request on success. Rejects with the transaction ID of this request
- * on failure.
- */
- public resend(): Promise<string> {
- if (this.needsResend && this.txnIdDefers.length > 0) {
- // we already have a resend queued, so just return the same promise
- return this.txnIdDefers[this.txnIdDefers.length - 1].promise;
- }
- this.needsResend = true;
- this.txnId = this.client.makeTxnId();
- const d = defer<string>();
- this.txnIdDefers.push({
- ...d,
- txnId: this.txnId,
- });
- this.abortController?.abort();
- this.abortController = new AbortController();
- return d.promise;
- }
-
- private resolveTransactionDefers(txnId?: string): void {
- if (!txnId) {
- return;
- }
- // find the matching index
- let txnIndex = -1;
- for (let i = 0; i < this.txnIdDefers.length; i++) {
- if (this.txnIdDefers[i].txnId === txnId) {
- txnIndex = i;
- break;
- }
- }
- if (txnIndex === -1) {
- // this shouldn't happen; we shouldn't be seeing txn_ids for things we don't know about,
- // whine about it.
- logger.warn(`resolveTransactionDefers: seen ${txnId} but it isn't a pending txn, ignoring.`);
- return;
- }
- // This list is sorted in time, so if the input txnId ACKs in the middle of this array,
- // then everything before it that hasn't been ACKed yet never will and we should reject them.
- for (let i = 0; i < txnIndex; i++) {
- this.txnIdDefers[i].reject(this.txnIdDefers[i].txnId);
- }
- this.txnIdDefers[txnIndex].resolve(txnId);
- // clear out settled promises, including the one we resolved.
- this.txnIdDefers = this.txnIdDefers.slice(txnIndex + 1);
- }
-
- /**
- * Stop syncing with the server.
- */
- public stop(): void {
- this.terminated = true;
- this.abortController?.abort();
- // remove all listeners so things can be GC'd
- this.removeAllListeners(SlidingSyncEvent.Lifecycle);
- this.removeAllListeners(SlidingSyncEvent.List);
- this.removeAllListeners(SlidingSyncEvent.RoomData);
- }
-
- /**
- * Re-setup this connection e.g in the event of an expired session.
- */
- private resetup(): void {
- logger.warn("SlidingSync: resetting connection info");
- // any pending txn ID defers will be forgotten already by the server, so clear them out
- this.txnIdDefers.forEach((d) => {
- d.reject(d.txnId);
- });
- this.txnIdDefers = [];
- // resend sticky params and de-confirm all subscriptions
- this.lists.forEach((l) => {
- l.setModified(true);
- });
- this.confirmedRoomSubscriptions = new Set<string>(); // leave desired ones alone though!
- // reset the connection as we might be wedged
- this.needsResend = true;
- this.abortController?.abort();
- this.abortController = new AbortController();
- }
-
- /**
- * Start syncing with the server. Blocks until stopped.
- */
- public async start(): Promise<void> {
- this.abortController = new AbortController();
-
- let currentPos: string | undefined;
- while (!this.terminated) {
- this.needsResend = false;
- let doNotUpdateList = false;
- let resp: MSC3575SlidingSyncResponse | undefined;
- try {
- const listModifiedCount = this.listModifiedCount;
- const reqLists: Record<string, MSC3575List> = {};
- this.lists.forEach((l: SlidingList, key: string) => {
- reqLists[key] = l.getList(false);
- });
- const reqBody: MSC3575SlidingSyncRequest = {
- lists: reqLists,
- pos: currentPos,
- timeout: this.timeoutMS,
- clientTimeout: this.timeoutMS + BUFFER_PERIOD_MS,
- extensions: this.getExtensionRequest(currentPos === undefined),
- };
- // check if we are (un)subscribing to a room and modify request this one time for it
- const newSubscriptions = difference(this.desiredRoomSubscriptions, this.confirmedRoomSubscriptions);
- const unsubscriptions = difference(this.confirmedRoomSubscriptions, this.desiredRoomSubscriptions);
- if (unsubscriptions.size > 0) {
- reqBody.unsubscribe_rooms = Array.from(unsubscriptions);
- }
- if (newSubscriptions.size > 0) {
- reqBody.room_subscriptions = {};
- for (const roomId of newSubscriptions) {
- const customSubName = this.roomIdToCustomSubscription.get(roomId);
- let sub = this.roomSubscriptionInfo;
- if (customSubName && this.customSubscriptions.has(customSubName)) {
- sub = this.customSubscriptions.get(customSubName)!;
- }
- reqBody.room_subscriptions[roomId] = sub;
- }
- }
- if (this.txnId) {
- reqBody.txn_id = this.txnId;
- this.txnId = null;
- }
- this.pendingReq = this.client.slidingSync(reqBody, this.proxyBaseUrl, this.abortController.signal);
- resp = await this.pendingReq;
- currentPos = resp.pos;
- // update what we think we're subscribed to.
- for (const roomId of newSubscriptions) {
- this.confirmedRoomSubscriptions.add(roomId);
- }
- for (const roomId of unsubscriptions) {
- this.confirmedRoomSubscriptions.delete(roomId);
- }
- if (listModifiedCount !== this.listModifiedCount) {
- // the lists have been modified whilst we were waiting for 'await' to return, but the abort()
- // call did nothing. It is NOT SAFE to modify the list array now. We'll process the response but
- // not update list pointers.
- logger.debug("list modified during await call, not updating list");
- doNotUpdateList = true;
- }
- // mark all these lists as having been sent as sticky so we don't keep sending sticky params
- this.lists.forEach((l) => {
- l.setModified(false);
- });
- // set default empty values so we don't need to null check
- resp.lists = resp.lists || {};
- resp.rooms = resp.rooms || {};
- resp.extensions = resp.extensions || {};
- Object.keys(resp.lists).forEach((key: string) => {
- const list = this.lists.get(key);
- if (!list || !resp) {
- return;
- }
- list.joinedCount = resp.lists[key].count;
- });
- this.invokeLifecycleListeners(SlidingSyncState.RequestFinished, resp);
- } catch (err) {
- if ((<HTTPError>err).httpStatus) {
- this.invokeLifecycleListeners(SlidingSyncState.RequestFinished, null, <Error>err);
- if ((<HTTPError>err).httpStatus === 400) {
- // session probably expired TODO: assign an errcode
- // so drop state and re-request
- this.resetup();
- currentPos = undefined;
- await sleep(50); // in case the 400 was for something else; don't tightloop
- continue;
- } // else fallthrough to generic error handling
- } else if (this.needsResend || (<Error>err).name === "AbortError") {
- continue; // don't sleep as we caused this error by abort()ing the request.
- }
- logger.error(err);
- await sleep(5000);
- }
- if (!resp) {
- continue;
- }
- this.onPreExtensionsResponse(resp.extensions);
-
- Object.keys(resp.rooms).forEach((roomId) => {
- this.invokeRoomDataListeners(roomId, resp!.rooms[roomId]);
- });
-
- const listKeysWithUpdates: Set<string> = new Set();
- if (!doNotUpdateList) {
- for (const [key, list] of Object.entries(resp.lists)) {
- list.ops = list.ops || [];
- if (list.ops.length > 0) {
- listKeysWithUpdates.add(key);
- }
- this.processListOps(list, key);
- }
- }
- this.invokeLifecycleListeners(SlidingSyncState.Complete, resp);
- this.onPostExtensionsResponse(resp.extensions);
- listKeysWithUpdates.forEach((listKey: string) => {
- const list = this.lists.get(listKey);
- if (!list) {
- return;
- }
- this.emit(SlidingSyncEvent.List, listKey, list.joinedCount, Object.assign({}, list.roomIndexToRoomId));
- });
-
- this.resolveTransactionDefers(resp.txn_id);
- }
- }
-}
-
-const difference = (setA: Set<string>, setB: Set<string>): Set<string> => {
- const diff = new Set(setA);
- for (const elem of setB) {
- diff.delete(elem);
- }
- return diff;
-};
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/index.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/index.ts
deleted file mode 100644
index 650dd9a..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/index.ts
+++ /dev/null
@@ -1,248 +0,0 @@
-/*
-Copyright 2015 - 2021 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 { EventType } from "../@types/event";
-import { Room } from "../models/room";
-import { User } from "../models/user";
-import { IEvent, 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";
-import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage";
-import { EventEmitterEvents } from "../models/typed-event-emitter";
-
-export interface ISavedSync {
- nextBatch: string;
- roomsData: IRooms;
- accountData: IMinimalEvent[];
-}
-
-/**
- * A store for most of the data js-sdk needs to store, apart from crypto data
- */
-export interface IStore {
- readonly accountData: Map<string, MatrixEvent>; // type : content
-
- // XXX: The indexeddb store exposes a non-standard emitter for:
- // "degraded" event for when it falls back to being a memory store due to errors.
- // "closed" event for when the database closes unexpectedly
- on?: (event: EventEmitterEvents | "degraded" | "closed", handler: (...args: any[]) => void) => void;
-
- /** @returns whether or not the database was newly created in this session. */
- isNewlyCreated(): Promise<boolean>;
-
- /**
- * Get the sync token.
- */
- getSyncToken(): string | null;
-
- /**
- * Set the sync token.
- */
- setSyncToken(token: string): void;
-
- /**
- * Store the given room.
- * @param room - The room to be stored. All properties must be stored.
- */
- storeRoom(room: Room): void;
-
- /**
- * Retrieve a room by its' room ID.
- * @param roomId - The room ID.
- * @returns The room or null.
- */
- getRoom(roomId: string): Room | null;
-
- /**
- * Retrieve all known rooms.
- * @returns A list of rooms, which may be empty.
- */
- getRooms(): Room[];
-
- /**
- * Permanently delete a room.
- */
- removeRoom(roomId: string): void;
-
- /**
- * Retrieve a summary of all the rooms.
- * @returns A summary of each room.
- */
- getRoomSummaries(): RoomSummary[];
-
- /**
- * Store a User.
- * @param user - The user to store.
- */
- storeUser(user: User): void;
-
- /**
- * Retrieve a User by its' user ID.
- * @param userId - The user ID.
- * @returns The user or null.
- */
- getUser(userId: string): User | null;
-
- /**
- * Retrieve all known users.
- * @returns A list of users, which may be empty.
- */
- getUsers(): User[];
-
- /**
- * Retrieve scrollback for this room.
- * @param room - The matrix room
- * @param limit - The max number of old events to retrieve.
- * @returns An array of objects which will be at most 'limit'
- * length and at least 0. The objects are the raw event JSON.
- */
- scrollback(room: Room, limit: number): MatrixEvent[];
-
- /**
- * Store events for a room.
- * @param room - The room to store events for.
- * @param events - The events to store.
- * @param token - The token associated with these events.
- * @param toStart - True if these are paginated results.
- */
- storeEvents(room: Room, events: MatrixEvent[], token: string | null, toStart: boolean): void;
-
- /**
- * Store a filter.
- */
- storeFilter(filter: Filter): void;
-
- /**
- * Retrieve a filter.
- * @returns A filter or null.
- */
- getFilter(userId: string, filterId: string): Filter | null;
-
- /**
- * Retrieve a filter ID with the given name.
- * @param filterName - The filter name.
- * @returns The filter ID or null.
- */
- getFilterIdByName(filterName: string): string | null;
-
- /**
- * Set a filter name to ID mapping.
- */
- setFilterIdByName(filterName: string, filterId?: string): void;
-
- /**
- * Store user-scoped account data events
- * @param events - The events to store.
- */
- storeAccountDataEvents(events: MatrixEvent[]): void;
-
- /**
- * Get account data event by event type
- * @param eventType - The event type being queried
- */
- getAccountData(eventType: EventType | string): MatrixEvent | undefined;
-
- /**
- * setSyncData does nothing as there is no backing data store.
- *
- * @param syncData - The sync data
- * @returns An immediately resolved promise.
- */
- setSyncData(syncData: ISyncResponse): Promise<void>;
-
- /**
- * We never want to save because we have nothing to save to.
- *
- * @returns If the store wants to save
- */
- wantsSave(): boolean;
-
- /**
- * Save does nothing as there is no backing data store.
- */
- save(force?: boolean): void;
-
- /**
- * Startup does nothing.
- * @returns An immediately resolved promise.
- */
- startup(): Promise<void>;
-
- /**
- * @returns Promise which resolves with a sync response to restore the
- * client state to where it was at the last save, or null if there
- * is no saved sync data.
- */
- getSavedSync(): Promise<ISavedSync | null>;
-
- /**
- * @returns If there is a saved sync, the nextBatch token
- * for this sync, otherwise null.
- */
- getSavedSyncToken(): Promise<string | null>;
-
- /**
- * Delete all data from this store. Does nothing since this store
- * doesn't store anything.
- * @returns An immediately resolved promise.
- */
- deleteAllData(): Promise<void>;
-
- /**
- * Returns the out-of-band membership events for this room that
- * were previously loaded.
- * @returns the events, potentially an empty array if OOB loading didn't yield any new members
- * @returns in case the members for this room haven't been stored yet
- */
- getOutOfBandMembers(roomId: string): Promise<IStateEventWithRoomId[] | null>;
-
- /**
- * Stores the out-of-band membership events for this room. Note that
- * it still makes sense to store an empty array as the OOB status for the room is
- * marked as fetched, and getOutOfBandMembers will return an empty array instead of null
- * @param membershipEvents - the membership events to store
- * @returns when all members have been stored
- */
- setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void>;
-
- clearOutOfBandMembers(roomId: string): Promise<void>;
-
- getClientOptions(): Promise<IStartClientOpts | undefined>;
-
- storeClientOptions(options: IStartClientOpts): Promise<void>;
-
- getPendingEvents(roomId: string): Promise<Partial<IEvent>[]>;
-
- setPendingEvents(roomId: string, events: Partial<IEvent>[]): Promise<void>;
-
- /**
- * Stores batches of outgoing to-device messages
- */
- saveToDeviceBatches(batch: ToDeviceBatchWithTxnId[]): Promise<void>;
-
- /**
- * Fetches the oldest batch of to-device messages in the queue
- */
- getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch | null>;
-
- /**
- * Removes a specific batch of to-device messages from the queue
- */
- removeToDeviceBatch(id: number): Promise<void>;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-backend.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-backend.ts
deleted file mode 100644
index 008867d..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-backend.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
-Copyright 2021 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 { ISavedSync } from "./index";
-import { IEvent, IStateEventWithRoomId, IStoredClientOpts, ISyncResponse } from "../matrix";
-import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage";
-
-export interface IIndexedDBBackend {
- connect(onClose?: () => void): Promise<void>;
- syncToDatabase(userTuples: UserTuple[]): Promise<void>;
- isNewlyCreated(): Promise<boolean>;
- setSyncData(syncData: ISyncResponse): Promise<void>;
- getSavedSync(): Promise<ISavedSync | null>;
- getNextBatchToken(): Promise<string>;
- clearDatabase(): 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<IStoredClientOpts | undefined>;
- storeClientOptions(options: IStoredClientOpts): Promise<void>;
- saveToDeviceBatches(batches: ToDeviceBatchWithTxnId[]): Promise<void>;
- getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch | null>;
- removeToDeviceBatch(id: number): Promise<void>;
-}
-
-export type UserTuple = [userId: string, presenceEvent: Partial<IEvent>];
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-local-backend.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-local-backend.ts
deleted file mode 100644
index 80fed44..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-local-backend.ts
+++ /dev/null
@@ -1,597 +0,0 @@
-/*
-Copyright 2017 - 2021 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 { IMinimalEvent, ISyncData, ISyncResponse, SyncAccumulator } from "../sync-accumulator";
-import * as utils from "../utils";
-import * as IndexedDBHelpers from "../indexeddb-helpers";
-import { logger } from "../logger";
-import { IStateEventWithRoomId, IStoredClientOpts } from "../matrix";
-import { ISavedSync } from "./index";
-import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend";
-import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage";
-
-type DbMigration = (db: IDBDatabase) => void;
-const DB_MIGRATIONS: DbMigration[] = [
- (db): void => {
- // Make user store, clobber based on user ID. (userId property of User objects)
- db.createObjectStore("users", { keyPath: ["userId"] });
-
- // Make account data store, clobber based on event type.
- // (event.type property of MatrixEvent objects)
- db.createObjectStore("accountData", { keyPath: ["type"] });
-
- // Make /sync store (sync tokens, room data, etc), always clobber (const key).
- db.createObjectStore("sync", { keyPath: ["clobber"] });
- },
- (db): void => {
- const oobMembersStore = db.createObjectStore("oob_membership_events", {
- keyPath: ["room_id", "state_key"],
- });
- oobMembersStore.createIndex("room", "room_id");
- },
- (db): void => {
- db.createObjectStore("client_options", { keyPath: ["clobber"] });
- },
- (db): void => {
- db.createObjectStore("to_device_queue", { autoIncrement: true });
- },
- // Expand as needed.
-];
-const VERSION = DB_MIGRATIONS.length;
-
-/**
- * Helper method to collect results from a Cursor and promiseify it.
- * @param store - The store to perform openCursor on.
- * @param keyRange - Optional key range to apply on the cursor.
- * @param resultMapper - A function which is repeatedly called with a
- * Cursor.
- * Return the data you want to keep.
- * @returns Promise which resolves to an array of whatever you returned from
- * resultMapper.
- */
-function selectQuery<T>(
- store: IDBObjectStore,
- keyRange: IDBKeyRange | IDBValidKey | undefined,
- resultMapper: (cursor: IDBCursorWithValue) => T,
-): Promise<T[]> {
- const query = store.openCursor(keyRange);
- return new Promise((resolve, reject) => {
- const results: T[] = [];
- query.onerror = (): void => {
- reject(new Error("Query failed: " + query.error));
- };
- // collect results
- query.onsuccess = (): void => {
- const cursor = query.result;
- if (!cursor) {
- resolve(results);
- return; // end of results
- }
- results.push(resultMapper(cursor));
- cursor.continue();
- };
- });
-}
-
-function txnAsPromise(txn: IDBTransaction): Promise<Event> {
- return new Promise((resolve, reject) => {
- txn.oncomplete = function (event): void {
- resolve(event);
- };
- txn.onerror = function (): void {
- reject(txn.error);
- };
- });
-}
-
-function reqAsEventPromise(req: IDBRequest): Promise<Event> {
- return new Promise((resolve, reject) => {
- req.onsuccess = function (event): void {
- resolve(event);
- };
- req.onerror = function (): void {
- reject(req.error);
- };
- });
-}
-
-function reqAsPromise(req: IDBRequest): Promise<IDBRequest> {
- return new Promise((resolve, reject) => {
- req.onsuccess = (): void => resolve(req);
- req.onerror = (err): void => reject(err);
- });
-}
-
-function reqAsCursorPromise<T>(req: IDBRequest<T>): Promise<T> {
- return reqAsEventPromise(req).then((event) => req.result);
-}
-
-export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
- public static exists(indexedDB: IDBFactory, dbName: string): Promise<boolean> {
- dbName = "matrix-js-sdk:" + (dbName || "default");
- return IndexedDBHelpers.exists(indexedDB, dbName);
- }
-
- private readonly dbName: string;
- private readonly syncAccumulator: SyncAccumulator;
- private db?: IDBDatabase;
- private disconnected = true;
- private _isNewlyCreated = false;
- private syncToDatabasePromise?: Promise<void>;
- private pendingUserPresenceData: UserTuple[] = [];
-
- /**
- * Does the actual reading from and writing to the indexeddb
- *
- * Construct a new Indexed Database store backend. This requires a call to
- * `connect()` before this store can be used.
- * @param indexedDB - The Indexed DB interface e.g
- * `window.indexedDB`
- * @param dbName - Optional database name. The same name must be used
- * to open the same database.
- */
- public constructor(private readonly indexedDB: IDBFactory, dbName = "default") {
- this.dbName = "matrix-js-sdk:" + dbName;
- this.syncAccumulator = new SyncAccumulator();
- }
-
- /**
- * Attempt to connect to the database. This can fail if the user does not
- * grant permission.
- * @returns Promise which resolves if successfully connected.
- */
- public connect(onClose?: () => void): Promise<void> {
- if (!this.disconnected) {
- logger.log(`LocalIndexedDBStoreBackend.connect: already connected or connecting`);
- return Promise.resolve();
- }
-
- this.disconnected = false;
-
- logger.log(`LocalIndexedDBStoreBackend.connect: connecting...`);
- const req = this.indexedDB.open(this.dbName, VERSION);
- req.onupgradeneeded = (ev): void => {
- const db = req.result;
- const oldVersion = ev.oldVersion;
- logger.log(`LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`);
- if (oldVersion < 1) {
- // The database did not previously exist
- this._isNewlyCreated = true;
- }
- DB_MIGRATIONS.forEach((migration, index) => {
- if (oldVersion <= index) migration(db);
- });
- };
-
- req.onblocked = (): void => {
- logger.log(`can't yet open LocalIndexedDBStoreBackend because it is open elsewhere`);
- };
-
- logger.log(`LocalIndexedDBStoreBackend.connect: awaiting connection...`);
- return reqAsEventPromise(req).then(async () => {
- logger.log(`LocalIndexedDBStoreBackend.connect: connected`);
- this.db = req.result;
-
- // add a poorly-named listener for when deleteDatabase is called
- // so we can close our db connections.
- this.db.onversionchange = (): void => {
- this.db?.close(); // this does not call onclose
- this.disconnected = true;
- this.db = undefined;
- onClose?.();
- };
- this.db.onclose = (): void => {
- this.disconnected = true;
- this.db = undefined;
- onClose?.();
- };
-
- await this.init();
- });
- }
-
- /** @returns whether or not the database was newly created in this session. */
- public isNewlyCreated(): Promise<boolean> {
- return Promise.resolve(this._isNewlyCreated);
- }
-
- /**
- * Having connected, load initial data from the database and prepare for use
- * @returns Promise which resolves on success
- */
- private init(): Promise<unknown> {
- return Promise.all([this.loadAccountData(), this.loadSyncData()]).then(([accountData, syncData]) => {
- logger.log(`LocalIndexedDBStoreBackend: loaded initial data`);
- this.syncAccumulator.accumulate(
- {
- next_batch: syncData.nextBatch,
- rooms: syncData.roomsData,
- account_data: {
- events: accountData,
- },
- },
- true,
- );
- });
- }
-
- /**
- * Returns the out-of-band membership events for this room that
- * were previously loaded.
- * @returns the events, potentially an empty array if OOB loading didn't yield any new members
- * @returns in case the members for this room haven't been stored yet
- */
- 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: IStateEventWithRoomId[] = [];
- // did we encounter the oob_written marker object
- // amongst the results? That means OOB member
- // loading already happened for this room
- // but there were no members to persist as they
- // were all known already
- let oobWritten = false;
-
- request.onsuccess = (): void => {
- const cursor = request.result;
- if (!cursor) {
- // Unknown room
- if (!membershipEvents.length && !oobWritten) {
- return resolve(null);
- }
- return resolve(membershipEvents);
- }
- const record = cursor.value;
- if (record.oob_written) {
- oobWritten = true;
- } else {
- membershipEvents.push(record);
- }
- cursor.continue();
- };
- request.onerror = (err): void => {
- reject(err);
- };
- }).then((events) => {
- logger.log(`LL: got ${events?.length} membershipEvents from storage for room ${roomId} ...`);
- return events;
- });
- }
-
- /**
- * Stores the out-of-band membership events for this room. Note that
- * it still makes sense to store an empty array as the OOB status for the room is
- * marked as fetched, and getOutOfBandMembers will return an empty array instead of null
- * @param membershipEvents - the membership events to store
- */
- 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");
- const store = tx.objectStore("oob_membership_events");
- membershipEvents.forEach((e) => {
- store.put(e);
- });
- // aside from all the events, we also write a marker object to the store
- // to mark the fact that OOB members have been written for this room.
- // It's possible that 0 members need to be written as all where previously know
- // but we still need to know whether to return null or [] from getOutOfBandMembers
- // where null means out of band members haven't been stored yet for this room
- const markerObject = {
- room_id: roomId,
- oob_written: true,
- state_key: 0,
- };
- store.put(markerObject);
- await txnAsPromise(tx);
- logger.log(`LL: backend done storing for ${roomId}!`);
- }
-
- public async clearOutOfBandMembers(roomId: string): Promise<void> {
- // the approach to delete all members for a room
- // is to get the min and max state key from the index
- // for that room, and then delete between those
- // keys in the store.
- // this should be way faster than deleting every member
- // individually for a large room.
- const readTx = this.db!.transaction(["oob_membership_events"], "readonly");
- const store = readTx.objectStore("oob_membership_events");
- const roomIndex = store.index("room");
- const roomRange = IDBKeyRange.only(roomId);
-
- const minStateKeyProm = reqAsCursorPromise(roomIndex.openKeyCursor(roomRange, "next")).then(
- (cursor) => (<IDBValidKey[]>cursor?.primaryKey)[1],
- );
- const maxStateKeyProm = reqAsCursorPromise(roomIndex.openKeyCursor(roomRange, "prev")).then(
- (cursor) => (<IDBValidKey[]>cursor?.primaryKey)[1],
- );
- const [minStateKey, maxStateKey] = await Promise.all([minStateKeyProm, maxStateKeyProm]);
-
- const writeTx = this.db!.transaction(["oob_membership_events"], "readwrite");
- const writeStore = writeTx.objectStore("oob_membership_events");
- const membersKeyRange = IDBKeyRange.bound([roomId, minStateKey], [roomId, maxStateKey]);
-
- logger.log(
- `LL: Deleting all users + marker in storage for room ${roomId}, with key range:`,
- [roomId, minStateKey],
- [roomId, maxStateKey],
- );
- await reqAsPromise(writeStore.delete(membersKeyRange));
- }
-
- /**
- * Clear the entire database. This should be used when logging out of a client
- * to prevent mixing data between accounts.
- * @returns Resolved when the database is cleared.
- */
- public clearDatabase(): Promise<void> {
- return new Promise((resolve) => {
- logger.log(`Removing indexeddb instance: ${this.dbName}`);
- const req = this.indexedDB.deleteDatabase(this.dbName);
-
- req.onblocked = (): void => {
- logger.log(`can't yet delete indexeddb ${this.dbName} because it is open elsewhere`);
- };
-
- req.onerror = (): void => {
- // in firefox, with indexedDB disabled, this fails with a
- // DOMError. We treat this as non-fatal, so that we can still
- // use the app.
- logger.warn(`unable to delete js-sdk store indexeddb: ${req.error}`);
- resolve();
- };
-
- req.onsuccess = (): void => {
- logger.log(`Removed indexeddb instance: ${this.dbName}`);
- resolve();
- };
- });
- }
-
- /**
- * @param copy - If false, the data returned is from internal
- * buffers and must not be mutated. Otherwise, a copy is made before
- * returning such that the data can be safely mutated. Default: true.
- *
- * @returns Promise which resolves with a sync response to restore the
- * client state to where it was at the last save, or null if there
- * is no saved sync data.
- */
- public getSavedSync(copy = true): Promise<ISavedSync | null> {
- const data = this.syncAccumulator.getJSON();
- if (!data.nextBatch) return Promise.resolve(null);
- if (copy) {
- // We must deep copy the stored data so that the /sync processing code doesn't
- // corrupt the internal state of the sync accumulator (it adds non-clonable keys)
- return Promise.resolve(utils.deepCopy(data));
- } else {
- return Promise.resolve(data);
- }
- }
-
- public getNextBatchToken(): Promise<string> {
- return Promise.resolve(this.syncAccumulator.getNextBatchToken());
- }
-
- public setSyncData(syncData: ISyncResponse): Promise<void> {
- return Promise.resolve().then(() => {
- this.syncAccumulator.accumulate(syncData);
- });
- }
-
- /**
- * Sync users and all accumulated sync data to the database.
- * If a previous sync is in flight, the new data will be added to the
- * next sync and the current sync's promise will be returned.
- * @param userTuples - The user tuples
- * @returns Promise which resolves if the data was persisted.
- */
- public async syncToDatabase(userTuples: UserTuple[]): Promise<void> {
- if (this.syncToDatabasePromise) {
- logger.warn("Skipping syncToDatabase() as persist already in flight");
- this.pendingUserPresenceData.push(...userTuples);
- return this.syncToDatabasePromise;
- }
- userTuples.unshift(...this.pendingUserPresenceData);
- this.syncToDatabasePromise = this.doSyncToDatabase(userTuples);
- return this.syncToDatabasePromise;
- }
-
- private async doSyncToDatabase(userTuples: UserTuple[]): Promise<void> {
- try {
- const syncData = this.syncAccumulator.getJSON(true);
- await Promise.all([
- this.persistUserPresenceEvents(userTuples),
- this.persistAccountData(syncData.accountData),
- this.persistSyncData(syncData.nextBatch, syncData.roomsData),
- ]);
- } finally {
- this.syncToDatabasePromise = undefined;
- }
- }
-
- /**
- * Persist rooms /sync data along with the next batch token.
- * @param nextBatch - The next_batch /sync value.
- * @param roomsData - The 'rooms' /sync data from a SyncAccumulator
- * @returns Promise which resolves if the data was persisted.
- */
- private persistSyncData(nextBatch: string, roomsData: ISyncResponse["rooms"]): Promise<void> {
- logger.log("Persisting sync data up to", nextBatch);
- return utils.promiseTry<void>(() => {
- const txn = this.db!.transaction(["sync"], "readwrite");
- const store = txn.objectStore("sync");
- store.put({
- clobber: "-", // constant key so will always clobber
- nextBatch,
- roomsData,
- }); // put == UPSERT
- return txnAsPromise(txn).then(() => {
- logger.log("Persisted sync data up to", nextBatch);
- });
- });
- }
-
- /**
- * Persist a list of account data events. Events with the same 'type' will
- * be replaced.
- * @param accountData - An array of raw user-scoped account data events
- * @returns Promise which resolves if the events were persisted.
- */
- private persistAccountData(accountData: IMinimalEvent[]): Promise<void> {
- return utils.promiseTry<void>(() => {
- const txn = this.db!.transaction(["accountData"], "readwrite");
- const store = txn.objectStore("accountData");
- for (const event of accountData) {
- store.put(event); // put == UPSERT
- }
- return txnAsPromise(txn).then();
- });
- }
-
- /**
- * Persist a list of [user id, presence event] they are for.
- * Users with the same 'userId' will be replaced.
- * Presence events should be the event in its raw form (not the Event
- * object)
- * @param tuples - An array of [userid, event] tuples
- * @returns Promise which resolves if the users were persisted.
- */
- private persistUserPresenceEvents(tuples: UserTuple[]): Promise<void> {
- return utils.promiseTry<void>(() => {
- const txn = this.db!.transaction(["users"], "readwrite");
- const store = txn.objectStore("users");
- for (const tuple of tuples) {
- store.put({
- userId: tuple[0],
- event: tuple[1],
- }); // put == UPSERT
- }
- return txnAsPromise(txn).then();
- });
- }
-
- /**
- * Load all user presence events from the database. This is not cached.
- * FIXME: It would probably be more sensible to store the events in the
- * sync.
- * @returns A list of presence events in their raw form.
- */
- public getUserPresenceEvents(): Promise<UserTuple[]> {
- return utils.promiseTry<UserTuple[]>(() => {
- const txn = this.db!.transaction(["users"], "readonly");
- const store = txn.objectStore("users");
- return selectQuery(store, undefined, (cursor) => {
- return [cursor.value.userId, cursor.value.event];
- });
- });
- }
-
- /**
- * Load all the account data events from the database. This is not cached.
- * @returns A list of raw global account events.
- */
- private loadAccountData(): Promise<IMinimalEvent[]> {
- logger.log(`LocalIndexedDBStoreBackend: loading account data...`);
- return utils.promiseTry<IMinimalEvent[]>(() => {
- const txn = this.db!.transaction(["accountData"], "readonly");
- const store = txn.objectStore("accountData");
- return selectQuery(store, undefined, (cursor) => {
- return cursor.value;
- }).then((result: IMinimalEvent[]) => {
- logger.log(`LocalIndexedDBStoreBackend: loaded account data`);
- return result;
- });
- });
- }
-
- /**
- * Load the sync data from the database.
- * @returns An object with "roomsData" and "nextBatch" keys.
- */
- private loadSyncData(): Promise<ISyncData> {
- logger.log(`LocalIndexedDBStoreBackend: loading sync data...`);
- return utils.promiseTry<ISyncData>(() => {
- const txn = this.db!.transaction(["sync"], "readonly");
- const store = txn.objectStore("sync");
- return selectQuery(store, undefined, (cursor) => {
- return cursor.value;
- }).then((results: ISyncData[]) => {
- logger.log(`LocalIndexedDBStoreBackend: loaded sync data`);
- if (results.length > 1) {
- logger.warn("loadSyncData: More than 1 sync row found.");
- }
- return results.length > 0 ? results[0] : ({} as ISyncData);
- });
- });
- }
-
- public getClientOptions(): Promise<IStoredClientOpts | undefined> {
- return Promise.resolve().then(() => {
- const txn = this.db!.transaction(["client_options"], "readonly");
- const store = txn.objectStore("client_options");
- return selectQuery(store, undefined, (cursor) => {
- return cursor.value?.options;
- }).then((results) => results[0]);
- });
- }
-
- public async storeClientOptions(options: IStoredClientOpts): Promise<void> {
- const txn = this.db!.transaction(["client_options"], "readwrite");
- const store = txn.objectStore("client_options");
- store.put({
- clobber: "-", // constant key so will always clobber
- options: options,
- }); // put == UPSERT
- await txnAsPromise(txn);
- }
-
- public async saveToDeviceBatches(batches: ToDeviceBatchWithTxnId[]): Promise<void> {
- const txn = this.db!.transaction(["to_device_queue"], "readwrite");
- const store = txn.objectStore("to_device_queue");
- for (const batch of batches) {
- store.add(batch);
- }
- await txnAsPromise(txn);
- }
-
- public async getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch | null> {
- const txn = this.db!.transaction(["to_device_queue"], "readonly");
- const store = txn.objectStore("to_device_queue");
- const cursor = await reqAsCursorPromise(store.openCursor());
- if (!cursor) return null;
-
- const resultBatch = cursor.value as ToDeviceBatchWithTxnId;
-
- return {
- id: cursor.key as number,
- txnId: resultBatch.txnId,
- eventType: resultBatch.eventType,
- batch: resultBatch.batch,
- };
- }
-
- public async removeToDeviceBatch(id: number): Promise<void> {
- const txn = this.db!.transaction(["to_device_queue"], "readwrite");
- const store = txn.objectStore("to_device_queue");
- store.delete(id);
- await txnAsPromise(txn);
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-remote-backend.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-remote-backend.ts
deleted file mode 100644
index 7e2aa0c..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-remote-backend.ts
+++ /dev/null
@@ -1,203 +0,0 @@
-/*
-Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { logger } from "../logger";
-import { defer, IDeferred } from "../utils";
-import { ISavedSync } from "./index";
-import { IStoredClientOpts } from "../client";
-import { IStateEventWithRoomId, ISyncResponse } from "../matrix";
-import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend";
-import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage";
-
-export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend {
- private worker?: Worker;
- private nextSeq = 0;
- // The currently in-flight requests to the actual backend
- private inFlight: Record<number, IDeferred<any>> = {}; // seq: promise
- // Once we start connecting, we keep the promise and re-use it
- // if we try to connect again
- private startPromise?: Promise<void>;
- // Callback for when the IndexedDB gets closed unexpectedly
- private onClose?(): void;
-
- /**
- * An IndexedDB store backend where the actual backend sits in a web
- * worker.
- *
- * Construct a new Indexed Database store backend. This requires a call to
- * `connect()` before this store can be used.
- * @param workerFactory - Factory which produces a Worker
- * @param dbName - Optional database name. The same name must be used
- * to open the same database.
- */
- public constructor(private readonly workerFactory: () => Worker, private readonly dbName?: string) {}
-
- /**
- * Attempt to connect to the database. This can fail if the user does not
- * grant permission.
- * @returns Promise which resolves if successfully connected.
- */
- public connect(onClose?: () => void): Promise<void> {
- this.onClose = onClose;
- return this.ensureStarted().then(() => this.doCmd("connect"));
- }
-
- /**
- * Clear the entire database. This should be used when logging out of a client
- * to prevent mixing data between accounts.
- * @returns Resolved when the database is cleared.
- */
- public clearDatabase(): Promise<void> {
- return this.ensureStarted().then(() => this.doCmd("clearDatabase"));
- }
-
- /** @returns whether or not the database was newly created in this session. */
- public isNewlyCreated(): Promise<boolean> {
- return this.doCmd("isNewlyCreated");
- }
-
- /**
- * @returns Promise which resolves with a sync response to restore the
- * client state to where it was at the last save, or null if there
- * is no saved sync data.
- */
- public getSavedSync(): Promise<ISavedSync> {
- return this.doCmd("getSavedSync");
- }
-
- public getNextBatchToken(): Promise<string> {
- return this.doCmd("getNextBatchToken");
- }
-
- public setSyncData(syncData: ISyncResponse): Promise<void> {
- return this.doCmd("setSyncData", [syncData]);
- }
-
- public syncToDatabase(userTuples: UserTuple[]): Promise<void> {
- return this.doCmd("syncToDatabase", [userTuples]);
- }
-
- /**
- * Returns the out-of-band membership events for this room that
- * were previously loaded.
- * @returns the events, potentially an empty array if OOB loading didn't yield any new members
- * @returns in case the members for this room haven't been stored yet
- */
- public getOutOfBandMembers(roomId: string): Promise<IStateEventWithRoomId[] | null> {
- return this.doCmd("getOutOfBandMembers", [roomId]);
- }
-
- /**
- * Stores the out-of-band membership events for this room. Note that
- * it still makes sense to store an empty array as the OOB status for the room is
- * marked as fetched, and getOutOfBandMembers will return an empty array instead of null
- * @param membershipEvents - the membership events to store
- * @returns when all members have been stored
- */
- public setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void> {
- return this.doCmd("setOutOfBandMembers", [roomId, membershipEvents]);
- }
-
- public clearOutOfBandMembers(roomId: string): Promise<void> {
- return this.doCmd("clearOutOfBandMembers", [roomId]);
- }
-
- public getClientOptions(): Promise<IStoredClientOpts | undefined> {
- return this.doCmd("getClientOptions");
- }
-
- public storeClientOptions(options: IStoredClientOpts): Promise<void> {
- return this.doCmd("storeClientOptions", [options]);
- }
-
- /**
- * Load all user presence events from the database. This is not cached.
- * @returns A list of presence events in their raw form.
- */
- public getUserPresenceEvents(): Promise<UserTuple[]> {
- return this.doCmd("getUserPresenceEvents");
- }
-
- public async saveToDeviceBatches(batches: ToDeviceBatchWithTxnId[]): Promise<void> {
- return this.doCmd("saveToDeviceBatches", [batches]);
- }
-
- public async getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch | null> {
- return this.doCmd("getOldestToDeviceBatch");
- }
-
- public async removeToDeviceBatch(id: number): Promise<void> {
- return this.doCmd("removeToDeviceBatch", [id]);
- }
-
- private ensureStarted(): Promise<void> {
- if (!this.startPromise) {
- this.worker = this.workerFactory();
- this.worker.onmessage = this.onWorkerMessage;
-
- // tell the worker the db name.
- this.startPromise = this.doCmd("setupWorker", [this.dbName]).then(() => {
- logger.log("IndexedDB worker is ready");
- });
- }
- return this.startPromise;
- }
-
- private doCmd<T>(command: string, args?: any): Promise<T> {
- // wrap in a q so if the postMessage throws,
- // the promise automatically gets rejected
- return Promise.resolve().then(() => {
- const seq = this.nextSeq++;
- const def = defer<T>();
-
- this.inFlight[seq] = def;
-
- this.worker?.postMessage({ command, seq, args });
-
- return def.promise;
- });
- }
-
- private onWorkerMessage = (ev: MessageEvent): void => {
- const msg = ev.data;
-
- if (msg.command == "closed") {
- this.onClose?.();
- } else if (msg.command == "cmd_success" || msg.command == "cmd_fail") {
- if (msg.seq === undefined) {
- logger.error("Got reply from worker with no seq");
- return;
- }
-
- const def = this.inFlight[msg.seq];
- if (def === undefined) {
- logger.error("Got reply for unknown seq " + msg.seq);
- return;
- }
- delete this.inFlight[msg.seq];
-
- if (msg.command == "cmd_success") {
- def.resolve(msg.result);
- } else {
- const error = new Error(msg.error.message);
- error.name = msg.error.name;
- def.reject(error);
- }
- } else {
- logger.warn("Unrecognised message from worker: ", msg);
- }
- };
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-store-worker.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-store-worker.ts
deleted file mode 100644
index 52a7fa6..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-store-worker.ts
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
-Copyright 2017 - 2021 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 { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend";
-import { logger } from "../logger";
-
-interface ICmd {
- command: string;
- seq: number;
- args: any[];
-}
-
-/**
- * This class lives in the webworker and drives a LocalIndexedDBStoreBackend
- * controlled by messages from the main process.
- *
- * @example
- * It should be instantiated by a web worker script provided by the application
- * in a script, for example:
- * ```
- * import {IndexedDBStoreWorker} from 'matrix-js-sdk/lib/indexeddb-worker.js';
- * const remoteWorker = new IndexedDBStoreWorker(postMessage);
- * onmessage = remoteWorker.onMessage;
- * ```
- *
- * Note that it is advisable to import this class by referencing the file directly to
- * avoid a dependency on the whole js-sdk.
- *
- */
-export class IndexedDBStoreWorker {
- private backend?: LocalIndexedDBStoreBackend;
-
- /**
- * @param postMessage - The web worker postMessage function that
- * should be used to communicate back to the main script.
- */
- public constructor(private readonly postMessage: InstanceType<typeof Worker>["postMessage"]) {}
-
- private onClose = (): void => {
- this.postMessage.call(null, {
- command: "closed",
- });
- };
-
- /**
- * Passes a message event from the main script into the class. This method
- * can be directly assigned to the web worker `onmessage` variable.
- *
- * @param ev - The message event
- */
- public onMessage = (ev: MessageEvent): void => {
- const msg: ICmd = ev.data;
- let prom: Promise<any> | undefined;
-
- switch (msg.command) {
- case "setupWorker":
- // this is the 'indexedDB' global (where global != window
- // because it's a web worker and there is no window).
- this.backend = new LocalIndexedDBStoreBackend(indexedDB, msg.args[0]);
- prom = Promise.resolve();
- break;
- case "connect":
- prom = this.backend?.connect(this.onClose);
- break;
- case "isNewlyCreated":
- prom = this.backend?.isNewlyCreated();
- break;
- case "clearDatabase":
- prom = this.backend?.clearDatabase();
- break;
- case "getSavedSync":
- prom = this.backend?.getSavedSync(false);
- break;
- case "setSyncData":
- prom = this.backend?.setSyncData(msg.args[0]);
- break;
- case "syncToDatabase":
- prom = this.backend?.syncToDatabase(msg.args[0]);
- break;
- case "getUserPresenceEvents":
- prom = this.backend?.getUserPresenceEvents();
- break;
- case "getNextBatchToken":
- prom = this.backend?.getNextBatchToken();
- break;
- case "getOutOfBandMembers":
- prom = this.backend?.getOutOfBandMembers(msg.args[0]);
- break;
- case "clearOutOfBandMembers":
- prom = this.backend?.clearOutOfBandMembers(msg.args[0]);
- break;
- case "setOutOfBandMembers":
- prom = this.backend?.setOutOfBandMembers(msg.args[0], msg.args[1]);
- break;
- case "getClientOptions":
- prom = this.backend?.getClientOptions();
- break;
- case "storeClientOptions":
- prom = this.backend?.storeClientOptions(msg.args[0]);
- break;
- case "saveToDeviceBatches":
- prom = this.backend?.saveToDeviceBatches(msg.args[0]);
- break;
- case "getOldestToDeviceBatch":
- prom = this.backend?.getOldestToDeviceBatch();
- break;
- case "removeToDeviceBatch":
- prom = this.backend?.removeToDeviceBatch(msg.args[0]);
- break;
- }
-
- if (prom === undefined) {
- this.postMessage({
- command: "cmd_fail",
- seq: msg.seq,
- // Can't be an Error because they're not structured cloneable
- error: "Unrecognised command",
- });
- return;
- }
-
- prom.then(
- (ret) => {
- this.postMessage.call(null, {
- command: "cmd_success",
- seq: msg.seq,
- result: ret,
- });
- },
- (err) => {
- logger.error("Error running command: " + msg.command, err);
- this.postMessage.call(null, {
- command: "cmd_fail",
- seq: msg.seq,
- // Just send a string because Error objects aren't cloneable
- error: {
- message: err.message,
- name: err.name,
- },
- });
- },
- );
- };
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb.ts
deleted file mode 100644
index cc77bf9..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb.ts
+++ /dev/null
@@ -1,383 +0,0 @@
-/*
-Copyright 2017 - 2021 Vector Creations Ltd
-
-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.
-*/
-
-/* eslint-disable @babel/no-invalid-this */
-
-import { MemoryStore, IOpts as IBaseOpts } from "./memory";
-import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend";
-import { RemoteIndexedDBStoreBackend } from "./indexeddb-remote-backend";
-import { User } from "../models/user";
-import { IEvent, MatrixEvent } from "../models/event";
-import { logger } from "../logger";
-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";
-import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage";
-import { IStoredClientOpts } from "../client";
-
-/**
- * This is an internal module. See {@link IndexedDBStore} for the public class.
- */
-
-// If this value is too small we'll be writing very often which will cause
-// noticeable stop-the-world pauses. If this value is too big we'll be writing
-// so infrequently that the /sync size gets bigger on reload. Writing more
-// often does not affect the length of the pause since the entire /sync
-// response is persisted each time.
-const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes
-
-interface IOpts extends IBaseOpts {
- /** The Indexed DB interface e.g. `window.indexedDB` */
- indexedDB: IDBFactory;
- /** Optional database name. The same name must be used to open the same database. */
- dbName?: string;
- /** Optional factory to spin up a Worker to execute the IDB transactions within. */
- workerFactory?: () => Worker;
-}
-
-type EventHandlerMap = {
- // Fired when an IDB command fails on a degradable path, and the store falls back to MemoryStore
- // This signals the potential for data volatility.
- degraded: (e: Error) => void;
- // Fired when the IndexedDB gets closed unexpectedly, for example, if the underlying storage is removed or
- // if the user clears the database in the browser's history preferences.
- closed: () => void;
-};
-
-export class IndexedDBStore extends MemoryStore {
- public static exists(indexedDB: IDBFactory, dbName: string): Promise<boolean> {
- return LocalIndexedDBStoreBackend.exists(indexedDB, dbName);
- }
-
- /**
- * The backend instance.
- * Call through to this API if you need to perform specific indexeddb actions like deleting the database.
- */
- public readonly backend: IIndexedDBBackend;
-
- private startedUp = false;
- private syncTs = 0;
- // Records the last-modified-time of each user at the last point we saved
- // the database, such that we can derive the set if users that have been
- // modified since we last saved.
- private userModifiedMap: Record<string, number> = {}; // user_id : timestamp
- private emitter = new TypedEventEmitter<keyof EventHandlerMap, EventHandlerMap>();
-
- /**
- * Construct a new Indexed Database store, which extends MemoryStore.
- *
- * This store functions like a MemoryStore except it periodically persists
- * the contents of the store to an IndexedDB backend.
- *
- * All data is still kept in-memory but can be loaded from disk by calling
- * `startup()`. This can make startup times quicker as a complete
- * sync from the server is not required. This does not reduce memory usage as all
- * the data is eagerly fetched when `startup()` is called.
- * ```
- * let opts = { indexedDB: window.indexedDB, localStorage: window.localStorage };
- * let store = new IndexedDBStore(opts);
- * await store.startup(); // load from indexed db
- * let client = sdk.createClient({
- * store: store,
- * });
- * client.startClient();
- * client.on("sync", function(state, prevState, data) {
- * if (state === "PREPARED") {
- * console.log("Started up, now with go faster stripes!");
- * }
- * });
- * ```
- *
- * @param opts - Options object.
- */
- public constructor(opts: IOpts) {
- super(opts);
-
- if (!opts.indexedDB) {
- throw new Error("Missing required option: indexedDB");
- }
-
- if (opts.workerFactory) {
- this.backend = new RemoteIndexedDBStoreBackend(opts.workerFactory, opts.dbName);
- } else {
- this.backend = new LocalIndexedDBStoreBackend(opts.indexedDB, opts.dbName);
- }
- }
-
- public on = this.emitter.on.bind(this.emitter);
-
- /**
- * @returns Resolved when loaded from indexed db.
- */
- public startup(): Promise<void> {
- if (this.startedUp) {
- logger.log(`IndexedDBStore.startup: already started`);
- return Promise.resolve();
- }
-
- logger.log(`IndexedDBStore.startup: connecting to backend`);
- return this.backend
- .connect(this.onClose)
- .then(() => {
- logger.log(`IndexedDBStore.startup: loading presence events`);
- return this.backend.getUserPresenceEvents();
- })
- .then((userPresenceEvents) => {
- logger.log(`IndexedDBStore.startup: processing presence events`);
- userPresenceEvents.forEach(([userId, rawEvent]) => {
- const u = new User(userId);
- if (rawEvent) {
- u.setPresenceEvent(new MatrixEvent(rawEvent));
- }
- this.userModifiedMap[u.userId] = u.getLastModifiedTime();
- this.storeUser(u);
- });
- this.startedUp = true;
- });
- }
-
- private onClose = (): void => {
- this.emitter.emit("closed");
- };
-
- /**
- * @returns Promise which resolves with a sync response to restore the
- * client state to where it was at the last save, or null if there
- * is no saved sync data.
- */
- public getSavedSync = this.degradable((): Promise<ISavedSync | null> => {
- return this.backend.getSavedSync();
- }, "getSavedSync");
-
- /** @returns whether or not the database was newly created in this session. */
- public isNewlyCreated = this.degradable((): Promise<boolean> => {
- return this.backend.isNewlyCreated();
- }, "isNewlyCreated");
-
- /**
- * @returns If there is a saved sync, the nextBatch token
- * for this sync, otherwise null.
- */
- public getSavedSyncToken = this.degradable((): Promise<string | null> => {
- return this.backend.getNextBatchToken();
- }, "getSavedSyncToken");
-
- /**
- * Delete all data from this store.
- * @returns Promise which resolves if the data was deleted from the database.
- */
- public deleteAllData = this.degradable((): Promise<void> => {
- super.deleteAllData();
- return this.backend.clearDatabase().then(
- () => {
- logger.log("Deleted indexeddb data.");
- },
- (err) => {
- logger.error(`Failed to delete indexeddb data: ${err}`);
- throw err;
- },
- );
- });
-
- /**
- * Whether this store would like to save its data
- * Note that obviously whether the store wants to save or
- * not could change between calling this function and calling
- * save().
- *
- * @returns True if calling save() will actually save
- * (at the time this function is called).
- */
- public wantsSave(): boolean {
- const now = Date.now();
- return now - this.syncTs > WRITE_DELAY_MS;
- }
-
- /**
- * Possibly write data to the database.
- *
- * @param force - True to force a save to happen
- * @returns Promise resolves after the write completes
- * (or immediately if no write is performed)
- */
- public save(force = false): Promise<void> {
- if (force || this.wantsSave()) {
- return this.reallySave();
- }
- return Promise.resolve();
- }
-
- private reallySave = this.degradable((): Promise<void> => {
- this.syncTs = Date.now(); // set now to guard against multi-writes
-
- // work out changed users (this doesn't handle deletions but you
- // can't 'delete' users as they are just presence events).
- const userTuples: [userId: string, presenceEvent: Partial<IEvent>][] = [];
- for (const u of this.getUsers()) {
- if (this.userModifiedMap[u.userId] === u.getLastModifiedTime()) continue;
- if (!u.events.presence) continue;
-
- userTuples.push([u.userId, u.events.presence.event]);
-
- // note that we've saved this version of the user
- this.userModifiedMap[u.userId] = u.getLastModifiedTime();
- }
-
- return this.backend.syncToDatabase(userTuples);
- });
-
- public setSyncData = this.degradable((syncData: ISyncResponse): Promise<void> => {
- return this.backend.setSyncData(syncData);
- }, "setSyncData");
-
- /**
- * Returns the out-of-band membership events for this room that
- * were previously loaded.
- * @returns the events, potentially an empty array if OOB loading didn't yield any new members
- * @returns in case the members for this room haven't been stored yet
- */
- public getOutOfBandMembers = this.degradable((roomId: string): Promise<IStateEventWithRoomId[] | null> => {
- return this.backend.getOutOfBandMembers(roomId);
- }, "getOutOfBandMembers");
-
- /**
- * Stores the out-of-band membership events for this room. Note that
- * it still makes sense to store an empty array as the OOB status for the room is
- * marked as fetched, and getOutOfBandMembers will return an empty array instead of null
- * @param membershipEvents - the membership events to store
- * @returns when all members have been stored
- */
- 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);
- return this.backend.clearOutOfBandMembers(roomId);
- }, "clearOutOfBandMembers");
-
- public getClientOptions = this.degradable((): Promise<IStoredClientOpts | undefined> => {
- return this.backend.getClientOptions();
- }, "getClientOptions");
-
- public storeClientOptions = this.degradable((options: IStoredClientOpts): Promise<void> => {
- super.storeClientOptions(options);
- return this.backend.storeClientOptions(options);
- }, "storeClientOptions");
-
- /**
- * All member functions of `IndexedDBStore` that access the backend use this wrapper to
- * watch for failures after initial store startup, including `QuotaExceededError` as
- * free disk space changes, etc.
- *
- * When IndexedDB fails via any of these paths, we degrade this back to a `MemoryStore`
- * in place so that the current operation and all future ones are in-memory only.
- *
- * @param func - The degradable work to do.
- * @param fallback - The method name for fallback.
- * @returns A wrapped member function.
- */
- private degradable<A extends Array<any>, R = void>(
- func: DegradableFn<A, R>,
- fallback?: keyof MemoryStore,
- ): DegradableFn<A, R> {
- const fallbackFn = fallback ? (super[fallback] as Function) : null;
-
- return async (...args) => {
- try {
- return await func.call(this, ...args);
- } catch (e) {
- logger.error("IndexedDBStore failure, degrading to MemoryStore", e);
- this.emitter.emit("degraded", e as Error);
- try {
- // We try to delete IndexedDB after degrading since this store is only a
- // cache (the app will still function correctly without the data).
- // It's possible that deleting repair IndexedDB for the next app load,
- // potentially by making a little more space available.
- logger.log("IndexedDBStore trying to delete degraded data");
- await this.backend.clearDatabase();
- logger.log("IndexedDBStore delete after degrading succeeded");
- } catch (e) {
- logger.warn("IndexedDBStore delete after degrading failed", e);
- }
- // Degrade the store from being an instance of `IndexedDBStore` to instead be
- // an instance of `MemoryStore` so that future API calls use the memory path
- // directly and skip IndexedDB entirely. This should be safe as
- // `IndexedDBStore` already extends from `MemoryStore`, so we are making the
- // store become its parent type in a way. The mutator methods of
- // `IndexedDBStore` also maintain the state that `MemoryStore` uses (many are
- // not overridden at all).
- if (fallbackFn) {
- return fallbackFn.call(this, ...args);
- }
- }
- };
- }
-
- // XXX: ideally these would be stored in indexeddb as part of the room but,
- // we don't store rooms as such and instead accumulate entire sync responses atm.
- public async getPendingEvents(roomId: string): Promise<Partial<IEvent>[]> {
- if (!this.localStorage) return super.getPendingEvents(roomId);
-
- const serialized = this.localStorage.getItem(pendingEventsKey(roomId));
- if (serialized) {
- try {
- return JSON.parse(serialized);
- } catch (e) {
- logger.error("Could not parse persisted pending events", e);
- }
- }
- return [];
- }
-
- public async setPendingEvents(roomId: string, events: Partial<IEvent>[]): Promise<void> {
- if (!this.localStorage) return super.setPendingEvents(roomId, events);
-
- if (events.length > 0) {
- this.localStorage.setItem(pendingEventsKey(roomId), JSON.stringify(events));
- } else {
- this.localStorage.removeItem(pendingEventsKey(roomId));
- }
- }
-
- public saveToDeviceBatches(batches: ToDeviceBatchWithTxnId[]): Promise<void> {
- return this.backend.saveToDeviceBatches(batches);
- }
-
- public getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch | null> {
- return this.backend.getOldestToDeviceBatch();
- }
-
- public removeToDeviceBatch(id: number): Promise<void> {
- return this.backend.removeToDeviceBatch(id);
- }
-}
-
-/**
- * @param roomId - ID of the current room
- * @returns Storage key to retrieve pending events
- */
-function pendingEventsKey(roomId: string): string {
- return `mx_pending_events_${roomId}`;
-}
-
-type DegradableFn<A extends Array<any>, T> = (...args: A) => Promise<T>;
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/local-storage-events-emitter.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/local-storage-events-emitter.ts
deleted file mode 100644
index adb70cb..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/local-storage-events-emitter.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
-Copyright 2021 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 { TypedEventEmitter } from "../models/typed-event-emitter";
-
-export enum LocalStorageErrors {
- Global = "Global",
- SetItemError = "setItem",
- GetItemError = "getItem",
- RemoveItemError = "removeItem",
- ClearError = "clear",
- QuotaExceededError = "QuotaExceededError",
-}
-
-type EventHandlerMap = {
- [LocalStorageErrors.Global]: (error: Error) => void;
- [LocalStorageErrors.SetItemError]: (error: Error) => void;
- [LocalStorageErrors.GetItemError]: (error: Error) => void;
- [LocalStorageErrors.RemoveItemError]: (error: Error) => void;
- [LocalStorageErrors.ClearError]: (error: Error) => void;
- [LocalStorageErrors.QuotaExceededError]: (error: Error) => void;
-};
-
-/**
- * Used in element-web as a temporary hack to handle all the localStorage errors on the highest level possible
- * As of 15.11.2021 (DD/MM/YYYY) we're not properly handling local storage exceptions anywhere.
- * This store, as an event emitter, is used to re-emit local storage exceptions so that we can handle them
- * and show some kind of a "It's dead Jim" modal to the users, telling them that hey,
- * maybe you should check out your disk, as it's probably dying and your session may die with it.
- * See: https://github.com/vector-im/element-web/issues/18423
- */
-class LocalStorageErrorsEventsEmitter extends TypedEventEmitter<LocalStorageErrors, EventHandlerMap> {}
-export const localStorageErrorsEventsEmitter = new LocalStorageErrorsEventsEmitter();
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/memory.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/memory.ts
deleted file mode 100644
index d859ddd..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/memory.ts
+++ /dev/null
@@ -1,436 +0,0 @@
-/*
-Copyright 2015 - 2021 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.
-*/
-
-/**
- * This is an internal module. See {@link MemoryStore} for the public class.
- */
-
-import { EventType } from "../@types/event";
-import { Room } from "../models/room";
-import { User } from "../models/user";
-import { IEvent, 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";
-import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage";
-import { IStoredClientOpts } from "../client";
-import { MapWithDefault } from "../utils";
-
-function isValidFilterId(filterId?: string | number | null): boolean {
- const isValidStr =
- typeof filterId === "string" &&
- !!filterId &&
- filterId !== "undefined" && // exclude these as we've serialized undefined in localStorage before
- filterId !== "null";
-
- return isValidStr || typeof filterId === "number";
-}
-
-export interface IOpts {
- /** The local storage instance to persist some forms of data such as tokens. Rooms will NOT be stored. */
- localStorage?: Storage;
-}
-
-export class MemoryStore implements IStore {
- private rooms: Record<string, Room> = {}; // roomId: Room
- private users: Record<string, User> = {}; // userId: User
- private syncToken: string | null = null;
- // userId: {
- // filterId: Filter
- // }
- private filters: MapWithDefault<string, Map<string, Filter>> = new MapWithDefault(() => new Map());
- public accountData: Map<string, MatrixEvent> = new Map(); // type: content
- protected readonly localStorage?: Storage;
- private oobMembers: Map<string, IStateEventWithRoomId[]> = new Map(); // roomId: [member events]
- private pendingEvents: { [roomId: string]: Partial<IEvent>[] } = {};
- private clientOptions?: IStoredClientOpts;
- private pendingToDeviceBatches: IndexedToDeviceBatch[] = [];
- private nextToDeviceBatchId = 0;
-
- /**
- * Construct a new in-memory data store for the Matrix Client.
- * @param opts - Config options
- */
- public constructor(opts: IOpts = {}) {
- this.localStorage = opts.localStorage;
- }
-
- /**
- * Retrieve the token to stream from.
- * @returns The token or null.
- */
- public getSyncToken(): string | null {
- return this.syncToken;
- }
-
- /** @returns whether or not the database was newly created in this session. */
- public isNewlyCreated(): Promise<boolean> {
- return Promise.resolve(true);
- }
-
- /**
- * Set the token to stream from.
- * @param token - The token to stream from.
- */
- public setSyncToken(token: string): void {
- this.syncToken = token;
- }
-
- /**
- * Store the given room.
- * @param room - The room to be stored. All properties must be stored.
- */
- public storeRoom(room: Room): void {
- this.rooms[room.roomId] = room;
- // add listeners for room member changes so we can keep the room member
- // map up-to-date.
- room.currentState.on(RoomStateEvent.Members, this.onRoomMember);
- // add existing members
- room.currentState.getMembers().forEach((m) => {
- this.onRoomMember(null, room.currentState, m);
- });
- }
-
- /**
- * Called when a room member in a room being tracked by this store has been
- * updated.
- */
- private onRoomMember = (event: MatrixEvent | null, state: RoomState, member: RoomMember): void => {
- if (member.membership === "invite") {
- // We do NOT add invited members because people love to typo user IDs
- // which would then show up in these lists (!)
- return;
- }
-
- const user = this.users[member.userId] || new User(member.userId);
- if (member.name) {
- user.setDisplayName(member.name);
- if (member.events.member) {
- user.setRawDisplayName(member.events.member.getDirectionalContent().displayname);
- }
- }
- if (member.events.member && member.events.member.getContent().avatar_url) {
- user.setAvatarUrl(member.events.member.getContent().avatar_url);
- }
- this.users[user.userId] = user;
- };
-
- /**
- * Retrieve a room by its' room ID.
- * @param roomId - The room ID.
- * @returns The room or null.
- */
- public getRoom(roomId: string): Room | null {
- return this.rooms[roomId] || null;
- }
-
- /**
- * Retrieve all known rooms.
- * @returns A list of rooms, which may be empty.
- */
- public getRooms(): Room[] {
- return Object.values(this.rooms);
- }
-
- /**
- * Permanently delete a room.
- */
- public removeRoom(roomId: string): void {
- if (this.rooms[roomId]) {
- this.rooms[roomId].currentState.removeListener(RoomStateEvent.Members, this.onRoomMember);
- }
- delete this.rooms[roomId];
- }
-
- /**
- * Retrieve a summary of all the rooms.
- * @returns A summary of each room.
- */
- public getRoomSummaries(): RoomSummary[] {
- return Object.values(this.rooms).map(function (room) {
- return room.summary!;
- });
- }
-
- /**
- * Store a User.
- * @param user - The user to store.
- */
- public storeUser(user: User): void {
- this.users[user.userId] = user;
- }
-
- /**
- * Retrieve a User by its' user ID.
- * @param userId - The user ID.
- * @returns The user or null.
- */
- public getUser(userId: string): User | null {
- return this.users[userId] || null;
- }
-
- /**
- * Retrieve all known users.
- * @returns A list of users, which may be empty.
- */
- public getUsers(): User[] {
- return Object.values(this.users);
- }
-
- /**
- * Retrieve scrollback for this room.
- * @param room - The matrix room
- * @param limit - The max number of old events to retrieve.
- * @returns An array of objects which will be at most 'limit'
- * length and at least 0. The objects are the raw event JSON.
- */
- public scrollback(room: Room, limit: number): MatrixEvent[] {
- return [];
- }
-
- /**
- * Store events for a room. The events have already been added to the timeline
- * @param room - The room to store events for.
- * @param events - The events to store.
- * @param token - The token associated with these events.
- * @param toStart - True if these are paginated results.
- */
- public storeEvents(room: Room, events: MatrixEvent[], token: string | null, toStart: boolean): void {
- // no-op because they've already been added to the room instance.
- }
-
- /**
- * Store a filter.
- */
- public storeFilter(filter: Filter): void {
- if (!filter?.userId || !filter?.filterId) return;
- this.filters.getOrCreate(filter.userId).set(filter.filterId, filter);
- }
-
- /**
- * Retrieve a filter.
- * @returns A filter or null.
- */
- public getFilter(userId: string, filterId: string): Filter | null {
- return this.filters.get(userId)?.get(filterId) || null;
- }
-
- /**
- * Retrieve a filter ID with the given name.
- * @param filterName - The filter name.
- * @returns The filter ID or null.
- */
- public getFilterIdByName(filterName: string): string | null {
- if (!this.localStorage) {
- return null;
- }
- const key = "mxjssdk_memory_filter_" + filterName;
- // XXX Storage.getItem doesn't throw ...
- // or are we using something different
- // than window.localStorage in some cases
- // that does throw?
- // that would be very naughty
- try {
- const value = this.localStorage.getItem(key);
- if (isValidFilterId(value)) {
- return value;
- }
- } catch (e) {}
- return null;
- }
-
- /**
- * Set a filter name to ID mapping.
- */
- public setFilterIdByName(filterName: string, filterId?: string): void {
- if (!this.localStorage) {
- return;
- }
- const key = "mxjssdk_memory_filter_" + filterName;
- try {
- if (isValidFilterId(filterId)) {
- this.localStorage.setItem(key, filterId!);
- } else {
- this.localStorage.removeItem(key);
- }
- } catch (e) {}
- }
-
- /**
- * Store user-scoped account data events.
- * N.B. that account data only allows a single event per type, so multiple
- * events with the same type will replace each other.
- * @param events - The events to store.
- */
- public storeAccountDataEvents(events: MatrixEvent[]): void {
- events.forEach((event) => {
- // MSC3391: an event with content of {} should be interpreted as deleted
- const isDeleted = !Object.keys(event.getContent()).length;
- if (isDeleted) {
- this.accountData.delete(event.getType());
- } else {
- this.accountData.set(event.getType(), event);
- }
- });
- }
-
- /**
- * Get account data event by event type
- * @param eventType - The event type being queried
- * @returns the user account_data event of given type, if any
- */
- public getAccountData(eventType: EventType | string): MatrixEvent | undefined {
- return this.accountData.get(eventType);
- }
-
- /**
- * setSyncData does nothing as there is no backing data store.
- *
- * @param syncData - The sync data
- * @returns An immediately resolved promise.
- */
- public setSyncData(syncData: ISyncResponse): Promise<void> {
- return Promise.resolve();
- }
-
- /**
- * We never want to save becase we have nothing to save to.
- *
- * @returns If the store wants to save
- */
- public wantsSave(): boolean {
- return false;
- }
-
- /**
- * Save does nothing as there is no backing data store.
- * @param force - True to force a save (but the memory
- * store still can't save anything)
- */
- public save(force: boolean): void {}
-
- /**
- * Startup does nothing as this store doesn't require starting up.
- * @returns An immediately resolved promise.
- */
- public startup(): Promise<void> {
- return Promise.resolve();
- }
-
- /**
- * @returns Promise which resolves with a sync response to restore the
- * client state to where it was at the last save, or null if there
- * is no saved sync data.
- */
- public getSavedSync(): Promise<ISavedSync | null> {
- return Promise.resolve(null);
- }
-
- /**
- * @returns If there is a saved sync, the nextBatch token
- * for this sync, otherwise null.
- */
- public getSavedSyncToken(): Promise<string | null> {
- return Promise.resolve(null);
- }
-
- /**
- * Delete all data from this store.
- * @returns An immediately resolved promise.
- */
- public deleteAllData(): Promise<void> {
- this.rooms = {
- // roomId: Room
- };
- this.users = {
- // userId: User
- };
- this.syncToken = null;
- this.filters = new MapWithDefault(() => new Map());
- this.accountData = new Map(); // type : content
- return Promise.resolve();
- }
-
- /**
- * Returns the out-of-band membership events for this room that
- * were previously loaded.
- * @returns the events, potentially an empty array if OOB loading didn't yield any new members
- * @returns in case the members for this room haven't been stored yet
- */
- public getOutOfBandMembers(roomId: string): Promise<IStateEventWithRoomId[] | null> {
- return Promise.resolve(this.oobMembers.get(roomId) || null);
- }
-
- /**
- * Stores the out-of-band membership events for this room. Note that
- * it still makes sense to store an empty array as the OOB status for the room is
- * marked as fetched, and getOutOfBandMembers will return an empty array instead of null
- * @param membershipEvents - the membership events to store
- * @returns when all members have been stored
- */
- public setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void> {
- this.oobMembers.set(roomId, membershipEvents);
- return Promise.resolve();
- }
-
- public clearOutOfBandMembers(roomId: string): Promise<void> {
- this.oobMembers.delete(roomId);
- return Promise.resolve();
- }
-
- public getClientOptions(): Promise<IStoredClientOpts | undefined> {
- return Promise.resolve(this.clientOptions);
- }
-
- public storeClientOptions(options: IStoredClientOpts): Promise<void> {
- this.clientOptions = Object.assign({}, options);
- return Promise.resolve();
- }
-
- public async getPendingEvents(roomId: string): Promise<Partial<IEvent>[]> {
- return this.pendingEvents[roomId] ?? [];
- }
-
- public async setPendingEvents(roomId: string, events: Partial<IEvent>[]): Promise<void> {
- this.pendingEvents[roomId] = events;
- }
-
- public saveToDeviceBatches(batches: ToDeviceBatchWithTxnId[]): Promise<void> {
- for (const batch of batches) {
- this.pendingToDeviceBatches.push({
- id: this.nextToDeviceBatchId++,
- eventType: batch.eventType,
- txnId: batch.txnId,
- batch: batch.batch,
- });
- }
- return Promise.resolve();
- }
-
- public async getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch | null> {
- if (this.pendingToDeviceBatches.length === 0) return null;
- return this.pendingToDeviceBatches[0];
- }
-
- public removeToDeviceBatch(id: number): Promise<void> {
- this.pendingToDeviceBatches = this.pendingToDeviceBatches.filter((batch) => batch.id !== id);
- return Promise.resolve();
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/stub.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/stub.ts
deleted file mode 100644
index e4402ed..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/stub.ts
+++ /dev/null
@@ -1,267 +0,0 @@
-/*
-Copyright 2015 - 2021 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.
-*/
-
-/**
- * This is an internal module.
- */
-
-import { EventType } from "../@types/event";
-import { Room } from "../models/room";
-import { User } from "../models/user";
-import { IEvent, 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";
-import { IndexedToDeviceBatch, ToDeviceBatch } from "../models/ToDeviceMessage";
-import { IStoredClientOpts } from "../client";
-
-/**
- * Construct a stub store. This does no-ops on most store methods.
- */
-export class StubStore implements IStore {
- public readonly accountData = new Map(); // stub
- private fromToken: string | null = null;
-
- /** @returns whether or not the database was newly created in this session. */
- public isNewlyCreated(): Promise<boolean> {
- return Promise.resolve(true);
- }
-
- /**
- * Get the sync token.
- */
- public getSyncToken(): string | null {
- return this.fromToken;
- }
-
- /**
- * Set the sync token.
- */
- public setSyncToken(token: string): void {
- this.fromToken = token;
- }
-
- /**
- * No-op.
- */
- public storeRoom(room: Room): void {}
-
- /**
- * No-op.
- */
- public getRoom(roomId: string): Room | null {
- return null;
- }
-
- /**
- * No-op.
- * @returns An empty array.
- */
- public getRooms(): Room[] {
- return [];
- }
-
- /**
- * Permanently delete a room.
- */
- public removeRoom(roomId: string): void {
- return;
- }
-
- /**
- * No-op.
- * @returns An empty array.
- */
- public getRoomSummaries(): RoomSummary[] {
- return [];
- }
-
- /**
- * No-op.
- */
- public storeUser(user: User): void {}
-
- /**
- * No-op.
- */
- public getUser(userId: string): User | null {
- return null;
- }
-
- /**
- * No-op.
- */
- public getUsers(): User[] {
- return [];
- }
-
- /**
- * No-op.
- */
- public scrollback(room: Room, limit: number): MatrixEvent[] {
- return [];
- }
-
- /**
- * Store events for a room.
- * @param room - The room to store events for.
- * @param events - The events to store.
- * @param token - The token associated with these events.
- * @param toStart - True if these are paginated results.
- */
- public storeEvents(room: Room, events: MatrixEvent[], token: string | null, toStart: boolean): void {}
-
- /**
- * Store a filter.
- */
- public storeFilter(filter: Filter): void {}
-
- /**
- * Retrieve a filter.
- * @returns A filter or null.
- */
- public getFilter(userId: string, filterId: string): Filter | null {
- return null;
- }
-
- /**
- * Retrieve a filter ID with the given name.
- * @param filterName - The filter name.
- * @returns The filter ID or null.
- */
- public getFilterIdByName(filterName: string): string | null {
- return null;
- }
-
- /**
- * Set a filter name to ID mapping.
- */
- public setFilterIdByName(filterName: string, filterId?: string): void {}
-
- /**
- * Store user-scoped account data events
- * @param events - The events to store.
- */
- public storeAccountDataEvents(events: MatrixEvent[]): void {}
-
- /**
- * Get account data event by event type
- * @param eventType - The event type being queried
- */
- public getAccountData(eventType: EventType | string): MatrixEvent | undefined {
- return undefined;
- }
-
- /**
- * setSyncData does nothing as there is no backing data store.
- *
- * @param syncData - The sync data
- * @returns An immediately resolved promise.
- */
- public setSyncData(syncData: ISyncResponse): Promise<void> {
- return Promise.resolve();
- }
-
- /**
- * We never want to save because we have nothing to save to.
- *
- * @returns If the store wants to save
- */
- public wantsSave(): boolean {
- return false;
- }
-
- /**
- * Save does nothing as there is no backing data store.
- */
- public save(): void {}
-
- /**
- * Startup does nothing.
- * @returns An immediately resolved promise.
- */
- public startup(): Promise<void> {
- return Promise.resolve();
- }
-
- /**
- * @returns Promise which resolves with a sync response to restore the
- * client state to where it was at the last save, or null if there
- * is no saved sync data.
- */
- public getSavedSync(): Promise<ISavedSync | null> {
- return Promise.resolve(null);
- }
-
- /**
- * @returns If there is a saved sync, the nextBatch token
- * for this sync, otherwise null.
- */
- public getSavedSyncToken(): Promise<string | null> {
- return Promise.resolve(null);
- }
-
- /**
- * Delete all data from this store. Does nothing since this store
- * doesn't store anything.
- * @returns An immediately resolved promise.
- */
- public deleteAllData(): Promise<void> {
- return Promise.resolve();
- }
-
- public getOutOfBandMembers(): Promise<IStateEventWithRoomId[] | null> {
- return Promise.resolve(null);
- }
-
- public setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void> {
- return Promise.resolve();
- }
-
- public clearOutOfBandMembers(): Promise<void> {
- return Promise.resolve();
- }
-
- public getClientOptions(): Promise<IStoredClientOpts | undefined> {
- return Promise.resolve(undefined);
- }
-
- public storeClientOptions(options: IStoredClientOpts): Promise<void> {
- return Promise.resolve();
- }
-
- public async getPendingEvents(roomId: string): Promise<Partial<IEvent>[]> {
- return [];
- }
-
- public setPendingEvents(roomId: string, events: Partial<IEvent>[]): Promise<void> {
- return Promise.resolve();
- }
-
- public async saveToDeviceBatches(batch: ToDeviceBatch[]): Promise<void> {
- return Promise.resolve();
- }
-
- public getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch | null> {
- return Promise.resolve(null);
- }
-
- public async removeToDeviceBatch(id: number): Promise<void> {
- return Promise.resolve();
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/sync-accumulator.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/sync-accumulator.ts
deleted file mode 100644
index fef03d7..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/sync-accumulator.ts
+++ /dev/null
@@ -1,715 +0,0 @@
-/*
-Copyright 2017 - 2021 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.
-*/
-
-/**
- * This is an internal module. See {@link SyncAccumulator} for the public class.
- */
-
-import { logger } from "./logger";
-import { deepCopy, isSupportedReceiptType, MapWithDefault, recursiveMapToObject } from "./utils";
-import { IContent, IUnsigned } from "./models/event";
-import { IRoomSummary } from "./models/room-summary";
-import { EventType } from "./@types/event";
-import { MAIN_ROOM_TIMELINE, ReceiptContent, ReceiptType } from "./@types/read_receipts";
-import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync";
-
-interface IOpts {
- /**
- * The ideal maximum number of timeline entries to keep in the sync response.
- * This is best-effort, as clients do not always have a back-pagination token for each event,
- * so it's possible there may be slightly *less* than this value. There will never be more.
- * This cannot be 0 or else it makes it impossible to scroll back in a room.
- * Default: 50.
- */
- maxTimelineEntries?: number;
-}
-
-export interface IMinimalEvent {
- content: IContent;
- type: EventType | string;
- unsigned?: IUnsigned;
-}
-
-export interface IEphemeral {
- events: IMinimalEvent[];
-}
-
-/* eslint-disable camelcase */
-interface UnreadNotificationCounts {
- highlight_count?: number;
- notification_count?: number;
-}
-
-export interface IRoomEvent extends IMinimalEvent {
- event_id: string;
- sender: string;
- origin_server_ts: number;
- /** @deprecated - legacy field */
- age?: number;
-}
-
-export interface IStateEvent extends IRoomEvent {
- prev_content?: IContent;
- state_key: string;
-}
-
-interface IState {
- events: IStateEvent[];
-}
-
-export interface ITimeline {
- events: Array<IRoomEvent | IStateEvent>;
- limited?: boolean;
- prev_batch: string | null;
-}
-
-export interface IJoinedRoom {
- "summary": IRoomSummary;
- "state": IState;
- "timeline": ITimeline;
- "ephemeral": IEphemeral;
- "account_data": IAccountData;
- "unread_notifications": UnreadNotificationCounts;
- "unread_thread_notifications"?: Record<string, UnreadNotificationCounts>;
- "org.matrix.msc3773.unread_thread_notifications"?: Record<string, UnreadNotificationCounts>;
-}
-
-export interface IStrippedState {
- content: IContent;
- state_key: string;
- type: EventType | string;
- sender: string;
-}
-
-export interface IInviteState {
- events: IStrippedState[];
-}
-
-export interface IInvitedRoom {
- invite_state: IInviteState;
-}
-
-export interface ILeftRoom {
- state: IState;
- timeline: ITimeline;
- account_data: IAccountData;
-}
-
-export interface IRooms {
- [Category.Join]: Record<string, IJoinedRoom>;
- [Category.Invite]: Record<string, IInvitedRoom>;
- [Category.Leave]: Record<string, ILeftRoom>;
-}
-
-interface IPresence {
- events: IMinimalEvent[];
-}
-
-interface IAccountData {
- events: IMinimalEvent[];
-}
-
-export interface IToDeviceEvent {
- content: IContent;
- sender: string;
- type: string;
-}
-
-interface IToDevice {
- events: IToDeviceEvent[];
-}
-
-interface IDeviceLists {
- changed?: string[];
- left?: string[];
-}
-
-export interface ISyncResponse {
- "next_batch": string;
- "rooms": IRooms;
- "presence"?: IPresence;
- "account_data": IAccountData;
- "to_device"?: IToDevice;
- "device_lists"?: IDeviceLists;
- "device_one_time_keys_count"?: Record<string, number>;
-
- "device_unused_fallback_key_types"?: string[];
- "org.matrix.msc2732.device_unused_fallback_key_types"?: string[];
-}
-/* eslint-enable camelcase */
-
-export enum Category {
- Invite = "invite",
- Leave = "leave",
- Join = "join",
-}
-
-interface IRoom {
- _currentState: { [eventType: string]: { [stateKey: string]: IStateEvent } };
- _timeline: {
- event: IRoomEvent | IStateEvent;
- token: string | null;
- }[];
- _summary: Partial<IRoomSummary>;
- _accountData: { [eventType: string]: IMinimalEvent };
- _unreadNotifications: Partial<UnreadNotificationCounts>;
- _unreadThreadNotifications?: Record<string, Partial<UnreadNotificationCounts>>;
- _readReceipts: {
- [userId: string]: {
- data: IMinimalEvent;
- type: ReceiptType;
- eventId: string;
- };
- };
- _threadReadReceipts: {
- [threadId: string]: {
- [userId: string]: {
- data: IMinimalEvent;
- type: ReceiptType;
- eventId: string;
- };
- };
- };
-}
-
-export interface ISyncData {
- nextBatch: string;
- accountData: IMinimalEvent[];
- roomsData: IRooms;
-}
-
-type TaggedEvent = IRoomEvent & { _localTs?: number };
-
-function isTaggedEvent(event: IRoomEvent): event is TaggedEvent {
- return "_localTs" in event && event["_localTs"] !== undefined;
-}
-
-/**
- * The purpose of this class is to accumulate /sync responses such that a
- * complete "initial" JSON response can be returned which accurately represents
- * the sum total of the /sync responses accumulated to date. It only handles
- * room data: that is, everything under the "rooms" top-level key.
- *
- * This class is used when persisting room data so a complete /sync response can
- * be loaded from disk and incremental syncs can be performed on the server,
- * rather than asking the server to do an initial sync on startup.
- */
-export class SyncAccumulator {
- private accountData: Record<string, IMinimalEvent> = {}; // $event_type: Object
- private inviteRooms: Record<string, IInvitedRoom> = {}; // $roomId: { ... sync 'invite' json data ... }
- private joinRooms: { [roomId: string]: IRoom } = {};
- // the /sync token which corresponds to the last time rooms were
- // accumulated. We remember this so that any caller can obtain a
- // coherent /sync response and know at what point they should be
- // streaming from without losing events.
- private nextBatch: string | null = null;
-
- public constructor(private readonly opts: IOpts = {}) {
- this.opts.maxTimelineEntries = this.opts.maxTimelineEntries || 50;
- }
-
- public accumulate(syncResponse: ISyncResponse, fromDatabase = false): void {
- this.accumulateRooms(syncResponse, fromDatabase);
- this.accumulateAccountData(syncResponse);
- this.nextBatch = syncResponse.next_batch;
- }
-
- private accumulateAccountData(syncResponse: ISyncResponse): void {
- if (!syncResponse.account_data || !syncResponse.account_data.events) {
- return;
- }
- // Clobbers based on event type.
- syncResponse.account_data.events.forEach((e) => {
- this.accountData[e.type] = e;
- });
- }
-
- /**
- * Accumulate incremental /sync room data.
- * @param syncResponse - the complete /sync JSON
- * @param fromDatabase - True if the sync response is one saved to the database
- */
- private accumulateRooms(syncResponse: ISyncResponse, fromDatabase = false): void {
- if (!syncResponse.rooms) {
- return;
- }
- if (syncResponse.rooms.invite) {
- Object.keys(syncResponse.rooms.invite).forEach((roomId) => {
- this.accumulateRoom(roomId, Category.Invite, syncResponse.rooms.invite[roomId], fromDatabase);
- });
- }
- if (syncResponse.rooms.join) {
- Object.keys(syncResponse.rooms.join).forEach((roomId) => {
- this.accumulateRoom(roomId, Category.Join, syncResponse.rooms.join[roomId], fromDatabase);
- });
- }
- if (syncResponse.rooms.leave) {
- Object.keys(syncResponse.rooms.leave).forEach((roomId) => {
- this.accumulateRoom(roomId, Category.Leave, syncResponse.rooms.leave[roomId], fromDatabase);
- });
- }
- }
-
- private accumulateRoom(roomId: string, category: Category.Invite, data: IInvitedRoom, fromDatabase: boolean): void;
- private accumulateRoom(roomId: string, category: Category.Join, data: IJoinedRoom, fromDatabase: boolean): void;
- private accumulateRoom(roomId: string, category: Category.Leave, data: ILeftRoom, fromDatabase: boolean): void;
- private accumulateRoom(roomId: string, category: Category, data: any, fromDatabase = false): void {
- // Valid /sync state transitions
- // +--------+ <======+ 1: Accept an invite
- // +== | INVITE | | (5) 2: Leave a room
- // | +--------+ =====+ | 3: Join a public room previously
- // |(1) (4) | | left (handle as if new room)
- // V (2) V | 4: Reject an invite
- // +------+ ========> +--------+ 5: Invite to a room previously
- // | JOIN | (3) | LEAVE* | left (handle as if new room)
- // +------+ <======== +--------+
- //
- // * equivalent to "no state"
- switch (category) {
- case Category.Invite: // (5)
- this.accumulateInviteState(roomId, data as IInvitedRoom);
- break;
-
- case Category.Join:
- if (this.inviteRooms[roomId]) {
- // (1)
- // was previously invite, now join. We expect /sync to give
- // the entire state and timeline on 'join', so delete previous
- // invite state
- delete this.inviteRooms[roomId];
- }
- // (3)
- this.accumulateJoinState(roomId, data as IJoinedRoom, fromDatabase);
- break;
-
- case Category.Leave:
- if (this.inviteRooms[roomId]) {
- // (4)
- delete this.inviteRooms[roomId];
- } else {
- // (2)
- delete this.joinRooms[roomId];
- }
- break;
-
- default:
- logger.error("Unknown cateogory: ", category);
- }
- }
-
- private accumulateInviteState(roomId: string, data: IInvitedRoom): void {
- if (!data.invite_state || !data.invite_state.events) {
- // no new data
- return;
- }
- if (!this.inviteRooms[roomId]) {
- this.inviteRooms[roomId] = {
- invite_state: data.invite_state,
- };
- return;
- }
- // accumulate extra keys for invite->invite transitions
- // clobber based on event type / state key
- // We expect invite_state to be small, so just loop over the events
- const currentData = this.inviteRooms[roomId];
- data.invite_state.events.forEach((e) => {
- let hasAdded = false;
- for (let i = 0; i < currentData.invite_state.events.length; i++) {
- const current = currentData.invite_state.events[i];
- if (current.type === e.type && current.state_key == e.state_key) {
- currentData.invite_state.events[i] = e; // update
- hasAdded = true;
- }
- }
- if (!hasAdded) {
- currentData.invite_state.events.push(e);
- }
- });
- }
-
- // Accumulate timeline and state events in a room.
- private accumulateJoinState(roomId: string, data: IJoinedRoom, fromDatabase = false): void {
- // We expect this function to be called a lot (every /sync) so we want
- // this to be fast. /sync stores events in an array but we often want
- // to clobber based on type/state_key. Rather than convert arrays to
- // maps all the time, just keep private maps which contain
- // the actual current accumulated sync state, and array-ify it when
- // getJSON() is called.
-
- // State resolution:
- // The 'state' key is the delta from the previous sync (or start of time
- // if no token was supplied), to the START of the timeline. To obtain
- // the current state, we need to "roll forward" state by reading the
- // timeline. We want to store the current state so we can drop events
- // out the end of the timeline based on opts.maxTimelineEntries.
- //
- // 'state' 'timeline' current state
- // |-------x<======================>x
- // T I M E
- //
- // When getJSON() is called, we 'roll back' the current state by the
- // number of entries in the timeline to work out what 'state' should be.
-
- // Back-pagination:
- // On an initial /sync, the server provides a back-pagination token for
- // the start of the timeline. When /sync deltas come down, they also
- // include back-pagination tokens for the start of the timeline. This
- // means not all events in the timeline have back-pagination tokens, as
- // it is only the ones at the START of the timeline which have them.
- // In order for us to have a valid timeline (and back-pagination token
- // to match), we need to make sure that when we remove old timeline
- // events, that we roll forward to an event which has a back-pagination
- // token. This means we can't keep a strict sliding-window based on
- // opts.maxTimelineEntries, and we may have a few less. We should never
- // have more though, provided that the /sync limit is less than or equal
- // to opts.maxTimelineEntries.
-
- if (!this.joinRooms[roomId]) {
- // Create truly empty objects so event types of 'hasOwnProperty' and co
- // don't cause this code to break.
- this.joinRooms[roomId] = {
- _currentState: Object.create(null),
- _timeline: [],
- _accountData: Object.create(null),
- _unreadNotifications: {},
- _unreadThreadNotifications: {},
- _summary: {},
- _readReceipts: {},
- _threadReadReceipts: {},
- };
- }
- const currentData = this.joinRooms[roomId];
-
- if (data.account_data && data.account_data.events) {
- // clobber based on type
- data.account_data.events.forEach((e) => {
- currentData._accountData[e.type] = e;
- });
- }
-
- // these probably clobber, spec is unclear.
- if (data.unread_notifications) {
- currentData._unreadNotifications = data.unread_notifications;
- }
- currentData._unreadThreadNotifications =
- data[UNREAD_THREAD_NOTIFICATIONS.stable!] ?? data[UNREAD_THREAD_NOTIFICATIONS.unstable!] ?? undefined;
-
- if (data.summary) {
- const HEROES_KEY = "m.heroes";
- const INVITED_COUNT_KEY = "m.invited_member_count";
- const JOINED_COUNT_KEY = "m.joined_member_count";
-
- const acc = currentData._summary;
- const sum = data.summary;
- acc[HEROES_KEY] = sum[HEROES_KEY] || acc[HEROES_KEY];
- acc[JOINED_COUNT_KEY] = sum[JOINED_COUNT_KEY] || acc[JOINED_COUNT_KEY];
- acc[INVITED_COUNT_KEY] = sum[INVITED_COUNT_KEY] || acc[INVITED_COUNT_KEY];
- }
-
- data.ephemeral?.events?.forEach((e) => {
- // We purposefully do not persist m.typing events.
- // Technically you could refresh a browser before the timer on a
- // typing event is up, so it'll look like you aren't typing when
- // you really still are. However, the alternative is worse. If
- // we do persist typing events, it will look like people are
- // typing forever until someone really does start typing (which
- // will prompt Synapse to send down an actual m.typing event to
- // clobber the one we persisted).
- if (e.type !== EventType.Receipt || !e.content) {
- // This means we'll drop unknown ephemeral events but that
- // seems okay.
- return;
- }
- // Handle m.receipt events. They clobber based on:
- // (user_id, receipt_type)
- // but they are keyed in the event as:
- // content:{ $event_id: { $receipt_type: { $user_id: {json} }}}
- // so store them in the former so we can accumulate receipt deltas
- // quickly and efficiently (we expect a lot of them). Fold the
- // receipt type into the key name since we only have 1 at the
- // moment (m.read) and nested JSON objects are slower and more
- // of a hassle to work with. We'll inflate this back out when
- // getJSON() is called.
- Object.keys(e.content).forEach((eventId) => {
- Object.entries<ReceiptContent>(e.content[eventId]).forEach(([key, value]) => {
- if (!isSupportedReceiptType(key)) return;
-
- for (const userId of Object.keys(value)) {
- const data = e.content[eventId][key][userId];
-
- const receipt = {
- data: e.content[eventId][key][userId],
- type: key as ReceiptType,
- eventId: eventId,
- };
-
- if (!data.thread_id || data.thread_id === MAIN_ROOM_TIMELINE) {
- currentData._readReceipts[userId] = receipt;
- } else {
- currentData._threadReadReceipts = {
- ...currentData._threadReadReceipts,
- [data.thread_id]: {
- ...(currentData._threadReadReceipts[data.thread_id] ?? {}),
- [userId]: receipt,
- },
- };
- }
- }
- });
- });
- });
-
- // if we got a limited sync, we need to remove all timeline entries or else
- // we will have gaps in the timeline.
- if (data.timeline && data.timeline.limited) {
- currentData._timeline = [];
- }
-
- // Work out the current state. The deltas need to be applied in the order:
- // - existing state which didn't come down /sync.
- // - State events under the 'state' key.
- // - State events in the 'timeline'.
- data.state?.events?.forEach((e) => {
- setState(currentData._currentState, e);
- });
- data.timeline?.events?.forEach((e, index) => {
- // this nops if 'e' isn't a state event
- setState(currentData._currentState, e);
- // append the event to the timeline. The back-pagination token
- // corresponds to the first event in the timeline
- let transformedEvent: TaggedEvent;
- if (!fromDatabase) {
- transformedEvent = Object.assign({}, e);
- if (transformedEvent.unsigned !== undefined) {
- transformedEvent.unsigned = Object.assign({}, transformedEvent.unsigned);
- }
- const age = e.unsigned ? e.unsigned.age : e.age;
- if (age !== undefined) transformedEvent._localTs = Date.now() - age;
- } else {
- transformedEvent = e;
- }
-
- currentData._timeline.push({
- event: transformedEvent,
- token: index === 0 ? data.timeline.prev_batch ?? null : null,
- });
- });
-
- // attempt to prune the timeline by jumping between events which have
- // pagination tokens.
- if (currentData._timeline.length > this.opts.maxTimelineEntries!) {
- const startIndex = currentData._timeline.length - this.opts.maxTimelineEntries!;
- for (let i = startIndex; i < currentData._timeline.length; i++) {
- if (currentData._timeline[i].token) {
- // keep all events after this, including this one
- currentData._timeline = currentData._timeline.slice(i, currentData._timeline.length);
- break;
- }
- }
- }
- }
-
- /**
- * Return everything under the 'rooms' key from a /sync response which
- * represents all room data that should be stored. This should be paired
- * with the sync token which represents the most recent /sync response
- * provided to accumulate().
- * @param forDatabase - True to generate a sync to be saved to storage
- * @returns An object with a "nextBatch", "roomsData" and "accountData"
- * keys.
- * The "nextBatch" key is a string which represents at what point in the
- * /sync stream the accumulator reached. This token should be used when
- * restarting a /sync stream at startup. Failure to do so can lead to missing
- * events. The "roomsData" key is an Object which represents the entire
- * /sync response from the 'rooms' key onwards. The "accountData" key is
- * a list of raw events which represent global account data.
- */
- public getJSON(forDatabase = false): ISyncData {
- const data: IRooms = {
- join: {},
- invite: {},
- // always empty. This is set by /sync when a room was previously
- // in 'invite' or 'join'. On fresh startup, the client won't know
- // about any previous room being in 'invite' or 'join' so we can
- // just omit mentioning it at all, even if it has previously come
- // down /sync.
- // The notable exception is when a client is kicked or banned:
- // we may want to hold onto that room so the client can clearly see
- // why their room has disappeared. We don't persist it though because
- // it is unclear *when* we can safely remove the room from the DB.
- // Instead, we assume that if you're loading from the DB, you've
- // refreshed the page, which means you've seen the kick/ban already.
- leave: {},
- };
- Object.keys(this.inviteRooms).forEach((roomId) => {
- data.invite[roomId] = this.inviteRooms[roomId];
- });
- Object.keys(this.joinRooms).forEach((roomId) => {
- const roomData = this.joinRooms[roomId];
- const roomJson: IJoinedRoom = {
- ephemeral: { events: [] },
- account_data: { events: [] },
- state: { events: [] },
- timeline: {
- events: [],
- prev_batch: null,
- },
- unread_notifications: roomData._unreadNotifications,
- unread_thread_notifications: roomData._unreadThreadNotifications,
- summary: roomData._summary as IRoomSummary,
- };
- // Add account data
- Object.keys(roomData._accountData).forEach((evType) => {
- roomJson.account_data.events.push(roomData._accountData[evType]);
- });
-
- // Add receipt data
- const receiptEvent = {
- type: EventType.Receipt,
- room_id: roomId,
- content: {
- // $event_id: { "m.read": { $user_id: $json } }
- } as IContent,
- };
-
- const receiptEventContent: MapWithDefault<
- string,
- MapWithDefault<ReceiptType, Map<string, object>>
- > = new MapWithDefault(() => new MapWithDefault(() => new Map()));
-
- for (const [userId, receiptData] of Object.entries(roomData._readReceipts)) {
- receiptEventContent
- .getOrCreate(receiptData.eventId)
- .getOrCreate(receiptData.type)
- .set(userId, receiptData.data);
- }
-
- for (const threadReceipts of Object.values(roomData._threadReadReceipts)) {
- for (const [userId, receiptData] of Object.entries(threadReceipts)) {
- receiptEventContent
- .getOrCreate(receiptData.eventId)
- .getOrCreate(receiptData.type)
- .set(userId, receiptData.data);
- }
- }
-
- receiptEvent.content = recursiveMapToObject(receiptEventContent);
-
- // add only if we have some receipt data
- if (receiptEventContent.size > 0) {
- roomJson.ephemeral.events.push(receiptEvent as IMinimalEvent);
- }
-
- // Add timeline data
- roomData._timeline.forEach((msgData) => {
- if (!roomJson.timeline.prev_batch) {
- // the first event we add to the timeline MUST match up to
- // the prev_batch token.
- if (!msgData.token) {
- return; // this shouldn't happen as we prune constantly.
- }
- roomJson.timeline.prev_batch = msgData.token;
- }
-
- let transformedEvent: (IRoomEvent | IStateEvent) & { _localTs?: number };
- if (!forDatabase && isTaggedEvent(msgData.event)) {
- // This means we have to copy each event, so we can fix it up to
- // set a correct 'age' parameter whilst keeping the local timestamp
- // on our stored event. If this turns out to be a bottleneck, it could
- // be optimised either by doing this in the main process after the data
- // has been structured-cloned to go between the worker & main process,
- // or special-casing data from saved syncs to read the local timestamp
- // directly rather than turning it into age to then immediately be
- // transformed back again into a local timestamp.
- transformedEvent = Object.assign({}, msgData.event);
- if (transformedEvent.unsigned !== undefined) {
- transformedEvent.unsigned = Object.assign({}, transformedEvent.unsigned);
- }
- delete transformedEvent._localTs;
- transformedEvent.unsigned = transformedEvent.unsigned || {};
- transformedEvent.unsigned.age = Date.now() - msgData.event._localTs!;
- } else {
- transformedEvent = msgData.event;
- }
- roomJson.timeline.events.push(transformedEvent);
- });
-
- // Add state data: roll back current state to the start of timeline,
- // by "reverse clobbering" from the end of the timeline to the start.
- // Convert maps back into arrays.
- const rollBackState = Object.create(null);
- for (let i = roomJson.timeline.events.length - 1; i >= 0; i--) {
- const timelineEvent = roomJson.timeline.events[i];
- if (
- (timelineEvent as IStateEvent).state_key === null ||
- (timelineEvent as IStateEvent).state_key === undefined
- ) {
- continue; // not a state event
- }
- // since we're going back in time, we need to use the previous
- // state value else we'll break causality. We don't have the
- // complete previous state event, so we need to create one.
- const prevStateEvent = deepCopy(timelineEvent);
- if (prevStateEvent.unsigned) {
- if (prevStateEvent.unsigned.prev_content) {
- prevStateEvent.content = prevStateEvent.unsigned.prev_content;
- }
- if (prevStateEvent.unsigned.prev_sender) {
- prevStateEvent.sender = prevStateEvent.unsigned.prev_sender;
- }
- }
- setState(rollBackState, prevStateEvent);
- }
- Object.keys(roomData._currentState).forEach((evType) => {
- Object.keys(roomData._currentState[evType]).forEach((stateKey) => {
- let ev = roomData._currentState[evType][stateKey];
- if (rollBackState[evType] && rollBackState[evType][stateKey]) {
- // use the reverse clobbered event instead.
- ev = rollBackState[evType][stateKey];
- }
- roomJson.state.events.push(ev);
- });
- });
- data.join[roomId] = roomJson;
- });
-
- // Add account data
- const accData: IMinimalEvent[] = [];
- Object.keys(this.accountData).forEach((evType) => {
- accData.push(this.accountData[evType]);
- });
-
- return {
- nextBatch: this.nextBatch!,
- roomsData: data,
- accountData: accData,
- };
- }
-
- public getNextBatchToken(): string {
- return this.nextBatch!;
- }
-}
-
-function setState(eventMap: Record<string, Record<string, IStateEvent>>, event: IRoomEvent | IStateEvent): void {
- if ((event as IStateEvent).state_key === null || (event as IStateEvent).state_key === undefined || !event.type) {
- return;
- }
- if (!eventMap[event.type]) {
- eventMap[event.type] = Object.create(null);
- }
- eventMap[event.type][(event as IStateEvent).state_key] = event as IStateEvent;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/sync.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/sync.ts
deleted file mode 100644
index dc5217c..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/sync.ts
+++ /dev/null
@@ -1,1898 +0,0 @@
-/*
-Copyright 2015 - 2023 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.
-*/
-
-/*
- * TODO:
- * This class mainly serves to take all the syncing logic out of client.js and
- * into a separate file. It's all very fluid, and this class gut wrenches a lot
- * of MatrixClient props (e.g. http). Given we want to support WebSockets as
- * an alternative syncing API, we may want to have a proper syncing interface
- * for HTTP and WS at some point.
- */
-
-import { Optional } from "matrix-events-sdk";
-
-import type { SyncCryptoCallbacks } from "./common-crypto/CryptoBackend";
-import { User, UserEvent } from "./models/user";
-import { NotificationCountType, Room, RoomEvent } from "./models/room";
-import * as utils from "./utils";
-import { IDeferred, noUnsafeEventProps, unsafeProp } from "./utils";
-import { Filter } from "./filter";
-import { EventTimeline } from "./models/event-timeline";
-import { logger } from "./logger";
-import { InvalidStoreError, InvalidStoreState } from "./errors";
-import { ClientEvent, IStoredClientOpts, MatrixClient, PendingEventOrdering, ResetTimelineCallback } from "./client";
-import {
- IEphemeral,
- IInvitedRoom,
- IInviteState,
- IJoinedRoom,
- ILeftRoom,
- IMinimalEvent,
- IRoomEvent,
- IStateEvent,
- IStrippedState,
- ISyncResponse,
- ITimeline,
- IToDeviceEvent,
-} from "./sync-accumulator";
-import { MatrixEvent } from "./models/event";
-import { MatrixError, Method } from "./http-api";
-import { ISavedSync } from "./store";
-import { EventType } from "./@types/event";
-import { IPushRules } from "./@types/PushRules";
-import { RoomStateEvent, IMarkerFoundOptions } from "./models/room-state";
-import { RoomMemberEvent } from "./models/room-member";
-import { BeaconEvent } from "./models/beacon";
-import { IEventsResponse } from "./@types/requests";
-import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync";
-import { Feature, ServerSupport } from "./feature";
-import { Crypto } from "./crypto";
-
-const DEBUG = true;
-
-// /sync requests allow you to set a timeout= but the request may continue
-// beyond that and wedge forever, so we need to track how long we are willing
-// to keep open the connection. This constant is *ADDED* to the timeout= value
-// to determine the max time we're willing to wait.
-const BUFFER_PERIOD_MS = 80 * 1000;
-
-// Number of consecutive failed syncs that will lead to a syncState of ERROR as opposed
-// to RECONNECTING. This is needed to inform the client of server issues when the
-// keepAlive is successful but the server /sync fails.
-const FAILED_SYNC_ERROR_THRESHOLD = 3;
-
-export enum SyncState {
- /** Emitted after we try to sync more than `FAILED_SYNC_ERROR_THRESHOLD`
- * times and are still failing. Or when we enounter a hard error like the
- * token being invalid. */
- Error = "ERROR",
- /** Emitted after the first sync events are ready (this could even be sync
- * events from the cache) */
- Prepared = "PREPARED",
- /** Emitted when the sync loop is no longer running */
- Stopped = "STOPPED",
- /** Emitted after each sync request happens */
- Syncing = "SYNCING",
- /** Emitted after a connectivity error and we're ready to start syncing again */
- Catchup = "CATCHUP",
- /** Emitted for each time we try reconnecting. Will switch to `Error` after
- * we reach the `FAILED_SYNC_ERROR_THRESHOLD`
- */
- Reconnecting = "RECONNECTING",
-}
-
-// Room versions where "insertion", "batch", and "marker" events are controlled
-// by power-levels. MSC2716 is supported in existing room versions but they
-// should only have special meaning when the room creator sends them.
-const MSC2716_ROOM_VERSIONS = ["org.matrix.msc2716v3"];
-
-function getFilterName(userId: string, suffix?: string): string {
- // scope this on the user ID because people may login on many accounts
- // and they all need to be stored!
- return `FILTER_SYNC_${userId}` + (suffix ? "_" + suffix : "");
-}
-
-/* istanbul ignore next */
-function debuglog(...params: any[]): void {
- if (!DEBUG) return;
- logger.log(...params);
-}
-
-/**
- * Options passed into the constructor of SyncApi by MatrixClient
- */
-export interface SyncApiOptions {
- /**
- * Crypto manager
- *
- * @deprecated in favour of cryptoCallbacks
- */
- crypto?: Crypto;
-
- /**
- * If crypto is enabled on our client, callbacks into the crypto module
- */
- cryptoCallbacks?: SyncCryptoCallbacks;
-
- /**
- * A function which is called
- * with a room ID and returns a boolean. It should return 'true' if the SDK can
- * SAFELY remove events from this room. It may not be safe to remove events if
- * there are other references to the timelines for this room.
- */
- canResetEntireTimeline?: ResetTimelineCallback;
-}
-
-interface ISyncOptions {
- filter?: string;
- hasSyncedBefore?: boolean;
-}
-
-export interface ISyncStateData {
- /**
- * The matrix error if `state=ERROR`.
- */
- error?: Error;
- /**
- * The 'since' token passed to /sync.
- * `null` for the first successful sync since this client was
- * started. Only present if `state=PREPARED` or
- * `state=SYNCING`.
- */
- oldSyncToken?: string;
- /**
- * The 'next_batch' result from /sync, which
- * will become the 'since' token for the next call to /sync. Only present if
- * `state=PREPARED</code> or <code>state=SYNCING`.
- */
- nextSyncToken?: string;
- /**
- * True if we are working our way through a
- * backlog of events after connecting. Only present if `state=SYNCING`.
- */
- catchingUp?: boolean;
- fromCache?: boolean;
-}
-
-enum SetPresence {
- Offline = "offline",
- Online = "online",
- Unavailable = "unavailable",
-}
-
-interface ISyncParams {
- filter?: string;
- timeout: number;
- since?: string;
- // eslint-disable-next-line camelcase
- full_state?: boolean;
- // eslint-disable-next-line camelcase
- set_presence?: SetPresence;
- _cacheBuster?: string | number; // not part of the API itself
-}
-
-type WrappedRoom<T> = T & {
- room: Room;
- isBrandNewRoom: boolean;
-};
-
-/** add default settings to an IStoredClientOpts */
-export function defaultClientOpts(opts?: IStoredClientOpts): IStoredClientOpts {
- return {
- initialSyncLimit: 8,
- resolveInvitesToProfiles: false,
- pollTimeout: 30 * 1000,
- pendingEventOrdering: PendingEventOrdering.Chronological,
- threadSupport: false,
- ...opts,
- };
-}
-
-export function defaultSyncApiOpts(syncOpts?: SyncApiOptions): SyncApiOptions {
- return {
- canResetEntireTimeline: (_roomId): boolean => false,
- ...syncOpts,
- };
-}
-
-export class SyncApi {
- private readonly opts: IStoredClientOpts;
- private readonly syncOpts: SyncApiOptions;
-
- private _peekRoom: Optional<Room> = null;
- private currentSyncRequest?: Promise<ISyncResponse>;
- private abortController?: AbortController;
- private syncState: SyncState | null = null;
- private syncStateData?: ISyncStateData; // additional data (eg. error object for failed sync)
- private catchingUp = false;
- private running = false;
- private keepAliveTimer?: ReturnType<typeof setTimeout>;
- private connectionReturnedDefer?: IDeferred<boolean>;
- private notifEvents: MatrixEvent[] = []; // accumulator of sync events in the current sync response
- private failedSyncCount = 0; // Number of consecutive failed /sync requests
- private storeIsInvalid = false; // flag set if the store needs to be cleared before we can start
-
- /**
- * Construct an entity which is able to sync with a homeserver.
- * @param client - The matrix client instance to use.
- * @param opts - client config options
- * @param syncOpts - sync-specific options passed by the client
- * @internal
- */
- public constructor(private readonly client: MatrixClient, opts?: IStoredClientOpts, syncOpts?: SyncApiOptions) {
- this.opts = defaultClientOpts(opts);
- this.syncOpts = defaultSyncApiOpts(syncOpts);
-
- if (client.getNotifTimelineSet()) {
- client.reEmitter.reEmit(client.getNotifTimelineSet()!, [RoomEvent.Timeline, RoomEvent.TimelineReset]);
- }
- }
-
- public createRoom(roomId: string): Room {
- const room = _createAndReEmitRoom(this.client, roomId, this.opts);
-
- room.on(RoomStateEvent.Marker, (markerEvent, markerFoundOptions) => {
- this.onMarkerStateEvent(room, markerEvent, markerFoundOptions);
- });
-
- return room;
- }
-
- /** When we see the marker state change in the room, we know there is some
- * new historical messages imported by MSC2716 `/batch_send` somewhere in
- * the room and we need to throw away the timeline to make sure the
- * historical messages are shown when we paginate `/messages` again.
- * @param room - The room where the marker event was sent
- * @param markerEvent - The new marker event
- * @param setStateOptions - When `timelineWasEmpty` is set
- * as `true`, the given marker event will be ignored
- */
- private onMarkerStateEvent(
- room: Room,
- markerEvent: MatrixEvent,
- { timelineWasEmpty }: IMarkerFoundOptions = {},
- ): void {
- // We don't need to refresh the timeline if it was empty before the
- // marker arrived. This could be happen in a variety of cases:
- // 1. From the initial sync
- // 2. If it's from the first state we're seeing after joining the room
- // 3. Or whether it's coming from `syncFromCache`
- if (timelineWasEmpty) {
- logger.debug(
- `MarkerState: Ignoring markerEventId=${markerEvent.getId()} in roomId=${room.roomId} ` +
- `because the timeline was empty before the marker arrived which means there is nothing to refresh.`,
- );
- return;
- }
-
- const isValidMsc2716Event =
- // Check whether the room version directly supports MSC2716, in
- // which case, "marker" events are already auth'ed by
- // power_levels
- MSC2716_ROOM_VERSIONS.includes(room.getVersion()) ||
- // MSC2716 is also supported in all existing room versions but
- // special meaning should only be given to "insertion", "batch",
- // and "marker" events when they come from the room creator
- markerEvent.getSender() === room.getCreator();
-
- // It would be nice if we could also specifically tell whether the
- // historical messages actually affected the locally cached client
- // timeline or not. The problem is we can't see the prev_events of
- // the base insertion event that the marker was pointing to because
- // prev_events aren't available in the client API's. In most cases,
- // the history won't be in people's locally cached timelines in the
- // client, so we don't need to bother everyone about refreshing
- // their timeline. This works for a v1 though and there are use
- // cases like initially bootstrapping your bridged room where people
- // are likely to encounter the historical messages affecting their
- // current timeline (think someone signing up for Beeper and
- // importing their Whatsapp history).
- if (isValidMsc2716Event) {
- // Saw new marker event, let's let the clients know they should
- // refresh the timeline.
- logger.debug(
- `MarkerState: Timeline needs to be refreshed because ` +
- `a new markerEventId=${markerEvent.getId()} was sent in roomId=${room.roomId}`,
- );
- room.setTimelineNeedsRefresh(true);
- room.emit(RoomEvent.HistoryImportedWithinTimeline, markerEvent, room);
- } else {
- logger.debug(
- `MarkerState: Ignoring markerEventId=${markerEvent.getId()} in roomId=${room.roomId} because ` +
- `MSC2716 is not supported in the room version or for any room version, the marker wasn't sent ` +
- `by the room creator.`,
- );
- }
- }
-
- /**
- * Sync rooms the user has left.
- * @returns Resolved when they've been added to the store.
- */
- public async syncLeftRooms(): Promise<Room[]> {
- const client = this.client;
-
- // grab a filter with limit=1 and include_leave=true
- const filter = new Filter(this.client.credentials.userId);
- filter.setTimelineLimit(1);
- filter.setIncludeLeaveRooms(true);
-
- const localTimeoutMs = this.opts.pollTimeout! + BUFFER_PERIOD_MS;
-
- const filterId = await client.getOrCreateFilter(
- getFilterName(client.credentials.userId!, "LEFT_ROOMS"),
- filter,
- );
-
- const qps: ISyncParams = {
- timeout: 0, // don't want to block since this is a single isolated req
- filter: filterId,
- };
-
- const data = await client.http.authedRequest<ISyncResponse>(Method.Get, "/sync", qps as any, undefined, {
- localTimeoutMs,
- });
-
- let leaveRooms: WrappedRoom<ILeftRoom>[] = [];
- if (data.rooms?.leave) {
- leaveRooms = this.mapSyncResponseToRoomArray(data.rooms.leave);
- }
-
- const rooms = await Promise.all(
- leaveRooms.map(async (leaveObj) => {
- const room = leaveObj.room;
- if (!leaveObj.isBrandNewRoom) {
- // the intention behind syncLeftRooms is to add in rooms which were
- // *omitted* from the initial /sync. Rooms the user were joined to
- // but then left whilst the app is running will appear in this list
- // and we do not want to bother with them since they will have the
- // current state already (and may get dupe messages if we add
- // yet more timeline events!), so skip them.
- // NB: When we persist rooms to localStorage this will be more
- // complicated...
- return;
- }
- leaveObj.timeline = leaveObj.timeline || {
- prev_batch: null,
- events: [],
- };
- const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
-
- const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
-
- // set the back-pagination token. Do this *before* adding any
- // events so that clients can start back-paginating.
- room.getLiveTimeline().setPaginationToken(leaveObj.timeline.prev_batch, EventTimeline.BACKWARDS);
-
- await this.injectRoomEvents(room, stateEvents, events);
-
- room.recalculate();
- client.store.storeRoom(room);
- client.emit(ClientEvent.Room, room);
-
- this.processEventsForNotifs(room, events);
- return room;
- }),
- );
-
- return rooms.filter(Boolean) as Room[];
- }
-
- /**
- * Peek into a room. This will result in the room in question being synced so it
- * is accessible via getRooms(). Live updates for the room will be provided.
- * @param roomId - The room ID to peek into.
- * @returns A promise which resolves once the room has been added to the
- * store.
- */
- public peek(roomId: string): Promise<Room> {
- if (this._peekRoom?.roomId === roomId) {
- return Promise.resolve(this._peekRoom);
- }
-
- const client = this.client;
- this._peekRoom = this.createRoom(roomId);
- return this.client.roomInitialSync(roomId, 20).then((response) => {
- // make sure things are init'd
- response.messages = response.messages || { chunk: [] };
- response.messages.chunk = response.messages.chunk || [];
- response.state = response.state || [];
-
- // FIXME: Mostly duplicated from injectRoomEvents but not entirely
- // because "state" in this API is at the BEGINNING of the chunk
- const oldStateEvents = utils.deepCopy(response.state).map(client.getEventMapper());
- const stateEvents = response.state.map(client.getEventMapper());
- const messages = response.messages.chunk.map(client.getEventMapper());
-
- // XXX: copypasted from /sync until we kill off this minging v1 API stuff)
- // handle presence events (User objects)
- if (Array.isArray(response.presence)) {
- response.presence.map(client.getEventMapper()).forEach(function (presenceEvent) {
- let user = client.store.getUser(presenceEvent.getContent().user_id);
- if (user) {
- user.setPresenceEvent(presenceEvent);
- } else {
- user = createNewUser(client, presenceEvent.getContent().user_id);
- user.setPresenceEvent(presenceEvent);
- client.store.storeUser(user);
- }
- client.emit(ClientEvent.Event, presenceEvent);
- });
- }
-
- // set the pagination token before adding the events in case people
- // fire off pagination requests in response to the Room.timeline
- // events.
- if (response.messages.start) {
- this._peekRoom!.oldState.paginationToken = response.messages.start;
- }
-
- // set the state of the room to as it was after the timeline executes
- this._peekRoom!.oldState.setStateEvents(oldStateEvents);
- this._peekRoom!.currentState.setStateEvents(stateEvents);
-
- this.resolveInvites(this._peekRoom!);
- this._peekRoom!.recalculate();
-
- // roll backwards to diverge old state. addEventsToTimeline
- // will overwrite the pagination token, so make sure it overwrites
- // it with the right thing.
- this._peekRoom!.addEventsToTimeline(
- messages.reverse(),
- true,
- this._peekRoom!.getLiveTimeline(),
- response.messages.start,
- );
-
- client.store.storeRoom(this._peekRoom!);
- client.emit(ClientEvent.Room, this._peekRoom!);
-
- this.peekPoll(this._peekRoom!);
- return this._peekRoom!;
- });
- }
-
- /**
- * Stop polling for updates in the peeked room. NOPs if there is no room being
- * peeked.
- */
- public stopPeeking(): void {
- this._peekRoom = null;
- }
-
- /**
- * Do a peek room poll.
- * @param token - from= token
- */
- private peekPoll(peekRoom: Room, token?: string): void {
- if (this._peekRoom !== peekRoom) {
- debuglog("Stopped peeking in room %s", peekRoom.roomId);
- return;
- }
-
- // FIXME: gut wrenching; hard-coded timeout values
- this.client.http
- .authedRequest<IEventsResponse>(
- Method.Get,
- "/events",
- {
- room_id: peekRoom.roomId,
- timeout: String(30 * 1000),
- from: token,
- },
- undefined,
- {
- localTimeoutMs: 50 * 1000,
- abortSignal: this.abortController?.signal,
- },
- )
- .then(
- (res) => {
- if (this._peekRoom !== peekRoom) {
- debuglog("Stopped peeking in room %s", peekRoom.roomId);
- return;
- }
- // We have a problem that we get presence both from /events and /sync
- // however, /sync only returns presence for users in rooms
- // you're actually joined to.
- // in order to be sure to get presence for all of the users in the
- // peeked room, we handle presence explicitly here. This may result
- // in duplicate presence events firing for some users, which is a
- // performance drain, but such is life.
- // XXX: copypasted from /sync until we can kill this minging v1 stuff.
-
- res.chunk
- .filter(function (e) {
- return e.type === "m.presence";
- })
- .map(this.client.getEventMapper())
- .forEach((presenceEvent) => {
- let user = this.client.store.getUser(presenceEvent.getContent().user_id);
- if (user) {
- user.setPresenceEvent(presenceEvent);
- } else {
- user = createNewUser(this.client, presenceEvent.getContent().user_id);
- user.setPresenceEvent(presenceEvent);
- this.client.store.storeUser(user);
- }
- this.client.emit(ClientEvent.Event, presenceEvent);
- });
-
- // strip out events which aren't for the given room_id (e.g presence)
- // and also ephemeral events (which we're assuming is anything without
- // and event ID because the /events API doesn't separate them).
- const events = res.chunk
- .filter(function (e) {
- return e.room_id === peekRoom.roomId && e.event_id;
- })
- .map(this.client.getEventMapper());
-
- peekRoom.addLiveEvents(events);
- this.peekPoll(peekRoom, res.end);
- },
- (err) => {
- logger.error("[%s] Peek poll failed: %s", peekRoom.roomId, err);
- setTimeout(() => {
- this.peekPoll(peekRoom, token);
- }, 30 * 1000);
- },
- );
- }
-
- /**
- * Returns the current state of this sync object
- * @see MatrixClient#event:"sync"
- */
- public getSyncState(): SyncState | null {
- return this.syncState;
- }
-
- /**
- * Returns the additional data object associated with
- * the current sync state, or null if there is no
- * such data.
- * Sync errors, if available, are put in the 'error' key of
- * this object.
- */
- public getSyncStateData(): ISyncStateData | null {
- return this.syncStateData ?? null;
- }
-
- public async recoverFromSyncStartupError(savedSyncPromise: Promise<void> | undefined, error: Error): Promise<void> {
- // Wait for the saved sync to complete - we send the pushrules and filter requests
- // before the saved sync has finished so they can run in parallel, but only process
- // the results after the saved sync is done. Equivalently, we wait for it to finish
- // before reporting failures from these functions.
- await savedSyncPromise;
- const keepaliveProm = this.startKeepAlives();
- this.updateSyncState(SyncState.Error, { error });
- await keepaliveProm;
- }
-
- /**
- * Is the lazy loading option different than in previous session?
- * @param lazyLoadMembers - current options for lazy loading
- * @returns whether or not the option has changed compared to the previous session */
- private async wasLazyLoadingToggled(lazyLoadMembers = false): Promise<boolean> {
- // assume it was turned off before
- // if we don't know any better
- let lazyLoadMembersBefore = false;
- const isStoreNewlyCreated = await this.client.store.isNewlyCreated();
- if (!isStoreNewlyCreated) {
- const prevClientOptions = await this.client.store.getClientOptions();
- if (prevClientOptions) {
- lazyLoadMembersBefore = !!prevClientOptions.lazyLoadMembers;
- }
- return lazyLoadMembersBefore !== lazyLoadMembers;
- }
- return false;
- }
-
- private shouldAbortSync(error: MatrixError): boolean {
- if (error.errcode === "M_UNKNOWN_TOKEN") {
- // The logout already happened, we just need to stop.
- logger.warn("Token no longer valid - assuming logout");
- this.stop();
- this.updateSyncState(SyncState.Error, { error });
- return true;
- }
- return false;
- }
-
- private getPushRules = async (): Promise<void> => {
- try {
- debuglog("Getting push rules...");
- const result = await this.client.getPushRules();
- debuglog("Got push rules");
-
- this.client.pushRules = result;
- } catch (err) {
- logger.error("Getting push rules failed", err);
- if (this.shouldAbortSync(<MatrixError>err)) return;
- // wait for saved sync to complete before doing anything else,
- // otherwise the sync state will end up being incorrect
- debuglog("Waiting for saved sync before retrying push rules...");
- await this.recoverFromSyncStartupError(this.savedSyncPromise, <Error>err);
- return this.getPushRules(); // try again
- }
- };
-
- private buildDefaultFilter = (): Filter => {
- const filter = new Filter(this.client.credentials.userId);
- if (this.client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported) {
- filter.setUnreadThreadNotifications(true);
- }
- return filter;
- };
-
- private checkLazyLoadStatus = async (): Promise<void> => {
- debuglog("Checking lazy load status...");
- if (this.opts.lazyLoadMembers && this.client.isGuest()) {
- this.opts.lazyLoadMembers = false;
- }
- if (this.opts.lazyLoadMembers) {
- debuglog("Checking server lazy load support...");
- const supported = await this.client.doesServerSupportLazyLoading();
- if (supported) {
- debuglog("Enabling lazy load on sync filter...");
- if (!this.opts.filter) {
- this.opts.filter = this.buildDefaultFilter();
- }
- this.opts.filter.setLazyLoadMembers(true);
- } else {
- debuglog("LL: lazy loading requested but not supported " + "by server, so disabling");
- this.opts.lazyLoadMembers = false;
- }
- }
- // need to vape the store when enabling LL and wasn't enabled before
- debuglog("Checking whether lazy loading has changed in store...");
- const shouldClear = await this.wasLazyLoadingToggled(this.opts.lazyLoadMembers);
- if (shouldClear) {
- this.storeIsInvalid = true;
- const error = new InvalidStoreError(InvalidStoreState.ToggledLazyLoading, !!this.opts.lazyLoadMembers);
- this.updateSyncState(SyncState.Error, { error });
- // bail out of the sync loop now: the app needs to respond to this error.
- // we leave the state as 'ERROR' which isn't great since this normally means
- // we're retrying. The client must be stopped before clearing the stores anyway
- // so the app should stop the client, clear the store and start it again.
- logger.warn("InvalidStoreError: store is not usable: stopping sync.");
- return;
- }
- if (this.opts.lazyLoadMembers) {
- this.syncOpts.crypto?.enableLazyLoading();
- }
- try {
- debuglog("Storing client options...");
- await this.client.storeClientOptions();
- debuglog("Stored client options");
- } catch (err) {
- logger.error("Storing client options failed", err);
- throw err;
- }
- };
-
- private getFilter = async (): Promise<{
- filterId?: string;
- filter?: Filter;
- }> => {
- debuglog("Getting filter...");
- let filter: Filter;
- if (this.opts.filter) {
- filter = this.opts.filter;
- } else {
- filter = this.buildDefaultFilter();
- }
-
- let filterId: string;
- try {
- filterId = await this.client.getOrCreateFilter(getFilterName(this.client.credentials.userId!), filter);
- } catch (err) {
- logger.error("Getting filter failed", err);
- if (this.shouldAbortSync(<MatrixError>err)) return {};
- // wait for saved sync to complete before doing anything else,
- // otherwise the sync state will end up being incorrect
- debuglog("Waiting for saved sync before retrying filter...");
- await this.recoverFromSyncStartupError(this.savedSyncPromise, <Error>err);
- return this.getFilter(); // try again
- }
- return { filter, filterId };
- };
-
- private savedSyncPromise?: Promise<void>;
-
- /**
- * Main entry point
- */
- public async sync(): Promise<void> {
- this.running = true;
- this.abortController = new AbortController();
-
- global.window?.addEventListener?.("online", this.onOnline, false);
-
- if (this.client.isGuest()) {
- // no push rules for guests, no access to POST filter for guests.
- return this.doSync({});
- }
-
- // Pull the saved sync token out first, before the worker starts sending
- // all the sync data which could take a while. This will let us send our
- // first incremental sync request before we've processed our saved data.
- debuglog("Getting saved sync token...");
- const savedSyncTokenPromise = this.client.store.getSavedSyncToken().then((tok) => {
- debuglog("Got saved sync token");
- return tok;
- });
-
- this.savedSyncPromise = this.client.store
- .getSavedSync()
- .then((savedSync) => {
- debuglog(`Got reply from saved sync, exists? ${!!savedSync}`);
- if (savedSync) {
- return this.syncFromCache(savedSync);
- }
- })
- .catch((err) => {
- logger.error("Getting saved sync failed", err);
- });
-
- // We need to do one-off checks before we can begin the /sync loop.
- // These are:
- // 1) We need to get push rules so we can check if events should bing as we get
- // them from /sync.
- // 2) We need to get/create a filter which we can use for /sync.
- // 3) We need to check the lazy loading option matches what was used in the
- // stored sync. If it doesn't, we can't use the stored sync.
-
- // Now start the first incremental sync request: this can also
- // take a while so if we set it going now, we can wait for it
- // to finish while we process our saved sync data.
- await this.getPushRules();
- await this.checkLazyLoadStatus();
- const { filterId, filter } = await this.getFilter();
- if (!filter) return; // bail, getFilter failed
-
- // reset the notifications timeline to prepare it to paginate from
- // the current point in time.
- // The right solution would be to tie /sync pagination tokens into
- // /notifications API somehow.
- this.client.resetNotifTimelineSet();
-
- if (!this.currentSyncRequest) {
- let firstSyncFilter = filterId;
- const savedSyncToken = await savedSyncTokenPromise;
-
- if (savedSyncToken) {
- debuglog("Sending first sync request...");
- } else {
- debuglog("Sending initial sync request...");
- const initialFilter = this.buildDefaultFilter();
- initialFilter.setDefinition(filter.getDefinition());
- initialFilter.setTimelineLimit(this.opts.initialSyncLimit!);
- // Use an inline filter, no point uploading it for a single usage
- firstSyncFilter = JSON.stringify(initialFilter.getDefinition());
- }
-
- // Send this first sync request here so we can then wait for the saved
- // sync data to finish processing before we process the results of this one.
- this.currentSyncRequest = this.doSyncRequest({ filter: firstSyncFilter }, savedSyncToken);
- }
-
- // Now wait for the saved sync to finish...
- debuglog("Waiting for saved sync before starting sync processing...");
- await this.savedSyncPromise;
- // process the first sync request and continue syncing with the normal filterId
- return this.doSync({ filter: filterId });
- }
-
- /**
- * Stops the sync object from syncing.
- */
- public stop(): void {
- debuglog("SyncApi.stop");
- // It is necessary to check for the existance of
- // global.window AND global.window.removeEventListener.
- // Some platforms (e.g. React Native) register global.window,
- // but do not have global.window.removeEventListener.
- global.window?.removeEventListener?.("online", this.onOnline, false);
- this.running = false;
- this.abortController?.abort();
- if (this.keepAliveTimer) {
- clearTimeout(this.keepAliveTimer);
- this.keepAliveTimer = undefined;
- }
- }
-
- /**
- * Retry a backed off syncing request immediately. This should only be used when
- * the user <b>explicitly</b> attempts to retry their lost connection.
- * @returns True if this resulted in a request being retried.
- */
- public retryImmediately(): boolean {
- if (!this.connectionReturnedDefer) {
- return false;
- }
- this.startKeepAlives(0);
- return true;
- }
- /**
- * Process a single set of cached sync data.
- * @param savedSync - a saved sync that was persisted by a store. This
- * should have been acquired via client.store.getSavedSync().
- */
- private async syncFromCache(savedSync: ISavedSync): Promise<void> {
- debuglog("sync(): not doing HTTP hit, instead returning stored /sync data");
-
- const nextSyncToken = savedSync.nextBatch;
-
- // Set sync token for future incremental syncing
- this.client.store.setSyncToken(nextSyncToken);
-
- // No previous sync, set old token to null
- const syncEventData: ISyncStateData = {
- nextSyncToken,
- catchingUp: false,
- fromCache: true,
- };
-
- const data: ISyncResponse = {
- next_batch: nextSyncToken,
- rooms: savedSync.roomsData,
- account_data: {
- events: savedSync.accountData,
- },
- };
-
- try {
- await this.processSyncResponse(syncEventData, data);
- } catch (e) {
- logger.error("Error processing cached sync", e);
- }
-
- // Don't emit a prepared if we've bailed because the store is invalid:
- // in this case the client will not be usable until stopped & restarted
- // so this would be useless and misleading.
- if (!this.storeIsInvalid) {
- this.updateSyncState(SyncState.Prepared, syncEventData);
- }
- }
-
- /**
- * Invoke me to do /sync calls
- */
- private async doSync(syncOptions: ISyncOptions): Promise<void> {
- while (this.running) {
- const syncToken = this.client.store.getSyncToken();
-
- let data: ISyncResponse;
- try {
- if (!this.currentSyncRequest) {
- this.currentSyncRequest = this.doSyncRequest(syncOptions, syncToken);
- }
- data = await this.currentSyncRequest;
- } catch (e) {
- const abort = await this.onSyncError(<MatrixError>e);
- if (abort) return;
- continue;
- } finally {
- this.currentSyncRequest = undefined;
- }
-
- // set the sync token NOW *before* processing the events. We do this so
- // if something barfs on an event we can skip it rather than constantly
- // polling with the same token.
- this.client.store.setSyncToken(data.next_batch);
-
- // Reset after a successful sync
- this.failedSyncCount = 0;
-
- await this.client.store.setSyncData(data);
-
- const syncEventData = {
- oldSyncToken: syncToken ?? undefined,
- nextSyncToken: data.next_batch,
- catchingUp: this.catchingUp,
- };
-
- if (this.syncOpts.crypto) {
- // tell the crypto module we're about to process a sync
- // response
- await this.syncOpts.crypto.onSyncWillProcess(syncEventData);
- }
-
- try {
- await this.processSyncResponse(syncEventData, data);
- } catch (e) {
- // log the exception with stack if we have it, else fall back
- // to the plain description
- logger.error("Caught /sync error", e);
-
- // Emit the exception for client handling
- this.client.emit(ClientEvent.SyncUnexpectedError, <Error>e);
- }
-
- // update this as it may have changed
- syncEventData.catchingUp = this.catchingUp;
-
- // emit synced events
- if (!syncOptions.hasSyncedBefore) {
- this.updateSyncState(SyncState.Prepared, syncEventData);
- syncOptions.hasSyncedBefore = true;
- }
-
- // tell the crypto module to do its processing. It may block (to do a
- // /keys/changes request).
- if (this.syncOpts.cryptoCallbacks) {
- await this.syncOpts.cryptoCallbacks.onSyncCompleted(syncEventData);
- }
-
- // keep emitting SYNCING -> SYNCING for clients who want to do bulk updates
- this.updateSyncState(SyncState.Syncing, syncEventData);
-
- if (this.client.store.wantsSave()) {
- // We always save the device list (if it's dirty) before saving the sync data:
- // this means we know the saved device list data is at least as fresh as the
- // stored sync data which means we don't have to worry that we may have missed
- // device changes. We can also skip the delay since we're not calling this very
- // frequently (and we don't really want to delay the sync for it).
- if (this.syncOpts.crypto) {
- await this.syncOpts.crypto.saveDeviceList(0);
- }
-
- // tell databases that everything is now in a consistent state and can be saved.
- this.client.store.save();
- }
- }
-
- if (!this.running) {
- debuglog("Sync no longer running: exiting.");
- if (this.connectionReturnedDefer) {
- this.connectionReturnedDefer.reject();
- this.connectionReturnedDefer = undefined;
- }
- this.updateSyncState(SyncState.Stopped);
- }
- }
-
- private doSyncRequest(syncOptions: ISyncOptions, syncToken: string | null): Promise<ISyncResponse> {
- const qps = this.getSyncParams(syncOptions, syncToken);
- return this.client.http.authedRequest<ISyncResponse>(Method.Get, "/sync", qps as any, undefined, {
- localTimeoutMs: qps.timeout + BUFFER_PERIOD_MS,
- abortSignal: this.abortController?.signal,
- });
- }
-
- private getSyncParams(syncOptions: ISyncOptions, syncToken: string | null): ISyncParams {
- let timeout = this.opts.pollTimeout!;
-
- if (this.getSyncState() !== SyncState.Syncing || this.catchingUp) {
- // unless we are happily syncing already, we want the server to return
- // as quickly as possible, even if there are no events queued. This
- // serves two purposes:
- //
- // * When the connection dies, we want to know asap when it comes back,
- // so that we can hide the error from the user. (We don't want to
- // have to wait for an event or a timeout).
- //
- // * We want to know if the server has any to_device messages queued up
- // for us. We do that by calling it with a zero timeout until it
- // doesn't give us any more to_device messages.
- this.catchingUp = true;
- timeout = 0;
- }
-
- let filter = syncOptions.filter;
- if (this.client.isGuest() && !filter) {
- filter = this.getGuestFilter();
- }
-
- const qps: ISyncParams = { filter, timeout };
-
- if (this.opts.disablePresence) {
- qps.set_presence = SetPresence.Offline;
- }
-
- if (syncToken) {
- qps.since = syncToken;
- } else {
- // use a cachebuster for initialsyncs, to make sure that
- // we don't get a stale sync
- // (https://github.com/vector-im/vector-web/issues/1354)
- qps._cacheBuster = Date.now();
- }
-
- if ([SyncState.Reconnecting, SyncState.Error].includes(this.getSyncState()!)) {
- // we think the connection is dead. If it comes back up, we won't know
- // about it till /sync returns. If the timeout= is high, this could
- // be a long time. Set it to 0 when doing retries so we don't have to wait
- // for an event or a timeout before emiting the SYNCING event.
- qps.timeout = 0;
- }
-
- return qps;
- }
-
- private async onSyncError(err: MatrixError): Promise<boolean> {
- if (!this.running) {
- debuglog("Sync no longer running: exiting");
- if (this.connectionReturnedDefer) {
- this.connectionReturnedDefer.reject();
- this.connectionReturnedDefer = undefined;
- }
- this.updateSyncState(SyncState.Stopped);
- return true; // abort
- }
-
- logger.error("/sync error %s", err);
-
- if (this.shouldAbortSync(err)) {
- return true; // abort
- }
-
- this.failedSyncCount++;
- logger.log("Number of consecutive failed sync requests:", this.failedSyncCount);
-
- debuglog("Starting keep-alive");
- // Note that we do *not* mark the sync connection as
- // lost yet: we only do this if a keepalive poke
- // fails, since long lived HTTP connections will
- // go away sometimes and we shouldn't treat this as
- // erroneous. We set the state to 'reconnecting'
- // instead, so that clients can observe this state
- // if they wish.
- const keepAlivePromise = this.startKeepAlives();
-
- this.currentSyncRequest = undefined;
- // Transition from RECONNECTING to ERROR after a given number of failed syncs
- this.updateSyncState(
- this.failedSyncCount >= FAILED_SYNC_ERROR_THRESHOLD ? SyncState.Error : SyncState.Reconnecting,
- { error: err },
- );
-
- const connDidFail = await keepAlivePromise;
-
- // Only emit CATCHUP if we detected a connectivity error: if we didn't,
- // it's quite likely the sync will fail again for the same reason and we
- // want to stay in ERROR rather than keep flip-flopping between ERROR
- // and CATCHUP.
- if (connDidFail && this.getSyncState() === SyncState.Error) {
- this.updateSyncState(SyncState.Catchup, {
- catchingUp: true,
- });
- }
- return false;
- }
-
- /**
- * Process data returned from a sync response and propagate it
- * into the model objects
- *
- * @param syncEventData - Object containing sync tokens associated with this sync
- * @param data - The response from /sync
- */
- private async processSyncResponse(syncEventData: ISyncStateData, data: ISyncResponse): Promise<void> {
- const client = this.client;
-
- // data looks like:
- // {
- // next_batch: $token,
- // presence: { events: [] },
- // account_data: { events: [] },
- // device_lists: { changed: ["@user:server", ... ]},
- // to_device: { events: [] },
- // device_one_time_keys_count: { signed_curve25519: 42 },
- // rooms: {
- // invite: {
- // $roomid: {
- // invite_state: { events: [] }
- // }
- // },
- // join: {
- // $roomid: {
- // state: { events: [] },
- // timeline: { events: [], prev_batch: $token, limited: true },
- // ephemeral: { events: [] },
- // summary: {
- // m.heroes: [ $user_id ],
- // m.joined_member_count: $count,
- // m.invited_member_count: $count
- // },
- // account_data: { events: [] },
- // unread_notifications: {
- // highlight_count: 0,
- // notification_count: 0,
- // }
- // }
- // },
- // leave: {
- // $roomid: {
- // state: { events: [] },
- // timeline: { events: [], prev_batch: $token }
- // }
- // }
- // }
- // }
-
- // TODO-arch:
- // - Each event we pass through needs to be emitted via 'event', can we
- // do this in one place?
- // - The isBrandNewRoom boilerplate is boilerplatey.
-
- // handle presence events (User objects)
- if (Array.isArray(data.presence?.events)) {
- data.presence!.events.filter(noUnsafeEventProps)
- .map(client.getEventMapper())
- .forEach(function (presenceEvent) {
- let user = client.store.getUser(presenceEvent.getSender()!);
- if (user) {
- user.setPresenceEvent(presenceEvent);
- } else {
- user = createNewUser(client, presenceEvent.getSender()!);
- user.setPresenceEvent(presenceEvent);
- client.store.storeUser(user);
- }
- client.emit(ClientEvent.Event, presenceEvent);
- });
- }
-
- // handle non-room account_data
- if (Array.isArray(data.account_data?.events)) {
- const events = data.account_data.events.filter(noUnsafeEventProps).map(client.getEventMapper());
- const prevEventsMap = events.reduce<Record<string, MatrixEvent | undefined>>((m, c) => {
- m[c.getType()!] = client.store.getAccountData(c.getType());
- return m;
- }, {});
- client.store.storeAccountDataEvents(events);
- events.forEach(function (accountDataEvent) {
- // Honour push rules that come down the sync stream but also
- // honour push rules that were previously cached. Base rules
- // will be updated when we receive push rules via getPushRules
- // (see sync) before syncing over the network.
- if (accountDataEvent.getType() === EventType.PushRules) {
- const rules = accountDataEvent.getContent<IPushRules>();
- client.setPushRules(rules);
- }
- const prevEvent = prevEventsMap[accountDataEvent.getType()!];
- client.emit(ClientEvent.AccountData, accountDataEvent, prevEvent);
- return accountDataEvent;
- });
- }
-
- // handle to-device events
- if (data.to_device && Array.isArray(data.to_device.events) && data.to_device.events.length > 0) {
- let toDeviceMessages: IToDeviceEvent[] = data.to_device.events.filter(noUnsafeEventProps);
-
- if (this.syncOpts.cryptoCallbacks) {
- toDeviceMessages = await this.syncOpts.cryptoCallbacks.preprocessToDeviceMessages(toDeviceMessages);
- }
-
- const cancelledKeyVerificationTxns: string[] = [];
- toDeviceMessages
- .map(client.getEventMapper({ toDevice: true }))
- .map((toDeviceEvent) => {
- // map is a cheap inline forEach
- // We want to flag m.key.verification.start events as cancelled
- // if there's an accompanying m.key.verification.cancel event, so
- // we pull out the transaction IDs from the cancellation events
- // so we can flag the verification events as cancelled in the loop
- // below.
- if (toDeviceEvent.getType() === "m.key.verification.cancel") {
- const txnId: string = toDeviceEvent.getContent()["transaction_id"];
- if (txnId) {
- cancelledKeyVerificationTxns.push(txnId);
- }
- }
-
- // as mentioned above, .map is a cheap inline forEach, so return
- // the unmodified event.
- return toDeviceEvent;
- })
- .forEach(function (toDeviceEvent) {
- const content = toDeviceEvent.getContent();
- if (toDeviceEvent.getType() == "m.room.message" && content.msgtype == "m.bad.encrypted") {
- // the mapper already logged a warning.
- logger.log("Ignoring undecryptable to-device event from " + toDeviceEvent.getSender());
- return;
- }
-
- if (
- toDeviceEvent.getType() === "m.key.verification.start" ||
- toDeviceEvent.getType() === "m.key.verification.request"
- ) {
- const txnId = content["transaction_id"];
- if (cancelledKeyVerificationTxns.includes(txnId)) {
- toDeviceEvent.flagCancelled();
- }
- }
-
- client.emit(ClientEvent.ToDeviceEvent, toDeviceEvent);
- });
- } else {
- // no more to-device events: we can stop polling with a short timeout.
- this.catchingUp = false;
- }
-
- // the returned json structure is a bit crap, so make it into a
- // nicer form (array) after applying sanity to make sure we don't fail
- // on missing keys (on the off chance)
- let inviteRooms: WrappedRoom<IInvitedRoom>[] = [];
- let joinRooms: WrappedRoom<IJoinedRoom>[] = [];
- let leaveRooms: WrappedRoom<ILeftRoom>[] = [];
-
- if (data.rooms) {
- if (data.rooms.invite) {
- inviteRooms = this.mapSyncResponseToRoomArray(data.rooms.invite);
- }
- if (data.rooms.join) {
- joinRooms = this.mapSyncResponseToRoomArray(data.rooms.join);
- }
- if (data.rooms.leave) {
- leaveRooms = this.mapSyncResponseToRoomArray(data.rooms.leave);
- }
- }
-
- this.notifEvents = [];
-
- // Handle invites
- await utils.promiseMapSeries(inviteRooms, async (inviteObj) => {
- const room = inviteObj.room;
- const stateEvents = this.mapSyncEventsFormat(inviteObj.invite_state, room);
-
- await this.injectRoomEvents(room, stateEvents);
-
- const inviter = room.currentState.getStateEvents(EventType.RoomMember, client.getUserId()!)?.getSender();
-
- const crypto = client.crypto;
- if (crypto) {
- const parkedHistory = await crypto.cryptoStore.takeParkedSharedHistory(room.roomId);
- for (const parked of parkedHistory) {
- if (parked.senderId === inviter) {
- await crypto.olmDevice.addInboundGroupSession(
- room.roomId,
- parked.senderKey,
- parked.forwardingCurve25519KeyChain,
- parked.sessionId,
- parked.sessionKey,
- parked.keysClaimed,
- true,
- { sharedHistory: true, untrusted: true },
- );
- }
- }
- }
-
- if (inviteObj.isBrandNewRoom) {
- room.recalculate();
- client.store.storeRoom(room);
- client.emit(ClientEvent.Room, room);
- } else {
- // Update room state for invite->reject->invite cycles
- room.recalculate();
- }
- stateEvents.forEach(function (e) {
- client.emit(ClientEvent.Event, e);
- });
- });
-
- // Handle joins
- await utils.promiseMapSeries(joinRooms, async (joinObj) => {
- const room = joinObj.room;
- const stateEvents = this.mapSyncEventsFormat(joinObj.state, room);
- // Prevent events from being decrypted ahead of time
- // this helps large account to speed up faster
- // room::decryptCriticalEvent is in charge of decrypting all the events
- // required for a client to function properly
- const events = this.mapSyncEventsFormat(joinObj.timeline, room, false);
- const ephemeralEvents = this.mapSyncEventsFormat(joinObj.ephemeral);
- const accountDataEvents = this.mapSyncEventsFormat(joinObj.account_data);
-
- const encrypted = client.isRoomEncrypted(room.roomId);
- // We store the server-provided value first so it's correct when any of the events fire.
- if (joinObj.unread_notifications) {
- /**
- * We track unread notifications ourselves in encrypted rooms, so don't
- * bother setting it here. We trust our calculations better than the
- * server's for this case, and therefore will assume that our non-zero
- * count is accurate.
- *
- * @see import("./client").fixNotificationCountOnDecryption
- */
- if (!encrypted || joinObj.unread_notifications.notification_count === 0) {
- // In an encrypted room, if the room has notifications enabled then it's typical for
- // the server to flag all new messages as notifying. However, some push rules calculate
- // events as ignored based on their event contents (e.g. ignoring msgtype=m.notice messages)
- // so we want to calculate this figure on the client in all cases.
- room.setUnreadNotificationCount(
- NotificationCountType.Total,
- joinObj.unread_notifications.notification_count ?? 0,
- );
- }
-
- if (!encrypted || room.getUnreadNotificationCount(NotificationCountType.Highlight) <= 0) {
- // If the locally stored highlight count is zero, use the server provided value.
- room.setUnreadNotificationCount(
- NotificationCountType.Highlight,
- joinObj.unread_notifications.highlight_count ?? 0,
- );
- }
- }
-
- const unreadThreadNotifications =
- joinObj[UNREAD_THREAD_NOTIFICATIONS.name] ?? joinObj[UNREAD_THREAD_NOTIFICATIONS.altName!];
- if (unreadThreadNotifications) {
- // Only partially reset unread notification
- // We want to keep the client-generated count. Particularly important
- // for encrypted room that refresh their notification count on event
- // decryption
- room.resetThreadUnreadNotificationCount(Object.keys(unreadThreadNotifications));
- for (const [threadId, unreadNotification] of Object.entries(unreadThreadNotifications)) {
- if (!encrypted || unreadNotification.notification_count === 0) {
- room.setThreadUnreadNotificationCount(
- threadId,
- NotificationCountType.Total,
- unreadNotification.notification_count ?? 0,
- );
- }
-
- const hasNoNotifications =
- room.getThreadUnreadNotificationCount(threadId, NotificationCountType.Highlight) <= 0;
- if (!encrypted || (encrypted && hasNoNotifications)) {
- room.setThreadUnreadNotificationCount(
- threadId,
- NotificationCountType.Highlight,
- unreadNotification.highlight_count ?? 0,
- );
- }
- }
- } else {
- room.resetThreadUnreadNotificationCount();
- }
-
- joinObj.timeline = joinObj.timeline || ({} as ITimeline);
-
- if (joinObj.isBrandNewRoom) {
- // set the back-pagination token. Do this *before* adding any
- // events so that clients can start back-paginating.
- if (joinObj.timeline.prev_batch !== null) {
- room.getLiveTimeline().setPaginationToken(joinObj.timeline.prev_batch, EventTimeline.BACKWARDS);
- }
- } else if (joinObj.timeline.limited) {
- let limited = true;
-
- // we've got a limited sync, so we *probably* have a gap in the
- // timeline, so should reset. But we might have been peeking or
- // paginating and already have some of the events, in which
- // case we just want to append any subsequent events to the end
- // of the existing timeline.
- //
- // This is particularly important in the case that we already have
- // *all* of the events in the timeline - in that case, if we reset
- // the timeline, we'll end up with an entirely empty timeline,
- // which we'll try to paginate but not get any new events (which
- // will stop us linking the empty timeline into the chain).
- //
- for (let i = events.length - 1; i >= 0; i--) {
- const eventId = events[i].getId()!;
- if (room.getTimelineForEvent(eventId)) {
- debuglog(`Already have event ${eventId} in limited sync - not resetting`);
- limited = false;
-
- // we might still be missing some of the events before i;
- // we don't want to be adding them to the end of the
- // timeline because that would put them out of order.
- events.splice(0, i);
-
- // XXX: there's a problem here if the skipped part of the
- // timeline modifies the state set in stateEvents, because
- // we'll end up using the state from stateEvents rather
- // than the later state from timelineEvents. We probably
- // need to wind stateEvents forward over the events we're
- // skipping.
-
- break;
- }
- }
-
- if (limited) {
- room.resetLiveTimeline(
- joinObj.timeline.prev_batch,
- this.syncOpts.canResetEntireTimeline!(room.roomId) ? null : syncEventData.oldSyncToken ?? null,
- );
-
- // We have to assume any gap in any timeline is
- // reason to stop incrementally tracking notifications and
- // reset the timeline.
- client.resetNotifTimelineSet();
- }
- }
-
- // process any crypto events *before* emitting the RoomStateEvent events. This
- // avoids a race condition if the application tries to send a message after the
- // state event is processed, but before crypto is enabled, which then causes the
- // crypto layer to complain.
- if (this.syncOpts.cryptoCallbacks) {
- for (const e of stateEvents.concat(events)) {
- if (e.isState() && e.getType() === EventType.RoomEncryption && e.getStateKey() === "") {
- await this.syncOpts.cryptoCallbacks.onCryptoEvent(room, e);
- }
- }
- }
-
- try {
- await this.injectRoomEvents(room, stateEvents, events, syncEventData.fromCache);
- } catch (e) {
- logger.error(`Failed to process events on room ${room.roomId}:`, e);
- }
-
- // set summary after processing events,
- // because it will trigger a name calculation
- // which needs the room state to be up to date
- if (joinObj.summary) {
- room.setSummary(joinObj.summary);
- }
-
- // we deliberately don't add ephemeral events to the timeline
- room.addEphemeralEvents(ephemeralEvents);
-
- // we deliberately don't add accountData to the timeline
- room.addAccountData(accountDataEvents);
-
- room.recalculate();
- if (joinObj.isBrandNewRoom) {
- client.store.storeRoom(room);
- client.emit(ClientEvent.Room, room);
- }
-
- this.processEventsForNotifs(room, events);
-
- const emitEvent = (e: MatrixEvent): boolean => client.emit(ClientEvent.Event, e);
- stateEvents.forEach(emitEvent);
- events.forEach(emitEvent);
- ephemeralEvents.forEach(emitEvent);
- accountDataEvents.forEach(emitEvent);
-
- // 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
- room.decryptCriticalEvents();
- });
-
- // Handle leaves (e.g. kicked rooms)
- await utils.promiseMapSeries(leaveRooms, async (leaveObj) => {
- const room = leaveObj.room;
- const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
- const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
- const accountDataEvents = this.mapSyncEventsFormat(leaveObj.account_data);
-
- await this.injectRoomEvents(room, stateEvents, events);
- room.addAccountData(accountDataEvents);
-
- room.recalculate();
- if (leaveObj.isBrandNewRoom) {
- client.store.storeRoom(room);
- client.emit(ClientEvent.Room, room);
- }
-
- this.processEventsForNotifs(room, events);
-
- stateEvents.forEach(function (e) {
- client.emit(ClientEvent.Event, e);
- });
- events.forEach(function (e) {
- client.emit(ClientEvent.Event, e);
- });
- accountDataEvents.forEach(function (e) {
- client.emit(ClientEvent.Event, e);
- });
- });
-
- // update the notification timeline, if appropriate.
- // we only do this for live events, as otherwise we can't order them sanely
- // in the timeline relative to ones paginated in by /notifications.
- // XXX: we could fix this by making EventTimeline support chronological
- // ordering... but it doesn't, right now.
- if (syncEventData.oldSyncToken && this.notifEvents.length) {
- this.notifEvents.sort(function (a, b) {
- return a.getTs() - b.getTs();
- });
- this.notifEvents.forEach(function (event) {
- client.getNotifTimelineSet()?.addLiveEvent(event);
- });
- }
-
- // Handle device list updates
- if (data.device_lists) {
- if (this.syncOpts.crypto) {
- await this.syncOpts.crypto.handleDeviceListChanges(syncEventData, data.device_lists);
- } else {
- // FIXME if we *don't* have a crypto module, we still need to
- // invalidate the device lists. But that would require a
- // substantial bit of rework :/.
- }
- }
-
- // Handle one_time_keys_count
- if (data.device_one_time_keys_count) {
- const map = new Map<string, number>(Object.entries(data.device_one_time_keys_count));
- this.syncOpts.cryptoCallbacks?.preprocessOneTimeKeyCounts(map);
- }
- if (data.device_unused_fallback_key_types || data["org.matrix.msc2732.device_unused_fallback_key_types"]) {
- // The presence of device_unused_fallback_key_types indicates that the
- // server supports fallback keys. If there's no unused
- // signed_curve25519 fallback key we need a new one.
- const unusedFallbackKeys =
- data.device_unused_fallback_key_types || data["org.matrix.msc2732.device_unused_fallback_key_types"];
- this.syncOpts.cryptoCallbacks?.preprocessUnusedFallbackKeys(new Set<string>(unusedFallbackKeys || null));
- }
- }
-
- /**
- * Starts polling the connectivity check endpoint
- * @param delay - How long to delay until the first poll.
- * defaults to a short, randomised interval (to prevent
- * tight-looping if /versions succeeds but /sync etc. fail).
- * @returns which resolves once the connection returns
- */
- private startKeepAlives(delay?: number): Promise<boolean> {
- if (delay === undefined) {
- delay = 2000 + Math.floor(Math.random() * 5000);
- }
-
- if (this.keepAliveTimer !== null) {
- clearTimeout(this.keepAliveTimer);
- }
- if (delay > 0) {
- this.keepAliveTimer = setTimeout(this.pokeKeepAlive.bind(this), delay);
- } else {
- this.pokeKeepAlive();
- }
- if (!this.connectionReturnedDefer) {
- this.connectionReturnedDefer = utils.defer();
- }
- return this.connectionReturnedDefer.promise;
- }
-
- /**
- * Make a dummy call to /_matrix/client/versions, to see if the HS is
- * reachable.
- *
- * On failure, schedules a call back to itself. On success, resolves
- * this.connectionReturnedDefer.
- *
- * @param connDidFail - True if a connectivity failure has been detected. Optional.
- */
- private pokeKeepAlive(connDidFail = false): void {
- const success = (): void => {
- clearTimeout(this.keepAliveTimer);
- if (this.connectionReturnedDefer) {
- this.connectionReturnedDefer.resolve(connDidFail);
- this.connectionReturnedDefer = undefined;
- }
- };
-
- this.client.http
- .request(
- Method.Get,
- "/_matrix/client/versions",
- undefined, // queryParams
- undefined, // data
- {
- prefix: "",
- localTimeoutMs: 15 * 1000,
- abortSignal: this.abortController?.signal,
- },
- )
- .then(
- () => {
- success();
- },
- (err) => {
- if (err.httpStatus == 400 || err.httpStatus == 404) {
- // treat this as a success because the server probably just doesn't
- // support /versions: point is, we're getting a response.
- // We wait a short time though, just in case somehow the server
- // is in a mode where it 400s /versions responses and sync etc.
- // responses fail, this will mean we don't hammer in a loop.
- this.keepAliveTimer = setTimeout(success, 2000);
- } else {
- connDidFail = true;
- this.keepAliveTimer = setTimeout(
- this.pokeKeepAlive.bind(this, connDidFail),
- 5000 + Math.floor(Math.random() * 5000),
- );
- // A keepalive has failed, so we emit the
- // error state (whether or not this is the
- // first failure).
- // Note we do this after setting the timer:
- // this lets the unit tests advance the mock
- // clock when they get the error.
- this.updateSyncState(SyncState.Error, { error: err });
- }
- },
- );
- }
-
- private mapSyncResponseToRoomArray<T extends ILeftRoom | IJoinedRoom | IInvitedRoom>(
- obj: Record<string, T>,
- ): Array<WrappedRoom<T>> {
- // Maps { roomid: {stuff}, roomid: {stuff} }
- // to
- // [{stuff+Room+isBrandNewRoom}, {stuff+Room+isBrandNewRoom}]
- const client = this.client;
- return Object.keys(obj)
- .filter((k) => !unsafeProp(k))
- .map((roomId) => {
- const arrObj = obj[roomId] as T & { room: Room; isBrandNewRoom: boolean };
- let room = client.store.getRoom(roomId);
- let isBrandNewRoom = false;
- if (!room) {
- room = this.createRoom(roomId);
- isBrandNewRoom = true;
- }
- arrObj.room = room;
- arrObj.isBrandNewRoom = isBrandNewRoom;
- return arrObj;
- });
- }
-
- private mapSyncEventsFormat(
- obj: IInviteState | ITimeline | IEphemeral,
- room?: Room,
- decrypt = true,
- ): MatrixEvent[] {
- if (!obj || !Array.isArray(obj.events)) {
- return [];
- }
- const mapper = this.client.getEventMapper({ decrypt });
- type TaggedEvent = (IStrippedState | IRoomEvent | IStateEvent | IMinimalEvent) & { room_id?: string };
- return (obj.events as TaggedEvent[]).filter(noUnsafeEventProps).map(function (e) {
- if (room) {
- e.room_id = room.roomId;
- }
- return mapper(e);
- });
- }
-
- /**
- */
- private resolveInvites(room: Room): void {
- if (!room || !this.opts.resolveInvitesToProfiles) {
- return;
- }
- const client = this.client;
- // For each invited room member we want to give them a displayname/avatar url
- // if they have one (the m.room.member invites don't contain this).
- room.getMembersWithMembership("invite").forEach(function (member) {
- if (member.requestedProfileInfo) return;
- member.requestedProfileInfo = true;
- // try to get a cached copy first.
- const user = client.getUser(member.userId);
- let promise;
- if (user) {
- promise = Promise.resolve({
- avatar_url: user.avatarUrl,
- displayname: user.displayName,
- });
- } else {
- promise = client.getProfileInfo(member.userId);
- }
- promise.then(
- function (info) {
- // slightly naughty by doctoring the invite event but this means all
- // the code paths remain the same between invite/join display name stuff
- // which is a worthy trade-off for some minor pollution.
- const inviteEvent = member.events.member;
- if (inviteEvent?.getContent().membership !== "invite") {
- // between resolving and now they have since joined, so don't clobber
- return;
- }
- inviteEvent.getContent().avatar_url = info.avatar_url;
- inviteEvent.getContent().displayname = info.displayname;
- // fire listeners
- member.setMembershipEvent(inviteEvent, room.currentState);
- },
- function (err) {
- // OH WELL.
- },
- );
- });
- }
-
- /**
- * Injects events into a room's model.
- * @param stateEventList - A list of state events. This is the state
- * at the *START* of the timeline list if it is supplied.
- * @param timelineEventList - A list of timeline events, including threaded. Lower index
- * is earlier in time. Higher index is later.
- * @param fromCache - whether the sync response came from cache
- */
- public async injectRoomEvents(
- room: Room,
- stateEventList: MatrixEvent[],
- timelineEventList?: MatrixEvent[],
- fromCache = false,
- ): Promise<void> {
- // If there are no events in the timeline yet, initialise it with
- // the given state events
- const liveTimeline = room.getLiveTimeline();
- const timelineWasEmpty = liveTimeline.getEvents().length == 0;
- if (timelineWasEmpty) {
- // Passing these events into initialiseState will freeze them, so we need
- // to compute and cache the push actions for them now, otherwise sync dies
- // with an attempt to assign to read only property.
- // XXX: This is pretty horrible and is assuming all sorts of behaviour from
- // these functions that it shouldn't be. We should probably either store the
- // push actions cache elsewhere so we can freeze MatrixEvents, or otherwise
- // find some solution where MatrixEvents are immutable but allow for a cache
- // field.
- for (const ev of stateEventList) {
- this.client.getPushActionsForEvent(ev);
- }
- liveTimeline.initialiseState(stateEventList, {
- timelineWasEmpty,
- });
- }
-
- this.resolveInvites(room);
-
- // recalculate the room name at this point as adding events to the timeline
- // may make notifications appear which should have the right name.
- // XXX: This looks suspect: we'll end up recalculating the room once here
- // and then again after adding events (processSyncResponse calls it after
- // calling us) even if no state events were added. It also means that if
- // one of the room events in timelineEventList is something that needs
- // a recalculation (like m.room.name) we won't recalculate until we've
- // finished adding all the events, which will cause the notification to have
- // the old room name rather than the new one.
- room.recalculate();
-
- // If the timeline wasn't empty, we process the state events here: they're
- // defined as updates to the state before the start of the timeline, so this
- // starts to roll the state forward.
- // XXX: That's what we *should* do, but this can happen if we were previously
- // peeking in a room, in which case we obviously do *not* want to add the
- // state events here onto the end of the timeline. Historically, the js-sdk
- // has just set these new state events on the old and new state. This seems
- // very wrong because there could be events in the timeline that diverge the
- // state, in which case this is going to leave things out of sync. However,
- // for now I think it;s best to behave the same as the code has done previously.
- if (!timelineWasEmpty) {
- // XXX: As above, don't do this...
- //room.addLiveEvents(stateEventList || []);
- // Do this instead...
- room.oldState.setStateEvents(stateEventList || []);
- room.currentState.setStateEvents(stateEventList || []);
- }
-
- // Execute the timeline events. This will continue to diverge the current state
- // if the timeline has any state events in it.
- // 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 || [], {
- fromCache,
- timelineWasEmpty,
- });
- this.client.processBeaconEvents(room, timelineEventList);
- }
-
- /**
- * Takes a list of timelineEvents and adds and adds to notifEvents
- * as appropriate.
- * This must be called after the room the events belong to has been stored.
- *
- * @param timelineEventList - A list of timeline events. Lower index
- * is earlier in time. Higher index is later.
- */
- private processEventsForNotifs(room: Room, timelineEventList: MatrixEvent[]): void {
- // gather our notifications into this.notifEvents
- if (this.client.getNotifTimelineSet()) {
- for (const event of timelineEventList) {
- const pushActions = this.client.getPushActionsForEvent(event);
- if (pushActions?.notify && pushActions.tweaks?.highlight) {
- this.notifEvents.push(event);
- }
- }
- }
- }
-
- private getGuestFilter(): string {
- // Dev note: This used to be conditional to return a filter of 20 events maximum, but
- // the condition never went to the other branch. This is now hardcoded.
- return "{}";
- }
-
- /**
- * Sets the sync state and emits an event to say so
- * @param newState - The new state string
- * @param data - Object of additional data to emit in the event
- */
- private updateSyncState(newState: SyncState, data?: ISyncStateData): void {
- const old = this.syncState;
- this.syncState = newState;
- this.syncStateData = data;
- this.client.emit(ClientEvent.Sync, this.syncState, old, data);
- }
-
- /**
- * Event handler for the 'online' event
- * This event is generally unreliable and precise behaviour
- * varies between browsers, so we poll for connectivity too,
- * but this might help us reconnect a little faster.
- */
- private onOnline = (): void => {
- debuglog("Browser thinks we are back online");
- this.startKeepAlives(0);
- };
-}
-
-function createNewUser(client: MatrixClient, userId: string): User {
- const user = new User(userId);
- client.reEmitter.reEmit(user, [
- UserEvent.AvatarUrl,
- UserEvent.DisplayName,
- UserEvent.Presence,
- UserEvent.CurrentlyActive,
- UserEvent.LastPresenceTs,
- ]);
- return user;
-}
-
-// /!\ This function is not intended for public use! It's only exported from
-// here in order to share some common logic with sliding-sync-sdk.ts.
-export function _createAndReEmitRoom(client: MatrixClient, roomId: string, opts: Partial<IStoredClientOpts>): Room {
- const { timelineSupport } = client;
-
- const room = new Room(roomId, client, client.getUserId()!, {
- lazyLoadMembers: opts.lazyLoadMembers,
- pendingEventOrdering: opts.pendingEventOrdering,
- timelineSupport,
- });
-
- client.reEmitter.reEmit(room, [
- RoomEvent.Name,
- RoomEvent.Redaction,
- RoomEvent.RedactionCancelled,
- RoomEvent.Receipt,
- RoomEvent.Tags,
- RoomEvent.LocalEchoUpdated,
- RoomEvent.AccountData,
- RoomEvent.MyMembership,
- RoomEvent.Timeline,
- RoomEvent.TimelineReset,
- RoomStateEvent.Events,
- RoomStateEvent.Members,
- RoomStateEvent.NewMember,
- RoomStateEvent.Update,
- BeaconEvent.New,
- BeaconEvent.Update,
- BeaconEvent.Destroy,
- BeaconEvent.LivenessChange,
- ]);
-
- // We need to add a listener for RoomState.members in order to hook them
- // correctly.
- room.on(RoomStateEvent.NewMember, (event, state, member) => {
- member.user = client.getUser(member.userId) ?? undefined;
- client.reEmitter.reEmit(member, [
- RoomMemberEvent.Name,
- RoomMemberEvent.Typing,
- RoomMemberEvent.PowerLevel,
- RoomMemberEvent.Membership,
- ]);
- });
-
- return room;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/timeline-window.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/timeline-window.ts
deleted file mode 100644
index be64c3b..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/timeline-window.ts
+++ /dev/null
@@ -1,507 +0,0 @@
-/*
-Copyright 2016 - 2021 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 { Optional } from "matrix-events-sdk";
-
-import { Direction, EventTimeline } from "./models/event-timeline";
-import { logger } from "./logger";
-import { MatrixClient } from "./client";
-import { EventTimelineSet } from "./models/event-timeline-set";
-import { MatrixEvent } from "./models/event";
-
-/**
- * @internal
- */
-const DEBUG = false;
-
-/**
- * @internal
- */
-/* istanbul ignore next */
-const debuglog = DEBUG ? logger.log.bind(logger) : function (): void {};
-
-/**
- * the number of times we ask the server for more events before giving up
- *
- * @internal
- */
-const DEFAULT_PAGINATE_LOOP_LIMIT = 5;
-
-interface IOpts {
- /**
- * Maximum number of events to keep in the window. If more events are retrieved via pagination requests,
- * excess events will be dropped from the other end of the window.
- */
- windowLimit?: number;
-}
-
-export class TimelineWindow {
- private readonly windowLimit: number;
- // these will be TimelineIndex objects; they delineate the 'start' and
- // 'end' of the window.
- //
- // start.index is inclusive; end.index is exclusive.
- private start?: TimelineIndex;
- private end?: TimelineIndex;
- private eventCount = 0;
-
- /**
- * Construct a TimelineWindow.
- *
- * <p>This abstracts the separate timelines in a Matrix {@link Room} into a single iterable thing.
- * It keeps track of the start and endpoints of the window, which can be advanced with the help
- * of pagination requests.
- *
- * <p>Before the window is useful, it must be initialised by calling {@link TimelineWindow#load}.
- *
- * <p>Note that the window will not automatically extend itself when new events
- * are received from /sync; you should arrange to call {@link TimelineWindow#paginate}
- * on {@link RoomEvent.Timeline} events.
- *
- * @param client - MatrixClient to be used for context/pagination
- * requests.
- *
- * @param timelineSet - The timelineSet to track
- *
- * @param opts - Configuration options for this window
- */
- public constructor(
- private readonly client: MatrixClient,
- private readonly timelineSet: EventTimelineSet,
- opts: IOpts = {},
- ) {
- this.windowLimit = opts.windowLimit || 1000;
- }
-
- /**
- * Initialise the window to point at a given event, or the live timeline
- *
- * @param initialEventId - If given, the window will contain the
- * given event
- * @param initialWindowSize - Size of the initial window
- */
- public load(initialEventId?: string, initialWindowSize = 20): Promise<void> {
- // given an EventTimeline, find the event we were looking for, and initialise our
- // fields so that the event in question is in the middle of the window.
- const initFields = (timeline: Optional<EventTimeline>): void => {
- if (!timeline) {
- throw new Error("No timeline given to initFields");
- }
-
- let eventIndex: number;
-
- const events = timeline.getEvents();
-
- if (!initialEventId) {
- // we were looking for the live timeline: initialise to the end
- eventIndex = events.length;
- } else {
- eventIndex = events.findIndex((e) => e.getId() === initialEventId);
-
- if (eventIndex < 0) {
- throw new Error("getEventTimeline result didn't include requested event");
- }
- }
-
- const endIndex = Math.min(events.length, eventIndex + Math.ceil(initialWindowSize / 2));
- const startIndex = Math.max(0, endIndex - initialWindowSize);
- this.start = new TimelineIndex(timeline, startIndex - timeline.getBaseIndex());
- this.end = new TimelineIndex(timeline, endIndex - timeline.getBaseIndex());
- this.eventCount = endIndex - startIndex;
- };
-
- // We avoid delaying the resolution of the promise by a reactor tick if we already have the data we need,
- // which is important to keep room-switching feeling snappy.
- if (this.timelineSet.getTimelineForEvent(initialEventId)) {
- initFields(this.timelineSet.getTimelineForEvent(initialEventId));
- return Promise.resolve();
- } else if (initialEventId) {
- return this.client.getEventTimeline(this.timelineSet, initialEventId).then(initFields);
- } else {
- initFields(this.timelineSet.getLiveTimeline());
- return Promise.resolve();
- }
- }
-
- /**
- * Get the TimelineIndex of the window in the given direction.
- *
- * @param direction - EventTimeline.BACKWARDS to get the TimelineIndex
- * at the start of the window; EventTimeline.FORWARDS to get the TimelineIndex at
- * the end.
- *
- * @returns The requested timeline index if one exists, null
- * otherwise.
- */
- public getTimelineIndex(direction: Direction): TimelineIndex | null {
- if (direction == EventTimeline.BACKWARDS) {
- return this.start ?? null;
- } else if (direction == EventTimeline.FORWARDS) {
- return this.end ?? null;
- } else {
- throw new Error("Invalid direction '" + direction + "'");
- }
- }
-
- /**
- * Try to extend the window using events that are already in the underlying
- * TimelineIndex.
- *
- * @param direction - EventTimeline.BACKWARDS to try extending it
- * backwards; EventTimeline.FORWARDS to try extending it forwards.
- * @param size - number of events to try to extend by.
- *
- * @returns true if the window was extended, false otherwise.
- */
- public extend(direction: Direction, size: number): boolean {
- const tl = this.getTimelineIndex(direction);
-
- if (!tl) {
- debuglog("TimelineWindow: no timeline yet");
- return false;
- }
-
- const count = direction == EventTimeline.BACKWARDS ? tl.retreat(size) : tl.advance(size);
-
- if (count) {
- this.eventCount += count;
- debuglog("TimelineWindow: increased cap by " + count + " (now " + this.eventCount + ")");
- // remove some events from the other end, if necessary
- const excess = this.eventCount - this.windowLimit;
- if (excess > 0) {
- this.unpaginate(excess, direction != EventTimeline.BACKWARDS);
- }
- return true;
- }
-
- return false;
- }
-
- /**
- * Check if this window can be extended
- *
- * <p>This returns true if we either have more events, or if we have a
- * pagination token which means we can paginate in that direction. It does not
- * necessarily mean that there are more events available in that direction at
- * this time.
- *
- * @param direction - EventTimeline.BACKWARDS to check if we can
- * paginate backwards; EventTimeline.FORWARDS to check if we can go forwards
- *
- * @returns true if we can paginate in the given direction
- */
- public canPaginate(direction: Direction): boolean {
- const tl = this.getTimelineIndex(direction);
-
- if (!tl) {
- debuglog("TimelineWindow: no timeline yet");
- return false;
- }
-
- if (direction == EventTimeline.BACKWARDS) {
- if (tl.index > tl.minIndex()) {
- return true;
- }
- } else {
- if (tl.index < tl.maxIndex()) {
- return true;
- }
- }
-
- const hasNeighbouringTimeline = tl.timeline.getNeighbouringTimeline(direction);
- const paginationToken = tl.timeline.getPaginationToken(direction);
- return Boolean(hasNeighbouringTimeline) || Boolean(paginationToken);
- }
-
- /**
- * Attempt to extend the window
- *
- * @param direction - EventTimeline.BACKWARDS to extend the window
- * backwards (towards older events); EventTimeline.FORWARDS to go forwards.
- *
- * @param size - number of events to try to extend by. If fewer than this
- * number are immediately available, then we return immediately rather than
- * making an API call.
- *
- * @param makeRequest - whether we should make API calls to
- * fetch further events if we don't have any at all. (This has no effect if
- * the room already knows about additional events in the relevant direction,
- * even if there are fewer than 'size' of them, as we will just return those
- * we already know about.)
- *
- * @param requestLimit - limit for the number of API requests we
- * should make.
- *
- * @returns Promise which resolves to a boolean which is true if more events
- * were successfully retrieved.
- */
- public async paginate(
- direction: Direction,
- size: number,
- makeRequest = true,
- requestLimit = DEFAULT_PAGINATE_LOOP_LIMIT,
- ): Promise<boolean> {
- // Either wind back the message cap (if there are enough events in the
- // timeline to do so), or fire off a pagination request.
- const tl = this.getTimelineIndex(direction);
-
- if (!tl) {
- debuglog("TimelineWindow: no timeline yet");
- return false;
- }
-
- if (tl.pendingPaginate) {
- return tl.pendingPaginate;
- }
-
- // try moving the cap
- if (this.extend(direction, size)) {
- return true;
- }
-
- if (!makeRequest || requestLimit === 0) {
- // todo: should we return something different to indicate that there
- // might be more events out there, but we haven't found them yet?
- return false;
- }
-
- // try making a pagination request
- const token = tl.timeline.getPaginationToken(direction);
- if (!token) {
- debuglog("TimelineWindow: no token");
- return false;
- }
-
- debuglog("TimelineWindow: starting request");
-
- const prom = this.client
- .paginateEventTimeline(tl.timeline, {
- backwards: direction == EventTimeline.BACKWARDS,
- limit: size,
- })
- .finally(function () {
- tl.pendingPaginate = undefined;
- })
- .then((r) => {
- debuglog("TimelineWindow: request completed with result " + r);
- if (!r) {
- return this.paginate(direction, size, false, 0);
- }
-
- // recurse to advance the index into the results.
- //
- // If we don't get any new events, we want to make sure we keep asking
- // the server for events for as long as we have a valid pagination
- // token. In particular, we want to know if we've actually hit the
- // start of the timeline, or if we just happened to know about all of
- // the events thanks to https://matrix.org/jira/browse/SYN-645.
- //
- // On the other hand, we necessarily want to wait forever for the
- // server to make its mind up about whether there are other events,
- // because it gives a bad user experience
- // (https://github.com/vector-im/vector-web/issues/1204).
- return this.paginate(direction, size, true, requestLimit - 1);
- });
- tl.pendingPaginate = prom;
- return prom;
- }
-
- /**
- * Remove `delta` events from the start or end of the timeline.
- *
- * @param delta - number of events to remove from the timeline
- * @param startOfTimeline - if events should be removed from the start
- * of the timeline.
- */
- public unpaginate(delta: number, startOfTimeline: boolean): void {
- const tl = startOfTimeline ? this.start : this.end;
- if (!tl) {
- throw new Error(
- `Attempting to unpaginate startOfTimeline=${startOfTimeline} but don't have this direction`,
- );
- }
-
- // sanity-check the delta
- if (delta > this.eventCount || delta < 0) {
- throw new Error(
- `Attemting to unpaginate ${delta} events, but only have ${this.eventCount} in the timeline`,
- );
- }
-
- while (delta > 0) {
- const count = startOfTimeline ? tl.advance(delta) : tl.retreat(delta);
- if (count <= 0) {
- // sadness. This shouldn't be possible.
- throw new Error("Unable to unpaginate any further, but still have " + this.eventCount + " events");
- }
-
- delta -= count;
- this.eventCount -= count;
- debuglog("TimelineWindow.unpaginate: dropped " + count + " (now " + this.eventCount + ")");
- }
- }
-
- /**
- * Get a list of the events currently in the window
- *
- * @returns the events in the window
- */
- public getEvents(): MatrixEvent[] {
- if (!this.start) {
- // not yet loaded
- return [];
- }
-
- const result: MatrixEvent[] = [];
-
- // iterate through each timeline between this.start and this.end
- // (inclusive).
- let timeline = this.start.timeline;
- // eslint-disable-next-line no-constant-condition
- while (true) {
- const events = timeline.getEvents();
-
- // For the first timeline in the chain, we want to start at
- // this.start.index. For the last timeline in the chain, we want to
- // stop before this.end.index. Otherwise, we want to copy all of the
- // events in the timeline.
- //
- // (Note that both this.start.index and this.end.index are relative
- // to their respective timelines' BaseIndex).
- //
- let startIndex = 0;
- let endIndex = events.length;
- if (timeline === this.start.timeline) {
- startIndex = this.start.index + timeline.getBaseIndex();
- }
- if (timeline === this.end?.timeline) {
- endIndex = this.end.index + timeline.getBaseIndex();
- }
-
- for (let i = startIndex; i < endIndex; i++) {
- result.push(events[i]);
- }
-
- // if we're not done, iterate to the next timeline.
- if (timeline === this.end?.timeline) {
- break;
- } else {
- timeline = timeline.getNeighbouringTimeline(EventTimeline.FORWARDS)!;
- }
- }
-
- return result;
- }
-}
-
-/**
- * A thing which contains a timeline reference, and an index into it.
- * @internal
- */
-export class TimelineIndex {
- public pendingPaginate?: Promise<boolean>;
-
- // index: the indexes are relative to BaseIndex, so could well be negative.
- public constructor(public timeline: EventTimeline, public index: number) {}
-
- /**
- * @returns the minimum possible value for the index in the current
- * timeline
- */
- public minIndex(): number {
- return this.timeline.getBaseIndex() * -1;
- }
-
- /**
- * @returns the maximum possible value for the index in the current
- * timeline (exclusive - ie, it actually returns one more than the index
- * of the last element).
- */
- public maxIndex(): number {
- return this.timeline.getEvents().length - this.timeline.getBaseIndex();
- }
-
- /**
- * Try move the index forward, or into the neighbouring timeline
- *
- * @param delta - number of events to advance by
- * @returns number of events successfully advanced by
- */
- public advance(delta: number): number {
- if (!delta) {
- return 0;
- }
-
- // first try moving the index in the current timeline. See if there is room
- // to do so.
- let cappedDelta;
- if (delta < 0) {
- // we want to wind the index backwards.
- //
- // (this.minIndex() - this.index) is a negative number whose magnitude
- // is the amount of room we have to wind back the index in the current
- // timeline. We cap delta to this quantity.
- cappedDelta = Math.max(delta, this.minIndex() - this.index);
- if (cappedDelta < 0) {
- this.index += cappedDelta;
- return cappedDelta;
- }
- } else {
- // we want to wind the index forwards.
- //
- // (this.maxIndex() - this.index) is a (positive) number whose magnitude
- // is the amount of room we have to wind forward the index in the current
- // timeline. We cap delta to this quantity.
- cappedDelta = Math.min(delta, this.maxIndex() - this.index);
- if (cappedDelta > 0) {
- this.index += cappedDelta;
- return cappedDelta;
- }
- }
-
- // the index is already at the start/end of the current timeline.
- //
- // next see if there is a neighbouring timeline to switch to.
- const neighbour = this.timeline.getNeighbouringTimeline(
- delta < 0 ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS,
- );
- if (neighbour) {
- this.timeline = neighbour;
- if (delta < 0) {
- this.index = this.maxIndex();
- } else {
- this.index = this.minIndex();
- }
-
- debuglog("paginate: switched to new neighbour");
-
- // recurse, using the next timeline
- return this.advance(delta);
- }
-
- return 0;
- }
-
- /**
- * Try move the index backwards, or into the neighbouring timeline
- *
- * @param delta - number of events to retreat by
- * @returns number of events successfully retreated by
- */
- public retreat(delta: number): number {
- return this.advance(delta * -1) * -1;
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/utils.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/utils.ts
deleted file mode 100644
index 0c3aea7..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/utils.ts
+++ /dev/null
@@ -1,770 +0,0 @@
-/*
-Copyright 2015, 2016, 2019, 2023 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.
-*/
-
-/**
- * This is an internal module.
- */
-
-import unhomoglyph from "unhomoglyph";
-import promiseRetry from "p-retry";
-import { Optional } from "matrix-events-sdk";
-
-import { IEvent, MatrixEvent } from "./models/event";
-import { M_TIMESTAMP } from "./@types/location";
-import { ReceiptType } from "./@types/read_receipts";
-
-const interns = new Map<string, string>();
-
-/**
- * Internalises a string, reusing a known pointer or storing the pointer
- * if needed for future strings.
- * @param str - The string to internalise.
- * @returns The internalised string.
- */
-export function internaliseString(str: string): string {
- // Unwrap strings before entering the map, if we somehow got a wrapped
- // string as our input. This should only happen from tests.
- if ((str as unknown) instanceof String) {
- str = str.toString();
- }
-
- // Check the map to see if we can store the value
- if (!interns.has(str)) {
- interns.set(str, str);
- }
-
- // Return any cached string reference
- return interns.get(str)!;
-}
-
-/**
- * Encode a dictionary of query parameters.
- * Omits any undefined/null values.
- * @param params - A dict of key/values to encode e.g.
- * `{"foo": "bar", "baz": "taz"}`
- * @returns The encoded string e.g. foo=bar&baz=taz
- */
-export function encodeParams(params: QueryDict, urlSearchParams?: URLSearchParams): URLSearchParams {
- const searchParams = urlSearchParams ?? new URLSearchParams();
- for (const [key, val] of Object.entries(params)) {
- if (val !== undefined && val !== null) {
- if (Array.isArray(val)) {
- val.forEach((v) => {
- searchParams.append(key, String(v));
- });
- } else {
- searchParams.append(key, String(val));
- }
- }
- }
- return searchParams;
-}
-
-export type QueryDict = Record<string, string[] | string | number | boolean | undefined>;
-
-/**
- * Replace a stable parameter with the unstable naming for params
- */
-export function replaceParam(stable: string, unstable: string, dict: QueryDict): QueryDict {
- const result = {
- ...dict,
- [unstable]: dict[stable],
- };
- delete result[stable];
- return result;
-}
-
-/**
- * Decode a query string in `application/x-www-form-urlencoded` format.
- * @param query - A query string to decode e.g.
- * foo=bar&via=server1&server2
- * @returns The decoded object, if any keys occurred multiple times
- * then the value will be an array of strings, else it will be an array.
- * This behaviour matches Node's qs.parse but is built on URLSearchParams
- * for native web compatibility
- */
-export function decodeParams(query: string): Record<string, string | string[]> {
- const o: Record<string, string | string[]> = {};
- const params = new URLSearchParams(query);
- for (const key of params.keys()) {
- const val = params.getAll(key);
- o[key] = val.length === 1 ? val[0] : val;
- }
- return o;
-}
-
-/**
- * Encodes a URI according to a set of template variables. Variables will be
- * passed through encodeURIComponent.
- * @param pathTemplate - The path with template variables e.g. '/foo/$bar'.
- * @param variables - The key/value pairs to replace the template
- * variables with. E.g. `{ "$bar": "baz" }`.
- * @returns The result of replacing all template variables e.g. '/foo/baz'.
- */
-export function encodeUri(pathTemplate: string, variables: Record<string, Optional<string>>): string {
- for (const key in variables) {
- if (!variables.hasOwnProperty(key)) {
- continue;
- }
- const value = variables[key];
- if (value === undefined || value === null) {
- continue;
- }
- pathTemplate = pathTemplate.replace(key, encodeURIComponent(value));
- }
- return pathTemplate;
-}
-
-/**
- * The removeElement() method removes the first element in the array that
- * satisfies (returns true) the provided testing function.
- * @param array - The array.
- * @param fn - Function to execute on each value in the array, with the
- * function signature `fn(element, index, array)`. Return true to
- * remove this element and break.
- * @param reverse - True to search in reverse order.
- * @returns True if an element was removed.
- */
-export function removeElement<T>(array: T[], fn: (t: T, i?: number, a?: T[]) => boolean, reverse?: boolean): boolean {
- let i: number;
- if (reverse) {
- for (i = array.length - 1; i >= 0; i--) {
- if (fn(array[i], i, array)) {
- array.splice(i, 1);
- return true;
- }
- }
- } else {
- for (i = 0; i < array.length; i++) {
- if (fn(array[i], i, array)) {
- array.splice(i, 1);
- return true;
- }
- }
- }
- return false;
-}
-
-/**
- * Checks if the given thing is a function.
- * @param value - The thing to check.
- * @returns True if it is a function.
- */
-export function isFunction(value: any): boolean {
- return Object.prototype.toString.call(value) === "[object Function]";
-}
-
-/**
- * Checks that the given object has the specified keys.
- * @param obj - The object to check.
- * @param keys - The list of keys that 'obj' must have.
- * @throws If the object is missing keys.
- */
-// note using 'keys' here would shadow the 'keys' function defined above
-export function checkObjectHasKeys(obj: object, keys: string[]): void {
- for (const key of keys) {
- if (!obj.hasOwnProperty(key)) {
- throw new Error("Missing required key: " + key);
- }
- }
-}
-
-/**
- * Deep copy the given object. The object MUST NOT have circular references and
- * MUST NOT have functions.
- * @param obj - The object to deep copy.
- * @returns A copy of the object without any references to the original.
- */
-export function deepCopy<T>(obj: T): T {
- return JSON.parse(JSON.stringify(obj));
-}
-
-/**
- * Compare two objects for equality. The objects MUST NOT have circular references.
- *
- * @param x - The first object to compare.
- * @param y - The second object to compare.
- *
- * @returns true if the two objects are equal
- */
-export function deepCompare(x: any, y: any): boolean {
- // Inspired by
- // http://stackoverflow.com/questions/1068834/object-comparison-in-javascript#1144249
-
- // Compare primitives and functions.
- // Also check if both arguments link to the same object.
- if (x === y) {
- return true;
- }
-
- if (typeof x !== typeof y) {
- return false;
- }
-
- // special-case NaN (since NaN !== NaN)
- if (typeof x === "number" && isNaN(x) && isNaN(y)) {
- return true;
- }
-
- // special-case null (since typeof null == 'object', but null.constructor
- // throws)
- if (x === null || y === null) {
- return x === y;
- }
-
- // everything else is either an unequal primitive, or an object
- if (!(x instanceof Object)) {
- return false;
- }
-
- // check they are the same type of object
- if (x.constructor !== y.constructor || x.prototype !== y.prototype) {
- return false;
- }
-
- // special-casing for some special types of object
- if (x instanceof RegExp || x instanceof Date) {
- return x.toString() === y.toString();
- }
-
- // the object algorithm works for Array, but it's sub-optimal.
- if (Array.isArray(x)) {
- if (x.length !== y.length) {
- return false;
- }
-
- for (let i = 0; i < x.length; i++) {
- if (!deepCompare(x[i], y[i])) {
- return false;
- }
- }
- } else {
- // check that all of y's direct keys are in x
- for (const p in y) {
- if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
- return false;
- }
- }
-
- // finally, compare each of x's keys with y
- for (const p in x) {
- if (y.hasOwnProperty(p) !== x.hasOwnProperty(p) || !deepCompare(x[p], y[p])) {
- return false;
- }
- }
- }
- return true;
-}
-
-// Dev note: This returns an array of tuples, but jsdoc doesn't like that. https://github.com/jsdoc/jsdoc/issues/1703
-/**
- * Creates an array of object properties/values (entries) then
- * sorts the result by key, recursively. The input object must
- * ensure it does not have loops. If the input is not an object
- * then it will be returned as-is.
- * @param obj - The object to get entries of
- * @returns The entries, sorted by key.
- */
-export function deepSortedObjectEntries(obj: any): [string, any][] {
- if (typeof obj !== "object") return obj;
-
- // Apparently these are object types...
- if (obj === null || obj === undefined || Array.isArray(obj)) return obj;
-
- const pairs: [string, any][] = [];
- for (const [k, v] of Object.entries(obj)) {
- pairs.push([k, deepSortedObjectEntries(v)]);
- }
-
- // lexicographicCompare is faster than localeCompare, so let's use that.
- pairs.sort((a, b) => lexicographicCompare(a[0], b[0]));
-
- return pairs;
-}
-
-/**
- * Returns whether the given value is a finite number without type-coercion
- *
- * @param value - the value to test
- * @returns whether or not value is a finite number without type-coercion
- */
-export function isNumber(value: any): value is number {
- return typeof value === "number" && isFinite(value);
-}
-
-/**
- * Removes zero width chars, diacritics and whitespace from the string
- * Also applies an unhomoglyph on the string, to prevent similar looking chars
- * @param str - the string to remove hidden characters from
- * @returns a string with the hidden characters removed
- */
-export function removeHiddenChars(str: string): string {
- if (typeof str === "string") {
- return unhomoglyph(str.normalize("NFD").replace(removeHiddenCharsRegex, ""));
- }
- return "";
-}
-
-/**
- * Removes the direction override characters from a string
- * @returns string with chars removed
- */
-export function removeDirectionOverrideChars(str: string): string {
- if (typeof str === "string") {
- return str.replace(/[\u202d-\u202e]/g, "");
- }
- return "";
-}
-
-export function normalize(str: string): string {
- // Note: we have to match the filter with the removeHiddenChars() because the
- // function strips spaces and other characters (M becomes RN for example, in lowercase).
- return (
- removeHiddenChars(str.toLowerCase())
- // Strip all punctuation
- .replace(/[\\'!"#$%&()*+,\-./:;<=>?@[\]^_`{|}~\u2000-\u206f\u2e00-\u2e7f]/g, "")
- // We also doubly convert to lowercase to work around oddities of the library.
- .toLowerCase()
- );
-}
-
-// Regex matching bunch of unicode control characters and otherwise misleading/invisible characters.
-// Includes:
-// various width spaces U+2000 - U+200D
-// LTR and RTL marks U+200E and U+200F
-// LTR/RTL and other directional formatting marks U+202A - U+202F
-// Arabic Letter RTL mark U+061C
-// Combining characters U+0300 - U+036F
-// Zero width no-break space (BOM) U+FEFF
-// Blank/invisible characters (U2800, U2062-U2063)
-// eslint-disable-next-line no-misleading-character-class
-const removeHiddenCharsRegex = /[\u2000-\u200F\u202A-\u202F\u0300-\u036F\uFEFF\u061C\u2800\u2062-\u2063\s]/g;
-
-export function escapeRegExp(string: string): string {
- return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
-}
-
-export function globToRegexp(glob: string, extended = false): string {
- // From
- // https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
- // Because micromatch is about 130KB with dependencies,
- // and minimatch is not much better.
- const replacements: [RegExp, string | ((substring: string, ...args: any[]) => string)][] = [
- [/\\\*/g, ".*"],
- [/\?/g, "."],
- ];
- if (!extended) {
- replacements.push([
- /\\\[(!|)(.*)\\]/g,
- (_match: string, neg: string, pat: string): string =>
- ["[", neg ? "^" : "", pat.replace(/\\-/, "-"), "]"].join(""),
- ]);
- }
- return replacements.reduce(
- // https://github.com/microsoft/TypeScript/issues/30134
- (pat, args) => (args ? pat.replace(args[0], args[1] as any) : pat),
- escapeRegExp(glob),
- );
-}
-
-export function ensureNoTrailingSlash(url: string): string;
-export function ensureNoTrailingSlash(url: undefined): undefined;
-export function ensureNoTrailingSlash(url?: string): string | undefined;
-export function ensureNoTrailingSlash(url?: string): string | undefined {
- if (url?.endsWith("/")) {
- return url.slice(0, -1);
- } else {
- return url;
- }
-}
-
-/**
- * Returns a promise which resolves with a given value after the given number of ms
- */
-export function sleep<T>(ms: number, value?: T): Promise<T> {
- return new Promise((resolve) => {
- setTimeout(resolve, ms, value);
- });
-}
-
-/**
- * Promise/async version of {@link setImmediate}.
- */
-export function immediate(): Promise<void> {
- return new Promise(setImmediate);
-}
-
-export function isNullOrUndefined(val: any): boolean {
- return val === null || val === undefined;
-}
-
-export interface IDeferred<T> {
- resolve: (value: T | Promise<T>) => void;
- reject: (reason?: any) => void;
- promise: Promise<T>;
-}
-
-// Returns a Deferred
-export function defer<T = void>(): IDeferred<T> {
- let resolve!: IDeferred<T>["resolve"];
- let reject!: IDeferred<T>["reject"];
-
- const promise = new Promise<T>((_resolve, _reject) => {
- resolve = _resolve;
- reject = _reject;
- });
-
- return { resolve, reject, promise };
-}
-
-export async function promiseMapSeries<T>(
- promises: Array<T | Promise<T>>,
- fn: (t: T) => Promise<unknown> | undefined, // if async we don't care about the type as we only await resolution
-): Promise<void> {
- for (const o of promises) {
- await fn(await o);
- }
-}
-
-export function promiseTry<T>(fn: () => T | Promise<T>): Promise<T> {
- return Promise.resolve(fn());
-}
-
-// Creates and awaits all promises, running no more than `chunkSize` at the same time
-export async function chunkPromises<T>(fns: (() => Promise<T>)[], chunkSize: number): Promise<T[]> {
- const results: T[] = [];
- for (let i = 0; i < fns.length; i += chunkSize) {
- results.push(...(await Promise.all(fns.slice(i, i + chunkSize).map((fn) => fn()))));
- }
- return results;
-}
-
-/**
- * Retries the function until it succeeds or is interrupted. The given function must return
- * a promise which throws/rejects on error, otherwise the retry will assume the request
- * succeeded. The promise chain returned will contain the successful promise. The given function
- * should always return a new promise.
- * @param promiseFn - The function to call to get a fresh promise instance. Takes an
- * attempt count as an argument, for logging/debugging purposes.
- * @returns The promise for the retried operation.
- */
-export function simpleRetryOperation<T>(promiseFn: (attempt: number) => Promise<T>): Promise<T> {
- return promiseRetry(
- (attempt: number) => {
- return promiseFn(attempt);
- },
- {
- forever: true,
- factor: 2,
- minTimeout: 3000, // ms
- maxTimeout: 15000, // ms
- },
- );
-}
-
-// String averaging inspired by https://stackoverflow.com/a/2510816
-// Dev note: We make the alphabet a string because it's easier to write syntactically
-// than arrays. Thankfully, strings implement the useful parts of the Array interface
-// anyhow.
-
-/**
- * The default alphabet used by string averaging in this SDK. This matches
- * all usefully printable ASCII characters (0x20-0x7E, inclusive).
- */
-export const DEFAULT_ALPHABET = ((): string => {
- let str = "";
- for (let c = 0x20; c <= 0x7e; c++) {
- str += String.fromCharCode(c);
- }
- return str;
-})();
-
-/**
- * Pads a string using the given alphabet as a base. The returned string will be
- * padded at the end with the first character in the alphabet.
- *
- * This is intended for use with string averaging.
- * @param s - The string to pad.
- * @param n - The length to pad to.
- * @param alphabet - The alphabet to use as a single string.
- * @returns The padded string.
- */
-export function alphabetPad(s: string, n: number, alphabet = DEFAULT_ALPHABET): string {
- return s.padEnd(n, alphabet[0]);
-}
-
-/**
- * Converts a baseN number to a string, where N is the alphabet's length.
- *
- * This is intended for use with string averaging.
- * @param n - The baseN number.
- * @param alphabet - The alphabet to use as a single string.
- * @returns The baseN number encoded as a string from the alphabet.
- */
-export function baseToString(n: bigint, alphabet = DEFAULT_ALPHABET): string {
- // Developer note: the stringToBase() function offsets the character set by 1 so that repeated
- // characters (ie: "aaaaaa" in a..z) don't come out as zero. We have to reverse this here as
- // otherwise we'll be wrong in our conversion. Undoing a +1 before an exponent isn't very fun
- // though, so we rely on a lengthy amount of `x - 1` and integer division rules to reach a
- // sane state. This also means we have to do rollover detection: see below.
-
- const len = BigInt(alphabet.length);
- if (n <= len) {
- return alphabet[Number(n) - 1] ?? "";
- }
-
- let d = n / len;
- let r = Number(n % len) - 1;
-
- // Rollover detection: if the remainder is negative, it means that the string needs
- // to roll over by 1 character downwards (ie: in a..z, the previous to "aaa" would be
- // "zz").
- if (r < 0) {
- d -= BigInt(Math.abs(r)); // abs() is just to be clear what we're doing. Could also `+= r`.
- r = Number(len) - 1;
- }
-
- return baseToString(d, alphabet) + alphabet[r];
-}
-
-/**
- * Converts a string to a baseN number, where N is the alphabet's length.
- *
- * This is intended for use with string averaging.
- * @param s - The string to convert to a number.
- * @param alphabet - The alphabet to use as a single string.
- * @returns The baseN number.
- */
-export function stringToBase(s: string, alphabet = DEFAULT_ALPHABET): bigint {
- const len = BigInt(alphabet.length);
-
- // In our conversion to baseN we do a couple performance optimizations to avoid using
- // excess CPU and such. To create baseN numbers, the input string needs to be reversed
- // so the exponents stack up appropriately, as the last character in the unreversed
- // string has less impact than the first character (in "abc" the A is a lot more important
- // for lexicographic sorts). We also do a trick with the character codes to optimize the
- // alphabet lookup, avoiding an index scan of `alphabet.indexOf(reversedStr[i])` - we know
- // that the alphabet and (theoretically) the input string are constrained on character sets
- // and thus can do simple subtraction to end up with the same result.
-
- // Developer caution: we carefully cast to BigInt here to avoid losing precision. We cannot
- // rely on Math.pow() (for example) to be capable of handling our insane numbers.
-
- let result = BigInt(0);
- for (let i = s.length - 1, j = BigInt(0); i >= 0; i--, j++) {
- const charIndex = s.charCodeAt(i) - alphabet.charCodeAt(0);
-
- // We add 1 to the char index to offset the whole numbering scheme. We unpack this in
- // the baseToString() function.
- result += BigInt(1 + charIndex) * len ** j;
- }
- return result;
-}
-
-/**
- * Averages two strings, returning the midpoint between them. This is accomplished by
- * converting both to baseN numbers (where N is the alphabet's length) then averaging
- * those before re-encoding as a string.
- * @param a - The first string.
- * @param b - The second string.
- * @param alphabet - The alphabet to use as a single string.
- * @returns The midpoint between the strings, as a string.
- */
-export function averageBetweenStrings(a: string, b: string, alphabet = DEFAULT_ALPHABET): string {
- const padN = Math.max(a.length, b.length);
- const baseA = stringToBase(alphabetPad(a, padN, alphabet), alphabet);
- const baseB = stringToBase(alphabetPad(b, padN, alphabet), alphabet);
- const avg = (baseA + baseB) / BigInt(2);
-
- // Detect integer division conflicts. This happens when two numbers are divided too close so
- // we lose a .5 precision. We need to add a padding character in these cases.
- if (avg === baseA || avg == baseB) {
- return baseToString(avg, alphabet) + alphabet[0];
- }
-
- return baseToString(avg, alphabet);
-}
-
-/**
- * Finds the next string using the alphabet provided. This is done by converting the
- * string to a baseN number, where N is the alphabet's length, then adding 1 before
- * converting back to a string.
- * @param s - The string to start at.
- * @param alphabet - The alphabet to use as a single string.
- * @returns The string which follows the input string.
- */
-export function nextString(s: string, alphabet = DEFAULT_ALPHABET): string {
- return baseToString(stringToBase(s, alphabet) + BigInt(1), alphabet);
-}
-
-/**
- * Finds the previous string using the alphabet provided. This is done by converting the
- * string to a baseN number, where N is the alphabet's length, then subtracting 1 before
- * converting back to a string.
- * @param s - The string to start at.
- * @param alphabet - The alphabet to use as a single string.
- * @returns The string which precedes the input string.
- */
-export function prevString(s: string, alphabet = DEFAULT_ALPHABET): string {
- return baseToString(stringToBase(s, alphabet) - BigInt(1), alphabet);
-}
-
-/**
- * Compares strings lexicographically as a sort-safe function.
- * @param a - The first (reference) string.
- * @param b - The second (compare) string.
- * @returns Negative if the reference string is before the compare string;
- * positive if the reference string is after; and zero if equal.
- */
-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.
- if (a < b) {
- return -1;
- } else if (a > b) {
- return 1;
- } else {
- return 0;
- }
-}
-
-const collator = new Intl.Collator();
-/**
- * Performant language-sensitive string comparison
- * @param a - the first string to compare
- * @param b - the second string to compare
- */
-export function compare(a: string, b: string): number {
- return collator.compare(a, b);
-}
-
-/**
- * This function is similar to Object.assign() but it assigns recursively and
- * allows you to ignore nullish values from the source
- *
- * @returns the target object
- */
-export function recursivelyAssign<T1 extends T2, T2 extends Record<string, any>>(
- target: T1,
- source: T2,
- ignoreNullish = false,
-): T1 & T2 {
- for (const [sourceKey, sourceValue] of Object.entries(source)) {
- if (target[sourceKey] instanceof Object && sourceValue) {
- recursivelyAssign(target[sourceKey], sourceValue);
- continue;
- }
- if ((sourceValue !== null && sourceValue !== undefined) || !ignoreNullish) {
- target[sourceKey as keyof T1] = sourceValue;
- continue;
- }
- }
- return target as T1 & T2;
-}
-
-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);
-}
-
-export function isSupportedReceiptType(receiptType: string): boolean {
- return [ReceiptType.Read, ReceiptType.ReadPrivate].includes(receiptType as ReceiptType);
-}
-
-/**
- * Determines whether two maps are equal.
- * @param eq - The equivalence relation to compare values by. Defaults to strict equality.
- */
-export function mapsEqual<K, V>(x: Map<K, V>, y: Map<K, V>, eq = (v1: V, v2: V): boolean => v1 === v2): boolean {
- if (x.size !== y.size) return false;
- for (const [k, v1] of x) {
- const v2 = y.get(k);
- if (v2 === undefined || !eq(v1, v2)) return false;
- }
- return true;
-}
-
-function processMapToObjectValue(value: any): any {
- if (value instanceof Map) {
- // Value is a Map. Recursively map it to an object.
- return recursiveMapToObject(value);
- } else if (Array.isArray(value)) {
- // Value is an Array. Recursively map the value (e.g. to cover Array of Arrays).
- return value.map((v) => processMapToObjectValue(v));
- } else {
- return value;
- }
-}
-
-/**
- * Recursively converts Maps to plain objects.
- * Also supports sub-lists of Maps.
- */
-export function recursiveMapToObject(map: Map<any, any>): any {
- const targetMap = new Map();
-
- for (const [key, value] of map) {
- targetMap.set(key, processMapToObjectValue(value));
- }
-
- return Object.fromEntries(targetMap.entries());
-}
-
-export function unsafeProp<K extends keyof any | undefined>(prop: K): boolean {
- return prop === "__proto__" || prop === "prototype" || prop === "constructor";
-}
-
-export function safeSet<K extends keyof any>(obj: Record<any, any>, prop: K, value: any): void {
- if (unsafeProp(prop)) {
- throw new Error("Trying to modify prototype or constructor");
- }
-
- obj[prop] = value;
-}
-
-export function noUnsafeEventProps(event: Partial<IEvent>): boolean {
- return !(
- unsafeProp(event.room_id) ||
- unsafeProp(event.sender) ||
- unsafeProp(event.user_id) ||
- unsafeProp(event.event_id)
- );
-}
-
-export class MapWithDefault<K, V> extends Map<K, V> {
- public constructor(private createDefault: () => V) {
- super();
- }
-
- /**
- * Returns the value if the key already exists.
- * If not, it creates a new value under that key using the ctor callback and returns it.
- */
- public getOrCreate(key: K): V {
- if (!this.has(key)) {
- this.set(key, this.createDefault());
- }
-
- return this.get(key)!;
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/audioContext.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/audioContext.ts
deleted file mode 100644
index 7cf3ed3..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/audioContext.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
-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.
-*/
-
-let audioContext: AudioContext | null = null;
-let refCount = 0;
-
-/**
- * Acquires a reference to the shared AudioContext.
- * It's highly recommended to reuse this AudioContext rather than creating your
- * own, because multiple AudioContexts can be problematic in some browsers.
- * Make sure to call releaseContext when you're done using it.
- * @returns The shared AudioContext
- */
-export const acquireContext = (): AudioContext => {
- if (audioContext === null) audioContext = new AudioContext();
- refCount++;
- return audioContext;
-};
-
-/**
- * Signals that one of the references to the shared AudioContext has been
- * released, allowing the context and associated hardware resources to be
- * cleaned up if nothing else is using it.
- */
-export const releaseContext = (): void => {
- refCount--;
- if (refCount === 0) {
- audioContext?.close();
- audioContext = null;
- }
-};
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/call.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/call.ts
deleted file mode 100644
index cd75c10..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/call.ts
+++ /dev/null
@@ -1,2962 +0,0 @@
-/*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2017 New Vector Ltd
-Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
-Copyright 2021 - 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.
-*/
-
-/**
- * This is an internal module. See {@link createNewMatrixCall} for the public API.
- */
-
-import { v4 as uuidv4 } from "uuid";
-import { parse as parseSdp, write as writeSdp } from "sdp-transform";
-
-import { logger } from "../logger";
-import * as utils from "../utils";
-import { IContent, MatrixEvent } from "../models/event";
-import { EventType, ToDeviceMessageId } from "../@types/event";
-import { RoomMember } from "../models/room-member";
-import { randomString } from "../randomstring";
-import {
- MCallReplacesEvent,
- MCallAnswer,
- MCallInviteNegotiate,
- CallCapabilities,
- SDPStreamMetadataPurpose,
- SDPStreamMetadata,
- SDPStreamMetadataKey,
- MCallSDPStreamMetadataChanged,
- MCallSelectAnswer,
- MCAllAssertedIdentity,
- MCallCandidates,
- MCallBase,
- MCallHangupReject,
-} from "./callEventTypes";
-import { CallFeed } from "./callFeed";
-import { MatrixClient } from "../client";
-import { EventEmitterEvents, TypedEventEmitter } from "../models/typed-event-emitter";
-import { DeviceInfo } from "../crypto/deviceinfo";
-import { GroupCallUnknownDeviceError } from "./groupCall";
-import { IScreensharingOpts } from "./mediaHandler";
-import { MatrixError } from "../http-api";
-import { GroupCallStats } from "./stats/groupCallStats";
-
-interface CallOpts {
- // The room ID for this call.
- roomId: string;
- invitee?: string;
- // The Matrix Client instance to send events to.
- client: MatrixClient;
- /**
- * Whether relay through TURN should be forced.
- * @deprecated use opts.forceTURN when creating the matrix client
- * since it's only possible to set this option on outbound calls.
- */
- forceTURN?: boolean;
- // A list of TURN servers.
- turnServers?: Array<TurnServer>;
- opponentDeviceId?: string;
- opponentSessionId?: string;
- groupCallId?: string;
-}
-
-interface TurnServer {
- urls: Array<string>;
- username?: string;
- password?: string;
- ttl?: number;
-}
-
-interface AssertedIdentity {
- id: string;
- displayName: string;
-}
-
-enum MediaType {
- AUDIO = "audio",
- VIDEO = "video",
-}
-
-enum CodecName {
- OPUS = "opus",
- // add more as needed
-}
-
-// Used internally to specify modifications to codec parameters in SDP
-interface CodecParamsMod {
- mediaType: MediaType;
- codec: CodecName;
- enableDtx?: boolean; // true to enable discontinuous transmission, false to disable, undefined to leave as-is
- maxAverageBitrate?: number; // sets the max average bitrate, or undefined to leave as-is
-}
-
-export enum CallState {
- Fledgling = "fledgling",
- InviteSent = "invite_sent",
- WaitLocalMedia = "wait_local_media",
- CreateOffer = "create_offer",
- CreateAnswer = "create_answer",
- Connecting = "connecting",
- Connected = "connected",
- Ringing = "ringing",
- Ended = "ended",
-}
-
-export enum CallType {
- Voice = "voice",
- Video = "video",
-}
-
-export enum CallDirection {
- Inbound = "inbound",
- Outbound = "outbound",
-}
-
-export enum CallParty {
- Local = "local",
- Remote = "remote",
-}
-
-export enum CallEvent {
- Hangup = "hangup",
- State = "state",
- Error = "error",
- Replaced = "replaced",
-
- // The value of isLocalOnHold() has changed
- LocalHoldUnhold = "local_hold_unhold",
- // The value of isRemoteOnHold() has changed
- RemoteHoldUnhold = "remote_hold_unhold",
- // backwards compat alias for LocalHoldUnhold: remove in a major version bump
- HoldUnhold = "hold_unhold",
- // Feeds have changed
- FeedsChanged = "feeds_changed",
-
- AssertedIdentityChanged = "asserted_identity_changed",
-
- LengthChanged = "length_changed",
-
- DataChannel = "datachannel",
-
- SendVoipEvent = "send_voip_event",
-}
-
-export enum CallErrorCode {
- /** The user chose to end the call */
- UserHangup = "user_hangup",
-
- /** An error code when the local client failed to create an offer. */
- LocalOfferFailed = "local_offer_failed",
- /**
- * An error code when there is no local mic/camera to use. This may be because
- * the hardware isn't plugged in, or the user has explicitly denied access.
- */
- NoUserMedia = "no_user_media",
-
- /**
- * Error code used when a call event failed to send
- * because unknown devices were present in the room
- */
- UnknownDevices = "unknown_devices",
-
- /**
- * Error code used when we fail to send the invite
- * for some reason other than there being unknown devices
- */
- SendInvite = "send_invite",
-
- /**
- * An answer could not be created
- */
- CreateAnswer = "create_answer",
-
- /**
- * An offer could not be created
- */
- CreateOffer = "create_offer",
-
- /**
- * Error code used when we fail to send the answer
- * for some reason other than there being unknown devices
- */
- SendAnswer = "send_answer",
-
- /**
- * The session description from the other side could not be set
- */
- SetRemoteDescription = "set_remote_description",
-
- /**
- * The session description from this side could not be set
- */
- SetLocalDescription = "set_local_description",
-
- /**
- * A different device answered the call
- */
- AnsweredElsewhere = "answered_elsewhere",
-
- /**
- * No media connection could be established to the other party
- */
- IceFailed = "ice_failed",
-
- /**
- * The invite timed out whilst waiting for an answer
- */
- InviteTimeout = "invite_timeout",
-
- /**
- * The call was replaced by another call
- */
- Replaced = "replaced",
-
- /**
- * Signalling for the call could not be sent (other than the initial invite)
- */
- SignallingFailed = "signalling_timeout",
-
- /**
- * The remote party is busy
- */
- UserBusy = "user_busy",
-
- /**
- * We transferred the call off to somewhere else
- */
- Transferred = "transferred",
-
- /**
- * A call from the same user was found with a new session id
- */
- NewSession = "new_session",
-}
-
-/**
- * The version field that we set in m.call.* events
- */
-const VOIP_PROTO_VERSION = "1";
-
-/** The fallback ICE server to use for STUN or TURN protocols. */
-const FALLBACK_ICE_SERVER = "stun:turn.matrix.org";
-
-/** The length of time a call can be ringing for. */
-const CALL_TIMEOUT_MS = 60 * 1000; // ms
-/** The time after which we increment callLength */
-const CALL_LENGTH_INTERVAL = 1000; // ms
-/** The time after which we end the call, if ICE got disconnected */
-const ICE_DISCONNECTED_TIMEOUT = 30 * 1000; // ms
-
-export class CallError extends Error {
- public readonly code: string;
-
- public constructor(code: CallErrorCode, msg: string, err: Error) {
- // Still don't think there's any way to have proper nested errors
- super(msg + ": " + err);
-
- this.code = code;
- }
-}
-
-export function genCallID(): string {
- return Date.now().toString() + randomString(16);
-}
-
-function getCodecParamMods(isPtt: boolean): CodecParamsMod[] {
- const mods = [
- {
- mediaType: "audio",
- codec: "opus",
- enableDtx: true,
- maxAverageBitrate: isPtt ? 12000 : undefined,
- },
- ] as CodecParamsMod[];
-
- return mods;
-}
-
-export interface VoipEvent {
- type: "toDevice" | "sendEvent";
- eventType: string;
- userId?: string;
- opponentDeviceId?: string;
- roomId?: string;
- content: Record<string, unknown>;
-}
-
-/**
- * These now all have the call object as an argument. Why? Well, to know which call a given event is
- * about you have three options:
- * 1. Use a closure as the callback that remembers what call it's listening to. This can be
- * a pain because you need to pass the listener function again when you remove the listener,
- * which might be somewhere else.
- * 2. Use not-very-well-known fact that EventEmitter sets 'this' to the emitter object in the
- * callback. This doesn't really play well with modern Typescript and eslint and doesn't work
- * with our pattern of re-emitting events.
- * 3. Pass the object in question as an argument to the callback.
- *
- * Now that we have group calls which have to deal with multiple call objects, this will
- * become more important, and I think methods 1 and 2 are just going to cause issues.
- */
-export type CallEventHandlerMap = {
- [CallEvent.DataChannel]: (channel: RTCDataChannel, call: MatrixCall) => void;
- [CallEvent.FeedsChanged]: (feeds: CallFeed[], call: MatrixCall) => void;
- [CallEvent.Replaced]: (newCall: MatrixCall, oldCall: MatrixCall) => void;
- [CallEvent.Error]: (error: CallError, call: MatrixCall) => void;
- [CallEvent.RemoteHoldUnhold]: (onHold: boolean, call: MatrixCall) => void;
- [CallEvent.LocalHoldUnhold]: (onHold: boolean, call: MatrixCall) => void;
- [CallEvent.LengthChanged]: (length: number, call: MatrixCall) => void;
- [CallEvent.State]: (state: CallState, oldState: CallState, call: MatrixCall) => void;
- [CallEvent.Hangup]: (call: MatrixCall) => void;
- [CallEvent.AssertedIdentityChanged]: (call: MatrixCall) => void;
- /* @deprecated */
- [CallEvent.HoldUnhold]: (onHold: boolean) => void;
- [CallEvent.SendVoipEvent]: (event: VoipEvent, call: MatrixCall) => void;
-};
-
-// The key of the transceiver map (purpose + media type, separated by ':')
-type TransceiverKey = string;
-
-// generates keys for the map of transceivers
-// kind is unfortunately a string rather than MediaType as this is the type of
-// track.kind
-function getTransceiverKey(purpose: SDPStreamMetadataPurpose, kind: TransceiverKey): string {
- return purpose + ":" + kind;
-}
-
-export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap> {
- public roomId: string;
- public callId: string;
- public invitee?: string;
- public hangupParty?: CallParty;
- public hangupReason?: string;
- public direction?: CallDirection;
- public ourPartyId: string;
- public peerConn?: RTCPeerConnection;
- public toDeviceSeq = 0;
-
- // whether this call should have push-to-talk semantics
- // This should be set by the consumer on incoming & outgoing calls.
- public isPtt = false;
-
- private _state = CallState.Fledgling;
- private readonly client: MatrixClient;
- private readonly forceTURN?: boolean;
- private readonly turnServers: Array<TurnServer>;
- // A queue for candidates waiting to go out.
- // We try to amalgamate candidates into a single candidate message where
- // possible
- private candidateSendQueue: Array<RTCIceCandidate> = [];
- private candidateSendTries = 0;
- private candidatesEnded = false;
- private feeds: Array<CallFeed> = [];
-
- // our transceivers for each purpose and type of media
- private transceivers = new Map<TransceiverKey, RTCRtpTransceiver>();
-
- private inviteOrAnswerSent = false;
- private waitForLocalAVStream = false;
- private successor?: MatrixCall;
- private opponentMember?: RoomMember;
- private opponentVersion?: number | string;
- // The party ID of the other side: undefined if we haven't chosen a partner
- // yet, null if we have but they didn't send a party ID.
- private opponentPartyId: string | null | undefined;
- private opponentCaps?: CallCapabilities;
- private iceDisconnectedTimeout?: ReturnType<typeof setTimeout>;
- private inviteTimeout?: ReturnType<typeof setTimeout>;
- private readonly removeTrackListeners = new Map<MediaStream, () => void>();
-
- // 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
- private remoteOnHold = false;
-
- // the stats for the call at the point it ended. We can't get these after we
- // tear the call down, so we just grab a snapshot before we stop the call.
- // The typescript definitions have this type as 'any' :(
- private callStatsAtEnd?: any[];
-
- // Perfect negotiation state: https://www.w3.org/TR/webrtc/#perfect-negotiation-example
- private makingOffer = false;
- private ignoreOffer = false;
-
- private responsePromiseChain?: Promise<void>;
-
- // If candidates arrive before we've picked an opponent (which, in particular,
- // will happen if the opponent sends candidates eagerly before the user answers
- // the call) we buffer them up here so we can then add the ones from the party we pick
- private remoteCandidateBuffer = new Map<string, RTCIceCandidate[]>();
-
- private remoteAssertedIdentity?: AssertedIdentity;
- private remoteSDPStreamMetadata?: SDPStreamMetadata;
-
- private callLengthInterval?: ReturnType<typeof setInterval>;
- private callStartTime?: number;
-
- private opponentDeviceId?: string;
- private opponentDeviceInfo?: DeviceInfo;
- private opponentSessionId?: string;
- public groupCallId?: string;
-
- // Used to keep the timer for the delay before actually stopping our
- // video track after muting (see setLocalVideoMuted)
- private stopVideoTrackTimer?: ReturnType<typeof setTimeout>;
- // Used to allow connection without Video and Audio. To establish a webrtc connection without media a Data channel is
- // needed At the moment this property is true if we allow MatrixClient with isVoipWithNoMediaAllowed = true
- private readonly isOnlyDataChannelAllowed: boolean;
- private stats: GroupCallStats | undefined;
-
- /**
- * Construct a new Matrix Call.
- * @param opts - Config options.
- */
- public constructor(opts: CallOpts) {
- super();
-
- this.roomId = opts.roomId;
- this.invitee = opts.invitee;
- this.client = opts.client;
-
- if (!this.client.deviceId) throw new Error("Client must have a device ID to start calls");
-
- this.forceTURN = opts.forceTURN ?? false;
- this.ourPartyId = this.client.deviceId;
- this.opponentDeviceId = opts.opponentDeviceId;
- this.opponentSessionId = opts.opponentSessionId;
- this.groupCallId = opts.groupCallId;
- // Array of Objects with urls, username, credential keys
- this.turnServers = opts.turnServers || [];
- if (this.turnServers.length === 0 && this.client.isFallbackICEServerAllowed()) {
- this.turnServers.push({
- urls: [FALLBACK_ICE_SERVER],
- });
- }
- for (const server of this.turnServers) {
- utils.checkObjectHasKeys(server, ["urls"]);
- }
- this.callId = genCallID();
- // If the Client provides calls without audio and video we need a datachannel for a webrtc connection
- this.isOnlyDataChannelAllowed = this.client.isVoipWithNoMediaAllowed;
- }
-
- /**
- * Place a voice call to this room.
- * @throws If you have not specified a listener for 'error' events.
- */
- public async placeVoiceCall(): Promise<void> {
- await this.placeCall(true, false);
- }
-
- /**
- * Place a video call to this room.
- * @throws If you have not specified a listener for 'error' events.
- */
- public async placeVideoCall(): Promise<void> {
- await this.placeCall(true, true);
- }
-
- /**
- * Create a datachannel using this call's peer connection.
- * @param label - A human readable label for this datachannel
- * @param options - An object providing configuration options for the data channel.
- */
- public createDataChannel(label: string, options: RTCDataChannelInit | undefined): RTCDataChannel {
- const dataChannel = this.peerConn!.createDataChannel(label, options);
- this.emit(CallEvent.DataChannel, dataChannel, this);
- return dataChannel;
- }
-
- public getOpponentMember(): RoomMember | undefined {
- return this.opponentMember;
- }
-
- public getOpponentDeviceId(): string | undefined {
- return this.opponentDeviceId;
- }
-
- public getOpponentSessionId(): string | undefined {
- return this.opponentSessionId;
- }
-
- public opponentCanBeTransferred(): boolean {
- return Boolean(this.opponentCaps && this.opponentCaps["m.call.transferee"]);
- }
-
- public opponentSupportsDTMF(): boolean {
- return Boolean(this.opponentCaps && this.opponentCaps["m.call.dtmf"]);
- }
-
- public getRemoteAssertedIdentity(): AssertedIdentity | undefined {
- return this.remoteAssertedIdentity;
- }
-
- public get state(): CallState {
- return this._state;
- }
-
- private set state(state: CallState) {
- const oldState = this._state;
- this._state = state;
- this.emit(CallEvent.State, state, oldState, this);
- }
-
- public get type(): CallType {
- // we may want to look for a video receiver here rather than a track to match the
- // sender behaviour, although in practice they should be the same thing
- return this.hasUserMediaVideoSender || this.hasRemoteUserMediaVideoTrack ? CallType.Video : CallType.Voice;
- }
-
- public get hasLocalUserMediaVideoTrack(): boolean {
- return !!this.localUsermediaStream?.getVideoTracks().length;
- }
-
- public get hasRemoteUserMediaVideoTrack(): boolean {
- return this.getRemoteFeeds().some((feed) => {
- return feed.purpose === SDPStreamMetadataPurpose.Usermedia && feed.stream?.getVideoTracks().length;
- });
- }
-
- public get hasLocalUserMediaAudioTrack(): boolean {
- return !!this.localUsermediaStream?.getAudioTracks().length;
- }
-
- public get hasRemoteUserMediaAudioTrack(): boolean {
- return this.getRemoteFeeds().some((feed) => {
- return feed.purpose === SDPStreamMetadataPurpose.Usermedia && !!feed.stream?.getAudioTracks().length;
- });
- }
-
- private get hasUserMediaAudioSender(): boolean {
- return Boolean(this.transceivers.get(getTransceiverKey(SDPStreamMetadataPurpose.Usermedia, "audio"))?.sender);
- }
-
- private get hasUserMediaVideoSender(): boolean {
- return Boolean(this.transceivers.get(getTransceiverKey(SDPStreamMetadataPurpose.Usermedia, "video"))?.sender);
- }
-
- public get localUsermediaFeed(): CallFeed | undefined {
- return this.getLocalFeeds().find((feed) => feed.purpose === SDPStreamMetadataPurpose.Usermedia);
- }
-
- public get localScreensharingFeed(): CallFeed | undefined {
- return this.getLocalFeeds().find((feed) => feed.purpose === SDPStreamMetadataPurpose.Screenshare);
- }
-
- public get localUsermediaStream(): MediaStream | undefined {
- return this.localUsermediaFeed?.stream;
- }
-
- public get localScreensharingStream(): MediaStream | undefined {
- return this.localScreensharingFeed?.stream;
- }
-
- public get remoteUsermediaFeed(): CallFeed | undefined {
- return this.getRemoteFeeds().find((feed) => feed.purpose === SDPStreamMetadataPurpose.Usermedia);
- }
-
- public get remoteScreensharingFeed(): CallFeed | undefined {
- return this.getRemoteFeeds().find((feed) => feed.purpose === SDPStreamMetadataPurpose.Screenshare);
- }
-
- public get remoteUsermediaStream(): MediaStream | undefined {
- return this.remoteUsermediaFeed?.stream;
- }
-
- public get remoteScreensharingStream(): MediaStream | undefined {
- return this.remoteScreensharingFeed?.stream;
- }
-
- private getFeedByStreamId(streamId: string): CallFeed | undefined {
- return this.getFeeds().find((feed) => feed.stream.id === streamId);
- }
-
- /**
- * Returns an array of all CallFeeds
- * @returns CallFeeds
- */
- public getFeeds(): Array<CallFeed> {
- return this.feeds;
- }
-
- /**
- * Returns an array of all local CallFeeds
- * @returns local CallFeeds
- */
- public getLocalFeeds(): Array<CallFeed> {
- return this.feeds.filter((feed) => feed.isLocal());
- }
-
- /**
- * Returns an array of all remote CallFeeds
- * @returns remote CallFeeds
- */
- public getRemoteFeeds(): Array<CallFeed> {
- return this.feeds.filter((feed) => !feed.isLocal());
- }
-
- private async initOpponentCrypto(): Promise<void> {
- if (!this.opponentDeviceId) return;
- if (!this.client.getUseE2eForGroupCall()) return;
- // It's possible to want E2EE and yet not have the means to manage E2EE
- // ourselves (for example if the client is a RoomWidgetClient)
- if (!this.client.isCryptoEnabled()) {
- // All we know is the device ID
- this.opponentDeviceInfo = new DeviceInfo(this.opponentDeviceId);
- return;
- }
- // if we've got to this point, we do want to init crypto, so throw if we can't
- if (!this.client.crypto) throw new Error("Crypto is not initialised.");
-
- const userId = this.invitee || this.getOpponentMember()?.userId;
-
- if (!userId) throw new Error("Couldn't find opponent user ID to init crypto");
-
- const deviceInfoMap = await this.client.crypto.deviceList.downloadKeys([userId], false);
- this.opponentDeviceInfo = deviceInfoMap.get(userId)?.get(this.opponentDeviceId);
- if (this.opponentDeviceInfo === undefined) {
- throw new GroupCallUnknownDeviceError(userId);
- }
- }
-
- /**
- * Generates and returns localSDPStreamMetadata
- * @returns localSDPStreamMetadata
- */
- private getLocalSDPStreamMetadata(updateStreamIds = false): SDPStreamMetadata {
- const metadata: SDPStreamMetadata = {};
- for (const localFeed of this.getLocalFeeds()) {
- if (updateStreamIds) {
- localFeed.sdpMetadataStreamId = localFeed.stream.id;
- }
-
- metadata[localFeed.sdpMetadataStreamId] = {
- purpose: localFeed.purpose,
- audio_muted: localFeed.isAudioMuted(),
- video_muted: localFeed.isVideoMuted(),
- };
- }
- return metadata;
- }
-
- /**
- * Returns true if there are no incoming feeds,
- * otherwise returns false
- * @returns no incoming feeds
- */
- public noIncomingFeeds(): boolean {
- return !this.feeds.some((feed) => !feed.isLocal());
- }
-
- private pushRemoteFeed(stream: MediaStream): void {
- // Fallback to old behavior if the other side doesn't support SDPStreamMetadata
- if (!this.opponentSupportsSDPStreamMetadata()) {
- this.pushRemoteFeedWithoutMetadata(stream);
- return;
- }
-
- const userId = this.getOpponentMember()!.userId;
- const purpose = this.remoteSDPStreamMetadata![stream.id].purpose;
- const audioMuted = this.remoteSDPStreamMetadata![stream.id].audio_muted;
- const videoMuted = this.remoteSDPStreamMetadata![stream.id].video_muted;
-
- if (!purpose) {
- logger.warn(
- `Call ${this.callId} pushRemoteFeed() ignoring stream because we didn't get any metadata about it (streamId=${stream.id})`,
- );
- return;
- }
-
- if (this.getFeedByStreamId(stream.id)) {
- logger.warn(
- `Call ${this.callId} pushRemoteFeed() ignoring stream because we already have a feed for it (streamId=${stream.id})`,
- );
- return;
- }
-
- this.feeds.push(
- new CallFeed({
- client: this.client,
- call: this,
- roomId: this.roomId,
- userId,
- deviceId: this.getOpponentDeviceId(),
- stream,
- purpose,
- audioMuted,
- videoMuted,
- }),
- );
-
- this.emit(CallEvent.FeedsChanged, this.feeds, this);
-
- logger.info(
- `Call ${this.callId} pushRemoteFeed() pushed stream (streamId=${stream.id}, active=${stream.active}, purpose=${purpose})`,
- );
- }
-
- /**
- * This method is used ONLY if the other client doesn't support sending SDPStreamMetadata
- */
- private pushRemoteFeedWithoutMetadata(stream: MediaStream): void {
- const userId = this.getOpponentMember()!.userId;
- // We can guess the purpose here since the other client can only send one stream
- const purpose = SDPStreamMetadataPurpose.Usermedia;
- const oldRemoteStream = this.feeds.find((feed) => !feed.isLocal())?.stream;
-
- // Note that we check by ID and always set the remote stream: Chrome appears
- // to make new stream objects when transceiver directionality is changed and the 'active'
- // status of streams change - Dave
- // If we already have a stream, check this stream has the same id
- if (oldRemoteStream && stream.id !== oldRemoteStream.id) {
- logger.warn(
- `Call ${this.callId} pushRemoteFeedWithoutMetadata() ignoring new stream because we already have stream (streamId=${stream.id})`,
- );
- return;
- }
-
- if (this.getFeedByStreamId(stream.id)) {
- logger.warn(
- `Call ${this.callId} pushRemoteFeedWithoutMetadata() ignoring stream because we already have a feed for it (streamId=${stream.id})`,
- );
- return;
- }
-
- this.feeds.push(
- new CallFeed({
- client: this.client,
- call: this,
- roomId: this.roomId,
- audioMuted: false,
- videoMuted: false,
- userId,
- deviceId: this.getOpponentDeviceId(),
- stream,
- purpose,
- }),
- );
-
- this.emit(CallEvent.FeedsChanged, this.feeds, this);
-
- logger.info(
- `Call ${this.callId} pushRemoteFeedWithoutMetadata() pushed stream (streamId=${stream.id}, active=${stream.active})`,
- );
- }
-
- private pushNewLocalFeed(stream: MediaStream, purpose: SDPStreamMetadataPurpose, addToPeerConnection = true): void {
- const userId = this.client.getUserId()!;
-
- // Tracks don't always start off enabled, eg. chrome will give a disabled
- // audio track if you ask for user media audio and already had one that
- // you'd set to disabled (presumably because it clones them internally).
- setTracksEnabled(stream.getAudioTracks(), true);
- setTracksEnabled(stream.getVideoTracks(), true);
-
- if (this.getFeedByStreamId(stream.id)) {
- logger.warn(
- `Call ${this.callId} pushNewLocalFeed() ignoring stream because we already have a feed for it (streamId=${stream.id})`,
- );
- return;
- }
-
- this.pushLocalFeed(
- new CallFeed({
- client: this.client,
- roomId: this.roomId,
- audioMuted: false,
- videoMuted: false,
- userId,
- deviceId: this.getOpponentDeviceId(),
- stream,
- purpose,
- }),
- addToPeerConnection,
- );
- }
-
- /**
- * Pushes supplied feed to the call
- * @param callFeed - to push
- * @param addToPeerConnection - whether to add the tracks to the peer connection
- */
- public pushLocalFeed(callFeed: CallFeed, addToPeerConnection = true): void {
- if (this.feeds.some((feed) => callFeed.stream.id === feed.stream.id)) {
- logger.info(
- `Call ${this.callId} pushLocalFeed() ignoring duplicate local stream (streamId=${callFeed.stream.id})`,
- );
- return;
- }
-
- this.feeds.push(callFeed);
-
- if (addToPeerConnection) {
- for (const track of callFeed.stream.getTracks()) {
- logger.info(
- `Call ${this.callId} pushLocalFeed() adding track to peer connection (id=${track.id}, kind=${track.kind}, streamId=${callFeed.stream.id}, streamPurpose=${callFeed.purpose}, enabled=${track.enabled})`,
- );
-
- const tKey = getTransceiverKey(callFeed.purpose, track.kind);
- if (this.transceivers.has(tKey)) {
- // we already have a sender, so we re-use it. We try to re-use transceivers as much
- // as possible because they can't be removed once added, so otherwise they just
- // accumulate which makes the SDP very large very quickly: in fact it only takes
- // about 6 video tracks to exceed the maximum size of an Olm-encrypted
- // Matrix event.
- const transceiver = this.transceivers.get(tKey)!;
-
- transceiver.sender.replaceTrack(track);
- // set the direction to indicate we're going to start sending again
- // (this will trigger the re-negotiation)
- transceiver.direction = transceiver.direction === "inactive" ? "sendonly" : "sendrecv";
- } else {
- // create a new one. We need to use addTrack rather addTransceiver for this because firefox
- // doesn't yet implement RTCRTPSender.setStreams()
- // (https://bugzilla.mozilla.org/show_bug.cgi?id=1510802) so we'd have no way to group the
- // two tracks together into a stream.
- const newSender = this.peerConn!.addTrack(track, callFeed.stream);
-
- // now go & fish for the new transceiver
- const newTransceiver = this.peerConn!.getTransceivers().find((t) => t.sender === newSender);
- if (newTransceiver) {
- this.transceivers.set(tKey, newTransceiver);
- } else {
- logger.warn(
- `Call ${this.callId} pushLocalFeed() didn't find a matching transceiver after adding track!`,
- );
- }
- }
- }
- }
-
- logger.info(
- `Call ${this.callId} pushLocalFeed() pushed stream (id=${callFeed.stream.id}, active=${callFeed.stream.active}, purpose=${callFeed.purpose})`,
- );
-
- this.emit(CallEvent.FeedsChanged, this.feeds, this);
- }
-
- /**
- * Removes local call feed from the call and its tracks from the peer
- * connection
- * @param callFeed - to remove
- */
- public removeLocalFeed(callFeed: CallFeed): void {
- const audioTransceiverKey = getTransceiverKey(callFeed.purpose, "audio");
- const videoTransceiverKey = getTransceiverKey(callFeed.purpose, "video");
-
- for (const transceiverKey of [audioTransceiverKey, videoTransceiverKey]) {
- // this is slightly mixing the track and transceiver API but is basically just shorthand.
- // There is no way to actually remove a transceiver, so this just sets it to inactive
- // (or recvonly) and replaces the source with nothing.
- if (this.transceivers.has(transceiverKey)) {
- const transceiver = this.transceivers.get(transceiverKey)!;
- if (transceiver.sender) this.peerConn!.removeTrack(transceiver.sender);
- }
- }
-
- if (callFeed.purpose === SDPStreamMetadataPurpose.Screenshare) {
- this.client.getMediaHandler().stopScreensharingStream(callFeed.stream);
- }
-
- this.deleteFeed(callFeed);
- }
-
- private deleteAllFeeds(): void {
- for (const feed of this.feeds) {
- if (!feed.isLocal() || !this.groupCallId) {
- feed.dispose();
- }
- }
-
- this.feeds = [];
- this.emit(CallEvent.FeedsChanged, this.feeds, this);
- }
-
- private deleteFeedByStream(stream: MediaStream): void {
- const feed = this.getFeedByStreamId(stream.id);
- if (!feed) {
- logger.warn(
- `Call ${this.callId} deleteFeedByStream() didn't find the feed to delete (streamId=${stream.id})`,
- );
- return;
- }
- this.deleteFeed(feed);
- }
-
- private deleteFeed(feed: CallFeed): void {
- feed.dispose();
- this.feeds.splice(this.feeds.indexOf(feed), 1);
- this.emit(CallEvent.FeedsChanged, this.feeds, this);
- }
-
- // The typescript definitions have this type as 'any' :(
- public async getCurrentCallStats(): Promise<any[] | undefined> {
- if (this.callHasEnded()) {
- return this.callStatsAtEnd;
- }
-
- return this.collectCallStats();
- }
-
- private async collectCallStats(): Promise<any[] | undefined> {
- // This happens when the call fails before it starts.
- // For example when we fail to get capture sources
- if (!this.peerConn) return;
-
- const statsReport = await this.peerConn.getStats();
- const stats: any[] = [];
- statsReport.forEach((item) => {
- stats.push(item);
- });
-
- return stats;
- }
-
- /**
- * Configure this call from an invite event. Used by MatrixClient.
- * @param event - The m.call.invite event
- */
- public async initWithInvite(event: MatrixEvent): Promise<void> {
- const invite = event.getContent<MCallInviteNegotiate>();
- this.direction = CallDirection.Inbound;
-
- // make sure we have valid turn creds. Unless something's gone wrong, it should
- // poll and keep the credentials valid so this should be instant.
- const haveTurnCreds = await this.client.checkTurnServers();
- if (!haveTurnCreds) {
- logger.warn(
- `Call ${this.callId} initWithInvite() failed to get TURN credentials! Proceeding with call anyway...`,
- );
- }
-
- const sdpStreamMetadata = invite[SDPStreamMetadataKey];
- if (sdpStreamMetadata) {
- this.updateRemoteSDPStreamMetadata(sdpStreamMetadata);
- } else {
- logger.debug(
- `Call ${this.callId} initWithInvite() did not get any SDPStreamMetadata! Can not send/receive multiple streams`,
- );
- }
-
- this.peerConn = this.createPeerConnection();
- // we must set the party ID before await-ing on anything: the call event
- // handler will start giving us more call events (eg. candidates) so if
- // we haven't set the party ID, we'll ignore them.
- this.chooseOpponent(event);
- await this.initOpponentCrypto();
- try {
- await this.peerConn.setRemoteDescription(invite.offer);
- await this.addBufferedIceCandidates();
- } catch (e) {
- logger.debug(`Call ${this.callId} initWithInvite() failed to set remote description`, e);
- this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false);
- return;
- }
-
- const remoteStream = this.feeds.find((feed) => !feed.isLocal())?.stream;
-
- // According to previous comments in this file, firefox at some point did not
- // add streams until media started arriving on them. Testing latest firefox
- // (81 at time of writing), this is no longer a problem, so let's do it the correct way.
- //
- // For example in case of no media webrtc connections like screen share only call we have to allow webrtc
- // connections without remote media. In this case we always use a data channel. At the moment we allow as well
- // only data channel as media in the WebRTC connection with this setup here.
- if (!this.isOnlyDataChannelAllowed && (!remoteStream || remoteStream.getTracks().length === 0)) {
- logger.error(
- `Call ${this.callId} initWithInvite() no remote stream or no tracks after setting remote description!`,
- );
- this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false);
- return;
- }
-
- this.state = CallState.Ringing;
-
- if (event.getLocalAge()) {
- // Time out the call if it's ringing for too long
- const ringingTimer = setTimeout(() => {
- if (this.state == CallState.Ringing) {
- logger.debug(`Call ${this.callId} initWithInvite() invite has expired. Hanging up.`);
- this.hangupParty = CallParty.Remote; // effectively
- this.state = CallState.Ended;
- this.stopAllMedia();
- if (this.peerConn!.signalingState != "closed") {
- this.peerConn!.close();
- }
- this.stats?.removeStatsReportGatherer(this.callId);
- this.emit(CallEvent.Hangup, this);
- }
- }, invite.lifetime - event.getLocalAge());
-
- const onState = (state: CallState): void => {
- if (state !== CallState.Ringing) {
- clearTimeout(ringingTimer);
- this.off(CallEvent.State, onState);
- }
- };
- this.on(CallEvent.State, onState);
- }
- }
-
- /**
- * Configure this call from a hangup or reject event. Used by MatrixClient.
- * @param event - The m.call.hangup event
- */
- public initWithHangup(event: MatrixEvent): void {
- // perverse as it may seem, sometimes we want to instantiate a call with a
- // hangup message (because when getting the state of the room on load, events
- // come in reverse order and we want to remember that a call has been hung up)
- this.state = CallState.Ended;
- }
-
- private shouldAnswerWithMediaType(
- wantedValue: boolean | undefined,
- valueOfTheOtherSide: boolean,
- type: "audio" | "video",
- ): boolean {
- if (wantedValue && !valueOfTheOtherSide) {
- // TODO: Figure out how to do this
- logger.warn(
- `Call ${this.callId} shouldAnswerWithMediaType() unable to answer with ${type} because the other side isn't sending it either.`,
- );
- return false;
- } else if (
- !utils.isNullOrUndefined(wantedValue) &&
- wantedValue !== valueOfTheOtherSide &&
- !this.opponentSupportsSDPStreamMetadata()
- ) {
- logger.warn(
- `Call ${this.callId} shouldAnswerWithMediaType() unable to answer with ${type}=${wantedValue} because the other side doesn't support it. Answering with ${type}=${valueOfTheOtherSide}.`,
- );
- return valueOfTheOtherSide!;
- }
- return wantedValue ?? valueOfTheOtherSide!;
- }
-
- /**
- * Answer a call.
- */
- public async answer(audio?: boolean, video?: boolean): Promise<void> {
- if (this.inviteOrAnswerSent) return;
- // TODO: Figure out how to do this
- if (audio === false && video === false) throw new Error("You CANNOT answer a call without media");
-
- if (!this.localUsermediaStream && !this.waitForLocalAVStream) {
- const prevState = this.state;
- const answerWithAudio = this.shouldAnswerWithMediaType(audio, this.hasRemoteUserMediaAudioTrack, "audio");
- const answerWithVideo = this.shouldAnswerWithMediaType(video, this.hasRemoteUserMediaVideoTrack, "video");
-
- this.state = CallState.WaitLocalMedia;
- this.waitForLocalAVStream = true;
-
- try {
- const stream = await this.client.getMediaHandler().getUserMediaStream(answerWithAudio, answerWithVideo);
- this.waitForLocalAVStream = false;
- const usermediaFeed = new CallFeed({
- client: this.client,
- roomId: this.roomId,
- userId: this.client.getUserId()!,
- deviceId: this.client.getDeviceId() ?? undefined,
- stream,
- purpose: SDPStreamMetadataPurpose.Usermedia,
- audioMuted: false,
- videoMuted: false,
- });
-
- const feeds = [usermediaFeed];
-
- if (this.localScreensharingFeed) {
- feeds.push(this.localScreensharingFeed);
- }
-
- this.answerWithCallFeeds(feeds);
- } catch (e) {
- if (answerWithVideo) {
- // Try to answer without video
- logger.warn(
- `Call ${this.callId} answer() failed to getUserMedia(), trying to getUserMedia() without video`,
- );
- this.state = prevState;
- this.waitForLocalAVStream = false;
- await this.answer(answerWithAudio, false);
- } else {
- this.getUserMediaFailed(<Error>e);
- return;
- }
- }
- } else if (this.waitForLocalAVStream) {
- this.state = CallState.WaitLocalMedia;
- }
- }
-
- public answerWithCallFeeds(callFeeds: CallFeed[]): void {
- if (this.inviteOrAnswerSent) return;
-
- this.queueGotCallFeedsForAnswer(callFeeds);
- }
-
- /**
- * Replace this call with a new call, e.g. for glare resolution. Used by
- * MatrixClient.
- * @param newCall - The new call.
- */
- public replacedBy(newCall: MatrixCall): void {
- logger.debug(`Call ${this.callId} replacedBy() running (newCallId=${newCall.callId})`);
- if (this.state === CallState.WaitLocalMedia) {
- logger.debug(
- `Call ${this.callId} replacedBy() telling new call to wait for local media (newCallId=${newCall.callId})`,
- );
- newCall.waitForLocalAVStream = true;
- } else if ([CallState.CreateOffer, CallState.InviteSent].includes(this.state)) {
- if (newCall.direction === CallDirection.Outbound) {
- newCall.queueGotCallFeedsForAnswer([]);
- } else {
- logger.debug(
- `Call ${this.callId} replacedBy() handing local stream to new call(newCallId=${newCall.callId})`,
- );
- newCall.queueGotCallFeedsForAnswer(this.getLocalFeeds().map((feed) => feed.clone()));
- }
- }
- this.successor = newCall;
- this.emit(CallEvent.Replaced, newCall, this);
- this.hangup(CallErrorCode.Replaced, true);
- }
-
- /**
- * Hangup a call.
- * @param reason - The reason why the call is being hung up.
- * @param suppressEvent - True to suppress emitting an event.
- */
- public hangup(reason: CallErrorCode, suppressEvent: boolean): void {
- if (this.callHasEnded()) return;
-
- logger.debug(`Call ${this.callId} hangup() ending call (reason=${reason})`);
- this.terminate(CallParty.Local, reason, !suppressEvent);
- // We don't want to send hangup here if we didn't even get to sending an invite
- if ([CallState.Fledgling, CallState.WaitLocalMedia].includes(this.state)) return;
- const content: IContent = {};
- // Don't send UserHangup reason to older clients
- if ((this.opponentVersion && this.opponentVersion !== 0) || reason !== CallErrorCode.UserHangup) {
- content["reason"] = reason;
- }
- this.sendVoipEvent(EventType.CallHangup, content);
- }
-
- /**
- * Reject a call
- * This used to be done by calling hangup, but is a separate method and protocol
- * event as of MSC2746.
- */
- public reject(): void {
- if (this.state !== CallState.Ringing) {
- throw Error("Call must be in 'ringing' state to reject!");
- }
-
- if (this.opponentVersion === 0) {
- logger.info(
- `Call ${this.callId} reject() opponent version is less than 1: sending hangup instead of reject (opponentVersion=${this.opponentVersion})`,
- );
- this.hangup(CallErrorCode.UserHangup, true);
- return;
- }
-
- logger.debug("Rejecting call: " + this.callId);
- this.terminate(CallParty.Local, CallErrorCode.UserHangup, true);
- this.sendVoipEvent(EventType.CallReject, {});
- }
-
- /**
- * Adds an audio and/or video track - upgrades the call
- * @param audio - should add an audio track
- * @param video - should add an video track
- */
- private async upgradeCall(audio: boolean, video: boolean): Promise<void> {
- // We don't do call downgrades
- if (!audio && !video) return;
- if (!this.opponentSupportsSDPStreamMetadata()) return;
-
- try {
- logger.debug(`Call ${this.callId} upgradeCall() upgrading call (audio=${audio}, video=${video})`);
- const getAudio = audio || this.hasLocalUserMediaAudioTrack;
- const getVideo = video || this.hasLocalUserMediaVideoTrack;
-
- // updateLocalUsermediaStream() will take the tracks, use them as
- // replacement and throw the stream away, so it isn't reusable
- const stream = await this.client.getMediaHandler().getUserMediaStream(getAudio, getVideo, false);
- await this.updateLocalUsermediaStream(stream, audio, video);
- } catch (error) {
- logger.error(`Call ${this.callId} upgradeCall() failed to upgrade the call`, error);
- this.emit(
- CallEvent.Error,
- new CallError(CallErrorCode.NoUserMedia, "Failed to get camera access: ", <Error>error),
- this,
- );
- }
- }
-
- /**
- * Returns true if this.remoteSDPStreamMetadata is defined, otherwise returns false
- * @returns can screenshare
- */
- public opponentSupportsSDPStreamMetadata(): boolean {
- return Boolean(this.remoteSDPStreamMetadata);
- }
-
- /**
- * If there is a screensharing stream returns true, otherwise returns false
- * @returns is screensharing
- */
- public isScreensharing(): boolean {
- return Boolean(this.localScreensharingStream);
- }
-
- /**
- * Starts/stops screensharing
- * @param enabled - the desired screensharing state
- * @param desktopCapturerSourceId - optional id of the desktop capturer source to use
- * @returns new screensharing state
- */
- public async setScreensharingEnabled(enabled: boolean, opts?: IScreensharingOpts): Promise<boolean> {
- // Skip if there is nothing to do
- if (enabled && this.isScreensharing()) {
- logger.warn(
- `Call ${this.callId} setScreensharingEnabled() there is already a screensharing stream - there is nothing to do!`,
- );
- return true;
- } else if (!enabled && !this.isScreensharing()) {
- logger.warn(
- `Call ${this.callId} setScreensharingEnabled() there already isn't a screensharing stream - there is nothing to do!`,
- );
- return false;
- }
-
- // Fallback to replaceTrack()
- if (!this.opponentSupportsSDPStreamMetadata()) {
- return this.setScreensharingEnabledWithoutMetadataSupport(enabled, opts);
- }
-
- logger.debug(`Call ${this.callId} setScreensharingEnabled() running (enabled=${enabled})`);
- if (enabled) {
- try {
- const stream = await this.client.getMediaHandler().getScreensharingStream(opts);
- if (!stream) return false;
- this.pushNewLocalFeed(stream, SDPStreamMetadataPurpose.Screenshare);
- return true;
- } catch (err) {
- logger.error(`Call ${this.callId} setScreensharingEnabled() failed to get screen-sharing stream:`, err);
- return false;
- }
- } else {
- const audioTransceiver = this.transceivers.get(
- getTransceiverKey(SDPStreamMetadataPurpose.Screenshare, "audio"),
- );
- const videoTransceiver = this.transceivers.get(
- getTransceiverKey(SDPStreamMetadataPurpose.Screenshare, "video"),
- );
-
- for (const transceiver of [audioTransceiver, videoTransceiver]) {
- // this is slightly mixing the track and transceiver API but is basically just shorthand
- // for removing the sender.
- if (transceiver && transceiver.sender) this.peerConn!.removeTrack(transceiver.sender);
- }
-
- this.client.getMediaHandler().stopScreensharingStream(this.localScreensharingStream!);
- this.deleteFeedByStream(this.localScreensharingStream!);
- return false;
- }
- }
-
- /**
- * Starts/stops screensharing
- * Should be used ONLY if the opponent doesn't support SDPStreamMetadata
- * @param enabled - the desired screensharing state
- * @param desktopCapturerSourceId - optional id of the desktop capturer source to use
- * @returns new screensharing state
- */
- private async setScreensharingEnabledWithoutMetadataSupport(
- enabled: boolean,
- opts?: IScreensharingOpts,
- ): Promise<boolean> {
- logger.debug(
- `Call ${this.callId} setScreensharingEnabledWithoutMetadataSupport() running (enabled=${enabled})`,
- );
- if (enabled) {
- try {
- const stream = await this.client.getMediaHandler().getScreensharingStream(opts);
- if (!stream) return false;
-
- const track = stream.getTracks().find((track) => track.kind === "video");
-
- const sender = this.transceivers.get(
- getTransceiverKey(SDPStreamMetadataPurpose.Usermedia, "video"),
- )?.sender;
-
- sender?.replaceTrack(track ?? null);
-
- this.pushNewLocalFeed(stream, SDPStreamMetadataPurpose.Screenshare, false);
-
- return true;
- } catch (err) {
- logger.error(
- `Call ${this.callId} setScreensharingEnabledWithoutMetadataSupport() failed to get screen-sharing stream:`,
- err,
- );
- return false;
- }
- } else {
- const track = this.localUsermediaStream?.getTracks().find((track) => track.kind === "video");
- const sender = this.transceivers.get(
- getTransceiverKey(SDPStreamMetadataPurpose.Usermedia, "video"),
- )?.sender;
- sender?.replaceTrack(track ?? null);
-
- this.client.getMediaHandler().stopScreensharingStream(this.localScreensharingStream!);
- this.deleteFeedByStream(this.localScreensharingStream!);
-
- return false;
- }
- }
-
- /**
- * Replaces/adds the tracks from the passed stream to the localUsermediaStream
- * @param stream - to use a replacement for the local usermedia stream
- */
- public async updateLocalUsermediaStream(
- stream: MediaStream,
- forceAudio = false,
- forceVideo = false,
- ): Promise<void> {
- const callFeed = this.localUsermediaFeed!;
- const audioEnabled = forceAudio || (!callFeed.isAudioMuted() && !this.remoteOnHold);
- const videoEnabled = forceVideo || (!callFeed.isVideoMuted() && !this.remoteOnHold);
- logger.log(
- `Call ${this.callId} updateLocalUsermediaStream() running (streamId=${stream.id}, audio=${audioEnabled}, video=${videoEnabled})`,
- );
- setTracksEnabled(stream.getAudioTracks(), audioEnabled);
- setTracksEnabled(stream.getVideoTracks(), videoEnabled);
-
- // We want to keep the same stream id, so we replace the tracks rather
- // than the whole stream.
-
- // Firstly, we replace the tracks in our localUsermediaStream.
- for (const track of this.localUsermediaStream!.getTracks()) {
- this.localUsermediaStream!.removeTrack(track);
- track.stop();
- }
- for (const track of stream.getTracks()) {
- this.localUsermediaStream!.addTrack(track);
- }
-
- // Then replace the old tracks, if possible.
- for (const track of stream.getTracks()) {
- const tKey = getTransceiverKey(SDPStreamMetadataPurpose.Usermedia, track.kind);
-
- const transceiver = this.transceivers.get(tKey);
- const oldSender = transceiver?.sender;
- let added = false;
- if (oldSender) {
- try {
- logger.info(
- `Call ${this.callId} updateLocalUsermediaStream() replacing track (id=${track.id}, kind=${track.kind}, streamId=${stream.id}, streamPurpose=${callFeed.purpose})`,
- );
- await oldSender.replaceTrack(track);
- // Set the direction to indicate we're going to be sending.
- // This is only necessary in the cases where we're upgrading
- // the call to video after downgrading it.
- transceiver.direction = transceiver.direction === "inactive" ? "sendonly" : "sendrecv";
- added = true;
- } catch (error) {
- logger.warn(
- `Call ${this.callId} updateLocalUsermediaStream() replaceTrack failed: adding new transceiver instead`,
- error,
- );
- }
- }
-
- if (!added) {
- logger.info(
- `Call ${this.callId} updateLocalUsermediaStream() adding track to peer connection (id=${track.id}, kind=${track.kind}, streamId=${stream.id}, streamPurpose=${callFeed.purpose})`,
- );
-
- const newSender = this.peerConn!.addTrack(track, this.localUsermediaStream!);
- const newTransceiver = this.peerConn!.getTransceivers().find((t) => t.sender === newSender);
- if (newTransceiver) {
- this.transceivers.set(tKey, newTransceiver);
- } else {
- logger.warn(
- `Call ${this.callId} updateLocalUsermediaStream() couldn't find matching transceiver for newly added track!`,
- );
- }
- }
- }
- }
-
- /**
- * Set whether our outbound video should be muted or not.
- * @param muted - True to mute the outbound video.
- * @returns the new mute state
- */
- public async setLocalVideoMuted(muted: boolean): Promise<boolean> {
- logger.log(`Call ${this.callId} setLocalVideoMuted() running ${muted}`);
-
- // if we were still thinking about stopping and removing the video
- // track: don't, because we want it back.
- if (!muted && this.stopVideoTrackTimer !== undefined) {
- clearTimeout(this.stopVideoTrackTimer);
- this.stopVideoTrackTimer = undefined;
- }
-
- if (!(await this.client.getMediaHandler().hasVideoDevice())) {
- return this.isLocalVideoMuted();
- }
-
- if (!this.hasUserMediaVideoSender && !muted) {
- this.localUsermediaFeed?.setAudioVideoMuted(null, muted);
- await this.upgradeCall(false, true);
- return this.isLocalVideoMuted();
- }
-
- // we may not have a video track - if not, re-request usermedia
- if (!muted && this.localUsermediaStream!.getVideoTracks().length === 0) {
- const stream = await this.client.getMediaHandler().getUserMediaStream(true, true);
- await this.updateLocalUsermediaStream(stream);
- }
-
- this.localUsermediaFeed?.setAudioVideoMuted(null, muted);
-
- this.updateMuteStatus();
- await this.sendMetadataUpdate();
-
- // if we're muting video, set a timeout to stop & remove the video track so we release
- // the camera. We wait a short time to do this because when we disable a track, WebRTC
- // will send black video for it. If we just stop and remove it straight away, the video
- // will just freeze which means that when we unmute video, the other side will briefly
- // get a static frame of us from before we muted. This way, the still frame is just black.
- // A very small delay is not always enough so the theory here is that it needs to be long
- // enough for WebRTC to encode a frame: 120ms should be long enough even if we're only
- // doing 10fps.
- if (muted) {
- this.stopVideoTrackTimer = setTimeout(() => {
- for (const t of this.localUsermediaStream!.getVideoTracks()) {
- t.stop();
- this.localUsermediaStream!.removeTrack(t);
- }
- }, 120);
- }
-
- return this.isLocalVideoMuted();
- }
-
- /**
- * Check if local video is muted.
- *
- * If there are multiple video tracks, <i>all</i> of the tracks need to be muted
- * for this to return true. This means if there are no video tracks, this will
- * return true.
- * @returns True if the local preview video is muted, else false
- * (including if the call is not set up yet).
- */
- public isLocalVideoMuted(): boolean {
- return this.localUsermediaFeed?.isVideoMuted() ?? false;
- }
-
- /**
- * Set whether the microphone should be muted or not.
- * @param muted - True to mute the mic.
- * @returns the new mute state
- */
- public async setMicrophoneMuted(muted: boolean): Promise<boolean> {
- logger.log(`Call ${this.callId} setMicrophoneMuted() running ${muted}`);
- if (!(await this.client.getMediaHandler().hasAudioDevice())) {
- return this.isMicrophoneMuted();
- }
-
- if (!muted && (!this.hasUserMediaAudioSender || !this.hasLocalUserMediaAudioTrack)) {
- await this.upgradeCall(true, false);
- return this.isMicrophoneMuted();
- }
- this.localUsermediaFeed?.setAudioVideoMuted(muted, null);
- this.updateMuteStatus();
- await this.sendMetadataUpdate();
- return this.isMicrophoneMuted();
- }
-
- /**
- * Check if the microphone is muted.
- *
- * If there are multiple audio tracks, <i>all</i> of the tracks need to be muted
- * for this to return true. This means if there are no audio tracks, this will
- * return true.
- * @returns True if the mic is muted, else false (including if the call
- * is not set up yet).
- */
- public isMicrophoneMuted(): boolean {
- return this.localUsermediaFeed?.isAudioMuted() ?? false;
- }
-
- /**
- * @returns true if we have put the party on the other side of the call on hold
- * (that is, we are signalling to them that we are not listening)
- */
- public isRemoteOnHold(): boolean {
- return this.remoteOnHold;
- }
-
- public setRemoteOnHold(onHold: boolean): void {
- if (this.isRemoteOnHold() === onHold) return;
- this.remoteOnHold = onHold;
-
- for (const transceiver of this.peerConn!.getTransceivers()) {
- // We don't send hold music or anything so we're not actually
- // sending anything, but sendrecv is fairly standard for hold and
- // it makes it a lot easier to figure out who's put who on hold.
- transceiver.direction = onHold ? "sendonly" : "sendrecv";
- }
- this.updateMuteStatus();
- this.sendMetadataUpdate();
-
- this.emit(CallEvent.RemoteHoldUnhold, this.remoteOnHold, this);
- }
-
- /**
- * Indicates whether we are 'on hold' to the remote party (ie. if true,
- * they cannot hear us).
- * @returns true if the other party has put us on hold
- */
- public isLocalOnHold(): boolean {
- if (this.state !== CallState.Connected) return false;
-
- let callOnHold = true;
-
- // We consider a call to be on hold only if *all* the tracks are on hold
- // (is this the right thing to do?)
- for (const transceiver of this.peerConn!.getTransceivers()) {
- const trackOnHold = ["inactive", "recvonly"].includes(transceiver.currentDirection!);
-
- if (!trackOnHold) callOnHold = false;
- }
-
- return callOnHold;
- }
-
- /**
- * Sends a DTMF digit to the other party
- * @param digit - The digit (nb. string - '#' and '*' are dtmf too)
- */
- public sendDtmfDigit(digit: string): void {
- for (const sender of this.peerConn!.getSenders()) {
- if (sender.track?.kind === "audio" && sender.dtmf) {
- sender.dtmf.insertDTMF(digit);
- return;
- }
- }
-
- throw new Error("Unable to find a track to send DTMF on");
- }
-
- private updateMuteStatus(): void {
- const micShouldBeMuted = this.isMicrophoneMuted() || this.remoteOnHold;
- const vidShouldBeMuted = this.isLocalVideoMuted() || this.remoteOnHold;
-
- logger.log(
- `Call ${this.callId} updateMuteStatus stream ${
- this.localUsermediaStream!.id
- } micShouldBeMuted ${micShouldBeMuted} vidShouldBeMuted ${vidShouldBeMuted}`,
- );
-
- setTracksEnabled(this.localUsermediaStream!.getAudioTracks(), !micShouldBeMuted);
- setTracksEnabled(this.localUsermediaStream!.getVideoTracks(), !vidShouldBeMuted);
- }
-
- public async sendMetadataUpdate(): Promise<void> {
- await this.sendVoipEvent(EventType.CallSDPStreamMetadataChangedPrefix, {
- [SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(),
- });
- }
-
- private gotCallFeedsForInvite(callFeeds: CallFeed[], requestScreenshareFeed = false): void {
- if (this.successor) {
- this.successor.queueGotCallFeedsForAnswer(callFeeds);
- return;
- }
- if (this.callHasEnded()) {
- this.stopAllMedia();
- return;
- }
-
- for (const feed of callFeeds) {
- this.pushLocalFeed(feed);
- }
-
- if (requestScreenshareFeed) {
- this.peerConn!.addTransceiver("video", {
- direction: "recvonly",
- });
- }
-
- this.state = CallState.CreateOffer;
-
- logger.debug(`Call ${this.callId} gotUserMediaForInvite() run`);
- // Now we wait for the negotiationneeded event
- }
-
- private async sendAnswer(): Promise<void> {
- const answerContent = {
- answer: {
- sdp: this.peerConn!.localDescription!.sdp,
- // type is now deprecated as of Matrix VoIP v1, but
- // required to still be sent for backwards compat
- type: this.peerConn!.localDescription!.type,
- },
- [SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(true),
- } as MCallAnswer;
-
- answerContent.capabilities = {
- "m.call.transferee": this.client.supportsCallTransfer,
- "m.call.dtmf": false,
- };
-
- // We have just taken the local description from the peerConn which will
- // contain all the local candidates added so far, so we can discard any candidates
- // we had queued up because they'll be in the answer.
- const discardCount = this.discardDuplicateCandidates();
- logger.info(
- `Call ${this.callId} sendAnswer() discarding ${discardCount} candidates that will be sent in answer`,
- );
-
- try {
- await this.sendVoipEvent(EventType.CallAnswer, answerContent);
- // If this isn't the first time we've tried to send the answer,
- // we may have candidates queued up, so send them now.
- this.inviteOrAnswerSent = true;
- } catch (error) {
- // We've failed to answer: back to the ringing state
- this.state = CallState.Ringing;
- if (error instanceof MatrixError && error.event) this.client.cancelPendingEvent(error.event);
-
- let code = CallErrorCode.SendAnswer;
- let message = "Failed to send answer";
- if ((<Error>error).name == "UnknownDeviceError") {
- code = CallErrorCode.UnknownDevices;
- message = "Unknown devices present in the room";
- }
- this.emit(CallEvent.Error, new CallError(code, message, <Error>error), this);
- throw error;
- }
-
- // error handler re-throws so this won't happen on error, but
- // we don't want the same error handling on the candidate queue
- this.sendCandidateQueue();
- }
-
- private queueGotCallFeedsForAnswer(callFeeds: CallFeed[]): void {
- // Ensure only one negotiate/answer event is being processed at a time.
- if (this.responsePromiseChain) {
- this.responsePromiseChain = this.responsePromiseChain.then(() => this.gotCallFeedsForAnswer(callFeeds));
- } else {
- this.responsePromiseChain = this.gotCallFeedsForAnswer(callFeeds);
- }
- }
-
- // Enables DTX (discontinuous transmission) on the given session to reduce
- // bandwidth when transmitting silence
- private mungeSdp(description: RTCSessionDescriptionInit, mods: CodecParamsMod[]): void {
- // The only way to enable DTX at this time is through SDP munging
- const sdp = parseSdp(description.sdp!);
-
- sdp.media.forEach((media) => {
- const payloadTypeToCodecMap = new Map<number, string>();
- const codecToPayloadTypeMap = new Map<string, number>();
- for (const rtp of media.rtp) {
- payloadTypeToCodecMap.set(rtp.payload, rtp.codec);
- codecToPayloadTypeMap.set(rtp.codec, rtp.payload);
- }
-
- for (const mod of mods) {
- if (mod.mediaType !== media.type) continue;
-
- if (!codecToPayloadTypeMap.has(mod.codec)) {
- logger.info(
- `Call ${this.callId} mungeSdp() ignoring SDP modifications for ${mod.codec} as it's not present.`,
- );
- continue;
- }
-
- const extraConfig: string[] = [];
- if (mod.enableDtx !== undefined) {
- extraConfig.push(`usedtx=${mod.enableDtx ? "1" : "0"}`);
- }
- if (mod.maxAverageBitrate !== undefined) {
- extraConfig.push(`maxaveragebitrate=${mod.maxAverageBitrate}`);
- }
-
- let found = false;
- for (const fmtp of media.fmtp) {
- if (payloadTypeToCodecMap.get(fmtp.payload) === mod.codec) {
- found = true;
- fmtp.config += ";" + extraConfig.join(";");
- }
- }
- if (!found) {
- media.fmtp.push({
- payload: codecToPayloadTypeMap.get(mod.codec)!,
- config: extraConfig.join(";"),
- });
- }
- }
- });
- description.sdp = writeSdp(sdp);
- }
-
- private async createOffer(): Promise<RTCSessionDescriptionInit> {
- const offer = await this.peerConn!.createOffer();
- this.mungeSdp(offer, getCodecParamMods(this.isPtt));
- return offer;
- }
-
- private async createAnswer(): Promise<RTCSessionDescriptionInit> {
- const answer = await this.peerConn!.createAnswer();
- this.mungeSdp(answer, getCodecParamMods(this.isPtt));
- return answer;
- }
-
- private async gotCallFeedsForAnswer(callFeeds: CallFeed[]): Promise<void> {
- if (this.callHasEnded()) return;
-
- this.waitForLocalAVStream = false;
-
- for (const feed of callFeeds) {
- this.pushLocalFeed(feed);
- }
-
- this.state = CallState.CreateAnswer;
-
- let answer: RTCSessionDescriptionInit;
- try {
- this.getRidOfRTXCodecs();
- answer = await this.createAnswer();
- } catch (err) {
- logger.debug(`Call ${this.callId} gotCallFeedsForAnswer() failed to create answer: `, err);
- this.terminate(CallParty.Local, CallErrorCode.CreateAnswer, true);
- return;
- }
-
- try {
- await this.peerConn!.setLocalDescription(answer);
-
- // make sure we're still going
- if (this.callHasEnded()) return;
-
- this.state = CallState.Connecting;
-
- // Allow a short time for initial candidates to be gathered
- await new Promise((resolve) => {
- setTimeout(resolve, 200);
- });
-
- // make sure the call hasn't ended before we continue
- if (this.callHasEnded()) return;
-
- this.sendAnswer();
- } catch (err) {
- logger.debug(`Call ${this.callId} gotCallFeedsForAnswer() error setting local description!`, err);
- this.terminate(CallParty.Local, CallErrorCode.SetLocalDescription, true);
- return;
- }
- }
-
- /**
- * Internal
- */
- private gotLocalIceCandidate = (event: RTCPeerConnectionIceEvent): void => {
- if (event.candidate) {
- if (this.candidatesEnded) {
- logger.warn(
- `Call ${this.callId} gotLocalIceCandidate() got candidate after candidates have ended - ignoring!`,
- );
- return;
- }
-
- logger.debug(`Call ${this.callId} got local ICE ${event.candidate.sdpMid} ${event.candidate.candidate}`);
-
- if (this.callHasEnded()) return;
-
- // As with the offer, note we need to make a copy of this object, not
- // pass the original: that broke in Chrome ~m43.
- if (event.candidate.candidate === "") {
- this.queueCandidate(null);
- } else {
- this.queueCandidate(event.candidate);
- }
- }
- };
-
- private onIceGatheringStateChange = (event: Event): void => {
- logger.debug(
- `Call ${this.callId} onIceGatheringStateChange() ice gathering state changed to ${
- this.peerConn!.iceGatheringState
- }`,
- );
- if (this.peerConn?.iceGatheringState === "complete") {
- this.queueCandidate(null);
- }
- };
-
- public async onRemoteIceCandidatesReceived(ev: MatrixEvent): Promise<void> {
- if (this.callHasEnded()) {
- //debuglog("Ignoring remote ICE candidate because call has ended");
- return;
- }
-
- const content = ev.getContent<MCallCandidates>();
- const candidates = content.candidates;
- if (!candidates) {
- logger.info(
- `Call ${this.callId} onRemoteIceCandidatesReceived() ignoring candidates event with no candidates!`,
- );
- return;
- }
-
- const fromPartyId = content.version === 0 ? null : content.party_id || null;
-
- if (this.opponentPartyId === undefined) {
- // we haven't picked an opponent yet so save the candidates
- if (fromPartyId) {
- logger.info(
- `Call ${this.callId} onRemoteIceCandidatesReceived() buffering ${candidates.length} candidates until we pick an opponent`,
- );
- const bufferedCandidates = this.remoteCandidateBuffer.get(fromPartyId) || [];
- bufferedCandidates.push(...candidates);
- this.remoteCandidateBuffer.set(fromPartyId, bufferedCandidates);
- }
- return;
- }
-
- if (!this.partyIdMatches(content)) {
- logger.info(
- `Call ${this.callId} onRemoteIceCandidatesReceived() ignoring candidates from party ID ${content.party_id}: we have chosen party ID ${this.opponentPartyId}`,
- );
-
- return;
- }
-
- await this.addIceCandidates(candidates);
- }
-
- /**
- * Used by MatrixClient.
- */
- public async onAnswerReceived(event: MatrixEvent): Promise<void> {
- const content = event.getContent<MCallAnswer>();
- logger.debug(`Call ${this.callId} onAnswerReceived() running (hangupParty=${content.party_id})`);
-
- if (this.callHasEnded()) {
- logger.debug(`Call ${this.callId} onAnswerReceived() ignoring answer because call has ended`);
- return;
- }
-
- if (this.opponentPartyId !== undefined) {
- logger.info(
- `Call ${this.callId} onAnswerReceived() ignoring answer from party ID ${content.party_id}: we already have an answer/reject from ${this.opponentPartyId}`,
- );
- return;
- }
-
- this.chooseOpponent(event);
- await this.addBufferedIceCandidates();
-
- this.state = CallState.Connecting;
-
- const sdpStreamMetadata = content[SDPStreamMetadataKey];
- if (sdpStreamMetadata) {
- this.updateRemoteSDPStreamMetadata(sdpStreamMetadata);
- } else {
- logger.warn(
- `Call ${this.callId} onAnswerReceived() did not get any SDPStreamMetadata! Can not send/receive multiple streams`,
- );
- }
-
- try {
- await this.peerConn!.setRemoteDescription(content.answer);
- } catch (e) {
- logger.debug(`Call ${this.callId} onAnswerReceived() failed to set remote description`, e);
- this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false);
- return;
- }
-
- // If the answer we selected has a party_id, send a select_answer event
- // We do this after setting the remote description since otherwise we'd block
- // call setup on it
- if (this.opponentPartyId !== null) {
- try {
- await this.sendVoipEvent(EventType.CallSelectAnswer, {
- selected_party_id: this.opponentPartyId,
- });
- } catch (err) {
- // This isn't fatal, and will just mean that if another party has raced to answer
- // the call, they won't know they got rejected, so we carry on & don't retry.
- logger.warn(`Call ${this.callId} onAnswerReceived() failed to send select_answer event`, err);
- }
- }
- }
-
- public async onSelectAnswerReceived(event: MatrixEvent): Promise<void> {
- if (this.direction !== CallDirection.Inbound) {
- logger.warn(
- `Call ${this.callId} onSelectAnswerReceived() got select_answer for an outbound call: ignoring`,
- );
- return;
- }
-
- const selectedPartyId = event.getContent<MCallSelectAnswer>().selected_party_id;
-
- if (selectedPartyId === undefined || selectedPartyId === null) {
- logger.warn(
- `Call ${this.callId} onSelectAnswerReceived() got nonsensical select_answer with null/undefined selected_party_id: ignoring`,
- );
- return;
- }
-
- if (selectedPartyId !== this.ourPartyId) {
- logger.info(
- `Call ${this.callId} onSelectAnswerReceived() got select_answer for party ID ${selectedPartyId}: we are party ID ${this.ourPartyId}.`,
- );
- // The other party has picked somebody else's answer
- await this.terminate(CallParty.Remote, CallErrorCode.AnsweredElsewhere, true);
- }
- }
-
- public async onNegotiateReceived(event: MatrixEvent): Promise<void> {
- const content = event.getContent<MCallInviteNegotiate>();
- const description = content.description;
- if (!description || !description.sdp || !description.type) {
- logger.info(`Call ${this.callId} onNegotiateReceived() ignoring invalid m.call.negotiate event`);
- return;
- }
- // Politeness always follows the direction of the call: in a glare situation,
- // we pick either the inbound or outbound call, so one side will always be
- // inbound and one outbound
- const polite = this.direction === CallDirection.Inbound;
-
- // Here we follow the perfect negotiation logic from
- // https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Perfect_negotiation
- const offerCollision =
- description.type === "offer" && (this.makingOffer || this.peerConn!.signalingState !== "stable");
-
- this.ignoreOffer = !polite && offerCollision;
- if (this.ignoreOffer) {
- logger.info(
- `Call ${this.callId} onNegotiateReceived() ignoring colliding negotiate event because we're impolite`,
- );
- return;
- }
-
- const prevLocalOnHold = this.isLocalOnHold();
-
- const sdpStreamMetadata = content[SDPStreamMetadataKey];
- if (sdpStreamMetadata) {
- this.updateRemoteSDPStreamMetadata(sdpStreamMetadata);
- } else {
- logger.warn(
- `Call ${this.callId} onNegotiateReceived() received negotiation event without SDPStreamMetadata!`,
- );
- }
-
- try {
- await this.peerConn!.setRemoteDescription(description);
-
- if (description.type === "offer") {
- let answer: RTCSessionDescriptionInit;
- try {
- this.getRidOfRTXCodecs();
- answer = await this.createAnswer();
- } catch (err) {
- logger.debug(`Call ${this.callId} onNegotiateReceived() failed to create answer: `, err);
- this.terminate(CallParty.Local, CallErrorCode.CreateAnswer, true);
- return;
- }
-
- await this.peerConn!.setLocalDescription(answer);
-
- this.sendVoipEvent(EventType.CallNegotiate, {
- description: this.peerConn!.localDescription?.toJSON(),
- [SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(true),
- });
- }
- } catch (err) {
- logger.warn(`Call ${this.callId} onNegotiateReceived() failed to complete negotiation`, err);
- }
-
- const newLocalOnHold = this.isLocalOnHold();
- if (prevLocalOnHold !== newLocalOnHold) {
- this.emit(CallEvent.LocalHoldUnhold, newLocalOnHold, this);
- // also this one for backwards compat
- this.emit(CallEvent.HoldUnhold, newLocalOnHold);
- }
- }
-
- private updateRemoteSDPStreamMetadata(metadata: SDPStreamMetadata): void {
- this.remoteSDPStreamMetadata = utils.recursivelyAssign(this.remoteSDPStreamMetadata || {}, metadata, true);
- for (const feed of this.getRemoteFeeds()) {
- const streamId = feed.stream.id;
- const metadata = this.remoteSDPStreamMetadata![streamId];
-
- feed.setAudioVideoMuted(metadata?.audio_muted, metadata?.video_muted);
- feed.purpose = this.remoteSDPStreamMetadata![streamId]?.purpose;
- }
- }
-
- public onSDPStreamMetadataChangedReceived(event: MatrixEvent): void {
- const content = event.getContent<MCallSDPStreamMetadataChanged>();
- const metadata = content[SDPStreamMetadataKey];
- this.updateRemoteSDPStreamMetadata(metadata);
- }
-
- public async onAssertedIdentityReceived(event: MatrixEvent): Promise<void> {
- const content = event.getContent<MCAllAssertedIdentity>();
- if (!content.asserted_identity) return;
-
- this.remoteAssertedIdentity = {
- id: content.asserted_identity.id,
- displayName: content.asserted_identity.display_name,
- };
- this.emit(CallEvent.AssertedIdentityChanged, this);
- }
-
- public callHasEnded(): boolean {
- // This exists as workaround to typescript trying to be clever and erroring
- // when putting if (this.state === CallState.Ended) return; twice in the same
- // function, even though that function is async.
- return this.state === CallState.Ended;
- }
-
- private queueGotLocalOffer(): void {
- // Ensure only one negotiate/answer event is being processed at a time.
- if (this.responsePromiseChain) {
- this.responsePromiseChain = this.responsePromiseChain.then(() => this.wrappedGotLocalOffer());
- } else {
- this.responsePromiseChain = this.wrappedGotLocalOffer();
- }
- }
-
- private async wrappedGotLocalOffer(): Promise<void> {
- this.makingOffer = true;
- try {
- // XXX: in what situations do we believe gotLocalOffer actually throws? It appears
- // to handle most of its exceptions itself and terminate the call. I'm not entirely
- // sure it would ever throw, so I can't add a test for these lines.
- // Also the tense is different between "gotLocalOffer" and "getLocalOfferFailed" so
- // it's not entirely clear whether getLocalOfferFailed is just misnamed or whether
- // they've been cross-polinated somehow at some point.
- await this.gotLocalOffer();
- } catch (e) {
- this.getLocalOfferFailed(e as Error);
- return;
- } finally {
- this.makingOffer = false;
- }
- }
-
- private async gotLocalOffer(): Promise<void> {
- logger.debug(`Call ${this.callId} gotLocalOffer() running`);
-
- if (this.callHasEnded()) {
- logger.debug(
- `Call ${this.callId} gotLocalOffer() ignoring newly created offer because the call has ended"`,
- );
- return;
- }
-
- let offer: RTCSessionDescriptionInit;
- try {
- this.getRidOfRTXCodecs();
- offer = await this.createOffer();
- } catch (err) {
- logger.debug(`Call ${this.callId} gotLocalOffer() failed to create offer: `, err);
- this.terminate(CallParty.Local, CallErrorCode.CreateOffer, true);
- return;
- }
-
- try {
- await this.peerConn!.setLocalDescription(offer);
- } catch (err) {
- logger.debug(`Call ${this.callId} gotLocalOffer() error setting local description!`, err);
- this.terminate(CallParty.Local, CallErrorCode.SetLocalDescription, true);
- return;
- }
-
- if (this.peerConn!.iceGatheringState === "gathering") {
- // Allow a short time for initial candidates to be gathered
- await new Promise((resolve) => {
- setTimeout(resolve, 200);
- });
- }
-
- if (this.callHasEnded()) return;
-
- const eventType = this.state === CallState.CreateOffer ? EventType.CallInvite : EventType.CallNegotiate;
-
- const content = {
- lifetime: CALL_TIMEOUT_MS,
- } as MCallInviteNegotiate;
-
- if (eventType === EventType.CallInvite && this.invitee) {
- content.invitee = this.invitee;
- }
-
- // clunky because TypeScript can't follow the types through if we use an expression as the key
- if (this.state === CallState.CreateOffer) {
- content.offer = this.peerConn!.localDescription?.toJSON();
- } else {
- content.description = this.peerConn!.localDescription?.toJSON();
- }
-
- content.capabilities = {
- "m.call.transferee": this.client.supportsCallTransfer,
- "m.call.dtmf": false,
- };
-
- content[SDPStreamMetadataKey] = this.getLocalSDPStreamMetadata(true);
-
- // Get rid of any candidates waiting to be sent: they'll be included in the local
- // description we just got and will send in the offer.
- const discardCount = this.discardDuplicateCandidates();
- logger.info(
- `Call ${this.callId} gotLocalOffer() discarding ${discardCount} candidates that will be sent in offer`,
- );
-
- try {
- await this.sendVoipEvent(eventType, content);
- } catch (error) {
- logger.error(`Call ${this.callId} gotLocalOffer() failed to send invite`, error);
- if (error instanceof MatrixError && error.event) this.client.cancelPendingEvent(error.event);
-
- let code = CallErrorCode.SignallingFailed;
- let message = "Signalling failed";
- if (this.state === CallState.CreateOffer) {
- code = CallErrorCode.SendInvite;
- message = "Failed to send invite";
- }
- if ((<Error>error).name == "UnknownDeviceError") {
- code = CallErrorCode.UnknownDevices;
- message = "Unknown devices present in the room";
- }
-
- this.emit(CallEvent.Error, new CallError(code, message, <Error>error), this);
- this.terminate(CallParty.Local, code, false);
-
- // no need to carry on & send the candidate queue, but we also
- // don't want to rethrow the error
- return;
- }
-
- this.sendCandidateQueue();
- if (this.state === CallState.CreateOffer) {
- this.inviteOrAnswerSent = true;
- this.state = CallState.InviteSent;
- this.inviteTimeout = setTimeout(() => {
- this.inviteTimeout = undefined;
- if (this.state === CallState.InviteSent) {
- this.hangup(CallErrorCode.InviteTimeout, false);
- }
- }, CALL_TIMEOUT_MS);
- }
- }
-
- private getLocalOfferFailed = (err: Error): void => {
- logger.error(`Call ${this.callId} getLocalOfferFailed() running`, err);
-
- this.emit(
- CallEvent.Error,
- new CallError(CallErrorCode.LocalOfferFailed, "Failed to get local offer!", err),
- this,
- );
- this.terminate(CallParty.Local, CallErrorCode.LocalOfferFailed, false);
- };
-
- private getUserMediaFailed = (err: Error): void => {
- if (this.successor) {
- this.successor.getUserMediaFailed(err);
- return;
- }
-
- logger.warn(`Call ${this.callId} getUserMediaFailed() failed to get user media - ending call`, err);
-
- this.emit(
- CallEvent.Error,
- new CallError(
- CallErrorCode.NoUserMedia,
- "Couldn't start capturing media! Is your microphone set up and " + "does this app have permission?",
- err,
- ),
- this,
- );
- this.terminate(CallParty.Local, CallErrorCode.NoUserMedia, false);
- };
-
- private onIceConnectionStateChanged = (): void => {
- if (this.callHasEnded()) {
- return; // because ICE can still complete as we're ending the call
- }
- logger.debug(
- `Call ${this.callId} onIceConnectionStateChanged() running (state=${this.peerConn?.iceConnectionState})`,
- );
-
- // ideally we'd consider the call to be connected when we get media but
- // chrome doesn't implement any of the 'onstarted' events yet
- if (["connected", "completed"].includes(this.peerConn?.iceConnectionState ?? "")) {
- clearTimeout(this.iceDisconnectedTimeout);
- this.iceDisconnectedTimeout = undefined;
- this.state = CallState.Connected;
-
- if (!this.callLengthInterval && !this.callStartTime) {
- this.callStartTime = Date.now();
-
- this.callLengthInterval = setInterval(() => {
- this.emit(CallEvent.LengthChanged, Math.round((Date.now() - this.callStartTime!) / 1000), this);
- }, CALL_LENGTH_INTERVAL);
- }
- } else if (this.peerConn?.iceConnectionState == "failed") {
- // Firefox for Android does not yet have support for restartIce()
- // (the types say it's always defined though, so we have to cast
- // to prevent typescript from warning).
- if (this.peerConn?.restartIce as (() => void) | null) {
- this.candidatesEnded = false;
- this.peerConn!.restartIce();
- } else {
- logger.info(
- `Call ${this.callId} onIceConnectionStateChanged() hanging up call (ICE failed and no ICE restart method)`,
- );
- this.hangup(CallErrorCode.IceFailed, false);
- }
- } else if (this.peerConn?.iceConnectionState == "disconnected") {
- this.iceDisconnectedTimeout = setTimeout(() => {
- logger.info(
- `Call ${this.callId} onIceConnectionStateChanged() hanging up call (ICE disconnected for too long)`,
- );
- this.hangup(CallErrorCode.IceFailed, false);
- }, ICE_DISCONNECTED_TIMEOUT);
- this.state = CallState.Connecting;
- }
-
- // In PTT mode, override feed status to muted when we lose connection to
- // the peer, since we don't want to block the line if they're not saying anything.
- // Experimenting in Chrome, this happens after 5 or 6 seconds, which is probably
- // fast enough.
- if (this.isPtt && ["failed", "disconnected"].includes(this.peerConn!.iceConnectionState)) {
- for (const feed of this.getRemoteFeeds()) {
- feed.setAudioVideoMuted(true, true);
- }
- }
- };
-
- private onSignallingStateChanged = (): void => {
- logger.debug(`Call ${this.callId} onSignallingStateChanged() running (state=${this.peerConn?.signalingState})`);
- };
-
- private onTrack = (ev: RTCTrackEvent): void => {
- if (ev.streams.length === 0) {
- logger.warn(
- `Call ${this.callId} onTrack() called with streamless track streamless (kind=${ev.track.kind})`,
- );
- return;
- }
-
- const stream = ev.streams[0];
- this.pushRemoteFeed(stream);
-
- if (!this.removeTrackListeners.has(stream)) {
- const onRemoveTrack = (): void => {
- if (stream.getTracks().length === 0) {
- logger.info(`Call ${this.callId} onTrack() removing track (streamId=${stream.id})`);
- this.deleteFeedByStream(stream);
- stream.removeEventListener("removetrack", onRemoveTrack);
- this.removeTrackListeners.delete(stream);
- }
- };
- stream.addEventListener("removetrack", onRemoveTrack);
- this.removeTrackListeners.set(stream, onRemoveTrack);
- }
- };
-
- private onDataChannel = (ev: RTCDataChannelEvent): void => {
- this.emit(CallEvent.DataChannel, ev.channel, this);
- };
-
- /**
- * This method removes all video/rtx codecs from screensharing video
- * transceivers. This is necessary since they can cause problems. Without
- * this the following steps should produce an error:
- * Chromium calls Firefox
- * Firefox answers
- * Firefox starts screen-sharing
- * Chromium starts screen-sharing
- * Call crashes for Chromium with:
- * [96685:23:0518/162603.933321:ERROR:webrtc_video_engine.cc(3296)] RTX codec (PT=97) mapped to PT=96 which is not in the codec list.
- * [96685:23:0518/162603.933377:ERROR:webrtc_video_engine.cc(1171)] GetChangedRecvParameters called without any video codecs.
- * [96685:23:0518/162603.933430:ERROR:sdp_offer_answer.cc(4302)] Failed to set local video description recv parameters for m-section with mid='2'. (INVALID_PARAMETER)
- */
- private getRidOfRTXCodecs(): void {
- // RTCRtpReceiver.getCapabilities and RTCRtpSender.getCapabilities don't seem to be supported on FF
- if (!RTCRtpReceiver.getCapabilities || !RTCRtpSender.getCapabilities) return;
-
- const recvCodecs = RTCRtpReceiver.getCapabilities("video")!.codecs;
- const sendCodecs = RTCRtpSender.getCapabilities("video")!.codecs;
- const codecs = [...sendCodecs, ...recvCodecs];
-
- for (const codec of codecs) {
- if (codec.mimeType === "video/rtx") {
- const rtxCodecIndex = codecs.indexOf(codec);
- codecs.splice(rtxCodecIndex, 1);
- }
- }
-
- const screenshareVideoTransceiver = this.transceivers.get(
- getTransceiverKey(SDPStreamMetadataPurpose.Screenshare, "video"),
- );
- if (screenshareVideoTransceiver) screenshareVideoTransceiver.setCodecPreferences(codecs);
- }
-
- private onNegotiationNeeded = async (): Promise<void> => {
- logger.info(`Call ${this.callId} onNegotiationNeeded() negotiation is needed!`);
-
- if (this.state !== CallState.CreateOffer && this.opponentVersion === 0) {
- logger.info(
- `Call ${this.callId} onNegotiationNeeded() opponent does not support renegotiation: ignoring negotiationneeded event`,
- );
- return;
- }
-
- this.queueGotLocalOffer();
- };
-
- public onHangupReceived = (msg: MCallHangupReject): void => {
- logger.debug(`Call ${this.callId} onHangupReceived() running`);
-
- // party ID must match (our chosen partner hanging up the call) or be undefined (we haven't chosen
- // a partner yet but we're treating the hangup as a reject as per VoIP v0)
- if (this.partyIdMatches(msg) || this.state === CallState.Ringing) {
- // default reason is user_hangup
- this.terminate(CallParty.Remote, msg.reason || CallErrorCode.UserHangup, true);
- } else {
- logger.info(
- `Call ${this.callId} onHangupReceived() ignoring message from party ID ${msg.party_id}: our partner is ${this.opponentPartyId}`,
- );
- }
- };
-
- public onRejectReceived = (msg: MCallHangupReject): void => {
- logger.debug(`Call ${this.callId} onRejectReceived() running`);
-
- // No need to check party_id for reject because if we'd received either
- // an answer or reject, we wouldn't be in state InviteSent
-
- const shouldTerminate =
- // reject events also end the call if it's ringing: it's another of
- // our devices rejecting the call.
- [CallState.InviteSent, CallState.Ringing].includes(this.state) ||
- // also if we're in the init state and it's an inbound call, since
- // this means we just haven't entered the ringing state yet
- (this.state === CallState.Fledgling && this.direction === CallDirection.Inbound);
-
- if (shouldTerminate) {
- this.terminate(CallParty.Remote, msg.reason || CallErrorCode.UserHangup, true);
- } else {
- logger.debug(`Call ${this.callId} onRejectReceived() called in wrong state (state=${this.state})`);
- }
- };
-
- public onAnsweredElsewhere = (msg: MCallAnswer): void => {
- logger.debug(`Call ${this.callId} onAnsweredElsewhere() running`);
- this.terminate(CallParty.Remote, CallErrorCode.AnsweredElsewhere, true);
- };
-
- /**
- * @internal
- */
- private async sendVoipEvent(eventType: string, content: object): Promise<void> {
- const realContent = Object.assign({}, content, {
- version: VOIP_PROTO_VERSION,
- call_id: this.callId,
- party_id: this.ourPartyId,
- conf_id: this.groupCallId,
- });
-
- if (this.opponentDeviceId) {
- const toDeviceSeq = this.toDeviceSeq++;
- const content = {
- ...realContent,
- device_id: this.client.deviceId,
- sender_session_id: this.client.getSessionId(),
- dest_session_id: this.opponentSessionId,
- seq: toDeviceSeq,
- [ToDeviceMessageId]: uuidv4(),
- };
-
- this.emit(
- CallEvent.SendVoipEvent,
- {
- type: "toDevice",
- eventType,
- userId: this.invitee || this.getOpponentMember()?.userId,
- opponentDeviceId: this.opponentDeviceId,
- content,
- },
- this,
- );
-
- const userId = this.invitee || this.getOpponentMember()!.userId;
- if (this.client.getUseE2eForGroupCall()) {
- if (!this.opponentDeviceInfo) {
- logger.warn(`Call ${this.callId} sendVoipEvent() failed: we do not have opponentDeviceInfo`);
- return;
- }
-
- await this.client.encryptAndSendToDevices(
- [
- {
- userId,
- deviceInfo: this.opponentDeviceInfo,
- },
- ],
- {
- type: eventType,
- content,
- },
- );
- } else {
- await this.client.sendToDevice(
- eventType,
- new Map<string, any>([[userId, new Map([[this.opponentDeviceId, content]])]]),
- );
- }
- } else {
- this.emit(
- CallEvent.SendVoipEvent,
- {
- type: "sendEvent",
- eventType,
- roomId: this.roomId,
- content: realContent,
- userId: this.invitee || this.getOpponentMember()?.userId,
- },
- this,
- );
-
- await this.client.sendEvent(this.roomId!, eventType, realContent);
- }
- }
-
- /**
- * Queue a candidate to be sent
- * @param content - The candidate to queue up, or null if candidates have finished being generated
- * and end-of-candidates should be signalled
- */
- private queueCandidate(content: RTCIceCandidate | null): void {
- // We partially de-trickle candidates by waiting for `delay` before sending them
- // amalgamated, in order to avoid sending too many m.call.candidates events and hitting
- // rate limits in Matrix.
- // In practice, it'd be better to remove rate limits for m.call.*
-
- // N.B. this deliberately lets you queue and send blank candidates, which MSC2746
- // currently proposes as the way to indicate that candidate gathering is complete.
- // This will hopefully be changed to an explicit rather than implicit notification
- // shortly.
- if (content) {
- this.candidateSendQueue.push(content);
- } else {
- this.candidatesEnded = true;
- }
-
- // Don't send the ICE candidates yet if the call is in the ringing state: this
- // means we tried to pick (ie. started generating candidates) and then failed to
- // send the answer and went back to the ringing state. Queue up the candidates
- // to send if we successfully send the answer.
- // Equally don't send if we haven't yet sent the answer because we can send the
- // first batch of candidates along with the answer
- if (this.state === CallState.Ringing || !this.inviteOrAnswerSent) return;
-
- // MSC2746 recommends these values (can be quite long when calling because the
- // callee will need a while to answer the call)
- const delay = this.direction === CallDirection.Inbound ? 500 : 2000;
-
- if (this.candidateSendTries === 0) {
- setTimeout(() => {
- this.sendCandidateQueue();
- }, delay);
- }
- }
-
- // Discard all non-end-of-candidates messages
- // Return the number of candidate messages that were discarded.
- // Call this method before sending an invite or answer message
- private discardDuplicateCandidates(): number {
- let discardCount = 0;
- const newQueue: RTCIceCandidate[] = [];
-
- for (let i = 0; i < this.candidateSendQueue.length; i++) {
- const candidate = this.candidateSendQueue[i];
- if (candidate.candidate === "") {
- newQueue.push(candidate);
- } else {
- discardCount++;
- }
- }
-
- this.candidateSendQueue = newQueue;
-
- return discardCount;
- }
-
- /*
- * Transfers this call to another user
- */
- public async transfer(targetUserId: string): Promise<void> {
- // Fetch the target user's global profile info: their room avatar / displayname
- // could be different in whatever room we share with them.
- const profileInfo = await this.client.getProfileInfo(targetUserId);
-
- const replacementId = genCallID();
-
- const body = {
- replacement_id: genCallID(),
- target_user: {
- id: targetUserId,
- display_name: profileInfo.displayname,
- avatar_url: profileInfo.avatar_url,
- },
- create_call: replacementId,
- } as MCallReplacesEvent;
-
- await this.sendVoipEvent(EventType.CallReplaces, body);
-
- await this.terminate(CallParty.Local, CallErrorCode.Transferred, true);
- }
-
- /*
- * Transfers this call to the target call, effectively 'joining' the
- * two calls (so the remote parties on each call are connected together).
- */
- public async transferToCall(transferTargetCall: MatrixCall): Promise<void> {
- const targetUserId = transferTargetCall.getOpponentMember()?.userId;
- const targetProfileInfo = targetUserId ? await this.client.getProfileInfo(targetUserId) : undefined;
- const opponentUserId = this.getOpponentMember()?.userId;
- const transfereeProfileInfo = opponentUserId ? await this.client.getProfileInfo(opponentUserId) : undefined;
-
- const newCallId = genCallID();
-
- const bodyToTransferTarget = {
- // the replacements on each side have their own ID, and it's distinct from the
- // ID of the new call (but we can use the same function to generate it)
- replacement_id: genCallID(),
- target_user: {
- id: opponentUserId,
- display_name: transfereeProfileInfo?.displayname,
- avatar_url: transfereeProfileInfo?.avatar_url,
- },
- await_call: newCallId,
- } as MCallReplacesEvent;
-
- await transferTargetCall.sendVoipEvent(EventType.CallReplaces, bodyToTransferTarget);
-
- const bodyToTransferee = {
- replacement_id: genCallID(),
- target_user: {
- id: targetUserId,
- display_name: targetProfileInfo?.displayname,
- avatar_url: targetProfileInfo?.avatar_url,
- },
- create_call: newCallId,
- } as MCallReplacesEvent;
-
- await this.sendVoipEvent(EventType.CallReplaces, bodyToTransferee);
-
- await this.terminate(CallParty.Local, CallErrorCode.Transferred, true);
- await transferTargetCall.terminate(CallParty.Local, CallErrorCode.Transferred, true);
- }
-
- private async terminate(hangupParty: CallParty, hangupReason: CallErrorCode, shouldEmit: boolean): Promise<void> {
- if (this.callHasEnded()) return;
-
- this.hangupParty = hangupParty;
- this.hangupReason = hangupReason;
- this.state = CallState.Ended;
-
- if (this.inviteTimeout) {
- clearTimeout(this.inviteTimeout);
- this.inviteTimeout = undefined;
- }
- if (this.iceDisconnectedTimeout !== undefined) {
- clearTimeout(this.iceDisconnectedTimeout);
- this.iceDisconnectedTimeout = undefined;
- }
- if (this.callLengthInterval) {
- clearInterval(this.callLengthInterval);
- this.callLengthInterval = undefined;
- }
- if (this.stopVideoTrackTimer !== undefined) {
- clearTimeout(this.stopVideoTrackTimer);
- this.stopVideoTrackTimer = undefined;
- }
-
- for (const [stream, listener] of this.removeTrackListeners) {
- stream.removeEventListener("removetrack", listener);
- }
- this.removeTrackListeners.clear();
-
- this.callStatsAtEnd = await this.collectCallStats();
-
- // Order is important here: first we stopAllMedia() and only then we can deleteAllFeeds()
- this.stopAllMedia();
- this.deleteAllFeeds();
-
- if (this.peerConn && this.peerConn.signalingState !== "closed") {
- this.peerConn.close();
- }
- this.stats?.removeStatsReportGatherer(this.callId);
-
- if (shouldEmit) {
- this.emit(CallEvent.Hangup, this);
- }
-
- this.client.callEventHandler!.calls.delete(this.callId);
- }
-
- private stopAllMedia(): void {
- logger.debug(`Call ${this.callId} stopAllMedia() running`);
-
- for (const feed of this.feeds) {
- // Slightly awkward as local feed need to go via the correct method on
- // the MediaHandler so they get removed from MediaHandler (remote tracks
- // don't)
- // NB. We clone local streams when passing them to individual calls in a group
- // call, so we can (and should) stop the clones once we no longer need them:
- // the other clones will continue fine.
- if (feed.isLocal() && feed.purpose === SDPStreamMetadataPurpose.Usermedia) {
- this.client.getMediaHandler().stopUserMediaStream(feed.stream);
- } else if (feed.isLocal() && feed.purpose === SDPStreamMetadataPurpose.Screenshare) {
- this.client.getMediaHandler().stopScreensharingStream(feed.stream);
- } else if (!feed.isLocal()) {
- logger.debug(`Call ${this.callId} stopAllMedia() stopping stream (streamId=${feed.stream.id})`);
- for (const track of feed.stream.getTracks()) {
- track.stop();
- }
- }
- }
- }
-
- private checkForErrorListener(): void {
- if (this.listeners(EventEmitterEvents.Error).length === 0) {
- throw new Error("You MUST attach an error listener using call.on('error', function() {})");
- }
- }
-
- private async sendCandidateQueue(): Promise<void> {
- if (this.candidateSendQueue.length === 0 || this.callHasEnded()) {
- return;
- }
-
- const candidates = this.candidateSendQueue;
- this.candidateSendQueue = [];
- ++this.candidateSendTries;
- const content = { candidates: candidates.map((candidate) => candidate.toJSON()) };
- if (this.candidatesEnded) {
- // If there are no more candidates, signal this by adding an empty string candidate
- content.candidates.push({
- candidate: "",
- });
- }
- logger.debug(`Call ${this.callId} sendCandidateQueue() attempting to send ${candidates.length} candidates`);
- try {
- await this.sendVoipEvent(EventType.CallCandidates, content);
- // reset our retry count if we have successfully sent our candidates
- // otherwise queueCandidate() will refuse to try to flush the queue
- this.candidateSendTries = 0;
-
- // Try to send candidates again just in case we received more candidates while sending.
- this.sendCandidateQueue();
- } catch (error) {
- // don't retry this event: we'll send another one later as we might
- // have more candidates by then.
- if (error instanceof MatrixError && error.event) this.client.cancelPendingEvent(error.event);
-
- // put all the candidates we failed to send back in the queue
- this.candidateSendQueue.push(...candidates);
-
- if (this.candidateSendTries > 5) {
- logger.debug(
- `Call ${this.callId} sendCandidateQueue() failed to send candidates on attempt ${this.candidateSendTries}. Giving up on this call.`,
- error,
- );
-
- const code = CallErrorCode.SignallingFailed;
- const message = "Signalling failed";
-
- this.emit(CallEvent.Error, new CallError(code, message, <Error>error), this);
- this.hangup(code, false);
-
- return;
- }
-
- const delayMs = 500 * Math.pow(2, this.candidateSendTries);
- ++this.candidateSendTries;
- logger.debug(
- `Call ${this.callId} sendCandidateQueue() failed to send candidates. Retrying in ${delayMs}ms`,
- error,
- );
- setTimeout(() => {
- this.sendCandidateQueue();
- }, delayMs);
- }
- }
-
- /**
- * Place a call to this room.
- * @throws if you have not specified a listener for 'error' events.
- * @throws if have passed audio=false.
- */
- public async placeCall(audio: boolean, video: boolean): Promise<void> {
- if (!audio) {
- throw new Error("You CANNOT start a call without audio");
- }
- this.state = CallState.WaitLocalMedia;
-
- try {
- const stream = await this.client.getMediaHandler().getUserMediaStream(audio, video);
-
- // make sure all the tracks are enabled (same as pushNewLocalFeed -
- // we probably ought to just have one code path for adding streams)
- setTracksEnabled(stream.getAudioTracks(), true);
- setTracksEnabled(stream.getVideoTracks(), true);
-
- const callFeed = new CallFeed({
- client: this.client,
- roomId: this.roomId,
- userId: this.client.getUserId()!,
- deviceId: this.client.getDeviceId() ?? undefined,
- stream,
- purpose: SDPStreamMetadataPurpose.Usermedia,
- audioMuted: false,
- videoMuted: false,
- });
- await this.placeCallWithCallFeeds([callFeed]);
- } catch (e) {
- this.getUserMediaFailed(<Error>e);
- return;
- }
- }
-
- /**
- * Place a call to this room with call feed.
- * @param callFeeds - to use
- * @throws if you have not specified a listener for 'error' events.
- * @throws if have passed audio=false.
- */
- public async placeCallWithCallFeeds(callFeeds: CallFeed[], requestScreenshareFeed = false): Promise<void> {
- this.checkForErrorListener();
- this.direction = CallDirection.Outbound;
-
- await this.initOpponentCrypto();
-
- // XXX Find a better way to do this
- this.client.callEventHandler!.calls.set(this.callId, this);
-
- // make sure we have valid turn creds. Unless something's gone wrong, it should
- // poll and keep the credentials valid so this should be instant.
- const haveTurnCreds = await this.client.checkTurnServers();
- if (!haveTurnCreds) {
- logger.warn(
- `Call ${this.callId} placeCallWithCallFeeds() failed to get TURN credentials! Proceeding with call anyway...`,
- );
- }
-
- // create the peer connection now so it can be gathering candidates while we get user
- // media (assuming a candidate pool size is configured)
- this.peerConn = this.createPeerConnection();
- this.gotCallFeedsForInvite(callFeeds, requestScreenshareFeed);
- }
-
- private createPeerConnection(): RTCPeerConnection {
- const pc = new window.RTCPeerConnection({
- iceTransportPolicy: this.forceTURN ? "relay" : undefined,
- iceServers: this.turnServers,
- iceCandidatePoolSize: this.client.iceCandidatePoolSize,
- bundlePolicy: "max-bundle",
- });
-
- // 'connectionstatechange' would be better, but firefox doesn't implement that.
- pc.addEventListener("iceconnectionstatechange", this.onIceConnectionStateChanged);
- pc.addEventListener("signalingstatechange", this.onSignallingStateChanged);
- pc.addEventListener("icecandidate", this.gotLocalIceCandidate);
- pc.addEventListener("icegatheringstatechange", this.onIceGatheringStateChange);
- pc.addEventListener("track", this.onTrack);
- pc.addEventListener("negotiationneeded", this.onNegotiationNeeded);
- pc.addEventListener("datachannel", this.onDataChannel);
-
- this.stats?.addStatsReportGatherer(this.callId, "unknown", pc);
- return pc;
- }
-
- private partyIdMatches(msg: MCallBase): boolean {
- // They must either match or both be absent (in which case opponentPartyId will be null)
- // Also we ignore party IDs on the invite/offer if the version is 0, so we must do the same
- // here and use null if the version is 0 (woe betide any opponent sending messages in the
- // same call with different versions)
- const msgPartyId = msg.version === 0 ? null : msg.party_id || null;
- return msgPartyId === this.opponentPartyId;
- }
-
- // Commits to an opponent for the call
- // ev: An invite or answer event
- private chooseOpponent(ev: MatrixEvent): void {
- // I choo-choo-choose you
- const msg = ev.getContent<MCallInviteNegotiate | MCallAnswer>();
-
- logger.debug(`Call ${this.callId} chooseOpponent() running (partyId=${msg.party_id})`);
-
- this.opponentVersion = msg.version;
- if (this.opponentVersion === 0) {
- // set to null to indicate that we've chosen an opponent, but because
- // they're v0 they have no party ID (even if they sent one, we're ignoring it)
- this.opponentPartyId = null;
- } else {
- // set to their party ID, or if they're naughty and didn't send one despite
- // not being v0, set it to null to indicate we picked an opponent with no
- // party ID
- this.opponentPartyId = msg.party_id || null;
- }
- this.opponentCaps = msg.capabilities || ({} as CallCapabilities);
- this.opponentMember = this.client.getRoom(this.roomId)!.getMember(ev.getSender()!) ?? undefined;
- }
-
- private async addBufferedIceCandidates(): Promise<void> {
- const bufferedCandidates = this.remoteCandidateBuffer.get(this.opponentPartyId!);
- if (bufferedCandidates) {
- logger.info(
- `Call ${this.callId} addBufferedIceCandidates() adding ${bufferedCandidates.length} buffered candidates for opponent ${this.opponentPartyId}`,
- );
- await this.addIceCandidates(bufferedCandidates);
- }
- this.remoteCandidateBuffer.clear();
- }
-
- private async addIceCandidates(candidates: RTCIceCandidate[]): Promise<void> {
- for (const candidate of candidates) {
- if (
- (candidate.sdpMid === null || candidate.sdpMid === undefined) &&
- (candidate.sdpMLineIndex === null || candidate.sdpMLineIndex === undefined)
- ) {
- logger.debug(`Call ${this.callId} addIceCandidates() got remote ICE end-of-candidates`);
- } else {
- logger.debug(
- `Call ${this.callId} addIceCandidates() got remote ICE candidate (sdpMid=${candidate.sdpMid}, candidate=${candidate.candidate})`,
- );
- }
-
- try {
- await this.peerConn!.addIceCandidate(candidate);
- } catch (err) {
- if (!this.ignoreOffer) {
- logger.info(`Call ${this.callId} addIceCandidates() failed to add remote ICE candidate`, err);
- }
- }
- }
- }
-
- public get hasPeerConnection(): boolean {
- return Boolean(this.peerConn);
- }
-
- public initStats(stats: GroupCallStats, peerId = "unknown"): void {
- this.stats = stats;
- this.stats.start();
- }
-}
-
-export function setTracksEnabled(tracks: Array<MediaStreamTrack>, enabled: boolean): void {
- for (const track of tracks) {
- track.enabled = enabled;
- }
-}
-
-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()
- *
- * Create a new Matrix call for the browser.
- * @param client - The client instance to use.
- * @param roomId - The room the call is in.
- * @param options - DEPRECATED optional options map.
- * @returns the call or null if the browser doesn't support calling.
- */
-export function createNewMatrixCall(
- client: MatrixClient,
- roomId: string,
- options?: Pick<CallOpts, "forceTURN" | "invitee" | "opponentDeviceId" | "opponentSessionId" | "groupCallId">,
-): MatrixCall | null {
- if (!supportsMatrixCall()) return null;
-
- const optionsForceTURN = options ? options.forceTURN : false;
-
- const opts: CallOpts = {
- client: client,
- roomId: roomId,
- invitee: options?.invitee,
- turnServers: client.getTurnServers(),
- // call level options
- forceTURN: client.forceTURN || optionsForceTURN,
- opponentDeviceId: options?.opponentDeviceId,
- opponentSessionId: options?.opponentSessionId,
- groupCallId: options?.groupCallId,
- };
- const call = new MatrixCall(opts);
-
- client.reEmitter.reEmit(call, Object.values(CallEvent));
-
- return call;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/callEventHandler.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/callEventHandler.ts
deleted file mode 100644
index 4ee183a..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/callEventHandler.ts
+++ /dev/null
@@ -1,425 +0,0 @@
-/*
-Copyright 2020 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 { MatrixEvent } from "../models/event";
-import { logger } from "../logger";
-import { CallDirection, CallError, CallErrorCode, CallState, createNewMatrixCall, MatrixCall } from "./call";
-import { EventType } from "../@types/event";
-import { ClientEvent, MatrixClient } from "../client";
-import { MCallAnswer, MCallHangupReject } from "./callEventTypes";
-import { GroupCall, GroupCallErrorCode, GroupCallEvent, GroupCallUnknownDeviceError } from "./groupCall";
-import { RoomEvent } from "../models/room";
-
-// Don't ring unless we'd be ringing for at least 3 seconds: the user needs some
-// time to press the 'accept' button
-const RING_GRACE_PERIOD = 3000;
-
-export enum CallEventHandlerEvent {
- Incoming = "Call.incoming",
-}
-
-export type CallEventHandlerEventHandlerMap = {
- /**
- * Fires whenever an incoming call arrives.
- * @param call - The incoming call.
- * @example
- * ```
- * matrixClient.on("Call.incoming", function(call){
- * call.answer(); // auto-answer
- * });
- * ```
- */
- [CallEventHandlerEvent.Incoming]: (call: MatrixCall) => void;
-};
-
-export class CallEventHandler {
- // XXX: Most of these are only public because of the tests
- public calls: Map<string, MatrixCall>;
- public callEventBuffer: MatrixEvent[];
- public nextSeqByCall: Map<string, number> = new Map();
- public toDeviceEventBuffers: Map<string, Array<MatrixEvent>> = new Map();
-
- private client: MatrixClient;
- private candidateEventsByCall: Map<string, Array<MatrixEvent>>;
- private eventBufferPromiseChain?: Promise<void>;
-
- public constructor(client: MatrixClient) {
- this.client = client;
- this.calls = new Map<string, MatrixCall>();
- // The sync code always emits one event at a time, so it will patiently
- // wait for us to finish processing a call invite before delivering the
- // next event, even if that next event is a hangup. We therefore accumulate
- // all our call events and then process them on the 'sync' event, ie.
- // each time a sync has completed. This way, we can avoid emitting incoming
- // call events if we get both the invite and answer/hangup in the same sync.
- // This happens quite often, eg. replaying sync from storage, catchup sync
- // after loading and after we've been offline for a bit.
- this.callEventBuffer = [];
- this.candidateEventsByCall = new Map<string, Array<MatrixEvent>>();
- }
-
- public start(): void {
- this.client.on(ClientEvent.Sync, this.onSync);
- this.client.on(RoomEvent.Timeline, this.onRoomTimeline);
- this.client.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
- }
-
- public stop(): void {
- this.client.removeListener(ClientEvent.Sync, this.onSync);
- this.client.removeListener(RoomEvent.Timeline, this.onRoomTimeline);
- this.client.removeListener(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
- }
-
- private onSync = (): void => {
- // Process the current event buffer and start queuing into a new one.
- const currentEventBuffer = this.callEventBuffer;
- this.callEventBuffer = [];
-
- // Ensure correct ordering by only processing this queue after the previous one has finished processing
- if (this.eventBufferPromiseChain) {
- this.eventBufferPromiseChain = this.eventBufferPromiseChain.then(() =>
- this.evaluateEventBuffer(currentEventBuffer),
- );
- } else {
- this.eventBufferPromiseChain = this.evaluateEventBuffer(currentEventBuffer);
- }
- };
-
- private async evaluateEventBuffer(eventBuffer: MatrixEvent[]): Promise<void> {
- await Promise.all(eventBuffer.map((event) => this.client.decryptEventIfNeeded(event)));
-
- const callEvents = eventBuffer.filter((event) => {
- const eventType = event.getType();
- return eventType.startsWith("m.call.") || eventType.startsWith("org.matrix.call.");
- });
-
- 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 event of callEvents) {
- const eventType = event.getType();
-
- if (eventType === EventType.CallAnswer || eventType === EventType.CallHangup) {
- ignoreCallIds.add(event.getContent().call_id);
- }
- }
-
- // Process call events in the order that they were received
- for (const event of callEvents) {
- const eventType = event.getType();
- const callId = event.getContent().call_id;
-
- if (eventType === EventType.CallInvite && ignoreCallIds.has(callId)) {
- // This call has previously been answered or hung up: ignore it
- continue;
- }
-
- try {
- await this.handleCallEvent(event);
- } catch (e) {
- logger.error("CallEventHandler evaluateEventBuffer() caught exception handling call event", e);
- }
- }
- }
-
- private onRoomTimeline = (event: MatrixEvent): void => {
- this.callEventBuffer.push(event);
- };
-
- private onToDeviceEvent = (event: MatrixEvent): void => {
- const content = event.getContent();
-
- if (!content.call_id) {
- this.callEventBuffer.push(event);
- return;
- }
-
- if (!this.nextSeqByCall.has(content.call_id)) {
- this.nextSeqByCall.set(content.call_id, 0);
- }
-
- if (content.seq === undefined) {
- this.callEventBuffer.push(event);
- return;
- }
-
- const nextSeq = this.nextSeqByCall.get(content.call_id) || 0;
-
- if (content.seq !== nextSeq) {
- if (!this.toDeviceEventBuffers.has(content.call_id)) {
- this.toDeviceEventBuffers.set(content.call_id, []);
- }
-
- const buffer = this.toDeviceEventBuffers.get(content.call_id)!;
- const index = buffer.findIndex((e) => e.getContent().seq > content.seq);
-
- if (index === -1) {
- buffer.push(event);
- } else {
- buffer.splice(index, 0, event);
- }
- } else {
- const callId = content.call_id;
- this.callEventBuffer.push(event);
- this.nextSeqByCall.set(callId, content.seq + 1);
-
- const buffer = this.toDeviceEventBuffers.get(callId);
-
- let nextEvent = buffer && buffer.shift();
-
- while (nextEvent && nextEvent.getContent().seq === this.nextSeqByCall.get(callId)) {
- this.callEventBuffer.push(nextEvent);
- this.nextSeqByCall.set(callId, nextEvent.getContent().seq + 1);
- nextEvent = buffer!.shift();
- }
- }
- };
-
- private async handleCallEvent(event: MatrixEvent): Promise<void> {
- this.client.emit(ClientEvent.ReceivedVoipEvent, event);
-
- const content = event.getContent();
- const callRoomId =
- event.getRoomId() || this.client.groupCallEventHandler!.getGroupCallById(content.conf_id)?.room?.roomId;
- const groupCallId = content.conf_id;
- const type = event.getType() as EventType;
- const senderId = event.getSender()!;
- let call = content.call_id ? this.calls.get(content.call_id) : undefined;
-
- let opponentDeviceId: string | undefined;
-
- let groupCall: GroupCall | undefined;
- if (groupCallId) {
- groupCall = this.client.groupCallEventHandler!.getGroupCallById(groupCallId);
-
- if (!groupCall) {
- logger.warn(
- `CallEventHandler handleCallEvent() could not find a group call - ignoring event (groupCallId=${groupCallId}, type=${type})`,
- );
- return;
- }
-
- opponentDeviceId = content.device_id;
-
- if (!opponentDeviceId) {
- logger.warn(
- `CallEventHandler handleCallEvent() could not find a device id - ignoring event (senderId=${senderId})`,
- );
- groupCall.emit(GroupCallEvent.Error, new GroupCallUnknownDeviceError(senderId));
- return;
- }
-
- if (content.dest_session_id !== this.client.getSessionId()) {
- logger.warn(
- "CallEventHandler handleCallEvent() call event does not match current session id - ignoring",
- );
- return;
- }
- }
-
- const weSentTheEvent =
- senderId === this.client.credentials.userId &&
- (opponentDeviceId === undefined || opponentDeviceId === this.client.getDeviceId()!);
-
- if (!callRoomId) return;
-
- if (type === EventType.CallInvite) {
- // ignore invites you send
- if (weSentTheEvent) return;
- // expired call
- if (event.getLocalAge() > content.lifetime - RING_GRACE_PERIOD) return;
- // stale/old invite event
- if (call && call.state === CallState.Ended) return;
-
- if (call) {
- logger.warn(
- `CallEventHandler handleCallEvent() already has a call but got an invite - clobbering (callId=${content.call_id})`,
- );
- }
-
- if (content.invitee && content.invitee !== this.client.getUserId()) {
- return; // This invite was meant for another user in the room
- }
-
- const timeUntilTurnCresExpire = (this.client.getTurnServersExpiry() ?? 0) - Date.now();
- logger.info(
- "CallEventHandler handleCallEvent() current turn creds expire in " + timeUntilTurnCresExpire + " ms",
- );
- call =
- createNewMatrixCall(this.client, callRoomId, {
- forceTURN: this.client.forceTURN,
- opponentDeviceId,
- groupCallId,
- opponentSessionId: content.sender_session_id,
- }) ?? undefined;
- if (!call) {
- logger.log(
- `CallEventHandler handleCallEvent() this client does not support WebRTC (callId=${content.call_id})`,
- );
- // don't hang up the call: there could be other clients
- // connected that do support WebRTC and declining the
- // the call on their behalf would be really annoying.
- return;
- }
-
- call.callId = content.call_id;
- const stats = groupCall?.getGroupCallStats();
- if (stats) {
- call.initStats(stats);
- }
-
- try {
- await call.initWithInvite(event);
- } catch (e) {
- if (e instanceof CallError) {
- if (e.code === GroupCallErrorCode.UnknownDevice) {
- groupCall?.emit(GroupCallEvent.Error, e);
- } else {
- logger.error(e);
- }
- }
- }
- this.calls.set(call.callId, call);
-
- // if we stashed candidate events for that call ID, play them back now
- if (this.candidateEventsByCall.get(call.callId)) {
- for (const ev of this.candidateEventsByCall.get(call.callId)!) {
- call.onRemoteIceCandidatesReceived(ev);
- }
- }
-
- // Were we trying to call that user (room)?
- let existingCall: MatrixCall | undefined;
- for (const thisCall of this.calls.values()) {
- const isCalling = [CallState.WaitLocalMedia, CallState.CreateOffer, CallState.InviteSent].includes(
- thisCall.state,
- );
-
- if (
- call.roomId === thisCall.roomId &&
- thisCall.direction === CallDirection.Outbound &&
- call.getOpponentMember()?.userId === thisCall.invitee &&
- isCalling
- ) {
- existingCall = thisCall;
- break;
- }
- }
-
- if (existingCall) {
- if (existingCall.callId > call.callId) {
- logger.log(
- `CallEventHandler handleCallEvent() detected glare - answering incoming call and canceling outgoing call (incomingId=${call.callId}, outgoingId=${existingCall.callId})`,
- );
- existingCall.replacedBy(call);
- } else {
- logger.log(
- `CallEventHandler handleCallEvent() detected glare - hanging up incoming call (incomingId=${call.callId}, outgoingId=${existingCall.callId})`,
- );
- call.hangup(CallErrorCode.Replaced, true);
- }
- } else {
- this.client.emit(CallEventHandlerEvent.Incoming, call);
- }
- return;
- } else if (type === EventType.CallCandidates) {
- if (weSentTheEvent) return;
-
- if (!call) {
- // store the candidates; we may get a call eventually.
- if (!this.candidateEventsByCall.has(content.call_id)) {
- this.candidateEventsByCall.set(content.call_id, []);
- }
- this.candidateEventsByCall.get(content.call_id)!.push(event);
- } else {
- call.onRemoteIceCandidatesReceived(event);
- }
- return;
- } else if ([EventType.CallHangup, EventType.CallReject].includes(type)) {
- // Note that we also observe our own hangups here so we can see
- // if we've already rejected a call that would otherwise be valid
- if (!call) {
- // if not live, store the fact that the call has ended because
- // we're probably getting events backwards so
- // the hangup will come before the invite
- call =
- createNewMatrixCall(this.client, callRoomId, {
- opponentDeviceId,
- opponentSessionId: content.sender_session_id,
- }) ?? undefined;
- if (call) {
- call.callId = content.call_id;
- call.initWithHangup(event);
- this.calls.set(content.call_id, call);
- }
- } else {
- if (call.state !== CallState.Ended) {
- if (type === EventType.CallHangup) {
- call.onHangupReceived(content as MCallHangupReject);
- } else {
- call.onRejectReceived(content as MCallHangupReject);
- }
-
- // @ts-expect-error typescript thinks the state can't be 'ended' because we're
- // inside the if block where it wasn't, but it could have changed because
- // on[Hangup|Reject]Received are side-effecty.
- if (call.state === CallState.Ended) this.calls.delete(content.call_id);
- }
- }
- return;
- }
-
- // The following events need a call and a peer connection
- if (!call || !call.hasPeerConnection) {
- logger.info(
- `CallEventHandler handleCallEvent() discarding possible call event as we don't have a call (type=${type})`,
- );
- return;
- }
- // Ignore remote echo
- if (event.getContent().party_id === call.ourPartyId) return;
-
- switch (type) {
- case EventType.CallAnswer:
- if (weSentTheEvent) {
- if (call.state === CallState.Ringing) {
- call.onAnsweredElsewhere(content as MCallAnswer);
- }
- } else {
- call.onAnswerReceived(event);
- }
- break;
- case EventType.CallSelectAnswer:
- call.onSelectAnswerReceived(event);
- break;
-
- case EventType.CallNegotiate:
- call.onNegotiateReceived(event);
- break;
-
- case EventType.CallAssertedIdentity:
- case EventType.CallAssertedIdentityPrefix:
- call.onAssertedIdentityReceived(event);
- break;
-
- case EventType.CallSDPStreamMetadataChanged:
- case EventType.CallSDPStreamMetadataChangedPrefix:
- call.onSDPStreamMetadataChangedReceived(event);
- break;
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/callEventTypes.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/callEventTypes.ts
deleted file mode 100644
index f06ed5b..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/callEventTypes.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-// allow non-camelcase as these are events type that go onto the wire
-/* eslint-disable camelcase */
-
-import { CallErrorCode } from "./call";
-
-// TODO: Change to "sdp_stream_metadata" when MSC3077 is merged
-export const SDPStreamMetadataKey = "org.matrix.msc3077.sdp_stream_metadata";
-
-export enum SDPStreamMetadataPurpose {
- Usermedia = "m.usermedia",
- Screenshare = "m.screenshare",
-}
-
-export interface SDPStreamMetadataObject {
- purpose: SDPStreamMetadataPurpose;
- audio_muted: boolean;
- video_muted: boolean;
-}
-
-export interface SDPStreamMetadata {
- [key: string]: SDPStreamMetadataObject;
-}
-
-export interface CallCapabilities {
- "m.call.transferee": boolean;
- "m.call.dtmf": boolean;
-}
-
-export interface CallReplacesTarget {
- id: string;
- display_name: string;
- avatar_url: string;
-}
-
-export interface MCallBase {
- call_id: string;
- version: string | number;
- party_id?: string;
- sender_session_id?: string;
- dest_session_id?: string;
-}
-
-export interface MCallAnswer extends MCallBase {
- answer: RTCSessionDescription;
- capabilities?: CallCapabilities;
- [SDPStreamMetadataKey]: SDPStreamMetadata;
-}
-
-export interface MCallSelectAnswer extends MCallBase {
- selected_party_id: string;
-}
-
-export interface MCallInviteNegotiate extends MCallBase {
- offer: RTCSessionDescription;
- description: RTCSessionDescription;
- lifetime: number;
- capabilities?: CallCapabilities;
- invitee?: string;
- sender_session_id?: string;
- dest_session_id?: string;
- [SDPStreamMetadataKey]: SDPStreamMetadata;
-}
-
-export interface MCallSDPStreamMetadataChanged extends MCallBase {
- [SDPStreamMetadataKey]: SDPStreamMetadata;
-}
-
-export interface MCallReplacesEvent extends MCallBase {
- replacement_id: string;
- target_user: CallReplacesTarget;
- create_call: string;
- await_call: string;
- target_room: string;
-}
-
-export interface MCAllAssertedIdentity extends MCallBase {
- asserted_identity: {
- id: string;
- display_name: string;
- avatar_url: string;
- };
-}
-
-export interface MCallCandidates extends MCallBase {
- candidates: RTCIceCandidate[];
-}
-
-export interface MCallHangupReject extends MCallBase {
- reason?: CallErrorCode;
-}
-
-/* eslint-enable camelcase */
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/callFeed.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/callFeed.ts
deleted file mode 100644
index 505cf56..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/callFeed.ts
+++ /dev/null
@@ -1,361 +0,0 @@
-/*
-Copyright 2021 Š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.
-*/
-
-import { SDPStreamMetadataPurpose } from "./callEventTypes";
-import { acquireContext, releaseContext } from "./audioContext";
-import { MatrixClient } from "../client";
-import { RoomMember } from "../models/room-member";
-import { logger } from "../logger";
-import { TypedEventEmitter } from "../models/typed-event-emitter";
-import { CallEvent, CallState, MatrixCall } from "./call";
-
-const POLLING_INTERVAL = 200; // ms
-export const SPEAKING_THRESHOLD = -60; // dB
-const SPEAKING_SAMPLE_COUNT = 8; // samples
-
-export interface ICallFeedOpts {
- client: MatrixClient;
- roomId?: string;
- userId: string;
- deviceId: string | undefined;
- stream: MediaStream;
- purpose: SDPStreamMetadataPurpose;
- /**
- * Whether or not the remote SDPStreamMetadata says audio is muted
- */
- audioMuted: boolean;
- /**
- * Whether or not the remote SDPStreamMetadata says video is muted
- */
- videoMuted: boolean;
- /**
- * The MatrixCall which is the source of this CallFeed
- */
- call?: MatrixCall;
-}
-
-export enum CallFeedEvent {
- NewStream = "new_stream",
- MuteStateChanged = "mute_state_changed",
- LocalVolumeChanged = "local_volume_changed",
- VolumeChanged = "volume_changed",
- ConnectedChanged = "connected_changed",
- Speaking = "speaking",
- Disposed = "disposed",
-}
-
-type EventHandlerMap = {
- [CallFeedEvent.NewStream]: (stream: MediaStream) => void;
- [CallFeedEvent.MuteStateChanged]: (audioMuted: boolean, videoMuted: boolean) => void;
- [CallFeedEvent.LocalVolumeChanged]: (localVolume: number) => void;
- [CallFeedEvent.VolumeChanged]: (volume: number) => void;
- [CallFeedEvent.ConnectedChanged]: (connected: boolean) => void;
- [CallFeedEvent.Speaking]: (speaking: boolean) => void;
- [CallFeedEvent.Disposed]: () => void;
-};
-
-export class CallFeed extends TypedEventEmitter<CallFeedEvent, EventHandlerMap> {
- public stream: MediaStream;
- public sdpMetadataStreamId: string;
- public userId: string;
- public readonly deviceId: string | undefined;
- public purpose: SDPStreamMetadataPurpose;
- public speakingVolumeSamples: number[];
-
- private client: MatrixClient;
- private call?: MatrixCall;
- private roomId?: string;
- private audioMuted: boolean;
- private videoMuted: boolean;
- private localVolume = 1;
- private measuringVolumeActivity = false;
- private audioContext?: AudioContext;
- private analyser?: AnalyserNode;
- private frequencyBinCount?: Float32Array;
- private speakingThreshold = SPEAKING_THRESHOLD;
- private speaking = false;
- private volumeLooperTimeout?: ReturnType<typeof setTimeout>;
- private _disposed = false;
- private _connected = false;
-
- public constructor(opts: ICallFeedOpts) {
- super();
-
- this.client = opts.client;
- this.call = opts.call;
- this.roomId = opts.roomId;
- this.userId = opts.userId;
- this.deviceId = opts.deviceId;
- this.purpose = opts.purpose;
- this.audioMuted = opts.audioMuted;
- this.videoMuted = opts.videoMuted;
- this.speakingVolumeSamples = new Array(SPEAKING_SAMPLE_COUNT).fill(-Infinity);
- this.sdpMetadataStreamId = opts.stream.id;
-
- this.updateStream(null, opts.stream);
- this.stream = opts.stream; // updateStream does this, but this makes TS happier
-
- if (this.hasAudioTrack) {
- this.initVolumeMeasuring();
- }
-
- if (opts.call) {
- opts.call.addListener(CallEvent.State, this.onCallState);
- this.onCallState(opts.call.state);
- }
- }
-
- public get connected(): boolean {
- // Local feeds are always considered connected
- return this.isLocal() || this._connected;
- }
-
- private set connected(connected: boolean) {
- this._connected = connected;
- this.emit(CallFeedEvent.ConnectedChanged, this.connected);
- }
-
- private get hasAudioTrack(): boolean {
- return this.stream.getAudioTracks().length > 0;
- }
-
- private updateStream(oldStream: MediaStream | null, newStream: MediaStream): void {
- if (newStream === oldStream) return;
-
- if (oldStream) {
- oldStream.removeEventListener("addtrack", this.onAddTrack);
- this.measureVolumeActivity(false);
- }
-
- this.stream = newStream;
- newStream.addEventListener("addtrack", this.onAddTrack);
-
- if (this.hasAudioTrack) {
- this.initVolumeMeasuring();
- } else {
- this.measureVolumeActivity(false);
- }
-
- this.emit(CallFeedEvent.NewStream, this.stream);
- }
-
- private initVolumeMeasuring(): void {
- if (!this.hasAudioTrack) return;
- if (!this.audioContext) this.audioContext = acquireContext();
-
- this.analyser = this.audioContext.createAnalyser();
- this.analyser.fftSize = 512;
- this.analyser.smoothingTimeConstant = 0.1;
-
- const mediaStreamAudioSourceNode = this.audioContext.createMediaStreamSource(this.stream);
- mediaStreamAudioSourceNode.connect(this.analyser);
-
- this.frequencyBinCount = new Float32Array(this.analyser.frequencyBinCount);
- }
-
- private onAddTrack = (): void => {
- this.emit(CallFeedEvent.NewStream, this.stream);
- };
-
- private onCallState = (state: CallState): void => {
- if (state === CallState.Connected) {
- this.connected = true;
- } else if (state === CallState.Connecting) {
- this.connected = false;
- }
- };
-
- /**
- * Returns callRoom member
- * @returns member of the callRoom
- */
- public getMember(): RoomMember | null {
- const callRoom = this.client.getRoom(this.roomId);
- return callRoom?.getMember(this.userId) ?? null;
- }
-
- /**
- * Returns true if CallFeed is local, otherwise returns false
- * @returns is local?
- */
- public isLocal(): boolean {
- return (
- this.userId === this.client.getUserId() &&
- (this.deviceId === undefined || this.deviceId === this.client.getDeviceId())
- );
- }
-
- /**
- * Returns true if audio is muted or if there are no audio
- * tracks, otherwise returns false
- * @returns is audio muted?
- */
- public isAudioMuted(): boolean {
- return this.stream.getAudioTracks().length === 0 || this.audioMuted;
- }
-
- /**
- * Returns true video is muted or if there are no video
- * tracks, otherwise returns false
- * @returns is video muted?
- */
- public isVideoMuted(): boolean {
- // We assume only one video track
- return this.stream.getVideoTracks().length === 0 || this.videoMuted;
- }
-
- public isSpeaking(): boolean {
- return this.speaking;
- }
-
- /**
- * Replaces the current MediaStream with a new one.
- * The stream will be different and new stream as remote parties are
- * concerned, but this can be used for convenience locally to set up
- * volume listeners automatically on the new stream etc.
- * @param newStream - new stream with which to replace the current one
- */
- public setNewStream(newStream: MediaStream): void {
- this.updateStream(this.stream, newStream);
- }
-
- /**
- * Set one or both of feed's internal audio and video video mute state
- * Either value may be null to leave it as-is
- * @param audioMuted - is the feed's audio muted?
- * @param videoMuted - is the feed's video muted?
- */
- public setAudioVideoMuted(audioMuted: boolean | null, videoMuted: boolean | null): void {
- if (audioMuted !== null) {
- if (this.audioMuted !== audioMuted) {
- this.speakingVolumeSamples.fill(-Infinity);
- }
- this.audioMuted = audioMuted;
- }
- if (videoMuted !== null) this.videoMuted = videoMuted;
- this.emit(CallFeedEvent.MuteStateChanged, this.audioMuted, this.videoMuted);
- }
-
- /**
- * Starts emitting volume_changed events where the emitter value is in decibels
- * @param enabled - emit volume changes
- */
- public measureVolumeActivity(enabled: boolean): void {
- if (enabled) {
- if (!this.analyser || !this.frequencyBinCount || !this.hasAudioTrack) return;
-
- this.measuringVolumeActivity = true;
- this.volumeLooper();
- } else {
- this.measuringVolumeActivity = false;
- this.speakingVolumeSamples.fill(-Infinity);
- this.emit(CallFeedEvent.VolumeChanged, -Infinity);
- }
- }
-
- public setSpeakingThreshold(threshold: number): void {
- this.speakingThreshold = threshold;
- }
-
- private volumeLooper = (): void => {
- if (!this.analyser) return;
-
- if (!this.measuringVolumeActivity) return;
-
- this.analyser.getFloatFrequencyData(this.frequencyBinCount!);
-
- let maxVolume = -Infinity;
- for (const volume of this.frequencyBinCount!) {
- if (volume > maxVolume) {
- maxVolume = volume;
- }
- }
-
- this.speakingVolumeSamples.shift();
- this.speakingVolumeSamples.push(maxVolume);
-
- this.emit(CallFeedEvent.VolumeChanged, maxVolume);
-
- let newSpeaking = false;
-
- for (const volume of this.speakingVolumeSamples) {
- if (volume > this.speakingThreshold) {
- newSpeaking = true;
- break;
- }
- }
-
- if (this.speaking !== newSpeaking) {
- this.speaking = newSpeaking;
- this.emit(CallFeedEvent.Speaking, this.speaking);
- }
-
- this.volumeLooperTimeout = setTimeout(this.volumeLooper, POLLING_INTERVAL);
- };
-
- public clone(): CallFeed {
- const mediaHandler = this.client.getMediaHandler();
- const stream = this.stream.clone();
- logger.log(`CallFeed clone() cloning stream (originalStreamId=${this.stream.id}, newStreamId${stream.id})`);
-
- if (this.purpose === SDPStreamMetadataPurpose.Usermedia) {
- mediaHandler.userMediaStreams.push(stream);
- } else {
- mediaHandler.screensharingStreams.push(stream);
- }
-
- return new CallFeed({
- client: this.client,
- roomId: this.roomId,
- userId: this.userId,
- deviceId: this.deviceId,
- stream,
- purpose: this.purpose,
- audioMuted: this.audioMuted,
- videoMuted: this.videoMuted,
- });
- }
-
- public dispose(): void {
- clearTimeout(this.volumeLooperTimeout);
- this.stream?.removeEventListener("addtrack", this.onAddTrack);
- this.call?.removeListener(CallEvent.State, this.onCallState);
- if (this.audioContext) {
- this.audioContext = undefined;
- this.analyser = undefined;
- releaseContext();
- }
- this._disposed = true;
- this.emit(CallFeedEvent.Disposed);
- }
-
- public get disposed(): boolean {
- return this._disposed;
- }
-
- private set disposed(value: boolean) {
- this._disposed = value;
- }
-
- public getLocalVolume(): number {
- return this.localVolume;
- }
-
- public setLocalVolume(localVolume: number): void {
- this.localVolume = localVolume;
- this.emit(CallFeedEvent.LocalVolumeChanged, localVolume);
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/groupCall.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/groupCall.ts
deleted file mode 100644
index c0896c4..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/groupCall.ts
+++ /dev/null
@@ -1,1598 +0,0 @@
-import { TypedEventEmitter } from "../models/typed-event-emitter";
-import { CallFeed, SPEAKING_THRESHOLD } from "./callFeed";
-import { MatrixClient, IMyDevice } from "../client";
-import {
- CallErrorCode,
- CallEvent,
- CallEventHandlerMap,
- CallState,
- genCallID,
- MatrixCall,
- setTracksEnabled,
- createNewMatrixCall,
- CallError,
-} from "./call";
-import { RoomMember } from "../models/room-member";
-import { Room } from "../models/room";
-import { RoomStateEvent } from "../models/room-state";
-import { logger } from "../logger";
-import { ReEmitter } from "../ReEmitter";
-import { SDPStreamMetadataPurpose } from "./callEventTypes";
-import { MatrixEvent } from "../models/event";
-import { EventType } from "../@types/event";
-import { CallEventHandlerEvent } from "./callEventHandler";
-import { GroupCallEventHandlerEvent } from "./groupCallEventHandler";
-import { IScreensharingOpts } from "./mediaHandler";
-import { mapsEqual } from "../utils";
-import { GroupCallStats } from "./stats/groupCallStats";
-import { ByteSentStatsReport, ConnectionStatsReport, StatsReport } from "./stats/statsReport";
-
-export enum GroupCallIntent {
- Ring = "m.ring",
- Prompt = "m.prompt",
- Room = "m.room",
-}
-
-export enum GroupCallType {
- Video = "m.video",
- Voice = "m.voice",
-}
-
-export enum GroupCallTerminationReason {
- CallEnded = "call_ended",
-}
-
-export type CallsByUserAndDevice = Map<string, Map<string, MatrixCall>>;
-
-/**
- * Because event names are just strings, they do need
- * to be unique over all event types of event emitter.
- * Some objects could emit more then one set of events.
- */
-export enum GroupCallEvent {
- GroupCallStateChanged = "group_call_state_changed",
- ActiveSpeakerChanged = "active_speaker_changed",
- CallsChanged = "calls_changed",
- UserMediaFeedsChanged = "user_media_feeds_changed",
- ScreenshareFeedsChanged = "screenshare_feeds_changed",
- LocalScreenshareStateChanged = "local_screenshare_state_changed",
- LocalMuteStateChanged = "local_mute_state_changed",
- ParticipantsChanged = "participants_changed",
- Error = "group_call_error",
-}
-
-export type GroupCallEventHandlerMap = {
- [GroupCallEvent.GroupCallStateChanged]: (newState: GroupCallState, oldState: GroupCallState) => void;
- [GroupCallEvent.ActiveSpeakerChanged]: (activeSpeaker: CallFeed | undefined) => void;
- [GroupCallEvent.CallsChanged]: (calls: CallsByUserAndDevice) => void;
- [GroupCallEvent.UserMediaFeedsChanged]: (feeds: CallFeed[]) => void;
- [GroupCallEvent.ScreenshareFeedsChanged]: (feeds: CallFeed[]) => void;
- [GroupCallEvent.LocalScreenshareStateChanged]: (
- isScreensharing: boolean,
- feed?: CallFeed,
- sourceId?: string,
- ) => void;
- [GroupCallEvent.LocalMuteStateChanged]: (audioMuted: boolean, videoMuted: boolean) => void;
- [GroupCallEvent.ParticipantsChanged]: (participants: Map<RoomMember, Map<string, ParticipantState>>) => void;
- /**
- * Fires whenever an error occurs when call.js encounters an issue with setting up the call.
- * <p>
- * The error given will have a code equal to either `MatrixCall.ERR_LOCAL_OFFER_FAILED` or
- * `MatrixCall.ERR_NO_USER_MEDIA`. `ERR_LOCAL_OFFER_FAILED` is emitted when the local client
- * fails to create an offer. `ERR_NO_USER_MEDIA` is emitted when the user has denied access
- * to their audio/video hardware.
- * @param err - The error raised by MatrixCall.
- * @example
- * ```
- * matrixCall.on("error", function(err){
- * console.error(err.code, err);
- * });
- * ```
- */
- [GroupCallEvent.Error]: (error: GroupCallError) => void;
-};
-
-export enum GroupCallStatsReportEvent {
- ConnectionStats = "GroupCall.connection_stats",
- ByteSentStats = "GroupCall.byte_sent_stats",
-}
-
-export type GroupCallStatsReportEventHandlerMap = {
- [GroupCallStatsReportEvent.ConnectionStats]: (report: GroupCallStatsReport<ConnectionStatsReport>) => void;
- [GroupCallStatsReportEvent.ByteSentStats]: (report: GroupCallStatsReport<ByteSentStatsReport>) => void;
-};
-
-export enum GroupCallErrorCode {
- NoUserMedia = "no_user_media",
- UnknownDevice = "unknown_device",
- PlaceCallFailed = "place_call_failed",
-}
-
-export interface GroupCallStatsReport<T extends ConnectionStatsReport | ByteSentStatsReport> {
- report: T;
-}
-
-export class GroupCallError extends Error {
- public code: string;
-
- public constructor(code: GroupCallErrorCode, msg: string, err?: Error) {
- // Still don't think there's any way to have proper nested errors
- if (err) {
- super(msg + ": " + err);
- } else {
- super(msg);
- }
-
- this.code = code;
- }
-}
-
-export class GroupCallUnknownDeviceError extends GroupCallError {
- public constructor(public userId: string) {
- super(GroupCallErrorCode.UnknownDevice, "No device found for " + userId);
- }
-}
-
-export class OtherUserSpeakingError extends Error {
- public constructor() {
- super("Cannot unmute: another user is speaking");
- }
-}
-
-export interface IGroupCallDataChannelOptions {
- ordered: boolean;
- maxPacketLifeTime: number;
- maxRetransmits: number;
- protocol: string;
-}
-
-export interface IGroupCallRoomState {
- "m.intent": GroupCallIntent;
- "m.type": GroupCallType;
- "io.element.ptt"?: boolean;
- // TODO: Specify data-channels
- "dataChannelsEnabled"?: boolean;
- "dataChannelOptions"?: IGroupCallDataChannelOptions;
-}
-
-export interface IGroupCallRoomMemberFeed {
- purpose: SDPStreamMetadataPurpose;
-}
-
-export interface IGroupCallRoomMemberDevice {
- device_id: string;
- session_id: string;
- expires_ts: number;
- feeds: IGroupCallRoomMemberFeed[];
-}
-
-export interface IGroupCallRoomMemberCallState {
- "m.call_id": string;
- "m.foci"?: string[];
- "m.devices": IGroupCallRoomMemberDevice[];
-}
-
-export interface IGroupCallRoomMemberState {
- "m.calls": IGroupCallRoomMemberCallState[];
-}
-
-export enum GroupCallState {
- LocalCallFeedUninitialized = "local_call_feed_uninitialized",
- InitializingLocalCallFeed = "initializing_local_call_feed",
- LocalCallFeedInitialized = "local_call_feed_initialized",
- Entered = "entered",
- Ended = "ended",
-}
-
-export interface ParticipantState {
- sessionId: string;
- screensharing: boolean;
-}
-
-interface ICallHandlers {
- onCallFeedsChanged: (feeds: CallFeed[]) => void;
- onCallStateChanged: (state: CallState, oldState: CallState | undefined) => void;
- onCallHangup: (call: MatrixCall) => void;
- onCallReplaced: (newCall: MatrixCall) => void;
-}
-
-const DEVICE_TIMEOUT = 1000 * 60 * 60; // 1 hour
-
-function getCallUserId(call: MatrixCall): string | null {
- return call.getOpponentMember()?.userId || call.invitee || null;
-}
-
-export class GroupCall extends TypedEventEmitter<
- GroupCallEvent | CallEvent | GroupCallStatsReportEvent,
- GroupCallEventHandlerMap & CallEventHandlerMap & GroupCallStatsReportEventHandlerMap
-> {
- // Config
- public activeSpeakerInterval = 1000;
- public retryCallInterval = 5000;
- public participantTimeout = 1000 * 15;
- public pttMaxTransmitTime = 1000 * 20;
-
- public activeSpeaker?: CallFeed;
- public localCallFeed?: CallFeed;
- public localScreenshareFeed?: CallFeed;
- public localDesktopCapturerSourceId?: string;
- public readonly userMediaFeeds: CallFeed[] = [];
- public readonly screenshareFeeds: CallFeed[] = [];
- public groupCallId: string;
- public readonly allowCallWithoutVideoAndAudio: boolean;
-
- private readonly calls = new Map<string, Map<string, MatrixCall>>(); // user_id -> device_id -> MatrixCall
- private callHandlers = new Map<string, Map<string, ICallHandlers>>(); // user_id -> device_id -> ICallHandlers
- private activeSpeakerLoopInterval?: ReturnType<typeof setTimeout>;
- private retryCallLoopInterval?: ReturnType<typeof setTimeout>;
- private retryCallCounts: Map<string, Map<string, number>> = new Map(); // user_id -> device_id -> count
- private reEmitter: ReEmitter;
- private transmitTimer: ReturnType<typeof setTimeout> | null = null;
- private participantsExpirationTimer: ReturnType<typeof setTimeout> | null = null;
- private resendMemberStateTimer: ReturnType<typeof setInterval> | null = null;
- private initWithAudioMuted = false;
- private initWithVideoMuted = false;
- private initCallFeedPromise?: Promise<void>;
-
- private readonly stats: GroupCallStats;
-
- public constructor(
- private client: MatrixClient,
- public room: Room,
- public type: GroupCallType,
- public isPtt: boolean,
- public intent: GroupCallIntent,
- groupCallId?: string,
- private dataChannelsEnabled?: boolean,
- private dataChannelOptions?: IGroupCallDataChannelOptions,
- isCallWithoutVideoAndAudio?: boolean,
- ) {
- super();
- this.reEmitter = new ReEmitter(this);
- this.groupCallId = groupCallId ?? genCallID();
- this.creationTs =
- room.currentState.getStateEvents(EventType.GroupCallPrefix, this.groupCallId)?.getTs() ?? null;
- this.updateParticipants();
-
- room.on(RoomStateEvent.Update, this.onRoomState);
- this.on(GroupCallEvent.ParticipantsChanged, this.onParticipantsChanged);
- this.on(GroupCallEvent.GroupCallStateChanged, this.onStateChanged);
- this.on(GroupCallEvent.LocalScreenshareStateChanged, this.onLocalFeedsChanged);
- this.allowCallWithoutVideoAndAudio = !!isCallWithoutVideoAndAudio;
-
- const userID = this.client.getUserId() || "unknown";
- this.stats = new GroupCallStats(this.groupCallId, userID);
- this.stats.reports.on(StatsReport.CONNECTION_STATS, this.onConnectionStats);
- this.stats.reports.on(StatsReport.BYTE_SENT_STATS, this.onByteSentStats);
- }
-
- private onConnectionStats = (report: ConnectionStatsReport): void => {
- // @TODO: Implement data argumentation
- this.emit(GroupCallStatsReportEvent.ConnectionStats, { report });
- };
-
- private onByteSentStats = (report: ByteSentStatsReport): void => {
- // @TODO: Implement data argumentation
- this.emit(GroupCallStatsReportEvent.ByteSentStats, { report });
- };
-
- public async create(): Promise<GroupCall> {
- this.creationTs = Date.now();
- this.client.groupCallEventHandler!.groupCalls.set(this.room.roomId, this);
- this.client.emit(GroupCallEventHandlerEvent.Outgoing, this);
-
- const groupCallState: IGroupCallRoomState = {
- "m.intent": this.intent,
- "m.type": this.type,
- "io.element.ptt": this.isPtt,
- // TODO: Specify data-channels better
- "dataChannelsEnabled": this.dataChannelsEnabled,
- "dataChannelOptions": this.dataChannelsEnabled ? this.dataChannelOptions : undefined,
- };
-
- await this.client.sendStateEvent(this.room.roomId, EventType.GroupCallPrefix, groupCallState, this.groupCallId);
-
- return this;
- }
-
- private _state = GroupCallState.LocalCallFeedUninitialized;
-
- /**
- * The group call's state.
- */
- public get state(): GroupCallState {
- return this._state;
- }
-
- private set state(value: GroupCallState) {
- const prevValue = this._state;
- if (value !== prevValue) {
- this._state = value;
- this.emit(GroupCallEvent.GroupCallStateChanged, value, prevValue);
- }
- }
-
- private _participants = new Map<RoomMember, Map<string, ParticipantState>>();
-
- /**
- * The current participants in the call, as a map from members to device IDs
- * to participant info.
- */
- public get participants(): Map<RoomMember, Map<string, ParticipantState>> {
- return this._participants;
- }
-
- private set participants(value: Map<RoomMember, Map<string, ParticipantState>>) {
- const prevValue = this._participants;
- const participantStateEqual = (x: ParticipantState, y: ParticipantState): boolean =>
- x.sessionId === y.sessionId && x.screensharing === y.screensharing;
- const deviceMapsEqual = (x: Map<string, ParticipantState>, y: Map<string, ParticipantState>): boolean =>
- mapsEqual(x, y, participantStateEqual);
-
- // Only update if the map actually changed
- if (!mapsEqual(value, prevValue, deviceMapsEqual)) {
- this._participants = value;
- this.emit(GroupCallEvent.ParticipantsChanged, value);
- }
- }
-
- private _creationTs: number | null = null;
-
- /**
- * The timestamp at which the call was created, or null if it has not yet
- * been created.
- */
- public get creationTs(): number | null {
- return this._creationTs;
- }
-
- private set creationTs(value: number | null) {
- this._creationTs = value;
- }
-
- private _enteredViaAnotherSession = false;
-
- /**
- * Whether the local device has entered this call via another session, such
- * as a widget.
- */
- public get enteredViaAnotherSession(): boolean {
- return this._enteredViaAnotherSession;
- }
-
- public set enteredViaAnotherSession(value: boolean) {
- this._enteredViaAnotherSession = value;
- this.updateParticipants();
- }
-
- /**
- * Executes the given callback on all calls in this group call.
- * @param f - The callback.
- */
- public forEachCall(f: (call: MatrixCall) => void): void {
- for (const deviceMap of this.calls.values()) {
- for (const call of deviceMap.values()) f(call);
- }
- }
-
- public getLocalFeeds(): CallFeed[] {
- const feeds: CallFeed[] = [];
-
- if (this.localCallFeed) feeds.push(this.localCallFeed);
- if (this.localScreenshareFeed) feeds.push(this.localScreenshareFeed);
-
- return feeds;
- }
-
- public hasLocalParticipant(): boolean {
- return (
- this.participants.get(this.room.getMember(this.client.getUserId()!)!)?.has(this.client.getDeviceId()!) ??
- false
- );
- }
-
- /**
- * Determines whether the given call is one that we were expecting to exist
- * given our knowledge of who is participating in the group call.
- */
- private callExpected(call: MatrixCall): boolean {
- const userId = getCallUserId(call);
- const member = userId === null ? null : this.room.getMember(userId);
- const deviceId = call.getOpponentDeviceId();
- return member !== null && deviceId !== undefined && this.participants.get(member)?.get(deviceId) !== undefined;
- }
-
- public async initLocalCallFeed(): Promise<void> {
- if (this.state !== GroupCallState.LocalCallFeedUninitialized) {
- throw new Error(`Cannot initialize local call feed in the "${this.state}" state.`);
- }
- this.state = GroupCallState.InitializingLocalCallFeed;
-
- // wraps the real method to serialise calls, because we don't want to try starting
- // multiple call feeds at once
- if (this.initCallFeedPromise) return this.initCallFeedPromise;
-
- try {
- this.initCallFeedPromise = this.initLocalCallFeedInternal();
- await this.initCallFeedPromise;
- } finally {
- this.initCallFeedPromise = undefined;
- }
- }
-
- private async initLocalCallFeedInternal(): Promise<void> {
- logger.log(`GroupCall ${this.groupCallId} initLocalCallFeedInternal() running`);
-
- let stream: MediaStream;
-
- try {
- stream = await this.client.getMediaHandler().getUserMediaStream(true, this.type === GroupCallType.Video);
- } catch (error) {
- // If is allowed to join a call without a media stream, then we
- // don't throw an error here. But we need an empty Local Feed to establish
- // a connection later.
- if (this.allowCallWithoutVideoAndAudio) {
- stream = new MediaStream();
- } else {
- this.state = GroupCallState.LocalCallFeedUninitialized;
- throw error;
- }
- }
-
- // The call could've been disposed while we were waiting, and could
- // also have been started back up again (hello, React 18) so if we're
- // still in this 'initializing' state, carry on, otherwise bail.
- if (this._state !== GroupCallState.InitializingLocalCallFeed) {
- this.client.getMediaHandler().stopUserMediaStream(stream);
- throw new Error("Group call disposed while gathering media stream");
- }
-
- const callFeed = new CallFeed({
- client: this.client,
- roomId: this.room.roomId,
- userId: this.client.getUserId()!,
- deviceId: this.client.getDeviceId()!,
- stream,
- purpose: SDPStreamMetadataPurpose.Usermedia,
- audioMuted: this.initWithAudioMuted || stream.getAudioTracks().length === 0 || this.isPtt,
- videoMuted: this.initWithVideoMuted || stream.getVideoTracks().length === 0,
- });
-
- setTracksEnabled(stream.getAudioTracks(), !callFeed.isAudioMuted());
- setTracksEnabled(stream.getVideoTracks(), !callFeed.isVideoMuted());
-
- this.localCallFeed = callFeed;
- this.addUserMediaFeed(callFeed);
-
- this.state = GroupCallState.LocalCallFeedInitialized;
- }
-
- public async updateLocalUsermediaStream(stream: MediaStream): Promise<void> {
- if (this.localCallFeed) {
- const oldStream = this.localCallFeed.stream;
- this.localCallFeed.setNewStream(stream);
- const micShouldBeMuted = this.localCallFeed.isAudioMuted();
- const vidShouldBeMuted = this.localCallFeed.isVideoMuted();
- logger.log(
- `GroupCall ${this.groupCallId} updateLocalUsermediaStream() (oldStreamId=${oldStream.id}, newStreamId=${stream.id}, micShouldBeMuted=${micShouldBeMuted}, vidShouldBeMuted=${vidShouldBeMuted})`,
- );
- setTracksEnabled(stream.getAudioTracks(), !micShouldBeMuted);
- setTracksEnabled(stream.getVideoTracks(), !vidShouldBeMuted);
- this.client.getMediaHandler().stopUserMediaStream(oldStream);
- }
- }
-
- public async enter(): Promise<void> {
- if (this.state === GroupCallState.LocalCallFeedUninitialized) {
- await this.initLocalCallFeed();
- } else if (this.state !== GroupCallState.LocalCallFeedInitialized) {
- throw new Error(`Cannot enter call in the "${this.state}" state`);
- }
-
- logger.log(`GroupCall ${this.groupCallId} enter() running`);
- this.state = GroupCallState.Entered;
-
- this.client.on(CallEventHandlerEvent.Incoming, this.onIncomingCall);
-
- for (const call of this.client.callEventHandler!.calls.values()) {
- this.onIncomingCall(call);
- }
-
- this.retryCallLoopInterval = setInterval(this.onRetryCallLoop, this.retryCallInterval);
-
- this.activeSpeaker = undefined;
- this.onActiveSpeakerLoop();
- this.activeSpeakerLoopInterval = setInterval(this.onActiveSpeakerLoop, this.activeSpeakerInterval);
- }
-
- private dispose(): void {
- if (this.localCallFeed) {
- this.removeUserMediaFeed(this.localCallFeed);
- this.localCallFeed = undefined;
- }
-
- if (this.localScreenshareFeed) {
- this.client.getMediaHandler().stopScreensharingStream(this.localScreenshareFeed.stream);
- this.removeScreenshareFeed(this.localScreenshareFeed);
- this.localScreenshareFeed = undefined;
- this.localDesktopCapturerSourceId = undefined;
- }
-
- this.client.getMediaHandler().stopAllStreams();
-
- if (this.transmitTimer !== null) {
- clearTimeout(this.transmitTimer);
- this.transmitTimer = null;
- }
-
- if (this.retryCallLoopInterval !== undefined) {
- clearInterval(this.retryCallLoopInterval);
- this.retryCallLoopInterval = undefined;
- }
-
- if (this.participantsExpirationTimer !== null) {
- clearTimeout(this.participantsExpirationTimer);
- this.participantsExpirationTimer = null;
- }
-
- if (this.state !== GroupCallState.Entered) {
- return;
- }
-
- this.forEachCall((call) => call.hangup(CallErrorCode.UserHangup, false));
-
- this.activeSpeaker = undefined;
- clearInterval(this.activeSpeakerLoopInterval);
-
- this.retryCallCounts.clear();
- clearInterval(this.retryCallLoopInterval);
-
- this.client.removeListener(CallEventHandlerEvent.Incoming, this.onIncomingCall);
- this.stats.stop();
- }
-
- public leave(): void {
- this.dispose();
- this.state = GroupCallState.LocalCallFeedUninitialized;
- }
-
- public async terminate(emitStateEvent = true): Promise<void> {
- this.dispose();
-
- this.room.off(RoomStateEvent.Update, this.onRoomState);
- this.client.groupCallEventHandler!.groupCalls.delete(this.room.roomId);
- this.client.emit(GroupCallEventHandlerEvent.Ended, this);
- this.state = GroupCallState.Ended;
-
- if (emitStateEvent) {
- const existingStateEvent = this.room.currentState.getStateEvents(
- EventType.GroupCallPrefix,
- this.groupCallId,
- )!;
-
- await this.client.sendStateEvent(
- this.room.roomId,
- EventType.GroupCallPrefix,
- {
- ...existingStateEvent.getContent(),
- "m.terminated": GroupCallTerminationReason.CallEnded,
- },
- this.groupCallId,
- );
- }
- }
-
- /*
- * Local Usermedia
- */
-
- public isLocalVideoMuted(): boolean {
- if (this.localCallFeed) {
- return this.localCallFeed.isVideoMuted();
- }
-
- return true;
- }
-
- public isMicrophoneMuted(): boolean {
- if (this.localCallFeed) {
- return this.localCallFeed.isAudioMuted();
- }
-
- return true;
- }
-
- /**
- * Sets the mute state of the local participants's microphone.
- * @param muted - Whether to mute the microphone
- * @returns Whether muting/unmuting was successful
- */
- public async setMicrophoneMuted(muted: boolean): Promise<boolean> {
- // hasAudioDevice can block indefinitely if the window has lost focus,
- // and it doesn't make much sense to keep a device from being muted, so
- // we always allow muted = true changes to go through
- if (!muted && !(await this.client.getMediaHandler().hasAudioDevice())) {
- return false;
- }
-
- const sendUpdatesBefore = !muted && this.isPtt;
-
- // set a timer for the maximum transmit time on PTT calls
- if (this.isPtt) {
- // Set or clear the max transmit timer
- if (!muted && this.isMicrophoneMuted()) {
- this.transmitTimer = setTimeout(() => {
- this.setMicrophoneMuted(true);
- }, this.pttMaxTransmitTime);
- } else if (muted && !this.isMicrophoneMuted()) {
- if (this.transmitTimer !== null) clearTimeout(this.transmitTimer);
- this.transmitTimer = null;
- }
- }
-
- this.forEachCall((call) => call.localUsermediaFeed?.setAudioVideoMuted(muted, null));
-
- const sendUpdates = async (): Promise<void> => {
- const updates: Promise<void>[] = [];
- this.forEachCall((call) => updates.push(call.sendMetadataUpdate()));
-
- await Promise.all(updates).catch((e) =>
- logger.info(
- `GroupCall ${this.groupCallId} setMicrophoneMuted() failed to send some metadata updates`,
- e,
- ),
- );
- };
-
- if (sendUpdatesBefore) await sendUpdates();
-
- if (this.localCallFeed) {
- logger.log(
- `GroupCall ${this.groupCallId} setMicrophoneMuted() (streamId=${this.localCallFeed.stream.id}, muted=${muted})`,
- );
-
- // We needed this here to avoid an error in case user join a call without a device.
- // I can not use .then .catch functions because linter :-(
- try {
- if (!muted) {
- const stream = await this.client
- .getMediaHandler()
- .getUserMediaStream(true, !this.localCallFeed.isVideoMuted());
- if (stream === null) {
- // if case permission denied to get a stream stop this here
- /* istanbul ignore next */
- logger.log(
- `GroupCall ${this.groupCallId} setMicrophoneMuted() no device to receive local stream, muted=${muted}`,
- );
- return false;
- }
- }
- } catch (e) {
- /* istanbul ignore next */
- logger.log(
- `GroupCall ${this.groupCallId} setMicrophoneMuted() no device or permission to receive local stream, muted=${muted}`,
- );
- return false;
- }
-
- this.localCallFeed.setAudioVideoMuted(muted, null);
- // I don't believe its actually necessary to enable these tracks: they
- // are the one on the GroupCall's own CallFeed and are cloned before being
- // given to any of the actual calls, so these tracks don't actually go
- // anywhere. Let's do it anyway to avoid confusion.
- setTracksEnabled(this.localCallFeed.stream.getAudioTracks(), !muted);
- } else {
- logger.log(`GroupCall ${this.groupCallId} setMicrophoneMuted() no stream muted (muted=${muted})`);
- this.initWithAudioMuted = muted;
- }
-
- this.forEachCall((call) =>
- setTracksEnabled(call.localUsermediaFeed!.stream.getAudioTracks(), !muted && this.callExpected(call)),
- );
- this.emit(GroupCallEvent.LocalMuteStateChanged, muted, this.isLocalVideoMuted());
-
- if (!sendUpdatesBefore) await sendUpdates();
-
- return true;
- }
-
- /**
- * Sets the mute state of the local participants's video.
- * @param muted - Whether to mute the video
- * @returns Whether muting/unmuting was successful
- */
- public async setLocalVideoMuted(muted: boolean): Promise<boolean> {
- // hasAudioDevice can block indefinitely if the window has lost focus,
- // and it doesn't make much sense to keep a device from being muted, so
- // we always allow muted = true changes to go through
- if (!muted && !(await this.client.getMediaHandler().hasVideoDevice())) {
- return false;
- }
-
- if (this.localCallFeed) {
- /* istanbul ignore next */
- logger.log(
- `GroupCall ${this.groupCallId} setLocalVideoMuted() (stream=${this.localCallFeed.stream.id}, muted=${muted})`,
- );
-
- try {
- const stream = await this.client.getMediaHandler().getUserMediaStream(true, !muted);
- await this.updateLocalUsermediaStream(stream);
- this.localCallFeed.setAudioVideoMuted(null, muted);
- setTracksEnabled(this.localCallFeed.stream.getVideoTracks(), !muted);
- } catch (_) {
- // No permission to video device
- /* istanbul ignore next */
- logger.log(
- `GroupCall ${this.groupCallId} setLocalVideoMuted() no device or permission to receive local stream, muted=${muted}`,
- );
- return false;
- }
- } else {
- logger.log(`GroupCall ${this.groupCallId} setLocalVideoMuted() no stream muted (muted=${muted})`);
- this.initWithVideoMuted = muted;
- }
-
- const updates: Promise<unknown>[] = [];
- this.forEachCall((call) => updates.push(call.setLocalVideoMuted(muted)));
- await Promise.all(updates);
-
- // We setTracksEnabled again, independently from the call doing it
- // internally, since we might not be expecting the call
- this.forEachCall((call) =>
- setTracksEnabled(call.localUsermediaFeed!.stream.getVideoTracks(), !muted && this.callExpected(call)),
- );
-
- this.emit(GroupCallEvent.LocalMuteStateChanged, this.isMicrophoneMuted(), muted);
-
- return true;
- }
-
- public async setScreensharingEnabled(enabled: boolean, opts: IScreensharingOpts = {}): Promise<boolean> {
- if (enabled === this.isScreensharing()) {
- return enabled;
- }
-
- if (enabled) {
- try {
- logger.log(
- `GroupCall ${this.groupCallId} setScreensharingEnabled() is asking for screensharing permissions`,
- );
- const stream = await this.client.getMediaHandler().getScreensharingStream(opts);
-
- for (const track of stream.getTracks()) {
- const onTrackEnded = (): void => {
- this.setScreensharingEnabled(false);
- track.removeEventListener("ended", onTrackEnded);
- };
-
- track.addEventListener("ended", onTrackEnded);
- }
-
- logger.log(
- `GroupCall ${this.groupCallId} setScreensharingEnabled() granted screensharing permissions. Setting screensharing enabled on all calls`,
- );
-
- this.localDesktopCapturerSourceId = opts.desktopCapturerSourceId;
- this.localScreenshareFeed = new CallFeed({
- client: this.client,
- roomId: this.room.roomId,
- userId: this.client.getUserId()!,
- deviceId: this.client.getDeviceId()!,
- stream,
- purpose: SDPStreamMetadataPurpose.Screenshare,
- audioMuted: false,
- videoMuted: false,
- });
- this.addScreenshareFeed(this.localScreenshareFeed);
-
- this.emit(
- GroupCallEvent.LocalScreenshareStateChanged,
- true,
- this.localScreenshareFeed,
- this.localDesktopCapturerSourceId,
- );
-
- // TODO: handle errors
- this.forEachCall((call) => call.pushLocalFeed(this.localScreenshareFeed!.clone()));
-
- return true;
- } catch (error) {
- if (opts.throwOnFail) throw error;
- logger.error(
- `GroupCall ${this.groupCallId} setScreensharingEnabled() enabling screensharing error`,
- error,
- );
- this.emit(
- GroupCallEvent.Error,
- new GroupCallError(
- GroupCallErrorCode.NoUserMedia,
- "Failed to get screen-sharing stream: ",
- error as Error,
- ),
- );
- return false;
- }
- } else {
- this.forEachCall((call) => {
- if (call.localScreensharingFeed) call.removeLocalFeed(call.localScreensharingFeed);
- });
- this.client.getMediaHandler().stopScreensharingStream(this.localScreenshareFeed!.stream);
- this.removeScreenshareFeed(this.localScreenshareFeed!);
- this.localScreenshareFeed = undefined;
- this.localDesktopCapturerSourceId = undefined;
- this.emit(GroupCallEvent.LocalScreenshareStateChanged, false, undefined, undefined);
- return false;
- }
- }
-
- public isScreensharing(): boolean {
- return !!this.localScreenshareFeed;
- }
-
- /*
- * Call Setup
- *
- * There are two different paths for calls to be created:
- * 1. Incoming calls triggered by the Call.incoming event.
- * 2. Outgoing calls to the initial members of a room or new members
- * as they are observed by the RoomState.members event.
- */
-
- private onIncomingCall = (newCall: MatrixCall): void => {
- // The incoming calls may be for another room, which we will ignore.
- if (newCall.roomId !== this.room.roomId) {
- return;
- }
-
- if (newCall.state !== CallState.Ringing) {
- logger.warn(
- `GroupCall ${this.groupCallId} onIncomingCall() incoming call no longer in ringing state - ignoring`,
- );
- return;
- }
-
- if (!newCall.groupCallId || newCall.groupCallId !== this.groupCallId) {
- logger.log(
- `GroupCall ${this.groupCallId} onIncomingCall() ignored because it doesn't match the current group call`,
- );
- newCall.reject();
- return;
- }
-
- const opponentUserId = newCall.getOpponentMember()?.userId;
- if (opponentUserId === undefined) {
- logger.warn(`GroupCall ${this.groupCallId} onIncomingCall() incoming call with no member - ignoring`);
- return;
- }
-
- const deviceMap = this.calls.get(opponentUserId) ?? new Map<string, MatrixCall>();
- const prevCall = deviceMap.get(newCall.getOpponentDeviceId()!);
-
- if (prevCall?.callId === newCall.callId) return;
-
- logger.log(
- `GroupCall ${this.groupCallId} onIncomingCall() incoming call (userId=${opponentUserId}, callId=${newCall.callId})`,
- );
-
- if (prevCall) prevCall.hangup(CallErrorCode.Replaced, false);
-
- this.initCall(newCall);
-
- const feeds = this.getLocalFeeds().map((feed) => feed.clone());
- if (!this.callExpected(newCall)) {
- // Disable our tracks for users not explicitly participating in the
- // call but trying to receive the feeds
- for (const feed of feeds) {
- setTracksEnabled(feed.stream.getAudioTracks(), false);
- setTracksEnabled(feed.stream.getVideoTracks(), false);
- }
- }
- newCall.answerWithCallFeeds(feeds);
-
- deviceMap.set(newCall.getOpponentDeviceId()!, newCall);
- this.calls.set(opponentUserId, deviceMap);
- this.emit(GroupCallEvent.CallsChanged, this.calls);
- };
-
- /**
- * Determines whether a given participant expects us to call them (versus
- * them calling us).
- * @param userId - The participant's user ID.
- * @param deviceId - The participant's device ID.
- * @returns Whether we need to place an outgoing call to the participant.
- */
- private wantsOutgoingCall(userId: string, deviceId: string): boolean {
- const localUserId = this.client.getUserId()!;
- const localDeviceId = this.client.getDeviceId()!;
- return (
- // If a user's ID is less than our own, they'll call us
- userId >= localUserId &&
- // If this is another one of our devices, compare device IDs to tell whether it'll call us
- (userId !== localUserId || deviceId > localDeviceId)
- );
- }
-
- /**
- * Places calls to all participants that we're responsible for calling.
- */
- private placeOutgoingCalls(): void {
- let callsChanged = false;
-
- for (const [{ userId }, participantMap] of this.participants) {
- const callMap = this.calls.get(userId) ?? new Map<string, MatrixCall>();
-
- for (const [deviceId, participant] of participantMap) {
- const prevCall = callMap.get(deviceId);
-
- if (
- prevCall?.getOpponentSessionId() !== participant.sessionId &&
- this.wantsOutgoingCall(userId, deviceId)
- ) {
- callsChanged = true;
-
- if (prevCall !== undefined) {
- logger.debug(
- `GroupCall ${this.groupCallId} placeOutgoingCalls() replacing call (userId=${userId}, deviceId=${deviceId}, callId=${prevCall.callId})`,
- );
- prevCall.hangup(CallErrorCode.NewSession, false);
- }
-
- const newCall = createNewMatrixCall(this.client, this.room.roomId, {
- invitee: userId,
- opponentDeviceId: deviceId,
- opponentSessionId: participant.sessionId,
- groupCallId: this.groupCallId,
- });
-
- if (newCall === null) {
- logger.error(
- `GroupCall ${this.groupCallId} placeOutgoingCalls() failed to create call (userId=${userId}, device=${deviceId})`,
- );
- callMap.delete(deviceId);
- } else {
- this.initCall(newCall);
- callMap.set(deviceId, newCall);
-
- logger.debug(
- `GroupCall ${this.groupCallId} placeOutgoingCalls() placing call (userId=${userId}, deviceId=${deviceId}, sessionId=${participant.sessionId})`,
- );
-
- newCall
- .placeCallWithCallFeeds(
- this.getLocalFeeds().map((feed) => feed.clone()),
- participant.screensharing,
- )
- .then(() => {
- if (this.dataChannelsEnabled) {
- newCall.createDataChannel("datachannel", this.dataChannelOptions);
- }
- })
- .catch((e) => {
- logger.warn(
- `GroupCall ${this.groupCallId} placeOutgoingCalls() failed to place call (userId=${userId})`,
- e,
- );
-
- if (e instanceof CallError && e.code === GroupCallErrorCode.UnknownDevice) {
- this.emit(GroupCallEvent.Error, e);
- } else {
- this.emit(
- GroupCallEvent.Error,
- new GroupCallError(
- GroupCallErrorCode.PlaceCallFailed,
- `Failed to place call to ${userId}`,
- ),
- );
- }
-
- newCall.hangup(CallErrorCode.SignallingFailed, false);
- if (callMap.get(deviceId) === newCall) callMap.delete(deviceId);
- });
- }
- }
- }
-
- if (callMap.size > 0) {
- this.calls.set(userId, callMap);
- } else {
- this.calls.delete(userId);
- }
- }
-
- if (callsChanged) this.emit(GroupCallEvent.CallsChanged, this.calls);
- }
-
- /*
- * Room Member State
- */
-
- private getMemberStateEvents(): MatrixEvent[];
- private getMemberStateEvents(userId: string): MatrixEvent | null;
- private getMemberStateEvents(userId?: string): MatrixEvent[] | MatrixEvent | null {
- return userId === undefined
- ? this.room.currentState.getStateEvents(EventType.GroupCallMemberPrefix)
- : this.room.currentState.getStateEvents(EventType.GroupCallMemberPrefix, userId);
- }
-
- private onRetryCallLoop = (): void => {
- let needsRetry = false;
-
- for (const [{ userId }, participantMap] of this.participants) {
- const callMap = this.calls.get(userId);
- let retriesMap = this.retryCallCounts.get(userId);
-
- for (const [deviceId, participant] of participantMap) {
- const call = callMap?.get(deviceId);
- const retries = retriesMap?.get(deviceId) ?? 0;
-
- if (
- call?.getOpponentSessionId() !== participant.sessionId &&
- this.wantsOutgoingCall(userId, deviceId) &&
- retries < 3
- ) {
- if (retriesMap === undefined) {
- retriesMap = new Map();
- this.retryCallCounts.set(userId, retriesMap);
- }
- retriesMap.set(deviceId, retries + 1);
- needsRetry = true;
- }
- }
- }
-
- if (needsRetry) this.placeOutgoingCalls();
- };
-
- private initCall(call: MatrixCall): void {
- const opponentMemberId = getCallUserId(call);
-
- if (!opponentMemberId) {
- throw new Error("Cannot init call without user id");
- }
-
- const onCallFeedsChanged = (): void => this.onCallFeedsChanged(call);
- const onCallStateChanged = (state: CallState, oldState?: CallState): void =>
- this.onCallStateChanged(call, state, oldState);
- const onCallHangup = this.onCallHangup;
- const onCallReplaced = (newCall: MatrixCall): void => this.onCallReplaced(call, newCall);
-
- let deviceMap = this.callHandlers.get(opponentMemberId);
- if (deviceMap === undefined) {
- deviceMap = new Map();
- this.callHandlers.set(opponentMemberId, deviceMap);
- }
-
- deviceMap.set(call.getOpponentDeviceId()!, {
- onCallFeedsChanged,
- onCallStateChanged,
- onCallHangup,
- onCallReplaced,
- });
-
- call.on(CallEvent.FeedsChanged, onCallFeedsChanged);
- call.on(CallEvent.State, onCallStateChanged);
- call.on(CallEvent.Hangup, onCallHangup);
- call.on(CallEvent.Replaced, onCallReplaced);
-
- call.isPtt = this.isPtt;
-
- this.reEmitter.reEmit(call, Object.values(CallEvent));
-
- call.initStats(this.stats);
-
- onCallFeedsChanged();
- }
-
- private disposeCall(call: MatrixCall, hangupReason: CallErrorCode): void {
- const opponentMemberId = getCallUserId(call);
- const opponentDeviceId = call.getOpponentDeviceId()!;
-
- if (!opponentMemberId) {
- throw new Error("Cannot dispose call without user id");
- }
-
- const deviceMap = this.callHandlers.get(opponentMemberId)!;
- const { onCallFeedsChanged, onCallStateChanged, onCallHangup, onCallReplaced } =
- deviceMap.get(opponentDeviceId)!;
-
- call.removeListener(CallEvent.FeedsChanged, onCallFeedsChanged);
- call.removeListener(CallEvent.State, onCallStateChanged);
- call.removeListener(CallEvent.Hangup, onCallHangup);
- call.removeListener(CallEvent.Replaced, onCallReplaced);
-
- deviceMap.delete(opponentMemberId);
- if (deviceMap.size === 0) this.callHandlers.delete(opponentMemberId);
-
- if (call.hangupReason === CallErrorCode.Replaced) {
- return;
- }
-
- const usermediaFeed = this.getUserMediaFeed(opponentMemberId, opponentDeviceId);
-
- if (usermediaFeed) {
- this.removeUserMediaFeed(usermediaFeed);
- }
-
- const screenshareFeed = this.getScreenshareFeed(opponentMemberId, opponentDeviceId);
-
- if (screenshareFeed) {
- this.removeScreenshareFeed(screenshareFeed);
- }
- }
-
- private onCallFeedsChanged = (call: MatrixCall): void => {
- const opponentMemberId = getCallUserId(call);
- const opponentDeviceId = call.getOpponentDeviceId()!;
-
- if (!opponentMemberId) {
- throw new Error("Cannot change call feeds without user id");
- }
-
- const currentUserMediaFeed = this.getUserMediaFeed(opponentMemberId, opponentDeviceId);
- const remoteUsermediaFeed = call.remoteUsermediaFeed;
- const remoteFeedChanged = remoteUsermediaFeed !== currentUserMediaFeed;
-
- if (remoteFeedChanged) {
- if (!currentUserMediaFeed && remoteUsermediaFeed) {
- this.addUserMediaFeed(remoteUsermediaFeed);
- } else if (currentUserMediaFeed && remoteUsermediaFeed) {
- this.replaceUserMediaFeed(currentUserMediaFeed, remoteUsermediaFeed);
- } else if (currentUserMediaFeed && !remoteUsermediaFeed) {
- this.removeUserMediaFeed(currentUserMediaFeed);
- }
- }
-
- const currentScreenshareFeed = this.getScreenshareFeed(opponentMemberId, opponentDeviceId);
- const remoteScreensharingFeed = call.remoteScreensharingFeed;
- const remoteScreenshareFeedChanged = remoteScreensharingFeed !== currentScreenshareFeed;
-
- if (remoteScreenshareFeedChanged) {
- if (!currentScreenshareFeed && remoteScreensharingFeed) {
- this.addScreenshareFeed(remoteScreensharingFeed);
- } else if (currentScreenshareFeed && remoteScreensharingFeed) {
- this.replaceScreenshareFeed(currentScreenshareFeed, remoteScreensharingFeed);
- } else if (currentScreenshareFeed && !remoteScreensharingFeed) {
- this.removeScreenshareFeed(currentScreenshareFeed);
- }
- }
- };
-
- private onCallStateChanged = (call: MatrixCall, state: CallState, _oldState: CallState | undefined): void => {
- if (state === CallState.Ended) return;
-
- const audioMuted = this.localCallFeed!.isAudioMuted();
-
- if (call.localUsermediaStream && call.isMicrophoneMuted() !== audioMuted) {
- call.setMicrophoneMuted(audioMuted);
- }
-
- const videoMuted = this.localCallFeed!.isVideoMuted();
-
- if (call.localUsermediaStream && call.isLocalVideoMuted() !== videoMuted) {
- call.setLocalVideoMuted(videoMuted);
- }
-
- const opponentUserId = call.getOpponentMember()?.userId;
- if (state === CallState.Connected && opponentUserId) {
- const retriesMap = this.retryCallCounts.get(opponentUserId);
- retriesMap?.delete(call.getOpponentDeviceId()!);
- if (retriesMap?.size === 0) this.retryCallCounts.delete(opponentUserId);
- }
- };
-
- private onCallHangup = (call: MatrixCall): void => {
- if (call.hangupReason === CallErrorCode.Replaced) return;
-
- const opponentUserId = call.getOpponentMember()?.userId ?? this.room.getMember(call.invitee!)!.userId;
- const deviceMap = this.calls.get(opponentUserId);
-
- // Sanity check that this call is in fact in the map
- if (deviceMap?.get(call.getOpponentDeviceId()!) === call) {
- this.disposeCall(call, call.hangupReason as CallErrorCode);
- deviceMap.delete(call.getOpponentDeviceId()!);
- if (deviceMap.size === 0) this.calls.delete(opponentUserId);
- this.emit(GroupCallEvent.CallsChanged, this.calls);
- }
- };
-
- private onCallReplaced = (prevCall: MatrixCall, newCall: MatrixCall): void => {
- const opponentUserId = prevCall.getOpponentMember()!.userId;
-
- let deviceMap = this.calls.get(opponentUserId);
- if (deviceMap === undefined) {
- deviceMap = new Map();
- this.calls.set(opponentUserId, deviceMap);
- }
-
- prevCall.hangup(CallErrorCode.Replaced, false);
- this.initCall(newCall);
- deviceMap.set(prevCall.getOpponentDeviceId()!, newCall);
- this.emit(GroupCallEvent.CallsChanged, this.calls);
- };
-
- /*
- * UserMedia CallFeed Event Handlers
- */
-
- public getUserMediaFeed(userId: string, deviceId: string): CallFeed | undefined {
- return this.userMediaFeeds.find((f) => f.userId === userId && f.deviceId! === deviceId);
- }
-
- private addUserMediaFeed(callFeed: CallFeed): void {
- this.userMediaFeeds.push(callFeed);
- callFeed.measureVolumeActivity(true);
- this.emit(GroupCallEvent.UserMediaFeedsChanged, this.userMediaFeeds);
- }
-
- private replaceUserMediaFeed(existingFeed: CallFeed, replacementFeed: CallFeed): void {
- const feedIndex = this.userMediaFeeds.findIndex(
- (f) => f.userId === existingFeed.userId && f.deviceId! === existingFeed.deviceId,
- );
-
- if (feedIndex === -1) {
- throw new Error("Couldn't find user media feed to replace");
- }
-
- this.userMediaFeeds.splice(feedIndex, 1, replacementFeed);
-
- existingFeed.dispose();
- replacementFeed.measureVolumeActivity(true);
- this.emit(GroupCallEvent.UserMediaFeedsChanged, this.userMediaFeeds);
- }
-
- private removeUserMediaFeed(callFeed: CallFeed): void {
- const feedIndex = this.userMediaFeeds.findIndex(
- (f) => f.userId === callFeed.userId && f.deviceId! === callFeed.deviceId,
- );
-
- if (feedIndex === -1) {
- throw new Error("Couldn't find user media feed to remove");
- }
-
- this.userMediaFeeds.splice(feedIndex, 1);
-
- callFeed.dispose();
- this.emit(GroupCallEvent.UserMediaFeedsChanged, this.userMediaFeeds);
-
- if (this.activeSpeaker === callFeed) {
- this.activeSpeaker = this.userMediaFeeds[0];
- this.emit(GroupCallEvent.ActiveSpeakerChanged, this.activeSpeaker);
- }
- }
-
- private onActiveSpeakerLoop = (): void => {
- let topAvg: number | undefined = undefined;
- let nextActiveSpeaker: CallFeed | undefined = undefined;
-
- for (const callFeed of this.userMediaFeeds) {
- if (callFeed.isLocal() && this.userMediaFeeds.length > 1) continue;
-
- const total = callFeed.speakingVolumeSamples.reduce(
- (acc, volume) => acc + Math.max(volume, SPEAKING_THRESHOLD),
- );
- const avg = total / callFeed.speakingVolumeSamples.length;
-
- if (!topAvg || avg > topAvg) {
- topAvg = avg;
- nextActiveSpeaker = callFeed;
- }
- }
-
- if (nextActiveSpeaker && this.activeSpeaker !== nextActiveSpeaker && topAvg && topAvg > SPEAKING_THRESHOLD) {
- this.activeSpeaker = nextActiveSpeaker;
- this.emit(GroupCallEvent.ActiveSpeakerChanged, this.activeSpeaker);
- }
- };
-
- /*
- * Screenshare Call Feed Event Handlers
- */
-
- public getScreenshareFeed(userId: string, deviceId: string): CallFeed | undefined {
- return this.screenshareFeeds.find((f) => f.userId === userId && f.deviceId! === deviceId);
- }
-
- private addScreenshareFeed(callFeed: CallFeed): void {
- this.screenshareFeeds.push(callFeed);
- this.emit(GroupCallEvent.ScreenshareFeedsChanged, this.screenshareFeeds);
- }
-
- private replaceScreenshareFeed(existingFeed: CallFeed, replacementFeed: CallFeed): void {
- const feedIndex = this.screenshareFeeds.findIndex(
- (f) => f.userId === existingFeed.userId && f.deviceId! === existingFeed.deviceId,
- );
-
- if (feedIndex === -1) {
- throw new Error("Couldn't find screenshare feed to replace");
- }
-
- this.screenshareFeeds.splice(feedIndex, 1, replacementFeed);
-
- existingFeed.dispose();
- this.emit(GroupCallEvent.ScreenshareFeedsChanged, this.screenshareFeeds);
- }
-
- private removeScreenshareFeed(callFeed: CallFeed): void {
- const feedIndex = this.screenshareFeeds.findIndex(
- (f) => f.userId === callFeed.userId && f.deviceId! === callFeed.deviceId,
- );
-
- if (feedIndex === -1) {
- throw new Error("Couldn't find screenshare feed to remove");
- }
-
- this.screenshareFeeds.splice(feedIndex, 1);
-
- callFeed.dispose();
- this.emit(GroupCallEvent.ScreenshareFeedsChanged, this.screenshareFeeds);
- }
-
- /**
- * Recalculates and updates the participant map to match the room state.
- */
- private updateParticipants(): void {
- const localMember = this.room.getMember(this.client.getUserId()!)!;
- if (!localMember) {
- // The client hasn't fetched enough of the room state to get our own member
- // event. This probably shouldn't happen, but sanity check & exit for now.
- logger.warn(
- `GroupCall ${this.groupCallId} updateParticipants() tried to update participants before local room member is available`,
- );
- return;
- }
-
- if (this.participantsExpirationTimer !== null) {
- clearTimeout(this.participantsExpirationTimer);
- this.participantsExpirationTimer = null;
- }
-
- if (this.state === GroupCallState.Ended) {
- this.participants = new Map();
- return;
- }
-
- const participants = new Map<RoomMember, Map<string, ParticipantState>>();
- const now = Date.now();
- const entered = this.state === GroupCallState.Entered || this.enteredViaAnotherSession;
- let nextExpiration = Infinity;
-
- for (const e of this.getMemberStateEvents()) {
- const member = this.room.getMember(e.getStateKey()!);
- const content = e.getContent<Record<any, unknown>>();
- const calls: Record<any, unknown>[] = Array.isArray(content["m.calls"]) ? content["m.calls"] : [];
- const call = calls.find((call) => call["m.call_id"] === this.groupCallId);
- const devices: Record<any, unknown>[] = Array.isArray(call?.["m.devices"]) ? call!["m.devices"] : [];
-
- // Filter out invalid and expired devices
- let validDevices = devices.filter(
- (d) =>
- typeof d.device_id === "string" &&
- typeof d.session_id === "string" &&
- typeof d.expires_ts === "number" &&
- d.expires_ts > now &&
- Array.isArray(d.feeds),
- ) as unknown as IGroupCallRoomMemberDevice[];
-
- // Apply local echo for the unentered case
- if (!entered && member?.userId === this.client.getUserId()!) {
- validDevices = validDevices.filter((d) => d.device_id !== this.client.getDeviceId()!);
- }
-
- // Must have a connected device and be joined to the room
- if (validDevices.length > 0 && member?.membership === "join") {
- const deviceMap = new Map<string, ParticipantState>();
- participants.set(member, deviceMap);
-
- for (const d of validDevices) {
- deviceMap.set(d.device_id, {
- sessionId: d.session_id,
- screensharing: d.feeds.some((f) => f.purpose === SDPStreamMetadataPurpose.Screenshare),
- });
- if (d.expires_ts < nextExpiration) nextExpiration = d.expires_ts;
- }
- }
- }
-
- // Apply local echo for the entered case
- if (entered) {
- let deviceMap = participants.get(localMember);
- if (deviceMap === undefined) {
- deviceMap = new Map();
- participants.set(localMember, deviceMap);
- }
-
- if (!deviceMap.has(this.client.getDeviceId()!)) {
- deviceMap.set(this.client.getDeviceId()!, {
- sessionId: this.client.getSessionId(),
- screensharing: this.getLocalFeeds().some((f) => f.purpose === SDPStreamMetadataPurpose.Screenshare),
- });
- }
- }
-
- this.participants = participants;
- if (nextExpiration < Infinity) {
- this.participantsExpirationTimer = setTimeout(() => this.updateParticipants(), nextExpiration - now);
- }
- }
-
- /**
- * Updates the local user's member state with the devices returned by the given function.
- * @param fn - A function from the current devices to the new devices. If it
- * returns null, the update will be skipped.
- * @param keepAlive - Whether the request should outlive the window.
- */
- private async updateDevices(
- fn: (devices: IGroupCallRoomMemberDevice[]) => IGroupCallRoomMemberDevice[] | null,
- keepAlive = false,
- ): Promise<void> {
- const now = Date.now();
- const localUserId = this.client.getUserId()!;
-
- const event = this.getMemberStateEvents(localUserId);
- const content = event?.getContent<Record<any, unknown>>() ?? {};
- const calls: Record<any, unknown>[] = Array.isArray(content["m.calls"]) ? content["m.calls"] : [];
-
- let call: Record<any, unknown> | null = null;
- const otherCalls: Record<any, unknown>[] = [];
- for (const c of calls) {
- if (c["m.call_id"] === this.groupCallId) {
- call = c;
- } else {
- otherCalls.push(c);
- }
- }
- if (call === null) call = {};
-
- const devices: Record<any, unknown>[] = Array.isArray(call["m.devices"]) ? call["m.devices"] : [];
-
- // Filter out invalid and expired devices
- const validDevices = devices.filter(
- (d) =>
- typeof d.device_id === "string" &&
- typeof d.session_id === "string" &&
- typeof d.expires_ts === "number" &&
- d.expires_ts > now &&
- Array.isArray(d.feeds),
- ) as unknown as IGroupCallRoomMemberDevice[];
-
- const newDevices = fn(validDevices);
- if (newDevices === null) return;
-
- const newCalls = [...(otherCalls as unknown as IGroupCallRoomMemberCallState[])];
- if (newDevices.length > 0) {
- newCalls.push({
- ...call,
- "m.call_id": this.groupCallId,
- "m.devices": newDevices,
- });
- }
-
- const newContent: IGroupCallRoomMemberState = { "m.calls": newCalls };
-
- await this.client.sendStateEvent(this.room.roomId, EventType.GroupCallMemberPrefix, newContent, localUserId, {
- keepAlive,
- });
- }
-
- private async addDeviceToMemberState(): Promise<void> {
- await this.updateDevices((devices) => [
- ...devices.filter((d) => d.device_id !== this.client.getDeviceId()!),
- {
- device_id: this.client.getDeviceId()!,
- session_id: this.client.getSessionId(),
- expires_ts: Date.now() + DEVICE_TIMEOUT,
- feeds: this.getLocalFeeds().map((feed) => ({ purpose: feed.purpose })),
- // TODO: Add data channels
- },
- ]);
- }
-
- private async updateMemberState(): Promise<void> {
- // Clear the old update interval before proceeding
- if (this.resendMemberStateTimer !== null) {
- clearInterval(this.resendMemberStateTimer);
- this.resendMemberStateTimer = null;
- }
-
- if (this.state === GroupCallState.Entered) {
- // Add the local device
- await this.addDeviceToMemberState();
-
- // Resend the state event every so often so it doesn't become stale
- this.resendMemberStateTimer = setInterval(async () => {
- logger.log(`GroupCall ${this.groupCallId} updateMemberState() resending call member state"`);
- try {
- await this.addDeviceToMemberState();
- } catch (e) {
- logger.error(
- `GroupCall ${this.groupCallId} updateMemberState() failed to resend call member state`,
- e,
- );
- }
- }, (DEVICE_TIMEOUT * 3) / 4);
- } else {
- // Remove the local device
- await this.updateDevices(
- (devices) => devices.filter((d) => d.device_id !== this.client.getDeviceId()!),
- true,
- );
- }
- }
-
- /**
- * Cleans up our member state by filtering out logged out devices, inactive
- * devices, and our own device (if we know we haven't entered).
- */
- public async cleanMemberState(): Promise<void> {
- const { devices: myDevices } = await this.client.getDevices();
- const deviceMap = new Map<string, IMyDevice>(myDevices.map((d) => [d.device_id, d]));
-
- // updateDevices takes care of filtering out inactive devices for us
- await this.updateDevices((devices) => {
- const newDevices = devices.filter((d) => {
- const device = deviceMap.get(d.device_id);
- return (
- device?.last_seen_ts !== undefined &&
- !(
- d.device_id === this.client.getDeviceId()! &&
- this.state !== GroupCallState.Entered &&
- !this.enteredViaAnotherSession
- )
- );
- });
-
- // Skip the update if the devices are unchanged
- return newDevices.length === devices.length ? null : newDevices;
- });
- }
-
- private onRoomState = (): void => this.updateParticipants();
-
- private onParticipantsChanged = (): void => {
- // Re-run setTracksEnabled on all calls, so that participants that just
- // left get denied access to our media, and participants that just
- // joined get granted access
- this.forEachCall((call) => {
- const expected = this.callExpected(call);
- for (const feed of call.getLocalFeeds()) {
- setTracksEnabled(feed.stream.getAudioTracks(), !feed.isAudioMuted() && expected);
- setTracksEnabled(feed.stream.getVideoTracks(), !feed.isVideoMuted() && expected);
- }
- });
-
- if (this.state === GroupCallState.Entered) this.placeOutgoingCalls();
- };
-
- private onStateChanged = (newState: GroupCallState, oldState: GroupCallState): void => {
- if (
- newState === GroupCallState.Entered ||
- oldState === GroupCallState.Entered ||
- newState === GroupCallState.Ended
- ) {
- // We either entered, left, or ended the call
- this.updateParticipants();
- this.updateMemberState().catch((e) =>
- logger.error(
- `GroupCall ${this.groupCallId} onStateChanged() failed to update member state devices"`,
- e,
- ),
- );
- }
- };
-
- private onLocalFeedsChanged = (): void => {
- if (this.state === GroupCallState.Entered) {
- this.updateMemberState().catch((e) =>
- logger.error(
- `GroupCall ${this.groupCallId} onLocalFeedsChanged() failed to update member state feeds`,
- e,
- ),
- );
- }
- };
-
- public getGroupCallStats(): GroupCallStats {
- return this.stats;
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/groupCallEventHandler.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/groupCallEventHandler.ts
deleted file mode 100644
index 08487bd..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/groupCallEventHandler.ts
+++ /dev/null
@@ -1,232 +0,0 @@
-/*
-Copyright 2021 Š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.
-*/
-
-import { MatrixEvent } from "../models/event";
-import { MatrixClient, ClientEvent } from "../client";
-import { GroupCall, GroupCallIntent, GroupCallType, IGroupCallDataChannelOptions } from "./groupCall";
-import { Room } from "../models/room";
-import { RoomState, RoomStateEvent } from "../models/room-state";
-import { RoomMember } from "../models/room-member";
-import { logger } from "../logger";
-import { EventType } from "../@types/event";
-import { SyncState } from "../sync";
-
-export enum GroupCallEventHandlerEvent {
- Incoming = "GroupCall.incoming",
- Outgoing = "GroupCall.outgoing",
- Ended = "GroupCall.ended",
- Participants = "GroupCall.participants",
-}
-
-export type GroupCallEventHandlerEventHandlerMap = {
- [GroupCallEventHandlerEvent.Incoming]: (call: GroupCall) => void;
- [GroupCallEventHandlerEvent.Outgoing]: (call: GroupCall) => void;
- [GroupCallEventHandlerEvent.Ended]: (call: GroupCall) => void;
- [GroupCallEventHandlerEvent.Participants]: (participants: RoomMember[], call: GroupCall) => void;
-};
-
-interface RoomDeferred {
- prom: Promise<void>;
- resolve?: () => void;
-}
-
-export class GroupCallEventHandler {
- public groupCalls = new Map<string, GroupCall>(); // roomId -> GroupCall
-
- // All rooms we know about and whether we've seen a 'Room' event
- // for them. The promise will be fulfilled once we've processed that
- // event which means we're "up to date" on what calls are in a room
- // and get
- private roomDeferreds = new Map<string, RoomDeferred>();
-
- public constructor(private client: MatrixClient) {}
-
- public async start(): Promise<void> {
- // We wait until the client has started syncing for real.
- // This is because we only support one call at a time, and want
- // the latest. We therefore want the latest state of the room before
- // we create a group call for the room so we can be fairly sure that
- // the group call we create is really the latest one.
- if (this.client.getSyncState() !== SyncState.Syncing) {
- logger.debug("GroupCallEventHandler start() waiting for client to start syncing");
- await new Promise<void>((resolve) => {
- const onSync = (): void => {
- if (this.client.getSyncState() === SyncState.Syncing) {
- this.client.off(ClientEvent.Sync, onSync);
- return resolve();
- }
- };
- this.client.on(ClientEvent.Sync, onSync);
- });
- }
-
- const rooms = this.client.getRooms();
-
- for (const room of rooms) {
- this.createGroupCallForRoom(room);
- }
-
- this.client.on(ClientEvent.Room, this.onRoomsChanged);
- this.client.on(RoomStateEvent.Events, this.onRoomStateChanged);
- }
-
- public stop(): void {
- this.client.removeListener(RoomStateEvent.Events, this.onRoomStateChanged);
- }
-
- private getRoomDeferred(roomId: string): RoomDeferred {
- let deferred = this.roomDeferreds.get(roomId);
- if (deferred === undefined) {
- let resolveFunc: () => void;
- deferred = {
- prom: new Promise<void>((resolve) => {
- resolveFunc = resolve;
- }),
- };
- deferred.resolve = resolveFunc!;
- this.roomDeferreds.set(roomId, deferred);
- }
-
- return deferred;
- }
-
- public waitUntilRoomReadyForGroupCalls(roomId: string): Promise<void> {
- return this.getRoomDeferred(roomId).prom;
- }
-
- public getGroupCallById(groupCallId: string): GroupCall | undefined {
- return [...this.groupCalls.values()].find((groupCall) => groupCall.groupCallId === groupCallId);
- }
-
- private createGroupCallForRoom(room: Room): void {
- const callEvents = room.currentState.getStateEvents(EventType.GroupCallPrefix);
- const sortedCallEvents = callEvents.sort((a, b) => b.getTs() - a.getTs());
-
- for (const callEvent of sortedCallEvents) {
- const content = callEvent.getContent();
-
- if (content["m.terminated"] || callEvent.isRedacted()) {
- continue;
- }
-
- logger.debug(
- `GroupCallEventHandler createGroupCallForRoom() choosing group call from possible calls (stateKey=${callEvent.getStateKey()}, ts=${callEvent.getTs()}, roomId=${
- room.roomId
- }, numOfPossibleCalls=${callEvents.length})`,
- );
-
- this.createGroupCallFromRoomStateEvent(callEvent);
- break;
- }
-
- logger.info(`GroupCallEventHandler createGroupCallForRoom() processed room (roomId=${room.roomId})`);
- this.getRoomDeferred(room.roomId).resolve!();
- }
-
- private createGroupCallFromRoomStateEvent(event: MatrixEvent): GroupCall | undefined {
- const roomId = event.getRoomId();
- const content = event.getContent();
-
- const room = this.client.getRoom(roomId);
-
- if (!room) {
- logger.warn(
- `GroupCallEventHandler createGroupCallFromRoomStateEvent() couldn't find room for call (roomId=${roomId})`,
- );
- return;
- }
-
- const groupCallId = event.getStateKey();
-
- const callType = content["m.type"];
-
- if (!Object.values(GroupCallType).includes(callType)) {
- logger.warn(
- `GroupCallEventHandler createGroupCallFromRoomStateEvent() received invalid call type (type=${callType}, roomId=${roomId})`,
- );
- return;
- }
-
- const callIntent = content["m.intent"];
-
- if (!Object.values(GroupCallIntent).includes(callIntent)) {
- logger.warn(`Received invalid group call intent (type=${callType}, roomId=${roomId})`);
- return;
- }
-
- const isPtt = Boolean(content["io.element.ptt"]);
-
- let dataChannelOptions: IGroupCallDataChannelOptions | undefined;
-
- if (content?.dataChannelsEnabled && content?.dataChannelOptions) {
- // Pull out just the dataChannelOptions we want to support.
- const { ordered, maxPacketLifeTime, maxRetransmits, protocol } = content.dataChannelOptions;
- dataChannelOptions = { ordered, maxPacketLifeTime, maxRetransmits, protocol };
- }
-
- const groupCall = new GroupCall(
- this.client,
- room,
- callType,
- isPtt,
- callIntent,
- groupCallId,
- // Because without Media section a WebRTC connection is not possible, so need a RTCDataChannel to set up a
- // no media WebRTC connection anyway.
- content?.dataChannelsEnabled || this.client.isVoipWithNoMediaAllowed,
- dataChannelOptions,
- this.client.isVoipWithNoMediaAllowed,
- );
-
- this.groupCalls.set(room.roomId, groupCall);
- this.client.emit(GroupCallEventHandlerEvent.Incoming, groupCall);
-
- return groupCall;
- }
-
- private onRoomsChanged = (room: Room): void => {
- this.createGroupCallForRoom(room);
- };
-
- private onRoomStateChanged = (event: MatrixEvent, state: RoomState): void => {
- const eventType = event.getType();
-
- if (eventType === EventType.GroupCallPrefix) {
- const groupCallId = event.getStateKey();
- const content = event.getContent();
-
- const currentGroupCall = this.groupCalls.get(state.roomId);
-
- if (!currentGroupCall && !content["m.terminated"] && !event.isRedacted()) {
- this.createGroupCallFromRoomStateEvent(event);
- } else if (currentGroupCall && currentGroupCall.groupCallId === groupCallId) {
- if (content["m.terminated"] || event.isRedacted()) {
- currentGroupCall.terminate(false);
- } else if (content["m.type"] !== currentGroupCall.type) {
- // TODO: Handle the callType changing when the room state changes
- logger.warn(
- `GroupCallEventHandler onRoomStateChanged() currently does not support changing type (roomId=${state.roomId})`,
- );
- }
- } else if (currentGroupCall && currentGroupCall.groupCallId !== groupCallId) {
- // TODO: Handle new group calls and multiple group calls
- logger.warn(
- `GroupCallEventHandler onRoomStateChanged() currently does not support multiple calls (roomId=${state.roomId})`,
- );
- }
- }
- };
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/mediaHandler.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/mediaHandler.ts
deleted file mode 100644
index 7f65835..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/mediaHandler.ts
+++ /dev/null
@@ -1,469 +0,0 @@
-/*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2017 New Vector Ltd
-Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
-Copyright 2021 - 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.
-*/
-
-import { TypedEventEmitter } from "../models/typed-event-emitter";
-import { GroupCallType, GroupCallState } from "../webrtc/groupCall";
-import { logger } from "../logger";
-import { MatrixClient } from "../client";
-
-export enum MediaHandlerEvent {
- LocalStreamsChanged = "local_streams_changed",
-}
-
-export type MediaHandlerEventHandlerMap = {
- [MediaHandlerEvent.LocalStreamsChanged]: () => void;
-};
-
-export interface IScreensharingOpts {
- desktopCapturerSourceId?: string;
- audio?: boolean;
- // For electron screen capture, there are very few options for detecting electron
- // apart from inspecting the user agent or just trying getDisplayMedia() and
- // catching the failure, so we do the latter - this flag tells the function to just
- // throw an error so we can catch it in this case, rather than logging and emitting.
- throwOnFail?: boolean;
-}
-
-export interface AudioSettings {
- autoGainControl: boolean;
- echoCancellation: boolean;
- noiseSuppression: boolean;
-}
-
-export class MediaHandler extends TypedEventEmitter<
- MediaHandlerEvent.LocalStreamsChanged,
- MediaHandlerEventHandlerMap
-> {
- private audioInput?: string;
- private audioSettings?: AudioSettings;
- private videoInput?: string;
- private localUserMediaStream?: MediaStream;
- public userMediaStreams: MediaStream[] = [];
- public screensharingStreams: MediaStream[] = [];
-
- // Promise chain to serialise calls to getMediaStream
- private getMediaStreamPromise?: Promise<MediaStream>;
-
- public constructor(private client: MatrixClient) {
- super();
- }
-
- public restoreMediaSettings(audioInput: string, videoInput: string): void {
- this.audioInput = audioInput;
- this.videoInput = videoInput;
- }
-
- /**
- * Set an audio input device to use for MatrixCalls
- * @param deviceId - the identifier for the device
- * undefined treated as unset
- */
- public async setAudioInput(deviceId: string): Promise<void> {
- logger.info(`MediaHandler setAudioInput() running (deviceId=${deviceId})`);
-
- if (this.audioInput === deviceId) return;
-
- this.audioInput = deviceId;
- await this.updateLocalUsermediaStreams();
- }
-
- /**
- * Set audio settings for MatrixCalls
- * @param opts - audio options to set
- */
- public async setAudioSettings(opts: AudioSettings): Promise<void> {
- logger.info(`MediaHandler setAudioSettings() running (opts=${JSON.stringify(opts)})`);
-
- this.audioSettings = Object.assign({}, opts) as AudioSettings;
- await this.updateLocalUsermediaStreams();
- }
-
- /**
- * Set a video input device to use for MatrixCalls
- * @param deviceId - the identifier for the device
- * undefined treated as unset
- */
- public async setVideoInput(deviceId: string): Promise<void> {
- logger.info(`MediaHandler setVideoInput() running (deviceId=${deviceId})`);
-
- if (this.videoInput === deviceId) return;
-
- this.videoInput = deviceId;
- await this.updateLocalUsermediaStreams();
- }
-
- /**
- * Set media input devices to use for MatrixCalls
- * @param audioInput - the identifier for the audio device
- * @param videoInput - the identifier for the video device
- * undefined treated as unset
- */
- public async setMediaInputs(audioInput: string, videoInput: string): Promise<void> {
- logger.log(`MediaHandler setMediaInputs() running (audioInput: ${audioInput} videoInput: ${videoInput})`);
- this.audioInput = audioInput;
- this.videoInput = videoInput;
- await this.updateLocalUsermediaStreams();
- }
-
- /*
- * Requests new usermedia streams and replace the old ones
- */
- public async updateLocalUsermediaStreams(): Promise<void> {
- if (this.userMediaStreams.length === 0) return;
-
- const callMediaStreamParams: Map<string, { audio: boolean; video: boolean }> = new Map();
- for (const call of this.client.callEventHandler!.calls.values()) {
- callMediaStreamParams.set(call.callId, {
- audio: call.hasLocalUserMediaAudioTrack,
- video: call.hasLocalUserMediaVideoTrack,
- });
- }
-
- for (const stream of this.userMediaStreams) {
- logger.log(`MediaHandler updateLocalUsermediaStreams() stopping all tracks (streamId=${stream.id})`);
- for (const track of stream.getTracks()) {
- track.stop();
- }
- }
-
- this.userMediaStreams = [];
- this.localUserMediaStream = undefined;
-
- for (const call of this.client.callEventHandler!.calls.values()) {
- if (call.callHasEnded() || !callMediaStreamParams.has(call.callId)) {
- continue;
- }
-
- const { audio, video } = callMediaStreamParams.get(call.callId)!;
-
- logger.log(
- `MediaHandler updateLocalUsermediaStreams() calling getUserMediaStream() (callId=${call.callId})`,
- );
- const stream = await this.getUserMediaStream(audio, video);
-
- if (call.callHasEnded()) {
- continue;
- }
-
- await call.updateLocalUsermediaStream(stream);
- }
-
- for (const groupCall of this.client.groupCallEventHandler!.groupCalls.values()) {
- if (!groupCall.localCallFeed) {
- continue;
- }
-
- logger.log(
- `MediaHandler updateLocalUsermediaStreams() calling getUserMediaStream() (groupCallId=${groupCall.groupCallId})`,
- );
- const stream = await this.getUserMediaStream(true, groupCall.type === GroupCallType.Video);
-
- if (groupCall.state === GroupCallState.Ended) {
- continue;
- }
-
- await groupCall.updateLocalUsermediaStream(stream);
- }
-
- this.emit(MediaHandlerEvent.LocalStreamsChanged);
- }
-
- public async hasAudioDevice(): Promise<boolean> {
- try {
- const devices = await navigator.mediaDevices.enumerateDevices();
- return devices.filter((device) => device.kind === "audioinput").length > 0;
- } catch (err) {
- logger.log(`MediaHandler hasAudioDevice() calling navigator.mediaDevices.enumerateDevices with error`, err);
- return false;
- }
- }
-
- public async hasVideoDevice(): Promise<boolean> {
- try {
- const devices = await navigator.mediaDevices.enumerateDevices();
- return devices.filter((device) => device.kind === "videoinput").length > 0;
- } catch (err) {
- logger.log(`MediaHandler hasVideoDevice() calling navigator.mediaDevices.enumerateDevices with error`, err);
- return false;
- }
- }
-
- /**
- * @param audio - should have an audio track
- * @param video - should have a video track
- * @param reusable - is allowed to be reused by the MediaHandler
- * @returns based on passed parameters
- */
- public async getUserMediaStream(audio: boolean, video: boolean, reusable = true): Promise<MediaStream> {
- // Serialise calls, othertwise we can't sensibly re-use the stream
- if (this.getMediaStreamPromise) {
- this.getMediaStreamPromise = this.getMediaStreamPromise.then(() => {
- return this.getUserMediaStreamInternal(audio, video, reusable);
- });
- } else {
- this.getMediaStreamPromise = this.getUserMediaStreamInternal(audio, video, reusable);
- }
-
- return this.getMediaStreamPromise;
- }
-
- private async getUserMediaStreamInternal(audio: boolean, video: boolean, reusable: boolean): Promise<MediaStream> {
- const shouldRequestAudio = audio && (await this.hasAudioDevice());
- const shouldRequestVideo = video && (await this.hasVideoDevice());
-
- let stream: MediaStream;
-
- let canReuseStream = true;
- if (this.localUserMediaStream) {
- // This figures out if we can reuse the current localUsermediaStream
- // based on whether or not the "mute state" (presence of tracks of a
- // given kind) matches what is being requested
- if (shouldRequestAudio !== this.localUserMediaStream.getAudioTracks().length > 0) {
- canReuseStream = false;
- }
- if (shouldRequestVideo !== this.localUserMediaStream.getVideoTracks().length > 0) {
- canReuseStream = false;
- }
-
- // This code checks that the device ID is the same as the localUserMediaStream stream, but we update
- // the localUserMediaStream whenever the device ID changes (apart from when restoring) so it's not
- // clear why this would ever be different, unless there's a race.
- if (
- shouldRequestAudio &&
- this.localUserMediaStream.getAudioTracks()[0]?.getSettings()?.deviceId !== this.audioInput
- ) {
- canReuseStream = false;
- }
- if (
- shouldRequestVideo &&
- this.localUserMediaStream.getVideoTracks()[0]?.getSettings()?.deviceId !== this.videoInput
- ) {
- canReuseStream = false;
- }
- } else {
- canReuseStream = false;
- }
-
- if (!canReuseStream) {
- const constraints = this.getUserMediaContraints(shouldRequestAudio, shouldRequestVideo);
- stream = await navigator.mediaDevices.getUserMedia(constraints);
- logger.log(
- `MediaHandler getUserMediaStreamInternal() calling getUserMediaStream (streamId=${
- stream.id
- }, shouldRequestAudio=${shouldRequestAudio}, shouldRequestVideo=${shouldRequestVideo}, constraints=${JSON.stringify(
- constraints,
- )})`,
- );
-
- for (const track of stream.getTracks()) {
- const settings = track.getSettings();
-
- if (track.kind === "audio") {
- this.audioInput = settings.deviceId!;
- } else if (track.kind === "video") {
- this.videoInput = settings.deviceId!;
- }
- }
-
- if (reusable) {
- this.localUserMediaStream = stream;
- }
- } else {
- stream = this.localUserMediaStream!.clone();
- logger.log(
- `MediaHandler getUserMediaStreamInternal() cloning (oldStreamId=${this.localUserMediaStream?.id} newStreamId=${stream.id} shouldRequestAudio=${shouldRequestAudio} shouldRequestVideo=${shouldRequestVideo})`,
- );
-
- if (!shouldRequestAudio) {
- for (const track of stream.getAudioTracks()) {
- stream.removeTrack(track);
- }
- }
-
- if (!shouldRequestVideo) {
- for (const track of stream.getVideoTracks()) {
- stream.removeTrack(track);
- }
- }
- }
-
- if (reusable) {
- this.userMediaStreams.push(stream);
- }
-
- this.emit(MediaHandlerEvent.LocalStreamsChanged);
-
- return stream;
- }
-
- /**
- * Stops all tracks on the provided usermedia stream
- */
- public stopUserMediaStream(mediaStream: MediaStream): void {
- logger.log(`MediaHandler stopUserMediaStream() stopping (streamId=${mediaStream.id})`);
- for (const track of mediaStream.getTracks()) {
- track.stop();
- }
-
- const index = this.userMediaStreams.indexOf(mediaStream);
-
- if (index !== -1) {
- logger.debug(
- `MediaHandler stopUserMediaStream() splicing usermedia stream out stream array (streamId=${mediaStream.id})`,
- mediaStream.id,
- );
- this.userMediaStreams.splice(index, 1);
- }
-
- this.emit(MediaHandlerEvent.LocalStreamsChanged);
-
- if (this.localUserMediaStream === mediaStream) {
- this.localUserMediaStream = undefined;
- }
- }
-
- /**
- * @param desktopCapturerSourceId - sourceId for Electron DesktopCapturer
- * @param reusable - is allowed to be reused by the MediaHandler
- * @returns based on passed parameters
- */
- public async getScreensharingStream(opts: IScreensharingOpts = {}, reusable = true): Promise<MediaStream> {
- let stream: MediaStream;
-
- if (this.screensharingStreams.length === 0) {
- const screenshareConstraints = this.getScreenshareContraints(opts);
-
- if (opts.desktopCapturerSourceId) {
- // We are using Electron
- logger.debug(
- `MediaHandler getScreensharingStream() calling getUserMedia() (opts=${JSON.stringify(opts)})`,
- );
- stream = await navigator.mediaDevices.getUserMedia(screenshareConstraints);
- } else {
- // We are not using Electron
- logger.debug(
- `MediaHandler getScreensharingStream() calling getDisplayMedia() (opts=${JSON.stringify(opts)})`,
- );
- stream = await navigator.mediaDevices.getDisplayMedia(screenshareConstraints);
- }
- } else {
- const matchingStream = this.screensharingStreams[this.screensharingStreams.length - 1];
- logger.log(`MediaHandler getScreensharingStream() cloning (streamId=${matchingStream.id})`);
- stream = matchingStream.clone();
- }
-
- if (reusable) {
- this.screensharingStreams.push(stream);
- }
-
- this.emit(MediaHandlerEvent.LocalStreamsChanged);
-
- return stream;
- }
-
- /**
- * Stops all tracks on the provided screensharing stream
- */
- public stopScreensharingStream(mediaStream: MediaStream): void {
- logger.debug(`MediaHandler stopScreensharingStream() stopping stream (streamId=${mediaStream.id})`);
- for (const track of mediaStream.getTracks()) {
- track.stop();
- }
-
- const index = this.screensharingStreams.indexOf(mediaStream);
-
- if (index !== -1) {
- logger.debug(`MediaHandler stopScreensharingStream() splicing stream out (streamId=${mediaStream.id})`);
- this.screensharingStreams.splice(index, 1);
- }
-
- this.emit(MediaHandlerEvent.LocalStreamsChanged);
- }
-
- /**
- * Stops all local media tracks
- */
- public stopAllStreams(): void {
- for (const stream of this.userMediaStreams) {
- logger.log(`MediaHandler stopAllStreams() stopping (streamId=${stream.id})`);
- for (const track of stream.getTracks()) {
- track.stop();
- }
- }
-
- for (const stream of this.screensharingStreams) {
- for (const track of stream.getTracks()) {
- track.stop();
- }
- }
-
- this.userMediaStreams = [];
- this.screensharingStreams = [];
- this.localUserMediaStream = undefined;
-
- this.emit(MediaHandlerEvent.LocalStreamsChanged);
- }
-
- private getUserMediaContraints(audio: boolean, video: boolean): MediaStreamConstraints {
- const isWebkit = !!navigator.webkitGetUserMedia;
-
- return {
- audio: audio
- ? {
- deviceId: this.audioInput ? { ideal: this.audioInput } : undefined,
- autoGainControl: this.audioSettings ? { ideal: this.audioSettings.autoGainControl } : undefined,
- echoCancellation: this.audioSettings ? { ideal: this.audioSettings.echoCancellation } : undefined,
- noiseSuppression: this.audioSettings ? { ideal: this.audioSettings.noiseSuppression } : undefined,
- }
- : false,
- video: video
- ? {
- deviceId: this.videoInput ? { ideal: this.videoInput } : undefined,
- /* We want 640x360. Chrome will give it only if we ask exactly,
- FF refuses entirely if we ask exactly, so have to ask for ideal
- instead
- XXX: Is this still true?
- */
- width: isWebkit ? { exact: 640 } : { ideal: 640 },
- height: isWebkit ? { exact: 360 } : { ideal: 360 },
- }
- : false,
- };
- }
-
- private getScreenshareContraints(opts: IScreensharingOpts): DesktopCapturerConstraints {
- const { desktopCapturerSourceId, audio } = opts;
- if (desktopCapturerSourceId) {
- return {
- audio: audio ?? false,
- video: {
- mandatory: {
- chromeMediaSource: "desktop",
- chromeMediaSourceId: desktopCapturerSourceId,
- },
- },
- };
- } else {
- return {
- audio: audio ?? false,
- video: true,
- };
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/connectionStats.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/connectionStats.ts
deleted file mode 100644
index dbde6e5..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/connectionStats.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
-Copyright 2023 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 { TransportStats } from "./transportStats";
-import { Bitrate } from "./media/mediaTrackStats";
-
-export interface ConnectionStatsBandwidth {
- /**
- * bytes per second
- */
- download: number;
- /**
- * bytes per second
- */
- upload: number;
-}
-
-export interface ConnectionStatsBitrate extends Bitrate {
- audio?: Bitrate;
- video?: Bitrate;
-}
-
-export interface PacketLoos {
- total: number;
- download: number;
- upload: number;
-}
-
-export class ConnectionStats {
- public bandwidth: ConnectionStatsBitrate = {} as ConnectionStatsBitrate;
- public bitrate: ConnectionStatsBitrate = {} as ConnectionStatsBitrate;
- public packetLoss: PacketLoos = {} as PacketLoos;
- public transport: TransportStats[] = [];
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/connectionStatsReporter.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/connectionStatsReporter.ts
deleted file mode 100644
index c43b9b4..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/connectionStatsReporter.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
-Copyright 2023 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 { Bitrate } from "./media/mediaTrackStats";
-
-export class ConnectionStatsReporter {
- public static buildBandwidthReport(now: RTCIceCandidatePairStats): Bitrate {
- const availableIncomingBitrate = now.availableIncomingBitrate;
- const availableOutgoingBitrate = now.availableOutgoingBitrate;
-
- return {
- download: availableIncomingBitrate ? Math.round(availableIncomingBitrate / 1000) : 0,
- upload: availableOutgoingBitrate ? Math.round(availableOutgoingBitrate / 1000) : 0,
- };
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/groupCallStats.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/groupCallStats.ts
deleted file mode 100644
index 6d8c566..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/groupCallStats.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
-Copyright 2023 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 { StatsReportGatherer } from "./statsReportGatherer";
-import { StatsReportEmitter } from "./statsReportEmitter";
-
-export class GroupCallStats {
- private timer: undefined | ReturnType<typeof setTimeout>;
- private readonly gatherers: Map<string, StatsReportGatherer> = new Map<string, StatsReportGatherer>();
- public readonly reports = new StatsReportEmitter();
-
- public constructor(private groupCallId: string, private userId: string, private interval: number = 10000) {}
-
- public start(): void {
- if (this.timer === undefined) {
- this.timer = setInterval(() => {
- this.processStats();
- }, this.interval);
- }
- }
-
- public stop(): void {
- if (this.timer !== undefined) {
- clearInterval(this.timer);
- this.gatherers.forEach((c) => c.stopProcessingStats());
- }
- }
-
- public hasStatsReportGatherer(callId: string): boolean {
- return this.gatherers.has(callId);
- }
-
- public addStatsReportGatherer(callId: string, userId: string, peerConnection: RTCPeerConnection): boolean {
- if (this.hasStatsReportGatherer(callId)) {
- return false;
- }
- this.gatherers.set(callId, new StatsReportGatherer(callId, userId, peerConnection, this.reports));
- return true;
- }
-
- public removeStatsReportGatherer(callId: string): boolean {
- return this.gatherers.delete(callId);
- }
-
- public getStatsReportGatherer(callId: string): StatsReportGatherer | undefined {
- return this.hasStatsReportGatherer(callId) ? this.gatherers.get(callId) : undefined;
- }
-
- private processStats(): void {
- this.gatherers.forEach((c) => c.processStats(this.groupCallId, this.userId));
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/media/mediaSsrcHandler.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/media/mediaSsrcHandler.ts
deleted file mode 100644
index e606051..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/media/mediaSsrcHandler.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
-Copyright 2023 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 { parse as parseSdp } from "sdp-transform";
-
-export type Mid = string;
-export type Ssrc = string;
-export type MapType = "local" | "remote";
-
-export class MediaSsrcHandler {
- private readonly ssrcToMid = { local: new Map<Mid, Ssrc[]>(), remote: new Map<Mid, Ssrc[]>() };
-
- public findMidBySsrc(ssrc: Ssrc, type: "local" | "remote"): Mid | undefined {
- let mid: Mid | undefined;
- this.ssrcToMid[type].forEach((ssrcs, m) => {
- if (ssrcs.find((s) => s == ssrc)) {
- mid = m;
- return;
- }
- });
- return mid;
- }
-
- public parse(description: string, type: MapType): void {
- const sdp = parseSdp(description);
- const ssrcToMid = new Map<Mid, Ssrc[]>();
- sdp.media.forEach((m) => {
- if ((!!m.mid && m.type === "video") || m.type === "audio") {
- const ssrcs: Ssrc[] = [];
- m.ssrcs?.forEach((ssrc) => {
- if (ssrc.attribute === "cname") {
- ssrcs.push(`${ssrc.id}`);
- }
- });
- ssrcToMid.set(`${m.mid}`, ssrcs);
- }
- });
- this.ssrcToMid[type] = ssrcToMid;
- }
-
- public getSsrcToMidMap(type: MapType): Map<Mid, Ssrc[]> {
- return this.ssrcToMid[type];
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/media/mediaTrackHandler.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/media/mediaTrackHandler.ts
deleted file mode 100644
index 32580b1..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/media/mediaTrackHandler.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
-Copyright 2023 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.
-*/
-
-export type TrackId = string;
-
-export class MediaTrackHandler {
- public constructor(private readonly pc: RTCPeerConnection) {}
-
- public getLocalTracks(kind: "audio" | "video"): MediaStreamTrack[] {
- const isNotNullAndKind = (track: MediaStreamTrack | null): boolean => {
- return track !== null && track.kind === kind;
- };
- // @ts-ignore The linter don't get it
- return this.pc
- .getTransceivers()
- .filter((t) => t.currentDirection === "sendonly" || t.currentDirection === "sendrecv")
- .filter((t) => t.sender !== null)
- .map((t) => t.sender)
- .map((s) => s.track)
- .filter(isNotNullAndKind);
- }
-
- public getTackById(trackId: string): MediaStreamTrack | undefined {
- return this.pc
- .getTransceivers()
- .map((t) => {
- if (t?.sender.track !== null && t.sender.track.id === trackId) {
- return t.sender.track;
- }
- if (t?.receiver.track !== null && t.receiver.track.id === trackId) {
- return t.receiver.track;
- }
- return undefined;
- })
- .find((t) => t !== undefined);
- }
-
- public getLocalTrackIdByMid(mid: string): string | undefined {
- const transceiver = this.pc.getTransceivers().find((t) => t.mid === mid);
- if (transceiver !== undefined && !!transceiver.sender && !!transceiver.sender.track) {
- return transceiver.sender.track.id;
- }
- return undefined;
- }
-
- public getRemoteTrackIdByMid(mid: string): string | undefined {
- const transceiver = this.pc.getTransceivers().find((t) => t.mid === mid);
- if (transceiver !== undefined && !!transceiver.receiver && !!transceiver.receiver.track) {
- return transceiver.receiver.track.id;
- }
- return undefined;
- }
-
- public getActiveSimulcastStreams(): number {
- //@TODO implement this right.. Check how many layer configured
- return 3;
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/media/mediaTrackStats.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/media/mediaTrackStats.ts
deleted file mode 100644
index 69ee9bd..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/media/mediaTrackStats.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
-Copyright 2023 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 { TrackId } from "./mediaTrackHandler";
-
-export interface PacketLoss {
- packetsTotal: number;
- packetsLost: number;
- isDownloadStream: boolean;
-}
-
-export interface Bitrate {
- /**
- * bytes per second
- */
- download: number;
- /**
- * bytes per second
- */
- upload: number;
-}
-
-export interface Resolution {
- width: number;
- height: number;
-}
-
-export type TrackStatsType = "local" | "remote";
-
-export class MediaTrackStats {
- private loss: PacketLoss = { packetsTotal: 0, packetsLost: 0, isDownloadStream: false };
- private bitrate: Bitrate = { download: 0, upload: 0 };
- private resolution: Resolution = { width: -1, height: -1 };
- private framerate = 0;
- private codec = "";
-
- public constructor(
- public readonly trackId: TrackId,
- public readonly type: TrackStatsType,
- public readonly kind: "audio" | "video",
- ) {}
-
- public getType(): TrackStatsType {
- return this.type;
- }
-
- public setLoss(loos: PacketLoss): void {
- this.loss = loos;
- }
-
- public getLoss(): PacketLoss {
- return this.loss;
- }
-
- public setResolution(resolution: Resolution): void {
- this.resolution = resolution;
- }
-
- public getResolution(): Resolution {
- return this.resolution;
- }
-
- public setFramerate(framerate: number): void {
- this.framerate = framerate;
- }
-
- public getFramerate(): number {
- return this.framerate;
- }
-
- public setBitrate(bitrate: Bitrate): void {
- this.bitrate = bitrate;
- }
-
- public getBitrate(): Bitrate {
- return this.bitrate;
- }
-
- public setCodec(codecShortType: string): boolean {
- this.codec = codecShortType;
- return true;
- }
-
- public getCodec(): string {
- return this.codec;
- }
-
- public resetBitrate(): void {
- this.bitrate = { download: 0, upload: 0 };
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/media/mediaTrackStatsHandler.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/media/mediaTrackStatsHandler.ts
deleted file mode 100644
index 6fb119c..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/media/mediaTrackStatsHandler.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
-Copyright 2023 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 { TrackID } from "../statsReport";
-import { MediaTrackStats } from "./mediaTrackStats";
-import { MediaTrackHandler } from "./mediaTrackHandler";
-import { MediaSsrcHandler } from "./mediaSsrcHandler";
-
-export class MediaTrackStatsHandler {
- private readonly track2stats = new Map<TrackID, MediaTrackStats>();
-
- public constructor(
- public readonly mediaSsrcHandler: MediaSsrcHandler,
- public readonly mediaTrackHandler: MediaTrackHandler,
- ) {}
-
- /**
- * Find tracks by rtc stats
- * Argument report is any because the stats api is not consistent:
- * For example `trackIdentifier`, `mid` not existing in every implementations
- * https://www.w3.org/TR/webrtc-stats/#dom-rtcinboundrtpstreamstats
- * https://developer.mozilla.org/en-US/docs/Web/API/RTCInboundRtpStreamStats
- */
- public findTrack2Stats(report: any, type: "remote" | "local"): MediaTrackStats | undefined {
- let trackID;
- if (report.trackIdentifier) {
- trackID = report.trackIdentifier;
- } else if (report.mid) {
- trackID =
- type === "remote"
- ? this.mediaTrackHandler.getRemoteTrackIdByMid(report.mid)
- : this.mediaTrackHandler.getLocalTrackIdByMid(report.mid);
- } else if (report.ssrc) {
- const mid = this.mediaSsrcHandler.findMidBySsrc(report.ssrc, type);
- if (!mid) {
- return undefined;
- }
- trackID =
- type === "remote"
- ? this.mediaTrackHandler.getRemoteTrackIdByMid(report.mid)
- : this.mediaTrackHandler.getLocalTrackIdByMid(report.mid);
- }
-
- if (!trackID) {
- return undefined;
- }
-
- let trackStats = this.track2stats.get(trackID);
-
- if (!trackStats) {
- const track = this.mediaTrackHandler.getTackById(trackID);
- if (track !== undefined) {
- const kind: "audio" | "video" = track.kind === "audio" ? track.kind : "video";
- trackStats = new MediaTrackStats(trackID, type, kind);
- this.track2stats.set(trackID, trackStats);
- } else {
- return undefined;
- }
- }
- return trackStats;
- }
-
- public findLocalVideoTrackStats(report: any): MediaTrackStats | undefined {
- const localVideoTracks = this.mediaTrackHandler.getLocalTracks("video");
- if (localVideoTracks.length === 0) {
- return undefined;
- }
- return this.findTrack2Stats(report, "local");
- }
-
- public getTrack2stats(): Map<TrackID, MediaTrackStats> {
- return this.track2stats;
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/statsReport.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/statsReport.ts
deleted file mode 100644
index 56d6c4b..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/statsReport.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
-Copyright 2023 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 { ConnectionStatsBandwidth, ConnectionStatsBitrate, PacketLoos } from "./connectionStats";
-import { TransportStats } from "./transportStats";
-import { Resolution } from "./media/mediaTrackStats";
-
-export enum StatsReport {
- CONNECTION_STATS = "StatsReport.connection_stats",
- BYTE_SENT_STATS = "StatsReport.byte_sent_stats",
-}
-
-export type TrackID = string;
-export type ByteSend = number;
-
-export interface ByteSentStatsReport extends Map<TrackID, ByteSend> {
- // is a map: `local trackID` => byte send
-}
-
-export interface ConnectionStatsReport {
- bandwidth: ConnectionStatsBandwidth;
- bitrate: ConnectionStatsBitrate;
- packetLoss: PacketLoos;
- resolution: ResolutionMap;
- framerate: FramerateMap;
- codec: CodecMap;
- transport: TransportStats[];
-}
-
-export interface ResolutionMap {
- local: Map<TrackID, Resolution>;
- remote: Map<TrackID, Resolution>;
-}
-
-export interface FramerateMap {
- local: Map<TrackID, number>;
- remote: Map<TrackID, number>;
-}
-
-export interface CodecMap {
- local: Map<TrackID, string>;
- remote: Map<TrackID, string>;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/statsReportBuilder.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/statsReportBuilder.ts
deleted file mode 100644
index c1af471..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/statsReportBuilder.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
-Copyright 2023 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 { CodecMap, ConnectionStatsReport, FramerateMap, ResolutionMap, TrackID } from "./statsReport";
-import { MediaTrackStats, Resolution } from "./media/mediaTrackStats";
-
-export class StatsReportBuilder {
- public static build(stats: Map<TrackID, MediaTrackStats>): ConnectionStatsReport {
- const report = {} as ConnectionStatsReport;
-
- // process stats
- const totalPackets = {
- download: 0,
- upload: 0,
- };
- const lostPackets = {
- download: 0,
- upload: 0,
- };
- let bitrateDownload = 0;
- let bitrateUpload = 0;
- const resolutions: ResolutionMap = {
- local: new Map<TrackID, Resolution>(),
- remote: new Map<TrackID, Resolution>(),
- };
- const framerates: FramerateMap = { local: new Map<TrackID, number>(), remote: new Map<TrackID, number>() };
- const codecs: CodecMap = { local: new Map<TrackID, string>(), remote: new Map<TrackID, string>() };
-
- let audioBitrateDownload = 0;
- let audioBitrateUpload = 0;
- let videoBitrateDownload = 0;
- let videoBitrateUpload = 0;
-
- for (const [trackId, trackStats] of stats) {
- // process packet loss stats
- const loss = trackStats.getLoss();
- const type = loss.isDownloadStream ? "download" : "upload";
-
- totalPackets[type] += loss.packetsTotal;
- lostPackets[type] += loss.packetsLost;
-
- // process bitrate stats
- bitrateDownload += trackStats.getBitrate().download;
- bitrateUpload += trackStats.getBitrate().upload;
-
- // collect resolutions and framerates
- if (trackStats.kind === "audio") {
- audioBitrateDownload += trackStats.getBitrate().download;
- audioBitrateUpload += trackStats.getBitrate().upload;
- } else {
- videoBitrateDownload += trackStats.getBitrate().download;
- videoBitrateUpload += trackStats.getBitrate().upload;
- }
-
- resolutions[trackStats.getType()].set(trackId, trackStats.getResolution());
- framerates[trackStats.getType()].set(trackId, trackStats.getFramerate());
- codecs[trackStats.getType()].set(trackId, trackStats.getCodec());
-
- trackStats.resetBitrate();
- }
-
- report.bitrate = {
- upload: bitrateUpload,
- download: bitrateDownload,
- };
-
- report.bitrate.audio = {
- upload: audioBitrateUpload,
- download: audioBitrateDownload,
- };
-
- report.bitrate.video = {
- upload: videoBitrateUpload,
- download: videoBitrateDownload,
- };
-
- report.packetLoss = {
- total: StatsReportBuilder.calculatePacketLoss(
- lostPackets.download + lostPackets.upload,
- totalPackets.download + totalPackets.upload,
- ),
- download: StatsReportBuilder.calculatePacketLoss(lostPackets.download, totalPackets.download),
- upload: StatsReportBuilder.calculatePacketLoss(lostPackets.upload, totalPackets.upload),
- };
- report.framerate = framerates;
- report.resolution = resolutions;
- report.codec = codecs;
- return report;
- }
-
- private static calculatePacketLoss(lostPackets: number, totalPackets: number): number {
- if (!totalPackets || totalPackets <= 0 || !lostPackets || lostPackets <= 0) {
- return 0;
- }
-
- return Math.round((lostPackets / totalPackets) * 100);
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/statsReportEmitter.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/statsReportEmitter.ts
deleted file mode 100644
index cf01470..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/statsReportEmitter.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
-Copyright 2023 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 { TypedEventEmitter } from "../../models/typed-event-emitter";
-import { ByteSentStatsReport, ConnectionStatsReport, StatsReport } from "./statsReport";
-
-export type StatsReportHandlerMap = {
- [StatsReport.BYTE_SENT_STATS]: (report: ByteSentStatsReport) => void;
- [StatsReport.CONNECTION_STATS]: (report: ConnectionStatsReport) => void;
-};
-
-export class StatsReportEmitter extends TypedEventEmitter<StatsReport, StatsReportHandlerMap> {
- public emitByteSendReport(byteSentStats: ByteSentStatsReport): void {
- this.emit(StatsReport.BYTE_SENT_STATS, byteSentStats);
- }
-
- public emitConnectionStatsReport(report: ConnectionStatsReport): void {
- this.emit(StatsReport.CONNECTION_STATS, report);
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/statsReportGatherer.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/statsReportGatherer.ts
deleted file mode 100644
index 769ba6e..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/statsReportGatherer.ts
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
-Copyright 2023 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 { ConnectionStats } from "./connectionStats";
-import { StatsReportEmitter } from "./statsReportEmitter";
-import { ByteSend, ByteSentStatsReport, TrackID } from "./statsReport";
-import { ConnectionStatsReporter } from "./connectionStatsReporter";
-import { TransportStatsReporter } from "./transportStatsReporter";
-import { MediaSsrcHandler } from "./media/mediaSsrcHandler";
-import { MediaTrackHandler } from "./media/mediaTrackHandler";
-import { MediaTrackStatsHandler } from "./media/mediaTrackStatsHandler";
-import { TrackStatsReporter } from "./trackStatsReporter";
-import { StatsReportBuilder } from "./statsReportBuilder";
-import { StatsValueFormatter } from "./statsValueFormatter";
-
-export class StatsReportGatherer {
- private isActive = true;
- private previousStatsReport: RTCStatsReport | undefined;
- private currentStatsReport: RTCStatsReport | undefined;
- private readonly connectionStats = new ConnectionStats();
-
- private readonly trackStats: MediaTrackStatsHandler;
-
- // private readonly ssrcToMid = { local: new Map<Mid, Ssrc[]>(), remote: new Map<Mid, Ssrc[]>() };
-
- public constructor(
- public readonly callId: string,
- public readonly remoteUserId: string,
- private readonly pc: RTCPeerConnection,
- private readonly emitter: StatsReportEmitter,
- private readonly isFocus = true,
- ) {
- pc.addEventListener("signalingstatechange", this.onSignalStateChange.bind(this));
- this.trackStats = new MediaTrackStatsHandler(new MediaSsrcHandler(), new MediaTrackHandler(pc));
- }
-
- public async processStats(groupCallId: string, localUserId: string): Promise<boolean> {
- if (this.isActive) {
- const statsPromise = this.pc.getStats();
- if (typeof statsPromise?.then === "function") {
- return statsPromise
- .then((report) => {
- // @ts-ignore
- this.currentStatsReport = typeof report?.result === "function" ? report.result() : report;
- try {
- this.processStatsReport(groupCallId, localUserId);
- } catch (error) {
- this.isActive = false;
- return false;
- }
-
- this.previousStatsReport = this.currentStatsReport;
- return true;
- })
- .catch((error) => {
- this.handleError(error);
- return false;
- });
- }
- this.isActive = false;
- }
- return Promise.resolve(false);
- }
-
- private processStatsReport(groupCallId: string, localUserId: string): void {
- const byteSentStats: ByteSentStatsReport = new Map<TrackID, ByteSend>();
-
- this.currentStatsReport?.forEach((now) => {
- const before = this.previousStatsReport ? this.previousStatsReport.get(now.id) : null;
- // RTCIceCandidatePairStats - https://w3c.github.io/webrtc-stats/#candidatepair-dict*
- if (now.type === "candidate-pair" && now.nominated && now.state === "succeeded") {
- this.connectionStats.bandwidth = ConnectionStatsReporter.buildBandwidthReport(now);
- this.connectionStats.transport = TransportStatsReporter.buildReport(
- this.currentStatsReport,
- now,
- this.connectionStats.transport,
- this.isFocus,
- );
-
- // RTCReceivedRtpStreamStats
- // https://w3c.github.io/webrtc-stats/#receivedrtpstats-dict*
- // RTCSentRtpStreamStats
- // https://w3c.github.io/webrtc-stats/#sentrtpstats-dict*
- } else if (now.type === "inbound-rtp" || now.type === "outbound-rtp") {
- const trackStats = this.trackStats.findTrack2Stats(
- now,
- now.type === "inbound-rtp" ? "remote" : "local",
- );
- if (!trackStats) {
- return;
- }
-
- if (before) {
- TrackStatsReporter.buildPacketsLost(trackStats, now, before);
- }
-
- // Get the resolution and framerate for only remote video sources here. For the local video sources,
- // 'track' stats will be used since they have the updated resolution based on the simulcast streams
- // currently being sent. Promise based getStats reports three 'outbound-rtp' streams and there will be
- // more calculations needed to determine what is the highest resolution stream sent by the client if the
- // 'outbound-rtp' stats are used.
- if (now.type === "inbound-rtp") {
- TrackStatsReporter.buildFramerateResolution(trackStats, now);
- if (before) {
- TrackStatsReporter.buildBitrateReceived(trackStats, now, before);
- }
- } else if (before) {
- byteSentStats.set(trackStats.trackId, StatsValueFormatter.getNonNegativeValue(now.bytesSent));
- TrackStatsReporter.buildBitrateSend(trackStats, now, before);
- }
- TrackStatsReporter.buildCodec(this.currentStatsReport, trackStats, now);
- } else if (now.type === "track" && now.kind === "video" && !now.remoteSource) {
- const trackStats = this.trackStats.findLocalVideoTrackStats(now);
- if (!trackStats) {
- return;
- }
- TrackStatsReporter.buildFramerateResolution(trackStats, now);
- TrackStatsReporter.calculateSimulcastFramerate(
- trackStats,
- now,
- before,
- this.trackStats.mediaTrackHandler.getActiveSimulcastStreams(),
- );
- }
- });
-
- this.emitter.emitByteSendReport(byteSentStats);
- this.processAndEmitReport();
- }
-
- public setActive(isActive: boolean): void {
- this.isActive = isActive;
- }
-
- public getActive(): boolean {
- return this.isActive;
- }
-
- private handleError(_: any): void {
- this.isActive = false;
- }
-
- private processAndEmitReport(): void {
- const report = StatsReportBuilder.build(this.trackStats.getTrack2stats());
-
- this.connectionStats.bandwidth = report.bandwidth;
- this.connectionStats.bitrate = report.bitrate;
- this.connectionStats.packetLoss = report.packetLoss;
-
- this.emitter.emitConnectionStatsReport({
- ...report,
- transport: this.connectionStats.transport,
- });
-
- this.connectionStats.transport = [];
- }
-
- public stopProcessingStats(): void {}
-
- private onSignalStateChange(): void {
- if (this.pc.signalingState === "stable") {
- if (this.pc.currentRemoteDescription) {
- this.trackStats.mediaSsrcHandler.parse(this.pc.currentRemoteDescription.sdp, "remote");
- }
- if (this.pc.currentLocalDescription) {
- this.trackStats.mediaSsrcHandler.parse(this.pc.currentLocalDescription.sdp, "local");
- }
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/statsValueFormatter.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/statsValueFormatter.ts
deleted file mode 100644
index c658fa6..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/statsValueFormatter.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
-Copyright 2023 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.
-*/
-export class StatsValueFormatter {
- public static getNonNegativeValue(imput: any): number {
- let value = imput;
-
- if (typeof value !== "number") {
- value = Number(value);
- }
-
- if (isNaN(value)) {
- return 0;
- }
-
- return Math.max(0, value);
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/trackStatsReporter.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/trackStatsReporter.ts
deleted file mode 100644
index 1f6fcd6..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/trackStatsReporter.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-import { MediaTrackStats } from "./media/mediaTrackStats";
-import { StatsValueFormatter } from "./statsValueFormatter";
-
-export class TrackStatsReporter {
- public static buildFramerateResolution(trackStats: MediaTrackStats, now: any): void {
- const resolution = {
- height: now.frameHeight,
- width: now.frameWidth,
- };
- const frameRate = now.framesPerSecond;
-
- if (resolution.height && resolution.width) {
- trackStats.setResolution(resolution);
- }
- trackStats.setFramerate(Math.round(frameRate || 0));
- }
-
- public static calculateSimulcastFramerate(trackStats: MediaTrackStats, now: any, before: any, layer: number): void {
- let frameRate = trackStats.getFramerate();
- if (!frameRate) {
- if (before) {
- const timeMs = now.timestamp - before.timestamp;
-
- if (timeMs > 0 && now.framesSent) {
- const numberOfFramesSinceBefore = now.framesSent - before.framesSent;
-
- frameRate = (numberOfFramesSinceBefore / timeMs) * 1000;
- }
- }
-
- if (!frameRate) {
- return;
- }
- }
-
- // Reset frame rate to 0 when video is suspended as a result of endpoint falling out of last-n.
- frameRate = layer ? Math.round(frameRate / layer) : 0;
- trackStats.setFramerate(frameRate);
- }
-
- public static buildCodec(report: RTCStatsReport | undefined, trackStats: MediaTrackStats, now: any): void {
- const codec = report?.get(now.codecId);
-
- if (codec) {
- /**
- * The mime type has the following form: video/VP8 or audio/ISAC,
- * so we what to keep just the type after the '/', audio and video
- * keys will be added on the processing side.
- */
- const codecShortType = codec.mimeType.split("/")[1];
-
- codecShortType && trackStats.setCodec(codecShortType);
- }
- }
-
- public static buildBitrateReceived(trackStats: MediaTrackStats, now: any, before: any): void {
- trackStats.setBitrate({
- download: TrackStatsReporter.calculateBitrate(
- now.bytesReceived,
- before.bytesReceived,
- now.timestamp,
- before.timestamp,
- ),
- upload: 0,
- });
- }
-
- public static buildBitrateSend(trackStats: MediaTrackStats, now: any, before: any): void {
- trackStats.setBitrate({
- download: 0,
- upload: this.calculateBitrate(now.bytesSent, before.bytesSent, now.timestamp, before.timestamp),
- });
- }
-
- public static buildPacketsLost(trackStats: MediaTrackStats, now: any, before: any): void {
- const key = now.type === "outbound-rtp" ? "packetsSent" : "packetsReceived";
-
- let packetsNow = now[key];
- if (!packetsNow || packetsNow < 0) {
- packetsNow = 0;
- }
-
- const packetsBefore = StatsValueFormatter.getNonNegativeValue(before[key]);
- const packetsDiff = Math.max(0, packetsNow - packetsBefore);
-
- const packetsLostNow = StatsValueFormatter.getNonNegativeValue(now.packetsLost);
- const packetsLostBefore = StatsValueFormatter.getNonNegativeValue(before.packetsLost);
- const packetsLostDiff = Math.max(0, packetsLostNow - packetsLostBefore);
-
- trackStats.setLoss({
- packetsTotal: packetsDiff + packetsLostDiff,
- packetsLost: packetsLostDiff,
- isDownloadStream: now.type !== "outbound-rtp",
- });
- }
-
- private static calculateBitrate(
- bytesNowAny: any,
- bytesBeforeAny: any,
- nowTimestamp: number,
- beforeTimestamp: number,
- ): number {
- const bytesNow = StatsValueFormatter.getNonNegativeValue(bytesNowAny);
- const bytesBefore = StatsValueFormatter.getNonNegativeValue(bytesBeforeAny);
- const bytesProcessed = Math.max(0, bytesNow - bytesBefore);
-
- const timeMs = nowTimestamp - beforeTimestamp;
- let bitrateKbps = 0;
-
- if (timeMs > 0) {
- // TODO is there any reason to round here?
- bitrateKbps = Math.round((bytesProcessed * 8) / timeMs);
- }
-
- return bitrateKbps;
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/transportStats.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/transportStats.ts
deleted file mode 100644
index 2b6e975..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/transportStats.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
-Copyright 2023 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.
-*/
-
-export interface TransportStats {
- ip: string;
- type: string;
- localIp: string;
- isFocus: boolean;
- localCandidateType: string;
- remoteCandidateType: string;
- networkType: string;
- rtt: number;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/transportStatsReporter.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/transportStatsReporter.ts
deleted file mode 100644
index d419a73..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/stats/transportStatsReporter.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { TransportStats } from "./transportStats";
-
-export class TransportStatsReporter {
- public static buildReport(
- report: RTCStatsReport | undefined,
- now: RTCIceCandidatePairStats,
- conferenceStatsTransport: TransportStats[],
- isFocus: boolean,
- ): TransportStats[] {
- const localUsedCandidate = report?.get(now.localCandidateId);
- const remoteUsedCandidate = report?.get(now.remoteCandidateId);
-
- // RTCIceCandidateStats
- // https://w3c.github.io/webrtc-stats/#icecandidate-dict*
- if (remoteUsedCandidate && localUsedCandidate) {
- const remoteIpAddress =
- remoteUsedCandidate.ip !== undefined ? remoteUsedCandidate.ip : remoteUsedCandidate.address;
- const remotePort = remoteUsedCandidate.port;
- const ip = `${remoteIpAddress}:${remotePort}`;
-
- const localIpAddress =
- localUsedCandidate.ip !== undefined ? localUsedCandidate.ip : localUsedCandidate.address;
- const localPort = localUsedCandidate.port;
- const localIp = `${localIpAddress}:${localPort}`;
-
- const type = remoteUsedCandidate.protocol;
-
- // Save the address unless it has been saved already.
- if (
- !conferenceStatsTransport.some(
- (t: TransportStats) => t.ip === ip && t.type === type && t.localIp === localIp,
- )
- ) {
- conferenceStatsTransport.push({
- ip,
- type,
- localIp,
- isFocus,
- localCandidateType: localUsedCandidate.candidateType,
- remoteCandidateType: remoteUsedCandidate.candidateType,
- networkType: localUsedCandidate.networkType,
- rtt: now.currentRoundTripTime ? now.currentRoundTripTime * 1000 : NaN,
- } as TransportStats);
- }
- }
- return conferenceStatsTransport;
- }
-}