UNPKG

matrix-react-sdk

Version:
801 lines (757 loc) 113 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _events = require("events"); var _matrix = require("matrix-js-sdk/src/matrix"); var _types = require("matrix-js-sdk/src/types"); var _utils = require("matrix-js-sdk/src/utils"); var _logger = require("matrix-js-sdk/src/logger"); var _PlatformPeg = _interopRequireDefault(require("../PlatformPeg")); var _MatrixClientPeg = require("../MatrixClientPeg"); var _SettingsStore = _interopRequireDefault(require("../settings/SettingsStore")); var _SettingLevel = require("../settings/SettingLevel"); /* Copyright 2024 New Vector Ltd. Copyright 2019-2021 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ // The time in ms that the crawler will wait loop iterations if there // have not been any checkpoints to consume in the last iteration. const CRAWLER_IDLE_TIME = 5000; // The maximum number of events our crawler should fetch in a single crawl. const EVENTS_PER_CRAWL = 100; /* * Event indexing class that wraps the platform specific event indexing. */ class EventIndex extends _events.EventEmitter { constructor(...args) { super(...args); (0, _defineProperty2.default)(this, "crawlerCheckpoints", []); (0, _defineProperty2.default)(this, "crawler", null); (0, _defineProperty2.default)(this, "currentCheckpoint", null); /* * The sync event listener. * * The listener has two cases: * - First sync after start up, check if the index is empty, add * initial checkpoints, if so. Start the crawler background task. * - Every other sync, tell the event index to commit all the queued up * live events */ (0, _defineProperty2.default)(this, "onSync", async (state, prevState, data) => { const indexManager = _PlatformPeg.default.get()?.getEventIndexingManager(); if (!indexManager) return; if (prevState === "PREPARED" && state === "SYNCING") { // If our indexer is empty we're most likely running Element the // first time with indexing support or running it with an // initial sync. Add checkpoints to crawl our encrypted rooms. const eventIndexWasEmpty = await indexManager.isEventIndexEmpty(); if (eventIndexWasEmpty) await this.addInitialCheckpoints(); this.startCrawler(); return; } if (prevState === "SYNCING" && state === "SYNCING") { // A sync was done, presumably we queued up some live events, // commit them now. await indexManager.commitLiveEvents(); } }); /* * The Room.timeline listener. * * This listener waits for live events in encrypted rooms, if they are * decrypted or unencrypted we queue them to be added to the index, * otherwise we save their event id and wait for them in the Event.decrypted * listener. */ (0, _defineProperty2.default)(this, "onRoomTimeline", async (ev, room, toStartOfTimeline, removed, data) => { if (!room) return; // notification timeline, we'll get this event again with a room specific timeline const client = _MatrixClientPeg.MatrixClientPeg.safeGet(); // We only index encrypted rooms locally. if (!client.isRoomEncrypted(ev.getRoomId())) return; if (ev.isRedaction()) { return this.redactEvent(ev); } // If it isn't a live event or if it's redacted there's nothing to do. if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) { return; } await client.decryptEventIfNeeded(ev); await this.addLiveEventToIndex(ev); }); (0, _defineProperty2.default)(this, "onRoomStateEvent", async (ev, state) => { if (!_MatrixClientPeg.MatrixClientPeg.safeGet().isRoomEncrypted(state.roomId)) return; if (ev.getType() === _matrix.EventType.RoomEncryption && !(await this.isRoomIndexed(state.roomId))) { _logger.logger.log("EventIndex: Adding a checkpoint for a newly encrypted room", state.roomId); this.addRoomCheckpoint(state.roomId, true); } }); /* * Removes a redacted event from our event index. * We cannot rely on Room.redaction as this only fires if the redaction applied to an event the js-sdk has loaded. */ (0, _defineProperty2.default)(this, "redactEvent", async ev => { const indexManager = _PlatformPeg.default.get()?.getEventIndexingManager(); if (!indexManager) return; const associatedId = ev.getAssociatedId(); if (!associatedId) return; try { await indexManager.deleteEvent(associatedId); } catch (e) { _logger.logger.log("EventIndex: Error deleting event from index", e); } }); /* * The Room.timelineReset listener. * * Listens for timeline resets that are caused by a limited timeline to * re-add checkpoints for rooms that need to be crawled again. */ (0, _defineProperty2.default)(this, "onTimelineReset", async room => { if (!room) return; if (!_MatrixClientPeg.MatrixClientPeg.safeGet().isRoomEncrypted(room.roomId)) return; _logger.logger.log("EventIndex: Adding a checkpoint because of a limited timeline", room.roomId); this.addRoomCheckpoint(room.roomId, false); }); } async init() { const indexManager = _PlatformPeg.default.get()?.getEventIndexingManager(); if (!indexManager) return; this.crawlerCheckpoints = await indexManager.loadCheckpoints(); _logger.logger.log("EventIndex: Loaded checkpoints", this.crawlerCheckpoints); this.registerListeners(); } /** * Register event listeners that are necessary for the event index to work. */ registerListeners() { const client = _MatrixClientPeg.MatrixClientPeg.safeGet(); client.on(_matrix.ClientEvent.Sync, this.onSync); client.on(_matrix.RoomEvent.Timeline, this.onRoomTimeline); client.on(_matrix.RoomEvent.TimelineReset, this.onTimelineReset); client.on(_matrix.RoomStateEvent.Events, this.onRoomStateEvent); } /** * Remove the event index specific event listeners. */ removeListeners() { const client = _MatrixClientPeg.MatrixClientPeg.get(); if (client === null) return; client.removeListener(_matrix.ClientEvent.Sync, this.onSync); client.removeListener(_matrix.RoomEvent.Timeline, this.onRoomTimeline); client.removeListener(_matrix.RoomEvent.TimelineReset, this.onTimelineReset); client.removeListener(_matrix.RoomStateEvent.Events, this.onRoomStateEvent); } /** * Get crawler checkpoints for the encrypted rooms and store them in the index. */ async addInitialCheckpoints() { const indexManager = _PlatformPeg.default.get()?.getEventIndexingManager(); if (!indexManager) return; const client = _MatrixClientPeg.MatrixClientPeg.safeGet(); const rooms = client.getRooms(); const isRoomEncrypted = room => { return client.isRoomEncrypted(room.roomId); }; // We only care to crawl the encrypted rooms, non-encrypted // rooms can use the search provided by the homeserver. const encryptedRooms = rooms.filter(isRoomEncrypted); _logger.logger.log("EventIndex: Adding initial crawler checkpoints"); // Gather the prev_batch tokens and create checkpoints for // our message crawler. await Promise.all(encryptedRooms.map(async room => { const timeline = room.getLiveTimeline(); const token = timeline.getPaginationToken(_matrix.Direction.Backward); const backCheckpoint = { roomId: room.roomId, token: token, direction: _matrix.Direction.Backward, fullCrawl: true }; const forwardCheckpoint = { roomId: room.roomId, token: token, direction: _matrix.Direction.Forward }; try { if (backCheckpoint.token) { await indexManager.addCrawlerCheckpoint(backCheckpoint); this.crawlerCheckpoints.push(backCheckpoint); } if (forwardCheckpoint.token) { await indexManager.addCrawlerCheckpoint(forwardCheckpoint); this.crawlerCheckpoints.push(forwardCheckpoint); } } catch (e) { _logger.logger.log("EventIndex: Error adding initial checkpoints for room", room.roomId, backCheckpoint, forwardCheckpoint, e); } })); } /** * Check if an event should be added to the event index. * * Most notably we filter events for which decryption failed, are redacted * or aren't of a type that we know how to index. * * @param {MatrixEvent} ev The event that should be checked. * @returns {bool} Returns true if the event can be indexed, false * otherwise. */ isValidEvent(ev) { const isUsefulType = [_matrix.EventType.RoomMessage, _matrix.EventType.RoomName, _matrix.EventType.RoomTopic].includes(ev.getType()); const validEventType = isUsefulType && !ev.isRedacted() && !ev.isDecryptionFailure(); let validMsgType = true; let hasContentValue = true; if (ev.getType() === _matrix.EventType.RoomMessage && !ev.isRedacted()) { // Expand this if there are more invalid msgtypes. const msgtype = ev.getContent().msgtype; if (!msgtype) validMsgType = false;else validMsgType = !msgtype.startsWith("m.key.verification"); if (!ev.getContent().body) hasContentValue = false; } else if (ev.getType() === _matrix.EventType.RoomTopic && !ev.isRedacted()) { if (!ev.getContent().topic) hasContentValue = false; } else if (ev.getType() === _matrix.EventType.RoomName && !ev.isRedacted()) { if (!ev.getContent().name) hasContentValue = false; } return validEventType && validMsgType && hasContentValue; } eventToJson(ev) { const e = ev.getEffectiveEvent(); if (ev.isEncrypted()) { // Let us store some additional data so we can re-verify the event. // The js-sdk checks if an event is encrypted using the algorithm, // the sender key and ed25519 signing key are used to find the // correct device that sent the event which allows us to check the // verification state of the event, either directly or using cross // signing. e.curve25519Key = ev.getSenderKey(); e.ed25519Key = ev.getClaimedEd25519Key(); e.algorithm = ev.getWireContent().algorithm; e.forwardingCurve25519KeyChain = ev.getForwardingCurve25519KeyChain(); } else { // Make sure that unencrypted events don't contain any of that data, // despite what the server might give to us. delete e.curve25519Key; delete e.ed25519Key; delete e.algorithm; delete e.forwardingCurve25519KeyChain; } return e; } /** * Queue up live events to be added to the event index. * * @param {MatrixEvent} ev The event that should be added to the index. */ async addLiveEventToIndex(ev) { const indexManager = _PlatformPeg.default.get()?.getEventIndexingManager(); if (!indexManager || !this.isValidEvent(ev)) return; const e = this.eventToJson(ev); const profile = { displayname: ev.sender?.rawDisplayName, avatar_url: ev.sender?.getMxcAvatarUrl() }; await indexManager.addEventToIndex(e, profile); } /** * Emmit that the crawler has changed the checkpoint that it's currently * handling. */ emitNewCheckpoint() { this.emit("changedCheckpoint", this.currentRoom()); } async addEventsFromLiveTimeline(timeline) { const events = timeline.getEvents(); for (let i = 0; i < events.length; i++) { const ev = events[i]; await this.addLiveEventToIndex(ev); } } async addRoomCheckpoint(roomId, fullCrawl = false) { const indexManager = _PlatformPeg.default.get()?.getEventIndexingManager(); if (!indexManager) return; const client = _MatrixClientPeg.MatrixClientPeg.safeGet(); const room = client.getRoom(roomId); if (!room) return; const timeline = room.getLiveTimeline(); const token = timeline.getPaginationToken(_matrix.Direction.Backward); if (!token) { // The room doesn't contain any tokens, meaning the live timeline // contains all the events, add those to the index. await this.addEventsFromLiveTimeline(timeline); return; } const checkpoint = { roomId: room.roomId, token: token, fullCrawl: fullCrawl, direction: _matrix.Direction.Backward }; _logger.logger.log("EventIndex: Adding checkpoint", checkpoint); try { await indexManager.addCrawlerCheckpoint(checkpoint); } catch (e) { _logger.logger.log("EventIndex: Error adding new checkpoint for room", room.roomId, checkpoint, e); } this.crawlerCheckpoints.push(checkpoint); } /** * The main crawler loop. * * Goes through crawlerCheckpoints and fetches events from the server to be * added to the EventIndex. * * If a /room/{roomId}/messages request doesn't contain any events, stop the * crawl, otherwise create a new checkpoint and push it to the * crawlerCheckpoints queue, so we go through them in a round-robin way. */ async crawlerFunc() { let cancelled = false; const client = _MatrixClientPeg.MatrixClientPeg.safeGet(); const indexManager = _PlatformPeg.default.get()?.getEventIndexingManager(); if (!indexManager) return; this.crawler = { cancel: () => { cancelled = true; } }; let idle = false; while (!cancelled) { let sleepTime = _SettingsStore.default.getValueAt(_SettingLevel.SettingLevel.DEVICE, "crawlerSleepTime"); // Don't let the user configure a lower sleep time than 100 ms. sleepTime = Math.max(sleepTime, 100); if (idle) { sleepTime = CRAWLER_IDLE_TIME; } if (this.currentCheckpoint !== null) { this.currentCheckpoint = null; this.emitNewCheckpoint(); } await (0, _utils.sleep)(sleepTime); if (cancelled) { break; } const checkpoint = this.crawlerCheckpoints.shift(); /// There is no checkpoint available currently, one may appear if // a sync with limited room timelines happens, so go back to sleep. if (checkpoint === undefined) { idle = true; continue; } this.currentCheckpoint = checkpoint; this.emitNewCheckpoint(); idle = false; // We have a checkpoint, let us fetch some messages, again, very // conservatively to not bother our homeserver too much. const eventMapper = client.getEventMapper({ preventReEmit: true }); // TODO we need to ensure to use member lazy loading with this // request so we get the correct profiles. let res; try { res = await client.createMessagesRequest(checkpoint.roomId, checkpoint.token, EVENTS_PER_CRAWL, checkpoint.direction); } catch (e) { if (e instanceof _matrix.HTTPError && e.httpStatus === 403) { _logger.logger.log("EventIndex: Removing checkpoint as we don't have ", "permissions to fetch messages from this room.", checkpoint); try { await indexManager.removeCrawlerCheckpoint(checkpoint); } catch (e) { _logger.logger.log("EventIndex: Error removing checkpoint", checkpoint, e); // We don't push the checkpoint here back, it will // hopefully be removed after a restart. But let us // ignore it for now as we don't want to hammer the // endpoint. } continue; } _logger.logger.log("EventIndex: Error crawling using checkpoint:", checkpoint, ",", e); this.crawlerCheckpoints.push(checkpoint); continue; } if (cancelled) { this.crawlerCheckpoints.push(checkpoint); break; } if (res.chunk.length === 0) { _logger.logger.log("EventIndex: Done with the checkpoint", checkpoint); // We got to the start/end of our timeline, lets just // delete our checkpoint and go back to sleep. try { await indexManager.removeCrawlerCheckpoint(checkpoint); } catch (e) { _logger.logger.log("EventIndex: Error removing checkpoint", checkpoint, e); } continue; } // Convert the plain JSON events into Matrix events so they get // decrypted if necessary. const matrixEvents = res.chunk.map(eventMapper); let stateEvents = []; if (res.state !== undefined) { stateEvents = res.state.map(eventMapper); } const profiles = {}; stateEvents.forEach(ev => { if (ev.getContent().membership === _types.KnownMembership.Join) { profiles[ev.getSender()] = { displayname: ev.getContent().displayname, avatar_url: ev.getContent().avatar_url }; } }); const decryptionPromises = matrixEvents.filter(event => event.isEncrypted()).map(event => { return client.decryptEventIfNeeded(event, { emit: false }); }); // Let us wait for all the events to get decrypted. await Promise.all(decryptionPromises); // TODO if there are no events at this point we're missing a lot // decryption keys, do we want to retry this checkpoint at a later // stage? const filteredEvents = matrixEvents.filter(this.isValidEvent); // Collect the redaction events, so we can delete the redacted events from the index. const redactionEvents = matrixEvents.filter(ev => ev.isRedaction()); // Let us convert the events back into a format that EventIndex can // consume. const events = filteredEvents.map(ev => { const e = this.eventToJson(ev); let profile = {}; if (e.sender in profiles) profile = profiles[e.sender]; const object = { event: e, profile: profile }; return object; }); let newCheckpoint = null; // The token can be null for some reason. Don't create a checkpoint // in that case since adding it to the db will fail. if (res.end) { // Create a new checkpoint so we can continue crawling the room // for messages. newCheckpoint = { roomId: checkpoint.roomId, token: res.end, fullCrawl: checkpoint.fullCrawl, direction: checkpoint.direction }; } try { for (let i = 0; i < redactionEvents.length; i++) { const ev = redactionEvents[i]; const eventId = ev.getAssociatedId(); if (eventId) { await indexManager.deleteEvent(eventId); } else { _logger.logger.warn("EventIndex: Redaction event doesn't contain a valid associated event id", ev); } } const eventsAlreadyAdded = await indexManager.addHistoricEvents(events, newCheckpoint, checkpoint); // We didn't get a valid new checkpoint from the server, nothing // to do here anymore. if (!newCheckpoint) { _logger.logger.log("EventIndex: The server didn't return a valid ", "new checkpoint, not continuing the crawl.", checkpoint); continue; } // If all events were already indexed we assume that we caught // up with our index and don't need to crawl the room further. // Let us delete the checkpoint in that case, otherwise push // the new checkpoint to be used by the crawler. if (eventsAlreadyAdded === true && newCheckpoint.fullCrawl !== true) { _logger.logger.log("EventIndex: Checkpoint had already all events", "added, stopping the crawl", checkpoint); await indexManager.removeCrawlerCheckpoint(newCheckpoint); } else { if (eventsAlreadyAdded === true) { _logger.logger.log("EventIndex: Checkpoint had already all events", "added, but continuing due to a full crawl", checkpoint); } this.crawlerCheckpoints.push(newCheckpoint); } } catch (e) { _logger.logger.log("EventIndex: Error during a crawl", e); // An error occurred, put the checkpoint back so we // can retry. this.crawlerCheckpoints.push(checkpoint); } } this.crawler = null; } /** * Start the crawler background task. */ startCrawler() { if (this.crawler !== null) return; this.crawlerFunc(); } /** * Stop the crawler background task. */ stopCrawler() { if (this.crawler === null) return; this.crawler.cancel(); } /** * Close the event index. * * This removes all the MatrixClient event listeners, stops the crawler * task, and closes the index. */ async close() { const indexManager = _PlatformPeg.default.get()?.getEventIndexingManager(); this.removeListeners(); this.stopCrawler(); await indexManager?.closeEventIndex(); } /** * Search the event index using the given term for matching events. * * @param {ISearchArgs} searchArgs The search configuration for the search, * sets the search term and determines the search result contents. * * @return {Promise<IResultRoomEvents[]>} A promise that will resolve to an array * of search results once the search is done. */ async search(searchArgs) { const indexManager = _PlatformPeg.default.get()?.getEventIndexingManager(); return indexManager?.searchEventIndex(searchArgs); } /** * Load events that contain URLs from the event index. * * @param {Room} room The room for which we should fetch events containing * URLs * * @param {number} limit The maximum number of events to fetch. * * @param {string} fromEvent From which event should we continue fetching * events from the index. This is only needed if we're continuing to fill * the timeline, e.g. if we're paginating. This needs to be set to a event * id of an event that was previously fetched with this function. * * @param {string} direction The direction in which we will continue * fetching events. EventTimeline.BACKWARDS to continue fetching events that * are older than the event given in fromEvent, EventTimeline.FORWARDS to * fetch newer events. * * @returns {Promise<MatrixEvent[]>} Resolves to an array of events that * contain URLs. */ async loadFileEvents(room, limit = 10, fromEvent, direction = _matrix.EventTimeline.BACKWARDS) { const client = _MatrixClientPeg.MatrixClientPeg.safeGet(); const indexManager = _PlatformPeg.default.get()?.getEventIndexingManager(); if (!indexManager) return []; const loadArgs = { roomId: room.roomId, limit: limit }; if (fromEvent) { loadArgs.fromEvent = fromEvent; loadArgs.direction = direction; } let events; // Get our events from the event index. try { events = await indexManager.loadFileEvents(loadArgs); } catch (e) { _logger.logger.log("EventIndex: Error getting file events", e); return []; } const eventMapper = client.getEventMapper(); // Turn the events into MatrixEvent objects. const matrixEvents = events.map(e => { const matrixEvent = eventMapper(e.event); const member = new _matrix.RoomMember(room.roomId, matrixEvent.getSender()); // We can't really reconstruct the whole room state from our // EventIndex to calculate the correct display name. Use the // disambiguated form always instead. member.name = e.profile.displayname + " (" + matrixEvent.getSender() + ")"; // This is sets the avatar URL. const memberEvent = eventMapper({ content: { membership: _types.KnownMembership.Join, avatar_url: e.profile.avatar_url, displayname: e.profile.displayname }, type: _matrix.EventType.RoomMember, event_id: matrixEvent.getId() + ":eventIndex", room_id: matrixEvent.getRoomId(), sender: matrixEvent.getSender(), origin_server_ts: matrixEvent.getTs(), state_key: matrixEvent.getSender() }); // We set this manually to avoid emitting RoomMember.membership and // RoomMember.name events. member.events.member = memberEvent; matrixEvent.sender = member; return matrixEvent; }); return matrixEvents; } /** * Fill a timeline with events that contain URLs. * * @param {TimelineSet} timelineSet The TimelineSet the Timeline belongs to, * used to check if we're adding duplicate events. * * @param {Timeline} timeline The Timeline which should be filed with * events. * * @param {Room} room The room for which we should fetch events containing * URLs * * @param {number} limit The maximum number of events to fetch. * * @param {string} fromEvent From which event should we continue fetching * events from the index. This is only needed if we're continuing to fill * the timeline, e.g. if we're paginating. This needs to be set to a event * id of an event that was previously fetched with this function. * * @param {string} direction The direction in which we will continue * fetching events. EventTimeline.BACKWARDS to continue fetching events that * are older than the event given in fromEvent, EventTimeline.FORWARDS to * fetch newer events. * * @returns {Promise<boolean>} Resolves to true if events were added to the * timeline, false otherwise. */ async populateFileTimeline(timelineSet, timeline, room, limit = 10, fromEvent, direction = _matrix.EventTimeline.BACKWARDS) { const matrixEvents = await this.loadFileEvents(room, limit, fromEvent, direction); // If this is a normal fill request, not a pagination request, we need // to get our events in the BACKWARDS direction but populate them in the // forwards direction. // This needs to happen because a fill request might come with an // existing timeline e.g. if you close and re-open the FilePanel. if (fromEvent === null) { matrixEvents.reverse(); direction = direction == _matrix.EventTimeline.BACKWARDS ? _matrix.EventTimeline.FORWARDS : _matrix.EventTimeline.BACKWARDS; } // Add the events to the timeline of the file panel. matrixEvents.forEach(e => { if (!timelineSet.eventIdToTimeline(e.getId())) { timelineSet.addEventToTimeline(e, timeline, direction == _matrix.EventTimeline.BACKWARDS); } }); let ret = false; let paginationToken = ""; // Set the pagination token to the oldest event that we retrieved. if (matrixEvents.length > 0) { paginationToken = matrixEvents[matrixEvents.length - 1].getId(); ret = true; } _logger.logger.log("EventIndex: Populating file panel with", matrixEvents.length, "events and setting the pagination token to", paginationToken); timeline.setPaginationToken(paginationToken, _matrix.EventTimeline.BACKWARDS); return ret; } /** * Emulate a TimelineWindow pagination() request with the event index as the event source * * Might not fetch events from the index if the timeline already contains * events that the window isn't showing. * * @param {Room} room The room for which we should fetch events containing * URLs * * @param {TimelineWindow} timelineWindow The timeline window that should be * populated with new events. * * @param {string} direction The direction in which we should paginate. * EventTimeline.BACKWARDS to paginate back, EventTimeline.FORWARDS to * paginate forwards. * * @param {number} limit The maximum number of events to fetch while * paginating. * * @returns {Promise<boolean>} Resolves to a boolean which is true if more * events were successfully retrieved. */ paginateTimelineWindow(room, timelineWindow, direction, limit) { const tl = timelineWindow.getTimelineIndex(direction); if (!tl) return Promise.resolve(false); if (tl.pendingPaginate) return tl.pendingPaginate; if (timelineWindow.extend(direction, limit)) { return Promise.resolve(true); } const paginationMethod = async (timelineWindow, timelineIndex, room, direction, limit) => { const timeline = timelineIndex.timeline; const timelineSet = timeline.getTimelineSet(); const token = timeline.getPaginationToken(direction) ?? undefined; const ret = await this.populateFileTimeline(timelineSet, timeline, room, limit, token, direction); timelineIndex.pendingPaginate = undefined; timelineWindow.extend(direction, limit); return ret; }; const paginationPromise = paginationMethod(timelineWindow, tl, room, direction, limit); tl.pendingPaginate = paginationPromise; return paginationPromise; } /** * Get statistical information of the index. * * @return {Promise<IIndexStats>} A promise that will resolve to the index * statistics. */ async getStats() { const indexManager = _PlatformPeg.default.get()?.getEventIndexingManager(); return indexManager?.getStats(); } /** * Check if the room with the given id is already indexed. * * @param {string} roomId The ID of the room which we want to check if it * has been already indexed. * * @return {Promise<boolean>} Returns true if the index contains events for * the given room, false otherwise. */ async isRoomIndexed(roomId) { const indexManager = _PlatformPeg.default.get()?.getEventIndexingManager(); return indexManager?.isRoomIndexed(roomId); } /** * Get the room that we are currently crawling. * * @returns {Room} A MatrixRoom that is being currently crawled, null * if no room is currently being crawled. */ currentRoom() { if (this.currentCheckpoint === null && this.crawlerCheckpoints.length === 0) { return null; } const client = _MatrixClientPeg.MatrixClientPeg.safeGet(); if (this.currentCheckpoint !== null) { return client.getRoom(this.currentCheckpoint.roomId); } else { return client.getRoom(this.crawlerCheckpoints[0].roomId); } } crawlingRooms() { const totalRooms = new Set(); const crawlingRooms = new Set(); this.crawlerCheckpoints.forEach((checkpoint, index) => { crawlingRooms.add(checkpoint.roomId); }); if (this.currentCheckpoint !== null) { crawlingRooms.add(this.currentCheckpoint.roomId); } const client = _MatrixClientPeg.MatrixClientPeg.safeGet(); const rooms = client.getRooms(); const isRoomEncrypted = room => { return client.isRoomEncrypted(room.roomId); }; const encryptedRooms = rooms.filter(isRoomEncrypted); encryptedRooms.forEach((room, index) => { totalRooms.add(room.roomId); }); return { crawlingRooms, totalRooms }; } } exports.default = EventIndex; //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJfZXZlbnRzIiwicmVxdWlyZSIsIl9tYXRyaXgiLCJfdHlwZXMiLCJfdXRpbHMiLCJfbG9nZ2VyIiwiX1BsYXRmb3JtUGVnIiwiX2ludGVyb3BSZXF1aXJlRGVmYXVsdCIsIl9NYXRyaXhDbGllbnRQZWciLCJfU2V0dGluZ3NTdG9yZSIsIl9TZXR0aW5nTGV2ZWwiLCJDUkFXTEVSX0lETEVfVElNRSIsIkVWRU5UU19QRVJfQ1JBV0wiLCJFdmVudEluZGV4IiwiRXZlbnRFbWl0dGVyIiwiY29uc3RydWN0b3IiLCJhcmdzIiwiX2RlZmluZVByb3BlcnR5MiIsImRlZmF1bHQiLCJzdGF0ZSIsInByZXZTdGF0ZSIsImRhdGEiLCJpbmRleE1hbmFnZXIiLCJQbGF0Zm9ybVBlZyIsImdldCIsImdldEV2ZW50SW5kZXhpbmdNYW5hZ2VyIiwiZXZlbnRJbmRleFdhc0VtcHR5IiwiaXNFdmVudEluZGV4RW1wdHkiLCJhZGRJbml0aWFsQ2hlY2twb2ludHMiLCJzdGFydENyYXdsZXIiLCJjb21taXRMaXZlRXZlbnRzIiwiZXYiLCJyb29tIiwidG9TdGFydE9mVGltZWxpbmUiLCJyZW1vdmVkIiwiY2xpZW50IiwiTWF0cml4Q2xpZW50UGVnIiwic2FmZUdldCIsImlzUm9vbUVuY3J5cHRlZCIsImdldFJvb21JZCIsImlzUmVkYWN0aW9uIiwicmVkYWN0RXZlbnQiLCJsaXZlRXZlbnQiLCJpc1JlZGFjdGVkIiwiZGVjcnlwdEV2ZW50SWZOZWVkZWQiLCJhZGRMaXZlRXZlbnRUb0luZGV4Iiwicm9vbUlkIiwiZ2V0VHlwZSIsIkV2ZW50VHlwZSIsIlJvb21FbmNyeXB0aW9uIiwiaXNSb29tSW5kZXhlZCIsImxvZ2dlciIsImxvZyIsImFkZFJvb21DaGVja3BvaW50IiwiYXNzb2NpYXRlZElkIiwiZ2V0QXNzb2NpYXRlZElkIiwiZGVsZXRlRXZlbnQiLCJlIiwiaW5pdCIsImNyYXdsZXJDaGVja3BvaW50cyIsImxvYWRDaGVja3BvaW50cyIsInJlZ2lzdGVyTGlzdGVuZXJzIiwib24iLCJDbGllbnRFdmVudCIsIlN5bmMiLCJvblN5bmMiLCJSb29tRXZlbnQiLCJUaW1lbGluZSIsIm9uUm9vbVRpbWVsaW5lIiwiVGltZWxpbmVSZXNldCIsIm9uVGltZWxpbmVSZXNldCIsIlJvb21TdGF0ZUV2ZW50IiwiRXZlbnRzIiwib25Sb29tU3RhdGVFdmVudCIsInJlbW92ZUxpc3RlbmVycyIsInJlbW92ZUxpc3RlbmVyIiwicm9vbXMiLCJnZXRSb29tcyIsImVuY3J5cHRlZFJvb21zIiwiZmlsdGVyIiwiUHJvbWlzZSIsImFsbCIsIm1hcCIsInRpbWVsaW5lIiwiZ2V0TGl2ZVRpbWVsaW5lIiwidG9rZW4iLCJnZXRQYWdpbmF0aW9uVG9rZW4iLCJEaXJlY3Rpb24iLCJCYWNrd2FyZCIsImJhY2tDaGVja3BvaW50IiwiZGlyZWN0aW9uIiwiZnVsbENyYXdsIiwiZm9yd2FyZENoZWNrcG9pbnQiLCJGb3J3YXJkIiwiYWRkQ3Jhd2xlckNoZWNrcG9pbnQiLCJwdXNoIiwiaXNWYWxpZEV2ZW50IiwiaXNVc2VmdWxUeXBlIiwiUm9vbU1lc3NhZ2UiLCJSb29tTmFtZSIsIlJvb21Ub3BpYyIsImluY2x1ZGVzIiwidmFsaWRFdmVudFR5cGUiLCJpc0RlY3J5cHRpb25GYWlsdXJlIiwidmFsaWRNc2dUeXBlIiwiaGFzQ29udGVudFZhbHVlIiwibXNndHlwZSIsImdldENvbnRlbnQiLCJzdGFydHNXaXRoIiwiYm9keSIsInRvcGljIiwibmFtZSIsImV2ZW50VG9Kc29uIiwiZ2V0RWZmZWN0aXZlRXZlbnQiLCJpc0VuY3J5cHRlZCIsImN1cnZlMjU1MTlLZXkiLCJnZXRTZW5kZXJLZXkiLCJlZDI1NTE5S2V5IiwiZ2V0Q2xhaW1lZEVkMjU1MTlLZXkiLCJhbGdvcml0aG0iLCJnZXRXaXJlQ29udGVudCIsImZvcndhcmRpbmdDdXJ2ZTI1NTE5S2V5Q2hhaW4iLCJnZXRGb3J3YXJkaW5nQ3VydmUyNTUxOUtleUNoYWluIiwicHJvZmlsZSIsImRpc3BsYXluYW1lIiwic2VuZGVyIiwicmF3RGlzcGxheU5hbWUiLCJhdmF0YXJfdXJsIiwiZ2V0TXhjQXZhdGFyVXJsIiwiYWRkRXZlbnRUb0luZGV4IiwiZW1pdE5ld0NoZWNrcG9pbnQiLCJlbWl0IiwiY3VycmVudFJvb20iLCJhZGRFdmVudHNGcm9tTGl2ZVRpbWVsaW5lIiwiZXZlbnRzIiwiZ2V0RXZlbnRzIiwiaSIsImxlbmd0aCIsImdldFJvb20iLCJjaGVja3BvaW50IiwiY3Jhd2xlckZ1bmMiLCJjYW5jZWxsZWQiLCJjcmF3bGVyIiwiY2FuY2VsIiwiaWRsZSIsInNsZWVwVGltZSIsIlNldHRpbmdzU3RvcmUiLCJnZXRWYWx1ZUF0IiwiU2V0dGluZ0xldmVsIiwiREVWSUNFIiwiTWF0aCIsIm1heCIsImN1cnJlbnRDaGVja3BvaW50Iiwic2xlZXAiLCJzaGlmdCIsInVuZGVmaW5lZCIsImV2ZW50TWFwcGVyIiwiZ2V0RXZlbnRNYXBwZXIiLCJwcmV2ZW50UmVFbWl0IiwicmVzIiwiY3JlYXRlTWVzc2FnZXNSZXF1ZXN0IiwiSFRUUEVycm9yIiwiaHR0cFN0YXR1cyIsInJlbW92ZUNyYXdsZXJDaGVja3BvaW50IiwiY2h1bmsiLCJtYXRyaXhFdmVudHMiLCJzdGF0ZUV2ZW50cyIsInByb2ZpbGVzIiwiZm9yRWFjaCIsIm1lbWJlcnNoaXAiLCJLbm93bk1lbWJlcnNoaXAiLCJKb2luIiwiZ2V0U2VuZGVyIiwiZGVjcnlwdGlvblByb21pc2VzIiwiZXZlbnQiLCJmaWx0ZXJlZEV2ZW50cyIsInJlZGFjdGlvbkV2ZW50cyIsIm9iamVjdCIsIm5ld0NoZWNrcG9pbnQiLCJlbmQiLCJldmVudElkIiwid2FybiIsImV2ZW50c0FscmVhZHlBZGRlZCIsImFkZEhpc3RvcmljRXZlbnRzIiwic3RvcENyYXdsZXIiLCJjbG9zZSIsImNsb3NlRXZlbnRJbmRleCIsInNlYXJjaCIsInNlYXJjaEFyZ3MiLCJzZWFyY2hFdmVudEluZGV4IiwibG9hZEZpbGVFdmVudHMiLCJsaW1pdCIsImZyb21FdmVudCIsIkV2ZW50VGltZWxpbmUiLCJCQUNLV0FSRFMiLCJsb2FkQXJncyIsIm1hdHJpeEV2ZW50IiwibWVtYmVyIiwiUm9vbU1lbWJlciIsIm1lbWJlckV2ZW50IiwiY29udGVudCIsInR5cGUiLCJldmVudF9pZCIsImdldElkIiwicm9vbV9pZCIsIm9yaWdpbl9zZXJ2ZXJfdHMiLCJnZXRUcyIsInN0YXRlX2tleSIsInBvcHVsYXRlRmlsZVRpbWVsaW5lIiwidGltZWxpbmVTZXQiLCJyZXZlcnNlIiwiRk9SV0FSRFMiLCJldmVudElkVG9UaW1lbGluZSIsImFkZEV2ZW50VG9UaW1lbGluZSIsInJldCIsInBhZ2luYXRpb25Ub2tlbiIsInNldFBhZ2luYXRpb25Ub2tlbiIsInBhZ2luYXRlVGltZWxpbmVXaW5kb3ciLCJ0aW1lbGluZVdpbmRvdyIsInRsIiwiZ2V0VGltZWxpbmVJbmRleCIsInJlc29sdmUiLCJwZW5kaW5nUGFnaW5hdGUiLCJleHRlbmQiLCJwYWdpbmF0aW9uTWV0aG9kIiwidGltZWxpbmVJbmRleCIsImdldFRpbWVsaW5lU2V0IiwicGFnaW5hdGlvblByb21pc2UiLCJnZXRTdGF0cyIsImNyYXdsaW5nUm9vbXMiLCJ0b3RhbFJvb21zIiwiU2V0IiwiaW5kZXgiLCJhZGQiLCJleHBvcnRzIl0sInNvdXJjZXMiOlsiLi4vLi4vc3JjL2luZGV4aW5nL0V2ZW50SW5kZXgudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLypcbkNvcHlyaWdodCAyMDI0IE5ldyBWZWN0b3IgTHRkLlxuQ29weXJpZ2h0IDIwMTktMjAyMSBUaGUgTWF0cml4Lm9yZyBGb3VuZGF0aW9uIEMuSS5DLlxuXG5TUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogQUdQTC0zLjAtb25seSBPUiBHUEwtMy4wLW9ubHlcblBsZWFzZSBzZWUgTElDRU5TRSBmaWxlcyBpbiB0aGUgcmVwb3NpdG9yeSByb290IGZvciBmdWxsIGRldGFpbHMuXG4qL1xuXG5pbXBvcnQgeyBFdmVudEVtaXR0ZXIgfSBmcm9tIFwiZXZlbnRzXCI7XG5pbXBvcnQge1xuICAgIFJvb21NZW1iZXIsXG4gICAgUm9vbSxcbiAgICBSb29tRXZlbnQsXG4gICAgUm9vbVN0YXRlLFxuICAgIFJvb21TdGF0ZUV2ZW50LFxuICAgIE1hdHJpeEV2ZW50LFxuICAgIERpcmVjdGlvbixcbiAgICBFdmVudFRpbWVsaW5lLFxuICAgIEV2ZW50VGltZWxpbmVTZXQsXG4gICAgSVJvb21UaW1lbGluZURhdGEsXG4gICAgRXZlbnRUeXBlLFxuICAgIENsaWVudEV2ZW50LFxuICAgIE1hdHJpeENsaWVudCxcbiAgICBIVFRQRXJyb3IsXG4gICAgSUV2ZW50V2l0aFJvb21JZCxcbiAgICBJTWF0cml4UHJvZmlsZSxcbiAgICBJUmVzdWx0Um9vbUV2ZW50cyxcbiAgICBTeW5jU3RhdGVEYXRhLFxuICAgIFN5bmNTdGF0ZSxcbiAgICBUaW1lbGluZUluZGV4LFxuICAgIFRpbWVsaW5lV2luZG93LFxufSBmcm9tIFwibWF0cml4LWpzLXNkay9zcmMvbWF0cml4XCI7XG5pbXBvcnQgeyBLbm93bk1lbWJlcnNoaXAgfSBmcm9tIFwibWF0cml4LWpzLXNkay9zcmMvdHlwZXNcIjtcbmltcG9ydCB7IHNsZWVwIH0gZnJvbSBcIm1hdHJpeC1qcy1zZGsvc3JjL3V0aWxzXCI7XG5pbXBvcnQgeyBsb2dnZXIgfSBmcm9tIFwibWF0cml4LWpzLXNkay9zcmMvbG9nZ2VyXCI7XG5cbmltcG9ydCBQbGF0Zm9ybVBlZyBmcm9tIFwiLi4vUGxhdGZvcm1QZWdcIjtcbmltcG9ydCB7IE1hdHJpeENsaWVudFBlZyB9IGZyb20gXCIuLi9NYXRyaXhDbGllbnRQZWdcIjtcbmltcG9ydCBTZXR0aW5nc1N0b3JlIGZyb20gXCIuLi9zZXR0aW5ncy9TZXR0aW5nc1N0b3JlXCI7XG5pbXBvcnQgeyBTZXR0aW5nTGV2ZWwgfSBmcm9tIFwiLi4vc2V0dGluZ3MvU2V0dGluZ0xldmVsXCI7XG5pbXBvcnQgeyBJQ3Jhd2xlckNoZWNrcG9pbnQsIElFdmVudEFuZFByb2ZpbGUsIElJbmRleFN0YXRzLCBJTG9hZEFyZ3MsIElTZWFyY2hBcmdzIH0gZnJvbSBcIi4vQmFzZUV2ZW50SW5kZXhNYW5hZ2VyXCI7XG5cbi8vIFRoZSB0aW1lIGluIG1zIHRoYXQgdGhlIGNyYXdsZXIgd2lsbCB3YWl0IGxvb3AgaXRlcmF0aW9ucyBpZiB0aGVyZVxuLy8gaGF2ZSBub3QgYmVlbiBhbnkgY2hlY2twb2ludHMgdG8gY29uc3VtZSBpbiB0aGUgbGFzdCBpdGVyYXRpb24uXG5jb25zdCBDUkFXTEVSX0lETEVfVElNRSA9IDUwMDA7XG5cbi8vIFRoZSBtYXhpbXVtIG51bWJlciBvZiBldmVudHMgb3VyIGNyYXdsZXIgc2hvdWxkIGZldGNoIGluIGEgc2luZ2xlIGNyYXdsLlxuY29uc3QgRVZFTlRTX1BFUl9DUkFXTCA9IDEwMDtcblxuaW50ZXJmYWNlIElDcmF3bGVyIHtcbiAgICBjYW5jZWwoKTogdm9pZDtcbn1cblxuLypcbiAqIEV2ZW50IGluZGV4aW5nIGNsYXNzIHRoYXQgd3JhcHMgdGhlIHBsYXRmb3JtIHNwZWNpZmljIGV2ZW50IGluZGV4aW5nLlxuICovXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBFdmVudEluZGV4IGV4dGVuZHMgRXZlbnRFbWl0dGVyIHtcbiAgICBwcml2YXRlIGNyYXdsZXJDaGVja3BvaW50czogSUNyYXdsZXJDaGVja3BvaW50W10gPSBbXTtcbiAgICBwcml2YXRlIGNyYXdsZXI6IElDcmF3bGVyIHwgbnVsbCA9IG51bGw7XG4gICAgcHJpdmF0ZSBjdXJyZW50Q2hlY2twb2ludDogSUNyYXdsZXJDaGVja3BvaW50IHwgbnVsbCA9IG51bGw7XG5cbiAgICBwdWJsaWMgYXN5bmMgaW5pdCgpOiBQcm9taXNlPHZvaWQ+IHtcbiAgICAgICAgY29uc3QgaW5kZXhNYW5hZ2VyID0gUGxhdGZvcm1QZWcuZ2V0KCk/LmdldEV2ZW50SW5kZXhpbmdNYW5hZ2VyKCk7XG4gICAgICAgIGlmICghaW5kZXhNYW5hZ2VyKSByZXR1cm47XG5cbiAgICAgICAgdGhpcy5jcmF3bGVyQ2hlY2twb2ludHMgPSBhd2FpdCBpbmRleE1hbmFnZXIubG9hZENoZWNrcG9pbnRzKCk7XG4gICAgICAgIGxvZ2dlci5sb2coXCJFdmVudEluZGV4OiBMb2FkZWQgY2hlY2twb2ludHNcIiwgdGhpcy5jcmF3bGVyQ2hlY2twb2ludHMpO1xuXG4gICAgICAgIHRoaXMucmVnaXN0ZXJMaXN0ZW5lcnMoKTtcbiAgICB9XG5cbiAgICAvKipcbiAgICAgKiBSZWdpc3RlciBldmVudCBsaXN0ZW5lcnMgdGhhdCBhcmUgbmVjZXNzYXJ5IGZvciB0aGUgZXZlbnQgaW5kZXggdG8gd29yay5cbiAgICAgKi9cbiAgICBwdWJsaWMgcmVnaXN0ZXJMaXN0ZW5lcnMoKTogdm9pZCB7XG4gICAgICAgIGNvbnN0IGNsaWVudCA9IE1hdHJpeENsaWVudFBlZy5zYWZlR2V0KCk7XG5cbiAgICAgICAgY2xpZW50Lm9uKENsaWVudEV2ZW50LlN5bmMsIHRoaXMub25TeW5jKTtcbiAgICAgICAgY2xpZW50Lm9uKFJvb21FdmVudC5UaW1lbGluZSwgdGhpcy5vblJvb21UaW1lbGluZSk7XG4gICAgICAgIGNsaWVudC5vbihSb29tRXZlbnQuVGltZWxpbmVSZXNldCwgdGhpcy5vblRpbWVsaW5lUmVzZXQpO1xuICAgICAgICBjbGllbnQub24oUm9vbVN0YXRlRXZlbnQuRXZlbnRzLCB0aGlzLm9uUm9vbVN0YXRlRXZlbnQpO1xuICAgIH1cblxuICAgIC8qKlxuICAgICAqIFJlbW92ZSB0aGUgZXZlbnQgaW5kZXggc3BlY2lmaWMgZXZlbnQgbGlzdGVuZXJzLlxuICAgICAqL1xuICAgIHB1YmxpYyByZW1vdmVMaXN0ZW5lcnMoKTogdm9pZCB7XG4gICAgICAgIGNvbnN0IGNsaWVudCA9IE1hdHJpeENsaWVudFBlZy5nZXQoKTtcbiAgICAgICAgaWYgKGNsaWVudCA9PT0gbnVsbCkgcmV0dXJuO1xuXG4gICAgICAgIGNsaWVudC5yZW1vdmVMaXN0ZW5lcihDbGllbnRFdmVudC5TeW5jLCB0aGlzLm9uU3luYyk7XG4gICAgICAgIGNsaWVudC5yZW1vdmVMaXN0ZW5lcihSb29tRXZlbnQuVGltZWxpbmUsIHRoaXMub25Sb29tVGltZWxpbmUpO1xuICAgICAgICBjbGllbnQucmVtb3ZlTGlzdGVuZXIoUm9vbUV2ZW50LlRpbWVsaW5lUmVzZXQsIHRoaXMub25UaW1lbGluZVJlc2V0KTtcbiAgICAgICAgY2xpZW50LnJlbW92ZUxpc3RlbmVyKFJvb21TdGF0ZUV2ZW50LkV2ZW50cywgdGhpcy5vblJvb21TdGF0ZUV2ZW50KTtcbiAgICB9XG5cbiAgICAvKipcbiAgICAgKiBHZXQgY3Jhd2xlciBjaGVja3BvaW50cyBmb3IgdGhlIGVuY3J5cHRlZCByb29tcyBhbmQgc3RvcmUgdGhlbSBpbiB0aGUgaW5kZXguXG4gICAgICovXG4gICAgcHVibGljIGFzeW5jIGFkZEluaXRpYWxDaGVja3BvaW50cygpOiBQcm9taXNlPHZvaWQ+IHtcbiAgICAgICAgY29uc3QgaW5kZXhNYW5hZ2VyID0gUGxhdGZvcm1QZWcuZ2V0KCk/LmdldEV2ZW50SW5kZXhpbmdNYW5hZ2VyKCk7XG4gICAgICAgIGlmICghaW5kZXhNYW5hZ2VyKSByZXR1cm47XG4gICAgICAgIGNvbnN0IGNsaWVudCA9IE1hdHJpeENsaWVudFBlZy5zYWZlR2V0KCk7XG4gICAgICAgIGNvbnN0IHJvb21zID0gY2xpZW50LmdldFJvb21zKCk7XG5cbiAgICAgICAgY29uc3QgaXNSb29tRW5jcnlwdGVkID0gKHJvb206IFJvb20pOiBib29sZWFuID0+IHtcbiAgICAgICAgICAgIHJldHVybiBjbGllbnQuaXNSb29tRW5jcnlwdGVkKHJvb20ucm9vbUlkKTtcbiAgICAgICAgfTtcblxuICAgICAgICAvLyBXZSBvbmx5IGNhcmUgdG8gY3Jhd2wgdGhlIGVuY3J5cHRlZCByb29tcywgbm9uLWVuY3J5cHRlZFxuICAgICAgICAvLyByb29tcyBjYW4gdXNlIHRoZSBzZWFyY2ggcHJvdmlkZWQgYnkgdGhlIGhvbWVzZXJ2ZXIuXG4gICAgICAgIGNvbnN0IGVuY3J5cHRlZFJvb21zID0gcm9vbXMuZmlsdGVyKGlzUm9vbUVuY3J5cHRlZCk7XG5cbiAgICAgICAgbG9nZ2VyLmxvZyhcIkV2ZW50SW5kZXg6IEFkZGluZyBpbml0aWFsIGNyYXdsZXIgY2hlY2twb2ludHNcIik7XG5cbiAgICAgICAgLy8gR2F0aGVyIHRoZSBwcmV2X2JhdGNoIHRva2VucyBhbmQgY3JlYXRlIGNoZWNrcG9pbnRzIGZvclxuICAgICAgICAvLyBvdXIgbWVzc2FnZSBjcmF3bGVyLlxuICAgICAgICBhd2FpdCBQcm9taXNlLmFsbChcbiAgICAgICAgICAgIGVuY3J5cHRlZFJvb21zLm1hcChhc3luYyAocm9vbSk6IFByb21pc2U8dm9pZD4gPT4ge1xuICAgICAgICAgICAgICAgIGNvbnN0IHRpbWVsaW5lID0gcm9vbS5nZXRMaXZlVGltZWxpbmUoKTtcbiAgICAgICAgICAgICAgICBjb25zdCB0b2tlbiA9IHRpbWVsaW5lLmdldFBhZ2luYXRpb25Ub2tlbihEaXJlY3Rpb24uQmFja3dhcmQpO1xuXG4gICAgICAgICAgICAgICAgY29uc3QgYmFja0NoZWNrcG9pbnQ6IElDcmF3bGVyQ2hlY2twb2ludCA9IHtcbiAgICAgICAgICAgICAgICAgICAgcm9vbUlkOiByb29tLnJvb21JZCxcbiAgICAgICAgICAgICAgICAgICAgdG9rZW46IHRva2VuLFxuICAgICAgICAgICAgICAgICAgICBkaXJlY3Rpb246IERpcmVjdGlvbi5CYWNrd2FyZCxcbiAgICAgICAgICAgICAgICAgICAgZnVsbENyYXdsOiB0cnVlLFxuICAgICAgICAgICAgICAgIH07XG5cbiAgICAgICAgICAgICAgICBjb25zdCBmb3J3YXJkQ2hlY2twb2ludDogSUNyYXdsZXJDaGVja3BvaW50ID0ge1xuICAgICAgICAgICAgICAgICAgICByb29tSWQ6IHJvb20ucm9vbUlkLFxuICAgICAgICAgICAgICAgICAgICB0b2tlbjogdG9rZW4sXG4gICAgICAgICAgICAgICAgICAgIGRpcmVjdGlvbjogRGlyZWN0aW9uLkZvcndhcmQsXG4gICAgICAgICAgICAgICAgfTtcblxuICAgICAgICAgICAgICAgIHRyeSB7XG4gICAgICAgICAgICAgICAgICAgIGlmIChiYWNrQ2hlY2twb2ludC50b2tlbikge1xuICAgICAgICAgICAgICAgICAgICAgICAgYXdhaXQgaW5kZXhNYW5hZ2VyLmFkZENyYXdsZXJDaGVja3BvaW50KGJhY2tDaGVja3BvaW50KTtcbiAgICAgICAgICAgICAgICAgICAgICAgIHRoaXMuY3Jhd2xlckNoZWNrcG9pbnRzLnB1c2goYmFja0NoZWNrcG9pbnQpO1xuICAgICAgICAgICAgICAgICAgICB9XG5cbiAgICAgICAgICAgICAgICAgICAgaWYgKGZvcndhcmRDaGVja3BvaW50LnRva2VuKSB7XG4gICAgICAgICAgICAgICAgICAgICAgICBhd2FpdCBpbmRleE1hbmFnZXIuYWRkQ3Jhd2xlckNoZWNrcG9pbnQoZm9yd2FyZENoZWNrcG9pbnQpO1xuICAgICAgICAgICAgICAgICAgICAgICAgdGhpcy5jcmF3bGVyQ2hlY2twb2ludHMucHVzaChmb3J3YXJkQ2hlY2twb2ludCk7XG4gICAgICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICB9IGNhdGNoIChlKSB7XG4gICAgICAgICAgICAgICAgICAgIGxvZ2dlci5sb2coXG4gICAgICAgICAgICAgICAgICAgICAgICBcIkV2ZW50SW5kZXg6IEVycm9yIGFkZGluZyBpbml0aWFsIGNoZWNrcG9pbnRzIGZvciByb29tXCIsXG4gICAgICAgICAgICAgICAgICAgICAgICByb29tLnJvb21JZCxcbiAgICAgICAgICAgICAgICAgICAgICAgIGJhY2tDaGVja3BvaW50LFxuICAgICAgICAgICAgICAgICAgICAgICAgZm9yd2FyZENoZWNrcG9pbnQsXG4gICAgICAgICAgICAgICAgICAgICAgICBlLFxuICAgICAgICAgICAgICAgICAgICApO1xuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH0pLFxuICAgICAgICApO1xuICAgIH1cblxuICAgIC8qXG4gICAgICogVGhlIHN5bmMgZXZlbnQgbGlzdGVuZXIuXG4gICAgICpcbiAgICAgKiBUaGUgbGlzdGVuZXIgaGFzIHR3byBjYXNlczpcbiAgICAgKiAgICAgLSBGaXJzdCBzeW5jIGFmdGVyIHN0YXJ0IHVwLCBjaGVjayBpZiB0aGUgaW5kZXggaXMgZW1wdHksIGFkZFxuICAgICAqICAgICAgICAgaW5pdGlhbCBjaGVja3BvaW50cywgaWYgc28uIFN0YXJ0IHRoZSBjcmF3bGVyIGJhY2tncm91bmQgdGFzay5cbiAgICAgKiAgICAgLSBFdmVyeSBvdGhlciBzeW5jLCB0ZWxsIHRoZSBldmVudCBpbmRleCB0byBjb21taXQgYWxsIHRoZSBxdWV1ZWQgdXBcbiAgICAgKiAgICAgICAgIGxpdmUgZXZlbnRzXG4gICAgICovXG4gICAgcHJpdmF0ZSBvblN5bmMgPSBhc3luYyAoc3RhdGU6IFN5bmNTdGF0ZSwgcHJldlN0YXRlOiBTeW5jU3RhdGUgfCBudWxsLCBkYXRhPzogU3luY1N0YXRlRGF0YSk6IFByb21pc2U8dm9pZD4gPT4ge1xuICAgICAgICBjb25zdCBpbmRleE1hbmFnZXIgPSBQbGF0Zm9ybVBlZy5nZXQoKT8uZ2V0RXZlbnRJbmRleGluZ01hbmFnZXIoKTtcbiAgICAgICAgaWYgKCFpbmRleE1hbmFnZXIpIHJldHVybjtcblxuICAgICAgICBpZiAocHJldlN0YXRlID09PSBcIlBSRVBBUkVEXCIgJiYgc3RhdGUgPT09IFwiU1lOQ0lOR1wiKSB7XG4gICAgICAgICAgICAvLyBJZiBvdXIgaW5kZXhlciBpcyBlbXB0eSB3ZSdyZSBtb3N0IGxpa2VseSBydW5uaW5nIEVsZW1lbnQgdGhlXG4gICAgICAgICAgICAvLyBmaXJzdCB0aW1lIHdpdGggaW5kZXhpbmcgc3VwcG9ydCBvciBydW5uaW5nIGl0IHdpdGggYW5cbiAgICAgICAgICAgIC8vIGluaXRpYWwgc3luYy4gQWRkIGNoZWNrcG9pbnRzIHRvIGNyYXdsIG91ciBlbmNyeXB0ZWQgcm9vbXMuXG4gICAgICAgICAgICBjb25zdCBldmVudEluZGV4V2FzRW1wdHkgPSBhd2FpdCBpbmRleE1hbmFnZXIuaXNFdmVudEluZGV4RW1wdHkoKTtcbiAgICAgICAgICAgIGlmIChldmVudEluZGV4V2FzRW1wdHkpIGF3YWl0IHRoaXMuYWRkSW5pdGlhbENoZWNrcG9pbnRzKCk7XG5cbiAgICAgICAgICAgIHRoaXMuc3RhcnRDcmF3bGVyKCk7XG4gICAgICAgICAgICByZXR1cm47XG4gICAgICAgIH1cblxuICAgICAgICBpZiAocHJldlN0YXRlID09PSBcIlNZTkNJTkdcIiAmJiBzdGF0ZSA9PT0gXCJTWU5DSU5HXCIpIHtcbiAgICAgICAgICAgIC8vIEEgc3luYyB3YXMgZG9uZSwgcHJlc3VtYWJseSB3ZSBxdWV1ZWQgdXAgc29tZSBsaXZlIGV2ZW50cyxcbiAgICAgICAgICAgIC8vIGNvbW1pdCB0aGVtIG5vdy5cbiAgICAgICAgICAgIGF3YWl0IGluZGV4TWFuYWdlci5jb21taXRMaXZlRXZlbnRzKCk7XG4gICAgICAgIH1cbiAgICB9O1xuXG4gICAgLypcbiAgICAgKiBUaGUgUm9vbS50aW1lbGluZSBsaXN0ZW5lci5cbiAgICAgKlxuICAgICAqIFRoaXMgbGlzdGVuZXIgd2FpdHMgZm9yIGxpdmUgZXZlbnRzIGluIGVuY3J5cHRlZCByb29tcywgaWYgdGhleSBhcmVcbiAgICAgKiBkZWNyeXB0ZWQgb3IgdW5lbmNyeXB0ZWQgd2UgcXVldWUgdGhlbSB0byBiZSBhZGRlZCB0byB0aGUgaW5kZXgsXG4gICAgICogb3RoZXJ3aXNlIHdlIHNhdmUgdGhlaXIgZXZlbnQgaWQgYW5kIHdhaXQgZm9yIHRoZW0gaW4gdGhlIEV2ZW50LmRlY3J5cHRlZFxuICAgICAqIGxpc3RlbmVyLlxuICAgICAqL1xuICAgIHByaXZhdGUgb25Sb29tVGltZWxpbmUgPSBhc3luYyAoXG4gICAgICAgIGV2OiBNYXRyaXhFdmVudCxcbiAgICAgICAgcm9vbTogUm9vbSB8IHVuZGVmaW5lZCxcbiAgICAgICAgdG9TdGFydE9mVGltZWxpbmU6IGJvb2xlYW4gfCB1bmRlZmluZWQsXG4gICAgICAgIHJlbW92ZWQ6IGJvb2xlYW4sXG4gICAgICAgIGRhdGE6IElSb29tVGltZWxpbmVEYXRhLFxuICAgICk6IFByb21pc2U8dm9pZD4gPT4ge1xuICAgICAgICBpZiAoIXJvb20pIHJldHVybjsgLy8gbm90aWZpY2F0aW9uIHRpbWVsaW5lLCB3ZSdsbCBnZXQgdGhpcyBldmVudCBhZ2FpbiB3aXRoIGEgcm9vbSBzcGVjaWZpYyB0aW1lbGluZVxuXG4gICAgICAgIGNvbnN0IGNsaWVudCA9IE1hdHJpeENsaWVudFBlZy5zYWZlR2V0KCk7XG5cbiAgICAgICAgLy8gV2Ugb25seSBpbmRleCBlbmNyeXB0ZWQgcm9vbXMgbG9jYWxseS5cbiAgICAgICAgaWYgKCFjbGllbnQuaXNSb29tRW5jcnlwdGVkKGV2LmdldFJvb21JZCgpISkpIHJldHVybjtcblxuICAgICAgICBpZiAoZXYuaXNSZWRhY3Rpb24oKSkge1xuICAgICAgICAgICAgcmV0dXJuIHRoaXMucmVkYWN0RXZlbnQoZXYpO1xuICAgICAgICB9XG5cbiAgICAgICAgLy8gSWYgaXQgaXNuJ3QgYSBsaXZlIGV2ZW50IG9yIGlmIGl0J3MgcmVkYWN0ZWQgdGhlcmUncyBub3RoaW5nIHRvIGRvLlxuICAgICAgICBpZiAodG9TdGFydE9mVGltZWxpbmUgfHwgIWRhdGEgfHwgIWRhdGEubGl2ZUV2ZW50IHx8IGV2LmlzUmVkYWN0ZWQoKSkge1xuICAgICAgICAgICAgcmV0dXJuO1xuICAgICAgICB9XG5cbiAgICAgICAgYXdhaXQgY2xpZW50LmRlY3J5cHRFdmVudElmTmVlZGVkKGV2KTtcblxuICAgICAgICBhd2FpdCB0aGlzLmFkZExpdmVFdmVudFRvSW5kZXgoZXYpO1xuICAgIH07XG5cbiAgICBwcml2YXRlIG9uUm9vbVN0YXRlRXZlbnQgPSBhc3luYyAoZXY6IE1hdHJpeEV2ZW50LCBzdGF0ZTogUm9vbVN0YXRlKTogUHJvbWlzZTx2b2lkPiA9PiB7XG4gICAgICAgIGlmICghTWF0cml4Q2xpZW50UGVnLnNhZmVHZXQoKS5pc1Jvb21FbmNyeXB0ZWQoc3RhdGUucm9vbUlkKSkgcmV0dXJuO1xuXG4gICAgICAgIGlmIChldi5nZXRUeXBlKCkgPT09IEV2ZW50VHlwZS5Sb29tRW5jcnlwdGlvbiAmJiAhKGF3YWl0IHRoaXMuaXNSb29tSW5kZXhlZChzdGF0ZS5yb29tSWQpKSkge1xuICAgICAgICAgICAgbG9nZ2VyLmxvZyhcIkV2ZW50SW5kZXg6IEFkZGluZyBhIGNoZWNrcG9pbnQgZm9yIGEgbmV3bHkgZW5jcnlwdGVkIHJvb21cIiwgc3RhdGUucm9vbUlkKTtcbiAgICAgICAgICAgIHRoaXMuYWRkUm9vbUNoZWNrcG9pbnQoc3RhdGUucm9vbUlkLCB0cnVlKTtcbiAgICAgICAgfVxuICAgIH07XG5cbiAgICAvKlxuICAgICAqIFJlbW92ZXMgYSByZWRhY3RlZCBldmVudCBmcm9tIG91ciBldmVudCBpbmRleC5cbiAgICAgKiBXZSBjYW5ub3QgcmVseSBvbiBSb29tLnJlZGFjdGlvbiBhcyB0aGlzIG9ubHkgZmlyZXMgaWYgdGhlIHJlZGFjdGlvbiBhcHBsaWVkIHRvIGFuIGV2ZW50IHRoZSBqcy1zZGsgaGFzIGxvYWRlZC5cbiAgICAgKi9cbiAgICBwcml2YXRlIHJlZGFjdEV2ZW50ID0gYXN5bmMgKGV2OiBNYXRyaXhFdmVudCk6IFByb21pc2U8dm9pZD4gPT4ge1xuICAgICAgICBjb25zdCBpbmRleE1hbmFnZXIgPSBQbGF0Zm9ybVBlZy5nZXQoKT8uZ2V0RXZlbnRJbmRleGluZ01hbmFnZXIoKTtcbiAgICAgICAgaWYgKCFpbmRleE1hbmFnZXIpIHJldHVybjtcblxuICAgICAgICBjb25zdCBhc3NvY2lhdGVkSWQgPSBldi5nZXRBc3NvY2lhdGVkSWQoKTtcbiAgICAgICAgaWYgKCFhc3NvY2lhdGVkSWQpIHJldHVybjtcblxuICAgICAgICB0cnkge1xuICAgICAgICAgICAgYXdhaXQgaW5kZXhNYW5hZ2VyLmRlbGV0ZUV2ZW50KGFzc29jaWF0ZWRJZCk7XG4gICAgICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgICAgIGxvZ2dlci5sb2coXCJFdmVudEluZGV4OiBFcnJvciBkZWxldGluZyBldmVudCBmcm9tIGluZGV4XCIsIGUpO1xuICAgICAgICB9XG4gICAgfTtcblxuICAgIC8qXG4gICAgICogVGhlIFJvb20udGltZWxpbmVSZXNldCBsaXN0ZW5lci5cbiAgICAgKlxuICAgICAqIExpc3RlbnMgZm9yIHRpbWVsaW5lIHJlc2V0cyB0aGF0IGFyZSBjYXVzZWQgYnkgYSBsaW1pdGVkIHRpbWVsaW5lIHRvXG4gICAgICogcmUtYWRkIGNoZWNrcG9pbnRzIGZvciByb29tcyB0aGF0IG5lZWQgdG8gYmUgY3Jhd2xlZCBhZ2Fpbi5cbiAgICAgKi9cbiAgICBwcml2YXRlIG9uVGltZWxpbmVSZXNldCA9IGFzeW5jIChyb29tOiBSb29tIHwgdW5kZWZpbmVkKTogUHJvbWlzZTx2b2lkPiA9PiB7XG4gICAgICAgIGlmICghcm9vbSkgcmV0dXJuO1xuICAgICAgICBpZiAoIU1hdHJpeENsaWVudFBlZy5zYWZlR2V0KCkuaXNSb29tRW5jcnlwdGVkKHJvb20ucm9vbUlkKSkgcmV0dXJuO1xuXG4gICAgICAgIGxvZ2dlci5sb2coXCJFdmVudEluZGV4OiBBZGRpbmcgYSBjaGVja3BvaW50IGJlY2F1c2Ugb2YgYSBsaW1pdGVkIHRpbWVsaW5lXCIsIHJvb20ucm9vbUlkKTtcblxuICAgICAgICB0aGlzLmFkZFJvb21DaGVja3BvaW50KHJvb20ucm9vbUlkLCBmYWxzZSk7XG4gICAgfTtcblxuICAgIC8qKlxuICAgICAqIENoZWNrIGlmIGFuIGV2ZW50IHNob3VsZCBiZSBhZGRlZCB0byB0aGUgZXZlbnQgaW5kZXguXG4gICAgICpcbiAgICAgKiBNb3N0IG5vdGFibHkgd2UgZmlsdGVyIGV2ZW50cyBmb3Igd2hpY2ggZGVjcnlwdGlvbiBmYWlsZWQsIGFyZSByZWRhY3RlZFxuICAgICAqIG9yIGFyZW4ndCBvZiBhIHR5cGUgdGhhdCB3ZSBrbm93IGhvdyB0byBpbmRleC5cbiAgICAgKlxuICAgICAqIEBwYXJhbSB7TWF0cml4RXZlbnR9IGV2IFRoZSBldmVudCB0aGF0IHNob3VsZCBiZSBjaGVja2VkLlxuICAgICAqIEByZXR1cm5zIHtib29sfSBSZXR1cm5zIHRydWUgaWYgdGhlIGV2ZW50IGNhbiBiZSBpbmRleGVkLCBmYWxzZVxuICAgICAqIG90aGVyd2lzZS5cbiAgICAgKi9cbiAgICBwcml2YXRlIGlzVmFsaWRFdmVudChldjogTWF0cml4RXZlbnQpOiBib29sZWFuIHtcbiAgICAgICAgY29uc3QgaXNVc2VmdWxUeXBlID0gW0V2ZW50VHlwZS5Sb29tTWVzc2FnZSwgRXZlbnRUeXBlLlJvb21OYW1lLCBFdmVudFR5cGUuUm9vbVRvcGljXS5pbmNsdWRlcyhcbiAgICAgICAgICAgIGV2LmdldFR5cGUoKSBhcyBFdmVudFR5cGUsXG4gICAgICAgICk7XG4gICAgICAgIGNvbnN0IHZhbGlkRXZlbnRUeXBlID0gaXNVc2VmdWxUeXBlICYmICFldi5pc1JlZGFjdGVkKCkgJiYgIWV2LmlzRGVjcnlwdGlvbkZhaWx1cmUoKTtcblxuICAgICAgICBsZXQgdmFsaWRNc2dUeXBlID0gdHJ1ZTtcbiAgICAgICAgbGV0IGhhc0NvbnRlbnRWYWx1ZSA9IHRydWU7XG5cbiAgICAgICAgaWYgKGV2LmdldFR5cGUoKSA9PT0gRXZlbnRUeXBlLlJvb21NZXNzYWdlICYmICFldi5pc1JlZGFjdGVkKCkpIHtcbiAgICAgICAgICAgIC8vIEV4cGFuZCB0aGlzIGlmIHRoZXJlIGFyZSBtb3JlIGludmFsaWQgbXNndHlwZXMuXG4gICAgICAgICAgICBjb25zdCBtc2d0eXB