"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
    result["default"] = mod;
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
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 IrcUser_1 = require("../models/IrcUser");
const IrcRoom_1 = require("../models/IrcRoom");
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 matrix_lastactive_1 = require("matrix-lastactive");
const Provisioner_js_1 = require("../provisioning/Provisioner.js");
const PublicitySyncer_1 = require("./PublicitySyncer");
const matrix_appservice_1 = require("matrix-appservice");
const matrix_appservice_bridge_1 = require("matrix-appservice-bridge");
const MembershipQueue_1 = require("../util/MembershipQueue");
const BridgeStateSyncer_1 = require("./BridgeStateSyncer");
const prom_client_1 = require("prom-client");
const MetricsWorker_1 = require("../workers/MetricsWorker");
const PackageInfo_1 = require("../util/PackageInfo");
const log = 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
class IrcBridge {
    constructor(config, registration) {
        this.config = config;
        this.registration = registration;
        this.onAliasQueried = null;
        this.ircServers = [];
        this.memberListSyncers = {};
        this.joinedRoomList = [];
        this.activityTracker = null;
        this.startedUp = false;
        this.debugApi = null;
        this.provisioner = null;
        this.timers = null;
        // TODO: Don't log this to stdout
        matrix_appservice_bridge_1.Logging.configure({ console: config.ircService.logging.level });
        if (config.ircService.debugApi && config.ircService.debugApi.enabled) {
            this.activityTracker = new matrix_lastactive_1.MatrixActivityTracker({
                homeserverUrl: this.config.homeserver.url,
                accessToken: registration.getAppServiceToken(),
                usePresence: this.config.homeserver.enablePresence,
                serverName: this.config.homeserver.domain,
                logger: logging_1.getLogger("MxActivityTracker"),
                defaultOnline: true,
            });
        }
        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 roomLinkValidation = undefined;
        const provisioning = config.ircService.provisioning;
        if (provisioning && provisioning.enabled &&
            typeof (provisioning.ruleFile) === "string") {
            roomLinkValidation = {
                ruleFile: provisioning.ruleFile,
                triggerEndpoint: provisioning.enableReload
            };
        }
        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`,
            };
        }
        this.membershipCache = new matrix_appservice_bridge_1.MembershipCache();
        this.bridge = new matrix_appservice_bridge_1.Bridge(Object.assign(Object.assign({ 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) : null,
                onLog: this.onLog.bind(this),
                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, roomUpgradeOpts: {
                consumeEvent: true,
                migrateGhosts: false,
                onRoomMigrated: this.onRoomUpgrade.bind(this),
                migrateStoreEntries: false,
            }, membershipCache: this.membershipCache, migrateStoreEntries: false }));
        this.membershipQueue = new MembershipQueue_1.MembershipQueue(this.bridge);
        this.matrixHandler = new MatrixHandler_1.MatrixHandler(this, this.config.matrixHandler || {}, this.membershipQueue);
        this.ircHandler = new IrcHandler_1.IrcHandler(this, this.config.ircHandler, this.membershipQueue);
        // 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;
        if (this.config.ircService.metrics && this.config.ircService.metrics.enabled) {
            this.initialiseMetrics();
        }
        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_1.AppService({
            homeserverToken,
            httpMaxSizeBytes: (this.config.advanced || {}).maxTxnSize || TXN_SIZE_DEFAULT,
        });
    }
    initialiseMetrics() {
        const zeroAge = new matrix_appservice_bridge_1.PrometheusMetrics.AgeCounters();
        const registry = new prom_client_1.Registry();
        const usingRemoteMetrics = !!this.config.ircService.metrics.port;
        const metrics = this.bridge.getPrometheusMetrics(!usingRemoteMetrics, registry);
        if (this.config.ircService.metrics.port) {
            log.info(`Started metrics on http://${this.config.ircService.metrics.host}:${this.config.ircService.metrics.port}`);
            MetricsWorker_1.spawnMetricsWorker(this.config.ircService.metrics.port, this.config.ircService.metrics.host, () => {
                metrics.refresh();
                return registry.metrics();
            });
        }
        this.bridge.registerBridgeGauges(() => {
            const remoteUsersByAge = new matrix_appservice_bridge_1.PrometheusMetrics.AgeCounters(this.config.ircService.metrics.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"],
            })
        };
        // Custom IRC metrics
        const reconnQueue = metrics.addGauge({
            name: "clientpool_reconnect_queue",
            help: "Number of disconnected irc connections waiting to reconnect.",
            labels: ["server"]
        });
        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 ircHandlerCalls = metrics.addCounter({
            name: "irchandler_calls",
            help: "Track calls made to the IRC Handler",
            labels: ["method"]
        });
        const matrixHandlerConnFailureKicks = metrics.addCounter({
            name: "matrixhandler_connection_failure_kicks",
            help: "Track IRC connection failures resulting in kicks",
            labels: ["server"]
        });
        metrics.addCounter({
            name: "app_version",
            help: "Version number of the bridge",
            labels: ["version"],
        }).inc({ version: PackageInfo_1.getBridgeVersion() }, 1);
        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);
            });
            Object.keys(this.memberListSyncers).forEach((server) => {
                memberListLeaveQueue.set({ server }, this.memberListSyncers[server].getUsersWaitingToLeave());
                memberListJoinQueue.set({ server }, this.memberListSyncers[server].getUsersWaitingToJoin());
            });
            const ircMetrics = this.ircHandler.getMetrics();
            Object.entries(ircMetrics).forEach((kv) => {
                ircHandlerCalls.inc({ method: kv[0] }, kv[1]);
            });
        });
    }
    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 run(port) {
        var _a;
        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;
        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();
            log.info("Using NeDBDataStore for Datastore");
            this.dataStore = new NedbDataStore_1.NeDBDataStore(this.bridge.getUserStore(), this.bridge.getRoomStore(), this.config.homeserver.domain, pkeyPath);
        }
        else {
            throw Error("Incorrect database config");
        }
        await this.dataStore.removeConfigMappings();
        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());
            if (this.dataStore instanceof NedbDataStore_1.NeDBDataStore) {
                // monkey patch inspect() values to avoid useless NeDB
                // struct spam on the debug API.
                this.bridge.getUserStore().inspect = () => "UserStore";
                this.bridge.getRoomStore().inspect = () => "RoomStore";
            }
            this.debugApi.run();
        }
        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 = 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);
        }
        if (this.ircServers.length === 0) {
            throw Error("No IRC servers specified.");
        }
        // run the bridge (needs to be done prior to configure IRC side)
        await this.bridge.run(port, undefined, this.appservice, this.config.homeserver.bindHostname);
        this.addRequestCallbacks();
        if (!this.registration.getSenderLocalpart() ||
            !this.registration.getAppServiceToken()) {
            throw Error("FATAL: Registration file is missing a sender_localpart and/or AS token.");
        }
        // 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();
        for (const roomId of this.joinedRoomList) {
            this.membershipCache.setMemberEntry(roomId, this.appServiceUserId, "join");
        }
        if ((_a = this.config.ircService.bridgeInfoState) === null || _a === void 0 ? void 0 : _a.enabled) {
            this.bridgeStateSyncer = new BridgeStateSyncer_1.BridgeStateSyncer(this.dataStore, this.bridge, this);
            if (this.config.ircService.bridgeInfoState.initial) {
                this.bridgeStateSyncer.beginSync().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 = [];
        // HACK: Remember reconnectIntervals to put them back later
        //  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). This is just the
        //  quickest way to disable scheduler.
        const reconnectIntervalsMap = Object.create(null);
        this.ircServers.forEach((server) => {
            reconnectIntervalsMap[server.domain] = server.getReconnectIntervalMs();
            server.config.ircClients.reconnectIntervalMs = 0;
            // TODO reduce deps required to make MemberListSyncers.
            // TODO Remove injectJoinFn bodge
            this.memberListSyncers[server.domain] = new MemberListSyncer_1.MemberListSyncer(this, 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,
                    },
                    _injected: true,
                    _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_js_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 bluebird_1.default.all(memberlistPromises);
        // Reset reconnectIntervals
        this.ircServers.forEach((server) => {
            server.config.ircClients.reconnectIntervalMs = reconnectIntervalsMap[server.domain];
        });
        log.info("Startup complete.");
        this.startedUp = true;
    }
    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);
        }
    }
    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}`);
        }
        // SUCCESS
        this.bridge.getRequestFactory().addDefaultResolveCallback((req, _res) => {
            const res = _res;
            if (res === BridgeRequest_1.BridgeRequestErr.ERR_VIRTUAL_USER) {
                logMessage(req, "IGNORE virtual user");
                return; // these aren't true successes so don't skew graphs
            }
            else if (res === BridgeRequest_1.BridgeRequestErr.ERR_NOT_MAPPED) {
                logMessage(req, "IGNORE not mapped");
                return; // these aren't true successes so don't skew graphs
            }
            else if (res === BridgeRequest_1.BridgeRequestErr.ERR_DROPPED) {
                logMessage(req, "IGNORE dropped");
                this.logMetric(req, "dropped");
                return;
            }
            logMessage(req, "SUCCESS");
            this.logMetric(req, "success");
        });
        // FAILURE
        this.bridge.getRequestFactory().addDefaultRejectCallback((req) => {
            logMessage(req, "FAILED");
            this.logMetric(req, "fail");
            BridgeRequest_1.BridgeRequest.HandleExceptionForSentry(req, "fail");
        });
        // DELAYED
        this.bridge.getRequestFactory().addDefaultTimeoutCallback((req) => {
            logMessage(req, "DELAYED");
        }, DELAY_TIME_MS);
        // DEAD
        this.bridge.getRequestFactory().addDefaultTimeoutCallback((req) => {
            logMessage(req, "DEAD");
            this.logMetric(req, "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() {
        log.info("Killing all clients");
        await this.clientPool.killAllClients();
        if (this.dataStore) {
            await this.dataStore.destroy();
        }
    }
    get isStartedUp() {
        return this.startedUp;
    }
    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) {
        const intent = this.bridge.getIntent(from.userId);
        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
                });
            }
            else {
                await intent.sendMessage(room.getId(), {
                    msgtype: action.msgType,
                    body: action.text
                });
            }
            return;
        }
        else if (action.type === "topic" && action.text) {
            await intent.setRoomTopic(room.getId(), action.text);
            return;
        }
        throw Error("Unknown action: " + action.type);
    }
    uploadTextFile(fileName, plaintext) {
        return this.bridge.getIntent().getClient().uploadContent(Buffer.from(plaintext), {
            name: fileName,
            type: "text/plain; charset=utf-8",
            rawResponse: false,
            onlyContentUri: true,
        });
    }
    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.getClient().credentials.userId);
        matrixUser.setDisplayName(displayName);
        await this.getStore().storeMatrixUser(matrixUser);
        return matrixUser;
    }
    onEvent(request) {
        request.outcomeFrom(this._onEvent(request));
    }
    async _onEvent(baseRequest) {
        const event = baseRequest.getData();
        let updatePromise = null;
        if (event.sender && this.activityTracker) {
            updatePromise = this.dataStore.updateLastSeenTimeForUser(event.sender);
            this.activityTracker.bumpLastActiveTime(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;
                }
            }
            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 === "m.room.member") {
            if (!event.content || !event.content.membership) {
                return BridgeRequest_1.BridgeRequestErr.ERR_NOT_MAPPED;
            }
            this.ircHandler.onMatrixMemberEvent(event);
            const target = new matrix_appservice_bridge_1.MatrixUser(event.state_key);
            const sender = new matrix_appservice_bridge_1.MatrixUser(event.sender);
            if (event.content.membership === "invite") {
                await this.matrixHandler.onInvite(request, event, sender, target);
            }
            else if (event.content.membership === "join") {
                await this.matrixHandler.onJoin(request, event, target);
            }
            else if (["ban", "leave"].indexOf(event.content.membership) !== -1) {
                // 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, event, sender, target);
                }
                else {
                    await this.matrixHandler.onLeave(request, event, 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);
        }
    }
    getThirdPartyProtocol() {
        const servers = this.getServers();
        return bluebird_1.default.resolve({
            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",
                },
            },
            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 trackChannel(server, channel, key) {
        if (!server.isBotEnabled()) {
            log.info("trackChannel: Bot is disabled.");
            return new IrcRoom_1.IrcRoom(server, channel);
        }
        const client = await this.getBotClient(server);
        try {
            return await client.joinChannel(channel, key);
        }
        catch (ex) {
            log.error(ex);
            throw Error("Failed to join channel");
        }
    }
    connectToIrcNetworks() {
        return promiseutil.allSettled(this.ircServers.map((server) => bluebird_1.default.cast(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);
    }
    sendIrcAction(ircRoom, bridgedClient, action) {
        log.info("Sending IRC message in %s as %s (connected=%s)", ircRoom.channel, bridgedClient.nick, Boolean(bridgedClient.unsafeClient));
        return 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.
         */
        const bot = this.bridge.getBot();
        let gotRooms = false;
        while (!gotRooms) {
            try {
                const roomIds = await bot.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 bluebird_1.default.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();
        log.info("Migrating state");
        const stateEvents = await asBot.getClient().roomState(oldRoomId);
        //TODO:  _getRoomInfo is a private func and should be replaced.
        const roomInfo = asBot._getRoomInfo(oldRoomId, {
            state: {
                events: stateEvents
            }
        });
        const bridgingEvent = stateEvents.find((ev) => ev.type === "m.room.bridging");
        const bridgeInfoEvent = stateEvents.find((ev) => ev.type === BridgeStateSyncer_1.BridgeStateSyncer.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 bluebird_1.default.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, room.getChannel());
            });
        }));
        log.info(`Ghost migration to ${newRoomId} complete`);
    }
    async connectionReap(logCb, reqServerName, maxIdleHours, reason = "User is inactive", dry = false, defaultOnline, excludeRegex, limit) {
        if (!this.activityTracker) {
            throw Error("activityTracker is not enabled");
        }
        if (!maxIdleHours || maxIdleHours < 0) {
            throw Error("'since' must be greater than 0");
        }
        const maxIdleTime = maxIdleHours * 60 * 60 * 1000;
        const server = reqServerName ? this.getServer(reqServerName) : this.getServers()[0];
        const serverName = server === null || server === void 0 ? void 0 : server.getReadableName();
        if (server === null) {
            throw Error("Server not found");
        }
        log.warn(`Running connection reaper for ${serverName} dryrun=${dry}`);
        const req = new BridgeRequest_1.BridgeRequest(this.bridge.getRequestFactory().newRequest());
        logCb(`Connection reaping for ${serverName}`);
        const users = this.clientPool.getConnectedMatrixUsersForServer(server);
        logCb(`${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)) {
                logCb(`${userId} is excluded`);
                continue;
            }
            const { online, inactiveMs } = await this.activityTracker.isUserOnline(userId, maxIdleTime, defaultOnline);
            if (online) {
                continue;
            }
            const clients = this.clientPool.getBridgedClientsForUserId(userId);
            if (clients.length === 0) {
                logCb(`${userId} has no active clients`);
                continue;
            }
            usersToActiveTime.set(userId, inactiveMs);
        }
        logCb(`${usersToActiveTime.size} users are considered idle`);
        const sortedByActiveTime = [...usersToActiveTime.entries()].sort((a, b) => b[1] - a[1]).map(user => user[0]);
        let userNumber = 0;
        for (const userId of sortedByActiveTime) {
            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}/${usersToActiveTime.size})`);
        }
        logCb(`Quit ${userNumber}/${users.length}`);
    }
    async atBridgedRoomLimit() {
        var _a;
        const limit = (_a = this.config.ircService.provisioning) === null || _a === void 0 ? void 0 : _a.roomLimit;
        if (!limit) {
            return false;
        }
        const current = await this.dataStore.getRoomCount();
        return current >= limit;
    }
}
exports.IrcBridge = IrcBridge;
IrcBridge.DEFAULT_LOCALPART = "appservice-irc";
//# sourceMappingURL=IrcBridge.js.map