diff --git a/packages/basic.gblib/services/GBVMService.ts b/packages/basic.gblib/services/GBVMService.ts index 802f5645..43ad8411 100644 --- a/packages/basic.gblib/services/GBVMService.ts +++ b/packages/basic.gblib/services/GBVMService.ts @@ -32,7 +32,7 @@ import { GBMinInstance, GBService, IGBCoreService, GBLog } from 'botlib'; import * as Fs from 'fs'; -import * as ji from 'just-indent' +import * as ji from 'just-indent'; import { GBServer } from '../../../src/app.js'; import { GBDeployer } from '../../core.gbapp/services/GBDeployer.js'; import { CollectionUtil } from 'pragmatismo-io-framework'; @@ -52,8 +52,8 @@ import { GBLogEx } from '../../core.gbapp/services/GBLogEx.js'; import { GuaribasUser } from '../../security.gbapp/models/index.js'; import { SystemKeywords } from './SystemKeywords.js'; import { Sequelize, QueryTypes } from '@sequelize/core'; -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; /** * @fileoverview Decision was to priorize security(isolation) and debugging, @@ -68,7 +68,8 @@ export class GBVMService extends GBService { public static API_PORT = 1111; public async loadDialogPackage(folder: string, min: GBMinInstance, core: IGBCoreService, deployer: GBDeployer) { - const files = await walkPromise(folder); + const ignore = Path.join('work', DialogKeywords.getGBAIPath(min.botId, 'gbdialog'), 'node_modules'); + const files = await walkPromise(folder, { ignore: [ignore] }); await CollectionUtil.asyncForEach(files, async file => { if (!file) { @@ -77,9 +78,7 @@ export class GBVMService extends GBService { let filename: string = file.name; - if (filename.endsWith('.docx')) { - filename = await this.loadDialog(filename, folder, min); - } + filename = await this.loadDialog(filename, folder, min); }); } @@ -91,14 +90,11 @@ export class GBVMService extends GBService { //check every key for being same return Object.keys(obj1).every(function (key) { - //if object - if ((typeof obj1[key] == "object") && (typeof obj2[key] == "object")) { - + if (typeof obj1[key] == 'object' && typeof obj2[key] == 'object') { //recursively check return GBVMService.compare(obj1[key], obj2[key]); } else { - //do the normal compare return obj1[key] === obj2[key]; } @@ -106,9 +102,22 @@ export class GBVMService extends GBService { } public async loadDialog(filename: string, folder: string, min: GBMinInstance) { + const isWord = filename.endsWith('.docx'); + if ( + !( + isWord || + filename.endsWith('.vbs') || + filename.endsWith('.vb') || + filename.endsWith('.vba') || + filename.endsWith('.bas') || + filename.endsWith('.basic') + ) + ) { + return; + } const wordFile = filename; - const vbsFile = filename.substr(0, filename.indexOf('docx')) + 'vbs'; + const vbsFile = isWord ? filename.substr(0, filename.indexOf('docx')) + 'vbs' : filename; const fullVbsFile = urlJoin(folder, vbsFile); const docxStat = Fs.statSync(urlJoin(folder, wordFile)); const interval = 3000; // If compiled is older 30 seconds, then recompile. @@ -125,11 +134,10 @@ export class GBVMService extends GBService { // }; // let { baseUrl, client } = await GBDeployer.internalGetDriveClient(min); - + // await client.api('/subscriptions') // .post(subscription); - if (Fs.existsSync(fullVbsFile)) { const vbsStat = Fs.statSync(fullVbsFile); if (docxStat['mtimeMs'] < vbsStat['mtimeMs'] + interval) { @@ -184,7 +192,6 @@ export class GBVMService extends GBService { min.sandBoxMap[mainName.toLowerCase().trim()] = parsedCode; return filename; } - private processNodeModules(folder: string, min: GBMinInstance) { const node_modules = urlJoin(process.env.PWD, folder, 'node_modules'); if (!Fs.existsSync(node_modules)) { @@ -215,7 +222,6 @@ export class GBVMService extends GBService { } private syncStorageFromTABLE(folder: string, filename: string, min: GBMinInstance, mainName: string) { - const tablesFile = urlJoin(folder, `${filename}.tables.json`); let sync = false; @@ -225,7 +231,6 @@ export class GBVMService extends GBService { const tableDef = JSON.parse(Fs.readFileSync(tablesFile, 'utf8')) as any; const getTypeBasedOnCondition = (t, size) => { - if (1) { switch (t) { case 'string': @@ -249,7 +254,6 @@ export class GBVMService extends GBService { default: return { type: 'TABLE', name: t }; } - } else { switch (t) { case 'string': @@ -273,7 +277,6 @@ export class GBVMService extends GBService { default: return { key: 'TABLE', name: t }; } - } }; @@ -286,22 +289,17 @@ export class GBVMService extends GBService { if (Fs.existsSync(filePath)) { connections = JSON.parse(Fs.readFileSync(filePath, 'utf8')); } - const shouldSync = min.core.getParam( - min.instance, - 'Synchronize Database', - false - ); - - tableDef.forEach(async (t) => { + const shouldSync = min.core.getParam(min.instance, 'Synchronize Database', false); + tableDef.forEach(async t => { const tableName = t.name.trim(); // Determines autorelationship. Object.keys(t.fields).forEach(key => { let obj = t.fields[key]; obj.type = getTypeBasedOnCondition(obj.type, obj.size); - if (obj.type.key === "TABLE") { - obj.type.key = "BIGINT"; + if (obj.type.key === 'TABLE') { + obj.type.key = 'BIGINT'; associations.push({ from: tableName, to: obj.type.name }); } }); @@ -320,11 +318,12 @@ export class GBVMService extends GBService { const username = con['storageUsername']; const password = con['storagePassword']; - const logging: boolean | Function = GBConfigService.get('STORAGE_LOGGING') === 'true' - ? (str: string): void => { - GBLogEx.info(min, str); - } - : false; + const logging: boolean | Function = + GBConfigService.get('STORAGE_LOGGING') === 'true' + ? (str: string): void => { + GBLogEx.info(min, str); + } + : false; const encrypt: boolean = GBConfigService.get('STORAGE_ENCRYPT') === 'true'; const acquire = parseInt(GBConfigService.get('STORAGE_ACQUIRE_TIMEOUT')); @@ -362,9 +361,9 @@ export class GBVMService extends GBService { min[`llmconnection`] = { type: dialect, username, - database: storageName, password + database: storageName, + password }; - } } @@ -373,11 +372,9 @@ export class GBVMService extends GBService { } // Field checking, syncs if there is any difference. - const seq = min[connectionName] ? min[connectionName] - : minBoot.core.sequelize; + const seq = min[connectionName] ? min[connectionName] : minBoot.core.sequelize; if (seq) { - const model = seq.models[tableName]; if (model) { // Except Id, checks if has same number of fields. @@ -386,12 +383,11 @@ export class GBVMService extends GBService { let obj1 = t.fields[key]; let obj2 = model['fieldRawAttributesMap'][key]; - if (key !== "id") { + if (key !== 'id') { if (obj1 && obj2) { equals++; } } - }); if (equals != Object.keys(t.fields).length) { @@ -401,23 +397,25 @@ export class GBVMService extends GBService { seq.define(tableName, t.fields); - // New table checking, if needs sync. + // New table checking, if needs sync. let tables; if (con.storageDriver === 'mssql') { - tables = await seq.query(`SELECT table_name, table_schema + tables = await seq.query( + `SELECT table_name, table_schema FROM information_schema.tables WHERE table_type = 'BASE TABLE' - ORDER BY table_name ASC`, { - type: QueryTypes.RAW - })[0]; - } - else if (con.storageDriver === 'mariadb') { + ORDER BY table_name ASC`, + { + type: QueryTypes.RAW + } + )[0]; + } else if (con.storageDriver === 'mariadb') { tables = await seq.getQueryInterface().showAllTables(); } let found = false; - tables.forEach((storageTable) => { + tables.forEach(storageTable => { if (storageTable['table_name'] === tableName) { found = true; } @@ -432,15 +430,17 @@ export class GBVMService extends GBService { try { to.hasMany(from); } catch (error) { - throw new Error(`BASIC: Invalid relationship in ${mainName}: from ${e.from} to ${e.to} (${min.botId})... ${error.message}`); + throw new Error( + `BASIC: Invalid relationship in ${mainName}: from ${e.from} to ${e.to} (${min.botId})... ${error.message}` + ); } - }); - if (sync && shouldSync) { - - GBLogEx.info(min, `BASIC: Syncing changes for TABLE ${connectionName} ${tableName} keyword (${min.botId})...`); + GBLogEx.info( + min, + `BASIC: Syncing changes for TABLE ${connectionName} ${tableName} keyword (${min.botId})...` + ); await seq.sync({ alter: true, @@ -454,7 +454,6 @@ export class GBVMService extends GBService { } public async translateBASIC(mainName, filename: any, min: GBMinInstance) { - // Converts General Bots BASIC into regular VBS let basicCode: string = Fs.readFileSync(filename, 'utf8'); @@ -467,17 +466,14 @@ export class GBVMService extends GBService { await s.deleteScheduleIfAny(min, mainName); let i = 1; - await CollectionUtil.asyncForEach(schedules, async (syntax) => { - + await CollectionUtil.asyncForEach(schedules, async syntax => { if (s) { - await s.createOrUpdateSchedule(min, syntax, `${mainName};${i++}`); + await s.createOrUpdateSchedule(min, syntax, `${mainName};${i++}`); } }); basicCode = basicCode.replace(/^\s*SET SCHEDULE (.*)/gim, ''); - - // Process INCLUDE keyword to include another // dialog inside the dialog. @@ -718,7 +714,6 @@ export class GBVMService extends GBService { Fs.writeFileSync(jsfile, code); GBLogEx.info(min, `[GBVMService] Finished loading of ${filename}, JavaScript from Word: \n ${code}`); - } private async executeTasks(min, tasks) { @@ -726,12 +721,10 @@ export class GBVMService extends GBService { const task = tasks[i]; if (task.kind === 'writeTableDefinition') { - // Creates an empty object that will receive Sequelize fields. const tablesFile = `${task.file}.tables.json`; Fs.writeFileSync(tablesFile, JSON.stringify(task.tables)); - } } } @@ -750,10 +743,10 @@ export class GBVMService extends GBService { lines.forEach(line => { if (line.trim()) { console.log(line); - const keyword = /\s*SET SCHEDULE (.*)/gi; + const keyword = /^\s*SET SCHEDULE (.*)/gi; let result: any = keyword.exec(line); if (result) { - result = result[1].replace(/\`|\"|\'/, '') + result = result[1].replace(/\`|\"|\'/, ''); result = result.trim(); results.push(result); } @@ -762,7 +755,7 @@ export class GBVMService extends GBService { return results; } - + private async getTextFromWord(folder: string, filename: string) { return new Promise(async (resolve, reject) => { const path = urlJoin(folder, filename); @@ -784,7 +777,6 @@ export class GBVMService extends GBService { } public static normalizeQuotes(text: any) { - text = text.replace(/\"/gm, '`'); text = text.replace(/\¨/gm, '`'); text = text.replace(/\“/gm, '`'); @@ -798,27 +790,22 @@ export class GBVMService extends GBService { public static getMetadata(mainName: string, propertiesText, description) { let properties = {}; if (!propertiesText || !description) { - - return {} + return {}; } const getType = asClause => { - asClause = asClause.trim().toUpperCase(); if (asClause.indexOf('STRING') !== -1) { return 'string'; - } - else if (asClause.indexOf('OBJECT') !== -1) { + } else if (asClause.indexOf('OBJECT') !== -1) { return 'object'; - } - else if (asClause.indexOf('INTEGER') !== -1 || asClause.indexOf('NUMBER') !== -1) { + } else if (asClause.indexOf('INTEGER') !== -1 || asClause.indexOf('NUMBER') !== -1) { return 'number'; } else { return 'enum'; } }; - for (let i = 0; i < propertiesText.length; i++) { const propertiesExp = propertiesText[i]; const t = getType(propertiesExp[2]); @@ -841,21 +828,19 @@ export class GBVMService extends GBService { properties[propertiesExp[1].trim()] = element; } - let json = { - type: "function", + type: 'function', function: { name: `${mainName}`, description: description ? description : '', parameters: zodToJsonSchema(z.object(properties)) } - } + }; return json; } public async parseField(line) { - let required = line.indexOf('*') !== -1; let unique = /\bunique\b/gi.test(line); let primaryKey = /\bkey\b/gi.test(line); @@ -877,7 +862,8 @@ export class GBVMService extends GBService { let definition = { allowNull: !required, - unique: unique, primaryKey: primaryKey, + unique: unique, + primaryKey: primaryKey, autoIncrement: autoIncrement }; definition['type'] = t; @@ -896,7 +882,6 @@ export class GBVMService extends GBService { * @param code General Bots BASIC */ public async convert(filename: string, mainName: string, code: string) { - // Start and End of VB2TS tags of processing. code = process.env.ENABLE_AUTH ? `hear GBLogExin as login\n${code}` : code; @@ -917,7 +902,6 @@ export class GBVMService extends GBService { const outputLines = []; let emmitIndex = 1; for (let i = 1; i <= lines.length; i++) { - let line = lines[i - 1]; // Remove lines before statements. @@ -968,12 +952,12 @@ export class GBVMService extends GBService { const endTableKeyword = /^\s*END TABLE\s*/gim; let endTableReg = endTableKeyword.exec(line); if (endTableReg && table) { - tables.push({ - name: table, fields: fields, connection: connection + name: table, + fields: fields, + connection: connection }); - fields = {}; table = null; connection = null; @@ -1013,14 +997,14 @@ export class GBVMService extends GBService { const talkKeyword = /^\s*BEGIN TALK\s*/gim; let talkReg = talkKeyword.exec(line); if (talkReg && !talk) { - talk = "await dk.talk ({pid: pid, text: `"; + talk = 'await dk.talk ({pid: pid, text: `'; emmit = false; } const systemPromptKeyword = /^\s*BEGIN SYSTEM PROMPT\s*/gim; let systemPromptReg = systemPromptKeyword.exec(line); if (systemPromptReg && !systemPrompt) { - systemPrompt = "await sys.setSystemPrompt ({pid: pid, text: `"; + systemPrompt = 'await sys.setSystemPrompt ({pid: pid, text: `'; emmit = false; } @@ -1038,9 +1022,10 @@ export class GBVMService extends GBService { if (tables) { tasks.push({ - kind: 'writeTableDefinition', file: filename, tables + kind: 'writeTableDefinition', + file: filename, + tables }); - } code = `${outputLines.join('\n')}\n`; @@ -1053,14 +1038,7 @@ export class GBVMService extends GBService { /** * Executes the converted JavaScript from BASIC code inside execution context. */ - public static async callVM( - text: string, - min: GBMinInstance, - step, - pid, - debug: boolean = false, - params = [] - ) { + public static async callVM(text: string, min: GBMinInstance, step, pid, debug: boolean = false, params = []) { // Creates a class DialogKeywords which is the *this* pointer // in BASIC. @@ -1076,8 +1054,7 @@ export class GBVMService extends GBService { // These variables will be automatically be available as normal BASIC variables. try { - variables['aadToken'] = await (min.adminService as any)['acquireElevatedToken'] - (min.instance.instanceId, false); + variables['aadToken'] = await (min.adminService as any)['acquireElevatedToken'](min.instance.instanceId, false); } catch (error) { variables['aadToken'] = 'ERROR: Configure /setupSecurity before using aadToken variable.'; } @@ -1118,12 +1095,10 @@ export class GBVMService extends GBService { let code = min.sandBoxMap[text]; const channel = step ? step.context.activity.channelId : 'web'; - const dk = new DialogKeywords(); const sys = new SystemKeywords(); await dk.setFilter({ pid: pid, value: null }); - // Find all tokens in .gbot Config. const strFind = ' Client ID'; @@ -1159,7 +1134,7 @@ export class GBVMService extends GBService { return await new Promise((resolve, reject) => { sandbox['resolve'] = resolve; // TODO: #411 sandbox['reject'] = reject; - sandbox['reject'] = ()=>{}; + sandbox['reject'] = () => {}; const vm1 = new NodeVM({ allowAsync: true, @@ -1176,7 +1151,6 @@ export class GBVMService extends GBService { const s = new VMScript(code, { filename: scriptPath }); result = vm1.run(s); }); - })(); } else { const runnerPath = urlJoin( @@ -1207,10 +1181,15 @@ export class GBVMService extends GBService { } catch (error) { throw new Error(`BASIC RUNTIME ERR: ${error.message ? error.message : error}\n Stack:${error.stack}`); } - } - public static createProcessInfo(user: GuaribasUser, min: GBMinInstance, channel: any, executable: string, step = null) { + public static createProcessInfo( + user: GuaribasUser, + min: GBMinInstance, + channel: any, + executable: string, + step = null + ) { const pid = GBAdminService.getNumberIdentifier(); GBServer.globals.processes[pid] = { pid: pid, diff --git a/packages/basic.gblib/services/ScheduleServices.ts b/packages/basic.gblib/services/ScheduleServices.ts index 6975ddaf..de19c832 100644 --- a/packages/basic.gblib/services/ScheduleServices.ts +++ b/packages/basic.gblib/services/ScheduleServices.ts @@ -47,33 +47,30 @@ import { GBLogEx } from '../../core.gbapp/services/GBLogEx.js'; * Basic services for BASIC manipulation. */ export class ScheduleServices extends GBService { - public async deleteScheduleIfAny(min: GBMinInstance, name: string) { - let i = 1; while (i <= 10) { const task = min['scheduleMap'] ? min['scheduleMap'][name + i] : null; if (task) { task.destroy(); - const id = `${name};${i}`; - - delete min['scheduleMap'][id]; - const count = await GuaribasSchedule.destroy({ - where: { - instanceId: min.instance.instanceId, - name: id - } - }); - - if (count > 0) { - GBLogEx.info(min, `BASIC: Removed ${name} SET SCHEDULE and ${count} rows from storage on: ${min.botId}...`); - } } + const id = `${name};${i}`; + + delete min['scheduleMap'][id]; + const count = await GuaribasSchedule.destroy({ + where: { + instanceId: min.instance.instanceId, + name: id + } + }); + + if (count > 0) { + GBLogEx.info(min, `BASIC: Removed ${name} SET SCHEDULE and ${count} rows from storage on: ${min.botId}...`); + } + i++; - } - } /** @@ -113,12 +110,10 @@ export class ScheduleServices extends GBService { let i = 0; let lastName = ''; - await CollectionUtil.asyncForEach(schedules, async (item) => { - + await CollectionUtil.asyncForEach(schedules, async item => { if (item.name === lastName) { item.name = item.name + ++i; - } - else { + } else { i = 0; } @@ -169,7 +164,6 @@ export class ScheduleServices extends GBService { }, options ); - } catch (error) { GBLogEx.error(min, `Running .gbdialog word ${item.name} : ${error}...`); } diff --git a/packages/core.gbapp/services/GBConfigService.ts b/packages/core.gbapp/services/GBConfigService.ts index 38984ad9..16fe7c5b 100644 --- a/packages/core.gbapp/services/GBConfigService.ts +++ b/packages/core.gbapp/services/GBConfigService.ts @@ -79,8 +79,11 @@ export class GBConfigService { public static get(key: string): string | undefined { let value = GBConfigService.tryGet(key); - if (value === undefined) { + if (!value) { switch (key) { + case 'STORAGE_NAME': + value = null; + break; case 'CLOUD_USERNAME': value = undefined; break; diff --git a/packages/core.gbapp/services/GBCoreService.ts b/packages/core.gbapp/services/GBCoreService.ts index a54ff315..7374fa83 100644 --- a/packages/core.gbapp/services/GBCoreService.ts +++ b/packages/core.gbapp/services/GBCoreService.ts @@ -242,7 +242,7 @@ export class GBCoreService implements IGBCoreService { * Loads all items to start several listeners. */ public async loadInstances(): Promise { - if (process.env.LOAD_ONLY !== undefined) { + if (process.env.LOAD_ONLY) { const bots = process.env.LOAD_ONLY.split(`;`); const and = []; await CollectionUtil.asyncForEach(bots, async e => { @@ -814,8 +814,6 @@ ENDPOINT_UPDATE=true mkdirp.sync(libraryPath); } - - await this.syncBotStorage(instances, 'default', deployer, libraryPath); const files = Fs.readdirSync(libraryPath); diff --git a/packages/core.gbapp/services/GBDeployer.ts b/packages/core.gbapp/services/GBDeployer.ts index d493166a..7fec4b09 100644 --- a/packages/core.gbapp/services/GBDeployer.ts +++ b/packages/core.gbapp/services/GBDeployer.ts @@ -254,6 +254,11 @@ export class GBDeployer implements IGBDeployer { if (GBConfigService.get('STORAGE_NAME')) { await this.deployBotOnAzure(instance, GBServer.globals.publicAddress); } + + // Makes available bot to the channels and .gbui interfaces. + + await GBServer.globals.minService.mountBot(instance); + // Creates remaining objects on the cloud and updates instance information. return instance; @@ -310,11 +315,7 @@ export class GBDeployer implements IGBDeployer { subscriptionId ); - // Makes available bot to the channels and .gbui interfaces. - - await GBServer.globals.minService.mountBot(instance); - await GBServer.globals.minService.ensureAPI(); - } + } // Saves final instance object and returns it. diff --git a/packages/core.gbapp/services/GBMinService.ts b/packages/core.gbapp/services/GBMinService.ts index a3aeb96e..254122b8 100644 --- a/packages/core.gbapp/services/GBMinService.ts +++ b/packages/core.gbapp/services/GBMinService.ts @@ -167,9 +167,6 @@ export class GBMinService { let i = 1; - if (instances.length > 1) { - } - await CollectionUtil.asyncForEach( instances, (async instance => { @@ -184,12 +181,9 @@ export class GBMinService { GBLog.error(`Error mounting bot ${instance.botId}: ${error.message}\n${error.stack}`); } }).bind(this) + ); - // Loads API. - - await this.ensureAPI(); - // Loads schedules. GBLogEx.info(0, `Loading SET SCHEDULE entries...`); @@ -199,6 +193,37 @@ export class GBMinService { GBLogEx.info(0, `All Bot instances loaded.`); } + public async startSimpleTest(min) { + if (process.env.TEST_MESSAGE && min['isDefault']) { + GBLogEx.info(min, `Starting auto test with '${process.env.TEST_MESSAGE}'.`); + + const client = await GBUtil.getDirectLineClient(min); + + const response = await client.apis.Conversations.Conversations_StartConversation(); + const conversationId = response.obj.conversationId; + GBServer.globals.debugConversationId = conversationId; + + const steps = process.env.TEST_MESSAGE.split(';'); + + await CollectionUtil.asyncForEach(steps, async (step) => { + client.apis.Conversations.Conversations_PostActivity({ + conversationId: conversationId, + activity: { + textFormat: 'plain', + text: step, + type: 'message', + from: { + id: 'test', + name: 'test' + } + } + }); + + await GBUtil.sleep(3000); + }); + } + } + /** * Removes bot endpoint from web listeners and remove bot instance * from list of global server bot instances. @@ -256,6 +281,8 @@ export class GBMinService { // https://github.com/GeneralBots/BotServer/issues/286 // min['groupCache'] = await KBService.getGroupReplies(instance.instanceId); + min['isDefault'] = GBServer.globals.minInstances.length === 0; + GBServer.globals.minInstances.push(min); const user = null; // No user context. @@ -330,9 +357,6 @@ export class GBMinService { GBLogEx.info(1, `WebDav for ${botId} loaded.`); }); } - // Loads Named Entity data for this bot. - - // TODO: await KBService.RefreshNER(min); // Calls the loadBot context.activity for all packages. @@ -356,34 +380,6 @@ export class GBMinService { }); GBLog.verbose(`GeneralBots(${instance.engineName}) listening on: ${url}.`); - // Test code. - if (process.env.TEST_MESSAGE) { - GBLogEx.info(min, `Starting auto test with '${process.env.TEST_MESSAGE}'.`); - - const client = await GBUtil.getDirectLineClient(min); - - const response = await client.apis.Conversations.Conversations_StartConversation(); - const conversationId = response.obj.conversationId; - GBServer.globals.debugConversationId = conversationId; - - const steps = process.env.TEST_MESSAGE.split(';'); - - await CollectionUtil.asyncForEach(steps, async step => { - client.apis.Conversations.Conversations_PostActivity({ - conversationId: conversationId, - activity: { - textFormat: 'plain', - text: step, - type: 'message', - from: { - id: 'test', - name: 'test' - } - } - }); - - await GBUtil.sleep(3000); - }); // Generates MS Teams manifest. @@ -394,7 +390,6 @@ export class GBMinService { const data = await this.deployer.getBotManifest(instance); Fs.writeFileSync(packageTeams, data); } - } // Serves individual URL for each bot user interface. @@ -463,6 +458,11 @@ export class GBMinService { .bind(min); GBDeployer.mountGBKBAssets(`${botId}.gbkb`, botId, `${botId}.gbkb`); + + // Loads API. + + await this.ensureAPI(); + } public static getProviderName(req: any, res: any) { @@ -839,6 +839,8 @@ export class GBMinService { let url = `/api/messages/${instance.botId}`; GBServer.globals.server.post(url, receiver); + url = `/api/messages`; + GBServer.globals.server.post(url, receiver); // NLP Manager. @@ -849,9 +851,6 @@ export class GBMinService { GBServer.globals.minBoot = min; GBServer.globals.minBoot.instance.marketplaceId = GBConfigService.get('MARKETPLACE_ID'); GBServer.globals.minBoot.instance.marketplacePassword = GBConfigService.get('MARKETPLACE_SECRET'); - } else { - url = `/api/messages`; - GBServer.globals.server.post(url, receiver); } if (min.instance.facebookWorkplaceVerifyToken) { diff --git a/packages/core.gbapp/services/router/bridge.ts b/packages/core.gbapp/services/router/bridge.ts index 71ba5ab2..07ad1e4b 100644 --- a/packages/core.gbapp/services/router/bridge.ts +++ b/packages/core.gbapp/services/router/bridge.ts @@ -12,197 +12,214 @@ const conversationsCleanupInterval = 10000; const conversations: { [key: string]: IConversation } = {}; const botDataStore: { [key: string]: IBotData } = {}; -export const getRouter = (serviceUrl: string, botUrl: string, conversationInitRequired = true, botId): express.Router => { - const router = express.Router(); +export const getRouter = ( + serviceUrl: string, + botUrl: string, + conversationInitRequired = true, + botId +): express.Router => { + const router = express.Router(); - router.use(bodyParser.json()); // for parsing application/json - router.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded - router.use((req, res, next) => { - res.header('Access-Control-Allow-Origin', '*'); - res.header('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE, PATCH, OPTIONS'); - res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, x-ms-bot-agent'); - next(); - }); + router.use(bodyParser.json()); // for parsing application/json + router.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded + router.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE, PATCH, OPTIONS'); + res.header( + 'Access-Control-Allow-Headers', + 'Origin, X-Requested-With, Content-Type, Accept, Authorization, x-ms-bot-agent' + ); + next(); + }); - // CLIENT ENDPOINT - router.options(`/directline/${botId}/`, (req, res) => { - res.status(200).end(); - }); + // CLIENT ENDPOINT + router.options(`/directline/${botId}/`, (req, res) => { + res.status(200).end(); + }); - // Creates a conversation - const reqs = (req, res) => { - - const conversationId: string = uuidv4.v4().toString(); - conversations[conversationId] = { - conversationId, - history: [], - }; - console.log('Created conversation with conversationId: ' + conversationId); - - const activity = createConversationUpdateActivity(serviceUrl, conversationId); - fetch(botUrl, { - method: 'POST', - body: JSON.stringify(activity), - headers: { - 'Content-Type': 'application/json', - }, - }).then((response) => { - res.status(response.status).send({ - conversationId, - expiresIn, - }); - }); + // Creates a conversation + const reqs = (req, res) => { + const conversationId: string = uuidv4.v4().toString(); + conversations[conversationId] = { + conversationId, + history: [] }; + console.log('Created conversation with conversationId: ' + conversationId); - router.post('/v3/directline/conversations',reqs ); - router.post(`/directline/${botId}/conversations`,reqs ); - router.post(`/directline/conversations`,reqs ); - - // Reconnect API - router.get('/v3/directline/conversations/:conversationId', (req, res) => { - const conversation = getConversation(req.params.conversationId, conversationInitRequired); - if (conversation) { - res.status(200).send(conversation); - } else { - // Conversation was never initialized - res.status(400).send(); + const activity = createConversationUpdateActivity(serviceUrl, conversationId); + fetch(botUrl, { + method: 'POST', + body: JSON.stringify(activity), + headers: { + 'Content-Type': 'application/json' + } + }).then(response => { + res.status(response.status).send({ + conversationId, + expiresIn + }); + }); + }; + + router.post('/v3/directline/conversations', reqs); + router.post(`/directline/${botId}/conversations`, reqs); + router.post(`/directline/conversations`, reqs); + + // Reconnect API + router.get('/v3/directline/conversations/:conversationId', (req, res) => { + const conversation = getConversation(req.params.conversationId, conversationInitRequired); + if (conversation) { + res.status(200).send(conversation); + } else { + // Conversation was never initialized + res.status(400).send(); + } + + console.warn('/v3/directline/conversations/:conversationId not implemented'); + }); + + // Gets activities from store (local history array for now) + router.get(`/directline/${botId}/conversations/:conversationId/activities`, (req, res) => { + const watermark = req.query.watermark && req.query.watermark !== 'null' ? Number(req.query.watermark) : 0; + + const conversation = getConversation(req.params.conversationId, conversationInitRequired); + + if (conversation) { + // If the bot has pushed anything into the history array + if (conversation.history.length > watermark) { + const activities = conversation.history.slice(watermark); + res.status(200).json({ + activities, + watermark: watermark + activities.length + }); + } else { + res.status(200).send({ + activities: [], + watermark + }); + } + } else { + // Conversation was never initialized + res.status(400).send(); + } + }); + + // Sends message to bot. Assumes message activities + router.post(`/directline/${botId}/conversations/:conversationId/activities`, (req, res) => { + const incomingActivity = req.body; + // Make copy of activity. Add required fields + const activity = createMessageActivity(incomingActivity, serviceUrl, req.params.conversationId); + + const conversation = getConversation(req.params.conversationId, conversationInitRequired); + + if (conversation) { + conversation.history.push(activity); + fetch(botUrl, { + method: 'POST', + body: JSON.stringify(activity), + headers: { + 'Content-Type': 'application/json' } - - console.warn('/v3/directline/conversations/:conversationId not implemented'); - }); + }).then(response => { + res.status(response.status).json({ id: activity.id }); + }); + } else { + // Conversation was never initialized + res.status(400).send(); + } + }); - // Gets activities from store (local history array for now) - router.get(`/directline/${botId}/conversations/:conversationId/activities`, (req, res) => { - const watermark = req.query.watermark && req.query.watermark !== 'null' ? Number(req.query.watermark) : 0; + router.post('/v3/directline/conversations/:conversationId/upload', (req, res) => { + console.warn('/v3/directline/conversations/:conversationId/upload not implemented'); + }); + router.get('/v3/directline/conversations/:conversationId/stream', (req, res) => { + console.warn('/v3/directline/conversations/:conversationId/stream not implemented'); + }); - const conversation = getConversation(req.params.conversationId, conversationInitRequired); + // BOT CONVERSATION ENDPOINT - if (conversation) { - // If the bot has pushed anything into the history array - if (conversation.history.length > watermark) { - const activities = conversation.history.slice(watermark); - res.status(200).json({ - activities, - watermark: watermark + activities.length, - }); - } else { - res.status(200).send({ - activities: [], - watermark, - }); - } - } else { - // Conversation was never initialized - res.status(400).send(); - } - }); + router.post('/v3/conversations', (req, res) => { + console.warn('/v3/conversations not implemented'); + }); - // Sends message to bot. Assumes message activities - router.post(`/directline/${botId}/conversations/:conversationId/activities`, (req, res) => { - const incomingActivity = req.body; - // Make copy of activity. Add required fields - const activity = createMessageActivity(incomingActivity, serviceUrl, req.params.conversationId); + router.post('/v3/conversations/:conversationId/activities', (req, res) => { + let activity: IActivity; - const conversation = getConversation(req.params.conversationId, conversationInitRequired); + activity = req.body; + activity.id = uuidv4.v4(); + activity.from = { id: 'id', name: 'Bot' }; - if (conversation) { - conversation.history.push(activity); - fetch(botUrl, { - method: 'POST', - body: JSON.stringify(activity), - headers: { - 'Content-Type': 'application/json', - }, - }).then((response) => { - res.status(response.status).json({ id: activity.id }); - }); - } else { - // Conversation was never initialized - res.status(400).send(); - } - }); + const conversation = getConversation(req.params.conversationId, conversationInitRequired); + if (conversation) { + conversation.history.push(activity); + res.status(200).send(); + } else { + // Conversation was never initialized + res.status(400).send(); + } + }); - router.post('/v3/directline/conversations/:conversationId/upload', (req, res) => { console.warn('/v3/directline/conversations/:conversationId/upload not implemented'); }); - router.get('/v3/directline/conversations/:conversationId/stream', (req, res) => { console.warn('/v3/directline/conversations/:conversationId/stream not implemented'); }); + router.post('/v3/conversations/:conversationId/activities/:activityId', (req, res) => { + let activity: IActivity; - // BOT CONVERSATION ENDPOINT + activity = req.body; + activity.id = uuidv4.v4(); + activity.from = { id: 'id', name: 'Bot' }; - router.post('/v3/conversations', (req, res) => { console.warn('/v3/conversations not implemented'); }); + const conversation = getConversation(req.params.conversationId, conversationInitRequired); + if (conversation) { + conversation.history.push(activity); + res.status(200).send(); + } else { + // Conversation was never initialized + res.status(400).send(); + } + }); - router.post('/v3/conversations/:conversationId/activities', (req, res) => { - let activity: IActivity; + router.get('/v3/conversations/:conversationId/members', (req, res) => { + console.warn('/v3/conversations/:conversationId/members not implemented'); + }); + router.get('/v3/conversations/:conversationId/activities/:activityId/members', (req, res) => { + console.warn('/v3/conversations/:conversationId/activities/:activityId/members'); + }); - activity = req.body; - activity.id = uuidv4.v4(); - activity.from = { id: 'id', name: 'Bot' }; + // BOTSTATE ENDPOINT - const conversation = getConversation(req.params.conversationId, conversationInitRequired); - if (conversation) { - conversation.history.push(activity); - res.status(200).send(); - } else { - // Conversation was never initialized - res.status(400).send(); - } - }); + router.get('/v3/botstate/:channelId/users/:userId', (req, res) => { + console.log('Called GET user data'); + getBotData(req, res); + }); - router.post('/v3/conversations/:conversationId/activities/:activityId', (req, res) => { - let activity: IActivity; + router.get('/v3/botstate/:channelId/conversations/:conversationId', (req, res) => { + console.log('Called GET conversation data'); + getBotData(req, res); + }); - activity = req.body; - activity.id = uuidv4.v4(); - activity.from = { id: 'id', name: 'Bot' }; + router.get('/v3/botstate/:channelId/conversations/:conversationId/users/:userId', (req, res) => { + console.log('Called GET private conversation data'); + getBotData(req, res); + }); - const conversation = getConversation(req.params.conversationId, conversationInitRequired); - if (conversation) { - conversation.history.push(activity); - res.status(200).send(); - } else { - // Conversation was never initialized - res.status(400).send(); - } - }); + router.post('/v3/botstate/:channelId/users/:userId', (req, res) => { + console.log('Called POST setUserData'); + setUserData(req, res); + }); - router.get('/v3/conversations/:conversationId/members', (req, res) => { console.warn('/v3/conversations/:conversationId/members not implemented'); }); - router.get('/v3/conversations/:conversationId/activities/:activityId/members', (req, res) => { console.warn('/v3/conversations/:conversationId/activities/:activityId/members'); }); + router.post('/v3/botstate/:channelId/conversations/:conversationId', (req, res) => { + console.log('Called POST setConversationData'); + setConversationData(req, res); + }); - // BOTSTATE ENDPOINT + router.post('/v3/botstate/:channelId/conversations/:conversationId/users/:userId', (req, res) => { + setPrivateConversationData(req, res); + }); - router.get('/v3/botstate/:channelId/users/:userId', (req, res) => { - console.log('Called GET user data'); - getBotData(req, res); - }); + router.delete('/v3/botstate/:channelId/users/:userId', (req, res) => { + console.log('Called DELETE deleteStateForUser'); + deleteStateForUser(req, res); + }); - router.get('/v3/botstate/:channelId/conversations/:conversationId', (req, res) => { - console.log(('Called GET conversation data')); - getBotData(req, res); - }); - - router.get('/v3/botstate/:channelId/conversations/:conversationId/users/:userId', (req, res) => { - console.log('Called GET private conversation data'); - getBotData(req, res); - }); - - router.post('/v3/botstate/:channelId/users/:userId', (req, res) => { - console.log('Called POST setUserData'); - setUserData(req, res); - }); - - router.post('/v3/botstate/:channelId/conversations/:conversationId', (req, res) => { - console.log('Called POST setConversationData'); - setConversationData(req, res); - }); - - router.post('/v3/botstate/:channelId/conversations/:conversationId/users/:userId', (req, res) => { - setPrivateConversationData(req, res); - }); - - router.delete('/v3/botstate/:channelId/users/:userId', (req, res) => { - console.log('Called DELETE deleteStateForUser'); - deleteStateForUser(req, res); - }); - - return router; + return router; }; /** @@ -212,114 +229,132 @@ export const getRouter = (serviceUrl: string, botUrl: string, conversationInitRe * @param conversationInitRequired Requires that a conversation is initialized before it is accessed, returning a 400 * when not the case. If set to false, a new conversation reference is created on the fly. This is true by default. */ -export const initializeRoutes = (app: express.Express, port: number, botUrl: string, conversationInitRequired = true, botId) => { - conversationsCleanup(); +export const initializeRoutes = ( + app: express.Express, + port: number, + botUrl: string, + conversationInitRequired = true, + botId +) => { + conversationsCleanup(); - const directLineEndpoint = `http://127.0.0.1:${port}`; - const router = getRouter(directLineEndpoint, botUrl, conversationInitRequired, botId); - - app.use(router); - console.log(`Routing messages to bot on ${botUrl}`); + const directLineEndpoint = `http://127.0.0.1:${port}`; + const router = getRouter(directLineEndpoint, botUrl, conversationInitRequired, botId); + app.use(router); }; const getConversation = (conversationId: string, conversationInitRequired: boolean) => { - - // Create conversation on the fly when needed and init not required - if (!conversations[conversationId] && !conversationInitRequired) { - conversations[conversationId] = { - conversationId, - history: [], - }; - } - return conversations[conversationId]; + // Create conversation on the fly when needed and init not required + if (!conversations[conversationId] && !conversationInitRequired) { + conversations[conversationId] = { + conversationId, + history: [] + }; + } + return conversations[conversationId]; }; const getBotDataKey = (channelId: string, conversationId: string, userId: string) => { - return `$${channelId || '*'}!${conversationId || '*'}!${userId || '*'}`; + return `$${channelId || '*'}!${conversationId || '*'}!${userId || '*'}`; }; const setBotData = (channelId: string, conversationId: string, userId: string, incomingData: IBotData): IBotData => { - const key = getBotDataKey(channelId, conversationId, userId); - const newData: IBotData = { - eTag: new Date().getTime().toString(), - data: incomingData.data, - }; + const key = getBotDataKey(channelId, conversationId, userId); + const newData: IBotData = { + eTag: new Date().getTime().toString(), + data: incomingData.data + }; - if (incomingData) { - botDataStore[key] = newData; - } else { - delete botDataStore[key]; - newData.eTag = '*'; - } + if (incomingData) { + botDataStore[key] = newData; + } else { + delete botDataStore[key]; + newData.eTag = '*'; + } - return newData; + return newData; }; const getBotData = (req: express.Request, res: express.Response) => { - const key = getBotDataKey(req.params.channelId, req.params.conversationId, req.params.userId); - console.log('Data key: ' + key); + const key = getBotDataKey(req.params.channelId, req.params.conversationId, req.params.userId); + console.log('Data key: ' + key); - res.status(200).send(botDataStore[key] || { data: null, eTag: '*' }); + res.status(200).send(botDataStore[key] || { data: null, eTag: '*' }); }; const setUserData = (req: express.Request, res: express.Response) => { - res.status(200).send(setBotData(req.params.channelId, req.params.conversationId, req.params.userId, req.body)); + res.status(200).send(setBotData(req.params.channelId, req.params.conversationId, req.params.userId, req.body)); }; const setConversationData = (req: express.Request, res: express.Response) => { - res.status(200).send(setBotData(req.params.channelId, req.params.conversationId, req.params.userId, req.body)); + res.status(200).send(setBotData(req.params.channelId, req.params.conversationId, req.params.userId, req.body)); }; const setPrivateConversationData = (req: express.Request, res: express.Response) => { - res.status(200).send(setBotData(req.params.channelId, req.params.conversationId, req.params.userId, req.body)); + res.status(200).send(setBotData(req.params.channelId, req.params.conversationId, req.params.userId, req.body)); }; -export const start = (server, botId)=>{ - const port = GBConfigService.getServerPort(); - initializeRoutes(server, Number(port), `http://127.0.0.1:${port}/api/messages/${botId}`, null, botId); -} +export const start = (server, botId) => { + const port = GBConfigService.getServerPort(); + initializeRoutes(server, Number(port), `http://127.0.0.1:${port}/api/messages/${botId}`, null, botId); + + if (botId === 'default') { + initializeRoutes(server, Number(port), `http://127.0.0.1:${port}/api/messages`, null, botId); + } +}; const deleteStateForUser = (req: express.Request, res: express.Response) => { - Object.keys(botDataStore) - .forEach((key) => { - if (key.endsWith(`!{req.query.userId}`)) { - delete botDataStore[key]; - } - }); - res.status(200).send(); + Object.keys(botDataStore).forEach(key => { + if (key.endsWith(`!{req.query.userId}`)) { + delete botDataStore[key]; + } + }); + res.status(200).send(); }; // CLIENT ENDPOINT HELPERS -const createMessageActivity = (incomingActivity: IMessageActivity, serviceUrl: string, conversationId: string): IMessageActivity => { - return { ...incomingActivity, channelId: 'emulator', serviceUrl, conversation: { id: conversationId }, id: uuidv4.v4() }; +const createMessageActivity = ( + incomingActivity: IMessageActivity, + serviceUrl: string, + conversationId: string +): IMessageActivity => { + return { + ...incomingActivity, + channelId: 'emulator', + serviceUrl, + conversation: { id: conversationId }, + id: uuidv4.v4() + }; }; const createConversationUpdateActivity = (serviceUrl: string, conversationId: string): IConversationUpdateActivity => { - const activity: IConversationUpdateActivity = { - type: 'conversationUpdate', - channelId: 'emulator', - serviceUrl, - conversation: { id: conversationId }, - id: uuidv4.v4(), - membersAdded: [], - membersRemoved: [], - from: { id: 'offline-directline', name: 'Offline Directline Server' }, - }; - return activity; + const activity: IConversationUpdateActivity = { + type: 'conversationUpdate', + channelId: 'emulator', + serviceUrl, + conversation: { id: conversationId }, + id: uuidv4.v4(), + membersAdded: [], + membersRemoved: [], + from: { id: 'offline-directline', name: 'Offline Directline Server' } + }; + return activity; }; const conversationsCleanup = () => { - setInterval(() => { - const expiresTime = moment().subtract(expiresIn, 'seconds'); - Object.keys(conversations).forEach((conversationId) => { - if (conversations[conversationId].history.length > 0) { - const lastTime = moment(conversations[conversationId].history[conversations[conversationId].history.length - 1].localTimestamp); - if (lastTime < expiresTime) { - delete conversations[conversationId]; - console.log('deleted cId: ' + conversationId); - } - } - }); - }, conversationsCleanupInterval); + setInterval(() => { + const expiresTime = moment().subtract(expiresIn, 'seconds'); + Object.keys(conversations).forEach(conversationId => { + if (conversations[conversationId].history.length > 0) { + const lastTime = moment( + conversations[conversationId].history[conversations[conversationId].history.length - 1].localTimestamp + ); + if (lastTime < expiresTime) { + delete conversations[conversationId]; + console.log('deleted cId: ' + conversationId); + } + } + }); + }, conversationsCleanupInterval); }; diff --git a/src/app.ts b/src/app.ts index 3b968706..6c7f1b11 100644 --- a/src/app.ts +++ b/src/app.ts @@ -122,10 +122,7 @@ export class GBServer { }); process.on('uncaughtException', (err, p) => { - GBLogEx.error( - 0, - `GBEXCEPTION: ${GBUtil.toYAML(err)}` - ); + GBLogEx.error(0, `GBEXCEPTION: ${GBUtil.toYAML(err)}`); }); process.on('unhandledRejection', (err, p) => { @@ -138,10 +135,7 @@ export class GBServer { } if (!bypass) { - GBLogEx.error( - 0, - `GBREJECTION: ${GBUtil.toYAML(err)}` - ); + GBLogEx.error(0, `GBREJECTION: ${GBUtil.toYAML(err)}`); } }); @@ -186,7 +180,6 @@ export class GBServer { // Creates a boot instance or load it from storage. - if (GBConfigService.get('STORAGE_SERVER')) { azureDeployer = await AzureDeployerService.createInstance(deployer); await core.initStorage(); @@ -245,17 +238,20 @@ export class GBServer { } } - if (!GBConfigService.get('STORAGE_NAME')) { + const conversationalService: GBConversationalService = new GBConversationalService(core); + const adminService: GBAdminService = new GBAdminService(core); + const minService: GBMinService = new GBMinService(core, conversationalService, adminService, deployer); + GBServer.globals.minService = minService; + + // Just sync if not using LOAD_ONLY. + + if (!GBConfigService.get('STORAGE_NAME') && !process.env.LOAD_ONLY) { await core['ensureFolders'](instances, deployer); } GBServer.globals.bootInstance = instances[0]; // Builds minimal service infrastructure. - const conversationalService: GBConversationalService = new GBConversationalService(core); - const adminService: GBAdminService = new GBAdminService(core); - const minService: GBMinService = new GBMinService(core, conversationalService, adminService, deployer); - GBServer.globals.minService = minService; await minService.buildMin(instances); server.all('*', async (req, res, next) => { @@ -291,6 +287,8 @@ export class GBServer { GBLogEx.info(0, `The Bot Server is in RUNNING mode...`); + await minService.startSimpleTest(GBServer.globals.minBoot); + // Opens Navigator. if (process.env.DEV_OPEN_BROWSER) { diff --git a/src/util.ts b/src/util.ts index ebdc46c8..488b9ddb 100644 --- a/src/util.ts +++ b/src/util.ts @@ -76,8 +76,8 @@ export class GBUtil { } }; if (!GBConfigService.get('STORAGE_NAME')) { - config['spec'].url = `http://127.0.0.1:${GBConfigService.getServerPort()}/api/messages`, - config['spec'].servers = [{ url: `http://127.0.0.1:${GBConfigService.getServerPort()}/api/messages` }]; + config['spec'].url = `http://127.0.0.1:${GBConfigService.getServerPort()}/api/messages/${min.botId}`, + config['spec'].servers = [{ url: `http://127.0.0.1:${GBConfigService.getServerPort()}/api/messages/${min.botId}` }]; config['spec'].openapi = '3.0.0'; delete config['spec'].host; delete config['spec'].swagger;