"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 chalk_1 = __importDefault(require("chalk"));
const crypto_1 = __importDefault(require("crypto"));
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const auth_1 = __importDefault(require("./plugins/auth"));
const client_1 = __importDefault(require("./client"));
const config_1 = __importDefault(require("./config"));
const webpush_1 = __importDefault(require("./plugins/webpush"));
const log_1 = __importDefault(require("./log"));
class ClientManager {
    clients;
    sockets;
    identHandler;
    webPush;
    constructor() {
        this.clients = [];
    }
    init(identHandler, sockets) {
        this.sockets = sockets;
        this.identHandler = identHandler;
        this.webPush = new webpush_1.default();
        if (!config_1.default.values.public) {
            this.loadUsers();
            // LDAP does not have user commands, and users are dynamically
            // created upon logon, so we don't need to watch for new files
            if (!config_1.default.values.ldap.enable) {
                this.autoloadUsers();
            }
        }
    }
    findClient(name) {
        name = name.toLowerCase();
        return this.clients.find((u) => u.name.toLowerCase() === name);
    }
    loadUsers() {
        let users = this.getUsers();
        if (users.length === 0) {
            log_1.default.info(`There are currently no users. Create one with ${chalk_1.default.bold("thelounge add <name>")}.`);
            return;
        }
        const alreadySeenUsers = new Set();
        users = users.filter((user) => {
            user = user.toLowerCase();
            if (alreadySeenUsers.has(user)) {
                log_1.default.error(`There is more than one user named "${chalk_1.default.bold(user)}". Usernames are now case insensitive, duplicate users will not load.`);
                return false;
            }
            alreadySeenUsers.add(user);
            return true;
        });
        // This callback is used by Auth plugins to load users they deem acceptable
        const callbackLoadUser = (user) => {
            this.loadUser(user);
        };
        if (!auth_1.default.loadUsers(users, callbackLoadUser)) {
            // Fallback to loading all users
            users.forEach((name) => this.loadUser(name));
        }
    }
    autoloadUsers() {
        fs_1.default.watch(config_1.default.getUsersPath(), (_eventType, file) => {
            if (!file || !file.endsWith(".json")) {
                return;
            }
            const name = file.slice(0, -5);
            const userPath = config_1.default.getUserConfigPath(name);
            if (fs_1.default.existsSync(userPath)) {
                this.loadUser(name);
                return;
            }
            const client = lodash_1.default.find(this.clients, { name });
            if (client) {
                client.quit(true);
                this.clients = lodash_1.default.without(this.clients, client);
                log_1.default.info(`User ${chalk_1.default.bold(name)} disconnected and removed.`);
            }
        });
    }
    loadUser(name) {
        const userConfig = this.readUserConfig(name);
        if (!userConfig) {
            return;
        }
        let client = this.findClient(name);
        if (client) {
            if (userConfig.password !== client.config.password) {
                /**
                 * If we happen to reload an existing client, make super duper sure we
                 * have their latest password. We're not replacing the entire config
                 * object, because that could have undesired consequences.
                 *
                 * @see https://github.com/thelounge/thelounge/issues/598
                 */
                client.config.password = userConfig.password;
                log_1.default.info(`Password for user ${chalk_1.default.bold(name)} was reset.`);
            }
        }
        else {
            client = new client_1.default(this, name, userConfig);
            client.connect();
            this.clients.push(client);
        }
        return client;
    }
    getUsers = function () {
        if (!fs_1.default.existsSync(config_1.default.getUsersPath())) {
            return [];
        }
        return fs_1.default
            .readdirSync(config_1.default.getUsersPath())
            .filter((file) => file.endsWith(".json"))
            .map((file) => file.slice(0, -5));
    };
    addUser(name, password, enableLog) {
        if (path_1.default.basename(name) !== name) {
            throw new Error(`${name} is an invalid username.`);
        }
        const userPath = config_1.default.getUserConfigPath(name);
        if (fs_1.default.existsSync(userPath)) {
            log_1.default.error(`User ${chalk_1.default.green(name)} already exists.`);
            return false;
        }
        const user = {
            password: password || "",
            log: enableLog,
        };
        try {
            const tmpPath = userPath + ".tmp";
            fs_1.default.writeFileSync(tmpPath, JSON.stringify(user, null, "\t"), {
                mode: 0o600,
            });
            fs_1.default.renameSync(tmpPath, userPath);
        }
        catch (e) {
            log_1.default.error(`Failed to create user ${chalk_1.default.green(name)} (${e})`);
            throw e;
        }
        try {
            const userFolderStat = fs_1.default.statSync(config_1.default.getUsersPath());
            const userFileStat = fs_1.default.statSync(userPath);
            if (userFolderStat &&
                userFileStat &&
                (userFolderStat.uid !== userFileStat.uid || userFolderStat.gid !== userFileStat.gid)) {
                log_1.default.warn(`User ${chalk_1.default.green(name)} has been created, but with a different uid (or gid) than expected.`);
                log_1.default.warn("The file owner has been changed to the expected user. " +
                    "To prevent any issues, please run thelounge commands " +
                    "as the correct user that owns the config folder.");
                log_1.default.warn("See https://thelounge.chat/docs/usage#using-the-correct-system-user for more information.");
                fs_1.default.chownSync(userPath, userFolderStat.uid, userFolderStat.gid);
            }
        }
        catch (e) {
            // We're simply verifying file owner as a safe guard for users
            // that run `thelounge add` as root, so we don't care if it fails
        }
        return true;
    }
    getDataToSave(client) {
        const json = Object.assign({}, client.config, {
            networks: client.networks.map((n) => n.export()),
        });
        const newUser = JSON.stringify(json, null, "\t");
        const newHash = crypto_1.default.createHash("sha256").update(newUser).digest("hex");
        return { newUser, newHash };
    }
    saveUser(client, callback) {
        const { newUser, newHash } = this.getDataToSave(client);
        // Do not write to disk if the exported data hasn't actually changed
        if (client.fileHash === newHash) {
            return;
        }
        const pathReal = config_1.default.getUserConfigPath(client.name);
        const pathTemp = pathReal + ".tmp";
        try {
            // Write to a temp file first, in case the write fails
            // we do not lose the original file (for example when disk is full)
            fs_1.default.writeFileSync(pathTemp, newUser, {
                mode: 0o600,
            });
            fs_1.default.renameSync(pathTemp, pathReal);
            return callback ? callback() : true;
        }
        catch (e) {
            log_1.default.error(`Failed to update user ${chalk_1.default.green(client.name)} (${e})`);
            if (callback) {
                callback(e);
            }
        }
    }
    removeUser(name) {
        const userPath = config_1.default.getUserConfigPath(name);
        if (!fs_1.default.existsSync(userPath)) {
            log_1.default.error(`Tried to remove non-existing user ${chalk_1.default.green(name)}.`);
            return false;
        }
        fs_1.default.unlinkSync(userPath);
        return true;
    }
    readUserConfig(name) {
        const userPath = config_1.default.getUserConfigPath(name);
        if (!fs_1.default.existsSync(userPath)) {
            log_1.default.error(`Tried to read non-existing user ${chalk_1.default.green(name)}`);
            return false;
        }
        try {
            const data = fs_1.default.readFileSync(userPath, "utf-8");
            return JSON.parse(data);
        }
        catch (e) {
            log_1.default.error(`Failed to read user ${chalk_1.default.bold(name)}: ${e}`);
        }
        return false;
    }
}
exports.default = ClientManager;
