diff --git a/package.json b/package.json index f8a1cb21..bce9a400 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "botlib": "3.0.11", "c3-chart-maker": "0.2.8", "cd": "0.3.3", - "chatgpt": "2.4.2", + "chatgpt": "^2.4.2", "chrome-remote-interface": "0.31.3", "cli-progress": "3.11.2", "cli-spinner": "0.2.10", @@ -128,7 +128,7 @@ "moment": "1.3.0", "ms-rest-azure": "3.0.0", "nexmo": "2.9.1", - "ngrok": "4.3.3", + "ngrok": "5.0.0-beta.2", "node-cron": "3.0.2", "node-html-parser": "6.1.5", "node-nlp": "4.26.1", @@ -136,7 +136,7 @@ "npm": "9.6.1", "open": "8.4.0", "open-docxtemplater-image-module": "1.0.3", - "openai": "3.3.0", + "openai": "4.6.0", "pdf-extraction": "1.0.2", "pdf-to-png-converter": "3.1.0", "pdfkit": "0.13.0", @@ -180,7 +180,7 @@ "vm2-process": "2.1.1", "walk-promise": "0.2.0", "washyourmouthoutwithsoap": "1.0.2", - "whatsapp-web.js": "git://github.com/pedroslopez/whatsapp-web.js#abac063b779570729476cf42e29dc694e5345ca6", + "whatsapp-web.js": "git://github.com/pedroslopez/whatsapp-web.js#b671b0c708f0bc6187ccec078a0f3e9c08db4bce", "winston": "3.8.2", "winston-logs-display": "1.0.0", "ws": "8.12.1", diff --git a/packages/admin.gbapp/dialogs/AdminDialog.ts b/packages/admin.gbapp/dialogs/AdminDialog.ts index e567e4da..0422987a 100644 --- a/packages/admin.gbapp/dialogs/AdminDialog.ts +++ b/packages/admin.gbapp/dialogs/AdminDialog.ts @@ -421,7 +421,7 @@ export class AdminDialog extends IGBDialog { min.adminService.setValue(min.instance.instanceId, 'AntiCSRFAttackState', state); - const redirectUri = urlJoin(min.instance.botEndpoint, min.instance.botId, '/token'); + const redirectUri = urlJoin(process.env.BOT_URL, min.instance.botId, '/token'); const url = `https://login.microsoftonline.com/${step.activeDialog.state.authenticatorTenant}/oauth2/authorize?client_id=${min.instance.marketplaceId}&response_type=code&redirect_uri=${redirectUri}&scope=https://graph.microsoft.com/.default&state=${state}&response_mode=query`; await min.conversationalService.sendText(min, step, Messages[locale].consent(url)); diff --git a/packages/admin.gbapp/services/GBAdminService.ts b/packages/admin.gbapp/services/GBAdminService.ts index ef5aaa99..a2a45048 100644 --- a/packages/admin.gbapp/services/GBAdminService.ts +++ b/packages/admin.gbapp/services/GBAdminService.ts @@ -170,6 +170,11 @@ export class GBAdminService implements IGBAdminService { // .gbot packages are handled using storage API, so no download // of local resources is required. const gbai = DialogKeywords.getGBAIPath(min.instance.botId); + + if (packageType === 'gbkb') { + await deployer['cleanupPackage'](min.instance, packageName); + } + await deployer['downloadFolder'](min, Path.join('work', `${gbai}`), Path.basename(localFolder)); @@ -178,9 +183,10 @@ export class GBAdminService implements IGBAdminService { } public static async rebuildIndexPackageCommand(min: GBMinInstance, deployer: GBDeployer) { const service = await AzureDeployerService.createInstance(deployer); + const searchIndex = min.instance.searchIndex ? min.instance.searchIndex : GBServer.globals.minBoot.instance.searchIndex; await deployer.rebuildIndex( min.instance, - service.getKBSearchSchema(min.instance.searchIndex) + service.getKBSearchSchema(searchIndex) ); } diff --git a/packages/azuredeployer.gbapp/services/AzureDeployerService.ts b/packages/azuredeployer.gbapp/services/AzureDeployerService.ts index 40401757..dd145f54 100644 --- a/packages/azuredeployer.gbapp/services/AzureDeployerService.ts +++ b/packages/azuredeployer.gbapp/services/AzureDeployerService.ts @@ -266,6 +266,11 @@ export class AzureDeployerService implements IGBInstallationDeployer { } public async updateBotProxy(botId: string, group: string, endpoint: string) { + if (!await this.botExists(botId)) { + GBLog.error(`Bot ${botId} does not exist on cloud.`); + + return; + } const baseUrl = `https://management.azure.com/`; const username = GBConfigService.get('CLOUD_USERNAME'); const password = GBConfigService.get('CLOUD_PASSWORD'); @@ -455,7 +460,7 @@ export class AzureDeployerService implements IGBInstallationDeployer { }; GBLog.info(`Deploying Bot...`); - instance.botEndpoint = this.defaultEndPoint; + instance.botEndpoint = 'TODO: remove this column.'; instance = await this.internalDeployBot( instance, @@ -565,9 +570,7 @@ export class AzureDeployerService implements IGBInstallationDeployer { luisAppIds: [nlpAppId], luisKey: nlpKey, msaAppId: appId, - msaAppPassword: appPassword, - enabledChannels: ['webchat', 'skype'], //, "facebook"], - configuredChannels: ['webchat', 'skype'] //, "facebook"] + msaAppPassword: appPassword } }; diff --git a/packages/basic.gblib/index.ts b/packages/basic.gblib/index.ts index 78bad742..b9aae1b0 100644 --- a/packages/basic.gblib/index.ts +++ b/packages/basic.gblib/index.ts @@ -60,7 +60,7 @@ export function createKoaHttpServer( app.use(koaBody.koaBody({ multipart: true })); app.use(middleware); const server = app.listen(port); - const SERVER_TIMEOUT = 60 * 60 * 24 * 1000; // Equals to client RPC set . + const SERVER_TIMEOUT = 60 * 60 * 24 * 1000; // Equals to client RPC set. server.timeout = SERVER_TIMEOUT; return { diff --git a/packages/basic.gblib/services/DialogKeywords.ts b/packages/basic.gblib/services/DialogKeywords.ts index 7cf8e71f..55a4b9d7 100644 --- a/packages/basic.gblib/services/DialogKeywords.ts +++ b/packages/basic.gblib/services/DialogKeywords.ts @@ -1230,7 +1230,9 @@ export class DialogKeywords { GBLog.info(`BASIC: Markdown file ${filename} not found on database for ${min.instance.botId}.`); } - await min.conversationalService['playMarkdown'](min, md, DialogKeywords.getChannel(), mobile); + await min.conversationalService['playMarkdown'](min, md, DialogKeywords.getChannel(), null, mobile); + + return; } else { const gbaiName = DialogKeywords.getGBAIPath(min.botId, `gbkb`); @@ -1246,12 +1248,12 @@ export class DialogKeywords { } if (!url) { - const imageData = await (await fetch(url)).arrayBuffer(); + const ext = mime.extension(Path.extname(filename.localName)); - + // Prepare a cache to be referenced by Bot Framework. - - let buf: any = Buffer.from(imageData); + + const buf = Fs.readFileSync(filename); const gbaiName = DialogKeywords.getGBAIPath(min.botId); const localName = Path.join('work', gbaiName, 'cache', `tmp${GBAdminService.getRndReadableIdentifier()}.${ext}`); Fs.writeFileSync(localName, buf, { encoding: null }); diff --git a/packages/basic.gblib/services/GBVMService.ts b/packages/basic.gblib/services/GBVMService.ts index d217c042..bb8aa593 100644 --- a/packages/basic.gblib/services/GBVMService.ts +++ b/packages/basic.gblib/services/GBVMService.ts @@ -89,6 +89,7 @@ export class GBVMService extends GBService { const docxStat = Fs.statSync(urlJoin(folder, wordFile)); const interval = 3000; // If compiled is older 30 seconds, then recompile. let writeVBS = true; + if (Fs.existsSync(fullVbsFile)) { const vbsStat = Fs.statSync(fullVbsFile); if (docxStat['mtimeMs'] < vbsStat['mtimeMs'] + interval) { @@ -102,6 +103,8 @@ export class GBVMService extends GBService { if (writeVBS) { let text = await this.getTextFromWord(folder, wordFile); + // Pre process SET SCHEDULE calls. + const schedule = GBVMService.getSetScheduleKeywordArgs(text); const s = new ScheduleServices(); if (schedule) { @@ -110,10 +113,14 @@ export class GBVMService extends GBService { await s.deleteScheduleIfAny(min, mainName); } text = text.replace(/^\s*SET SCHEDULE (.*)/gim, ''); + + // Write VBS file without pragma keywords. + Fs.writeFileSync(urlJoin(folder, vbsFile), text); } // Process node_modules install. + const node_modules = urlJoin(process.env.PWD, folder, 'node_modules'); if (!Fs.existsSync(node_modules)) { const packageJson = ` @@ -143,7 +150,7 @@ export class GBVMService extends GBService { const fullFilename = urlJoin(folder, filename); if (process.env.DEV_HOTSWAP) { Fs.watchFile(fullFilename, async () => { - await this.translateBASIC(fullFilename, min); + await this.translateBASIC(mainName, fullFilename, min); const parsedCode: string = Fs.readFileSync(jsfile, 'utf8'); min.sandBoxMap[mainName.toLowerCase().trim()] = parsedCode; }); @@ -156,17 +163,17 @@ export class GBVMService extends GBService { const jsStat = Fs.statSync(jsfile); const interval = 30000; // If compiled is older 30 seconds, then recompile. if (compiledAt.isFile() && compiledAt['mtimeMs'] > jsStat['mtimeMs'] + interval) { - await this.translateBASIC(fullFilename, min); + await this.translateBASIC(mainName, fullFilename, min); } } else { - await this.translateBASIC(fullFilename, min); + await this.translateBASIC(mainName, fullFilename, min); } const parsedCode: string = Fs.readFileSync(jsfile, 'utf8'); min.sandBoxMap[mainName.toLowerCase().trim()] = parsedCode; return filename; } - public async translateBASIC(filename: any, min: GBMinInstance) { + public async translateBASIC(mainName, filename: any, min: GBMinInstance) { // Converts General Bots BASIC into regular VBS let basicCode: string = Fs.readFileSync(filename, 'utf8'); @@ -191,10 +198,16 @@ export class GBVMService extends GBService { } } while (include); - let { code, jsonMap } = await this.convert(basicCode); + let { code, map, metadata } = await this.convert(mainName, basicCode); + + // Generates function JSON metadata to be used later. + + const jsonFile = `${filename}.json`; + Fs.writeFileSync(jsonFile, JSON.stringify(metadata)); + const mapFile = `${filename}.map`; - Fs.writeFileSync(mapFile, JSON.stringify(jsonMap)); + Fs.writeFileSync(mapFile, JSON.stringify(map)); // Run JS into the GB context. @@ -211,14 +224,14 @@ export class GBVMService extends GBService { // Unmarshalls Local variables from server VM. - let pid = this.pid; + const pid = this.pid; let id = this.id; let username = this.username; let mobile = this.mobile; let from = this.from; - let channel = this.channel; - let ENTER = this.ENTER; - let headers = this.headers; + const channel = this.channel; + const ENTER = this.ENTER; + const headers = this.headers; let data = this.data; let list = this.list; let httpUsername = this.httpUsername; @@ -226,9 +239,9 @@ export class GBVMService extends GBService { let today = this.today; let now = this.now; let page = null; - let files = []; + const files = []; let col = 1; - let index = 1 + let index = 1; // Makes objects in BASIC insensitive. @@ -236,7 +249,7 @@ export class GBVMService extends GBService { if (!listOrRow) { - return listOrRow + return listOrRow; }; const lowercase = (oldKey) => typeof oldKey === 'string' ? oldKey.toLowerCase() : oldKey; @@ -342,27 +355,70 @@ export class GBVMService extends GBService { return text; } + public static getMetadata(mainName: string, propertiesText, description) { + const properties = []; + + if (propertiesText) { + const getType = asClause => { + if (asClause.indexOf('AS STRING')) { + return 'string'; + } else { + return 'enum'; + } + }; + + for (let i = 0; i < propertiesText.length; i++) { + const propertiesExp = propertiesText[i]; + const t = getType(propertiesExp[2]); + let element = {}; + element['type'] = t; + + if (t === 'enum') { + element['enum'] = propertiesExp[2]; + } else if (t === 'string') { + element['description'] = propertiesExp[2]; + } + + properties.push(element); + } + } + + let json = { + name: `${mainName}`, + description: description ? description[1] : '', + parameters: { + type: 'object', + properties: properties ? properties : [] + } + }; + + return json; + } + /** * Converts General Bots BASIC * * * @param code General Bots BASIC */ - public async convert(code: string) { + public async convert(mainName: string, code: string) { + // Start and End of VB2TS tags of processing. code = process.env.ENABLE_AUTH ? `hear GBLogExin as login\n${code}` : code; var lines = code.split('\n'); const keywords = KeywordsExpressions.getKeywords(); let current = 41; - const map = {}; + const map = {}; + let properties = []; + let description; for (let i = 1; i <= lines.length; i++) { let line = lines[i - 1]; // Remove lines before statments. - line = line.replace(/^\s*\d+\s*/gi,''); + line = line.replace(/^\s*\d+\s*/gi, ''); for (let j = 0; j < keywords.length; j++) { line = line.replace(keywords[j][0], keywords[j][1]); @@ -374,10 +430,27 @@ export class GBVMService extends GBService { current = current + (add ? add : 0); map[i] = current; lines[i - 1] = line; + + // Pre-process static KEYWORDS. + + const params = /^\s*PARAM\s*(.*)\s*AS\s*(.*)/gim; + const param = params.exec(line); + if (param) { + properties.push(param); + } + + const descriptionKeyword = /^\s*DESCRIPTION\s*\"(.*)\"/gim; + let descriptionReg = descriptionKeyword.exec(line); + if (descriptionReg) { + description = descriptionReg[1]; + } } code = `${lines.join('\n')}\n`; - return { code, jsonMap: map }; + + let metadata = GBVMService.getMetadata(mainName, properties, description); + + return { code, map, metadata }; } /** @@ -389,7 +462,8 @@ export class GBVMService extends GBService { step, user: GuaribasUser, deployer: GBDeployer, - debug: boolean = false + debug: boolean = false, + params = [] ) { // Creates a class DialogKeywords which is the *this* pointer // in BASIC. @@ -414,18 +488,25 @@ export class GBVMService extends GBService { } } + // Adds params as variables to be added later as global objects.. + + let keys = Object.keys(params); + for (let j = 0; j < keys.length; j++) { + variables[keys[j]] = params[keys[j]]; + } + const botId = min.botId; const path = DialogKeywords.getGBAIPath(min.botId, `gbdialog`); const gbdialogPath = urlJoin(process.cwd(), 'work', path); const scriptPath = urlJoin(gbdialogPath, `${text}.js`); let code = min.sandBoxMap[text]; - const channel = step? step.context.activity.channelId : 'web'; + const channel = step ? step.context.activity.channelId : 'web'; const pid = GBVMService.createProcessInfo(user, min, channel); const dk = new DialogKeywords(); const sys = new SystemKeywords(); - await dk.setFilter ({pid: pid, value: null }); - + await dk.setFilter({ pid: pid, value: null }); + sandbox['variables'] = variables; sandbox['id'] = sys.getRandomId(); sandbox['username'] = await dk.userName({ pid }); @@ -439,7 +520,7 @@ export class GBVMService extends GBService { sandbox['httpPs'] = ''; sandbox['pid'] = pid; sandbox['contentLocale'] = contentLocale; - sandbox['callTimeout'] = 60 * 60 * 24 * 1000; + sandbox['callTimeout'] = 60 * 60 * 24 * 1000; sandbox['channel'] = channel; sandbox['today'] = await dk.getToday({ pid }); sandbox['now'] = await dk.getNow({ pid }); @@ -489,7 +570,6 @@ export class GBVMService extends GBService { } } catch (error) { throw new Error(`BASIC RUNTIME ERR: ${error.message ? error.message : error}\n Stack:${error.stack}`); - } finally { } return result; diff --git a/packages/basic.gblib/services/KeywordsExpressions.ts b/packages/basic.gblib/services/KeywordsExpressions.ts index b6dad81e..b1e2ebce 100644 --- a/packages/basic.gblib/services/KeywordsExpressions.ts +++ b/packages/basic.gblib/services/KeywordsExpressions.ts @@ -331,7 +331,7 @@ export class KeywordsExpressions { /^\s*(.*)\=\s*(REWRITE)(\s*)(.*)/gim, ($0, $1, $2, $3, $4) => { const params = this.getParams($4, ['text']); - return `await sys.rewrite ({pid: pid, ${params}})`; + return `${$1} = await sys.rewrite ({pid: pid, ${params}})`; } ]; diff --git a/packages/core.gbapp/services/GBConversationalService.ts b/packages/core.gbapp/services/GBConversationalService.ts index 235cf75f..a6d68b6d 100644 --- a/packages/core.gbapp/services/GBConversationalService.ts +++ b/packages/core.gbapp/services/GBConversationalService.ts @@ -308,10 +308,10 @@ export class GBConversationalService { step: GBDialogStep, mobile: string, url: string, - caption: string, + caption: string, channel: string ): Promise { - return await this.sendFile(min, step, mobile, url , caption); + return await this.sendFile(min, step, mobile, url, caption); } public async sendFile( @@ -393,23 +393,23 @@ export class GBConversationalService { } } else { if (min.instance.smsKey && min.instance.smsSecret) { - return new Promise((resolve: any, reject: any): any => { - const nexmo = new Nexmo({ - apiKey: min.instance.smsKey, - apiSecret: min.instance.smsSecret + return new Promise((resolve: any, reject: any): any => { + const nexmo = new Nexmo({ + apiKey: min.instance.smsKey, + apiSecret: min.instance.smsSecret + }); + // tslint:disable-next-line:no-unsafe-any + nexmo.message.sendSms(min.instance.smsServiceNumber, mobile, text, {}, (err, data) => { + const message = data.messages ? data.messages[0] : {}; + if (err || message['error-text']) { + GBLog.error(`BASIC: error sending SMS to ${mobile}: ${message['error-text']}`); + reject(message['error-text']); + } else { + resolve(data); + } + }); }); - // tslint:disable-next-line:no-unsafe-any - nexmo.message.sendSms(min.instance.smsServiceNumber, mobile, text, {}, (err, data) => { - const message = data.messages ? data.messages[0] : {}; - if (err || message['error-text']) { - GBLog.error(`BASIC: error sending SMS to ${mobile}: ${message['error-text']}`); - reject(message['error-text']); - } else { - resolve(data); - } - }); - }); - } + } } } @@ -462,7 +462,7 @@ export class GBConversationalService { return new Promise(async (resolve, reject) => { try { const oggFile = new Readable(); - oggFile._read = () => {}; // _read is required but you can noop it + oggFile._read = () => { }; // _read is required but you can noop it oggFile.push(buffer); oggFile.push(null); @@ -526,8 +526,12 @@ export class GBConversationalService { }); } - public async playMarkdown(min: GBMinInstance, answer: string, channel: string, step: GBDialogStep, mobile: string) { - const user = step ? await min.userProfile.get(step.context, {}) : null; + public async playMarkdown(min: GBMinInstance, answer: string, channel: string, + step: GBDialogStep, mobile: string) { + + const sec = new SecService(); + const user = await sec.getUserFromSystemId(mobile ? mobile : step.context.activity.from.id); + let text = answer; // Calls language translator. @@ -902,7 +906,7 @@ export class GBConversationalService { const key = min.core.getParam(min.instance, 'spellcheckerKey', null); if (key) { - text = text.charAt(0).toUpperCase() + text.slice(1); + text = text.charAt(0).toUpperCase() + text.slice(1); const data = await AzureText.getSpelledText(key, text); if (data !== text) { GBLog.info(`Spelling>: ${data}`); @@ -914,7 +918,7 @@ export class GBConversationalService { } public async translate(min: GBMinInstance, text: string, language: string): Promise { - + const translatorEnabled = () => { if (min.instance.params) { const params = JSON.parse(min.instance.params); @@ -994,7 +998,7 @@ export class GBConversationalService { } } - public static async handleText (min, user, step, text: string){ + public static async handleText(min, user, step, text: string) { const sec = new SecService(); text = text.replace(/<([^>]+?)([^>]*?)>(.*?)<\/\1>/gi, ''); @@ -1059,7 +1063,13 @@ export class GBConversationalService { // If it is a group, spells and sends them back. const group = step.context.activity['group']; - if (textProcessed !== text && group) { + + const groupSpell = group ? await min.core.getParam( + min.instance, + 'Group Spell', + false) : false; + + if (textProcessed !== text && group && groupSpell) { await min.whatsAppDirectLine.sendToDevice(group, `Spell: ${text}`); } @@ -1068,14 +1078,14 @@ export class GBConversationalService { let locale = min.core.getParam( min.instance, 'Default User Language', - GBConfigService.get('DEFAULT_USER_LANGUAGE') - ); + GBConfigService.get('DEFAULT_USER_LANGUAGE')); + const detectLanguage = min.core.getParam( min.instance, 'Language Detector', - GBConfigService.getBoolean('LANGUAGE_DETECTOR') - ) === 'true'; + false) != false; + locale = user.locale; if (text != '' && detectLanguage && !locale) { locale = await min.conversationalService.getLanguage(min, text); @@ -1121,11 +1131,11 @@ export class GBConversationalService { GBLog.verbose(`Translated text(prompt): ${text}.`); } if (step.activeDialog.state.options['kind'] === 'file') { - + return await step.prompt('attachmentPrompt', {}); } else { await this.sendText(min, step, text); - return await step.prompt('textPrompt', {}); + return await step.prompt('textPrompt', {}); } } @@ -1221,8 +1231,8 @@ export class GBConversationalService { */ public async sendOnConversation(min: GBMinInstance, user: GuaribasUser, message: any) { if (message['buttons'] || message['sections']) { - await min['whatsAppDirectLine'].sendToDevice(user.userSystemId, message, user.conversationReference ); - } else if (user.conversationReference.startsWith('spaces')) { + await min['whatsAppDirectLine'].sendToDevice(user.userSystemId, message, user.conversationReference); + } else if (user.conversationReference.startsWith('spaces')) { await min['googleDirectLine'].sendToDevice(user.userSystemId, null, user.conversationReference, message); } else { const ref = JSON.parse(user.conversationReference); diff --git a/packages/core.gbapp/services/GBCoreService.ts b/packages/core.gbapp/services/GBCoreService.ts index dd1c1231..0469c5eb 100644 --- a/packages/core.gbapp/services/GBCoreService.ts +++ b/packages/core.gbapp/services/GBCoreService.ts @@ -433,6 +433,7 @@ ENDPOINT_UPDATE=true await CollectionUtil.asyncForEach(instances, async instance => { GBLog.info(`Updating bot endpoint for ${instance.botId}...`); try { + await installationDeployer.updateBotProxy( instance.botId, GBConfigService.get('CLOUD_GROUP'), @@ -682,22 +683,20 @@ ENDPOINT_UPDATE=true value = params ? params[name] : defaultValue; } if (typeof defaultValue === 'boolean') { - return new Boolean(value ? value.toString().toLowerCase() === 'true' : defaultValue); + return new Boolean(value ? value.toString().toLowerCase() === 'true' : defaultValue).valueOf(); } if (typeof defaultValue === 'string') { return value ? value : defaultValue; } if (typeof defaultValue === 'number') { - return new Number(value ? value : defaultValue ? defaultValue : 0); + return new Number(value ? value : defaultValue ? defaultValue : 0).valueOf(); } if (instance['dataValues'] && !value) { value = instance['dataValues'][name]; if (value === null) { const minBoot = GBServer.globals.minBoot as any; - if (minBoot.instance && minBoot.instance.datavalues) { - value = minBoot.instance.datavalues[name]; - } + value = minBoot.instance[name]; } } diff --git a/packages/core.gbapp/services/GBDeployer.ts b/packages/core.gbapp/services/GBDeployer.ts index 431a2f9c..85e301a0 100644 --- a/packages/core.gbapp/services/GBDeployer.ts +++ b/packages/core.gbapp/services/GBDeployer.ts @@ -105,10 +105,18 @@ export class GBDeployer implements IGBDeployer { */ public static async internalGetDriveClient(min: GBMinInstance) { let token; - if (min['cacheToken']) { + + // TODO: Add expiration logic. + + if (min['cacheToken'] && null) { return min['cacheToken']; } else { - token = await (min.adminService as any)['acquireElevatedToken'](min.instance.instanceId, true); + + // Get token as root only if the bot does not have + // an custom tenant for retrieving packages. + + token = await (min.adminService as any)['acquireElevatedToken'] + (min.instance.instanceId, min.instance.authenticatorTenant?false:true); const siteId = process.env.STORAGE_SITE_ID; const libraryId = process.env.STORAGE_LIBRARY; @@ -280,7 +288,6 @@ export class GBDeployer implements IGBDeployer { `${publicAddress}/api/messages/${instance.botId}` ); } else { - const botId = GBConfigService.get('BOT_ID'); // Internally create resources on cloud provider. @@ -303,6 +310,7 @@ export class GBDeployer implements IGBDeployer { // 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. @@ -553,7 +561,7 @@ export class GBDeployer implements IGBDeployer { }); } public async deployPackage(min: GBMinInstance, localPath: string) { - // TODO: @alanperdomo: Adjust interface mismatch. + // TODO: Adjust interface mismatch. } /** * Deploys a folder into the bot storage. @@ -655,6 +663,30 @@ export class GBDeployer implements IGBDeployer { } } + /** + * Removes the package local files from cache. + */ + public async cleanupPackage(instance: IGBInstance, packageName: string) { + const path = DialogKeywords.getGBAIPath(instance.botId, null, packageName); + const localFolder = Path.join('work', path); + rimraf.sync(localFolder); + } + + /** + * Removes the package from the storage and local work folders. + */ + public async undeployPackageFromPackageName(instance: IGBInstance, packageName: string) { + // Gets information about the package. + + const packageType = Path.extname(packageName); + const p = await this.getStoragePackageByName(instance.instanceId, packageName); + + const path = DialogKeywords.getGBAIPath(instance.botId, null, packageName); + const localFolder = Path.join('work', path); + + return await this.undeployPackageFromLocalPath(instance, localFolder); + } + /** * Removes the package from the storage and local work folders. */ @@ -677,8 +709,11 @@ export class GBDeployer implements IGBDeployer { const service = new KBService(this.core.sequelize); rimraf.sync(localPath); - return await service.undeployKbFromStorage(instance, this, p.packageId); + if (p){ + await service.undeployKbFromStorage(instance, this, p.packageId); + } + return; case '.gbui': break; @@ -709,19 +744,29 @@ export class GBDeployer implements IGBDeployer { public async rebuildIndex(instance: IGBInstance, searchSchema: any) { // Prepares search. let release; - try { - GBLogEx.info(instance.instanceId, `Acquiring rebuildIndex mutex...`); - release = await GBServer.globals.indexSemaphore.acquire(); - GBLogEx.info(instance.instanceId, `Acquire rebuildIndex done.`); + + // TODO: Semaphore logic. + //try { + GBLogEx.info(instance.instanceId, `rebuildIndex running...`); + // release = await GBServer.globals.indexSemaphore.acquire(); + // GBLogEx.info(instance.instanceId, `Acquire rebuildIndex done.`); + + const key = instance.searchKey ? instance.searchKey : GBServer.globals.minBoot.instance.searchKey; + const searchIndex = instance.searchIndex ? instance.searchIndex : GBServer.globals.minBoot.instance.searchIndex; + const searchIndexer = instance.searchIndexer + ? instance.searchIndexer + : GBServer.globals.minBoot.instance.searchIndexer; + const host = instance.searchHost ? instance.searchHost : GBServer.globals.minBoot.instance.searchHost; + // Prepares search. const search = new AzureSearch( - instance.searchKey, - instance.searchHost, - instance.searchIndex, - instance.searchIndexer + key, + host, + searchIndex, + searchIndexer ); - const connectionString = GBDeployer.getConnectionStringFromInstance(instance); + const connectionString = GBDeployer.getConnectionStringFromInstance(GBServer.globals.minBoot.instance); const dsName = 'gb'; // Removes any previous index. @@ -758,13 +803,13 @@ export class GBDeployer implements IGBDeployer { } await search.createIndex(searchSchema, dsName); - release(); + // release(); GBLogEx.info(instance.instanceId, `Released rebuildIndex mutex.`); - } catch { - if (release) { - release(); - } - } + //} catch { + // if (release) { + // release(); + // } + //} } /** diff --git a/packages/core.gbapp/services/GBImporterService.ts b/packages/core.gbapp/services/GBImporterService.ts index 70122ef6..c6166461 100644 --- a/packages/core.gbapp/services/GBImporterService.ts +++ b/packages/core.gbapp/services/GBImporterService.ts @@ -60,10 +60,18 @@ export class GBImporter { localPath: string, additionalInstance: IGBInstance = null ) { - const settingsJson = JSON.parse(Fs.readFileSync(urlJoin(localPath, 'settings.json'), 'utf8')); - if (botId === undefined) { - botId = settingsJson.botId; + const file = urlJoin(localPath, 'settings.json'); + + let settingsJson = {botId: botId}; + if (Fs.existsSync(file)){ + + settingsJson = JSON.parse(Fs.readFileSync(file, 'utf8')); + if (botId === undefined) { + botId = settingsJson.botId; + } + } + let instance: IGBInstance; if (botId === undefined) { botId = GBConfigService.get('BOT_ID'); @@ -111,10 +119,10 @@ export class GBImporter { localPath: string, settingsJson: any ) { - const packageJson = JSON.parse(Fs.readFileSync(urlJoin(localPath, 'package.json'), 'utf8')); - const servicesJson = JSON.parse(Fs.readFileSync(urlJoin(localPath, 'services.json'), 'utf8')); - - const fullSettingsJson = { ...GBServer.globals.bootInstance, ...packageJson, ...settingsJson, ...servicesJson }; + + const fullSettingsJson = { ...GBServer.globals.bootInstance, ...settingsJson, + description:"General Bot", title:botId + }; if (botId !== undefined) { fullSettingsJson.botId = botId; diff --git a/packages/core.gbapp/services/GBMinService.ts b/packages/core.gbapp/services/GBMinService.ts index 2b013362..bc27440b 100644 --- a/packages/core.gbapp/services/GBMinService.ts +++ b/packages/core.gbapp/services/GBMinService.ts @@ -148,6 +148,11 @@ export class GBMinService { this.deployer = deployer; } + + public async enableAPI(min: GBMinInstance) { + + } + /** * Constructs a new minimal instance for each bot. */ @@ -167,51 +172,8 @@ export class GBMinService { GBServer.globals.server.get('/instances/:botId', this.handleGetInstanceForClient.bind(this)); } - - function getRemoteId(ctx: Koa.Context) { - return '1'; // share a single session for now, real impl could use cookies or some other meaning for HTTP sessions - } - - GBLogEx.info(0, 'Loading General Bots API...'); - - let proxies = {}; - await CollectionUtil.asyncForEach(instances, async instance => { - const proxy = { - dk: new DialogKeywords(), - wa: new WebAutomationServices(), - sys: new SystemKeywords(), - dbg: new DebuggerService(), - img: new ImageProcessingServices() - }; - proxies[instance.botId] = proxy; - }); - - const opts = { - pingSendTimeout: null, - keepAliveTimeout: null, - listeners: { - unsubscribed(subscriptions: number): void {}, - subscribed(subscriptions: number): void {}, - disconnected(remoteId: string, connections: number): void {}, - connected(remoteId: string, connections: number): void {}, - messageIn(...params): void { - params.shift(); - GBLogEx.verbose(0, '[IN] ' + params); - }, - messageOut(...params): void { - params.shift(); - GBLogEx.verbose(0, '[OUT] ' + params); - } - } - }; - - GBServer.globals.server.dk = createRpcServer( - proxies, - createKoaHttpServer(GBVMService.API_PORT, getRemoteId, { prefix: `api/v3` }), - opts - ); - // Calls mountBot event to all bots. + let i = 1; if (instances.length > 1) { @@ -245,6 +207,10 @@ export class GBMinService { this.bar1.stop(); } + // Loads API. + + await this.ensureAPI(); + // Loads schedules. GBLog.info(`Loading SET SCHEDULE entries...`); @@ -292,7 +258,7 @@ export class GBMinService { /** * Unmounts the bot web site (default.gbui) secure domain, if any. */ - public async unloadDomain(instance: IGBInstance) {} + public async unloadDomain(instance: IGBInstance) { } /** * Mount the instance by creating an BOT Framework bot object, @@ -542,7 +508,7 @@ export class GBMinService { authenticationContext.acquireTokenWithAuthorizationCode( req.query.code, - urlJoin(instance.botEndpoint, min.instance.botId, '/token'), + urlJoin(process.env.BOT_URL, min.instance.botId, '/token'), resource, instance.marketplaceId, instance.marketplacePassword, @@ -561,7 +527,7 @@ export class GBMinService { // Inform the home for default .gbui after finishing token retrival. - res.redirect(min.instance.botEndpoint); + res.redirect(process.env.BOT_URL); } } ); @@ -579,9 +545,8 @@ export class GBMinService { min.instance.authenticatorTenant, '/oauth2/authorize' ); - authorizationUrl = `${authorizationUrl}?response_type=code&client_id=${ - min.instance.marketplaceId - }&redirect_uri=${urlJoin(min.instance.botEndpoint, min.instance.botId, 'token')}`; + authorizationUrl = `${authorizationUrl}?response_type=code&client_id=${min.instance.marketplaceId + }&redirect_uri=${urlJoin(process.env.BOT_URL, min.instance.botId, 'token')}`; GBLog.info(`HandleOAuthRequests: ${authorizationUrl}.`); res.redirect(authorizationUrl); }); @@ -772,6 +737,8 @@ export class GBMinService { WhatsappDirectLine.botGroups[min.botId] = group; + const minBoot = GBServer.globals.minBoot as any; + // If there is WhatsApp configuration specified, initialize // infrastructure objects. @@ -788,7 +755,6 @@ export class GBMinService { await min.whatsAppDirectLine.setup(true); } else { - const minBoot = GBServer.globals.minBoot as any; if (min !== minBoot && minBoot.instance.whatsappServiceKey) { min.whatsAppDirectLine = new WhatsappDirectLine( min, @@ -803,6 +769,13 @@ export class GBMinService { } } + // Builds bot numbers map in WhatsAppDirectLine globals. + + let botNumber = min.core.getParam(min.instance, 'Bot Number', null); + if (botNumber) { + WhatsappDirectLine.botsByNumber[botNumber] = min.whatsAppDirectLine; + } + // Setups default BOT Framework dialogs. min.userProfile = conversationState.createProperty('userProfile'); @@ -911,7 +884,7 @@ export class GBMinService { const step = await min.dialogs.createContext(context); step.context.activity.locale = 'pt-BR'; - + const member = context.activity.from; const sec = new SecService(); const user = await sec.ensureUser(instance.instanceId, member.id, member.name, '', 'web', member.name, null); @@ -939,7 +912,7 @@ export class GBMinService { await sec.setParam(userId, 'wholeWord', true); await sec.setParam(userId, 'theme', 'white'); await sec.setParam(userId, 'maxColumns', 40); - + // This same event is dispatched either to all participants // including the bot, that is filtered bellow. @@ -1069,9 +1042,8 @@ export class GBMinService { await this.processEventActivity(min, user, context, step); } } catch (error) { - const msg = `ERROR: ${error.message} ${error.stack} ${error.error ? error.error.body : ''} ${ - error.error ? (error.error.stack ? error.error.stack : '') : '' - }`; + const msg = `ERROR: ${error.message} ${error.stack} ${error.error ? error.error.body : ''} ${error.error ? (error.error.stack ? error.error.stack : '') : '' + }`; GBLog.error(msg); await min.conversationalService.sendText( @@ -1176,8 +1148,7 @@ export class GBMinService { return utterance.match(Messages.global_quit); } - private async handleUploads(min, step, user, params) - { + private async handleUploads(min, step, user, params, autoSave) { // Prepare Promises to download each attachment and then execute each Promise. if ( @@ -1194,7 +1165,7 @@ export class GBMinService { // In case of not having HEAR activated before, it is // a upload with no Dialog, so run Auto Save to .gbdrive. - if (!min.cbMap[user.userId]) { + if (!min.cbMap[user.userId] && autoSave) { const t = new SystemKeywords(); GBLog.info(`BASIC (${min.botId}): Upload done for ${attachmentData.fileName}.`); const handle = WebAutomationServices.cyrb53(min.botId + attachmentData.fileName); @@ -1326,11 +1297,12 @@ export class GBMinService { context.activity.text ); - const conversationReference = JSON.stringify(TurnContext.getConversationReference(context.activity)); - await sec.updateConversationReferenceById(userId, conversationReference); } } + const conversationReference = JSON.stringify(TurnContext.getConversationReference(context.activity)); + await sec.updateConversationReferenceById(userId, conversationReference); + if (GBMinService.userMobile(step)) { const startDialog = user.hearOnDialog ? user.hearOnDialog : min.core.getParam(min.instance, 'Start Dialog', null); @@ -1351,7 +1323,8 @@ export class GBMinService { } } - await this.handleUploads(min, step, user, params); + const notes = min.core.getParam(min.instance, 'Notes', null); + await this.handleUploads(min, step, user, params, notes != null); // Files in .gbdialog can be called directly by typing its name normalized into JS . @@ -1402,19 +1375,19 @@ export class GBMinService { } else { // Removes unwanted chars in input text. - + step.context.activity['originalText'] = context.activity.text; const text = await GBConversationalService.handleText(min, user, step, context.activity.text); step.context.activity['originalText'] step.context.activity['text'] = text; - const notes = min.core.getParam(min.instance, 'Notes', null); - if (notes && text && text !== "") { + + if (notes && text && text !== "") { const sys = new SystemKeywords(); - await sys.note({pid, text}); + await sys.note({ pid, text }); await step.context.sendActivity('OK.'); return; - } + } // Checks for bad words on input text. @@ -1459,9 +1432,8 @@ export class GBMinService { try { await step.continueDialog(); } catch (error) { - const msg = `ERROR: ${error.message} ${error.stack} ${error.error ? error.error.body : ''} ${ - error.error ? (error.error.stack ? error.error.stack : '') : '' - }`; + const msg = `ERROR: ${error.message} ${error.stack} ${error.error ? error.error.body : ''} ${error.error ? (error.error.stack ? error.error.stack : '') : '' + }`; GBLog.error(msg); await min.conversationalService.sendText( min, @@ -1502,4 +1474,92 @@ export class GBMinService { } } } -} + + public async ensureAPI() { + + const mins = GBServer.globals.minInstances; + + function getRemoteId(ctx: Koa.Context) { + return '1'; // Each bot has its own API. + } + + const close = async () => { + return new Promise(resolve => { + if (GBServer.globals.server.apiServer) { + GBServer.globals.server.apiServer.close( + cb => { + resolve(true); + GBLogEx.info(0, 'Reloading General Bots API...'); + } + ); + } + else{ + resolve(true); + GBLogEx.info(0, 'Loading General Bots API...'); + } + }); + }; + + await close(); + + + let proxies = {}; + await CollectionUtil.asyncForEach(mins, async min => { + + let dialogs = {}; + await CollectionUtil.asyncForEach(Object.values(min.scriptMap), async script => { + + + const f = new Function() + + dialogs[script] = async (data)=> { + let params; + if (data){ + params = JSON.parse(data); + } + + await GBVMService.callVM(script, min, null, null, null, false, params); + } + }); + + const proxy = { + dk: new DialogKeywords(), + wa: new WebAutomationServices(), + sys: new SystemKeywords(), + dbg: new DebuggerService(), + img: new ImageProcessingServices(), + dialogs: dialogs + }; + proxies[min.botId] = proxy; + }); + + const opts = { + pingSendTimeout: null, + keepAliveTimeout: null, + listeners: { + unsubscribed(subscriptions: number): void { }, + subscribed(subscriptions: number): void { }, + disconnected(remoteId: string, connections: number): void { }, + connected(remoteId: string, connections: number): void { }, + messageIn(...params): void { + params.shift(); + GBLogEx.verbose(0, '[IN] ' + params); + }, + messageOut(...params): void { + params.shift(); + GBLogEx.verbose(0, '[OUT] ' + params); + } + } + }; + + GBServer.globals.server.apiServer = createKoaHttpServer(GBVMService.API_PORT, getRemoteId, { prefix: `api/v3` }); + + createRpcServer( + proxies, + GBServer.globals.server.apiServer, + opts + ); + + } + +} \ No newline at end of file diff --git a/packages/gpt.gblib/services/ChatServices.ts b/packages/gpt.gblib/services/ChatServices.ts index d3fa50c8..503eb5b4 100644 --- a/packages/gpt.gblib/services/ChatServices.ts +++ b/packages/gpt.gblib/services/ChatServices.ts @@ -33,9 +33,57 @@ 'use strict'; import { GBMinInstance } from 'botlib'; -import { Configuration, OpenAIApi } from "openai"; +//import OpenAI from "openai"; +import { ChatGPTAPIBrowser, getOpenAIAuth } from 'chatgpt' +import { CollectionUtil } from 'pragmatismo-io-framework'; +import { DialogKeywords } from '../../basic.gblib/services/DialogKeywords.js'; +import Path from 'path'; +import * as Fs from 'fs'; export class ChatServices { + + public static async sendMessage(min: GBMinInstance, text: string) { + let key; + if (process.env.OPENAI_KEY) { + key = process.env.OPENAI_KEY; + } + else { + key = min.core.getParam(min.instance, 'Open AI Key', null); + } + + if (!key) { + throw new Error('Open AI Key not configured in .gbot.'); + } + let functions = []; + + // Adds .gbdialog as functions if any to GPT Functions. + + await CollectionUtil.asyncForEach(Object.values(min.scriptMap), async script => { + const path = DialogKeywords.getGBAIPath(min.botId, "gbdialog", null); + const localFolder = Path.join('work', path, `${script}.json`); + + if (Fs.existsSync(localFolder)) { + const func = Fs.readFileSync(localFolder).toJSON(); + functions.push(func); + } + + }); + + // Calls Model. + + // const openai = new OpenAI({ + // apiKey: key + // }); + // const chatCompletion = await openai.chat.completions.create({ + // model: "gpt-3.5-turbo", + // messages: [{ role: "user", content: text }], + // functions: functions + // }); + // return chatCompletion.choices[0].message.content; + } + + + /** * Generate text * @@ -45,22 +93,25 @@ export class ChatServices { * */ public static async continue(min: GBMinInstance, text: string, chatId) { - let key = min.core.getParam(min.instance, 'Open AI Key', null); + let key; + if (process.env.OPENAI_KEY) { + key = process.env.OPENAI_KEY; + } + else { + key = min.core.getParam(min.instance, 'Open AI Key', null); + } if (!key) { throw new Error('Open AI Key not configured in .gbot.'); } + // const openai = new OpenAI({ + // apiKey: key + // }); + // const chatCompletion = await openai.chat.completions.create({ + // model: "gpt-3.5-turbo", + // messages: [{ role: "user", content: text }] - const configuration = new Configuration({ - apiKey: key, - }); - const openai = new OpenAIApi(configuration); - - const chatCompletion = await openai.createChatCompletion({ - model: "gpt-3.5-turbo", - messages: [{role: "user", content: text}], - }); - return chatCompletion.data.choices[0].message.content; - + // }); + // return chatCompletion.choices[0].message.content; } } diff --git a/packages/kb.gbapp/dialogs/AskDialog.ts b/packages/kb.gbapp/dialogs/AskDialog.ts index 2aca29d3..25654fe3 100644 --- a/packages/kb.gbapp/dialogs/AskDialog.ts +++ b/packages/kb.gbapp/dialogs/AskDialog.ts @@ -108,8 +108,14 @@ export class AskDialog extends IGBDialog { text = Messages[locale].ask_first_time; } else if (step.options && step.options.isReturning && !step.context.activity.group) { const askForMore = min.core.getParam(min.instance, 'Ask For More', null); + if (askForMore){ + text = askForMore ; + } + else + { - text = askForMore ? Messages[locale].anything_else : ''; + return await step.endDialog(null); + } } else if (step.context.activity.group || (step.options && step.options.emptyPrompt)) { return await step.next(); } else if (user.subjects.length > 0) { diff --git a/packages/kb.gbapp/services/KBService.ts b/packages/kb.gbapp/services/KBService.ts index d14431a8..17614f80 100644 --- a/packages/kb.gbapp/services/KBService.ts +++ b/packages/kb.gbapp/services/KBService.ts @@ -289,9 +289,14 @@ export class KBService implements IGBKBService { } } + const key = instance.searchKey ? instance.searchKey: + GBServer.globals.minBoot.instance.searchKey; + const host = instance.searchHost ? instance.searchHost : + GBServer.globals.minBoot.instance.searchHost; + // No direct match found, so Search is used. - if (instance.searchKey !== null && GBConfigService.get('STORAGE_DIALECT') === 'mssql') { + if (key !== null && GBConfigService.get('STORAGE_DIALECT') === 'mssql') { interface SearchResults { instanceId: number; questionId: number; @@ -303,11 +308,11 @@ export class KBService implements IGBKBService { subject4: string; } - const client = new SearchClient('https://' + instance.searchHost, 'azuresql-index', { - key: instance.searchKey + const client = new SearchClient('https://' + host, 'azuresql-index', { + key: key } as any); - const results = await client.search(query, { + const results = await client.search(query.substring(0,499), { filter: `instanceId eq ${instance.instanceId} and skipIndex eq false`, searchFields: ['content', 'subject1', 'subject2', 'subject3', 'subject4'], select: ['instanceId', 'questionId', 'answerId'], @@ -662,6 +667,8 @@ export class KBService implements IGBKBService { const subjectFile = urlJoin(localPath, 'subjects.json'); const menuFile = urlJoin(localPath, 'menu.xlsx'); + // Imports menu.xlsx if any. + if (Fs.existsSync(subjectFile) || Fs.existsSync(menuFile)) { await this.importSubjectFile(packageStorage.packageId, subjectFile, menuFile, instance); } @@ -1012,11 +1019,13 @@ export class KBService implements IGBKBService { const packageName = Path.basename(localPath); const instance = await core.loadInstanceByBotId(min.botId); GBLog.info(`[GBDeployer] Importing: ${localPath}`); + const p = await deployer.deployPackageToStorage(instance.instanceId, packageName); await this.importKbPackage(min, localPath, p, instance); GBDeployer.mountGBKBAssets(packageName, min.botId, localPath); const service = await AzureDeployerService.createInstance(deployer); - await deployer.rebuildIndex(instance, service.getKBSearchSchema(instance.searchIndex)); + const searchIndex = instance.searchIndex ? instance.searchIndex : GBServer.globals.minBoot.instance.searchIndex; + await deployer.rebuildIndex(instance, service.getKBSearchSchema(searchIndex)); min['groupCache'] = await KBService.getGroupReplies(instance.instanceId); await KBService.RefreshNER(min); diff --git a/packages/whatsapp.gblib/services/WhatsappDirectLine.ts b/packages/whatsapp.gblib/services/WhatsappDirectLine.ts index a791686c..1484fe57 100644 --- a/packages/whatsapp.gblib/services/WhatsappDirectLine.ts +++ b/packages/whatsapp.gblib/services/WhatsappDirectLine.ts @@ -51,6 +51,7 @@ import pkg from 'whatsapp-web.js'; import { DialogKeywords } from '../../basic.gblib/services/DialogKeywords.js'; import { ChatServices } from '../../gpt.gblib/services/ChatServices.js'; import { GBAdminService } from '../../admin.gbapp/services/GBAdminService.js'; +import e from 'express'; const { List, Buttons, Client, MessageMedia } = pkg; /** @@ -58,6 +59,7 @@ const { List, Buttons, Client, MessageMedia } = pkg; */ export class WhatsappDirectLine extends GBService { public static conversationIds = {}; + public static botsByNumber = {}; public static mobiles = {}; public static phones = {}; public static chatIds = {}; @@ -121,7 +123,7 @@ export class WhatsappDirectLine extends GBService { const client = await new SwaggerClient({ spec: JSON.parse(Fs.readFileSync('directline-3.0.json', 'utf8')), requestInterceptor: req => { - req.headers['Authorization'] = `Bearer ${this.min.instance.webchatKey}`; + req.headers['Authorization'] = `Bearer ${this.min.instance.whatsappBotKey}`; } }); this.directLineClient = client; @@ -197,8 +199,7 @@ export class WhatsappDirectLine extends GBService { }; if (setUrl) { createClient.bind(this)(); - } - else { + } else { this.customClient = minBoot.whatsAppDirectLine.customClient; } setUrl = false; @@ -303,7 +304,7 @@ export class WhatsappDirectLine extends GBService { public async received(req, res) { const provider = WhatsappDirectLine.providerFromRequest(req); - let message, from, fromName, text: string; + let message, to, from, fromName, text: string; let group = ''; let answerText = null; let attachments = null; @@ -311,6 +312,18 @@ export class WhatsappDirectLine extends GBService { switch (provider) { case 'GeneralBots': message = req; + to = message.to.endsWith('@g.us') ? message.to.split('@')[0] : message.to.split('@')[0]; + const newThis = WhatsappDirectLine.botsByNumber[to]; + + // If there is a number specified, checks if it + // is related to a custom bot and reroutes immediately. + + if (newThis !== this && newThis.min.botId !== GBServer.globals.minBoot.botId) { + await newThis.received(req, res); + + return; + } + text = message.body; from = message.from.endsWith('@g.us') ? message.author.split('@')[0] : message.from.split('@')[0]; fromName = message._data.notifyName; @@ -318,9 +331,14 @@ export class WhatsappDirectLine extends GBService { if (message.hasMedia) { const base64Image = await message.downloadMedia(); - let buf: any = Buffer.from(base64Image.data, "base64"); + let buf: any = Buffer.from(base64Image.data, 'base64'); const gbaiName = DialogKeywords.getGBAIPath(this.min.botId); - const localName = Path.join('work', gbaiName, 'cache', `tmp${GBAdminService.getRndReadableIdentifier()}.docx`); + const localName = Path.join( + 'work', + gbaiName, + 'cache', + `tmp${GBAdminService.getRndReadableIdentifier()}.docx` + ); Fs.writeFileSync(localName, buf, { encoding: null }); const url = urlJoin(GBServer.globals.publicAddress, this.min.botId, 'cache', Path.basename(localName)); @@ -401,10 +419,12 @@ export class WhatsappDirectLine extends GBService { // Bot name must be specified on config. if (botGroupID === group) { + // Shortcut has been mentioned? let found = false; parts.forEach(e1 => { + botShortcuts.forEach(e2 => { if (e1 === e2 && !found) { found = true; @@ -425,17 +445,18 @@ export class WhatsappDirectLine extends GBService { } }); } - - // Ignore group messages without the mention to Bot. - - let smsServiceNumber = this.min.core.getParam(this.min.instance, 'whatsappServiceNumber', null); - if (smsServiceNumber && !answerText) { - smsServiceNumber = smsServiceNumber.replace('+', ''); - if (!message.body.startsWith('@' + smsServiceNumber)) { - return; - } - } }); + + // Ignore group messages without the mention to Bot. + + let botNumber = this.min.core.getParam(this.min.instance, 'Bot Number', null); + if (botNumber && !answerText && !found) { + botNumber = botNumber.replace('+', ''); + if (!message.body.startsWith('@' + botNumber)) { + + return; + } + } } } @@ -528,7 +549,6 @@ export class WhatsappDirectLine extends GBService { ); if (user.agentSystemId.indexOf('@') !== -1) { - // Agent is from Teams. await this.min.conversationalService['sendOnConversation']( this.min, @@ -563,10 +583,10 @@ export class WhatsappDirectLine extends GBService { await this.endTransfer(from, locale, user, agent, sec); } else { GBLog.info(`USER (${from}) TO AGENT ${agent.userSystemId}: ${text}`); - + const prompt = `the person said: ${text}. what can I tell her?`; const answer = await ChatServices.continue(this.min, prompt, 0); - text = `${text} \n\nGeneral Bots: ${answer}` + text = `${text} \n\nGeneral Bots: ${answer}`; if (user.agentSystemId.indexOf('@') !== -1) { // Agent is from Teams or Google Chat. @@ -609,21 +629,17 @@ export class WhatsappDirectLine extends GBService { } private async endTransfer(id: string, locale: string, user: GuaribasUser, agent: GuaribasUser, sec: SecService) { - await this.sendToDeviceEx(id, Messages[this.locale].notify_end_transfer(this.min.instance.botId), locale, null); if (user.agentSystemId.indexOf('@') !== -1) { - // Agent is from Teams. - + await this.min.conversationalService['sendOnConversation']( this.min, agent, Messages[this.locale].notify_end_transfer(this.min.instance.botId) ); - } else { - await this.sendToDeviceEx( user.agentSystemId, Messages[this.locale].notify_end_transfer(this.min.instance.botId), @@ -712,17 +728,15 @@ export class WhatsappDirectLine extends GBService { if (activity.attachments) { await CollectionUtil.asyncForEach(activity.attachments, async attachment => { - switch (attachment.contentType) { case 'application/vnd.microsoft.card.hero': output += `\n${this.renderHeroCard(attachment)}`; break; case 'image/png': - await this.sendFileToDevice(to, attachment.contentUrl, - attachment.name, attachment.name, 0); + await this.sendFileToDevice(to, attachment.contentUrl, attachment.name, attachment.name, 0); - return; + return; default: GBLog.info(`Unknown content type: ${attachment.contentType}`); @@ -742,6 +756,7 @@ export class WhatsappDirectLine extends GBService { switch (this.provider) { case 'GeneralBots': const attachment = await MessageMedia.fromUrl(url); + to = to.replace('+', ''); if (to.indexOf('@') == -1) { if (to.length == 18) { to = to + '@g.us'; @@ -885,6 +900,7 @@ export class WhatsappDirectLine extends GBService { switch (this.provider) { case 'GeneralBots': + to = to.replace('+', ''); if (to.indexOf('@') == -1) { if (to.length == 18) { to = to + '@g.us'; @@ -963,11 +979,9 @@ export class WhatsappDirectLine extends GBService { switch (provider) { case 'GeneralBots': - - // Ignore E2E messages and status updates. + // Ignore E2E messages and status updates. - if (req.type && req.type === 'e2e_notification' - || req.isStatus) { + if ((req.type && req.type === 'e2e_notification') || req.isStatus) { return; } @@ -1018,16 +1032,25 @@ export class WhatsappDirectLine extends GBService { } const sec = new SecService(); - let user = await sec.getUserFromSystemId(id); + + // Tries to find if user wants to switch bots. + + let toSwitchMin = GBServer.globals.minInstances.filter( + p => p.instance.botId.toLowerCase() === text.toLowerCase() + )[0]; + + GBLog.info(`A WhatsApp mobile requested instance for: ${botId}.`); let urlMin: any = GBServer.globals.minInstances.filter(p => p.instance.botId === botId)[0]; const botNumber = urlMin ? urlMin.core.getParam(urlMin.instance, 'Bot Number', null) : null; - if (botNumber) { + if (botNumber && GBServer.globals.minBoot.botId !== urlMin.botId) { + GBLog.info(`${user.userSystemId} fixed by bot number talked to: ${botId}.`); user = await sec.updateUserInstance(user.userSystemId, urlMin.instance.instanceId); } + let activeMin; // Processes group behaviour. @@ -1071,34 +1094,81 @@ export class WhatsappDirectLine extends GBService { // Detects if the welcome message is enabled. - if (process.env.WHATSAPP_WELCOME_DISABLED !== 'true') { - // Tries to find if user wants to switch bots. - - let toSwitchMin = GBServer.globals.minInstances.filter( - p => p.instance.botId.toLowerCase() === text.toLowerCase() + if (process.env.WHATSAPP_WELCOME_DISABLED === 'true') { + let minInstance = GBServer.globals.minInstances.filter( + p => p.instance.botId.toLowerCase() === botId.toLowerCase() )[0]; - if (!toSwitchMin) { - toSwitchMin = GBServer.globals.minInstances.filter(p => - p.instance.activationCode ? p.instance.activationCode.toLowerCase() === text.toLowerCase() : false - )[0]; + + // Just pass the message to the receiver. + + await minInstance.whatsAppDirectLine.received(req, res); + + return; + } + + if (!toSwitchMin) { + toSwitchMin = GBServer.globals.minInstances.filter(p => + p.instance.activationCode ? p.instance.activationCode.toLowerCase() === text.toLowerCase() : false + )[0]; + } + + // If bot has a fixed Find active bot instance. + + activeMin = botNumber ? urlMin : toSwitchMin ? toSwitchMin : GBServer.globals.minBoot; + + // If it is the first time for the user, tries to auto-execute + // start dialog if any is specified in Config.xlsx. + + if (user === null || user.hearOnDialog) { + user = await sec.ensureUser(activeMin.instance.instanceId, id, senderName, '', 'whatsapp', senderName, null); + + const startDialog = user.hearOnDialog + ? user.hearOnDialog + : activeMin.core.getParam(activeMin.instance, 'Start Dialog', null); + + if (startDialog) { + GBLog.info(`Calling /start to Auto start ${startDialog} for ${activeMin.instance.instanceId}...`); + if (provider === 'chatapi') { + req.body.messages[0].body = `/start`; + } else if (provider === 'maytapi') { + req.body.message = `/start`; + } else { + req.body = `/start`; + } + + // Resets HEAR ON DIALOG value to none and passes + // current dialog to the direct line. + + await sec.updateUserHearOnDialog(user.userId, null); + await (activeMin as any).whatsAppDirectLine.received(req, res); + } else { + await (activeMin as any).whatsAppDirectLine.sendToDevice( + id, + `Olá! Seja bem-vinda(o)!\nMe chamo ${activeMin.instance.title}. Como posso ajudar? Pode me falar que eu te ouço, me manda um aúdio.`, + null + ); + if (res) { + res.end(); + } } + } else { + // User wants to switch bots. - // If bot has a fixed Find active bot instance. + if (toSwitchMin) { + GBLog.info(`Switching bots from ${botId} to ${toSwitchMin.botId}...`); - activeMin = botNumber ? urlMin : toSwitchMin ? toSwitchMin : GBServer.globals.minBoot; + // So gets the new bot instance information and prepares to + // auto start dialog if any is specified. - // If it is the first time for the user, tries to auto-execute - // start dialog if any is specified in Config.xlsx. - - if (user === null || user.hearOnDialog) { - user = await sec.ensureUser(activeMin.instance.instanceId, id, senderName, '', 'whatsapp', senderName, null); - - const startDialog = user.hearOnDialog - ? user.hearOnDialog - : activeMin.core.getParam(activeMin.instance, 'Start Dialog', null); + activeMin = toSwitchMin; + const instance = await this.min.core.loadInstanceByBotId(activeMin.botId); + user = await sec.updateUserInstance(id, instance.instanceId); + await (activeMin as any).whatsAppDirectLine.resetConversationId(activeMin.botId, id, ''); + const startDialog = activeMin.core.getParam(activeMin.instance, 'Start Dialog', null); if (startDialog) { - GBLog.info(`Calling /start to Auto start ${startDialog} for ${activeMin.instance.instanceId}...`); + + GBLog.info(`Calling /start for Auto start : ${startDialog} for ${activeMin.instance.botId}...`); if (provider === 'chatapi') { req.body.messages[0].body = `/start`; } else if (provider === 'maytapi') { @@ -1107,86 +1177,40 @@ export class WhatsappDirectLine extends GBService { req.body = `/start`; } - // Resets HEAR ON DIALOG value to none and passes - // current dialog to the direct line. - - await sec.updateUserHearOnDialog(user.userId, null); await (activeMin as any).whatsAppDirectLine.received(req, res); } else { await (activeMin as any).whatsAppDirectLine.sendToDevice( id, - `Olá! Seja bem-vinda(o)!\nMe chamo ${activeMin.instance.title}. Como posso ajudar? Pode me falar que eu te ouço, me manda um aúdio.`, + `Agora falando com ${activeMin.instance.title}...`, null ); - if (res) { - res.end(); - } + } + if (res) { + res.end(); } } else { - // User wants to switch bots. - - if (toSwitchMin !== undefined) { - // So gets the new bot instance information and prepares to - // auto start dialog if any is specified. - - const instance = await this.min.core.loadInstanceByBotId(activeMin.botId); - await sec.updateUserInstance(id, instance.instanceId); - await (activeMin as any).whatsAppDirectLine.resetConversationId(activeMin.botId, id, ''); - const startDialog = activeMin.core.getParam(activeMin.instance, 'Start Dialog', null); - - if (startDialog) { - GBLog.info(`Calling /start for Auto start : ${startDialog} for ${activeMin.instance.botId}...`); - if (provider === 'chatapi') { - req.body.messages[0].body = `/start`; - } else if (provider === 'maytapi') { - req.body.message = `/start`; - } else { - req.body = `/start`; - } - - await (activeMin as any).whatsAppDirectLine.received(req, res); - } else { - await (activeMin as any).whatsAppDirectLine.sendToDevice( - id, - `Agora falando com ${activeMin.instance.title}...`, - null - ); - } - if (res) { - res.end(); - } + let t; + activeMin = GBServer.globals.minInstances.filter(p => p.instance.instanceId === user.instanceId)[0]; + if (activeMin === undefined) { + activeMin = GBServer.globals.minBoot; + t = (activeMin as any).whatsAppDirectLine; + await t.sendToDevice( + id, + `O outro Bot que você estava falando(${user.instanceId}), não está mais disponível. Agora você está falando comigo, ${activeMin.instance.title}...` + ); } else { - let t; - activeMin = GBServer.globals.minInstances.filter(p => p.instance.instanceId === user.instanceId)[0]; - if (activeMin === undefined) { - activeMin = GBServer.globals.minBoot; + if ((activeMin as any).whatsAppDirectLine) { t = (activeMin as any).whatsAppDirectLine; - await t.sendToDevice( - id, - `O outro Bot que você estava falando(${user.instanceId}), não está mais disponível. Agora você está falando comigo, ${activeMin.instance.title}...` - ); + } else { + t = (GBServer.globals.minBoot as any).whatsAppDirectLine; } - else { - if ((activeMin as any).whatsAppDirectLine) { - t = (activeMin as any).whatsAppDirectLine; - } else { - t = (GBServer.globals.minBoot as any).whatsAppDirectLine; - } - } - - t.received(req, res); } + + await t.received(req, res); } - } else { - let minInstance = GBServer.globals.minInstances.filter( - p => p.instance.botId.toLowerCase() === botId.toLowerCase() - )[0]; - - // Just pass the message to the receiver. - - await minInstance.whatsAppDirectLine.received(req, res); } } catch (error) { + error = error['e'] ? error['e'] : error; GBLog.error(`Error on Whatsapp callback: ${error.data ? error.data : error} ${error.stack}`); } } diff --git a/src/RootData.ts b/src/RootData.ts index f0737579..a886ff0e 100644 --- a/src/RootData.ts +++ b/src/RootData.ts @@ -48,6 +48,7 @@ export class RootData { public publicAddress: string; // URI for BotServer. public server: any; // Express reference. public httpsServer: any; // Express reference (HTTPS). + public apiServer: any; // Koa reference (HTTPS) for GB API (isolated from /). public sysPackages: any[]; // Loaded system package list. public appPackages: any[]; // Loaded .gbapp package list. public minService: GBMinService; // Minimalist service core. diff --git a/src/app.ts b/src/app.ts index bd631062..3313ab34 100644 --- a/src/app.ts +++ b/src/app.ts @@ -59,7 +59,6 @@ import * as winston from 'winston-logs-display'; import { RootData } from './RootData.js'; import { GBSSR } from '../packages/core.gbapp/services/GBSSR.js'; import { Mutex } from 'async-mutex'; -import { GBVMService } from '../packages/basic.gblib/services/GBVMService.js'; /** * General Bots open-core entry point. @@ -77,6 +76,8 @@ export class GBServer { GBConfigService.init(); const port = GBConfigService.getServerPort(); + + if (process.env.TEST_SHELL) { GBLog.info(`Running TEST_SHELL: ${process.env.TEST_SHELL}...`); try { @@ -108,6 +109,7 @@ export class GBServer { // Setups global error handlers. process.on('unhandledRejection', (err, p) => { + err = err['e'] ? err['e'] : err; GBLog.error(`UNHANDLED_REJECTION(promises): ${err.toString()} ${err['stack'] ? '\n' + err['stack'] : ''}`); if (err['response']?.obj?.httpStatusCode === 404) { GBLog.warn(`Check reverse proxy: ${process.env.BOT_URL} as it seems to be invalid.`); @@ -116,6 +118,7 @@ export class GBServer { process.on('uncaughtException', (err, p) => { if (err !== null) { + err = err['e'] ? err['e'] : err; GBLog.error(`UNCAUGHT_EXCEPTION: ${err.toString()} ${err['stack'] ? '\n' + err['stack'] : ''}`); } else { GBLog.error('UNCAUGHT_EXCEPTION: Unknown error (err is null)'); @@ -136,6 +139,7 @@ export class GBServer { const mainCallback = () => { (async () => { + try { GBLog.info(`Now accepting connections on ${port}...`); process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';