/*****************************************************************************\
|                                               ( )_  _                       |
|    _ _    _ __   _ _    __    ___ ___     _ _ | ,_)(_)  ___   ___     _     |
|   ( '_`\ ( '__)/'_` ) /'_ `\/' _ ` _ `\ /'_` )| |  | |/',__)/' v `\ /'_`\   |
|   | (_) )| |  ( (_| |( (_) || ( ) ( ) |( (_| || |_ | |\__, \| (˅) |( (_) )  |
|   | ,__/'(_)  `\__,_)`\__  |(_) (_) (_)`\__,_)`\__)(_)(____/(_) (_)`\___/'  |
|   | |                ( )_) |                                                |
|   (_)                 \___/'                                                |
|                                                                             |
| 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.GBMinService = void 0;
const { DialogSet, TextPrompt } = require('botbuilder-dialogs');
const express = require('express');
const request = require('request-promise-native');
const removeRoute = require('express-remove-route');
const AuthenticationContext = require('adal-node').AuthenticationContext;
const wash = require('washyourmouthoutwithsoap');
const { FacebookAdapter } = require('botbuilder-adapter-facebook');
const botbuilder_1 = require("botbuilder");
const botbuilder_dialogs_1 = require("botbuilder-dialogs");
const botlib_1 = require("botlib");
const pragmatismo_io_framework_1 = require("pragmatismo-io-framework");
const botframework_connector_1 = require("botframework-connector");
const app_1 = require("../../../src/app");
const GBAdminService_1 = require("../../admin.gbapp/services/GBAdminService");
const AnalyticsService_1 = require("../../analytics.gblib/services/AnalyticsService");
const GBVMService_1 = require("../../basic.gblib/services/GBVMService");
const KBService_1 = require("../../kb.gbapp/services/KBService");
const SecService_1 = require("../../security.gbapp/services/SecService");
const WhatsappDirectLine_1 = require("../../whatsapp.gblib/services/WhatsappDirectLine");
const strings_1 = require("../strings");
const GBConfigService_1 = require("./GBConfigService");
const GBConversationalService_1 = require("./GBConversationalService");
const GBDeployer_1 = require("./GBDeployer");
const urlJoin = require("url-join");
const fs = require("fs");
const GoogleChatDirectLine_1 = require("../../google-chat.gblib/services/GoogleChatDirectLine");
/**
 * Minimal service layer for a bot and encapsulation of BOT Framework calls.
 */
