"use strict";
/**
 *      Admin backend
 *
 *      Controls Adapter-Processes
 *
 *      Copyright 2014-2025 Denis Haev <dogafox@gmail.com>,
 *      MIT License
 *
 */
Object.defineProperty(exports, "__esModule", { value: true });
const semver = require("semver");
const axios_1 = require("axios");
const node_fs_1 = require("node:fs");
const node_os_1 = require("node:os");
const node_path_1 = require("node:path");
const node_crypto_1 = require("node:crypto");
const adapter_core_1 = require("@iobroker/adapter-core");
const i18n_1 = require("@iobroker/i18n");
const socket_classes_1 = require("@iobroker/socket-classes");
const ws_server_1 = require("@iobroker/ws-server");
const translations_1 = require("./lib/translations");
const web_1 = require("./lib/web");
const checkLinuxPass_1 = require("./lib/checkLinuxPass");
const adapterName = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, '..', 'package.json'), { encoding: 'utf-8' }))
    .name.split('.')
    .pop();
const { getInstalledInfo } = adapter_core_1.commonTools;
const ONE_HOUR_MS = 3_600_000;
const ERROR_PERMISSION = 'permissionError';
const CURRENT_MAX_MAJOR_NODEJS = 20;
const CURRENT_MAX_MAJOR_NPM = 8;
let socket;
let webServer;
let lastRepoUpdate;
// Cache of objects
let objects = {};
const secret = 'Zgfr56gFe87jJOM'; // Will be generated by first start
let systemLanguage = 'en';
class Admin extends adapter_core_1.Adapter {
    /** secret used for the socket connection */
    secret;
    /** Timer to update repository */
    timerRepo;
    updaterTimeout;
    ratingTimeout;
    timerNews;
    _tasks;
    changedPasswords = [];
    constructor(options = {}) {
        options = {
            ...options,
            name: adapterName, // adapter name
            logTransporter: true, // receive the logs
            systemConfig: true,
            install: () => void null,
        };
        super(options);
        this.on('objectChange', this.onObjectChange);
        this.on('stateChange', this.onStateChange);
        this.on('ready', this.onReady);
        this.on('message', this.onMessage);
        this.on('unload', this.onUnload);
        this.on('fileChange', this.onFileChange);
        this.on('log', this.onLog);
    }
    /**
     * Is called if a subscribed object changes
     *
     * @param id object ID that changed
     * @param obj the changed object value
     */
    onObjectChange = (id, obj) => {
        if (obj) {
            objects[id] = obj;
            if (id === 'system.config' && !this.config.language) {
                if (obj.common?.language) {
                    systemLanguage = obj.common.language;
                    if (webServer) {
                        webServer.setLanguage(systemLanguage);
                    }
                }
            }
            if (id === 'system.repositories' || id.match(/^system\.adapter\.[^.]+$/)) {
                if (this.updaterTimeout) {
                    clearTimeout(this.updaterTimeout);
                }
                this.updaterTimeout = setTimeout(() => {
                    this.updaterTimeout = null;
                    void this.writeUpdateInfo();
                }, 5_000);
            }
        }
        else {
            // console.log('objectDeleted: ' + id);
            if (objects[id]) {
                delete objects[id];
            }
        }
        // TODO Build in some threshold of messages
        socket?.objectChange(id, obj);
    };
    /**
     * Is called if a subscribed state was changed
     *
     * @param id state ID that changed
     * @param state the changed state value
     */
    onStateChange = (id, state) => {
        if (id === `system.adapter.${this.namespace}.plugins.sentry.enabled`) {
            webServer.resetIndexHtml();
        }
        socket?.stateChange(id, state);
    };
    onFileChange = (id, fileName, size) => {
        socket?.fileChange(id, fileName, size);
    };
    /**
     * Is called when databases are connected and adapter received configuration.
     */
    onReady = async () => {
        const systemConfig = await this.getForeignObjectAsync('system.config');
        if (systemConfig) {
            systemConfig.native = systemConfig.native || {};
            if (this.config.language) {
                systemLanguage = this.config.language;
            }
            else if (systemConfig.common?.language) {
                systemLanguage = systemConfig.common.language;
            }
            i18n_1.default.init(__dirname, systemLanguage).catch(e => this.log.error(`Cannot init i18n: ${e}`));
            if (!systemConfig.native.secret) {
                (0, node_crypto_1.randomBytes)(24, (_ex, buf) => {
                    this.secret = buf.toString('hex');
                    this.extendForeignObject('system.config', { native: { secret: this.secret } }).catch(e => this.log.error(`Cannot set secret: ${e}`));
                    this.init();
                });
            }
            else {
                this.secret = systemConfig.native.secret;
                this.init();
            }
        }
        else {
            this.secret = secret;
            this.log.error('Cannot find object system.config');
        }
    };
    /**
     * Some message was sent to this instance over the message box. Used by email, pushover, text2speech, ...
     * Using this method requires "common.messagebox" property to be set to true in io-package.json
     *
     * @param obj the message object
     */
    onMessage = (obj) => {
        if (!obj) {
            return;
        }
        if (obj.command === 'im') {
            // if not instance message
            socket.publishInstanceMessageAll(obj.from, obj.message.m, obj.message.s, obj.message.d);
        }
        else if (obj.command === 'checkFiles') {
            if (typeof obj.message === 'string') {
                try {
                    return (obj.callback &&
                        this.sendTo(obj.from, obj.command, { result: (0, node_fs_1.existsSync)(obj.message) }, obj.callback));
                }
                catch (e) {
                    return obj.callback && this.sendTo(obj.from, obj.command, { error: e.message }, obj.callback);
                }
            }
            else if (Array.isArray(obj.message)) {
                const result = {};
                for (let f = 0; f < obj.message.length; f++) {
                    try {
                        result[obj.message[f]] = (0, node_fs_1.existsSync)(obj.message[f]);
                    }
                    catch (e) {
                        result[obj.message[f]] = e.message;
                    }
                }
                return obj.callback && this.sendTo(obj.from, obj.command, result, obj.callback);
            }
        }
        else if (obj.command === 'autocomplete') {
            // just for test
            return (obj.callback &&
                this.sendTo(obj.from, obj.command, [
                    { value: 1, label: 'first' },
                    { value: 2, label: 'second' },
                ], obj.callback));
        }
        else if (obj.command === 'selectSendTo') {
            this.log.info(`SelectSendTo: ${JSON.stringify(obj.message)}`);
            // just for test
            return (obj.callback &&
                this.sendTo(obj.from, obj.command, [
                    { label: 'Afghanistan', value: 'AF' },
                    { label: 'Åland Islands', value: 'AX' },
                    { label: 'Albania', value: 'AL' },
                ], obj.callback));
        }
        else if (obj.command === 'url') {
            this.log.info(`url: ${JSON.stringify(obj.message)}`);
            // just for test
            return (obj.callback &&
                this.sendTo(obj.from, obj.command, { openUrl: obj.message._origin, saveConfig: true }, obj.callback));
        }
        else if (obj.command.startsWith('admin:')) {
            return this.processNotificationsGui(obj);
        }
        else if (obj.command.startsWith('test:')) {
            // just for test
            this.log.info(`test: ${JSON.stringify(obj.message)}`);
            return;
        }
        socket?.sendCommand(obj);
    };
    processNotificationsGui(obj) {
        if (obj.command === 'admin:getNotificationSchema') {
            const guiMessage = obj.message;
            const alreadyDone = this.changedPasswords.find(item => item.login === guiMessage.login && item.password === guiMessage.password);
            let schema;
            if (alreadyDone?.result === true) {
                schema = {
                    type: 'panel',
                    items: {
                        _info: {
                            type: 'staticText',
                            text: i18n_1.default.getTranslatedObject('The password for user "%s" was successfully changed', guiMessage.login),
                            style: { color: 'green' },
                            sm: 12,
                        },
                    },
                };
            }
            else if (alreadyDone?.result === false) {
                schema = {
                    type: 'panel',
                    items: {
                        _info: {
                            type: 'staticText',
                            text: i18n_1.default.getTranslatedObject('Cannot change password for %s:', guiMessage.login),
                            style: { color: 'orange' },
                            sm: 12,
                        },
                        _info1: {
                            newLine: true,
                            type: 'staticText',
                            text: `sudo passwd ${guiMessage.login}`,
                            style: { fontFamilies: 'monospace', color: 'green', backgroundColor: 'black' },
                            sm: 12,
                        },
                        _info3: {
                            newLine: true,
                            type: 'staticText',
                            text: i18n_1.default.getTranslatedObject('Enter your new password by prompt.'),
                            style: { fontFamilies: 'monospace', color: 'green', backgroundColor: 'black' },
                            sm: 12,
                        },
                    },
                };
            }
            else if (alreadyDone) {
                // Ask user for new password and for password repeat
                schema = {
                    type: 'panel',
                    items: {
                        _info: {
                            type: 'header',
                            size: 5,
                            text: i18n_1.default.getTranslatedObject(`User "%s" has well known password. We suggest to change it.`, guiMessage.login),
                            style: { color: 'orange' },
                            sm: 12,
                        },
                        password: {
                            newLine: true,
                            label: i18n_1.default.getTranslatedObject('New password'),
                            type: 'password',
                            help: i18n_1.default.getTranslatedObject('Minimal length is 6 chars'),
                            visible: true,
                            sm: 12,
                            md: 6,
                        },
                        passwordRepeat: {
                            label: i18n_1.default.getTranslatedObject('Password repeat'),
                            type: 'password',
                            visible: true,
                            sm: 12,
                            md: 6,
                        },
                        _send: {
                            newLine: true,
                            type: 'sendto',
                            command: 'admin:setPassword',
                            jsonData: `{ "oldPassword": "${guiMessage.password}", "login": "${guiMessage.login}", "password": "$\{data.password}", "passwordRepeat": "$\{data.passwordRepeat}" }`,
                            label: i18n_1.default.getTranslatedObject('Set password'),
                            disabled: '!data.password || !data.passwordRepeat || data.password.length < 6 || data.password !== data.passwordRepeat',
                            sm: 6,
                            md: 3,
                            variant: 'contained',
                        },
                    },
                };
            }
            else {
                schema = {
                    type: 'panel',
                    items: {
                        _info: {
                            type: 'staticText',
                            text: i18n_1.default.getTranslatedObject('This message is no more actual and was generated by other instance start'),
                            style: { color: 'grey' },
                            sm: 12,
                        },
                    },
                };
            }
            this.sendTo(obj.from, obj.command, { schema }, obj.callback);
        }
        else if (obj.command === 'admin:setPassword') {
            // compare password and passwordRepeat
            const guiMessage = obj.message;
            // empty password isn't allowed
            if (!guiMessage.password) {
                this.sendTo(obj.from, obj.command, {
                    command: {
                        command: 'message',
                        message: i18n_1.default.getTranslatedObject("Empty password isn't allowed"),
                        refresh: true,
                    },
                }, obj.callback);
            }
            else if (guiMessage.password !== guiMessage.passwordRepeat) {
                this.sendTo(obj.from, obj.command, {
                    command: {
                        command: 'message',
                        message: i18n_1.default.getTranslatedObject('Password and password repeat are not equal'),
                        refresh: true,
                    },
                }, obj.callback);
            }
            else if (guiMessage.password.length < 6) {
                this.sendTo(obj.from, obj.command, {
                    command: {
                        command: 'message',
                        message: i18n_1.default.getTranslatedObject('Password is too short (min 6 chars)'),
                        refresh: true,
                    },
                }, obj.callback);
            }
            else {
                void (0, checkLinuxPass_1.setLinuxPassword)(guiMessage.login, guiMessage.oldPassword, guiMessage.password).then(result => {
                    this.changedPasswords = this.changedPasswords.filter(item => item.password !== guiMessage.login && item.login !== guiMessage.oldPassword);
                    this.changedPasswords.push({
                        login: guiMessage.login,
                        password: guiMessage.oldPassword,
                        result: result === true,
                    });
                    if (result === true) {
                        this.sendTo(obj.from, obj.command, {
                            command: {
                                command: 'message',
                                message: i18n_1.default.getTranslatedObject('Password successfully changed for "%s"', guiMessage.login),
                                refresh: true,
                            },
                        }, obj.callback);
                    }
                    else {
                        this.sendTo(obj.from, obj.command, {
                            command: {
                                command: 'message',
                                message: i18n_1.default.getTranslatedObject(`Cannot change password for "%s": %s`, guiMessage.login, result),
                                style: { color: 'red' },
                                refresh: true,
                            },
                        }, obj.callback);
                    }
                });
            }
        }
    }
    /**
     * Is called when adapter shuts down - callback has to be called under any circumstances!
     *
     * @param callback Callback after unloading
     */
    onUnload = (callback) => {
        if (this.timerRepo) {
            clearTimeout(this.timerRepo);
            this.timerRepo = null;
        }
        if (this.timerNews) {
            clearTimeout(this.timerNews);
            this.timerNews = null;
        }
        if (this.ratingTimeout) {
            clearTimeout(this.ratingTimeout);
            this.ratingTimeout = null;
        }
        if (this.updaterTimeout) {
            clearTimeout(this.updaterTimeout);
            this.updaterTimeout = null;
        }
        try {
            this.log.info(`terminating http${this.config.secure ? 's' : ''} server on port ${this.config.port}`);
            socket?.close();
            webServer.close();
            callback();
        }
        catch {
            callback();
        }
    };
    onLog = (obj) => socket?.sendLog(obj);
    async createUpdateInfo() {
        const promises = [];
        // create a connected object and state
        const updatesNumberObj = objects[`${this.namespace}.info.updatesNumber`];
        if (!updatesNumberObj || !updatesNumberObj.common || updatesNumberObj.common.type !== 'number') {
            const obj = {
                _id: 'info.updatesNumber',
                type: 'state',
                common: {
                    role: 'indicator.updates',
                    name: {
                        en: 'Number of adapters to update',
                        de: 'Anzahl der zu aktualisierenden Adapter',
                        ru: 'Количество адаптеров для обновления',
                        pt: 'Número de adaptadores para atualizar',
                        nl: 'Aantal adapters om te updaten',
                        fr: "Nombre d'adaptateurs à mettre à jour",
                        it: 'Numero di adattatori da aggiornare',
                        es: 'Número de adaptadores para actualizar',
                        pl: 'Liczba adapterów do aktualizacji',
                        uk: 'Кількість адаптерів для оновлення',
                        'zh-cn': '要更新的适配器数量',
                    },
                    type: 'number',
                    read: true,
                    write: false,
                    def: 0,
                },
                native: {},
            };
            await this.setObject(obj._id, obj);
        }
        const updatesListObj = objects[`${this.namespace}.info.updatesList`];
        if (!updatesListObj || !updatesListObj.common || updatesListObj.common.type !== 'string') {
            const obj = {
                _id: 'info.updatesList',
                type: 'state',
                common: {
                    role: 'indicator.updates',
                    name: {
                        en: 'List of adapters to update',
                        de: 'Liste der zu aktualisierenden Adapter',
                        ru: 'Список адаптеров для обновления',
                        pt: 'Lista de adaptadores para atualizar',
                        nl: 'Lijst met adapters die moeten worden bijgewerkt',
                        fr: 'Liste des adaptateurs à mettre à jour',
                        it: 'Elenco degli adattatori da aggiornare',
                        es: 'Lista de adaptadores para actualizar',
                        pl: 'Lista adapterów do aktualizacji',
                        uk: 'Список адаптерів для оновлення',
                        'zh-cn': '要更新的适配器列表',
                    },
                    type: 'string',
                    read: true,
                    write: false,
                    def: '',
                },
                native: {},
            };
            await this.setObject(obj._id, obj);
        }
        const newUpdatesObj = objects[`${this.namespace}.info.newUpdates`];
        if (!newUpdatesObj || !newUpdatesObj.common || newUpdatesObj.common.type !== 'boolean') {
            const obj = {
                _id: 'info.newUpdates',
                type: 'state',
                common: {
                    role: 'indicator.updates',
                    name: {
                        en: 'Indicator if new adapter updates are available',
                        de: 'Anzeige, ob neue Adapter-Updates verfügbar sind',
                        ru: 'Индикатор наличия новых обновлений адаптера',
                        pt: 'Indicador se novas atualizações do adaptador estão disponíveis',
                        nl: 'Indicator of er nieuwe adapter-updates beschikbaar zijn',
                        fr: "Indicateur si de nouvelles mises à jour de l'adaptateur sont disponibles",
                        it: "Indicatore se sono disponibili nuovi aggiornamenti dell'adattatore",
                        es: 'Indicador si hay nuevas actualizaciones de adaptadores disponibles',
                        pl: 'Wskaźnik, czy dostępne są nowe aktualizacje adaptera',
                        uk: 'Індикатор наявності нових оновлень адаптера',
                        'zh-cn': '指示是否有新的适配器更新可用',
                    },
                    type: 'boolean',
                    read: true,
                    write: false,
                    def: false,
                },
                native: {},
            };
            promises.push(this.setObjectAsync(obj._id, obj));
        }
        const updatesJsonObj = objects[`${this.namespace}.info.updatesJson`];
        if (!updatesJsonObj || !updatesJsonObj.common || updatesJsonObj.common.type !== 'string') {
            const obj = {
                _id: 'info.updatesJson',
                type: 'state',
                common: {
                    role: 'indicator.updates',
                    name: {
                        en: 'JSON string with adapter update information',
                        de: 'JSON-String mit Adapteraktualisierungsinformationen',
                        ru: 'Строка JSON с информацией об обновлении адаптера',
                        pt: 'String JSON com informações de atualização do adaptador',
                        nl: 'JSON-tekenreeks met adapter-update-informatie',
                        fr: "Chaîne JSON avec les informations de mise à jour de l'adaptateur",
                        it: "Stringa JSON con informazioni sull'aggiornamento dell'adattatore",
                        es: 'Cadena JSON con información de actualización del adaptador',
                        pl: 'Ciąg JSON z informacjami o aktualizacji adaptera',
                        uk: 'Рядок JSON з інформацією про оновлення адаптера',
                        'zh-cn': '带有适配器更新信息的 JSON 字符串',
                    },
                    type: 'string',
                    read: true,
                    write: false,
                    def: '{}',
                },
                native: {},
            };
            promises.push(this.setObjectAsync(obj._id, obj));
        }
        const lastUpdateCheckObj = objects[`${this.namespace}.info.lastUpdateCheck`];
        if (!lastUpdateCheckObj || !lastUpdateCheckObj.common || lastUpdateCheckObj.common.type !== 'number') {
            const obj = {
                _id: 'info.lastUpdateCheck',
                type: 'state',
                common: {
                    role: 'value.time',
                    name: {
                        en: 'Timestamp of last update check',
                        de: 'Zeitstempel der letzten UpdatePrüfung',
                        ru: 'Отметка времени последней проверки обнвлений',
                        pt: 'Timestamp da última verificação de atualizaçã',
                        nl: 'Tijdstempel van laatste updatecontrole',
                        fr: 'Horodatage de la dernière vérificationde mise à jour',
                        it: "Timestamp dell'ultimo controllo di aggiornamento",
                        es: 'Marca de tiempo de la última verificación de actalización',
                        pl: 'Znacznik czasu ostatniego sprawdzenia aktualizacji',
                        uk: 'Відмітка часу останньої перевірки оновлень',
                        'zh-cn': '上次更新检查的时间戳',
                    },
                    type: 'number',
                    read: true,
                    write: false,
                    def: 0,
                },
                native: {},
            };
            promises.push(this.setObjectAsync(obj._id, obj));
        }
        await Promise.all(promises);
    }
    upToDate(v1, v2) {
        return semver.gt(v2, v1);
    }
    /**
     * Write the update information to the states
     */
    async writeUpdateInfo(
    /** current sources, if given */
    sources) {
        if (!objects['system.config'] || !objects['system.config'].common) {
            return this.log.warn('Repository cannot be read. Invalid "system.config" object.');
        }
        const activeRepo = objects['system.config']
            .common?.activeRepo;
        const systemRepos = objects['system.repositories'];
        if (!sources) {
            sources = {};
            // If multi-repo case. Actual case
            if (Array.isArray(activeRepo)) {
                if (systemRepos?.native?.repositories) {
                    for (const repo of activeRepo) {
                        if (systemRepos.native.repositories[repo]?.json) {
                            Object.assign(sources, systemRepos.native.repositories[repo].json);
                        }
                        else {
                            await this.setState('info.updatesNumber', 0, true);
                            await this.setState('info.updatesList', '', true);
                            await this.setState('info.newUpdates', false, true);
                            await this.setState('info.updatesJson', '{}', true);
                            await this.setState('info.lastUpdateCheck', Date.now(), true);
                            if (systemRepos.native.repositories[repo]) {
                                this.log.warn(`Repository cannot be read: Active repo - ${repo}`);
                            }
                            else {
                                this.log.warn('No repository source configured');
                            }
                        }
                    }
                }
            }
            else if (activeRepo && systemRepos?.native?.repositories?.[activeRepo]?.json) {
                // This is deprecated and must be deleted after a couple of years: BF 2025.02.07
                sources = systemRepos.native.repositories[activeRepo].json;
            }
        }
        if (!Object.keys(sources).length) {
            await this.setState('info.updatesNumber', 0, true);
            await this.setState('info.updatesList', '', true);
            await this.setState('info.newUpdates', false, true);
            await this.setState('info.updatesJson', '{}', true);
            await this.setState('info.lastUpdateCheck', Date.now(), true);
            if (Array.isArray(activeRepo)) {
                let found = false;
                if (systemRepos?.native?.repositories) {
                    activeRepo.forEach(repo => {
                        if (systemRepos.native.repositories[repo]) {
                            this.log.warn(`Active repository "${repo}" cannot be read`);
                            found = true;
                        }
                    });
                }
                if (!found) {
                    this.log.warn(`No repository source configured. Possible values: ${systemRepos?.native?.repositories
                        ? Object.keys(systemRepos.native.repositories).join(', ')
                        : 'none'}. Active repo(s): "${activeRepo.join('", "')}"`);
                }
            }
            else if (activeRepo && systemRepos?.native?.repositories?.[activeRepo]) {
                this.log.warn(`Repository cannot be read. Active repo: ${JSON.stringify(activeRepo)}`);
            }
            else {
                this.log.warn(`No repository source configured. Possible values: ${systemRepos?.native?.repositories
                    ? Object.keys(systemRepos.native.repositories).join(', ')
                    : 'none'}. Active repo: ${JSON.stringify(activeRepo)}`);
            }
            return;
        }
        const installed = getInstalledInfo();
        const list = [];
        const updatesJson = {};
        let newUpdateIndicator = false;
        const state = await this.getStateAsync('info.updatesJson');
        let oldUpdates;
        if (typeof state?.val === 'string') {
            try {
                oldUpdates = JSON.parse(state.val) || {};
            }
            catch (e) {
                oldUpdates = {};
                this.log.warn(`Cannot parse info.updatesJson: ${e}`);
            }
        }
        else {
            oldUpdates = {};
        }
        Object.keys(sources).forEach(name => {
            try {
                if (installed[name]?.version && sources[name].version) {
                    if (sources[name].version !== installed[name].version &&
                        !this.upToDate(sources[name].version, installed[name].version)) {
                        // Check if updates are new or already known to user
                        if (!oldUpdates ||
                            !oldUpdates[name] ||
                            oldUpdates[name].availableVersion !== sources[name].version) {
                            newUpdateIndicator = true;
                        } // endIf
                        updatesJson[name] = {
                            availableVersion: sources[name].version,
                            installedVersion: installed[name].version,
                        };
                        // remove the first part of the name
                        const n = name.indexOf('.');
                        list.push(n === -1 ? name : name.substring(n + 1));
                    }
                }
            }
            catch (err) {
                this.log.warn(`Error on version check for ${name}: ${err}`);
            }
        });
        await this.setState('info.updatesNumber', list.length, true);
        await this.setState('info.updatesList', list.join(', '), true);
        await this.setState('info.newUpdates', newUpdateIndicator, true);
        await this.setState('info.updatesJson', JSON.stringify(updatesJson), true);
        await this.setState('info.lastUpdateCheck', Date.now(), true);
        if (!newUpdateIndicator) {
            return;
        }
        const textArr = [];
        for (const [adapter, updateInfo] of Object.entries(updatesJson)) {
            const text = (0, translations_1.getAdapterUpdateText)({
                adapter,
                installedVersion: updateInfo.installedVersion,
                newVersion: updateInfo.availableVersion,
                lang: systemLanguage,
            });
            textArr.push(text);
        }
        await this.registerNotification('admin', 'adapterUpdates', textArr.join('\n'));
    }
    initSocket(server, store) {
        const settings = {
            language: this.config.language,
            defaultUser: this.config.defaultUser,
            ttl: this.config.ttl,
            secure: this.config.secure,
            auth: this.config.auth,
            port: this.config.port,
            secret: this.secret,
        };
        socket = new socket_classes_1.SocketAdmin(settings, this, objects);
        socket.start(server, ws_server_1.SocketIO, { store, oauth2Only: true });
        // subscribe on all object changes
        socket.subscribe('objectChange', '*');
        this.getForeignObjectAsync('system.meta.uuid')
            .then(async (obj) => {
            if (obj?.native) {
                try {
                    await socket.updateRatings();
                }
                catch (error) {
                    this.log.error(`Cannot fetch ratings: ${error}`);
                }
            }
        })
            .catch(error => this.log.error(`Cannot read UUID: ${error}`));
    }
    async applyRightsToObjects(pattern, types) {
        let len = 0;
        if (typeof types === 'object') {
            for (const type of types) {
                len += await this.applyRightsToObjects(pattern, type);
            }
        }
        else {
            try {
                const doc = await this.getObjectViewAsync('system', types, {
                    startkey: `${pattern}.`,
                    endkey: `${pattern}.\u9999`,
                });
                this._tasks = this._tasks || [];
                for (const row of doc.rows) {
                    const obj = row.value;
                    if (!obj.acl || obj.acl.owner !== this.config.defaultUser) {
                        obj.acl.owner = this.config.defaultUser;
                        await this.setForeignObjectAsync(obj._id, obj);
                        len++;
                    }
                }
            }
            catch (e) {
                this.log.error(`Error applying rights to objects: ${e.message}`);
            }
        }
        return len;
    }
    async applyRights() {
        this.config.accessAllowedConfigs = this.config.accessAllowedConfigs || [];
        this.config.accessAllowedTabs = this.config.accessAllowedTabs || [];
        let len = 0;
        for (const id of this.config.accessAllowedConfigs) {
            const obj = await this.getForeignObjectAsync(`system.adapter.${id}`);
            if (obj?.acl && obj.acl.owner !== this.config.defaultUser) {
                obj.acl.owner = this.config.defaultUser;
                await this.setForeignObjectAsync(`system.adapter.${id}`, obj);
                len++;
            }
        }
        for (const id of this.config.accessAllowedTabs) {
            if (id.startsWith('devices.')) {
                // change rights of all `alias.*`
                len += await this.applyRightsToObjects('alias', ['state', 'channel']);
            }
            else if (id.startsWith('javascript.')) {
                // change rights of all script.js.*
                len += await this.applyRightsToObjects('javascript', ['script', 'channel']);
            }
            else if (id.startsWith('fullcalendar.')) {
                // change rights of all fullcalendar.*
                len += await this.applyRightsToObjects('fullcalendar', ['schedule']);
            }
            else if (id.startsWith('scenes.')) {
                // change rights of all scenes.*
                len += await this.applyRightsToObjects('scenes', ['state', 'channel']);
            }
        }
        if (len) {
            this.log.info(`Updated ${len} objects`);
        }
    }
    /**
     * Read news from server and register them as notifications
     */
    async updateNews() {
        if (this.timerNews) {
            clearTimeout(this.timerNews);
            this.timerNews = null;
        }
        this.checkNodeJsVersion().catch(e => this.log.warn(`Cannot check node.js versions: ${e}`));
        let oldNews;
        let newEtag;
        const oldEtag = (await this.getStateAsync('info.newsETag'))?.val;
        let etag;
        try {
            const res = await axios_1.default.get('https://iobroker.live/repo/news-hash.json', {
                timeout: 13_000,
                validateStatus: status => status < 400,
            });
            etag = res.data;
        }
        catch (e) {
            this.log.warn(`Cannot update news: ${e.response ? e.response.data : e.message || e.code}`);
        }
        let _newNews;
        if (etag && etag.hash !== oldEtag) {
            newEtag = etag.hash;
            try {
                const res = await axios_1.default.get('https://iobroker.live/repo/news.json', {
                    timeout: 14_000,
                    validateStatus: status => status < 400,
                });
                _newNews = res.data;
            }
            catch (e) {
                this.log.warn(`Cannot update news_: ${e.response ? e.response.data : e.message || e.code}`);
            }
        }
        else {
            newEtag = oldEtag;
            _newNews = [];
        }
        const newNews = Array.isArray(_newNews) ? _newNews : [];
        const newsState = await this.getStateAsync('info.newsFeed');
        try {
            oldNews = newsState?.val ? JSON.parse(newsState.val) : [];
        }
        catch {
            oldNews = [];
        }
        const originalOldNews = JSON.stringify(oldNews);
        const lastState = await this.getStateAsync('info.newsLastId');
        // find time of last ID
        let time = '';
        if (lastState?.val) {
            const item = oldNews.find(item => item.id === lastState.val);
            if (item) {
                time = item.created;
            }
        }
        try {
            // add all IDs newer than last seen
            newNews.forEach(item => {
                if (!lastState || !time || item.created > time) {
                    if (!oldNews.find(it => it.created === item.created)) {
                        oldNews.push(item);
                    }
                }
            });
            oldNews.sort((a, b) => (a.created > b.created ? -1 : a.created < b.created ? 1 : 0));
            // delete news older than 3 months
            let i;
            for (i = oldNews.length - 1; i >= 0; i--) {
                if (Date.now() - new Date(oldNews[i].created).getTime() > 180 * 24 * 3_600_000) {
                    oldNews.splice(i, 1);
                }
            }
            if (originalOldNews !== JSON.stringify(oldNews)) {
                await this.registerNewsNotifications(oldNews, lastState?.val);
                await this.setStateAsync('info.newsFeed', JSON.stringify(oldNews), true);
            }
            if (newEtag !== oldEtag) {
                await this.setStateAsync('info.newsETag', newEtag, true);
            }
        }
        catch (e) {
            this.log.error(`Cannot update news: ${e.message}`);
        }
        this.timerNews = setTimeout(() => this.updateNews(), 24 * ONE_HOUR_MS + 1);
    }
    /**
     * Add the news to the notification system
     *
     * @param messages sorted news
     * @param lastMessageId lastMessageId, all after this has already been seen
     */
    async registerNewsNotifications(messages, lastMessageId) {
        const adapters = await this.getObjectViewAsync('system', 'adapter', {
            startkey: 'system.adapter.\u0000',
            endkey: 'system.adapter.\u9999',
        });
        const operatingSystem = (0, node_os_1.platform)();
        const instances = await this.getObjectViewAsync('system', 'instance', {
            startkey: 'system.adapter.\u0000',
            endkey: 'system.adapter.\u9999',
        });
        const activeRepo = (await this.getForeignObjectAsync('system.config'))?.common.activeRepo;
        const uuid = (await this.getForeignObjectAsync('system.meta.uuid'))?.native.uuid;
        const nodeVersion = process.version;
        const npmVersion = await this.getNpmVersion();
        const today = Date.now();
        for (const message of messages) {
            if (!message) {
                continue;
            }
            if (message.id === lastMessageId) {
                break;
            }
            let showIt = true;
            if (showIt && message['date-start'] && new Date(message['date-start']).getTime() > today) {
                showIt = false;
            }
            else if (showIt && message['date-end'] && new Date(message['date-end']).getTime() < today) {
                showIt = false;
            }
            else if (showIt && message.conditions && Object.keys(message.conditions).length > 0) {
                Object.keys(message.conditions).forEach(key => {
                    if (showIt) {
                        const adapter = adapters.rows.find(adapter => adapter.id === `system.adapter.${key}`);
                        const condition = message.conditions[key];
                        if (!adapter && condition !== '!installed') {
                            showIt = false;
                        }
                        else if (adapter && condition === '!installed') {
                            showIt = false;
                        }
                        else if (adapter && condition === 'active') {
                            showIt = this.checkActive(key, instances);
                        }
                        else if (adapter && condition === '!active') {
                            showIt = !this.checkActive(key, instances);
                        }
                        else if (adapter?.value) {
                            showIt = this.checkConditions(condition, adapter.value.common.version);
                        }
                    }
                });
            }
            if (showIt && message['node-version']) {
                showIt = this.checkConditions(message['node-version'], nodeVersion);
            }
            if (showIt && message['npm-version']) {
                showIt = this.checkConditions(message['npm-version'], npmVersion);
            }
            if (showIt && message.os) {
                showIt = operatingSystem === message.os;
            }
            if (showIt && message.repo) {
                // If multi-repo
                if (Array.isArray(activeRepo)) {
                    showIt = activeRepo.includes(message.repo);
                }
                else {
                    showIt = activeRepo === message.repo;
                }
            }
            if (showIt && message.uuid) {
                if (Array.isArray(message.uuid)) {
                    showIt = uuid && message.uuid.find(msgUuid => uuid === msgUuid);
                }
                else {
                    showIt = !!(uuid && uuid === message.uuid);
                }
            }
            if (showIt && message['number-of-objects']) {
                const res = await this.getObjectListAsync({ include_docs: true });
                const noObjects = res.rows.length;
                showIt = eval(`${noObjects} ${message['number-of-objects']}`);
            }
            if (showIt && message['objects-db-type']) {
                const objectsDbType = await this.getObjectsDbType();
                if (!message['objects-db-type'].includes(objectsDbType)) {
                    showIt = false;
                }
            }
            if (showIt) {
                this.log.info(`register notification ${message.class}`);
                await this.registerNotification('admin', `${message.class}News`, `${message.title.en}\n${message.content.en}`);
            }
        }
    }
    /**
     * Check if adapter is active
     *
     * @param adapterName name of the adapter
     * @param instances list of instances
     */
    checkActive(adapterName, instances) {
        return !!Object.keys(instances)
            .filter(id => id.startsWith(`adapter.system.${adapterName}.`))
            .find(id => instances.rows.find(row => id === row.id)?.value?.common.enabled);
    }
    /**
     * Check if conditions met
     *
     * @param condition condition to check
     * @param installedVersion installed version
     */
    checkConditions(condition, installedVersion) {
        if (condition.startsWith('equals')) {
            const vers = condition.substring(7, condition.length - 1).trim();
            return installedVersion === vers;
        }
        if (condition.startsWith('bigger') || condition.startsWith('greater')) {
            const vers = condition.substring(7, condition.length - 1).trim();
            try {
                return semver.gt(vers, installedVersion);
            }
            catch {
                return false;
            }
        }
        else if (condition.startsWith('smaller')) {
            const vers = condition.substring(8, condition.length - 1).trim();
            try {
                return semver.lt(installedVersion, vers);
            }
            catch {
                return false;
            }
        }
        else if (condition.startsWith('between')) {
            const vers1 = condition.substring(8, condition.indexOf(',')).trim();
            const vers2 = condition.substring(condition.indexOf(',') + 1, condition.length - 1).trim();
            try {
                return semver.gte(installedVersion, vers1) && semver.lte(installedVersion, vers2);
            }
            catch {
                return false;
            }
        }
        else {
            return true;
        }
    }
    /**
     * Get the objects db type
     */
    async getObjectsDbType() {
        const hostAlive = await this.getForeignStateAsync(`system.host.${this.host}.alive`);
        if (!hostAlive?.val) {
            return 'jsonl';
        }
        const diagData = await this.sendToHostAsync(this.host, 'getDiagData', 'normal');
        // @ts-expect-error messages are special and cannot be typed easily
        return diagData.objectsType;
    }
    /**
     * Get current npm version from controller
     */
    async getNpmVersion() {
        const hostAlive = await this.getForeignStateAsync(`system.host.${this.host}.alive`);
        if (!hostAlive?.val) {
            throw new Error('Host is offline');
        }
        const hostInfo = await this.sendToHostAsync(this.host, 'getHostInfo', {});
        // @ts-expect-error messages are special and cannot be typed easily
        return hostInfo.NPM;
    }
    async checkNodeJsVersion() {
        // allow only one admin instance to check the versions for every host
        if (this.instance !== 0) {
            const objs = await this.getObjectViewAsync('system', 'instance', {
                startkey: 'system.adapter.admin.',
                endkey: 'system.adapter.admin.\u9999',
            });
            let min = null;
            // find the lowest active instance on the same host
            for (const row of objs.rows) {
                const obj = row.value;
                if (obj?.common.enabled && obj.common.host === this.host) {
                    const instance = parseInt(row.id.split('.').pop());
                    if (min === null || min < instance) {
                        min = instance;
                    }
                }
            }
            if (this.instance !== min) {
                return;
            }
        }
        const response = await (0, axios_1.default)('https://nodejs.org/download/release/index.json');
        const result = {
            nodeNewest: '',
            nodeNewestNext: '',
            npmNewest: '',
            npmNewestNext: '',
            npmCurrent: '',
            nodeCurrent: process.version,
        };
        try {
            result.npmCurrent = await this.getNpmVersion();
        }
        catch (error) {
            this.log.warn(`Cannot get current npm version: ${error}`);
        }
        // https://nodejs.org/download/release/index.json
        // detect a new version of the same major version and new major version (that is allowed by ioBroker)
        try {
            const recommendedVersions = await this.getRecommendedVersions();
            // find newest suggested version
            const nodeNewestNext = response.data.find(item => item.version.startsWith(`v${recommendedVersions.node}.`));
            const nodeCurrentMajor = process.version.split('.')[0];
            const nodeNewest = response.data.find(item => item.version.startsWith(`${nodeCurrentMajor}.`));
            if (nodeNewestNext) {
                result.nodeNewestNext = nodeNewestNext.version;
            }
            if (nodeNewest) {
                result.nodeNewest = nodeNewest.version;
            }
            // find newest suggested version
            const npmNewestNext = nodeNewestNext || response.data.find(item => item.npm.startsWith(`${recommendedVersions.npm}.`));
            const npmNewest = response.data.find(item => item.version === process.version);
            if (npmNewestNext) {
                result.npmNewestNext = npmNewestNext.npm;
            }
            if (npmNewest) {
                result.npmNewest = npmNewest.npm;
            }
            const prefix = `system.host.${this.host}.versions`;
            await this.setForeignObjectNotExistsAsync(prefix, {
                type: 'channel',
                common: {
                    name: {
                        en: 'Node.js/Npm versions',
                        de: 'Node.js/Npm Versionen',
                        ru: 'Node.js/Npm версии',
                        pt: 'Versões Node.js/Npm',
                        nl: 'Node.js/Npm versions',
                        fr: 'Node.js/Npm versions',
                        it: 'Node.js/Npm versioni',
                        es: 'Node.js/Npm versiones',
                        pl: 'Wersja node.js/Npm',
                        uk: 'Версії Node.js/Npm',
                        'zh-cn': '页: 1',
                    },
                },
                native: {},
            });
            const states = [
                {
                    _id: 'nodeCurrent',
                    type: 'state',
                    common: {
                        role: 'state',
                        name: {
                            en: 'Current node.js version',
                            de: 'Aktuelle node.js Version',
                            ru: 'Текущая версия node.js',
                            pt: 'Versão atual do node.js',
                            nl: 'Current Node',
                            fr: 'Version actuelle node.js',
                            it: 'Versione attuale node.js',
                            es: 'Versión actual node.js',
                            pl: 'Aktualna wersja.js',
                            uk: 'Поточна версія вузла',
                            'zh-cn': '目前没有。',
                        },
                        type: 'string',
                        read: true,
                        write: false,
                        def: '',
                    },
                    native: {},
                },
                {
                    _id: 'nodeNewest',
                    type: 'state',
                    common: {
                        role: 'state',
                        name: {
                            en: 'Newest node.js version',
                            de: 'Neueste node.js Version',
                            ru: 'Новейшая версия node.js',
                            pt: 'Mais recente versão node.js',
                            nl: 'Nieuwste node',
                            fr: 'Nouvelle version node.js',
                            it: 'Nuova versione node.js',
                            es: 'Versión más reciente node.js',
                            pl: 'Najnowsza wersja węzła.js',
                            uk: 'Остання версія вузла',
                            'zh-cn': '最新版本',
                        },
                        type: 'string',
                        read: true,
                        write: false,
                        def: '',
                    },
                    native: {},
                },
                {
                    _id: 'nodeNewestNext',
                    type: 'state',
                    common: {
                        role: 'state',
                        name: {
                            en: 'Newest next major node.js version',
                            de: 'Neueste nächste große node.js Version',
                            ru: 'Новейшая следующая версия node.js',
                            pt: 'Mais nova versão principal node.js',
                            nl: 'Nieuwste volgende grote node',
                            fr: 'Nouvelle prochaine version node.js',
                            it: 'Nuova versione principale node.js',
                            es: 'Versión más reciente node.js',
                            pl: 'Najnowsza wersja węzła.js',
                            uk: 'Новейшая наступна версія вузла',
                            'zh-cn': '今后最新的重要内容。',
                        },
                        type: 'string',
                        read: true,
                        write: false,
                        def: '',
                    },
                    native: {},
                },
                {
                    _id: 'npmCurrent',
                    type: 'state',
                    common: {
                        role: 'state',
                        name: {
                            en: 'Current npm version',
                            de: 'Aktuelle Version',
                            ru: 'Текущая версия npm',
                            pt: 'Versão actual npm',
                            nl: 'Current Npm versie',
                            fr: 'Version actuelle npm',
                            it: 'Versione npm attuale',
                            es: 'Versión actual npm',
                            pl: 'Aktualna wersja',
                            uk: 'Поточна версія npm',
                            'zh-cn': '目前的印本',
                        },
                        type: 'string',
                        read: true,
                        write: false,
                        def: '',
                    },
                    native: {},
                },
                {
                    _id: 'npmNewest',
                    type: 'state',
                    common: {
                        role: 'state',
                        name: {
                            en: 'Newest npm version',
                            de: 'Neueste Version',
                            ru: 'Новейшая версия npm',
                            pt: 'Versão mais recente npm',
                            nl: 'Newest Npm versie',
                            fr: 'Nouvelle version npm',
                            it: 'Nuova versione npm',
                            es: 'Versión más reciente npm',
                            pl: 'Wersja nowa',
                            uk: 'Остання версія npm',
                            'zh-cn': '最新版本',
                        },
                        type: 'string',
                        read: true,
                        write: false,
                        def: '',
                    },
                    native: {},
                },
                {
                    _id: 'npmNewestNext',
                    type: 'state',
                    common: {
                        role: 'state',
                        name: {
                            en: 'Newest next major NPM version',
                            de: 'Neueste nächste große NPM-Version',
                            ru: 'Новейшая следующая крупная версия NPM',
                            pt: 'Mais nova versão principal do NPM',
                            nl: 'NPM',
                            fr: 'La version la plus récente',
                            it: 'Nuova versione NPM',
                            es: 'Versión NPM más reciente',
                            pl: 'Nowa wersja NPM',
                            uk: 'Новейша наступна велика версія NPM',
                            'zh-cn': '下一次主要国家预防计划',
                        },
                        type: 'string',
                        read: true,
                        write: false,
                        def: '',
                    },
                    native: {},
                },
            ];
            for (const state of states) {
                await this.setForeignObjectNotExistsAsync(`${prefix}.${state._id}`, state);
            }
            for (const [key, value] of Object.entries(result)) {
                await this.setForeignStateAsync(`${prefix}.${key}`, value.replace(/^v/, ''), true);
            }
        }
        catch {
            this.log.warn('Cannot check node.js/npm version');
        }
    }
    getData(callback) {
        this.log.info('requesting all objects');
        this.getObjectList({ include_docs: true }, (_err, res) => {
            this.log.info('received all objects');
            if (res) {
                objects = {};
                let tmpPath = '';
                for (const row of res.rows) {
                    objects[row.doc._id] = row.doc;
                    if (row.doc.type === 'instance' && row.doc.common?.tmpPath) {
                        if (tmpPath) {
                            this.log.warn('tmpPath has multiple definitions!!');
                        }
                        tmpPath = row.doc.common.tmpPath;
                    }
                }
                // Some adapters want access on specified tmp directory
                if (tmpPath) {
                    this.config.tmpPath = tmpPath;
                    this.config.tmpPathAllow = true;
                }
                void this.createUpdateInfo().then(() => this.writeUpdateInfo());
            }
            if (callback) {
                callback(this);
            }
        });
    }
    async checkRevokedVersions(repository) {
        try {
            const adapters = Object.keys(repository);
            const instances = await this.getObjectViewAsync('system', 'instance', {
                startkey: 'system.adapter.',
                endkey: 'system.adapter.\u9999',
            });
            for (const _adapter of adapters) {
                if (repository[_adapter].blockedVersions) {
                    // read a current version
                    if (Array.isArray(repository[_adapter].blockedVersions)) {
                        const instance = instances.rows.find(item => item.value?.common.name === _adapter && item.value.common.enabled);
                        if (instance?.value?.common?.version) {
                            for (let i = 0; i < repository[_adapter].blockedVersions.length; i++) {
                                try {
                                    if (semver.satisfies(instance.value.common.version, repository[_adapter].blockedVersions[i])) {
                                        // stop all instances
                                        for (let k = 0; k < instances.rows.length; k++) {
                                            const obj = instances.rows[k].value;
                                            if (obj?.common.enabled && obj.common.name === _adapter) {
                                                obj.common.enabled = false;
                                                await this.setForeignObjectAsync(obj._id, obj);
                                                this.log.warn(`Instance ${obj._id.replace('system.adapter.', '')} was disabled because blocked. Please update ${_adapter} to newer or available version`);
                                                const hostAlive = await this.getForeignStateAsync(`system.host.${obj.common.host}.alive`);
                                                if (hostAlive?.val) {
                                                    this.sendToHost(obj.common.host, 'addNotification', {
                                                        scope: 'system',
                                                        category: 'accessErrors', // change to 'blocked' when js-controller 4.1. released
                                                        instance: obj._id,
                                                        message: `Instance version was blocked. Please check for updates and update before restarting the instance`,
                                                    });
                                                }
                                                else {
                                                    this.log.warn(`Cannot add notification to ${obj.common.host} as it is offline`);
                                                }
                                            }
                                        }
                                    }
                                }
                                catch {
                                    this.log.error(`Cannot check revoked versions: ${repository[_adapter].blockedVersions[i]}`);
                                    // ignore
                                }
                            }
                        }
                    }
                    else {
                        this.log.error(`Invalid blockedVersions for ${_adapter}: ${JSON.stringify(repository[_adapter].blockedVersions)}. Expected array like ["<= 3.17.4"] or also ["~3.14.0", "~3.15.0", "~3.16.0"]`);
                    }
                }
            }
        }
        catch (e) {
            this.log.error(`Cannot check revoked versions: ${e}`);
        }
    }
    // update icons by all known default objects. Remove this function after 2 years (BF: 2021.04.20)
    updateIcons() {
        if ((0, node_fs_1.existsSync)(`${adapter_core_1.controllerDir}/io-package.json`)) {
            const ioPackage = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(adapter_core_1.controllerDir, 'io-package.json'), {
                encoding: 'utf-8',
            }));
            ioPackage.objects.forEach(async (obj) => {
                if (obj.common?.icon && obj.common.icon.length > 50) {
                    const cObj = await this.getForeignObjectAsync(obj._id);
                    if (cObj?.common && (!cObj.common.icon || cObj.common.icon.length < 50)) {
                        this.log.debug(`Update icon for ${cObj._id}`);
                        cObj.common.icon = obj.common.icon;
                        await this.setForeignObjectAsync(cObj._id, cObj);
                    }
                }
            });
        }
    }
    restartRepoUpdate() {
        // start the next cycle
        if (this.config.autoUpdate) {
            if (this.timerRepo) {
                clearTimeout(this.timerRepo);
                this.timerRepo = null;
            }
            this.log.debug(`Next repo update on ${new Date(Date.now() + this.config.autoUpdate * ONE_HOUR_MS + 1).toLocaleString()}`);
            this.timerRepo = setTimeout(async () => {
                this.timerRepo = null;
                await this.updateRegister();
            }, this.config.autoUpdate * ONE_HOUR_MS + 1);
        }
    }
    async getRecommendedVersions() {
        // Check if the host running
        const hostAlive = await this.getForeignStateAsync(`system.host.${this.host}.alive`);
        if (!hostAlive?.val) {
            return {
                node: CURRENT_MAX_MAJOR_NODEJS,
                npm: CURRENT_MAX_MAJOR_NPM,
            };
        }
        const repository = (await this.sendToHostAsync(this.host, 'getRepository', {}));
        const repoInfo = repository?._repoInfo;
        if (repoInfo?.recommendedVersions) {
            return {
                node: repoInfo.recommendedVersions?.nodeJsRecommended,
                npm: repoInfo.recommendedVersions?.npmRecommended,
            };
        }
        return {
            node: CURRENT_MAX_MAJOR_NODEJS,
            npm: CURRENT_MAX_MAJOR_NPM,
        };
    }
    /**
     * Read repository information from active repository
     */
    async updateRegister() {
        if (lastRepoUpdate && Date.now() - lastRepoUpdate < 3600000) {
            this.log.error('Automatic repository update is not allowed more than once an hour');
            this.restartRepoUpdate();
            return;
        }
        lastRepoUpdate = Date.now();
        try {
            const systemConfig = await this.getForeignObjectAsync('system.config');
            if (systemConfig?.common) {
                try {
                    const repos = await this.getForeignObjectAsync('system.repositories');
                    if (!repos || repos.ts === undefined) {
                        // start the next cycle
                        this.restartRepoUpdate();
                        return;
                    }
                    // Check if repositories exist
                    let exists = false;
                    const active = systemConfig.common.activeRepo;
                    // if repo is valid and actual
                    if (Array.isArray(active)) {
                        if (Date.now() < repos.ts + this.config.autoUpdate * ONE_HOUR_MS &&
                            !active.find(repo => !repos?.native?.repositories?.[repo]?.json)) {
                            exists = true;
                        }
                    }
                    else if (repos?.native?.repositories?.[active]?.json &&
                        Date.now() < repos.ts + this.config.autoUpdate * ONE_HOUR_MS) {
                        exists = true;
                    }
                    if (!exists) {
                        this.log.info('Request actual repository...');
                        // first check if the host is running
                        const aliveState = await this.getForeignStateAsync(`system.host.${this.host}.alive`);
                        if (!aliveState?.val) {
                            this.log.error('Host is not alive');
                            // start the next cycle
                            this.restartRepoUpdate();
                            return;
                        }
                        // request repo from host
                        this.sendToHost(this.host, 'getRepository', {
                            repo: active,
                            update: true,
                        }, _repository => {
                            if ((typeof _repository === 'string' || _repository instanceof Error) &&
                                _repository.toString().includes(ERROR_PERMISSION)) {
                                this.log.error('May not read "getRepository"');
                            }
                            else {
                                this.log.info('Repository received successfully.');
                                socket?.repoUpdated();
                                this.checkRevokedVersions(_repository).catch(e => this.log.error(`Cannot check revoked versions: ${e}`));
                            }
                            // start the next cycle
                            this.restartRepoUpdate();
                        });
                    }
                    else if (this.config.autoUpdate) {
                        let interval = repos.ts + this.config.autoUpdate * ONE_HOUR_MS - Date.now() + 1;
                        if (interval > 0x7fffffff) {
                            interval = 0x7fffffff;
                        }
                        this.log.debug(`Next repo update on ${new Date(Date.now() + interval).toLocaleString()}`);
                        if (this.timerRepo) {
                            clearTimeout(this.timerRepo);
                        }
                        this.timerRepo = setTimeout(async () => {
                            this.timerRepo = null;
                            await this.updateRegister();
                        }, interval);
                    }
                }
                catch (err) {
                    this.log.error(`May not read "system.repositories": ${err}`);
                }
            }
        }
        catch {
            this.log.error('May not read "system.config"');
        }
    }
    // this function re-check if the common objects like '0_userdata.0' exist
    async checkCommonObjects() {
        // try to find js-controller directory
        let objects;
        try {
            const dir = require.resolve('iobroker.js-controller/io-package.json').replace(/\\/g, '/');
            // dir is something like ./node_modules/iobroker.js-controller/build/cjs/main.js
            if ((0, node_fs_1.existsSync)(dir)) {
                const data = JSON.parse((0, node_fs_1.readFileSync)(dir).toString());
                if (data.objects) {
                    objects = data.objects;
                }
            }
        }
        catch {
            // ignore
        }
        if (objects) {
            for (let i = 0; i < objects.length; i++) {
                const obj = await this.getForeignObjectAsync(objects[i]._id);
                if (!obj) {
                    await this.setForeignObjectAsync(objects[i]._id, objects[i]);
                }
            }
        }
        else {
            // check the meta-object 0_userdata.0 and create it if required
            let userData = await this.getForeignObjectAsync('0_userdata.0');
            if (!userData) {
                userData = {
                    _id: '0_userdata.0',
                    type: 'meta',
                    common: {
                        icon: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDI0IDI0IiB3aWR0aD0iMjRweCI+DQogICAgPGcgZmlsbD0iY3VycmVudENvbG9yIj4NCiAgICAgICAgPHBhdGggZD0iTTE5LDV2MTRINVY1SDE5IE0xOSwzSDVDMy45LDMsMywzLjksMyw1djE0YzAsMS4xLDAuOSwyLDIsMmgxNGMxLjEsMCwyLTAuOSwyLTJWNUMyMSwzLjksMjAuMSwzLDE5LDNMMTksM3oiLz4NCiAgICAgICAgPHBhdGggZD0iTTE0LDE3SDd2LTJoN1YxN3ogTTE3LDEzSDd2LTJoMTBWMTN6IE0xNyw5SDdWN2gxMFY5eiIvPg0KICAgIDwvZz4NCjwvc3ZnPg==',
                        name: {
                            en: 'User objects and files root folder',
                            de: 'Stammordner für Benutzerobjekte und Dateien',
                            ru: 'Корневая папка пользовательских объектов и файлов',
                            pt: 'Pasta raiz de objetos e arquivos do usuário',
                            nl: 'Hoofdmap van objecten en bestanden van gebruikers',
                            fr: 'Objets utilisateur et dossier racine des fichiers',
                            it: "Cartella principale di oggetti e file dell'utente",
                            es: 'Carpeta raíz de objetos y archivos de usuario',
                            pl: 'Folder główny obiektów i plików użytkownika',
                            uk: "Коренева папка об'єктів користувача та файлів",
                            'zh-cn': '用户对象和文件根文件夹',
                        },
                        desc: {
                            en: 'Here you can upload your files or create your private objects and states',
                            de: 'Hier können eigene Dateien hochgeladen oder private Objekte und Zustände erstellt werden',
                            ru: 'Здесь вы можете загрузить свои файлы или создать свои личные объекты и состояния',
                            pt: 'Aqui você pode enviar seus arquivos ou criar seus objetos e estados particulares',
                            nl: 'Hier kunt u uw bestanden uploaden of uw privé-objecten en statussen maken',
                            fr: 'Ici, vous pouvez télécharger vos fichiers ou créer vos objets et états privés',
                            it: 'Qui puoi caricare i tuoi file o creare oggetti e stati privati',
                            es: 'Aquí puede cargar sus archivos o crear sus objetos y estados privados',
                            pl: 'Tutaj możesz przesyłać pliki lub tworzyć prywatne obiekty i stany',
                            uk: "Тут ви можете завантажити свої файли або створити свої приватні об'єкти та стани",
                            'zh-cn': '在这里您可以上传文件或创建私有对象和状态',
                        },
                        type: 'meta.user',
                        dontDelete: true,
                    },
                    acl: {
                        owner: 'system.user.admin',
                        ownerGroup: 'system.group.administrator',
                        object: 1604,
                    },
                };
                await this.setForeignObject(userData._id, userData);
            }
        }
    }
    /**
     * Initialize the adapter
     */
    init() {
        this.config.defaultUser = this.config.defaultUser || 'admin';
        if (!this.config.defaultUser.match(/^system\.user\./)) {
            this.config.defaultUser = `system.user.${this.config.defaultUser}`;
        }
        this.checkCommonObjects().catch((e) => this.log.warn(`Cannot check common objects: ${e?.message}`));
        this.getData(adapter => (webServer = new web_1.default(adapter.config, adapter, this.initSocket.bind(this), { systemLanguage })));
        if (this.config.accessApplyRights &&
            this.config.accessLimit &&
            !this.config.auth &&
            this.config.defaultUser !== 'system.user.admin') {
            this.applyRights().catch((e) => this.log.warn(`Cannot apply rights: ${e?.message}`));
        }
        // By default, update repository every 24 hours
        if (this.config.autoUpdate === undefined || this.config.autoUpdate === null) {
            this.config.autoUpdate = 24;
        }
        // interval in hours
        this.config.autoUpdate = Number(this.config.autoUpdate) || 0;
        if (this.config.autoUpdate && this.config.autoUpdate < 4) {
            this.config.autoUpdate = 4; // only every 4 hours - it is a minimal update interval
        }
        else if (this.config.autoUpdate > 590) {
            // 0x7FFFFFFF / ONE_HOUR_MS = 596
            this.config.autoUpdate = 590; // max interval is 2147483647 milliseconds
        }
        // check info.connected
        void this.getObjectAsync('info.connected').then(obj => {
            if (!obj) {
                const packageJson = JSON.parse((0, node_fs_1.readFileSync)(`${__dirname}/../io-package.json`).toString('utf8'));
                const obj = packageJson.instanceObjects.find((o) => o._id === 'info.connected');
                if (obj) {
                    return this.setObjectAsync(obj._id, obj);
                }
            }
        });
        if (this.config.autoUpdate) {
            void this.updateRegister().catch(e => this.log.error(`Cannot update repository: ${e}`));
        }
        void this.updateNews().catch(e => this.log.error(`Cannot update news: ${e}`));
        this.updateIcons();
        void this.validateUserData0().catch(e => this.log.error(`Cannot validate 0_userdata: ${e}`));
        void this.checkWellKnownPasswords().catch(e => this.log.error(`Cannot check well known passwords: ${e}`));
        this.subscribeForeignObjects(`system.adapter.${this.namespace}.plugins.sentry.enabled`);
    }
    /**
     * Create 0_userdata if it does not exist
     */
    async validateUserData0() {
        let obj;
        try {
            obj = await this.getForeignObjectAsync('0_userdata.0');
        }
        catch {
            // ignore
        }
        if (!obj) {
            try {
                const ioContent = (0, node_fs_1.readFileSync)(`${adapter_core_1.controllerDir}/io-package.json`).toString('utf8');
                const io = JSON.parse(ioContent);
                if (io.objects) {
                    const userData = io.objects.find((obj) => obj._id === '0_userdata.0');
                    if (userData) {
                        await this.setForeignObjectAsync(userData._id, userData);
                        this.log.info('Object 0_userdata.0 was re-created');
                    }
                }
            }
            catch (e) {
                this.log.error(`Cannot read ${adapter_core_1.controllerDir}/io-package.json: ${e}`);
            }
        }
    }
    async checkWellKnownPasswords() {
        if (process.platform !== 'linux') {
            return;
        }
        const found = await (0, checkLinuxPass_1.checkWellKnownPasswords)();
        if (found) {
            this.changedPasswords = this.changedPasswords.filter(item => item.login !== found.login && item.password !== found.password);
            this.changedPasswords.push(found);
            await this.registerNotification('admin', 'wellKnownPassword', i18n_1.default.translate('User: %s', found.login), {
                contextData: {
                    admin: {
                        notification: {
                            login: found.login,
                            password: found.password,
                            offlineMessage: i18n_1.default.getTranslatedObject('Offline message', found.login),
                        },
                    },
                },
            });
        }
    }
}
if (require.main !== module) {
    // Export the constructor in compact mode
    module.exports = (options) => new Admin(options);
}
else {
    // otherwise start the instance directly
    (() => new Admin())();
}
//# sourceMappingURL=main.js.map