/*****************************************************************************\
|                                               ( )_  _                       |
|    _ _    _ __   _ _    __    ___ ___     _ _ | ,_)(_)  ___  _   _    _     |
|   ( '_`\ ( '__)/'_` ) /'_ `\/' _ ` _ `\ /'_` )| |  | |/',__)/ \ /`\ /'_`\   |
|   | (_) )| |  ( (_| |( (_) || ( ) ( ) |( (_| || |_ | |\__, \| |*| |( (_) )  |
|   | ,__/'(_)  `\__,_)`\__  |(_) (_) (_)`\__,_)`\__)(_)(____/(_) (_)`\___/'  |
|   | |                ( )_) |                                                |
|   (_)                 \___/'                                                |
|                                                                             |
| General Bots Copyright (c) Pragmatismo.io. All rights reserved.             |
| Licensed under the AGPL-3.0.                                                |
|                                                                             |
| According to our dual licensing model, this program can be used either      |
| under the terms of the GNU Affero General Public License, version 3,        |
| or under a proprietary license.                                             |
|                                                                             |
| The texts of the GNU Affero General Public License with an additional       |
| permission and of our proprietary license can be found at and               |
| in the LICENSE file you have received along with this program.              |
|                                                                             |
| This program is distributed in the hope that it will be useful,             |
| but WITHOUT ANY WARRANTY, without even the implied warranty of              |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the                |
| GNU Affero General Public License for more details.                         |
|                                                                             |
| "General Bots" is a registered trademark of Pragmatismo.io.                 |
| The licensing of the program under the AGPLv3 does not imply a              |
| trademark license. Therefore any rights, title and interest in              |
| our trademarks remain entirely with us.                                     |
|                                                                             |
\*****************************************************************************/
/**
 * @fileoverview General Bots server core.
 */
'use strict';
Object.defineProperty(exports, "__esModule", { value: true });
exports.GBCoreService = void 0;
const botlib_1 = require("botlib");
const fs = require("fs");
const sequelize_typescript_1 = require("sequelize-typescript");
const sequelize_1 = require("sequelize");
const app_1 = require("../../../src/app");
const index_1 = require("../../admin.gbapp/index");
const GBAdminService_1 = require("../../admin.gbapp/services/GBAdminService");
const analytics_gblib_1 = require("../../analytics.gblib");
const StartDialog_1 = require("../../azuredeployer.gbapp/dialogs/StartDialog");
const core_gbapp_1 = require("../../core.gbapp");
const customer_satisfaction_gbapp_1 = require("../../customer-satisfaction.gbapp");
const kb_gbapp_1 = require("../../kb.gbapp");
const security_gbapp_1 = require("../../security.gbapp");
const index_2 = require("../../whatsapp.gblib/index");
const GBModel_1 = require("../models/GBModel");
const GBConfigService_1 = require("./GBConfigService");
const azuredeployer_gbapp_1 = require("../../azuredeployer.gbapp");
const sharepoint_gblib_1 = require("../../sharepoint.gblib");
const pragmatismo_io_framework_1 = require("pragmatismo-io-framework");
const GBVMService_1 = require("../../basic.gblib/services/GBVMService");
const basic_gblib_1 = require("../../basic.gblib");
const google_chat_gblib_1 = require("../../google-chat.gblib");
const opn = require('opn');
const cron = require('node-cron');
/**
 * GBCoreService contains main logic for handling storage services related
 * to instance handling. When the server starts a instance is needed and
 * if no instance is found a boot instance is created. After that high-level
 * instance management methods can be created.
 * Core scheduling, base network services are also handled in this service.
 */
