"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.IrcBridge = exports.METRIC_ACTIVE_USERS = exports.MEMBERSHIP_DEFAULT_TTL = void 0;
const bluebird_1 = __importDefault(require("bluebird"));
const extend_1 = __importDefault(require("extend"));
const promiseutil = __importStar(require("../promiseutil"));
const IrcHandler_1 = require("./IrcHandler");
const MatrixHandler_1 = require("./MatrixHandler");
const MemberListSyncer_1 = require("./MemberListSyncer");
const IrcServer_1 = require("../irc/IrcServer");
const ClientPool_1 = require("../irc/ClientPool");
const BridgedClient_1 = require("../irc/BridgedClient");
const IrcUser_1 = require("../models/IrcUser");
const BridgeRequest_1 = require("../models/BridgeRequest");
const NedbDataStore_1 = require("../datastore/NedbDataStore");
const PgDataStore_1 = require("../datastore/postgres/PgDataStore");
const logging_1 = require("../logging");
const DebugApi_1 = require("../DebugApi");
const Provisioner_1 = require("../provisioning/Provisioner");
const PublicitySyncer_1 = require("./PublicitySyncer");
const matrix_appservice_bridge_1 = require("matrix-appservice-bridge");
const MatrixAction_1 = require("../models/MatrixAction");
const prom_client_1 = require("prom-client");
const MetricsWorker_1 = require("../workers/MetricsWorker");
const http_1 = require("http");
const https_1 = require("https");
const RoomConfig_1 = require("./RoomConfig");
const PrivacyProtection_1 = require("../irc/PrivacyProtection");
const MatrixBanSync_1 = require("./MatrixBanSync");
const log = (0, logging_1.getLogger)("IrcBridge");
const DEFAULT_PORT = 8090;
const DELAY_TIME_MS = 10 * 1000;
const DELAY_FETCH_ROOM_LIST_MS = 3 * 1000;
const DEAD_TIME_MS = 5 * 60 * 1000;
const TXN_SIZE_DEFAULT = 10000000; // 10MB
const CLIENTS_BY_HOMESERVER_TOP_N = 20;
exports.MEMBERSHIP_DEFAULT_TTL = 10 * 60 * 1000;
/**
 * How old can a receipt be before we treat
 * it as stale.
 */
