"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const lodash_1 = __importDefault(require("lodash"));
const crypto_1 = __importDefault(require("crypto"));
const irc_framework_1 = __importDefault(require("irc-framework"));
const chan_1 = __importDefault(require("./chan"));
const msg_1 = __importDefault(require("./msg"));
const prefix_1 = __importDefault(require("./prefix"));
const helper_1 = __importDefault(require("../helper"));
const config_1 = __importDefault(require("../config"));
const sts_1 = __importDefault(require("../plugins/sts"));
const clientCertificate_1 = __importDefault(require("../plugins/clientCertificate"));
const msg_2 = require("../../shared/types/msg");
const chan_2 = require("../../shared/types/chan");
class Network {
    nick;
    name;
    host;
    port;
    tls;
    userDisconnected;
    rejectUnauthorized;
    password;
    awayMessage;
    commands;
    username;
    realname;
    leaveMessage;
    sasl;
    saslAccount;
    saslPassword;
    channels;
    uuid;
    proxyHost;
    proxyPort;
    proxyUsername;
    proxyPassword;
    proxyEnabled;
    highlightRegex;
    irc;
    chanCache;
    ignoreList;
    keepNick;
    status;
    serverOptions;
    // TODO: this is only available on export
    hasSTSPolicy;
    constructor(attr) {
        lodash_1.default.defaults(this, attr, {
            name: "",
            nick: "",
            host: "",
            port: 6667,
            tls: false,
            userDisconnected: false,
            rejectUnauthorized: false,
            password: "",
            awayMessage: "",
            commands: [],
            username: "",
            realname: "",
            leaveMessage: "",
            sasl: "",
            saslAccount: "",
            saslPassword: "",
            channels: [],
            irc: null,
            serverOptions: {
                CHANTYPES: ["#", "&"],
                PREFIX: new prefix_1.default([
                    { symbol: "!", mode: "Y" },
                    { symbol: "@", mode: "o" },
                    { symbol: "%", mode: "h" },
                    { symbol: "+", mode: "v" },
                ]),
                NETWORK: "",
            },
            proxyHost: "",
            proxyPort: 1080,
            proxyUsername: "",
            proxyPassword: "",
            proxyEnabled: false,
            chanCache: [],
            ignoreList: [],
            keepNick: null,
        });
        if (!this.uuid) {
            this.uuid = crypto_1.default.randomUUID();
        }
        if (!this.name) {
            this.name = this.host;
        }
        this.channels.unshift(new chan_1.default({
            name: this.name,
            type: chan_2.ChanType.LOBBY,
            // The lobby only starts as muted if every channel (unless it's special) is muted.
            // This is A) easier to implement and B) stops some confusion on startup.
            muted: this.channels.length >= 1 &&
                this.channels.every((chan) => chan.muted || chan.type === chan_2.ChanType.SPECIAL),
        }));
    }
    validate(client) {
        // Remove !, :, @ and whitespace characters from nicknames and usernames
        const cleanNick = (str) => str.replace(/[\x00\s:!@]/g, "_").substring(0, 100);
        // Remove new lines and limit length
        const cleanString = (str) => str.replace(/[\x00\r\n]/g, "").substring(0, 300);
        this.setNick(cleanNick(String(this.nick || config_1.default.getDefaultNick())));
        if (!this.username) {
            // If username is empty, make one from the provided nick
            this.username = this.nick.replace(/[^a-zA-Z0-9]/g, "");
        }
        this.username = cleanString(this.username) || "thelounge";
        this.realname = cleanString(this.realname) || this.nick;
        this.leaveMessage = cleanString(this.leaveMessage);
        this.password = cleanString(this.password);
        this.host = cleanString(this.host).toLowerCase();
        this.name = cleanString(this.name);
        this.saslAccount = cleanString(this.saslAccount);
        this.saslPassword = cleanString(this.saslPassword);
        this.proxyHost = cleanString(this.proxyHost);
        this.proxyPort = this.proxyPort || 1080;
        this.proxyUsername = cleanString(this.proxyUsername);
        this.proxyPassword = cleanString(this.proxyPassword);
        this.proxyEnabled = !!this.proxyEnabled;
        const error = function (network, text) {
            network.getLobby().pushMessage(client, new msg_1.default({
                type: msg_2.MessageType.ERROR,
                text: text,
            }), true);
        };
        if (!this.port) {
            this.port = this.tls ? 6697 : 6667;
        }
        if (!["", "plain", "external"].includes(this.sasl)) {
            this.sasl = "";
        }
        if (config_1.default.values.lockNetwork) {
            // This check is needed to prevent invalid user configurations
            if (!config_1.default.values.public &&
                this.host &&
                this.host.length > 0 &&
                this.host !== config_1.default.values.defaults.host) {
                error(this, `The hostname you specified (${this.host}) is not allowed.`);
                return false;
            }
            if (config_1.default.values.public) {
                this.name = config_1.default.values.defaults.name;
                // Sync lobby channel name
                this.getLobby().name = config_1.default.values.defaults.name;
            }
            this.host = config_1.default.values.defaults.host;
            this.port = config_1.default.values.defaults.port;
            this.tls = config_1.default.values.defaults.tls;
            this.rejectUnauthorized = config_1.default.values.defaults.rejectUnauthorized;
        }
        if (this.host.length === 0) {
            error(this, "You must specify a hostname to connect.");
            return false;
        }
        const stsPolicy = sts_1.default.get(this.host);
        if (stsPolicy && !this.tls) {
            error(this, `${this.host} has an active strict transport security policy, will connect to port ${stsPolicy.port} over a secure connection.`);
            this.port = stsPolicy.port;
            this.tls = true;
            this.rejectUnauthorized = true;
        }
        return true;
    }
    createIrcFramework(client) {
        this.irc = new irc_framework_1.default.Client({
            version: false, // We handle it ourselves
            outgoing_addr: config_1.default.values.bind,
            enable_chghost: true,
            enable_echomessage: true,
            enable_setname: true,
            auto_reconnect: true,
            // Exponential backoff maxes out at 300 seconds after 9 reconnects,
            // it will keep trying for well over an hour (plus the timeouts)
            auto_reconnect_max_retries: 30,
            // TODO: this type should be set after setIrcFrameworkOptions
        });
        this.setIrcFrameworkOptions(client);
        this.irc.requestCap([
            "znc.in/self-message", // Legacy echo-message for ZNC
            "znc.in/playback", // See http://wiki.znc.in/Playback
        ]);
    }
    setIrcFrameworkOptions(client) {
        this.irc.options.host = this.host;
        this.irc.options.port = this.port;
        this.irc.options.password = this.password;
        this.irc.options.nick = this.nick;
        this.irc.options.username = config_1.default.values.useHexIp
            ? helper_1.default.ip2hex(client.config.browser.ip)
            : this.username;
        this.irc.options.gecos = this.realname;
        this.irc.options.tls = this.tls;
        this.irc.options.rejectUnauthorized = this.rejectUnauthorized;
        this.irc.options.webirc = this.createWebIrc(client);
        this.irc.options.client_certificate = null;
        if (this.proxyEnabled) {
            this.irc.options.socks = {
                host: this.proxyHost,
                port: this.proxyPort,
                user: this.proxyUsername,
                pass: this.proxyPassword,
            };
        }
        else {
            delete this.irc.options.socks;
        }
        if (!this.sasl) {
            delete this.irc.options.sasl_mechanism;
            // irc-framework has a funny fallback where it uses nick + server pw
            // in the sasl handshake, if account is undefined, so we need an empty
            // object here to really turn it off
            this.irc.options.account = {};
        }
        else if (this.sasl === "external") {
            this.irc.options.sasl_mechanism = "EXTERNAL";
            this.irc.options.account = {};
            this.irc.options.client_certificate = clientCertificate_1.default.get(this.uuid);
        }
        else if (this.sasl === "plain") {
            delete this.irc.options.sasl_mechanism;
            this.irc.options.account = {
                account: this.saslAccount,
                password: this.saslPassword,
            };
        }
    }
    createWebIrc(client) {
        if (!config_1.default.values.webirc ||
            !Object.prototype.hasOwnProperty.call(config_1.default.values.webirc, this.host)) {
            return null;
        }
        const webircObject = {
            password: config_1.default.values.webirc[this.host],
            username: "thelounge",
            address: client.config.browser?.ip,
            hostname: client.config.browser?.hostname,
            options: {},
        };
        // https://ircv3.net/specs/extensions/webirc#options
        if (client.config.browser?.isSecure) {
            webircObject.options = {
                secure: true,
            };
        }
        if (typeof config_1.default.values.webirc[this.host] === "function") {
            webircObject.password = null;
            return config_1.default.values.webirc[this.host](webircObject, this);
        }
        return webircObject;
    }
    edit(client, args) {
        const oldNetworkName = this.name;
        const oldNick = this.nick;
        const oldRealname = this.realname;
        this.keepNick = null;
        this.nick = args.nick;
        this.host = String(args.host || "");
        this.name = String(args.name || "") || this.host;
        this.port = parseInt(args.port, 10);
        this.tls = !!args.tls;
        this.rejectUnauthorized = !!args.rejectUnauthorized;
        this.password = String(args.password || "");
        this.username = String(args.username || "");
        this.realname = String(args.realname || "");
        this.leaveMessage = String(args.leaveMessage || "");
        this.sasl = String(args.sasl || "");
        this.saslAccount = String(args.saslAccount || "");
        this.saslPassword = String(args.saslPassword || "");
        this.proxyHost = String(args.proxyHost || "");
        this.proxyPort = parseInt(args.proxyPort, 10);
        this.proxyUsername = String(args.proxyUsername || "");
        this.proxyPassword = String(args.proxyPassword || "");
        this.proxyEnabled = !!args.proxyEnabled;
        // Split commands into an array
        this.commands = String(args.commands || "")
            .replace(/\r\n|\r|\n/g, "\n")
            .split("\n")
            .filter((command) => command.length > 0);
        // Sync lobby channel name
        this.getLobby().name = this.name;
        if (!this.validate(client)) {
            return;
        }
        if (this.name !== oldNetworkName) {
            // Send updated network name to all connected clients
            client.emit("network:name", {
                uuid: this.uuid,
                name: this.name,
            });
        }
        if (this.irc) {
            if (this.nick !== oldNick) {
                if (this.irc.connected) {
                    // Send new nick straight away
                    this.irc.changeNick(this.nick);
                }
                else {
                    this.irc.user.nick = this.nick;
                    // Update UI nick straight away if IRC is not connected
                    client.emit("nick", {
                        network: this.uuid,
                        nick: this.nick,
                    });
                }
            }
            if (this.irc.connected &&
                this.realname !== oldRealname &&
                this.irc.network.cap.isEnabled("setname")) {
                this.irc.raw("SETNAME", this.realname);
            }
            this.setIrcFrameworkOptions(client);
            if (this.irc.options?.username) {
                this.irc.user.username = this.irc.options.username;
            }
            if (this.irc.options?.gecos) {
                this.irc.user.gecos = this.irc.options.gecos;
            }
        }
        client.save();
    }
    destroy() {
        this.channels.forEach((channel) => channel.destroy());
    }
    isIgnoredUser(data) {
        return this.ignoreList.some((entry) => helper_1.default.compareHostmask(entry, data));
    }
    setNick(nick) {
        this.nick = nick;
        this.highlightRegex = new RegExp(
        // Do not match characters and numbers (unless IRC color)
        "(?:^|[^a-z0-9]|\x03[0-9]{1,2})" +
            // Escape nickname, as it may contain regex stuff
            lodash_1.default.escapeRegExp(nick) +
            // Do not match characters and numbers
            "(?:[^a-z0-9]|$)", 
        // Case insensitive search
        "i");
        if (this.keepNick === nick) {
            this.keepNick = null;
        }
        if (this.irc?.options) {
            this.irc.options.nick = nick;
        }
    }
    getFilteredClone(lastActiveChannel, lastMessage) {
        return {
            uuid: this.uuid,
            name: this.name,
            nick: this.nick,
            serverOptions: this.serverOptions,
            status: this.getNetworkStatus(),
            channels: this.channels.map((channel) => channel.getFilteredClone(lastActiveChannel, lastMessage)),
        };
    }
    getNetworkStatus() {
        const status = {
            connected: false,
            secure: false,
        };
        if (this.irc && this.irc.connection && this.irc.connection.transport) {
            const transport = this.irc.connection.transport;
            if (transport.socket) {
                const isLocalhost = ["127.0.0.1", "::1"].includes(transport.socket.remoteAddress);
                const isAuthorized = transport.socket.encrypted && transport.socket.authorized;
                status.connected = transport.isConnected();
                status.secure = isAuthorized || isLocalhost;
            }
        }
        return status;
    }
    addChannel(newChan) {
        let index = this.channels.length; // Default to putting as the last item in the array
        // Don't sort special channels in amongst channels/users.
        if (newChan.type === chan_2.ChanType.CHANNEL || newChan.type === chan_2.ChanType.QUERY) {
            // We start at 1 so we don't test against the lobby
            for (let i = 1; i < this.channels.length; i++) {
                const compareChan = this.channels[i];
                // Negative if the new chan is alphabetically before the next chan in the list, positive if after
                if (newChan.name.localeCompare(compareChan.name, undefined, {
                    sensitivity: "base",
                }) <= 0 ||
                    (compareChan.type !== chan_2.ChanType.CHANNEL && compareChan.type !== chan_2.ChanType.QUERY)) {
                    index = i;
                    break;
                }
            }
        }
        this.channels.splice(index, 0, newChan);
        return index;
    }
    quit(quitMessage) {
        if (!this.irc) {
            return;
        }
        // https://ircv3.net/specs/extensions/sts#rescheduling-expiry-on-disconnect
        sts_1.default.refreshExpiration(this.host);
        this.irc.quit(quitMessage || this.leaveMessage || config_1.default.values.leaveMessage);
    }
    exportForEdit() {
        const fieldsToReturn = [
            "uuid",
            "name",
            "nick",
            "password",
            "username",
            "realname",
            "leaveMessage",
            "sasl",
            "saslAccount",
            "saslPassword",
            "commands",
            "proxyEnabled",
            "proxyHost",
            "proxyPort",
            "proxyUsername",
            "proxyPassword",
        ];
        if (!config_1.default.values.lockNetwork) {
            fieldsToReturn.push("host");
            fieldsToReturn.push("port");
            fieldsToReturn.push("tls");
            fieldsToReturn.push("rejectUnauthorized");
        }
        const data = lodash_1.default.pick(this, fieldsToReturn);
        data.hasSTSPolicy = !!sts_1.default.get(this.host);
        return data;
    }
    export() {
        const network = lodash_1.default.pick(this, [
            "uuid",
            "awayMessage",
            "nick",
            "name",
            "host",
            "port",
            "tls",
            "userDisconnected",
            "rejectUnauthorized",
            "password",
            "username",
            "realname",
            "leaveMessage",
            "sasl",
            "saslAccount",
            "saslPassword",
            "commands",
            "ignoreList",
            "proxyHost",
            "proxyPort",
            "proxyUsername",
            "proxyEnabled",
            "proxyPassword",
        ]);
        network.channels = this.channels
            .filter(function (channel) {
            return channel.type === chan_2.ChanType.CHANNEL || channel.type === chan_2.ChanType.QUERY;
        })
            .map(function (chan) {
            const keys = ["name", "muted"];
            if (chan.type === chan_2.ChanType.CHANNEL) {
                keys.push("key");
            }
            else if (chan.type === chan_2.ChanType.QUERY) {
                keys.push("type");
            }
            return lodash_1.default.pick(chan, keys);
            // Override the type because we're omitting ID
        });
        return network;
    }
    getChannel(name) {
        name = name.toLowerCase();
        return lodash_1.default.find(this.channels, function (that, i) {
            // Skip network lobby (it's always unshifted into first position)
            return i > 0 && that.name.toLowerCase() === name;
        });
    }
    getLobby() {
        return this.channels[0];
    }
}
exports.default = Network;