class GBCoreService {
    /**
     *
     */
    constructor() {
        this.adminService = new GBAdminService_1.GBAdminService(this);
    }
    async ensureInstances(instances, bootInstance, core) { }
    /**
     * Gets database config and connect to storage. Currently two databases
     * are available: SQL Server and SQLite.
     */
    async initStorage() {
        this.dialect = GBConfigService_1.GBConfigService.get('STORAGE_DIALECT');
        let host;
        let database;
        let username;
        let password;
        let storage;
        if (this.dialect === 'mssql') {
            host = GBConfigService_1.GBConfigService.get('STORAGE_SERVER');
            database = GBConfigService_1.GBConfigService.get('STORAGE_NAME');
            username = GBConfigService_1.GBConfigService.get('STORAGE_USERNAME');
            password = GBConfigService_1.GBConfigService.get('STORAGE_PASSWORD');
        }
        else if (this.dialect === 'sqlite') {
            storage = GBConfigService_1.GBConfigService.get('STORAGE_STORAGE');
        }
        else {
            throw new Error(`Unknown dialect: ${this.dialect}.`);
        }
        const logging = GBConfigService_1.GBConfigService.get('STORAGE_LOGGING') === 'true'
            ? (str) => {
                botlib_1.GBLog.info(str);
            }
            : false;
        const encrypt = GBConfigService_1.GBConfigService.get('STORAGE_ENCRYPT') === 'true';
        const acquire = parseInt(GBConfigService_1.GBConfigService.get('STORAGE_ACQUIRE_TIMEOUT'));
        const sequelizeOptions = {
            define: {
                freezeTableName: true,
                timestamps: false
            },
            host: host,
            logging: logging,
            dialect: this.dialect,
            storage: storage,
            dialectOptions: {
                options: {
                    trustServerCertificate: true,
                    encrypt: encrypt
                }
            },
            pool: {
                max: 32,
                min: 8,
                idle: 40000,
                evict: 40000,
                acquire: acquire
            }
        };
        this.sequelize = new sequelize_typescript_1.Sequelize(database, username, password, sequelizeOptions);
        // Specifies custom setup for MSFT...
        if (this.dialect === 'mssql') {
            this.queryGenerator = this.sequelize.getQueryInterface().QueryGenerator;
            // tslint:disable:no-unsafe-any
            this.createTableQuery = this.queryGenerator.createTableQuery;
            this.queryGenerator.createTableQuery = (tableName, attributes, options) => this.createTableQueryOverride(tableName, attributes, options);
            this.changeColumnQuery = this.queryGenerator.changeColumnQuery;
            this.queryGenerator.changeColumnQuery = (tableName, attributes) => this.changeColumnQueryOverride(tableName, attributes);
            // tslint:enable:no-unsafe-any
        }
    }
    /**
     * Checks wheather storage is acessible or not and opens firewall
     * in case of any connection block.
     */
    async checkStorage(installationDeployer) {
        try {
            await this.sequelize.authenticate();
        }
        catch (error) {
            botlib_1.GBLog.info('Opening storage firewall on infrastructure...');
            // tslint:disable:no-unsafe-any
            if (error.parent.code === 'ELOGIN') {
                await this.openStorageFrontier(installationDeployer);
            }
            else {
                throw error;
            }
            // tslint:ensable:no-unsafe-any
        }
    }
    /**
   * Syncronizes structure between model and tables in storage.
   */
    async syncDatabaseStructure() {
        if (GBConfigService_1.GBConfigService.get('STORAGE_SYNC') === 'true') {
            const alter = GBConfigService_1.GBConfigService.get('STORAGE_SYNC_ALTER') === 'true';
            botlib_1.GBLog.info('Syncing database...');
            return await this.sequelize.sync({
                alter: alter,
                force: false // Keep it false this due to data loss danger.
            });
        }
        else {
            const msg = `Database synchronization is disabled.`;
            botlib_1.GBLog.info(msg);
        }
    }
    /**
     * Loads all items to start several listeners.
     */
    async loadInstances() {
        if (process.env.LOAD_ONLY !== undefined) {
            const bots = process.env.LOAD_ONLY.split(`;`);
            const and = [];
            await pragmatismo_io_framework_1.CollectionUtil.asyncForEach(bots, async (e) => {
                and.push({ botId: e });
            });
            const options = {
                where: {
                    [sequelize_1.Op.or]: and
                }
            };
            return await GBModel_1.GuaribasInstance.findAll(options);
        }
        else {
            const options = { where: { state: 'active' } };
            return await GBModel_1.GuaribasInstance.findAll(options);
        }
    }
    /**
     * Loads just one Bot instance by its internal Id.
     */
    async loadInstanceById(instanceId) {
        const options = { where: { instanceId: instanceId, state: 'active' } };
        return await GBModel_1.GuaribasInstance.findOne(options);
    }
    /**
     * Loads just one Bot instance.
     */
    async loadInstanceByActivationCode(code) {
        let options = { where: { activationCode: code, state: 'active' } };
        return await GBModel_1.GuaribasInstance.findOne(options);
    }
    /**
     * Loads just one Bot instance.
     */
    async loadInstanceByBotId(botId) {
        const options = { where: {} };
        options.where = { botId: botId, state: 'active' };
        return await GBModel_1.GuaribasInstance.findOne(options);
    }
    /**
     * Writes .env required to start the full server. Used during
     * first startup, when user is asked some questions to create the
     * full base environment.
     */
    async writeEnv(instance) {
        const env = `
ADDITIONAL_DEPLOY_PATH=
ADMIN_PASS=${instance.adminPass}
BOT_ID=${instance.botId}
CLOUD_SUBSCRIPTIONID=${instance.cloudSubscriptionId}
CLOUD_LOCATION=${instance.cloudLocation}
CLOUD_GROUP=${instance.botId}
CLOUD_USERNAME=${instance.cloudUsername}
CLOUD_PASSWORD=${instance.cloudPassword}
MARKETPLACE_ID=${instance.marketplaceId}
MARKETPLACE_SECRET=${instance.marketplacePassword}
STORAGE_DIALECT=${instance.storageDialect}
STORAGE_SERVER=${instance.storageServer}
STORAGE_NAME=${instance.storageName}
STORAGE_USERNAME=${instance.storageUsername}
STORAGE_PASSWORD=${instance.storagePassword}
STORAGE_SYNC=true
ENDPOINT_UPDATE=true
`;
        fs.writeFileSync('.env', env);
    }
    /**
     * Certifies that network servers will reach back the development machine
     * when calling back from web services. This ensures that reverse proxy is
     * established.
     */
    async ensureProxy(port) {
        try {
            if (fs.existsSync('node_modules/ngrok/bin/ngrok.exe') || fs.existsSync('node_modules/ngrok/bin/ngrok')) {
                const ngrok = require('ngrok');
                return await ngrok.connect({ port: port }, 10);
            }
            else {
                botlib_1.GBLog.warn('ngrok executable not found (only tested on Windows). Check installation or node_modules folder.');
                return 'https://localhost';
            }
        }
        catch (error) {
            // There are false positive from ngrok regarding to no memory, but it's just
            // lack of connection.
            throw new Error(`Error connecting to remote ngrok server, please check network connection. ${error.msg}`);
        }
    }
    /**
     * Setup generic web hooks so .gbapps can expose application logic
     * and get called on demand.
     */
    installWebHook(isGet, url, callback) {
        if (isGet) {
            app_1.GBServer.globals.server.get(url, (req, res) => {
                callback(req, res);
            });
        }
        else {
            app_1.GBServer.globals.server.post(url, (req, res) => {
                callback(req, res);
            });
        }
    }
    /**
     * Defines the entry point dialog to be called whenever a user
     * starts talking to the bot.
     */
    setEntryPointDialog(dialogName) {
        app_1.GBServer.globals.entryPointDialog = dialogName;
    }
    /**
     * Replaces the default web application root path used to start the GB
     * with a custom home page.
     */
    setWWWRoot(localPath) {
        app_1.GBServer.globals.wwwroot = localPath;
    }
    /**
     * Removes a bot instance from storage.
     */
    async deleteInstance(botId) {
        const options = { where: {} };
        options.where = { botId: botId };
        await GBModel_1.GuaribasInstance.destroy(options);
    }
    /**
     * Saves a bot instance object to the storage handling
     * multi-column JSON based store 'params' field.
     */
    async saveInstance(fullInstance) {
        const options = { where: {} };
        options.where = { botId: fullInstance.botId };
        let instance = await GBModel_1.GuaribasInstance.findOne(options);
        // tslint:disable-next-line:prefer-object-spread
        if (instance) {
            instance = Object.assign(instance, fullInstance);
        }
        else {
            instance = Object.assign(new GBModel_1.GuaribasInstance(), fullInstance);
        }
        try {
            instance.params = JSON.stringify(JSON.parse(instance.params));
        }
        catch (err) {
            instance.params = JSON.stringify(instance.params);
        }
        return await instance.save();
    }
    /**
     * Loads all bot instances from object storage, if it's formatted.
     */
    async loadAllInstances(core, installationDeployer, proxyAddress) {
        botlib_1.GBLog.info(`Loading instances from storage...`);
        let instances;
        try {
            instances = await core.loadInstances();
            if (process.env.ENDPOINT_UPDATE === 'true') {
                await pragmatismo_io_framework_1.CollectionUtil.asyncForEach(instances, async (instance) => {
                    botlib_1.GBLog.info(`Updating bot endpoint for ${instance.botId}...`);
                    try {
                        await installationDeployer.updateBotProxy(instance.botId, GBConfigService_1.GBConfigService.get('CLOUD_GROUP'), `${proxyAddress}/api/messages/${instance.botId}`);
                    }
                    catch (error) {
                        if (error.code === 'ResourceNotFound') {
                            botlib_1.GBLog.warn(`Bot ${instance.botId} not found on resource group ${GBConfigService_1.GBConfigService.get('CLOUD_GROUP')}.`);
                        }
                        else {
                            throw new Error(`Error updating bot proxy, details: ${error}.`);
                        }
                    }
                });
            }
        }
        catch (error) {
            if (error.parent === undefined) {
                throw new Error(`Cannot connect to operating storage: ${error.message}.`);
            }
            else {
                // Check if storage is empty and needs formatting.
                const isInvalidObject = error.parent.number === 208 || error.parent.errno === 1; // MSSQL or SQLITE.
                if (isInvalidObject) {
                    if (GBConfigService_1.GBConfigService.get('STORAGE_SYNC') !== 'true') {
                        throw new Error(`Operating storage is out of sync or there is a storage connection error.
            Try setting STORAGE_SYNC to true in .env file. Error: ${error.message}.`);
                    }
                    else {
                        botlib_1.GBLog.info(`Storage is empty. After collecting storage structure from all .gbapps it will get synced.`);
                    }
                }
                else {
                    throw new Error(`Cannot connect to operating storage: ${error.message}.`);
                }
            }
        }
        return instances;
    }
    /**
     * Loads all system packages from 'packages' folder.
     */
    async loadSysPackages(core) {
        // NOTE: if there is any code before this line a semicolon
        // will be necessary before this line.
        // Loads all system packages.
        const sysPackages = [];
        await pragmatismo_io_framework_1.CollectionUtil.asyncForEach([
            index_1.GBAdminPackage,
            core_gbapp_1.GBCorePackage,
            security_gbapp_1.GBSecurityPackage,
            kb_gbapp_1.GBKBPackage,
            customer_satisfaction_gbapp_1.GBCustomerSatisfactionPackage,
            analytics_gblib_1.GBAnalyticsPackage,
            index_2.GBWhatsappPackage,
            azuredeployer_gbapp_1.GBAzureDeployerPackage,
            sharepoint_gblib_1.GBSharePointPackage,
            google_chat_gblib_1.GBGoogleChatPackage,
            basic_gblib_1.GBBasicPackage
        ], async (e) => {
            botlib_1.GBLog.info(`Loading sys package: ${e.name}...`);
            const p = Object.create(e.prototype);
            sysPackages.push(p);
            await p.loadPackage(core, core.sequelize);
        });
        return sysPackages;
    }
    /**
     * Verifies that an complex global password has been specified
     * before starting the server.
     */
    ensureAdminIsSecured() {
        const password = GBConfigService_1.GBConfigService.get('ADMIN_PASS');
        if (!GBAdminService_1.GBAdminService.StrongRegex.test(password)) {
            throw new Error('Please, define a really strong password in ADMIN_PASS environment variable before running the server.');
        }
    }
    /**
     * Creates the first bot instance (boot instance) used to "boot" the server.
     * At least one bot is required to perform conversational administrative tasks.
     * So a base main bot is always deployed and will act as root bot for
     * configuration tree with three levels: .env > root bot > all other bots.
     */
    async createBootInstance(core, installationDeployer, proxyAddress) {
        botlib_1.GBLog.info(`Deploying cognitive infrastructure (on the cloud / on premises)...`);
        try {
            const { instance, credentials, subscriptionId } = await StartDialog_1.StartDialog.createBaseInstance(installationDeployer);
            installationDeployer['core'] = this;
            const changedInstance = await installationDeployer.deployFarm(proxyAddress, instance, credentials, subscriptionId);
            await this.writeEnv(changedInstance);
            GBConfigService_1.GBConfigService.init();
            botlib_1.GBLog.info(`File .env written. Preparing storage and search for the first time...`);
            await this.openStorageFrontier(installationDeployer);
            await this.initStorage();
            return changedInstance;
        }
        catch (error) {
            botlib_1.GBLog.warn(`There is an error being thrown, so please cleanup any infrastructure objects
            created during this procedure and .env before running again.`);
            throw error;
        }
    }
    /**
     * Helper to get the web browser onpened in UI interfaces.
     */
    openBrowserInDevelopment() {
        if (process.env.NODE_ENV === 'development') {
            opn('http://localhost:4242');
        }
    }
    /**
     * SQL:
     *
     * // let sql: string = '' +
     * //   'IF OBJECT_ID(\'[UserGroup]\', \'U\') IS NULL' +
     * //   'CREATE TABLE [UserGroup] (' +
     * //   '  [id] INTEGER NOT NULL IDENTITY(1,1),' +
     * //   '  [userId] INTEGER NULL,' +
     * //   '  [groupId] INTEGER NULL,' +
     * //   '  [instanceId] INTEGER NULL,' +
     * //   '  PRIMARY KEY ([id1], [id2]),' +
     * //   '  FOREIGN KEY ([userId1], [userId2], [userId3]) REFERENCES [User] ([userId1], [userId2], [userId3]) ON DELETE NO ACTION,' +
     * //   '  FOREIGN KEY ([groupId1], [groupId2]) REFERENCES [Group] ([groupId1], [groupId1]) ON DELETE NO ACTION,' +
     * //   '  FOREIGN KEY ([instanceId]) REFERENCES [Instance] ([instanceId]) ON DELETE NO ACTION)'
     */
    createTableQueryOverride(tableName, attributes, options) {
        let sql = this.createTableQuery.apply(this.queryGenerator, [tableName, attributes, options]);
        const re1 = /CREATE\s+TABLE\s+\[([^\]]*)\]/;
        const matches = re1.exec(sql);
        if (matches !== null) {
            const table = matches[1];
            const re2 = /PRIMARY\s+KEY\s+\(\[[^\]]*\](?:,\s*\[[^\]]*\])*\)/;
            sql = sql.replace(re2, (match, ...args) => {
                return `CONSTRAINT [${table}_pk] ${match}`;
            });
            const re3 = /FOREIGN\s+KEY\s+\((\[[^\]]*\](?:,\s*\[[^\]]*\])*)\)/g;
            const re4 = /\[([^\]]*)\]/g;
            sql = sql.replace(re3, (match, ...args) => {
                const fkcols = args[0];
                let fkname = table;
                let matches2 = re4.exec(fkcols);
                while (matches2 !== null) {
                    fkname += `_${matches2[1]}`;
                    matches2 = re4.exec(fkcols);
                }
                return `CONSTRAINT [${fkname}_fk] FOREIGN KEY (${fkcols})`;
            });
        }
        return sql;
    }
    /**
     * SQL:
     * let sql = '' +
     * 'ALTER TABLE [UserGroup]' +
     * '  ADD CONSTRAINT [invalid1] FOREIGN KEY ([userId1], [userId2], [userId3]) REFERENCES [User] ([userId1], [userId2], [userId3]) ON DELETE NO ACTION,' +
     * '      CONSTRAINT [invalid2] FOREIGN KEY ([groupId1], [groupId2]) REFERENCES [Group] ([groupId1], [groupId2]) ON DELETE NO ACTION, ' +
     * '      CONSTRAINT [invalid3] FOREIGN KEY ([instanceId1]) REFERENCES [Instance] ([instanceId1]) ON DELETE NO ACTION'
     */
    changeColumnQueryOverride(tableName, attributes) {
        let sql = this.changeColumnQuery.apply(this.queryGenerator, [tableName, attributes]);
        const re1 = /ALTER\s+TABLE\s+\[([^\]]*)\]/;
        const matches = re1.exec(sql);
        if (matches !== null) {
            const table = matches[1];
            const re2 = /(ADD\s+)?CONSTRAINT\s+\[([^\]]*)\]\s+FOREIGN\s+KEY\s+\((\[[^\]]*\](?:,\s*\[[^\]]*\])*)\)/g;
            const re3 = /\[([^\]]*)\]/g;
            sql = sql.replace(re2, (match, ...args) => {
                const fkcols = args[2];
                let fkname = table;
                let matches2 = re3.exec(fkcols);
                while (matches2 !== null) {
                    fkname += `_${matches2[1]}`;
                    matches2 = re3.exec(fkcols);
                }
                return `${args[0] ? args[0] : ''}CONSTRAINT [${fkname}_fk] FOREIGN KEY (${fkcols})`;
            });
        }
        return sql;
    }
    /**
     * Opens storage firewall used by the server when starting to get root bot instance.
     */
    async openStorageFrontier(installationDeployer) {
        const group = GBConfigService_1.GBConfigService.get('CLOUD_GROUP');
        const serverName = GBConfigService_1.GBConfigService.get('STORAGE_SERVER').split('.database.windows.net')[0];
        await installationDeployer.openStorageFirewall(group, serverName);
    }
    /**
     * Get a dynamic param from instance. Dynamic params are defined in Config.xlsx
     * and loaded into the work folder from /publish command.
     *
     * @param name Name of param to get from instance.
     * @param defaultValue Value returned when no param is defined in Config.xlsx.
     */
    getParam(instance, name, defaultValue) {
        let value = null;
        if (instance.params) {
            const params = JSON.parse(instance.params);
            value = params ? params[name] : defaultValue;
        }
        if (typeof defaultValue === 'boolean') {
            return new Boolean(value ? value.toLowerCase() === 'true' : defaultValue);
        }
        if (typeof defaultValue === 'string') {
            return value ? value : defaultValue;
        }
        if (typeof defaultValue === 'number') {
            return new Number(value ? value : defaultValue ? defaultValue : 0);
        }
        if (instance['dataValues'] && !value) {
            value = instance['dataValues'][name];
            if (value === null) {
                const minBoot = app_1.GBServer.globals.minBoot;
                if (minBoot.instance && minBoot.instance.datavalues) {
                    value = minBoot.instance.datavalues[name];
                }
            }
        }
        return value;
    }
    /**
     * Load all cached schedule from BASIC SET SCHEDULE keyword.
     */
    async loadSchedules() {
        botlib_1.GBLog.info(`Loading instances from storage...`);
        let schedules;
        try {
            const options = { where: { state: 'active' } };
            schedules = await GBModel_1.GuaribasSchedule.findAll(options);
            if (process.env.ENDPOINT_UPDATE === 'true') {
                await pragmatismo_io_framework_1.CollectionUtil.asyncForEach(schedules, async (item) => {
                    botlib_1.GBLog.info(`Updating bot endpoint for ${item.botId}...`);
                    try {
                        const options = {
                            scheduled: true,
                            timezone: 'America/Sao_Paulo'
                        };
                        cron.schedule(item.schedule, async () => {
                            let script = item.name;
                            let min = app_1.GBServer.globals.minInstances.filter(p => p.instance.instanceId === item.instanceId)[0];
                            GBVMService_1.GBVMService.callVM(script, min, null, null);
                        }, options);
                        botlib_1.GBLog.info(`Running .gbdialog word ${item.name} on:${item.schedule}...`);
                    }
                    catch (error) { }
                });
            }
        }
        catch (error) {
            throw new Error(`Cannot schedule: ${error.message}.`);
        }
        return schedules;
    }
}
exports.GBCoreService = GBCoreService;
//# sourceMappingURL=GBCoreService.js.map