diff --git a/README.md b/README.md index 75125fd5..a541bf79 100644 --- a/README.md +++ b/README.md @@ -65,8 +65,8 @@ Notes: ### Configure the server to deploy specific directory 1. Create/Edit the .env file and add the ADDITIONAL_DEPLOY_PATH key pointing to the .gbai local parent folder of .gbapp, .gbot, .gbtheme, .gbkb package directories. -2. Specify DATABASE_SYNC to TRUE so database sync is run when the server is run. -3. In case of Microsoft SQL Server add the following keys: DATABASE_HOST, DATABASE_NAME, DATABASE_USERNAME, DATABASE_PASSWORD, DATABASE_DIALECT to `mssql`. +2. Specify STORAGE_SYNC to TRUE so database sync is run when the server is run. +3. In case of Microsoft SQL Server add the following keys: STORAGE_HOST, STORAGE_NAME, STORAGE_USERNAME, STORAGE_PASSWORD, STORAGE_DIALECT to `mssql`. Note: diff --git a/VERSION.md b/VERSION.md index af72d230..921b624d 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1,5 +1,8 @@ # Release History +## Version 0.1.4 + + ## Version 0.1.3 * FIX: Admin now is internationalized. @@ -24,8 +27,8 @@ ## Version 0.0.30 - FIX: Packages updated. -- NEW: DATABASE_SYNC_ALTER environment parameter. -- NEW: DATABASE_SYNC_FORCE environment parameter. +- NEW: STORAGE_SYNC_ALTER environment parameter. +- NEW: STORAGE_SYNC_FORCE environment parameter. - NEW: Define constraint names in MSSQL. ## Version 0.0.29 @@ -44,7 +47,7 @@ - FIX: Packages updated. - NEW: If a bot package's name begins with '.', then it is ignored. -- NEW: Created DATABASE_LOGGING environment parameter. +- NEW: Created STORAGE_LOGGING environment parameter. ## Version 0.0.25 diff --git a/deploy/admin.gbapp/dialogs/AdminDialog.ts b/deploy/admin.gbapp/dialogs/AdminDialog.ts index 7ec9db15..45a86f64 100644 --- a/deploy/admin.gbapp/dialogs/AdminDialog.ts +++ b/deploy/admin.gbapp/dialogs/AdminDialog.ts @@ -48,7 +48,6 @@ import { Messages } from "../strings"; * Dialogs for administration tasks. */ export class AdminDialog extends IGBDialog { - static async undeployPackageCommand(text: any, min: GBMinInstance) { let packageName = text.split(" ")[1]; let importer = new GBImporter(min.core); @@ -59,9 +58,7 @@ export class AdminDialog extends IGBDialog { ); } - static async deployPackageCommand(text: string, - deployer: GBDeployer - ) { + static async deployPackageCommand(text: string, deployer: GBDeployer) { let packageName = text.split(" ")[1]; let additionalPath = GBConfigService.get("ADDITIONAL_DEPLOY_PATH"); await deployer.deployPackageFromLocalPath( @@ -92,10 +89,7 @@ export class AdminDialog extends IGBDialog { password === GBConfigService.get("ADMIN_PASS") && GBAdminService.StrongRegex.test(password) ) { - - await dc.context.sendActivity( - Messages[locale].welcome - ); + await dc.context.sendActivity(Messages[locale].welcome); await dc.prompt("textPrompt", Messages[locale].which_task); } else { await dc.prompt("textPrompt", Messages[locale].wrong_password); @@ -105,11 +99,12 @@ export class AdminDialog extends IGBDialog { async (dc, value) => { const locale = dc.context.activity.locale; var text = value; - const user = min.userState.get(dc.context); let cmdName = text.split(" ")[0]; - dc.context.sendActivity(Messages[locale].working(cmdName)) + + dc.context.sendActivity(Messages[locale].working(cmdName)); + let unknownCommand = false; + if (text === "quit") { - user.authenticated = false; await dc.replace("/"); } else if (cmdName === "deployPackage") { await AdminDialog.deployPackageCommand(text, deployer); @@ -124,12 +119,19 @@ export class AdminDialog extends IGBDialog { await dc.replace("/admin", { firstRun: false }); } else if (cmdName === "setupSecurity") { await AdminDialog.setupSecurity(min, dc); + } else { + unknownCommand = true; } - else{ + + if (unknownCommand) { await dc.context.sendActivity(Messages[locale].unknown_command); - dc.endAll() - await dc.replace("/answer", { query: text }); + } else { + await dc.context.sendActivity( + Messages[locale].finshed_working(cmdName) + ); } + await dc.endAll(); + await dc.replace("/answer", { query: text }); } ]); } @@ -152,8 +154,6 @@ export class AdminDialog extends IGBDialog { min.instance.botId }/token&state=${state}&response_mode=query`; - await dc.context.sendActivity( - Messages[locale].consent(url) - ); + await dc.context.sendActivity(Messages[locale].consent(url)); } } diff --git a/deploy/admin.gbapp/strings.ts b/deploy/admin.gbapp/strings.ts index 621a74a6..dbd2c65d 100644 --- a/deploy/admin.gbapp/strings.ts +++ b/deploy/admin.gbapp/strings.ts @@ -3,7 +3,8 @@ export const Messages = { authenticate: "Please, authenticate:", welcome: "Welcome to Pragmatismo.io GeneralBots Administration.", which_task: "Which task do you wanna run now?", - working:(command)=> `I'm working on ${command}`, + working:(command)=> `I'm working on ${command}...`, + finshed_working:"Done.", unknown_command: text => `Well, but ${text} is not a administrative General Bots command, I will try to search for it.`, hi: text => `Hello, ${text}.`, diff --git a/deploy/core.gbapp/services/GBConfigService.ts b/deploy/core.gbapp/services/GBConfigService.ts index fb2e448c..bc2a0aff 100644 --- a/deploy/core.gbapp/services/GBConfigService.ts +++ b/deploy/core.gbapp/services/GBConfigService.ts @@ -58,11 +58,11 @@ export class GBConfigService { if (!value) { switch (key) { - case "DATABASE_DIALECT": + case "STORAGE_DIALECT": value = "sqlite" break - case "DATABASE_STORAGE": + case "STORAGE_STORAGE": value = "./guaribas.sqlite" break @@ -70,17 +70,17 @@ export class GBConfigService { value = undefined break - case "DATABASE_SYNC": - case "DATABASE_SYNC_ALTER": - case "DATABASE_SYNC_FORCE": + case "STORAGE_SYNC": + case "STORAGE_SYNC_ALTER": + case "STORAGE_SYNC_FORCE": value = "false" break - case "DATABASE_LOGGING": + case "STORAGE_LOGGING": value = "false" break - case "DATABASE_ENCRYPT": + case "STORAGE_ENCRYPT": value = "true" break diff --git a/deploy/core.gbapp/services/GBConversationalService.ts b/deploy/core.gbapp/services/GBConversationalService.ts index 2614ad2e..e8fef1ca 100644 --- a/deploy/core.gbapp/services/GBConversationalService.ts +++ b/deploy/core.gbapp/services/GBConversationalService.ts @@ -42,6 +42,7 @@ import { LuisRecognizer } from "botbuilder-ai"; import { MessageFactory } from "botbuilder"; import { Messages } from "../strings"; import { AzureText } from "pragmatismo-io-framework"; +import { any } from "bluebird"; const Nexmo = require("nexmo"); export interface LanguagePickerSettings { @@ -103,31 +104,44 @@ export class GBConversationalService implements IGBConversationalService { subscriptionKey: min.instance.nlpSubscriptionKey, serviceEndpoint: min.instance.nlpServerUrl }); - let res = await model.recognize(dc.context); + + let nlp: any; + try { + nlp = await model.recognize(dc.context); + } catch (error) { + let msg = `Error calling NLP server, check if you have a published model and assigned keys on the service. Error: ${ + error.statusCode ? error.statusCode : "" + } ${error.message}`; + return Promise.reject(new Error(msg)); + } // Resolves intents returned from LUIS. - let topIntent = LuisRecognizer.topIntent(res); + let topIntent = LuisRecognizer.topIntent(nlp); if (topIntent) { var intent = topIntent; var entity = - res.entities && res.entities.length > 0 - ? res.entities[0].entity.toUpperCase() + nlp.entities && nlp.entities.length > 0 + ? nlp.entities[0].entity.toUpperCase() : null; + if (intent === "None") { + return Promise.resolve(false); + } + logger.info("NLP called:" + intent + ", " + entity); try { - await dc.replace("/" + intent, res.entities); + await dc.replace("/" + intent, nlp.entities); + return Promise.resolve(true); } catch (error) { - let msg = `Error running NLP (${intent}): ${error}`; - logger.info(msg); - return Promise.reject(msg); + let msg = `Error finding dialog associated to NLP event: ${intent}: ${ + error.message + }`; + return Promise.reject(new Error(msg)); } - return Promise.resolve(true); - } else { - return Promise.resolve(false); } + return Promise.resolve(false); } async checkLanguage(dc, min, text) { diff --git a/deploy/core.gbapp/services/GBCoreService.ts b/deploy/core.gbapp/services/GBCoreService.ts index cd50946a..1ec2437a 100644 --- a/deploy/core.gbapp/services/GBCoreService.ts +++ b/deploy/core.gbapp/services/GBCoreService.ts @@ -77,7 +77,7 @@ export class GBCoreService implements IGBCoreService { * Constructor retrieves default values. */ constructor() { - this.dialect = GBConfigService.get("DATABASE_DIALECT") + this.dialect = GBConfigService.get("STORAGE_DIALECT") this.adminService = new GBAdminService(this) } @@ -94,22 +94,22 @@ export class GBCoreService implements IGBCoreService { let storage: string | undefined if (this.dialect === "mssql") { - host = GBConfigService.get("DATABASE_HOST") - database = GBConfigService.get("DATABASE_NAME") - username = GBConfigService.get("DATABASE_USERNAME") - password = GBConfigService.get("DATABASE_PASSWORD") + host = GBConfigService.get("STORAGE_HOST") + database = GBConfigService.get("STORAGE_NAME") + username = GBConfigService.get("STORAGE_USERNAME") + password = GBConfigService.get("STORAGE_PASSWORD") } else if (this.dialect === "sqlite") { - storage = GBConfigService.get("DATABASE_STORAGE") + storage = GBConfigService.get("STORAGE_STORAGE") } let logging = - GBConfigService.get("DATABASE_LOGGING") === "true" + GBConfigService.get("STORAGE_LOGGING") === "true" ? (str: string) => { logger.info(str) } : false - let encrypt = GBConfigService.get("DATABASE_ENCRYPT") === "true" + let encrypt = GBConfigService.get("STORAGE_ENCRYPT") === "true" this.sequelize = new Sequelize({ host: host, @@ -247,9 +247,9 @@ export class GBCoreService implements IGBCoreService { } async syncDatabaseStructure() { - if (GBConfigService.get("DATABASE_SYNC") === "true") { - const alter = GBConfigService.get("DATABASE_SYNC_ALTER") === "true" - const force = GBConfigService.get("DATABASE_SYNC_FORCE") === "true" + if (GBConfigService.get("STORAGE_SYNC") === "true") { + const alter = GBConfigService.get("STORAGE_SYNC_ALTER") === "true" + const force = GBConfigService.get("STORAGE_SYNC_FORCE") === "true" logger.info("Syncing database...") return this.sequelize.sync({ alter: alter, @@ -258,7 +258,6 @@ export class GBCoreService implements IGBCoreService { } else { let msg = "Database synchronization is disabled."; logger.info(msg) - return Promise.reject(msg) } } diff --git a/deploy/core.gbapp/services/GBDeployer.ts b/deploy/core.gbapp/services/GBDeployer.ts index a59794e7..6bfcc79b 100644 --- a/deploy/core.gbapp/services/GBDeployer.ts +++ b/deploy/core.gbapp/services/GBDeployer.ts @@ -152,7 +152,11 @@ export class GBDeployer { .done(async result => { logger.info(`App Package deployment done.`); - await core.syncDatabaseStructure(); + try{ + await core.syncDatabaseStructure(); + }catch(e){ + throw e; + } /** Deploys all .gbot files first. */ @@ -212,7 +216,7 @@ export class GBDeployer { }) .done(function(result) { if (botPackages.length === 0) { - logger.info( + logger.warn( "The server is running with no bot instances, at least one .gbot file must be deployed." ); } else { diff --git a/deploy/core.gbapp/services/GBMinService.ts b/deploy/core.gbapp/services/GBMinService.ts index 67189135..ce9b2032 100644 --- a/deploy/core.gbapp/services/GBMinService.ts +++ b/deploy/core.gbapp/services/GBMinService.ts @@ -62,6 +62,8 @@ import { IGBCoreService, IGBConversationalService } from "botlib"; +import { GuaribasInstance } from "../models/GBModel"; +import { Messages } from "../strings"; /** Minimal service layer for a bot. */ @@ -103,7 +105,8 @@ export class GBMinService { async buildMin( server: any, - appPackages: Array + appPackages: Array, + instances:GuaribasInstance[] ): Promise { // Serves default UI on root address '/'. @@ -112,10 +115,7 @@ export class GBMinService { "/", express.static(UrlJoin(GBDeployer.deployFolder, uiPackage, "build")) ); - - // Loads all bot instances from storage and starting loading them. - - let instances = await this.core.loadInstances(); + Promise.all( instances.map(async instance => { // Gets the authorization key for each instance from Bot Service. @@ -363,11 +363,15 @@ export class GBMinService { instance: any, appPackages: any[] ) { + return adapter.processActivity(req, res, async context => { + + const state = conversationState.get(context); + const dc = min.dialogs.createContext(context, state); + dc.context.activity.locale = "en-US"; // TODO: Make dynamic. + try { - const state = conversationState.get(context); - const dc = min.dialogs.createContext(context, state); - dc.context.activity.locale = "en-US"; + const user = min.userState.get(dc.context); if (!user.loaded) { @@ -459,10 +463,14 @@ export class GBMinService { } } } catch (error) { - let msg = `Error in main activity: ${error.message} ${ + let msg = `ERROR: ${error.message} ${ error.stack ? error.stack : "" }`; logger.error(msg); + + await dc.context.sendActivity(Messages[dc.context.activity.locale].very_sorry_about_error) + await dc.begin("/ask", { isReturning: true }); + } }); } @@ -487,10 +495,9 @@ export class GBMinService { return Promise.resolve(JSON.parse(json)); } catch (error) { let msg = `Error calling Direct Line client, verify Bot endpoint on the cloud. Error is: ${error}.`; - logger.error(msg); - return Promise.reject(msg); + return Promise.reject(new Error(msg)); } - } + } /** * Gets a Speech to Text / Text to Speech token from the provider. @@ -513,8 +520,7 @@ export class GBMinService { return await request(options); } catch (error) { let msg = `Error calling Speech to Text client. Error is: ${error}.`; - logger.error(msg); - return Promise.reject(msg); + return Promise.reject(new Error(msg)); } } } diff --git a/deploy/core.gbapp/strings.ts b/deploy/core.gbapp/strings.ts index 549b41e5..e39d839c 100644 --- a/deploy/core.gbapp/strings.ts +++ b/deploy/core.gbapp/strings.ts @@ -4,13 +4,16 @@ export const Messages = { good_morning: "good morning", good_evening: "good evening", good_night: "good night", - hi: (msg ) => `Hello, ${msg}.` + hi: (msg ) => `Hello, ${msg}.`, + very_sorry_about_error: `I'm sorry to inform that there was an error which was recorded to be solved.` + }, "pt-BR": { show_video: "Vou te mostrar um vídeo. Por favor, aguarde...", good_morning: "bom dia", good_evening: "boa tarde", good_night: "boa noite", - hi: (msg ) => `Oi, ${msg}.` + hi: (msg ) => `Oi, ${msg}.`, + very_sorry_about_error: `Lamento, ocorreu um erro que já foi registrado para ser tratado.` } }; diff --git a/deploy/kb.gbapp/services/KBService.ts b/deploy/kb.gbapp/services/KBService.ts index 684e990d..7a2fd923 100644 --- a/deploy/kb.gbapp/services/KBService.ts +++ b/deploy/kb.gbapp/services/KBService.ts @@ -148,7 +148,7 @@ export class KBService { } // TODO: Filter by instance. what = `${what}&$filter=instanceId eq ${instanceId}` try { - if (instance.searchKey && GBConfigService.get("DATABASE_DIALECT") == "mssql") { + if (instance.searchKey && GBConfigService.get("STORAGE_DIALECT") == "mssql") { let service = new AzureSearch( instance.searchKey, instance.searchHost, @@ -179,7 +179,7 @@ export class KBService { } } catch (reason) { - return Promise.reject(reason) + return Promise.reject(new Error(reason)); } } diff --git a/src/app.ts b/src/app.ts index 0be98eda..d9530c78 100644 --- a/src/app.ts +++ b/src/app.ts @@ -55,6 +55,7 @@ import { GBAdminPackage } from "../deploy/admin.gbapp/index"; import { GBCustomerSatisfactionPackage } from "../deploy/customer-satisfaction.gbapp"; import { IGBPackage } from "botlib"; import { GBAdminService } from "../deploy/admin.gbapp/services/GBAdminService"; +import { GuaribasInstance } from "deploy/core.gbapp/models/GBModel"; let appPackages = new Array(); @@ -62,7 +63,10 @@ let appPackages = new Array(); * General Bots open-core entry point. */ export class GBServer { - /** Program entry-point. */ + /** + * Program entry-point. + */ + static run() { // Creates a basic HTTP server that will serve several URL, one for each // bot instance. This allows the same server to attend multiple Bot on @@ -108,6 +112,8 @@ export class GBServer { ); } + // Creates the minimal service shared across all .gbapps. + let minService = new GBMinService( core, conversationalService, @@ -132,16 +138,48 @@ export class GBServer { p.loadPackage(core, core.sequelize); }); + // Loads all bot instances from object storage, if it's formatted. + + logger.info(`All instances are being now loaded...`); + let instances: GuaribasInstance[]; + try { + instances = await core.loadInstances(); + } catch (error) { + // Check if storage is empty and needs formatting. + + let isInvalidObject = + error.parent.number == 208 || error.parent.errno == 1; // MSSQL or SQLITE. + if ( + isInvalidObject && + GBConfigService.get("STORAGE_SYNC") !== "true" + ) { + throw `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 + }.`; + } + } + + // Deploy packages and format object store according to .gbapp storage models. + logger.info(`Deploying packages.`); await deployer.deployPackages(core, server, appPackages); + + // If instances is undefined here it's because storage has been formatted. + // Load all instances from .gbot found on deploy package directory. + if (!instances) { + instances = await core.loadInstances(); + } + + // Setup server dynamic (per bot instance) resources and listeners. + logger.info(`Building minimal instances.`); - await minService.buildMin(server, appPackages); - - logger.info(`All instances are now loaded and available.`); + await minService.buildMin(server, appPackages, instances); logger.info(`The Bot Server is in RUNNING mode...`); + return core; } catch (err) { logger.info(`STOP: ${err} ${err.stack ? err.stack : ""}`); + process.exit(1); } })(); });