matrix-react-sdk
Version:
SDK for matrix.org using React
801 lines (757 loc) • 113 kB
JavaScript
"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