class GBMinService {
    /**
     * Static initialization of minimal instance.
     */
    constructor(core, conversationalService, adminService, deployer) {
        this.core = core;
        this.conversationalService = conversationalService;
        this.adminService = adminService;
        this.deployer = deployer;
    }
    /**
     * Constructs a new minimal instance for each bot.
     */
    async buildMin(instances) {
        // Servers default UI on root address '/' if web enabled.
        if (process.env.DISABLE_WEB !== 'true') {
            const url = app_1.GBServer.globals.wwwroot
                ? app_1.GBServer.globals.wwwroot
                : urlJoin(GBDeployer_1.GBDeployer.deployFolder, GBMinService.uiPackage, 'build');
            app_1.GBServer.globals.server.use('/', express.static(url));
        }
        // Servers the bot information object via HTTP so clients can get
        // instance information stored on server.
        if (process.env.DISABLE_WEB !== 'true') {
            app_1.GBServer.globals.server.get('/instances/:botId', this.handleGetInstanceForClient.bind(this));
        }
        // Servers the WhatsApp callback.
        app_1.GBServer.globals.server.post('/webhooks/whatsapp', this.WhatsAppCallback.bind(this));
        // Call mountBot event to all bots.
        await pragmatismo_io_framework_1.CollectionUtil.asyncForEach(instances, async (instance) => {
            try {
                await this.mountBot(instance);
            }
            catch (error) {
                botlib_1.GBLog.error(`Error mounting bot ${instance.botId}: ${error.message}\n${error.stack}`);
            }
        });
    }
    /**
     * Removes bot endpoint from web listeners and remove bot instance
     * from list of global server bot instances.
     */
    async unmountBot(botId) {
        const url = `/api/messages/${botId}`;
        removeRoute(app_1.GBServer.globals.server, url);
        const uiUrl = `/${botId}`;
        removeRoute(app_1.GBServer.globals.server, uiUrl);
        app_1.GBServer.globals.minInstances = app_1.GBServer.globals.minInstances.filter(p => p.instance.botId !== botId);
    }
    /**
     * Mount the instance by creating an BOT Framework bot object,
     * serving bot endpoint in several URL like WhatsApp endpoint, .gbkb assets,
     * installing all BASIC artifacts from .gbdialog and OAuth2.
     */
    async mountBot(instance) {
        // Build bot adapter.
        const { min, adapter, conversationState } = await this.buildBotAdapter(instance, app_1.GBServer.globals.sysPackages, app_1.GBServer.globals.appPackages);
        app_1.GBServer.globals.minInstances.push(min);
        await this.deployer.deployPackage(min, 'packages/default.gbtheme');
        // Install per bot deployed packages.
        let packagePath = `work/${min.botId}.gbai/${min.botId}.gbdialog`;
        if (fs.existsSync(packagePath)) {
            await this.deployer.deployPackage(min, packagePath);
        }
        packagePath = `work/${min.botId}.gbai/${min.botId}.gbapp`;
        if (fs.existsSync(packagePath)) {
            await this.deployer.deployPackage(min, packagePath);
        }
        packagePath = `work/${min.botId}.gbai/${min.botId}.gbtheme`;
        if (fs.existsSync(packagePath)) {
            await this.deployer.deployPackage(min, packagePath);
        }
        packagePath = `work/${min.botId}.gbai/${min.botId}.gblib`;
        if (fs.existsSync(packagePath)) {
            await this.deployer.deployPackage(min, packagePath);
        }
        // Calls the loadBot context.activity for all packages.
        await this.invokeLoadBot(app_1.GBServer.globals.appPackages, app_1.GBServer.globals.sysPackages, min);
        // Serves individual URL for each bot conversational interface.
        const receiver = async (req, res) => {
            await this.receiver(req, res, conversationState, min, instance, app_1.GBServer.globals.appPackages);
        };
        const url = `/api/messages/${instance.botId}`;
        app_1.GBServer.globals.server.post(url, receiver);
        app_1.GBServer.globals.server.get(url, (req, res) => {
            if (req.query['hub.mode'] === 'subscribe') {
                if (req.query['hub.verify_token'] === process.env.FACEBOOK_VERIFY_TOKEN) {
                    const val = req.query['hub.challenge'];
                    res.send(val);
                }
                else {
                    botlib_1.GBLog.error('Failed to verify endpoint.');
                    res.send('OK');
                }
            }
            res.end();
        });
        botlib_1.GBLog.info(`GeneralBots(${instance.engineName}) listening on: ${url}.`);
        // Serves individual URL for each bot user interface.
        if (process.env.DISABLE_WEB !== 'true') {
            const uiUrl = `/${instance.botId}`;
            const uiUrlAlt = `/${instance.activationCode}`;
            app_1.GBServer.globals.server.use(uiUrl, express.static(urlJoin(GBDeployer_1.GBDeployer.deployFolder, GBMinService.uiPackage, 'build')));
            app_1.GBServer.globals.server.use(uiUrlAlt, express.static(urlJoin(GBDeployer_1.GBDeployer.deployFolder, GBMinService.uiPackage, 'build')));
            const domain = min.core.getParam(min.instance, 'Domain', null);
            if (domain) {
                app_1.GBServer.globals.server.use(domain, express.static(urlJoin(GBDeployer_1.GBDeployer.deployFolder, GBMinService.uiPackage, 'build')));
                botlib_1.GBLog.info(`Bot UI ${GBMinService.uiPackage} accessible at custom domain: ${domain}.`);
            }
            botlib_1.GBLog.info(`Bot UI ${GBMinService.uiPackage} accessible at: ${uiUrl} and ${uiUrlAlt}.`);
        }
        // Clients get redirected here in order to create an OAuth authorize url and redirect them to AAD.
        // There they will authenticate and give their consent to allow this app access to
        // some resource they own.
        this.handleOAuthRequests(app_1.GBServer.globals.server, min);
        // After consent is granted AAD redirects here.  The ADAL library
        // is invoked via the AuthenticationContext and retrieves an
        // access token that can be used to access the user owned resource.
        this.handleOAuthTokenRequests(app_1.GBServer.globals.server, min, instance);
        // Provides checking of instance health.
        this.createCheckHealthAddress(app_1.GBServer.globals.server, min, min.instance);
    }
    async WhatsAppCallback(req, res) {
        try {
            // Detects if the message is echo fro itself.
            const id = req.body.messages[0].chatId.split('@')[0];
            const senderName = req.body.messages[0].senderName;
            const text = req.body.messages[0].body;
            if (req.body.messages[0].fromMe) {
                res.end();
                return; // Exit here.
            }
            // Detects if the welcome message is enabled.
            let activeMin;
            if (process.env.WHATSAPP_WELCOME_DISABLED !== 'true') {
                // Tries to find if user wants to switch bots.
                let toSwitchMin = app_1.GBServer.globals.minInstances.filter(p => p.instance.botId.toLowerCase() === text.toLowerCase())[0];
                if (!toSwitchMin) {
                    toSwitchMin = app_1.GBServer.globals.minInstances.filter(p => p.instance.activationCode ? p.instance.activationCode.toLowerCase() === text.toLowerCase() : false)[0];
                }
                // Find active bot instance.
                activeMin = toSwitchMin ? toSwitchMin : app_1.GBServer.globals.minBoot;
                // If it is the first time for the user, tries to auto-execute
                // start dialog if any is specified in Config.xlsx.
                const sec = new SecService_1.SecService();
                let user = await sec.getUserFromSystemId(id);
                if (user === null || user.hearOnDialog) {
                    user = await sec.ensureUser(activeMin.instance.instanceId, id, senderName, '', 'whatsapp', senderName, null);
                    const startDialog = user.hearOnDialog ?
                        user.hearOnDialog :
                        activeMin.core.getParam(activeMin.instance, 'Start Dialog', null);
                    if (startDialog) {
                        botlib_1.GBLog.info(`Calling /start to Auto start ${startDialog} for ${activeMin.instance.instanceId}...`);
                        req.body.messages[0].body = `/start`;
                        // Resets HEAR ON DIALOG value to none and passes
                        // current dialog to the direct line.
                        await sec.updateUserHearOnDialog(user.userId, null);
                        await activeMin.whatsAppDirectLine.received(req, res);
                    }
                    else {
                        await activeMin.whatsAppDirectLine.sendToDevice(id, `Olá! Seja bem-vinda(o)!\nMe chamo ${activeMin.instance.title}. Como posso ajudar? Pode me falar que eu te ouço, me manda um aúdio.`);
                        res.end();
                    }
                }
                else {
                    // User wants to switch bots.
                    if (toSwitchMin !== undefined) {
                        // So gets the new bot instance information and prepares to
                        // auto start dialog if any is specified.
                        const instance = await this.core.loadInstanceByBotId(activeMin.botId);
                        await sec.updateUserInstance(id, instance.instanceId);
                        await activeMin.whatsAppDirectLine.resetConversationId(id);
                        const startDialog = activeMin.core.getParam(activeMin.instance, 'Start Dialog', null);
                        if (startDialog) {
                            botlib_1.GBLog.info(`Calling /start for Auto start : ${startDialog} for ${activeMin.instance.botId}...`);
                            req.body.messages[0].body = `/start`;
                            await activeMin.whatsAppDirectLine.received(req, res);
                        }
                        else {
                            await activeMin.whatsAppDirectLine.sendToDevice(id, `Agora falando com ${activeMin.instance.title}...`);
                        }
                        res.end();
                    }
                    else {
                        activeMin = app_1.GBServer.globals.minInstances.filter(p => p.instance.instanceId === user.instanceId)[0];
                        if (activeMin === undefined) {
                            activeMin = app_1.GBServer.globals.minBoot;
                            await activeMin.whatsAppDirectLine.sendToDevice(id, `O outro Bot que você estava falando(${user.instanceId}), não está mais disponível. Agora você está falando comigo, ${activeMin.instance.title}...`);
                        }
                        await activeMin.whatsAppDirectLine.received(req, res);
                    }
                }
            }
            else {
                // Just pass the message to the receiver.
                await app_1.GBServer.globals.minBoot.whatsAppDirectLine.received(req, res);
            }
        }
        catch (error) {
            botlib_1.GBLog.error(`Error on Whatsapp callback: ${error.data ? error.data : error}`);
        }
    }
    /**
     * Creates a listener that can be used by external monitors to check
     * bot instance health.
     */
    createCheckHealthAddress(server, min, instance) {
        server.get(`/${min.instance.botId}/check`, async (req, res) => {
            try {
                // Performs the checking of WhatsApp API if enabled for this instance.
                if (min.whatsAppDirectLine != undefined && instance.whatsappServiceKey !== null) {
                    if (!(await min.whatsAppDirectLine.check(min))) {
                        const error = `WhatsApp API lost connection.`;
                        botlib_1.GBLog.error(error);
                        res.status(500).send(error);
                        return;
                    }
                }
                // GB is OK, so 200.
                res.status(200).send(`General Bot ${min.botId} is healthly.`);
            }
            catch (error) {
                // GB is not OK, 500 and detail the information on response content.
                botlib_1.GBLog.error(error);
                res.status(500).send(error.toString());
            }
        });
    }
    /**
     * Handle OAuth2 web service calls for token requests
     * on https://<gbhost>/<BotId>/token URL.
     */
    handleOAuthTokenRequests(server, min, instance) {
        server.get(`/${min.instance.botId}/token`, async (req, res) => {
            // Checks request state by reading AntiCSRFAttackState from GB Admin infrastructure.
            const state = await min.adminService.getValue(instance.instanceId, 'AntiCSRFAttackState');
            if (req.query.state !== state) {
                const msg = 'WARNING: state field was not provided as anti-CSRF token';
                botlib_1.GBLog.error(msg);
                throw new Error(msg);
            }
            const authenticationContext = new AuthenticationContext(urlJoin(min.instance.authenticatorAuthorityHostUrl, min.instance.authenticatorTenant));
            const resource = 'https://graph.microsoft.com';
            // Calls MSFT to get token.
            authenticationContext.acquireTokenWithAuthorizationCode(req.query.code, urlJoin(instance.botEndpoint, min.instance.botId, '/token'), resource, instance.marketplaceId, instance.marketplacePassword, async (err, token) => {
                if (err) {
                    const msg = `handleOAuthTokenRequests: Error acquiring token: ${err}`;
                    botlib_1.GBLog.error(msg);
                    res.send(msg);
                }
                else {
                    // Saves token to the database.
                    await this.adminService.setValue(instance.instanceId, 'accessToken', token.accessToken);
                    await this.adminService.setValue(instance.instanceId, 'refreshToken', token.refreshToken);
                    await this.adminService.setValue(instance.instanceId, 'expiresOn', token.expiresOn.toString());
                    await this.adminService.setValue(instance.instanceId, 'AntiCSRFAttackState', undefined);
                    // Inform the home for default .gbui after finishing token retrival.
                    res.redirect(min.instance.botEndpoint);
                }
            });
        });
    }
    /**
     * Handle OAuth2 web service calls for authorization requests
     * on https://<gbhost>/<BotId>/auth URL.
     */
    handleOAuthRequests(server, min) {
        server.get(`/${min.instance.botId}/auth`, (req, res) => {
            let authorizationUrl = urlJoin(min.instance.authenticatorAuthorityHostUrl, min.instance.authenticatorTenant, '/oauth2/authorize');
            authorizationUrl = `${authorizationUrl}?response_type=code&client_id=${min.instance.marketplaceId}&redirect_uri=${urlJoin(min.instance.botEndpoint, min.instance.botId, 'token')}`;
            botlib_1.GBLog.info(`HandleOAuthRequests: ${authorizationUrl}.`);
            res.redirect(authorizationUrl);
        });
    }
    /**
     * Returns the instance object to clients requesting bot info.
     */
    async handleGetInstanceForClient(req, res) {
        // Translates the requested botId.
        let botId = req.params.botId;
        if (botId === '[default]' || botId === undefined) {
            botId = GBConfigService_1.GBConfigService.get('BOT_ID');
        }
        botlib_1.GBLog.info(`Client requested instance for: ${botId}.`);
        // Loads by the botId itself or by the activationCode field.
        let instance = await this.core.loadInstanceByBotId(botId);
        if (instance === null) {
            instance = await this.core.loadInstanceByActivationCode(botId);
        }
        if (instance !== null) {
            // Gets the webchat token, speech token and theme.
            const webchatTokenContainer = await this.getWebchatToken(instance);
            const speechToken = instance.speechKey != undefined ? await this.getSTSToken(instance) : null;
            let theme = instance.theme;
            // Sends all information to the .gbui web client.
            if (!theme) {
                theme = `default.gbtheme`;
            }
            res.send(JSON.stringify({
                instanceId: instance.instanceId,
                botId: botId,
                theme: theme,
                webchatToken: webchatTokenContainer.token,
                speechToken: speechToken,
                conversationId: webchatTokenContainer.conversationId,
                authenticatorTenant: instance.authenticatorTenant,
                authenticatorClientId: instance.marketplaceId,
                paramLogoImageUrl: this.core.getParam(instance, 'Logo Image Url', null),
                paramLogoImageAlt: this.core.getParam(instance, 'Logo Image Alt', null),
                paramLogoImageWidth: this.core.getParam(instance, 'Logo Image Width', null),
                paramLogoImageHeight: this.core.getParam(instance, 'Logo Image Height', null),
                paramLogoImageType: this.core.getParam(instance, 'Logo Image Type', null)
            }));
        }
        else {
            const error = `Instance not found while retrieving from .gbui web client: ${botId}.`;
            res.sendStatus(error);
            botlib_1.GBLog.error(error);
        }
    }
    /**
     * Gets Webchat token from Bot Service.
     */
    async getWebchatToken(instance) {
        const options = {
            url: 'https://directline.botframework.com/v3/directline/tokens/generate',
            method: 'POST',
            headers: {
                Authorization: `Bearer ${instance.webchatKey}`
            }
        };
        try {
            const json = await request(options);
            return JSON.parse(json);
        }
        catch (error) {
            const msg = `[botId:${instance.botId}] Error calling Direct Line to generate a token for Web control: ${error}.`;
            return Promise.reject(new Error(msg));
        }
    }
    /**
     * Gets a Speech to Text / Text to Speech token from the provider.
     */
    async getSTSToken(instance) {
        const options = {
            url: instance.speechEndpoint,
            method: 'POST',
            headers: {
                'Ocp-Apim-Subscription-Key': instance.speechKey
            }
        };
        try {
            return await request(options);
        }
        catch (error) {
            const msg = `Error calling Speech to Text client. Error is: ${error}.`;
            return Promise.reject(new Error(msg));
        }
    }
    /**
     * Builds the BOT Framework & GB infrastructures.
     */
    async buildBotAdapter(instance, sysPackages, appPackages) {
        // MSFT stuff.
        const adapter = new botbuilder_1.BotFrameworkAdapter({ appId: instance.marketplaceId, appPassword: instance.marketplacePassword });
        const storage = new botbuilder_1.MemoryStorage();
        const conversationState = new botbuilder_1.ConversationState(storage);
        const userState = new botbuilder_1.UserState(storage);
        adapter.use(new botbuilder_1.AutoSaveStateMiddleware(conversationState, userState));
        botframework_connector_1.MicrosoftAppCredentials.trustServiceUrl('https://directline.botframework.com', new Date(new Date().setFullYear(new Date().getFullYear() + 10)));
        // The minimal bot is built here.
        const min = new botlib_1.GBMinInstance();
        min.botId = instance.botId;
        min.bot = adapter;
        min.userState = userState;
        min.core = this.core;
        min.conversationalService = this.conversationalService;
        min.adminService = this.adminService;
        min.deployService = this.deployer;
        min.kbService = new KBService_1.KBService(this.core.sequelize);
        min.instance = instance;
        min.cbMap = {};
        min.scriptMap = {};
        min.sandBoxMap = {};
        min.packages = sysPackages;
        min.appPackages = appPackages;
        if (app_1.GBServer.globals.minBoot === undefined) {
            app_1.GBServer.globals.minBoot = min;
        }
        if (process.env.FACEBOOK_VERIFY_TOKEN) {
            min['fbAdapter'] = new FacebookAdapter({
                verify_token: process.env.FACEBOOK_VERIFY_TOKEN,
                app_secret: process.env.FACEBOOK_APP_SECRET,
                access_token: process.env.FACEBOOK_ACCESS_TOKEN
            });
        }
        // TODO: min.appPackages =  core.getPackagesByInstanceId(min.instance.instanceId);
        // Creates a hub of services available in .gbapps.
        await pragmatismo_io_framework_1.CollectionUtil.asyncForEach(min.appPackages, async (e) => {
            let services;
            if ((services = await e.onExchangeData(min, 'getServices', null))) {
                min.gbappServices = { ...min.gbappServices, ...services };
            }
        });
        if (min.instance.googlePrivateKey) {
            min['googleDirectLine'] = new GoogleChatDirectLine_1.GoogleChatDirectLine(min, min.botId, min.instance.googleBotKey, min.instance.googleChatSubscriptionName, min.instance.googleChatApiKey, min.instance.googleClientEmail, min.instance.googlePrivateKey.replace(/\\n/gm, '\n'), min.instance.googleProjectId);
            await min['googleDirectLine'].setup(true);
        }
        // If there is WhatsApp configuration specified, initialize
        // infrastructure objects.
        if (min.instance.whatsappServiceUrl) {
            min.whatsAppDirectLine = new WhatsappDirectLine_1.WhatsappDirectLine(min, min.botId, min.instance.whatsappBotKey, min.instance.whatsappServiceKey, min.instance.whatsappServiceNumber, min.instance.whatsappServiceUrl);
            await min.whatsAppDirectLine.setup(true);
        }
        else {
            const minBoot = app_1.GBServer.globals.minBoot;
            if (minBoot.instance.whatsappServiceUrl) {
                min.whatsAppDirectLine = new WhatsappDirectLine_1.WhatsappDirectLine(min, min.botId, min.instance.whatsappBotKey, minBoot.instance.whatsappServiceKey, minBoot.instance.whatsappServiceNumber, minBoot.instance.whatsappServiceUrl);
                await min.whatsAppDirectLine.setup(false);
            }
        }
        // Setups default BOT Framework dialogs.
        min.userProfile = conversationState.createProperty('userProfile');
        const dialogState = conversationState.createProperty('dialogState');
        min.dialogs = new DialogSet(dialogState);
        min.dialogs.add(new TextPrompt('textPrompt'));
        min.dialogs.add(new botbuilder_dialogs_1.ConfirmPrompt('confirmPrompt'));
        min.dialogs.add(new botbuilder_dialogs_1.OAuthPrompt('oAuthPrompt', {
            connectionName: 'OAuth2',
            text: 'Please sign in to General Bots.',
            title: 'Sign in',
            timeout: 300000
        }));
        return { min, adapter, conversationState };
    }
    /**
     * Performs calling of loadBot event in all .gbapps.
     */
    async invokeLoadBot(appPackages, sysPackages, min) {
        // Calls loadBot event in all .gbapp packages.
        await pragmatismo_io_framework_1.CollectionUtil.asyncForEach(sysPackages, async (p) => {
            p.sysPackages = sysPackages;
            if (p.getDialogs !== undefined) {
                const dialogs = await p.getDialogs(min);
                if (dialogs !== undefined) {
                    dialogs.forEach(dialog => {
                        min.dialogs.add(new botbuilder_dialogs_1.WaterfallDialog(dialog.id, dialog.waterfall));
                    });
                }
            }
            await p.loadBot(min);
        });
        // Adds all dialogs from .gbapps into global dialo list for this minimal instance.
        await pragmatismo_io_framework_1.CollectionUtil.asyncForEach(appPackages, async (p) => {
            p.sysPackages = sysPackages;
            await p.loadBot(min);
            if (p.getDialogs !== undefined) {
                const dialogs = await p.getDialogs(min);
                if (dialogs !== undefined) {
                    dialogs.forEach(dialog => {
                        min.dialogs.add(new botbuilder_dialogs_1.WaterfallDialog(dialog.id, dialog.waterfall));
                    });
                }
            }
        });
    }
    // TODO: Unify in util.
    userMobile(step) {
        if (isNaN(step.context.activity['mobile'])) {
            if (step.context.activity.from && !isNaN(step.context.activity.from.id)) {
                return step.context.activity.from.id;
            }
            return null;
        }
        else {
            return step.context.activity['mobile'];
        }
    }
    /**
     * BOT Framework web service hook method.
     */
    async receiver(req, res, conversationState, min, instance, appPackages) {
        let adapter = min.bot;
        if (req.body.object) {
            req['rawBody'] = JSON.stringify(req.body);
            adapter = min['fbAdapter'];
        }
        // Default activity processing and handler.
        await adapter['processActivity'](req, res, async (context) => {
            // Get loaded user state
            const step = await min.dialogs.createContext(context);
            step.context.activity.locale = 'pt-BR';
            let firstTime = false;
            try {
                const user = await min.userProfile.get(context, {});
                // First time processing.
                const sec = new SecService_1.SecService();
                if (!user.loaded) {
                    user.loaded = true;
                    user.subjects = [];
                    user.cb = undefined;
                    user.welcomed = false;
                    user.basicOptions = { maxLines: 100, translatorOn: true };
                    firstTime = true;
                    const service = new KBService_1.KBService(min.core.sequelize);
                    const data = await service.getFaqBySubjectArray(instance.instanceId, 'faq', undefined);
                    await min.conversationalService.sendEvent(min, step, 'play', {
                        playerType: 'bullet',
                        data: data.slice(0, 10)
                    });
                    // This same event is dispatched either to all participants
                    // including the bot, that is filtered bellow.
                    if (context.activity.from.id !== min.botId) {
                        // Creates a new row in user table if it does not exists.
                        const member = context.activity.from;
                        const persistedUser = await sec.ensureUser(instance.instanceId, member.id, member.name, '', 'web', member.name, null);
                        // Stores conversation associated to the user to group each message.
                        const analytics = new AnalyticsService_1.AnalyticsService();
                        user.systemUser = persistedUser;
                        user.conversation = await analytics.createConversation(persistedUser);
                    }
                    // Saves session user (persisted GuaribasUser is inside).
                    await min.userProfile.set(step.context, user);
                }
                user.systemUser = await sec.getUserFromSystemId(user.systemUser.userSystemId);
                await min.userProfile.set(step.context, user);
                // Required for MSTEAMS handling of persisted conversations.
                if (step.context.activity.channelId === 'msteams') {
                    const conversationReference = JSON.stringify(botbuilder_1.TurnContext.getConversationReference(context.activity));
                    await sec.updateConversationReferenceById(user.systemUser.userId, conversationReference);
                    if (!user.welcomed) {
                        const startDialog = min.core.getParam(min.instance, 'Start Dialog', null);
                        if (startDialog && !user.welcomed) {
                            user.welcomed = true;
                            botlib_1.GBLog.info(`Auto start (teams) dialog is now being called: ${startDialog} for ${min.instance.botId}...`);
                            await GBVMService_1.GBVMService.callVM(startDialog.toLowerCase(), min, step, this.deployer);
                        }
                    }
                }
                // Required for F0 handling of persisted conversations.
                botlib_1.GBLog.info(`User>: text:${context.activity.text} (type: ${context.activity.type}, name: ${context.activity.name}, channelId: ${context.activity.channelId}, value: ${context.activity.value})`);
                // Answer to specific BOT Framework event conversationUpdate to auto start dialogs.
                // Skips if the bot is talking.
                if (context.activity.type === 'conversationUpdate' &&
                    context.activity.membersAdded.length > 0) {
                    // Check if a bot or a human participant is being added to the conversation.
                    const member = context.activity.membersAdded[0];
                    if (context.activity.membersAdded[0].id === context.activity.recipient.id) {
                        botlib_1.GBLog.info(`Bot added to conversation, starting chat...`);
                        // Calls onNewSession event on each .gbapp package.
                        await pragmatismo_io_framework_1.CollectionUtil.asyncForEach(appPackages, async (e) => {
                            await e.onNewSession(min, step);
                        });
                        // Auto starts dialogs if any is specified.
                        const startDialog = min.core.getParam(min.instance, 'Start Dialog', null);
                        if (!startDialog && !user.welcomed) {
                            // Otherwise, calls / (root) to default welcome users.
                            await step.beginDialog('/');
                        }
                    }
                    else {
                        botlib_1.GBLog.info(`Person added to conversation: ${member.name}`);
                        if (this.userMobile(step)) {
                            const startDialog = min.core.getParam(min.instance, 'Start Dialog', null);
                            if (startDialog && !user.welcomed) {
                                user.welcomed = true;
                                botlib_1.GBLog.info(`Auto start (whatsapp) dialog is now being called: ${startDialog} for ${min.instance.instanceId}...`);
                                await GBVMService_1.GBVMService.callVM(startDialog.toLowerCase(), min, step, this.deployer);
                            }
                        }
                    }
                }
                else if (context.activity.type === 'message') {
                    // Process messages activities.
                    await this.processMessageActivity(context, min, step);
                }
                else if (context.activity.type === 'event') {
                    // Processes events activies.
                    await this.processEventActivity(min, user, context, step);
                }
                // Saves conversation state for later use.
                await conversationState.saveChanges(context, true);
            }
            catch (error) {
                const msg = `ERROR: ${error.message} ${error.stack ? error.stack : ''}`;
                botlib_1.GBLog.error(msg);
                await min.conversationalService.sendText(min, step, strings_1.Messages[step.context.activity.locale].very_sorry_about_error);
                await step.beginDialog('/ask', { isReturning: true });
            }
        });
    }
    /**
     * Called to handle all event sent by .gbui clients.
     */
    async processEventActivity(min, user, context, step) {
        if (context.activity.name === 'whoAmI') {
            await step.beginDialog('/whoAmI');
        }
        else if (context.activity.name === 'showSubjects') {
            await step.beginDialog('/menu', undefined);
        }
        else if (context.activity.name === 'giveFeedback') {
            await step.beginDialog('/feedback', {
                fromMenu: true
            });
        }
        else if (context.activity.name === 'showFAQ') {
            await step.beginDialog('/faq');
        }
        else if (context.activity.name === 'answerEvent') {
            await step.beginDialog('/answerEvent', {
                questionId: context.activity.data,
                fromFaq: true
            });
        }
        else if (context.activity.name === 'quality') {
            await step.beginDialog('/quality', {
                score: context.activity.data
            });
        }
        else if (context.activity.name === 'startGB') {
            const startDialog = min.core.getParam(min.instance, 'Start Dialog', null);
            if (startDialog && !user.welcomed) {
                user.welcomed = true;
                botlib_1.GBLog.info(`Auto start (web) dialog is now being called: ${startDialog} for ${min.instance.instanceId}...`);
                await GBVMService_1.GBVMService.callVM(startDialog.toLowerCase(), min, step, this.deployer);
            }
        }
        else if (context.activity.name === 'updateToken') {
            const token = context.activity.data;
            await step.beginDialog('/adminUpdateToken', { token: token });
        }
        else {
            await step.continueDialog();
        }
    }
    /**
     * Called to handle all text messages sent and received by the bot.
     */
    async processMessageActivity(context, min, step) {
        const sec = new SecService_1.SecService();
        // Removes <at>Bot Id</at> from MS Teams.
        context.activity.text = context.activity.text.trim();
        context.activity.text = context.activity.text.replace(/\<at\>.*\<\/at\>\s/gi, '');
        // Syn. temporary replacement (to be migrated to Synonyms.xlsx)
        context.activity.text = context.activity.text.replace(/\busuario\b/gi, 'usuário');
        context.activity.text = context.activity.text.replace(/\busuaria\b/gi, 'usuária');
        context.activity.text = context.activity.text.replace(/\busuarios\b/gi, 'usuários');
        context.activity.text = context.activity.text.replace(/\busuarias\b/gi, 'usuárias');
        context.activity.text = context.activity.text.replace(/\bfuncionario\b/gi, 'funcionário');
        context.activity.text = context.activity.text.replace(/\bfuncionarios\b/gi, 'funcionários');
        const user = await min.userProfile.get(context, {});
        let message;
        if (process.env.PRIVACY_STORE_MESSAGES === 'true') {
            // Adds message to the analytics layer.
            const analytics = new AnalyticsService_1.AnalyticsService();
            if (user) {
                if (!user.conversation) {
                    user.conversation = await analytics.createConversation(user.systemUser);
                }
                message = await analytics.createMessage(min.instance.instanceId, user.conversation, user.systemUser.userId, context.activity.text);
            }
        }
        // Checks for global exit kewywords cancelling any active dialogs.
        const globalQuit = (locale, utterance) => {
            return utterance.match(strings_1.Messages.global_quit);
        };
        // Files in .gbdialog can be called directly by typing its name normalized into JS .
        const isVMCall = Object.keys(min.scriptMap).find(key => min.scriptMap[key] === context.activity.text) !== undefined;
        if (isVMCall) {
            await GBVMService_1.GBVMService.callVM(context.activity.text, min, step, this.deployer);
        }
        else if (context.activity.text.charAt(0) === '/') {
            const text = context.activity.text;
            const parts = text.split(' ');
            const cmdOrDialogName = parts[0];
            parts.splice(0, 1);
            const args = parts.join(' ');
            if (cmdOrDialogName === '/start') {
                // TODO: Args to BASIC.
            }
            else if (cmdOrDialogName === '/call') {
                await GBVMService_1.GBVMService.callVM(args, min, step, this.deployer);
            }
            else {
                await step.beginDialog(cmdOrDialogName, { args: args });
            }
        }
        else if (globalQuit(step.context.activity.locale, context.activity.text)) {
            await step.cancelAllDialogs();
            await min.conversationalService.sendText(min, step, strings_1.Messages[step.context.activity.locale].canceled);
        }
        else if (context.activity.text === 'admin') {
            await step.beginDialog('/admin');
        }
        else if (context.activity.text.startsWith('{"title"')) {
            await step.beginDialog('/menu', JSON.parse(context.activity.text));
        }
        else if (!(await this.deployer.getStoragePackageByName(min.instance.instanceId, `${min.instance.botId}.gbkb`)) &&
            process.env.GBKB_ENABLE_AUTO_PUBLISH === 'true') {
            await min.conversationalService.sendText(min, step, `Oi, ainda não possuo pacotes de conhecimento publicados. Por favor, aguarde alguns segundos enquanto eu auto-publico alguns pacotes.`);
            await step.beginDialog('/publish', { confirm: true, firstTime: true });
        }
        else {
            // Removes unwanted chars in input text.
            let text = context.activity.text;
            const originalText = text;
            text = text.replace(/<([^>]+?)([^>]*?)>(.*?)<\/\1>/gi, '');
            // Saves special words (keep text) in tokens to prevent it from
            // spell checking and translation.
            const keepText = min.core.getParam(min.instance, 'Keep Text', '');
            let keepTextList = [];
            if (keepTextList) {
                keepTextList = keepTextList.concat(keepText.split(';'));
            }
            const replacements = [];
            await pragmatismo_io_framework_1.CollectionUtil.asyncForEach(min.appPackages, async (e) => {
                const result = await e.onExchangeData(min, 'getKeepText', {});
                if (result) {
                    keepTextList = keepTextList.concat(result);
                }
            });
            const getNormalizedRegExp = (value) => {
                var chars = [
                    { letter: 'a', reg: '[aáàãäâ]' },
                    { letter: 'e', reg: '[eéèëê]' },
                    { letter: 'i', reg: '[iíìïî]' },
                    { letter: 'o', reg: '[oóòõöô]' },
                    { letter: 'u', reg: '[uúùüû]' },
                    { letter: 'c', reg: '[cç]' }
                ];
                for (var i in chars) {
                    value = value.replace(new RegExp(chars[i].letter, 'gi'), chars[i].reg);
                }
                ;
                return value;
            };
            let textProcessed = text;
            if (keepTextList) {
                keepTextList = keepTextList.filter(p => p.trim() !== '');
                let i = 0;
                await pragmatismo_io_framework_1.CollectionUtil.asyncForEach(keepTextList, item => {
                    const it = GBConversationalService_1.GBConversationalService.removeDiacritics(item);
                    const noAccentText = GBConversationalService_1.GBConversationalService.removeDiacritics(textProcessed);
                    if (noAccentText.toLowerCase().indexOf(it.toLowerCase()) != -1) {
                        const replacementToken = 'X' + GBAdminService_1.GBAdminService.getNumberIdentifier().substr(0, 4);
                        replacements[i] = { text: item, replacementToken: replacementToken };
                        i++;
                        textProcessed = textProcessed.replace(new RegExp(`\\b${getNormalizedRegExp(it.trim())}\\b`, 'gi'), `${replacementToken}`);
                    }
                });
            }
            // Spells check the input text before translating,
            // keeping fixed tokens as specified in Config.
            text = await min.conversationalService.spellCheck(min, textProcessed);
            // Detects user typed language and updates their locale profile if applies.
            let locale = min.core.getParam(min.instance, 'Default User Language', GBConfigService_1.GBConfigService.get('DEFAULT_USER_LANGUAGE'));
            const detectLanguage = min.core.getParam(min.instance, 'Language Detector', GBConfigService_1.GBConfigService.getBoolean('LANGUAGE_DETECTOR')) === 'true';
            const systemUser = user.systemUser;
            locale = systemUser.locale;
            if (detectLanguage || !locale) {
                locale = await min.conversationalService.getLanguage(min, text);
                if (systemUser.locale != locale) {
                    user.systemUser = await sec.updateUserLocale(systemUser.userId, locale);
                    await min.userProfile.set(step.context, user);
                }
            }
            // Checks for bad words on input text.
            const hasBadWord = wash.check(locale, context.activity.text);
            if (hasBadWord) {
                return await step.beginDialog('/pleaseNoBadWords');
            }
            // Translates text into content language, keeping
            // reserved tokens specified in Config.
            const contentLocale = min.core.getParam(min.instance, 'Default Content Language', GBConfigService_1.GBConfigService.get('DEFAULT_CONTENT_LANGUAGE'));
            text = await min.conversationalService.translate(min, text, contentLocale);
            botlib_1.GBLog.info(`Translated text (processMessageActivity): ${text}.`);
            // Restores all token text back after spell checking and translation.
            if (keepTextList) {
                let i = 0;
                await pragmatismo_io_framework_1.CollectionUtil.asyncForEach(replacements, item => {
                    i++;
                    text = text.replace(new RegExp(`${item.replacementToken}`, 'gi'), item.text);
                });
            }
            step.context.activity['text'] = text;
            step.context.activity['originalText'] = originalText;
            botlib_1.GBLog.info(`Final text ready for NLP/Search/.gbapp: ${text}.`);
            if (user.systemUser.agentMode === 'self') {
                const manualUser = await sec.getUserFromAgentSystemId(user.systemUser.userSystemId);
                botlib_1.GBLog.info(`HUMAN AGENT (${user.systemUser.userSystemId}) TO USER ${manualUser.userSystemId}: ${text}`);
                const cmd = 'SEND FILE ';
                if (text.startsWith(cmd)) {
                    const filename = text.substr(cmd.length);
                    const message = await min.kbService.getAnswerTextByMediaName(min.instance.instanceId, filename);
                    if (message === null) {
                        botlib_1.GBLog.error(`File ${filename} not found in any .gbkb published. Check the name or publish again the associated .gbkb.`);
                    }
                    else {
                        await min.conversationalService.sendMarkdownToMobile(min, null, manualUser.userSystemId, message);
                    }
                }
                else {
                    await min.whatsAppDirectLine.sendToDeviceEx(manualUser.userSystemId, `${manualUser.agentSystemId}: ${text}`, locale);
                }
            }
            else {
                // If there is a dialog in course, continue to the next step.
                if (step.activeDialog !== undefined) {
                    await step.continueDialog();
                }
                else {
                    const startDialog = user.hearOnDialog ?
                        user.hearOnDialog :
                        min.core.getParam(min.instance, 'Start Dialog', null);
                    if (text !== startDialog) {
                        let nextDialog = null;
                        await pragmatismo_io_framework_1.CollectionUtil.asyncForEach(min.appPackages, async (e) => {
                            nextDialog = await e.onExchangeData(min, 'handleAnswer', {
                                query: text,
                                step: step,
                                notTranslatedQuery: originalText,
                                message: message ? message['dataValues'] : null,
                                user: user ? user.dataValues : null
                            });
                        });
                        await step.beginDialog(nextDialog ? nextDialog : '/answer', {
                            query: text,
                            user: user ? user.dataValues : null,
                            message: message
                        });
                    }
                }
            }
        }
    }
}
exports.GBMinService = GBMinService;
/**
 * Default General Bots User Interface package.
 */
GBMinService.uiPackage = 'default.gbui';
//# sourceMappingURL=GBMinService.js.map