const RECEIPT_CUTOFF_TIME_MS = 60000;
exports.METRIC_ACTIVE_USERS = "active_users";
class IrcBridge {
    constructor(config, registration, testOpts = { isDBInMemory: false }) {
        this.config = config;
        this.registration = registration;
        this.testOpts = testOpts;
        this.onAliasQueried = null;
        this.activityTracker = null;
        this.ircServers = [];
        this.memberListSyncers = {};
        this.joinedRoomList = [];
        this.bridgeState = "not-started";
        this.debugApi = null;
        this.provisioner = null;
        this.timers = null;
        // TODO: Don't log this to stdout
        matrix_appservice_bridge_1.Logger.configure({ console: config.ircService.logging.level });
        if (!this.config.database && this.config.ircService.databaseUri) {
            log.warn("ircService.databaseUri is a deprecated config option." +
                "Please use the database configuration block");
            this.config.database = {
                engine: "nedb",
                connectionString: this.config.ircService.databaseUri,
            };
        }
        let roomLinkValidationRules = undefined;
        const provisioning = config.ircService.provisioning;
        if (provisioning?.enabled && provisioning.rules) {
            roomLinkValidationRules = provisioning.rules;
        }
        let bridgeStoreConfig = {};
        if (this.config.database.engine === "nedb") {
            const dirPath = this.config.database.connectionString.substring("nedb://".length);
            bridgeStoreConfig = {
                roomStore: `${dirPath}/rooms.db`,
                userStore: `${dirPath}/users.db`,
                userActivityStore: `${dirPath}/user-activity.db`,
            };
        }
        else {
            bridgeStoreConfig = {
                disableStores: true,
            };
        }
        this.membershipCache = new matrix_appservice_bridge_1.MembershipCache();
        if (!this.registration.pushEphemeral) {
            log.info("Sending ephemeral events to the bridge is currently disabled in the registration file," +
                " so user activity will not be captured");
        }
        this.bridge = new matrix_appservice_bridge_1.Bridge({
            registration: this.registration,
            homeserverUrl: this.config.homeserver.url,
            domain: this.config.homeserver.domain,
            controller: {
                onEvent: this.onEvent.bind(this),
                onUserQuery: this.onUserQuery.bind(this),
                onAliasQuery: this.onAliasQuery.bind(this),
                onAliasQueried: this.onAliasQueried ?
                    this.onAliasQueried.bind(this) : undefined,
                onLog: this.onLog.bind(this),
                onEphemeralEvent: this.activityTracker ? this.onEphemeralEvent.bind(this) : undefined,
                thirdPartyLookup: {
                    protocols: ["irc"],
                    getProtocol: this.getThirdPartyProtocol.bind(this),
                    getLocation: this.getThirdPartyLocation.bind(this),
                    getUser: this.getThirdPartyUser.bind(this),
                },
            },
            ...bridgeStoreConfig,
            disableContext: true,
            suppressEcho: false,
            logRequestOutcome: false,
            queue: {
                type: "none",
                perRequest: false
            },
            intentOptions: {
                clients: {
                    dontCheckPowerLevel: true,
                    enablePresence: this.config.homeserver.enablePresence,
                },
                bot: {
                    dontCheckPowerLevel: true,
                    enablePresence: this.config.homeserver.enablePresence,
                }
            },
            // See note below for ESCAPE_DEFAULT
            escapeUserIds: false,
            roomLinkValidation: roomLinkValidationRules ? {
                rules: roomLinkValidationRules,
            } : undefined,
            roomUpgradeOpts: {
                consumeEvent: true,
                migrateGhosts: false,
                onRoomMigrated: this.onRoomUpgrade.bind(this),
                migrateStoreEntries: false, // Only NeDB supports this.
            },
            membershipCache: this.membershipCache,
            // For mocking the intent object,
            onIntentCreate: testOpts.onIntentCreate,
        });
        this.membershipQueue = new matrix_appservice_bridge_1.MembershipQueue(this.bridge, {
            concurrentRoomLimit: 3,
            maxAttempts: 5,
            actionDelayMs: 500,
            maxActionDelayMs: 5 * 60 * 1000,
            defaultTtlMs: 10 * 60 * 1000, // 10 mins
        });
        this.matrixBanSyncer = this.config.ircService.banLists && new MatrixBanSync_1.MatrixBanSync(this.config.ircService.banLists);
        this.matrixHandler = new MatrixHandler_1.MatrixHandler(this, this.config.ircService.matrixHandler, this.membershipQueue);
        this.privacyProtection = new PrivacyProtection_1.PrivacyProtection(this);
        this.ircHandler = new IrcHandler_1.IrcHandler(this, this.config.ircService.ircHandler, this.membershipQueue, this.privacyProtection);
        // By default the bridge will escape mxids, but the irc bridge isn't ready for this yet.
        matrix_appservice_bridge_1.MatrixUser.ESCAPE_DEFAULT = false;
        this.publicitySyncer = new PublicitySyncer_1.PublicitySyncer(this);
        const homeserverToken = this.registration.getHomeserverToken();
        if (!homeserverToken) {
            throw Error("No HS token defined");
        }
        this.appservice = new matrix_appservice_bridge_1.AppService({
            homeserverToken,
            httpMaxSizeBytes: this.config.advanced?.maxTxnSize ?? TXN_SIZE_DEFAULT,
        });
        this.roomConfigs = new RoomConfig_1.RoomConfig(this.bridge, this.config.ircService.perRoomConfig);
        if (this.config.ircService.RMAUlimit) {
            this.bridgeBlocker = new matrix_appservice_bridge_1.BridgeBlocker(this.config.ircService.RMAUlimit);
        }
    }
    async onConfigChanged(newConfig) {
        log.info(`Bridge config was reloaded, applying changes`);
        const oldConfig = this.config;
        if (oldConfig.advanced?.maxHttpSockets !== newConfig.advanced?.maxHttpSockets) {
            const maxSockets = newConfig.advanced?.maxHttpSockets ?? 1000;
            http_1.globalAgent.maxSockets = maxSockets;
            https_1.globalAgent.maxSockets = maxSockets;
            log.info(`Adjusted max sockets to ${maxSockets}`);
        }
        // We can't modify the maximum payload size after starting the http listener for the bridge, so
        // newConfig.advanced.maxTxnSize is ignored.
        if (oldConfig.homeserver.dropMatrixMessagesAfterSecs !== newConfig.homeserver.dropMatrixMessagesAfterSecs) {
            oldConfig.homeserver.dropMatrixMessagesAfterSecs = newConfig.homeserver.dropMatrixMessagesAfterSecs;
            log.info(`Adjusted dropMatrixMessagesAfterSecs to ${newConfig.homeserver.dropMatrixMessagesAfterSecs}`);
        }
        if (oldConfig.homeserver.media_url !== newConfig.homeserver.media_url) {
            oldConfig.homeserver.media_url = newConfig.homeserver.media_url;
            log.info(`Adjusted media_url to ${newConfig.homeserver.media_url}`);
        }
        this.ircHandler.onConfigChanged(newConfig.ircService.ircHandler || {});
        this.config.ircService.ircHandler = newConfig.ircService.ircHandler;
        this.matrixHandler.onConfigChanged(newConfig.ircService.matrixHandler);
        this.config.ircService.matrixHandler = newConfig.ircService.matrixHandler;
        this.config.ircService.permissions = newConfig.ircService.permissions;
        this.bridge.updateRoomLinkValidatorRules(
        // If no rules are specified, wipe them.
        newConfig.ircService.provisioning?.rules || { userIds: { conflict: [], exempt: [] } });
        this.config.ircService.provisioning.rules = newConfig.ircService.provisioning?.rules;
        this.roomConfigs.config = newConfig.ircService.perRoomConfig;
        const hasLoggingChanged = JSON.stringify(oldConfig.ircService.logging)
            !== JSON.stringify(newConfig.ircService.logging);
        if (hasLoggingChanged) {
            matrix_appservice_bridge_1.Logger.configure({ console: newConfig.ircService.logging.level });
        }
        const banSyncPromise = this.matrixBanSyncer?.syncRules(this.bridge.getIntent());
        await this.dataStore.removeConfigMappings();
        // All config mapped channels will be briefly unavailable
        await Promise.all(this.ircServers.map(async (server) => {
            let newServerConfig = newConfig.ircService.servers[server.domain];
            if (!newServerConfig) {
                log.warn(`Server ${server.domain} removed from config. Bridge will need to be restarted`);
                return;
            }
            newServerConfig = (0, extend_1.default)(true, {}, IrcServer_1.IrcServer.DEFAULT_CONFIG, newConfig.ircService.servers[server.domain]);
            server.reconfigure(newServerConfig, newConfig.homeserver.dropMatrixMessagesAfterSecs);
            await this.dataStore.setServerFromConfig(server, newServerConfig);
        }));
        await this.fetchJoinedRooms();
        await this.joinMappedMatrixRooms();
        await banSyncPromise;
        await this.clientPool.checkForBannedConnectedUsers();
    }
    initialiseMetrics(bindPort) {
        const zeroAge = new matrix_appservice_bridge_1.AgeCounters();
        const registry = new prom_client_1.Registry();
        if (!this.config.ircService.metrics) {
            return;
        }
        const { userActivityThresholdHours, remoteUserAgeBuckets } = this.config.ircService.metrics;
        const usingRemoteMetrics = !!this.config.ircService.metrics.port;
        const metrics = this.bridge.getPrometheusMetrics(!usingRemoteMetrics, registry);
        let metricsUrl = `${this.config.homeserver.bindHostname || "0.0.0.0"}:${bindPort}`;
        if (this.config.ircService.metrics.port) {
            const hostname = this.config.ircService.metrics.host || this.config.homeserver.bindHostname || "0.0.0.0";
            metricsUrl = `${hostname}:${this.config.ircService.metrics.port}`;
            (0, MetricsWorker_1.spawnMetricsWorker)(this.config.ircService.metrics.port, this.config.ircService.metrics.host, () => {
                metrics.refresh();
                return registry.metrics();
            });
        }
        log.info(`Started metrics on http://${metricsUrl}`);
        this.bridge.registerBridgeGauges(() => {
            const remoteUsersByAge = new matrix_appservice_bridge_1.PrometheusMetrics.AgeCounters(remoteUserAgeBuckets || ["1h", "1d", "1w"]);
            this.ircServers.forEach((server) => {
                this.clientPool.updateActiveConnectionMetrics(server.domain, remoteUsersByAge);
            });
            return {
                // TODO(paul): actually fill these in
                matrixRoomConfigs: 0,
                remoteRoomConfigs: 0,
                remoteGhosts: this.clientPool.countTotalConnections(),
                // matrixGhosts is provided automatically by the bridge
                // TODO(paul) IRC bridge doesn't maintain mtimes at the moment.
                //   Should probably make these metrics optional to most
                //   exporters
                matrixRoomsByAge: zeroAge,
                remoteRoomsByAge: zeroAge,
                matrixUsersByAge: zeroAge,
                remoteUsersByAge,
            };
        });
        this.timers = {
            matrix_request_seconds: metrics.addTimer({
                name: "matrix_request_seconds",
                help: "Histogram of processing durations of received Matrix messages",
                labels: ["outcome"],
            }),
            remote_request_seconds: metrics.addTimer({
                name: "remote_request_seconds",
                help: "Histogram of processing durations of received remote messages",
                labels: ["outcome"],
            }),
            irc_connection_time_ms: metrics.addTimer({
                name: "irc_connection_time_ms",
                help: "The time it took the user to receive the welcome message",
                buckets: [100, 500, 1000, 2500, 10000, 30000],
            }),
        };
        // Custom IRC metrics
        const reconnQueue = metrics.addGauge({
            name: "clientpool_reconnect_queue",
            help: "Number of disconnected irc connections waiting to reconnect.",
            labels: ["server"]
        });
        const clientStates = metrics.addGauge({
            name: "clientpool_client_states",
            help: "Number of clients in different states of connectedness.",
            labels: ["server", "state"]
        });
        const clientsByHomeserver = metrics.addGauge({
            name: "clientpool_by_homeserver",
            help: "Number of clients by homeserver and state. " +
                `Only lists the top ${CLIENTS_BY_HOMESERVER_TOP_N} homeservers`,
            labels: ["homeserver", "state"]
        });
        const memberListLeaveQueue = metrics.addGauge({
            name: "user_leave_queue",
            help: "Number of leave requests queued up for virtual users on the bridge.",
            labels: ["server"]
        });
        const memberListJoinQueue = metrics.addGauge({
            name: "user_join_queue",
            help: "Number of join requests queued up for virtual users on the bridge.",
            labels: ["server"]
        });
        const activeUsers = metrics.addGauge({
            name: exports.METRIC_ACTIVE_USERS,
            help: "Number of users actively using the bridge.",
            labels: ["remote"],
        });
        const ircHandlerCalls = metrics.addCounter({
            name: "irchandler_calls",
            help: "Track calls made to the IRC Handler",
            labels: ["method"]
        });
        const ircBlockedRooms = metrics.addGauge({
            name: "irc_blocked_rooms",
            help: "Track number of blocked rooms for I->M traffic",
            labels: ["method"]
        });
        const matrixHandlerConnFailureKicks = metrics.addCounter({
            name: "matrixhandler_connection_failure_kicks",
            help: "Track IRC connection failures resulting in kicks",
            labels: ["server"]
        });
        const maxRemoteGhosts = metrics.addGauge({
            name: "remote_ghosts_max",
            help: "The maximum number of remote ghosts",
            labels: ["server"]
        });
        const bridgeBlocked = metrics.addGauge({
            name: "bridge_blocked",
            help: "Is the bridge currently blocking messages",
        });
        metrics.addCollector(() => {
            this.ircServers.forEach((server) => {
                reconnQueue.set({ server: server.domain }, this.clientPool.totalReconnectsWaiting(server.domain));
                const mxMetrics = this.matrixHandler.getMetrics(server.domain);
                matrixHandlerConnFailureKicks.inc({ server: server.domain }, mxMetrics["connection_failure_kicks"] || 0);
                maxRemoteGhosts.set({ server: server.domain }, server.getMaxClients());
            });
            if (userActivityThresholdHours) {
                // Only collect if defined
                const currentTime = Date.now();
                const appserviceBot = this.bridge.getBot();
                if (!appserviceBot) {
                    // Not ready yet.
                    return;
                }
                this.dataStore.getLastSeenTimeForUsers().then((userSet) => {
                    let remote = 0;
                    let matrix = 0;
                    for (const user of userSet) {
                        const timeOffset = (currentTime - user.ts) / (60 * 60 * 1000); // Hours
                        if (timeOffset > userActivityThresholdHours) {
                            return;
                        }
                        else if (appserviceBot.isRemoteUser(user.user_id)) {
                            remote++;
                        }
                        else {
                            matrix++;
                        }
                    }
                    activeUsers.set({ remote: "true" }, remote);
                    activeUsers.set({ remote: "false" }, matrix);
                }).catch((ex) => {
                    log.warn("Failed to scrape for user activity", ex);
                });
            }
            Object.keys(this.memberListSyncers).forEach((server) => {
                memberListLeaveQueue.set({ server }, this.memberListSyncers[server].getUsersWaitingToLeave());
                memberListJoinQueue.set({ server }, this.memberListSyncers[server].getUsersWaitingToJoin());
            });
            ircBlockedRooms.set(this.privacyProtection.blockedRoomCount);
            const ircMetrics = this.ircHandler.getMetrics();
            Object.entries(ircMetrics).forEach((kv) => {
                ircHandlerCalls.inc({ method: kv[0] }, kv[1]);
            });
            bridgeBlocked.set(this.bridgeBlocker?.isBlocked ? 1 : 0);
        });
        metrics.addCollector(async () => {
            this.clientPool.collectConnectionStatesForAllServers(clientStates, clientsByHomeserver, CLIENTS_BY_HOMESERVER_TOP_N);
        });
        this.membershipQueue.registerMetrics();
    }
    get appServiceUserId() {
        return `@${this.registration.getSenderLocalpart()}:${this.domain}`;
    }
    getStore() {
        return this.dataStore;
    }
    getAppServiceBridge() {
        return this.bridge;
    }
    getClientPool() {
        return this.clientPool;
    }
    getProvisioner() {
        return this.provisioner;
    }
    get domain() {
        return this.config.homeserver.domain;
    }
    get stateSyncer() {
        return this.bridgeStateSyncer;
    }
    async pingBridge() {
        let internalRoom;
        try {
            internalRoom = await this.dataStore.getAdminRoomByUserId("-internal-");
            if (!internalRoom) {
                const result = await this.bridge.getIntent().createRoom({ options: {} });
                internalRoom = new matrix_appservice_bridge_1.MatrixRoom(result.room_id);
                this.dataStore.storeAdminRoom(internalRoom, "-internal-");
            }
            const time = await this.bridge.pingAppserviceRoute(internalRoom.getId());
            log.info(`Successfully pinged the bridge. Round trip took ${time}ms`);
        }
        catch (ex) {
            log.error("Homeserver cannot reach the bridge. You probably need to adjust your configuration.", ex);
        }
    }
    createInfoMapping(channel, networkId) {
        const network = this.getServer(networkId);
        return {
            protocol: {
                id: 'irc',
                displayname: 'IRC',
            },
            network: {
                id: networkId,
                displayname: network?.getReadableName(),
                avatar_url: network?.getIcon(),
            },
            channel: {
                id: channel,
            }
        };
    }
    async run(port) {
        this.bridgeState = 'starting';
        const dbConfig = this.config.database;
        // cli port, then config port, then default port
        port = port || this.config.homeserver.bindPort || DEFAULT_PORT;
        const pkeyPath = this.config.ircService.passwordEncryptionKeyPath;
        await this.bridge.initalise();
        await this.matrixBanSyncer?.syncRules(this.bridge.getIntent());
        this.matrixHandler.initalise();
        this.activityTracker = new matrix_appservice_bridge_1.ActivityTracker(this.bridge.getIntent().matrixClient, {
            usePresence: this.config.homeserver.enablePresence,
            serverName: this.config.homeserver.domain,
            defaultOnline: true,
        });
        if (dbConfig.engine === "postgres") {
            log.info("Using PgDataStore for Datastore");
            const pgDs = new PgDataStore_1.PgDataStore(this.config.homeserver.domain, dbConfig.connectionString, pkeyPath);
            await pgDs.ensureSchema();
            this.dataStore = pgDs;
        }
        else if (dbConfig.engine === "nedb") {
            await this.bridge.loadDatabases();
            const userStore = this.bridge.getUserStore();
            const roomStore = this.bridge.getRoomStore();
            const userActivityStore = this.bridge.getUserActivityStore();
            log.info("Using NeDBDataStore for Datastore");
            if (!userStore || !roomStore || !userActivityStore) {
                throw Error('Could not load user(Activity)Store or roomStore');
            }
            const ndbDatastore = new NedbDataStore_1.NeDBDataStore(userStore, userActivityStore, roomStore, this.config.homeserver.domain, pkeyPath);
            await ndbDatastore.runMigrations();
            this.dataStore = ndbDatastore;
            if (this.config.ircService.debugApi.enabled) {
                // monkey patch inspect() values to avoid useless NeDB
                // struct spam on the debug API.
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                userStore.inspect = () => "UserStore";
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                roomStore.inspect = () => "RoomStore";
            }
        }
        else {
            throw Error("Incorrect database config");
        }
        await this.dataStore.removeConfigMappings();
        if (this.activityTracker) {
            log.info("Restoring last active times from DB");
            const users = await this.dataStore.getLastSeenTimeForUsers();
            for (const user of users) {
                this.activityTracker.setLastActiveTime(user.user_id, user.ts);
            }
            log.info(`Restored ${users.length} last active times from DB`);
        }
        // maintain a list of IRC servers in-use
        const serverDomains = Object.keys(this.config.ircService.servers);
        for (let i = 0; i < serverDomains.length; i++) {
            const domain = serverDomains[i];
            const completeConfig = (0, extend_1.default)(true, {}, IrcServer_1.IrcServer.DEFAULT_CONFIG, this.config.ircService.servers[domain]);
            const server = new IrcServer_1.IrcServer(domain, completeConfig, this.config.homeserver.domain, this.config.homeserver.dropMatrixMessagesAfterSecs);
            // store the config mappings in the DB to keep everything in one place.
            await this.dataStore.setServerFromConfig(server, completeConfig);
            this.ircServers.push(server);
        }
        this.clientPool = new ClientPool_1.ClientPool(this, this.dataStore);
        if (this.config.ircService.debugApi.enabled) {
            this.debugApi = new DebugApi_1.DebugApi(this, this.config.ircService.debugApi.port, this.ircServers, this.clientPool, this.registration.getAppServiceToken());
            this.debugApi.run();
        }
        if (this.ircServers.length === 0) {
            throw Error("No IRC servers specified.");
        }
        if (this.config.ircService.userActivity) {
            const uatConfig = {
                ...matrix_appservice_bridge_1.UserActivityTrackerConfig.DEFAULT,
            };
            if (this.config.ircService.userActivity.minUserActiveDays !== undefined) {
                uatConfig.minUserActiveDays = this.config.ircService.userActivity.minUserActiveDays;
            }
            if (this.config.ircService.userActivity.inactiveAfterDays !== undefined) {
                uatConfig.inactiveAfterDays = this.config.ircService.userActivity.inactiveAfterDays;
            }
            this.bridge.opts.controller.userActivityTracker = new matrix_appservice_bridge_1.UserActivityTracker(uatConfig, await this.getStore().getUserActivity(), (changes) => this.onUserActivityChanged(changes).catch((ex) => log.warn("onUserActivityChanged encountered an error", ex)));
            this.bridgeBlocker?.checkLimits(this.bridge.opts.controller.userActivityTracker.countActiveUsers().allUsers).catch(ex => {
                log.warn(`Failed to run initial checkLimits for user activity tracker`, ex);
            });
        }
        // run the bridge (needs to be done prior to configure IRC side)
        await this.bridge.listen(port, this.config.homeserver.bindHostname, undefined, this.appservice);
        log.info(`Listening on ${this.config.homeserver.bindHostname || "0.0.0.0"}:${port}`);
        if (this.config.ircService.metrics && this.config.ircService.metrics.enabled) {
            this.initialiseMetrics(port);
        }
        this.addRequestCallbacks();
        if (!this.registration.getSenderLocalpart() ||
            !this.registration.getAppServiceToken()) {
            throw Error("FATAL: Registration file is missing a sender_localpart and/or AS token.");
        }
        if (!this.testOpts.skipPingCheck) {
            await this.pingBridge();
        }
        // Storing all the users we know about to avoid calling /register on them.
        const allUsers = await this.dataStore.getAllUserIds();
        const bot = this.bridge.getBot();
        allUsers.filter((u) => bot.isRemoteUser(u))
            .forEach((u) => this.membershipCache.setMemberEntry("", u, "join", {}));
        log.info("Fetching Matrix rooms that are already joined to...");
        await this.fetchJoinedRooms();
        if (this.config.ircService.bridgeInfoState?.enabled) {
            this.bridgeStateSyncer = new matrix_appservice_bridge_1.BridgeInfoStateSyncer(this.bridge, {
                bridgeName: 'org.matrix.appservice-irc',
                getMapping: async (roomId, { channel, networkId }) => this.createInfoMapping(channel, networkId),
            });
            if (this.config.ircService.bridgeInfoState.initial) {
                const mappings = await this.dataStore.getAllChannelMappings();
                this.bridgeStateSyncer.initialSync(mappings).then(() => {
                    log.info("Bridge state syncing completed");
                }).catch((err) => {
                    log.error("Bridge state syncing resulted in an error:", err);
                });
            }
        }
        log.info("Joining mapped Matrix rooms...");
        await this.joinMappedMatrixRooms();
        log.info("Syncing relevant membership lists...");
        const memberlistPromises = [];
        this.ircServers.forEach((server) => {
            //  If memberlist-syncing 100s of connections, the scheduler will cause massive
            //  waiting times for connections to be created.
            //  We disable this scheduling manually to allow people to send messages through
            //  quickly when starting up (effectively prioritising them).
            server.toggleReconnectInterval(false);
            // TODO reduce deps required to make MemberListSyncers.
            // TODO Remove injectJoinFn bodge
            this.memberListSyncers[server.domain] = new MemberListSyncer_1.MemberListSyncer(this, this.membershipQueue, this.bridge.getBot(), server, this.appServiceUserId, (roomId, joiningUserId, displayName, isFrontier) => {
                const req = new BridgeRequest_1.BridgeRequest(this.bridge.getRequestFactory().newRequest());
                const target = new matrix_appservice_bridge_1.MatrixUser(joiningUserId);
                // inject a fake join event which will do M->I connections and
                // therefore sync the member list
                return this.matrixHandler.onJoin(req, {
                    room_id: roomId,
                    content: {
                        displayname: displayName,
                        membership: "join",
                    },
                    _injected: true,
                    state_key: joiningUserId,
                    type: "m.room.member",
                    event_id: "!injected",
                    _frontier: isFrontier
                }, target);
            });
            memberlistPromises.push(this.memberListSyncers[server.domain].sync());
        });
        const provisioningEnabled = this.config.ircService.provisioning.enabled;
        const requestTimeoutSeconds = this.config.ircService.provisioning.requestTimeoutSeconds;
        this.provisioner = new Provisioner_1.Provisioner(this, provisioningEnabled, requestTimeoutSeconds);
        log.info("Connecting to IRC networks...");
        await this.connectToIrcNetworks();
        promiseutil.allSettled(this.ircServers.map((server) => {
            // Call MODE on all known channels to get modes of all channels
            return bluebird_1.default.cast(this.publicitySyncer.initModes(server));
        })).catch((err) => {
            log.error('Could not init modes for publicity syncer');
            log.error(err.stack);
        });
        await Promise.all(memberlistPromises);
        // Reset reconnectIntervals
        this.ircServers.forEach((server) => {
            server.toggleReconnectInterval(true);
        });
        log.info("Startup complete.");
        this.bridgeState = "running";
    }
    logMetric(req, outcome) {
        if (!this.timers) {
            return; // metrics are disabled
        }
        const isFromIrc = Boolean((req.getData() || {}).isFromIrc);
        const timer = this.timers[isFromIrc ? "remote_request_seconds" : "matrix_request_seconds"];
        if (timer) {
            timer.observe({ outcome }, req.getDuration() / 1000);
        }
    }
    logTime(key, time) {
        if (!this.timers) {
            return; // metrics are disabled
        }
        this.timers[key].observe(time);
    }
    addRequestCallbacks() {
        function logMessage(req, msg) {
            const data = req.getData();
            const dir = data && data.isFromIrc ? "I->M" : "M->I";
            const duration = " (" + req.getDuration() + "ms)";
            log.info(`[${req.getId()}] [${dir}] ${msg} ${duration}`);
        }
        const factory = this.bridge.getRequestFactory();
        // SUCCESS
        factory.addDefaultResolveCallback((req, _res) => {
            const res = _res;
            const bridgeRequest = req;
            if (res === BridgeRequest_1.BridgeRequestErr.ERR_VIRTUAL_USER) {
                logMessage(bridgeRequest, "IGNORE virtual user");
                return; // these aren't true successes so don't skew graphs
            }
            else if (res === BridgeRequest_1.BridgeRequestErr.ERR_NOT_MAPPED) {
                logMessage(bridgeRequest, "IGNORE not mapped");
                return; // these aren't true successes so don't skew graphs
            }
            else if (res === BridgeRequest_1.BridgeRequestErr.ERR_DROPPED) {
                logMessage(bridgeRequest, "IGNORE dropped");
                this.logMetric(bridgeRequest, "dropped");
                return;
            }
            logMessage(bridgeRequest, "SUCCESS");
            this.logMetric(bridgeRequest, "success");
        });
        // FAILURE
        factory.addDefaultRejectCallback((req) => {
            const bridgeRequest = req;
            logMessage(bridgeRequest, "FAILED");
            this.logMetric(bridgeRequest, "fail");
            BridgeRequest_1.BridgeRequest.HandleExceptionForSentry(req, "fail");
        });
        // DELAYED
        factory.addDefaultTimeoutCallback((req) => {
            logMessage(req, "DELAYED");
        }, DELAY_TIME_MS);
        // DEAD
        factory.addDefaultTimeoutCallback((req) => {
            const bridgeRequest = req;
            logMessage(bridgeRequest, "DEAD");
            this.logMetric(bridgeRequest, "dead");
            BridgeRequest_1.BridgeRequest.HandleExceptionForSentry(req, "dead");
        }, DEAD_TIME_MS);
    }
    // Kill the bridge by killing all IRC clients in memory.
    //  Killing a client means that it will disconnect forever
    //  and never do anything useful again.
    //  There is no guarentee that the bridge will do anything
    //  usefull once this has been called.
    //
    //  See (BridgedClient.prototype.kill)
    async kill(reason) {
        log.info("Killing bridge");
        this.bridgeState = "killed";
        log.info("Killing all clients");
        await this.clientPool.killAllClients(reason);
        if (this.dataStore) {
            await this.dataStore.destroy();
        }
        log.info("Closing bridge");
        await this.bridge.close();
        await this.appservice.close();
    }
    get isStartedUp() {
        return this.bridgeState === "running";
    }
    async joinMappedMatrixRooms() {
        const roomIds = await this.getStore().getRoomIdsFromConfig();
        const promises = roomIds.map(async (roomId) => {
            if (this.joinedRoomList.includes(roomId)) {
                log.debug(`Not joining ${roomId} because we are marked as joined`);
                return;
            }
            await this.bridge.getIntent().join(roomId);
        }).map(bluebird_1.default.cast);
        await promiseutil.allSettled(promises);
    }
    async sendMatrixAction(room, from, action) {
        if (this.bridgeBlocker?.isBlocked) {
            log.info("Bridge is blocked, dropping Matrix action");
            return;
        }
        const intent = this.bridge.getIntent(from?.userId);
        const extraContent = {};
        if (action.replyEvent) {
            extraContent["m.relates_to"] = {
                "m.in_reply_to": {
                    event_id: action.replyEvent,
                }
            };
        }
        if (action.msgType) {
            if (action.htmlText) {
                await intent.sendMessage(room.getId(), {
                    msgtype: action.msgType,
                    body: (action.text || action.htmlText.replace(/(<([^>]+)>)/ig, "") // strip html tags
                    ),
                    format: "org.matrix.custom.html",
                    formatted_body: action.htmlText,
                    ...extraContent,
                });
            }
            else {
                await intent.sendMessage(room.getId(), {
                    msgtype: action.msgType,
                    body: action.text,
                    ...extraContent,
                });
            }
            return;
        }
        else if (action.type === "topic" && action.text) {
            await intent.setRoomTopic(room.getId(), action.text);
            return;
        }
        throw Error("Unknown action: " + action.type);
    }
    async syncMembersInRoomToIrc(req, roomId, ircRoom, kickFailures = false) {
        const bot = this.getAppServiceBridge().getBot();
        const members = await bot.getJoinedMembers(roomId);
        req.log.info(`Syncing Matrix users to ${ircRoom.server.domain} ${ircRoom.channel} (${Object.keys(members).length})`);
        for (const [userId, { display_name }] of Object.entries(members)) {
            try {
                // If the user is banned, skip any connection attempts and go straight for a kick.
                const banReason = this.matrixBanSyncer?.isUserBanned(userId);
                if (banReason) {
                    req.log.debug(`Not syncing ${userId} - user banned (${banReason})`);
                    this.membershipQueue.leave(roomId, userId, req, true, `You are banned: ${banReason}`, this.appServiceUserId);
                    continue;
                }
                if (bot.isRemoteUser(userId)) {
                    // Don't bridge remote.
                    continue;
                }
                const client = await this.getClientPool().getBridgedClient(ircRoom.server, userId, display_name);
                if (client.inChannel(ircRoom.channel)) {
                    continue;
                }
                await client.joinChannel(ircRoom.channel);
                await new Promise(r => setTimeout(r, ircRoom.server.getMemberListFloodDelayMs()));
            }
            catch (ex) {
                if (!kickFailures) {
                    req.log.warn(`Failed to sync ${userId} to IRC channel`);
                    continue;
                }
                req.log.warn(`Failed to sync ${userId} to IRC channel, kicking from room.`);
                this.membershipQueue.leave(roomId, userId, req, true, "Couldn't connect you to this channel. Please try again later.", this.appServiceUserId);
            }
        }
    }
    uploadTextFile(fileName, plaintext) {
        return this.bridge.getIntent().uploadContent(Buffer.from(plaintext), {
            name: fileName,
            type: "text/plain; charset=utf-8",
        });
    }
    async getMatrixUser(ircUser) {
        let matrixUser = null;
        const userLocalpart = ircUser.server.getUserLocalpart(ircUser.nick);
        const displayName = ircUser.server.getDisplayNameFromNick(ircUser.nick);
        try {
            matrixUser = await this.getStore().getMatrixUserByLocalpart(userLocalpart);
            if (matrixUser) {
                return matrixUser;
            }
        }
        catch (e) {
            // user does not exist. Fall through.
        }
        log.info(`${userLocalpart} does not exist in the store yet, setting a profile`);
        const userIntent = this.bridge.getIntentFromLocalpart(userLocalpart);
        await userIntent.setDisplayName(displayName); // will also register this user
        matrixUser = new matrix_appservice_bridge_1.MatrixUser(userIntent.userId);
        matrixUser.setDisplayName(displayName);
        await this.getStore().storeMatrixUser(matrixUser);
        return matrixUser;
    }
    onEvent(request) {
        if (this.bridgeBlocker?.isBlocked) {
            log.info("Bridge is blocked, dropping Matrix event");
            return;
        }
        request.outcomeFrom(this._onEvent(request));
    }
    onEphemeralEvent(request) {
        // If we see one of these events over federation, bump the
        // last active time for those users.
        const event = request.getData();
        let userIds = undefined;
        if (!this.activityTracker) {
            return;
        }
        if (event.type === "m.presence" && event.content.presence === "online") {
            userIds = [event.sender];
        }
        else if (event.type === "m.receipt") {
            userIds = [];
            const currentTime = Date.now();
            // The homeserver will send us a map of all userIDs => ts for each event.
            // We are only interested in recent receipts though.
            for (const eventData of Object.values(event.content).map((v) => v["m.read"])) {
                for (const [userId, { ts }] of Object.entries(eventData)) {
                    if (currentTime - ts <= RECEIPT_CUTOFF_TIME_MS) {
                        userIds.push(userId);
                    }
                }
            }
        }
        else if (event.type === "m.typing") {
            userIds = event.content.user_ids;
        }
        if (userIds) {
            for (const userId of userIds) {
                this.activityTracker.setLastActiveTime(userId);
                this.dataStore.updateLastSeenTimeForUser(userId).catch((ex) => {
                    log.warn(`Failed to bump last active time for ${userId} in database`, ex);
                });
            }
        }
    }
    async _onEvent(baseRequest) {
        const event = baseRequest.getData();
        let updatePromise = null;
        if (event.sender && (this.activityTracker ||
            this.config.ircService.metrics?.userActivityThresholdHours !== undefined)) {
            updatePromise = this.dataStore.updateLastSeenTimeForUser(event.sender);
            if (this.activityTracker) {
                this.activityTracker.setLastActiveTime(event.sender);
            }
        }
        const request = new BridgeRequest_1.BridgeRequest(baseRequest);
        if (event.type === "m.room.message") {
            if (event.origin_server_ts && this.config.homeserver.dropMatrixMessagesAfterSecs) {
                const now = Date.now();
                if ((now - event.origin_server_ts) >
                    (1000 * this.config.homeserver.dropMatrixMessagesAfterSecs)) {
                    log.info("Dropping old m.room.message event %s timestamped %d", event.event_id, event.origin_server_ts);
                    return BridgeRequest_1.BridgeRequestErr.ERR_DROPPED;
                }
            }
            // Cheeky crafting event into MatrixMessageEvent
            await this.matrixHandler.onMessage(request, event);
        }
        else if (event.type === "m.room.topic" && event.state_key === "") {
            await this.matrixHandler.onMessage(request, event);
        }
        else if (event.type === RoomConfig_1.RoomConfig.STATE_EVENT_TYPE && typeof event.state_key === 'string') {
            this.roomConfigs.invalidateConfig(event.room_id, event.state_key);
        }
        else if (typeof event.state_key === 'string' && this.matrixBanSyncer?.isTrackingRoomState(event.room_id)) {
            if (await this.matrixBanSyncer.handleIncomingState(event, event.room_id)) {
                await this.clientPool.checkForBannedConnectedUsers();
            }
        }
        else if (event.type === "m.room.member" && event.state_key) {
            if (!event.content || !event.content.membership) {
                return BridgeRequest_1.BridgeRequestErr.ERR_NOT_MAPPED;
            }
            this.privacyProtection.clearRoomFromCache(event.room_id);
            this.ircHandler.onMatrixMemberEvent({ ...event, state_key: event.state_key, content: {
                    membership: event.content.membership,
                } });
            const target = new matrix_appservice_bridge_1.MatrixUser(event.state_key);
            const sender = new matrix_appservice_bridge_1.MatrixUser(event.sender);
            // We must define `state_key` explicitly again for TS to be happy.
            const memberEvent = { ...event, state_key: event.state_key };
            if (event.content.membership === "invite") {
                await this.matrixHandler.onInvite(request, memberEvent, sender, target);
            }
            else if (event.content.membership === "join") {
                await this.matrixHandler.onJoin(request, memberEvent, target);
            }
            else if (["ban", "leave"].includes(event.content.membership)) {
                // Given a "self-kick" is a leave, and you can't ban yourself,
                // if the 2 IDs are different then we know it is either a kick
                // or a ban (or a rescinded invite)
                const isKickOrBan = target.getId() !== sender.getId();
                if (isKickOrBan) {
                    await this.matrixHandler.onKick(request, memberEvent, sender, target);
                }
                else {
                    await this.matrixHandler.onLeave(request, memberEvent, target);
                }
            }
        }
        else if (event.type === "m.room.power_levels" && event.state_key === "") {
            this.ircHandler.roomAccessSyncer.onMatrixPowerlevelEvent(event);
        }
        try {
            // Await this *after* handling the event.
            await updatePromise;
        }
        catch (ex) {
            log.debug("Could not update last seen time for user: %s", ex);
        }
        return undefined;
    }
    async onUserQuery(matrixUser) {
        const baseRequest = this.bridge.getRequestFactory().newRequest();
        const request = new BridgeRequest_1.BridgeRequest(baseRequest);
        await this.matrixHandler.onUserQuery(request, matrixUser.getId());
        // TODO: Lean on the bridge lib more
        return null; // don't provision, we already do atm
    }
    async onAliasQuery(alias) {
        const baseRequest = this.bridge.getRequestFactory().newRequest();
        const request = new BridgeRequest_1.BridgeRequest(baseRequest);
        await this.matrixHandler.onAliasQuery(request, alias);
        // TODO: Lean on the bridge lib more
        return null; // don't provision, we already do atm
    }
    onLog(line, isError) {
        if (isError) {
            log.error(line);
        }
        else {
            log.info(line);
        }
    }
    async getThirdPartyProtocol() {
        const servers = this.getServers();
        return {
            user_fields: ["domain", "nick"],
            location_fields: ["domain", "channel"],
            field_types: {
                domain: {
                    regexp: "[a-z0-9-_]+(\.[a-z0-9-_]+)*",
                    placeholder: "irc.example.com",
                },
                nick: {
                    regexp: "[^#\\s]+",
                    placeholder: "SomeNick",
                },
                channel: {
                    // TODO(paul): Declare & and + in this list sometime when the
                    //   bridge can support them
                    regexp: "[#][^\\s]+",
                    placeholder: "#channel",
                },
            },
            // TODO: The spec requires we return an icon, but we don't have support
            // for one yet.
            icon: "",
            instances: servers.map((server) => {
                return {
                    network_id: server.getNetworkId(),
                    bot_user_id: this.appServiceUserId,
                    desc: server.config.name || server.domain,
                    icon: server.config.icon,
                    fields: {
                        domain: server.domain,
                    },
                };
            }),
        };
    }
    async getThirdPartyLocation(protocol, fields) {
        if (!fields.domain) {
            throw { err: "Expected 'domain' field", code: 400 };
        }
        const domain = fields.domain.toLowerCase();
        if (!fields.channel) {
            throw { err: "Expected 'channel' field", code: 400 };
        }
        // TODO(paul): this ought to use IRC network-specific casefolding (e.g. rfc1459)
        const channel = fields.channel.toLowerCase();
        const server = this.getServer(domain);
        if (!server) {
            return [];
        }
        if (!server.config.dynamicChannels.enabled) {
            return [];
        }
        const alias = server.getAliasFromChannel(channel);
        return [
            {
                alias: alias,
                protocol: "irc",
                fields: {
                    domain: domain,
                    channel: channel,
                }
            }
        ];
    }
    async getThirdPartyUser(protocol, fields) {
        if (!fields.domain) {
            throw { err: "Expected 'domain' field", code: 400 };
        }
        const domain = fields.domain.toLowerCase();
        if (!fields.nick) {
            throw { err: "Expected 'nick' field", code: 400 };
        }
        // TODO(paul): this ought to use IRC network-specific casefolding (e.g. rfc1459)
        const nick = fields.nick.toLowerCase();
        const server = this.getServer(domain);
        if (!server) {
            return [];
        }
        const userId = server.getUserIdFromNick(nick);
        return [
            {
                userid: userId,
                protocol: "irc",
                fields: {
                    domain: domain,
                    nick: nick,
                }
            }
        ];
    }
    getIrcUserFromCache(server, userId) {
        return this.clientPool.getBridgedClientByUserId(server, userId);
    }
    getBridgedClientsForUserId(userId) {
        return this.clientPool.getBridgedClientsForUserId(userId);
    }
    getBridgedClientsForRegex(regex) {
        return this.clientPool.getBridgedClientsForRegex(regex);
    }
    getBridgedClient(server, userId, displayName) {
        return this.clientPool.getBridgedClient(server, userId, displayName);
    }
    getServer(domainName) {
        return this.ircServers.find((s) => s.domain === domainName) || null;
    }
    getServers() {
        return this.ircServers || [];
    }
    getMemberListSyncer(server) {
        return this.memberListSyncers[server.domain];
    }
    // TODO: Check how many of the below functions need to reside on IrcBridge still.
    aliasToIrcChannel(alias) {
        const ircServer = this.getServers().find((s) => s.claimsAlias(alias));
        if (!ircServer) {
            return {};
        }
        return {
            server: ircServer,
            channel: ircServer.getChannelFromAlias(alias)
        };
    }
    getServerForUserId(userId) {
        return this.getServers().find((s) => s.claimsUserId(userId)) || null;
    }
    async matrixToIrcUser(user) {
        const server = this.getServerForUserId(user.getId());
        const ircInfo = {
            server: server,
            nick: server ? server.getNickFromUserId(user.getId()) : null
        };
        if (!ircInfo.server || !ircInfo.nick) {
            throw Error("User ID " + user.getId() + " doesn't map to a server/nick");
        }
        return new IrcUser_1.IrcUser(ircInfo.server, ircInfo.nick, true);
    }
    async connectToIrcNetworks() {
        await Promise.all(this.ircServers.map((server) => this.clientPool.loginToServer(server)));
    }
    /**
     * Determines if a nick name already exists.
     */
    async checkNickExists(server, nick) {
        log.info("Querying for nick %s on %s", nick, server.domain);
        const client = await this.getBotClient(server);
        return await client.whois(nick) !== null;
    }
    async joinBot(ircRoom) {
        if (!ircRoom.server.isBotEnabled()) {
            log.info("joinBot: Bot is disabled.");
            return;
        }
        const client = await this.getBotClient(ircRoom.server);
        try {
            await client.joinChannel(ircRoom.channel);
        }
        catch (ex) {
            log.error("Bot failed to join channel %s", ircRoom.channel);
        }
    }
    async partBot(ircRoom) {
        log.info("Parting bot from %s on %s", ircRoom.channel, ircRoom.server.domain);
        const client = await this.getBotClient(ircRoom.server);
        await client.leaveChannel(ircRoom.channel);
    }
    async sendIrcAction(ircRoom, bridgedClient, action) {
        if (this.bridgeBlocker?.isBlocked) {
            log.info("Bridge is blocked, dropping IRC action");
            return;
        }
        log.info("Sending IRC message in %s as %s (connected=%s)", ircRoom.channel, bridgedClient.nick, Boolean(bridgedClient.status === BridgedClient_1.BridgedClientStatus.CONNECTED));
        await bridgedClient.sendAction(ircRoom, action);
    }
    async getBotClient(server) {
        const botClient = this.clientPool.getBot(server);
        if (botClient) {
            return botClient;
        }
        return this.clientPool.loginToServer(server);
    }
    async fetchJoinedRooms() {
        /** Fetching joined rooms is quicker on larger homeservers than trying to
         * /join each room in the mappings list. To ensure we start quicker,
         * the bridge will block on this call rather than blocking on all join calls.
         * On the most overloaded servers even this call may take several attempts,
         * so it will block indefinitely.
         */
        if (!this.bridge) {
            throw Error('Bridge is not ready');
        }
        let gotRooms = false;
        while (!gotRooms && this.bridgeState === 'starting') {
            try {
                const roomIds = await this.bridge.getIntent().matrixClient.getJoinedRooms();
                gotRooms = true;
                this.joinedRoomList = roomIds;
                log.info(`ASBot is in ${roomIds.length} rooms!`);
            }
            catch (ex) {
                log.error(`Failed to fetch roomlist from joined_rooms: ${ex}. Retrying`);
                await promiseutil.delay(DELAY_FETCH_ROOM_LIST_MS);
            }
        }
    }
    async onRoomUpgrade(oldRoomId, newRoomId) {
        log.info(`Room has been upgraded from ${oldRoomId} to ${newRoomId}`);
        log.info("Migrating channels");
        await this.getStore().roomUpgradeOnRoomMigrated(oldRoomId, newRoomId);
        // Get the channels for the room_id
        const rooms = await this.getStore().getIrcChannelsForRoomId(newRoomId);
        // Get users who we wish to leave.
        const asBot = this.bridge.getBot();
        if (!asBot) {
            throw Error('AppserviceBot is not ready');
        }
        log.info("Migrating state");
        const stateEvents = await this.bridge.getIntent().matrixClient.getRoomState(oldRoomId);
        const roomInfo = await asBot.getRoomInfo(oldRoomId, {
            state: {
                events: stateEvents
            }
        });
        const bridgingEvent = stateEvents.find((ev) => ev.type === "m.room.bridging");
        const bridgeInfoEvent = stateEvents.find((ev) => ev.type === matrix_appservice_bridge_1.BridgeInfoStateSyncer.EventType);
        if (bridgingEvent) {
            try {
                await this.bridge.getIntent().sendStateEvent(newRoomId, bridgingEvent.type, bridgingEvent.state_key, bridgingEvent.content);
                log.info("m.room.bridging event copied to new room");
            }
            catch (ex) {
                // We may not have permissions to do so, which means we are basically stuffed.
                log.warn(`Could not send m.room.bridging event to new room: ${ex}`);
            }
        }
        if (bridgeInfoEvent) {
            try {
                await this.bridge.getIntent().sendStateEvent(newRoomId, bridgeInfoEvent.type, bridgingEvent.state_key, bridgingEvent.content);
                log.info("Bridge info event copied to new room");
            }
            catch (ex) {
                // We may not have permissions to do so, which means we are basically stuffed.
                log.warn(`Could not send bridge info event to new room: ${ex}`);
            }
        }
        log.info("Migrating ghosts");
        await Promise.all(rooms.map((room) => {
            return this.getBridgedClient(room.getServer(), roomInfo.realJoinedUsers[0]).then((client) => {
                // This will invoke NAMES and make members join the new room,
                // so we don't need to await it.
                client.getNicks(room.getChannel());
                log.info(`Leaving ${roomInfo.remoteJoinedUsers.length} users from old room ${oldRoomId}.`);
                this.memberListSyncers[room.getServer().domain].addToLeavePool(roomInfo.remoteJoinedUsers, oldRoomId);
            });
        }));
        log.info(`Ghost migration to ${newRoomId} complete`);
    }
    /**
     * Calculate the number of idle users
     * @param server The IRC server which we want to scope the idle check to.
     * @param minIdleHours The minimum number of hours to be considered idle.
     * @param defaultOnline Whether the user should be defaulted to online or offline if we hold no data for them.
     * @param excludeRegex A regex of users to exclude from the check.
     * @param maxIdleHours The maximum number of hours to be considered
     *                     idle before they aren't considered part of the pool. By default, this isn't checked.
     * @returns An ordered array of userIds by their idle time in ascending order.
     */
    async calculateIdlenessPool(server, minIdleHours, defaultOnline = true, excludeRegex, maxIdleHours) {
        if (!this.activityTracker) {
            throw Error("activityTracker is not enabled");
        }
        if (!minIdleHours || minIdleHours < 0) {
            throw Error("'since' must be greater than 0");
        }
        const minIdleTime = minIdleHours * 60 * 60 * 1000;
        const maxIdleTime = maxIdleHours && maxIdleHours * 60 * 60 * 1000;
        const users = this.clientPool.getConnectedMatrixUsersForServer(server);
        log.debug(`${users.length} users are connected to the bridge`);
        const exclude = excludeRegex ? new RegExp(excludeRegex) : null;
        const usersToActiveTime = new Map();
        for (const userId of users) {
            if (!userId) {
                // The bot user has a userId of null, ignore it.
                continue;
            }
            if (exclude && exclude.test(userId)) {
                log.debug(`${userId} is excluded`);
                continue;
            }
            const { online, inactiveMs } = await this.activityTracker.isUserOnline(userId, minIdleTime, defaultOnline);
            if (online) {
                continue;
            }
            if (maxIdleTime && inactiveMs > maxIdleTime) {
                continue;
            }
            const clients = this.clientPool.getBridgedClientsForUserId(userId);
            if (clients.length === 0) {
                log.debug(`${userId} has no active clients`);
                continue;
            }
            usersToActiveTime.set(userId, inactiveMs);
        }
        return [...usersToActiveTime.entries()].sort((a, b) => b[1] - a[1]).map(user => user[0]);
    }
    /**
     * Warn users that they are in danger of being reaped from a room.
     * @param serverName The name of the IRC server which we want to scope the idle check to.
     * @param maxIdleHours The maximum number of hours a user can be considered idle for.
     * @param msg A message to send to affected idle users.
     * @param defaultOnline Whether the user should be defaulted to online or offline if we hold no data for them.
     * @param excludeRegex A regex of users to exclude from the check.
     */
    async warnConnectionReap(req, serverName, minIdleHours, msg, defaultOnline, excludeRegex, limit) {
        if (!minIdleHours || minIdleHours < 0) {
            throw Error("'since' must be greater than 0");
        }
        const server = serverName ? this.getServer(serverName) : this.getServers()[0];
        if (server === null) {
            throw Error("Server not found");
        }
        let userNumber = 0;
        for (const user of await this.calculateIdlenessPool(
        // If a user has been inactive for double the time that we consider idle,
        // then there isn't any point in notifying them, it's probably a dead or idle account.
        server, minIdleHours, defaultOnline, excludeRegex, minIdleHours * 2)) {
            userNumber++;
            if (limit && userNumber > limit) {
                break;
            }
            const internalRoom = await this.ircHandler.getOrCreateAdminRoom(req, user, server);
            await this.sendMatrixAction(internalRoom, undefined, new MatrixAction_1.MatrixAction(MatrixAction_1.ActionType.Notice, msg));
            // Sleep between requests, to avoid murdering the homeserver
            await new Promise(r => setTimeout(() => r(), 500));
        }
    }
    async connectionReap(logCb, reqServerName, maxIdleHours, reason = "User is inactive", dry = false, defaultOnline, excludeRegex, limit) {
        if (!maxIdleHours || maxIdleHours < 0) {
            throw Error("'since' must be greater than 0");
        }
        const server = reqServerName ? this.getServer(reqServerName) : this.getServers()[0];
        if (server === null) {
            throw Error("Server not found");
        }
        const req = new BridgeRequest_1.BridgeRequest(this.bridge.getRequestFactory().newRequest());
        const idleUsers = await this.calculateIdlenessPool(server, maxIdleHours, defaultOnline, excludeRegex);
        logCb(`${(await idleUsers).length} users are considered idle`);
        const serverName = server?.getReadableName();
        log.warn(`Running connection reaper for ${serverName} dryrun=${dry}`);
        let userNumber = 0;
        for (const userId of idleUsers) {
            userNumber++;
            if (limit && userNumber > limit) {
                logCb(`Hit limit. Not kicking any more users.`);
                break;
            }
            const clients = this.clientPool.getBridgedClientsForUserId(userId);
            const quitRes = dry ? "dry-run" : await this.matrixHandler.quitUser(req, userId, clients, null, reason);
            if (quitRes !== null) {
                logCb(`Didn't quit ${userId}: ${quitRes}`);
                continue;
            }
            logCb(`Quit ${userId} (${userNumber}/${idleUsers.length})`);
        }
        logCb(`Quit ${userNumber}/${idleUsers.length}`);
    }
    async atBridgedRoomLimit() {
        const limit = this.config.ircService.provisioning?.roomLimit;
        if (!limit) {
            return false;
        }
        const current = await this.dataStore.getRoomCount();
        return current >= limit;
    }
    async onUserActivityChanged(userActivity) {
        if (!this.isStartedUp) {
            // Only handle activity if we're running
            return;
        }
        for (const userId of userActivity.changed) {
            await this.getStore().storeUserActivity(userId, userActivity.dataSet.users[userId]);
        }
        await this.bridgeBlocker?.checkLimits(userActivity.activeUsers);
    }
}
exports.IrcBridge = IrcBridge;
IrcBridge.DEFAULT_LOCALPART = "appservice-irc";
//# sourceMappingURL=IrcBridge.js.map