"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MatrixHandler = void 0;
const BridgeRequest_1 = require("../models/BridgeRequest");
const matrix_appservice_bridge_1 = require("matrix-appservice-bridge");
const MatrixAction_1 = require("../models/MatrixAction");
const IrcRoom_1 = require("../models/IrcRoom");
const IrcAction_1 = require("../models/IrcAction");
const formatting_1 = require("../irc/formatting");
const AdminRoomHandler_1 = require("./AdminRoomHandler");
const RoomCreation_1 = require("./RoomCreation");
async function reqHandler(req, promise) {
    try {
        const res = await promise;
        req.resolve(res);
        return res;
    }
    catch (err) {
        req.reject(err);
        throw err;
    }
}
const MSG_PMS_DISABLED = "[Bridge] Sorry, PMs are disabled on this bridge.";
const MSG_PMS_DISABLED_FEDERATION = "[Bridge] Sorry, PMs are disabled on this bridge over federation.";
/* Number of events to store in memory for use in replies. */
const DEFAULT_EVENT_CACHE_SIZE = 4096;
/* Length of the source text in a formatted reply message */
const REPLY_SOURCE_MAX_LENGTH = 32;
class MatrixHandler {
    constructor(ircBridge, config = {}, membershipQueue) {
        var _a;
        this.ircBridge = ircBridge;
        this.membershipQueue = membershipQueue;
        this.processingInvitesForRooms = {};
        this.eventCache = new Map();
        this.metrics = {};
        this.memberTracker = null;
        // maintain a list of room IDs which are being processed invite-wise. This is
        // required because invites are processed asyncly, so you could get invite->msg
        // and the message is processed before the room is created.
        this.eventCacheMaxSize = (_a = config.eventCacheSize) !== null && _a !== void 0 ? _a : DEFAULT_EVENT_CACHE_SIZE;
        // The media URL to use to transform mxc:// URLs when handling m.room.[file|image]s
        this.mediaUrl = ircBridge.config.homeserver.media_url || ircBridge.config.homeserver.url;
        this.adminHandler = new AdminRoomHandler_1.AdminRoomHandler(ircBridge, this);
    }
    // ===== Matrix Invite Handling =====
    /**
     * Process a Matrix invite event for an Admin room.
     * @param {Object} event : The Matrix invite event.
     * @param {Request} req : The request for this event.
     * @param {MatrixUser} inviter : The user who invited the bot.
     */
    async handleAdminRoomInvite(req, event, inviter) {
        req.log.info("Handling invite from user directed to bot.");
        // Real MX user inviting BOT to a private chat
        const mxRoom = new matrix_appservice_bridge_1.MatrixRoom(event.room_id);
        await this.membershipQueue.join(event.room_id, undefined, req, true);
        // Do not create an admin room if the room is marked as 'plumbed'
        const matrixClient = this.ircBridge.getAppServiceBridge().getIntent();
        try {
            const plumbedState = await matrixClient.getStateEvent(event.room_id, 'm.room.plumbing');
            if (plumbedState.status === "enabled") {
                req.log.info('This room is marked for plumbing (m.room.plumbing.status = "enabled"). ' +
                    'Not treating room as admin room.');
                return;
            }
        }
        catch (err) {
            req.log.info(`Not a plumbed room: Error retrieving m.room.plumbing (${err.data.error})`);
        }
        // clobber any previous admin room ID
        await this.ircBridge.getStore().storeAdminRoom(mxRoom, inviter.userId);
    }
    /**
     * Process a Matrix invite event for an Admin room.
     * @param {Object} event : The Matrix invite event.
     * @param {Request} req : The request for this event.
     * @param {IrcUser} invitedIrcUser : The IRC user the bot invited to a room.
     */
    async handleInviteFromBot(req, event, invitedIrcUser) {
        req.log.info("Handling invite from bot directed at %s on %s", invitedIrcUser.server.domain, invitedIrcUser.nick);
        // Bot inviting VMX to a matrix room which is mapped to IRC. Just make a
        // matrix user and join the room (we trust the bot, so no additional checks)
        const mxUser = await this.ircBridge.getMatrixUser(invitedIrcUser);
        await this.membershipQueue.join(event.room_id, mxUser.getId(), req, true);
    }
    async handleInviteFromUser(req, event, invited) {
        req.log.info("Handling invite from user directed at %s on %s", invited.nick, invited.server.domain);
        const invitedUser = await this.ircBridge.getMatrixUser(invited);
        const mxRoom = new matrix_appservice_bridge_1.MatrixRoom(event.room_id);
        const intent = this.ircBridge.getAppServiceBridge().getIntent(invitedUser.getId());
        const mxUser = new matrix_appservice_bridge_1.MatrixUser(event.sender);
        // Real MX user inviting VMX to a matrix room for PM chat
        if (!invited.server.allowsPms()) {
            req.log.error("Accepting invite, and then leaving: This server does not allow PMs.");
            await intent.join(event.room_id);
            await this.ircBridge.sendMatrixAction(mxRoom, invitedUser, new MatrixAction_1.MatrixAction("notice", MSG_PMS_DISABLED));
            await intent.leave(event.room_id);
            return;
        }
        // If no federated PMs are allowed, check the origin of the PM
        //  is same the domain as the bridge
        if (!invited.server.shouldFederatePMs()) {
            // Matches for the local part (the not-user part)
            if (mxUser.host !== this.ircBridge.domain) {
                req.log.error("Accepting invite, and then leaving: This server does not allow federated PMs.");
                await intent.join(event.room_id);
                await this.ircBridge.sendMatrixAction(mxRoom, invitedUser, new MatrixAction_1.MatrixAction("notice", MSG_PMS_DISABLED_FEDERATION));
                await intent.leave(event.room_id);
                return;
            }
        }
        // create a virtual Matrix user for the IRC user
        await this.membershipQueue.join(event.room_id, invitedUser.getId(), req, true);
        req.log.info("Joined %s to room %s", invitedUser.getId(), event.room_id);
        // check if this room is a PM room or not.
        let isPmRoom = event.content.is_direct === true;
        if (isPmRoom !== true) {
            // Legacy check
            const joinedMembers = Object.keys(await this.ircBridge.getAppServiceBridge().getBot().getJoinedMembers(event.room_id));
            isPmRoom = joinedMembers.length === 2 && joinedMembers.includes(event.sender);
        }
        if (isPmRoom) {
            // nick is the channel
            const ircRoom = new IrcRoom_1.IrcRoom(invited.server, invited.nick);
            await this.ircBridge.getStore().setPmRoom(ircRoom, mxRoom, event.sender, event.state_key);
            return;
        }
        req.log.error("This room isn't a 1:1 chat!");
        await intent.kick(event.room_id, invitedUser.getId(), "Group chat not supported.");
    }
    // === Admin room handling ===
    async onAdminMessage(req, event, adminRoom) {
        req.log.info("Received admin message from %s", event.sender);
        const botUser = new matrix_appservice_bridge_1.MatrixUser(this.ircBridge.appServiceUserId, undefined, false);
        // If an admin room has more than 2 people in it, kick the bot out
        let members = [];
        if (this.memberTracker) {
            // First call begins tracking, subsequent calls do nothing
            await this.memberTracker.trackRoom(adminRoom.getId());
            members = this.memberTracker.getState(adminRoom.getId(), 'm.room.member').filter((m) => m.content.membership && m.content.membership === "join");
        }
        else {
            req.log.warn('Member tracker not running');
        }
        if (members.length > 2) {
            req.log.error(`onAdminMessage: admin room has ${members.length}` +
                ` users instead of just 2; bot will leave`);
            // Notify users in admin room
            const notice = new MatrixAction_1.MatrixAction("notice", "There are more than 2 users in this admin room");
            await this.ircBridge.sendMatrixAction(adminRoom, botUser, notice);
            await this.ircBridge.getAppServiceBridge().getIntent(botUser.getId()).leave(adminRoom.getId());
            return;
        }
        await this.adminHandler.onAdminMessage(req, event, adminRoom);
        return;
    }
    async quitUser(req, userId, clientList, ircServer, reason) {
        let clients = clientList;
        if (ircServer) {
            // Filter to get the clients for the [specified] server
            clients = clientList.filter((bridgedClient) => bridgedClient.server.domain === ircServer.domain);
        }
        if (clients.length === 0) {
            req.log.info(`No bridgedClients for ${userId}`);
            return "You are not connected to any networks.";
        }
        for (let i = 0; i < clients.length; i++) {
            const bridgedClient = clients[i];
            if (bridgedClient.chanList.length === 0) {
                req.log.info(`Bridged client for ${userId} is not in any channels ` +
                    `on ${bridgedClient.server.domain}`);
            }
            else {
                // Get all rooms that the bridgedClient is in
                const uniqueRoomIds = new Set();
                (await Promise.all(bridgedClient.chanList.map((channel) => {
                    return this.ircBridge.getStore().getMatrixRoomsForChannel(bridgedClient.server, channel);
                })
                // flatten to a single unqiue set
                )).forEach((rSet) => rSet.forEach((r) => uniqueRoomIds.add(r.getId())));
                await Promise.all([...uniqueRoomIds].map(async (roomId) => {
                    try {
                        await this.membershipQueue.leave(roomId, userId, req, false, reason, this.ircBridge.appServiceUserId);
                    }
                    catch (err) {
                        req.log.error(err);
                        req.log.warn(`Could not kick ${bridgedClient.userId} ` +
                            `from bridged room ${roomId}: ${err.message}`);
                    }
                }));
            }
            req.log.info(`Killing bridgedClient (nick = ${bridgedClient.nick}) for ${bridgedClient.userId}`);
            // The success message will effectively be 'Your connection to ... has been lost.`
            await bridgedClient.kill(reason);
        }
        return null;
    }
    /**
     * Called when the AS receives a new Matrix invite/join/leave event.
     * @param {Object} event : The Matrix member event.
     */
    async _onMemberEvent(req, event) {
        if (!this.memberTracker) {
            const matrixClient = this.ircBridge.getAppServiceBridge().getClientFactory().getClientAs();
            this.memberTracker = new matrix_appservice_bridge_1.StateLookup({
                client: matrixClient,
                eventTypes: ['m.room.member']
            });
        }
        else {
            this.memberTracker.onEvent(event);
        }
    }
    /**
     * Called when a Matrix user tries to invite another user into a PM
     * @param {Object} event : The Matrix invite event.
     * @param {MatrixUser} inviter : The inviter (sender).
     * @param {MatrixUser} invitee : The invitee (receiver).
     * @return {Promise} which is resolved/rejected when the request finishes.
     */
    async handleInviteToPMRoom(req, event, inviter, invitee) {
        // We don't support this
        req.log.warn(`User ${inviter.getId()} tried to invite ${invitee.getId()} to a PM room. Disconnecting from room`);
        const store = this.ircBridge.getStore();
        const [room] = await store.getIrcChannelsForRoomId(event.room_id);
        await store.removePmRoom(event.room_id);
        const userId = room.server.getUserIdFromNick(room.channel);
        const intent = this.ircBridge.getAppServiceBridge().getIntent(userId);
        await intent.sendMessage(event.room_id, {
            msgtype: "m.notice",
            body: "This room has been disconnected from IRC. You cannot invite new users into a IRC PM. " +
                "Please create a new PM room.",
        });
        await intent.leave(event.room_id);
        return null;
    }
    /**
     * Called when the AS receives a new Matrix invite event.
     * @param {Object} event : The Matrix invite event.
     * @param {MatrixUser} inviter : The inviter (sender).
     * @param {MatrixUser} invitee : The invitee (receiver).
     * @return {Promise} which is resolved/rejected when the request finishes.
     */
    async _onInvite(req, event, inviter, invitee) {
        var _a;
        /*
        * (MX=Matrix user, VMX=Virtual matrix user, BOT=AS bot)
        * Valid invite flows:
        * [1] MX  --invite--> VMX  (starting a PM chat)
        * [2] bot --invite--> VMX  (invite-only room that the bot is in who is inviting virtuals)
        * [3] MX  --invite--> BOT  (admin room; auth)
        * [4] bot --invite--> MX   (bot telling real mx user IRC conn state) - Ignore.
        * [5] irc --invite--> MX   (real irc user PMing a Matrix user) - Ignore.
        * [6] MX  --invite--> BOT  (invite to private room to allow bot to bridge) - Ignore.
        * [7] MX  --invite--> MX   (matrix user inviting another matrix user)
        */
        req.log.info("onInvite: %s", JSON.stringify(event));
        this._onMemberEvent(req, event);
        // mark this room as being processed in case we simultaneously get
        // messages for this room (which would fail if we haven't done the
        // invite yet!)
        this.processingInvitesForRooms[event.room_id + event.state_key] = req.getPromise();
        req.getPromise().finally(() => {
            delete this.processingInvitesForRooms[event.room_id + event.state_key];
        });
        // Check if this room is known to us.
        const rooms = await this.ircBridge.getStore().getIrcChannelsForRoomId(event.room_id);
        const hasExistingRoom = rooms.length > 1;
        const inviteeIsVirtual = !!this.ircBridge.getServerForUserId(event.state_key);
        const inviterIsVirtual = !!this.ircBridge.getServerForUserId(event.sender);
        // work out which flow we're dealing with and fork off asap
        // is the invitee the bot?
        if (this.ircBridge.appServiceUserId === event.state_key) {
            if (event.content.is_direct && !hasExistingRoom) {
                // case [3]
                // This is a PM invite to the bot.
                await this.handleAdminRoomInvite(req, event, inviter);
            }
            // case[6]
            // Drop through so the invite stays active, but do not join the room.
        }
        else if (!inviterIsVirtual && ((_a = rooms[0]) === null || _a === void 0 ? void 0 : _a.getType()) === "pm") {
            // case[7]-pms
            return this.handleInviteToPMRoom(req, event, inviter, invitee);
        } // case[7]-groups falls through.
        // else is the invitee a real matrix user? If they are, there will be no IRC server
        else if (!inviteeIsVirtual) {
            // If this is a PM, we need to disconnect it
            // cases [4], [5]: We cannot accept on behalf of real matrix users, so nop
            return BridgeRequest_1.BridgeRequestErr.ERR_NOT_MAPPED;
        }
        else {
            // cases [1] and [2] : The invitee represents a real IRC user
            const ircUser = await this.ircBridge.matrixToIrcUser(invitee);
            // is the invite from the bot?
            if (this.ircBridge.appServiceUserId === event.sender) {
                await this.handleInviteFromBot(req, event, ircUser); // case [2]
            }
            else { // We check if this is an invite inside the func.
                await this.handleInviteFromUser(req, event, ircUser); // case [1]
            }
        }
        return null;
    }
    async _onJoin(req, event, user) {
        req.log.info("onJoin: %s", JSON.stringify(event));
        this._onMemberEvent(req, event);
        // membershiplists injects leave events when syncing initial membership
        // lists. We know if this event is injected because this flag is set.
        const syncKind = event._injected ? "initial" : "incremental";
        const promises = []; // one for each join request
        if (this.ircBridge.appServiceUserId === user.getId()) {
            // ignore messages from the bot
            return BridgeRequest_1.BridgeRequestErr.ERR_VIRTUAL_USER;
        }
        // is this a tracked channel?
        let ircRooms = await this.ircBridge.getStore().getIrcChannelsForRoomId(event.room_id);
        // =========== Bridge Bot Joining ===========
        // Make sure the bot is joining on all mapped IRC channels
        ircRooms.forEach((ircRoom) => {
            this.ircBridge.joinBot(ircRoom);
        });
        // =========== Client Joining ===========
        // filter out rooms which don't mirror matrix join parts and are NOT frontier
        // entries. Frontier entries must ALWAYS be joined else the IRC channel will
        // not be bridged!
        ircRooms = ircRooms.filter((room) => {
            return room.server.shouldSyncMembershipToIrc(syncKind, event.room_id) || event._frontier;
        });
        if (ircRooms.length === 0) {
            req.log.info("No tracked channels which mirror joins for this room.");
            return BridgeRequest_1.BridgeRequestErr.ERR_NOT_MAPPED;
        }
        // for each room (which may be on different servers)
        ircRooms.forEach((room) => {
            if (room.server.claimsUserId(user.getId())) {
                req.log.info("%s is a virtual user (claimed by %s)", user.getId(), room.server.domain);
                return;
            }
            // get the virtual IRC user for this user
            promises.push((async () => {
                let bridgedClient = null;
                try {
                    bridgedClient = await this.ircBridge.getBridgedClient(room.server, user.getId(), (event.content || {}).displayname);
                }
                catch (e) {
                    // We need to kick on failure to get a client.
                    req.log.info(`${user.getId()} failed to get a IRC connection. Kicking from room: ${e}`);
                    this.incrementMetric(room.server.domain, "connection_failure_kicks");
                    const excluded = room.server.isExcludedUser(user.getId());
                    await this.membershipQueue.leave(event.room_id, user.getId(), req, true, excluded && excluded.kickReason ? excluded.kickReason : `IRC connection failure.`, this.ircBridge.appServiceUserId);
                }
                if (!bridgedClient || !bridgedClient.userId) {
                    // For types, drop out early if we don't have a bridgedClient
                    return;
                }
                // Check for a displayname change and update nick accordingly.
                if (event.content && event.content.displayname !== bridgedClient.displayName) {
                    // Changing the nick requires that:
                    // - the server allows nick changes
                    // - the nick is not custom
                    const config = await this.ircBridge.getStore().getIrcClientConfig(bridgedClient.userId, room.server.domain);
                    if (config && room.server.allowsNickChanges() &&
                        !config.getDesiredNick()) {
                        try {
                            const newNick = room.server.getNick(bridgedClient.userId, event.content.displayname);
                            bridgedClient.changeNick(newNick, false);
                        }
                        catch (e) {
                            req.log.warn(`Didn't change nick on the IRC side: ${e}`);
                        }
                    }
                }
                await bridgedClient.joinChannel(room.channel); // join each channel
            })());
        });
        // We know ircRooms.length > 1. The only time when this isn't mapped into a Promise
        // is when there is a virtual user: TODO: clean this up! Control flow is hard.
        if (promises.length === 0) {
            return BridgeRequest_1.BridgeRequestErr.ERR_VIRTUAL_USER;
        }
        await Promise.all(promises);
        return null;
    }
    async _onKick(req, event, kicker, kickee) {
        req.log.info("onKick %s is kicking/banning %s from %s", kicker.getId(), kickee.getId(), event.room_id);
        this._onMemberEvent(req, event);
        /*
        We know this is a Matrix client kicking someone.
        There are 2 scenarios to consider here:
        - Matrix on Matrix kicking
        - Matrix on IRC kicking

        Matrix-Matrix
        =============
        __USER A____            ____USER B___
        |            |          |             |
        Matrix     vIRC1       Matrix        vIRC2 |     Effect
        -----------------------------------------------------------------------
        Kicker                 Kickee              |  vIRC2 parts channel.
                                                    This avoids potential permission issues
                                                    in case vIRC1 cannot kick vIRC2 on IRC.

        Matrix-IRC
        ==========
        __USER A____            ____USER B___
        |            |          |             |
        Matrix      vIRC        IRC       vMatrix  |     Effect
        -----------------------------------------------------------------------
        Kicker                            Kickee   |  vIRC tries to kick IRC via KICK command.
        */
        const ircRooms = await this.ircBridge.getStore().getIrcChannelsForRoomId(event.room_id);
        // do we have an active connection for the kickee? This tells us if they are real
        // or virtual.
        const kickeeClients = this.ircBridge.getBridgedClientsForUserId(kickee.getId());
        if (kickeeClients.length === 0) {
            // Matrix on IRC kicking, work out which IRC user to kick.
            let server = null;
            for (let i = 0; i < ircRooms.length; i++) {
                if (ircRooms[i].server.claimsUserId(kickee.getId())) {
                    server = ircRooms[i].server;
                    break;
                }
            }
            if (!server) {
                return; // kicking a bogus user
            }
            const kickeeNick = server.getNickFromUserId(kickee.getId());
            if (!kickeeNick) {
                return; // bogus virtual user ID
            }
            // work out which client will do the kicking
            const kickerClient = this.ircBridge.getIrcUserFromCache(server, kicker.getId());
            if (!kickerClient) {
                // well this is awkward.. whine about it and bail.
                req.log.error("%s has no client instance to send kick from. Cannot kick.", kicker.getId());
                return;
            }
            // we may be bridging this matrix room into many different IRC channels, and we want
            // to kick this user from all of them.
            for (let i = 0; i < ircRooms.length; i++) {
                if (ircRooms[i].server.domain !== server.domain) {
                    return;
                }
                kickerClient.kick(kickeeNick, ircRooms[i].channel, `Kicked by ${kicker.getId()}` +
                    (event.content.reason ? ` : ${event.content.reason}` : ""));
            }
        }
        else {
            // Matrix on Matrix kicking: part the channel.
            const kickeeServerLookup = {};
            kickeeClients.forEach((ircClient) => {
                kickeeServerLookup[ircClient.server.domain] = ircClient;
            });
            await Promise.all(ircRooms.map((async (ircRoom) => {
                // Make the connected IRC client leave the channel.
                const client = kickeeServerLookup[ircRoom.server.domain];
                if (!client) {
                    return; // not connected to this server
                }
                // If we aren't joined this will no-op.
                await client.leaveChannel(ircRoom.channel, `Kicked by ${kicker.getId()} ` +
                    (event.content.reason ? ` : ${event.content.reason}` : ""));
            })));
        }
    }
    async _onLeave(req, event, user) {
        req.log.info("onLeave: %s", JSON.stringify(event));
        // membershiplists injects leave events when syncing initial membership
        // lists. We know if this event is injected because this flag is set.
        const syncKind = event._injected ? "initial" : "incremental";
        if (this.ircBridge.appServiceUserId === user.getId()) {
            // ignore messages from the bot
            return BridgeRequest_1.BridgeRequestErr.ERR_VIRTUAL_USER;
        }
        // do we have an active connection for this user?
        let clientList = this.ircBridge.getBridgedClientsForUserId(user.getId());
        // filter out servers which don't mirror matrix join parts (unless it's a kick)
        clientList = clientList.filter((client) => {
            return (client.server.shouldSyncMembershipToIrc(syncKind, event.room_id) &&
                !client.server.claimsUserId(user.getId())); // not a virtual user
        });
        const serverLookup = {};
        clientList.forEach((ircClient) => {
            serverLookup[ircClient.server.domain] = ircClient;
        });
        // which channels should the connected client leave?
        const ircRooms = await this.ircBridge.getStore().getIrcChannelsForRoomId(event.room_id);
        // ========== Client Parting ==========
        // for each room, if we're connected to it, leave the channel.
        const promises = ircRooms.map(async (ircRoom) => {
            // Make the connected IRC client leave the channel.
            const client = serverLookup[ircRoom.server.domain];
            if (!client) {
                return; // not connected to this server
            }
            // leave it; if we aren't joined this will no-op.
            await client.leaveChannel(ircRoom.channel);
        });
        if (promises.length === 0) { // no connected clients
            return BridgeRequest_1.BridgeRequestErr.ERR_VIRTUAL_USER;
        }
        // =========== Bridge Bot Parting ===========
        // For membership list syncing only
        await Promise.all(ircRooms.map(async (ircRoom) => {
            const client = serverLookup[ircRoom.server.domain];
            if (!client) {
                return; // no client left the room, so no need to recheck part room.
            }
            if (!ircRoom.server.isBotEnabled()) {
                return; // don't do expensive queries needlessly
            }
            if (!ircRoom.server.shouldJoinChannelsIfNoUsers()) {
                if (ircRoom.server.domain) {
                    // this = IrcBridge
                    await this.ircBridge.getMemberListSyncer(ircRoom.server).checkBotPartRoom(ircRoom, req);
                }
            }
        }));
        await Promise.all(promises);
        return null;
    }
    /**
     * Called when the AS receives a new Matrix Event.
     * @return {Promise} which is resolved/rejected when the request finishes.
     */
    async _onMessage(req, event) {
        /*
        * Valid message flows:
        * Matrix --> IRC (Bridged communication)
        * Matrix --> Matrix (Admin room)
        */
        req.log.info("%s usr=%s rm=%s body=%s", event.type, event.sender, event.room_id, (event.content.body ? event.content.body.substring(0, 20) : ""));
        const mxAction = MatrixAction_1.MatrixAction.fromEvent(event, this.mediaUrl);
        const ircAction = IrcAction_1.IrcAction.fromMatrixAction(mxAction);
        if (ircAction === null) {
            req.log.info("IrcAction couldn't determine an action type.");
            return BridgeRequest_1.BridgeRequestErr.ERR_DROPPED;
        }
        // check if this message is from one of our virtual users
        const servers = this.ircBridge.getServers();
        for (let i = 0; i < servers.length; i++) {
            if (servers[i].claimsUserId(event.sender)) {
                req.log.info("%s is a virtual user (claimed by %s)", event.sender, servers[i].domain);
                return BridgeRequest_1.BridgeRequestErr.ERR_VIRTUAL_USER;
            }
        }
        // wait a while if we just got an invite else we may not have the mapping stored
        // yet...
        if (this.processingInvitesForRooms[event.room_id + event.sender]) {
            req.log.info("Holding request for %s until invite for room %s is done.", event.sender, event.room_id);
            await this.processingInvitesForRooms[event.room_id + event.sender];
            req.log.info("Finished holding event for %s in room %s", event.sender, event.room_id);
        }
        if (this.ircBridge.appServiceUserId === event.sender) {
            // ignore messages from the bot
            return BridgeRequest_1.BridgeRequestErr.ERR_VIRTUAL_USER;
        }
        const ircRooms = await this.ircBridge.getStore().getIrcChannelsForRoomId(event.room_id);
        // Sometimes bridge's message each other and get stuck in a silly loop. Ensure it's m.text
        if (ircRooms.length === 0 && event.content && event.content.msgtype === "m.text") {
            // This is used to ensure type safety.
            const body = event.content.body;
            if (body === undefined) {
                return BridgeRequest_1.BridgeRequestErr.ERR_DROPPED;
            }
            // could be an Admin room, so check.
            const adminRoom = await this.ircBridge.getStore().getAdminRoomById(event.room_id);
            if (!adminRoom) {
                req.log.info("No mapped channels.");
                return BridgeRequest_1.BridgeRequestErr.ERR_DROPPED;
            }
            // process admin request
            await this.onAdminMessage(req, Object.assign(Object.assign({}, event), { content: { body } }), adminRoom);
        }
        // Check for other matrix rooms which are bridged to this channel.
        // If there are other rooms, send this message directly to that room as the virtual matrix user.
        // E.g: send this message to MROOM2 and MROOM3:
        //
        // MROOM1            MROOM2             MROOM3
        //   |                 |                  |
        //   +->>MSG>>----------------------------+
        //                 |                  |
        //                #chan              #chan2
        //
        const otherMatrixRoomIdsToServers = Object.create(null);
        const messageSendPromiseSet = [];
        const fetchRoomsPromiseSet = [];
        ircRooms.forEach((ircRoom) => {
            if (ircRoom.server.claimsUserId(event.sender)) {
                req.log.info("%s is a virtual user (claimed by %s)", event.sender, ircRoom.server.domain);
                return;
            }
            req.log.info("Relaying message in %s on %s", ircRoom.channel, ircRoom.server.domain);
            if (ircRoom.getType() === "channel") {
                fetchRoomsPromiseSet.push((async () => {
                    const otherMatrixRooms = await this.ircBridge.getStore().getMatrixRoomsForChannel(ircRoom.server, ircRoom.channel);
                    otherMatrixRooms.forEach((mxRoom) => {
                        otherMatrixRoomIdsToServers[mxRoom.getId()] = ircRoom.server;
                    });
                })());
            }
            // If we already have a cached client then yay, but if we
            // don't then we need to hit out for their display name in
            // this room.
            let bridgedClient = this.ircBridge.getIrcUserFromCache(ircRoom.server, event.sender);
            if (!bridgedClient) {
                messageSendPromiseSet.push((async () => {
                    let displayName = undefined;
                    try {
                        const res = await this.ircBridge.getAppServiceBridge().getIntent().getStateEvent(event.room_id, "m.room.member", event.sender);
                        displayName = res.displayname;
                    }
                    catch (err) {
                        req.log.error("Failed to get display name: %s", err);
                        // this is non-fatal, continue.
                    }
                    bridgedClient = await this.ircBridge.getBridgedClient(ircRoom.server, event.sender, displayName);
                    await this.sendIrcAction(req, ircRoom, bridgedClient, ircAction, event);
                })());
            }
            else {
                // push each request so we don't block processing other rooms
                messageSendPromiseSet.push(this.sendIrcAction(req, ircRoom, bridgedClient, ircAction, event));
            }
        });
        await Promise.all(fetchRoomsPromiseSet);
        Object.keys(otherMatrixRoomIdsToServers).forEach((roomId) => {
            if (roomId === event.room_id) {
                return; // don't bounce back to the sender
            }
            const otherServer = otherMatrixRoomIdsToServers[roomId];
            // convert the sender's user ID to a nick and back to a virtual user for this server
            // then send from that user ID (yuck!).
            const n = otherServer.getNick(event.sender);
            const virtUserId = otherServer.getUserIdFromNick(n);
            messageSendPromiseSet.push(this.ircBridge.sendMatrixAction(new matrix_appservice_bridge_1.MatrixRoom(roomId), new matrix_appservice_bridge_1.MatrixUser(virtUserId), mxAction));
        });
        await Promise.all(messageSendPromiseSet);
        return null;
    }
    async sendIrcAction(req, ircRoom, ircClient, ircAction, event) {
        // Send the action as is if it is not a text message
        let text = event.content.body;
        if (event.content.msgtype !== "m.text" || !text) {
            await this.ircBridge.sendIrcAction(ircRoom, ircClient, ircAction);
            return;
        }
        let cacheBody = text;
        if (event.content["m.relates_to"] && event.content["m.relates_to"]["m.in_reply_to"]) {
            const eventId = event.content["m.relates_to"]["m.in_reply_to"].event_id;
            const reply = await this.textForReplyEvent(event, eventId, ircRoom);
            if (reply !== null) {
                ircAction.text = text = reply.formatted;
                cacheBody = reply.reply;
            }
        }
        this.eventCache.set(event.event_id, {
            body: cacheBody.substr(0, REPLY_SOURCE_MAX_LENGTH),
            sender: event.sender
        });
        // Cache events in here so we can refer to them for replies.
        if (this.eventCache.size > this.eventCacheMaxSize) {
            const delKey = this.eventCache.entries().next().value[0];
            this.eventCache.delete(delKey);
        }
        // The client might still be connected, for abundance of safety let's wait.
        await ircClient.waitForConnected();
        // Generate an array of individual messages that would be sent
        const potentialMessages = ircClient.getSplitMessages(ircRoom.channel, text);
        const lineLimit = ircRoom.server.getLineLimit();
        if (potentialMessages.length <= lineLimit) {
            await this.ircBridge.sendIrcAction(ircRoom, ircClient, ircAction);
            return;
        }
        // Message body too long, upload to HS instead
        // Use the current ISO datetime as the name of the file
        //  strip off milliseconds and replace 'T' with an underscore
        //  result e.g : 2016-08-03T10:40:48.620Z becomes 2016-08-03_10:40:48
        let fileName = new Date().toISOString()
            .split(/[T|\.]/)
            .splice(0, 2)
            .join('_') + '.txt';
        // somenick_2016-08-03_10:40:48.txt
        fileName = ircClient.nick + '_' + fileName;
        let contentUri = null;
        try {
            // Try to upload as a file and get URI
            //  (this could fail, see the catch statement)
            contentUri = await this.ircBridge.uploadTextFile(fileName, text);
        }
        catch (err) {
            // Uploading the file to HS could fail
            req.log.error("Failed to upload text file ", err);
        }
        // This is true if the upload was a success
        if (contentUri) {
            // Alter event object so that it is treated as if a file has been uploaded
            event.content.url = contentUri;
            event.content.msgtype = "m.file";
            event.content.body = "sent a long message: ";
            // Create a file event to reflect the recent upload
            const mAction = MatrixAction_1.MatrixAction.fromEvent(event, this.mediaUrl, "message.txt");
            const bigFileIrcAction = IrcAction_1.IrcAction.fromMatrixAction(mAction);
            if (!bigFileIrcAction) {
                return;
            }
            if (mAction.text) {
                // Replace "Posted a File with..."
                bigFileIrcAction.text = mAction.text;
            }
            // Notify the IRC side of the uploaded text file
            await this.ircBridge.sendIrcAction(ircRoom, ircClient, bigFileIrcAction);
        }
        else {
            req.log.warn("Sending truncated message");
            // Modify the event to become a truncated version of the original
            //  the truncation limits the number of lines sent to lineLimit.
            const msg = '\n...(truncated)';
            const sendingEvent = Object.assign(Object.assign({}, event), { content: {
                    msgtype: "m.text",
                    body: potentialMessages.splice(0, lineLimit - 1).join('\n') + msg
                } });
            // Recreate action from modified event
            const truncatedIrcAction = IrcAction_1.IrcAction.fromMatrixAction(MatrixAction_1.MatrixAction.fromEvent(sendingEvent, this.mediaUrl));
            if (truncatedIrcAction) {
                await this.ircBridge.sendIrcAction(ircRoom, ircClient, truncatedIrcAction);
            }
        }
    }
    /**
     * Called when the AS receives an alias query from the HS.
     * @param {string} roomAlias : The room alias queried.
     * @return {Promise} which is resolved/rejected when the request finishes.
     */
    async _onAliasQuery(req, roomAlias) {
        req.log.info("onAliasQuery %s", roomAlias);
        // check if alias maps to a valid IRC server and channel
        const channelInfo = this.ircBridge.aliasToIrcChannel(roomAlias);
        if (!channelInfo.channel) {
            throw new Error("Unknown alias: " + roomAlias); // bad alias
        }
        if (!channelInfo.server.createsPublicAliases()) {
            throw new Error("This server does not allow alias mappings.");
        }
        req.log.info("Mapped to %s on %s", channelInfo.channel, channelInfo.server.domain);
        // See if we are already tracking this channel (case-insensitive
        // channels but case-sensitive aliases)
        const matrixRooms = await this.ircBridge.getStore().getMatrixRoomsForChannel(channelInfo.server, channelInfo.channel);
        if (matrixRooms.length === 0) {
            await RoomCreation_1.trackChannelAndCreateRoom(this.ircBridge, req, {
                server: channelInfo.server,
                // lower case the name to join (there's a bug in the IRC lib
                // where the join callback never fires if you try to join
                // #WithCaps in channels :/)
                ircChannel: formatting_1.toIrcLowerCase(channelInfo.channel),
                roomAliasName: roomAlias.split(":")[0].substring(1),
                origin: "alias",
            });
        }
        else {
            // create an alias pointing to this room (take first)
            // TODO: Take first with public join_rules
            const roomId = matrixRooms[0].getId();
            req.log.info("Pointing alias %s to %s", roomAlias, roomId);
            await this.ircBridge.getAppServiceBridge().getBot().getClient().createAlias(roomAlias, roomId);
        }
    }
    /**
     * Called when the AS receives a user query from the HS.
     * @param {string} userId : The user ID queried.
     * @return {Promise} which is resolved/rejected when the request finishes.
     */
    async _onUserQuery(req, userId) {
        if (this.ircBridge.appServiceUserId === userId) {
            return;
        }
        req.log.info("onUserQuery: %s", userId);
        const matrixUser = new matrix_appservice_bridge_1.MatrixUser(userId);
        const ircUser = await this.ircBridge.matrixToIrcUser(matrixUser);
        await this.ircBridge.getMatrixUser(ircUser);
    }
    async textForReplyEvent(event, replyEventId, ircRoom) {
        const REPLY_REGEX = /> <(.*?)>(.*?)\n\n(.*)/;
        const REPLY_NAME_MAX_LENGTH = 12;
        const eventId = replyEventId;
        if (!event.content.body) {
            return null;
        }
        const match = REPLY_REGEX.exec(event.content.body);
        if (match === null || match.length !== 4) {
            return null;
        }
        let rplName;
        let rplSource;
        const rplText = match[3];
        const cachedEvent = this.eventCache.get(eventId);
        if (!cachedEvent) {
            // Fallback to fetching from the homeserver.
            try {
                const eventContent = await this.ircBridge.getAppServiceBridge().getIntent().getEvent(event.room_id, eventId);
                rplName = eventContent.sender;
                if (typeof (eventContent.content.body) !== "string") {
                    throw Error("'body' was not a string.");
                }
                const isReply = eventContent.content["m.relates_to"] &&
                    eventContent.content["m.relates_to"]["m.in_reply_to"];
                if (isReply) {
                    const sourceMatch = REPLY_REGEX.exec(eventContent.content.body);
                    rplSource = sourceMatch && sourceMatch.length === 4 ? sourceMatch[3] : event.content.body;
                }
                else {
                    rplSource = eventContent.content.body;
                }
                rplSource = rplSource.substr(0, REPLY_SOURCE_MAX_LENGTH);
                this.eventCache.set(eventId, { sender: rplName, body: rplSource });
            }
            catch (err) {
                // If we couldn't find the event, then frankly we can't
                // trust it and we won't treat it as a reply.
                return {
                    formatted: rplText,
                    reply: rplText,
                };
            }
        }
        else {
            rplName = cachedEvent.sender;
            rplSource = cachedEvent.body;
        }
        // Get the first non-blank line from the source.
        const lines = rplSource.split('\n').filter((line) => !/^\s*$/.test(line));
        if (lines.length > 0) {
            // Cut to a maximum length.
            rplSource = lines[0].substr(0, REPLY_SOURCE_MAX_LENGTH);
            // Ellipsis if needed.
            if (lines[0].length > REPLY_SOURCE_MAX_LENGTH) {
                rplSource = rplSource + "...";
            }
            // Wrap in formatting
            rplSource = ` "${rplSource}"`;
        }
        else {
            // Don't show a source because we couldn't format one.
            rplSource = "";
        }
        // Fetch the sender's IRC nick.
        const sourceClient = this.ircBridge.getIrcUserFromCache(ircRoom.server, rplName);
        if (sourceClient) {
            rplName = sourceClient.nick;
        }
        else {
            // If we couldn't find a client for them, they might be a ghost.
            const ghostName = ircRoom.getServer().getNickFromUserId(rplName);
            // If we failed to get a name, just make a guess of it.
            rplName = ghostName !== null ? ghostName : rplName.substr(1, Math.min(REPLY_NAME_MAX_LENGTH, rplName.indexOf(":") - 1));
        }
        return {
            formatted: `<${rplName}${rplSource}> ${rplText}`,
            reply: rplText,
        };
    }
    incrementMetric(serverDomain, metricName) {
        let metricSet = this.metrics[serverDomain];
        if (!metricSet) {
            metricSet = this.metrics[serverDomain] = {};
        }
        if (metricSet[metricName] === undefined) {
            metricSet[metricName] = 1;
        }
        else {
            metricSet[metricName]++;
        }
        this.metrics[serverDomain] = metricSet;
    }
    // EXPORTS
    onMemberEvent(req, event) {
        return reqHandler(req, this._onMemberEvent(req, event));
    }
    onInvite(req, event, inviter, invitee) {
        return reqHandler(req, this._onInvite(req, event, inviter, invitee));
    }
    onJoin(req, event, user) {
        return reqHandler(req, this._onJoin(req, event, user));
    }
    onLeave(req, event, user) {
        return reqHandler(req, this._onLeave(req, event, user));
    }
    onKick(req, event, kicker, kickee) {
        return reqHandler(req, this._onKick(req, event, kicker, kickee));
    }
    onMessage(req, event) {
        return reqHandler(req, this._onMessage(req, event));
    }
    onAliasQuery(req, alias) {
        return reqHandler(req, this._onAliasQuery(req, alias));
    }
    onUserQuery(req, userId) {
        return reqHandler(req, this._onUserQuery(req, userId));
    }
    getMetrics(serverDomain) {
        const metrics = this.metrics[serverDomain] || {};
        this.metrics[serverDomain] = {};
        return metrics || {};
    }
}
exports.MatrixHandler = MatrixHandler;
//# sourceMappingURL=MatrixHandler.js.map