diff options
Diffstat (limited to 'includes/external/matrix/node_modules/matrix-js-sdk/lib/sliding-sync-sdk.js')
-rw-r--r-- | includes/external/matrix/node_modules/matrix-js-sdk/lib/sliding-sync-sdk.js | 881 |
1 files changed, 881 insertions, 0 deletions
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/lib/sliding-sync-sdk.js b/includes/external/matrix/node_modules/matrix-js-sdk/lib/sliding-sync-sdk.js new file mode 100644 index 0000000..715187e --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/lib/sliding-sync-sdk.js @@ -0,0 +1,881 @@ +"use strict"; + +var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.SlidingSyncSdk = void 0; +var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); +var _room = require("./models/room"); +var _logger = require("./logger"); +var utils = _interopRequireWildcard(require("./utils")); +var _eventTimeline = require("./models/event-timeline"); +var _client = require("./client"); +var _sync = require("./sync"); +var _httpApi = require("./http-api"); +var _slidingSync = require("./sliding-sync"); +var _event = require("./@types/event"); +var _roomState = require("./models/room-state"); +var _roomMember = require("./models/room-member"); +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } +/* +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. +*/ + +// 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; +class ExtensionE2EE { + constructor(crypto) { + this.crypto = crypto; + } + name() { + return "e2ee"; + } + when() { + return _slidingSync.ExtensionState.PreProcess; + } + onRequest(isInitial) { + if (!isInitial) { + return undefined; + } + return { + enabled: true // this is sticky so only send it on the initial request + }; + } + + async onResponse(data) { + // 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({}); + } +} +class ExtensionToDevice { + constructor(client, cryptoCallbacks) { + this.client = client; + this.cryptoCallbacks = cryptoCallbacks; + (0, _defineProperty2.default)(this, "nextBatch", null); + } + name() { + return "to_device"; + } + when() { + return _slidingSync.ExtensionState.PreProcess; + } + onRequest(isInitial) { + const extReq = { + since: this.nextBatch !== null ? this.nextBatch : undefined + }; + if (isInitial) { + extReq["limit"] = 100; + extReq["enabled"] = true; + } + return extReq; + } + async onResponse(data) { + const cancelledKeyVerificationTxns = []; + 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 = 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.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(_client.ClientEvent.ToDeviceEvent, toDeviceEvent); + }); + this.nextBatch = data.next_batch; + } +} +class ExtensionAccountData { + constructor(client) { + this.client = client; + } + name() { + return "account_data"; + } + when() { + return _slidingSync.ExtensionState.PostProcess; + } + onRequest(isInitial) { + if (!isInitial) { + return undefined; + } + return { + enabled: true + }; + } + onResponse(data) { + 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.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(_client.ClientEvent.Event, e); + }); + } + } + processGlobalAccountData(globalAccountData) { + const events = mapEvents(this.client, undefined, globalAccountData); + const prevEventsMap = events.reduce((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() === _event.EventType.PushRules) { + const rules = accountDataEvent.getContent(); + this.client.setPushRules(rules); + } + const prevEvent = prevEventsMap[accountDataEvent.getType()]; + this.client.emit(_client.ClientEvent.AccountData, accountDataEvent, prevEvent); + return accountDataEvent; + }); + } +} +class ExtensionTyping { + constructor(client) { + this.client = client; + } + name() { + return "typing"; + } + when() { + return _slidingSync.ExtensionState.PostProcess; + } + onRequest(isInitial) { + if (!isInitial) { + return undefined; // don't send a JSON object for subsequent requests, we don't need to. + } + + return { + enabled: true + }; + } + onResponse(data) { + if (!(data !== null && data !== void 0 && data.rooms)) { + return; + } + for (const roomId in data.rooms) { + processEphemeralEvents(this.client, roomId, [data.rooms[roomId]]); + } + } +} +class ExtensionReceipts { + constructor(client) { + this.client = client; + } + name() { + return "receipts"; + } + when() { + return _slidingSync.ExtensionState.PostProcess; + } + onRequest(isInitial) { + if (isInitial) { + return { + enabled: true + }; + } + return undefined; // don't send a JSON object for subsequent requests, we don't need to. + } + + onResponse(data) { + if (!(data !== null && data !== void 0 && 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. + */ +class SlidingSyncSdk { + // accumulator of sync events in the current sync response + + constructor(slidingSync, client, opts, syncOpts) { + this.slidingSync = slidingSync; + this.client = client; + (0, _defineProperty2.default)(this, "opts", void 0); + (0, _defineProperty2.default)(this, "syncOpts", void 0); + (0, _defineProperty2.default)(this, "syncState", null); + (0, _defineProperty2.default)(this, "syncStateData", void 0); + (0, _defineProperty2.default)(this, "lastPos", null); + (0, _defineProperty2.default)(this, "failCount", 0); + (0, _defineProperty2.default)(this, "notifEvents", []); + this.opts = (0, _sync.defaultClientOpts)(opts); + this.syncOpts = (0, _sync.defaultSyncApiOpts)(syncOpts); + if (client.getNotifTimelineSet()) { + client.reEmitter.reEmit(client.getNotifTimelineSet(), [_room.RoomEvent.Timeline, _room.RoomEvent.TimelineReset]); + } + this.slidingSync.on(_slidingSync.SlidingSyncEvent.Lifecycle, this.onLifecycle.bind(this)); + this.slidingSync.on(_slidingSync.SlidingSyncEvent.RoomData, this.onRoomData.bind(this)); + const extensions = [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); + }); + } + onRoomData(roomId, roomData) { + let room = this.client.store.getRoom(roomId); + if (!room) { + if (!roomData.initial) { + _logger.logger.debug("initial flag not set but no stored room exists for room ", roomId, roomData); + return; + } + room = (0, _sync._createAndReEmitRoom)(this.client, roomId, this.opts); + } + this.processRoomData(this.client, room, roomData); + } + onLifecycle(state, resp, err) { + if (err) { + _logger.logger.debug("onLifecycle", state, err); + } + switch (state) { + case _slidingSync.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(_sync.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(_sync.SyncState.Syncing, { + oldSyncToken: this.lastPos, + nextSyncToken: resp.pos, + catchingUp: false, + fromCache: false + }); + this.lastPos = resp.pos; + break; + case _slidingSync.SlidingSyncState.RequestFinished: + if (err) { + this.failCount += 1; + this.updateSyncState(this.failCount > FAILED_SYNC_ERROR_THRESHOLD ? _sync.SyncState.Error : _sync.SyncState.Reconnecting, { + error: new _httpApi.MatrixError(err) + }); + if (this.shouldAbortSync(new _httpApi.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. + */ + async syncLeftRooms() { + 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. + */ + async peek(_roomId) { + return null; // TODO + } + + /** + * Stop polling for updates in the peeked room. NOPs if there is no room being + * peeked. + */ + stopPeeking() { + // TODO + } + + /** + * Returns the current state of this sync object + * @see MatrixClient#event:"sync" + */ + getSyncState() { + 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. + */ + getSyncStateData() { + var _this$syncStateData; + return (_this$syncStateData = this.syncStateData) !== null && _this$syncStateData !== void 0 ? _this$syncStateData : null; + } + + // Helper functions which set up JS SDK structs are below and are identical to the sync v2 counterparts + + createRoom(roomId) { + // XXX cargoculted from sync.ts + const { + timelineSupport + } = this.client; + const room = new _room.Room(roomId, this.client, this.client.getUserId(), { + lazyLoadMembers: this.opts.lazyLoadMembers, + pendingEventOrdering: this.opts.pendingEventOrdering, + timelineSupport + }); + this.client.reEmitter.reEmit(room, [_room.RoomEvent.Name, _room.RoomEvent.Redaction, _room.RoomEvent.RedactionCancelled, _room.RoomEvent.Receipt, _room.RoomEvent.Tags, _room.RoomEvent.LocalEchoUpdated, _room.RoomEvent.AccountData, _room.RoomEvent.MyMembership, _room.RoomEvent.Timeline, _room.RoomEvent.TimelineReset]); + this.registerStateListeners(room); + return room; + } + registerStateListeners(room) { + // 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, [_roomState.RoomStateEvent.Events, _roomState.RoomStateEvent.Members, _roomState.RoomStateEvent.NewMember, _roomState.RoomStateEvent.Update]); + room.currentState.on(_roomState.RoomStateEvent.NewMember, (event, state, member) => { + var _this$client$getUser; + member.user = (_this$client$getUser = this.client.getUser(member.userId)) !== null && _this$client$getUser !== void 0 ? _this$client$getUser : undefined; + this.client.reEmitter.reEmit(member, [_roomMember.RoomMemberEvent.Name, _roomMember.RoomMemberEvent.Typing, _roomMember.RoomMemberEvent.PowerLevel, _roomMember.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); + } */ + + shouldAbortSync(error) { + if (error.errcode === "M_UNKNOWN_TOKEN") { + // The logout already happened, we just need to stop. + _logger.logger.warn("Token no longer valid - assuming logout"); + this.stop(); + this.updateSyncState(_sync.SyncState.Error, { + error + }); + return true; + } + return false; + } + async processRoomData(client, room, roomData) { + 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 = []; // 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(); + 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 = []; + const newEvents = []; + 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(_room.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(_room.NotificationCountType.Highlight) <= 0) { + room.setUnreadNotificationCount(_room.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(_client.ClientEvent.Room, room); + } + inviteStateEvents.forEach(e => { + this.client.emit(_client.ClientEvent.Event, e); + }); + room.updateMyMembership("invite"); + return; + } + if (roomData.initial) { + var _roomData$prev_batch; + // set the back-pagination token. Do this *before* adding any + // events so that clients can start back-paginating. + room.getLiveTimeline().setPaginationToken((_roomData$prev_batch = roomData.prev_batch) !== null && _roomData$prev_batch !== void 0 ? _roomData$prev_batch : null, _eventTimeline.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(_client.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 => { + client.emit(_client.ClientEvent.Event, e); + if (e.isState() && e.getType() == _event.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(_client.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. + */ + injectRoomEvents(room, stateEventList, timelineEventList, numLive) { + 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 = []; + 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); + } + resolveInvites(room) { + 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. + }); + }); + } + retryImmediately() { + return true; + } + + /** + * Main entry point. Blocks until stop() is called. + */ + async sync() { + _logger.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.logger.debug("Getting push rules..."); + const result = await this.client.getPushRules(); + _logger.logger.debug("Got push rules"); + this.client.pushRules = result; + break; + } catch (err) { + _logger.logger.error("Getting push rules failed", err); + if (this.shouldAbortSync(err)) { + return; + } + } + } + + // start syncing + await this.slidingSync.start(); + } + + /** + * Stops the sync object from syncing. + */ + stop() { + _logger.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 + */ + updateSyncState(newState, data) { + const old = this.syncState; + this.syncState = newState; + this.syncStateData = data; + this.client.emit(_client.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. + */ + addNotifications(timelineEventList) { + // 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. + */ + purgeNotifications() { + this.notifEvents.sort(function (a, b) { + return a.getTs() - b.getTs(); + }); + this.notifEvents.forEach(event => { + var _this$client$getNotif; + (_this$client$getNotif = this.client.getNotifTimelineSet()) === null || _this$client$getNotif === void 0 ? void 0 : _this$client$getNotif.addLiveEvent(event); + }); + this.notifEvents = []; + } +} +exports.SlidingSyncSdk = SlidingSyncSdk; +function ensureNameEvent(client, roomId, roomData) { + // 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 === _event.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: _event.EventType.RoomName, + content: { + name: roomData.name + }, + sender: client.getUserId(), + origin_server_ts: new Date().getTime() + }); + return roomData; +} +// 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, roomId, events, decrypt = true) { + const mapper = client.getEventMapper({ + decrypt + }); + return events.map(function (e) { + e.room_id = roomId; + return mapper(e); + }); +} +function processEphemeralEvents(client, roomId, ephEvents) { + const ephemeralEvents = mapEvents(client, roomId, ephEvents); + const room = client.getRoom(roomId); + if (!room) { + _logger.logger.warn("got ephemeral events for room but room doesn't exist on client:", roomId); + return; + } + room.addEphemeralEvents(ephemeralEvents); + ephemeralEvents.forEach(e => { + client.emit(_client.ClientEvent.Event, e); + }); +} +//# sourceMappingURL=sliding-sync-sdk.js.map
\ No newline at end of file |