summaryrefslogtreecommitdiff
path: root/includes/external/matrix/node_modules/matrix-js-sdk/src/sliding-sync-sdk.ts
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/sliding-sync-sdk.ts
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/sliding-sync-sdk.ts')
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/sliding-sync-sdk.ts1027
1 files changed, 0 insertions, 1027 deletions
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);
- });
-}