fix(all): TRUE multicloud.

This commit is contained in:
Rodrigo Rodriguez 2024-08-21 13:09:50 -03:00
parent d5e47de73e
commit a98323dfd1
9 changed files with 437 additions and 430 deletions

View file

@ -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<boolean>(
min.instance,
'Synchronize Database',
false
);
tableDef.forEach(async (t) => {
const shouldSync = min.core.getParam<boolean>(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<string>(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,

View file

@ -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}...`);
}

View file

@ -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;

View file

@ -242,7 +242,7 @@ export class GBCoreService implements IGBCoreService {
* Loads all items to start several listeners.
*/
public async loadInstances(): Promise<IGBInstance[]> {
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);

View file

@ -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.

View file

@ -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) {

View file

@ -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);
};

View file

@ -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) {

View file

@ -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;