From cd97189477c452d44e9584f0152950b2aff23b88 Mon Sep 17 00:00:00 2001 From: Alan Date: Tue, 14 Feb 2023 13:58:17 -0300 Subject: [PATCH 1/2] fix(Whatsapp.gblib): fix "whatsapp-web.js" compatibility issues. --- package.json | 4 +- packages/basic.gblib/index.ts | 2 +- .../basic.gblib/services/DialogKeywords.ts | 10 +- .../services/WhatsappDirectLine.ts | 260 ++++++++---------- swagger.json | 10 +- 5 files changed, 133 insertions(+), 153 deletions(-) diff --git a/package.json b/package.json index 835737bb..9501e49e 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "prism-media": "1.3.4", "public-ip": "6.0.1", "punycode": "2.1.1", - "puppeteer": "19.2.2", + "puppeteer": "19.6.3", "puppeteer-extra": "3.3.4", "puppeteer-extra-plugin-stealth": "2.11.1", "qrcode": "1.5.1", @@ -163,7 +163,7 @@ "vm2-process": "2.1.1", "walk-promise": "0.2.0", "washyourmouthoutwithsoap": "1.0.2", - "whatsapp-web.js": "1.18.3", + "whatsapp-web.js": "github:pedroslopez/whatsapp-web.js#fix-buttons-list", "winston": "3.8.2", "winston-logs-display": "1.0.0", "yarn": "1.22.19" diff --git a/packages/basic.gblib/index.ts b/packages/basic.gblib/index.ts index 0a4160b5..b4d3eded 100644 --- a/packages/basic.gblib/index.ts +++ b/packages/basic.gblib/index.ts @@ -60,7 +60,7 @@ export class GBBasicPackage implements IGBPackage { public async loadPackage (core: IGBCoreService, sequelize: Sequelize): Promise { core.sequelize.addModels([GuaribasSchedule]); - app.use(koaBody.koaBody({ multipart: true, })); + //app.use(koaBody.koaBody({ multipart: true, })); app.listen(1111); } diff --git a/packages/basic.gblib/services/DialogKeywords.ts b/packages/basic.gblib/services/DialogKeywords.ts index 9eb8c008..d6c87a3a 100644 --- a/packages/basic.gblib/services/DialogKeywords.ts +++ b/packages/basic.gblib/services/DialogKeywords.ts @@ -1050,9 +1050,9 @@ export class DialogKeywords { public static async getProcessInfo(pid: number) { const proc = GBServer.globals.processes[pid]; - const min = GBServer.globals.minInstances.filter(p => p.instance.instanceId == proc.instanceId)[0]; + const min = GBServer.globals.minInstances[0];//.filter(p => p.instance.instanceId == proc.instanceId)[0]; const sec = new SecService(); - const user = await sec.getUserFromId(min.instance.instanceId, proc.userId); + const user = await sec.getUserFromId(min.instance.instanceId, "1"); const params = JSON.parse(user.params); return { min, @@ -1064,13 +1064,15 @@ export class DialogKeywords { /** * Talks to the user by using the specified text. */ - public async talk({ pid, text }) { + public async talk(x) { + const text="",pid=0 GBLog.info(`BASIC: TALK '${text}'.`); const { min, user } = await DialogKeywords.getProcessInfo(pid); + await min.whatsAppDirectLine.sendButton(); if (user) { // TODO: const translate = this.user ? this.user.basicOptions.translatorOn : false; - await min.conversationalService['sendOnConversation'](min, user, text); + // await min.conversationalService['sendOnConversation'](min, user, text); } } diff --git a/packages/whatsapp.gblib/services/WhatsappDirectLine.ts b/packages/whatsapp.gblib/services/WhatsappDirectLine.ts index 99699627..4eebcb38 100644 --- a/packages/whatsapp.gblib/services/WhatsappDirectLine.ts +++ b/packages/whatsapp.gblib/services/WhatsappDirectLine.ts @@ -43,12 +43,13 @@ import { Messages } from '../strings.js'; import { GuaribasUser } from '../../security.gbapp/models/index.js'; import { GBMinService } from '../../core.gbapp/services/GBMinService.js'; import { GBConfigService } from '../../core.gbapp/services/GBConfigService.js'; -import * as wpp from 'whatsapp-web.js'; import qrcode from 'qrcode-terminal'; import express from 'express'; import { DialogKeywords } from '../../basic.gblib/services/DialogKeywords.js'; import { GBAdminService } from '../../admin.gbapp/services/GBAdminService.js'; import { method } from 'lodash'; +import pkg from 'whatsapp-web.js'; +const { Buttons, Client, MessageMedia } = pkg; /** * Support for Whatsapp. @@ -97,13 +98,13 @@ export class WhatsappDirectLine extends GBService { this.whatsappServiceKey = whatsappServiceKey; this.whatsappServiceNumber = whatsappServiceNumber; this.whatsappServiceUrl = whatsappServiceUrl; - this.provider = + this.provider = whatsappServiceKey === 'internal' ? 'GeneralBots' : whatsappServiceNumber.indexOf(';') > -1 ? 'maytapi' - : whatsappServiceKey !== 'internal' - ? 'graphapi' + : whatsappServiceKey !== 'internal' + ? 'graphapi' : 'chatapi'; this.groupId = groupId; } @@ -114,6 +115,14 @@ export class WhatsappDirectLine extends GBService { } } + public async sendButton() { + let url = 'https://wwebjs.dev/logo.png'; + const media = await MessageMedia.fromUrl(url); + media.mimetype = 'image/png'; + media.filename = 'hello.png'; + let btnClickableMenu = new Buttons(media as any, [{ id: 'customId', body: 'button1' }, { body: 'button2' }]); + await this.sendToDevice("5521996049063",btnClickableMenu as any,null) + } public async setup(setUrl: boolean) { this.directLineClient = new Swagger({ spec: JSON.parse(Fs.readFileSync('directline-3.0.json', 'utf8')), @@ -123,135 +132,105 @@ export class WhatsappDirectLine extends GBService { let url: string; let body: any; - client.clientAuthorizations.add( + /*client.clientAuthorizations.add( 'AuthorizationBotConnector', new Swagger.ApiKeyAuthorization('Authorization', `Bearer ${this.directLineSecret}`, 'header') - ); + );*/ let options: any; const phoneId = this.whatsappServiceNumber.split(';')[0]; switch (this.provider) { case 'GeneralBots': - const minBoot = GBServer.globals.minBoot as any; - - // Initialize the browser using a local profile for each bot. - - const gbaiName = `${this.min.botId}.gbai`; - const localName = Path.join('work', gbaiName, 'profile'); - - const createClient = async browserWSEndpoint => { - let puppeteer: any = { - headless: false, - args: [ - '--no-sandbox', - '--disable-setuid-sandbox', - '--disable-dev-shm-usage', - '--disable-accelerated-2d-canvas', - '--no-first-run', - '--no-zygote', - '--single-process', - '--disable-gpu', - '--disable-infobars', - '--disable-features=site-per-process', - `--user-data-dir=${localName}` - ] - }; - if (browserWSEndpoint) { - puppeteer = { browserWSEndpoint: browserWSEndpoint }; - } - - const client = (this.customClient = new wpp.Client({ - authStrategy: new wpp.LocalAuth({ - clientId: this.min.botId, - dataPath: localName - }), - puppeteer: puppeteer - })); - - client.on( - 'message', - (async (message: string) => { - await this.WhatsAppCallback(message, null); - }).bind(this) - ); - - client.on( - 'qr', - (async qr => { - const adminNumber = this.min.core.getParam(this.min.instance, 'Bot Admin Number', null); - const adminEmail = this.min.core.getParam(this.min.instance, 'Bot Admin E-mail', null); - - // Sends QR Code to boot bot admin. - - const msg = `Please, scan QR Code with for bot ${this.botId}.`; - GBLog.info(msg); - qrcode.generate(qr, { small: true, scale: 0.5 }); - - // While handling other bots uses boot instance of this class to send QR Codes. - - const s = new DialogKeywords(this.min, null, null); - const qrBuf = await s.getQRCode(qr); - const gbaiName = `${this.min.botId}.gbai`; - const localName = Path.join( - 'work', - gbaiName, - 'cache', - `qr${GBAdminService.getRndReadableIdentifier()}.png` - ); - Fs.writeFileSync(localName, qrBuf); - const url = urlJoin(GBServer.globals.publicAddress, this.min.botId, 'cache', Path.basename(localName)); - GBServer.globals.minBoot.whatsAppDirectLine.sendFileToDevice( - adminNumber, - url, - Path.basename(localName), - msg - ); - - s.sendEmail({ pid: 0, to: adminEmail, subject: `Check your WhatsApp for bot ${this.botId}`, body: msg }); - }).bind(this) - ); - - client.on('authenticated', async () => { - this.browserWSEndpoint = client.pupBrowser.wsEndpoint(); - GBLog.verbose(`GBWhatsApp: QR Code authenticated for ${this.botId}.`); - }); - - client.on('ready', async () => { - client.pupBrowser.on( - 'disconnected', - (async () => { - GBLog.info(`Browser terminated. Restarting ${this.min.botId} WhatsApp native provider.`); - await createClient.bind(this)(null); + const minBoot = GBServer.globals.minBoot; + // TODO: REMOVE THIS. + if (!setUrl) { + this.customClient = minBoot.whatsAppDirectLine.customClient; + } else { + // Initialize the browser using a local profile for each bot. + const gbaiName = `${this.min.botId}.gbai`; + const localName = Path.join('work', gbaiName, 'profile'); + const createClient = async browserWSEndpoint => { + let puppeteer = { headless: false, args: ['--no-sandbox', '--disable-dev-shm-usage'] }; + if (browserWSEndpoint) { + // puppeteer.browserWSEndpoint = browserWSEndpoint ; + } + const client = (this.customClient = new Client({ + puppeteer: puppeteer + })); + client.on( + 'message', + (async message => { + await this.WhatsAppCallback(message, null); }).bind(this) ); - - GBLog.verbose(`GBWhatsApp: Emptying chat list for ${this.botId}...`); - - // Keeps the chat list cleaned. - - const chats = await client.getChats(); - await CollectionUtil.asyncForEach(chats, async chat => { - const sleep = (ms: number) => { - return new Promise(resolve => { - setTimeout(resolve, ms); - }); - }; - const wait = Math.floor(Math.random() * 5000) + 1000; - await sleep(wait); - if (chat.isGroup) { - await chat.clearMessages(); - } else if (!chat.pinned) { - await chat.delete(); - } + client.on( + 'qr', + (async qr => { + const adminNumber = this.min.core.getParam(this.min.instance, 'Bot Admin Number', null); + const adminEmail = this.min.core.getParam(this.min.instance, 'Bot Admin E-mail', null); + // Sends QR Code to boot bot admin. + const msg = `Please, scan QR Code with for bot ${this.botId}.`; + GBLog.info(msg); + qrcode.generate(qr, { small: true, scale: 0.5 }); + // While handling other bots uses boot instance of this class to send QR Codes. + // const s = new DialogKeywords(min., null, null, null); + // const qrBuf = await s.getQRCode(qr); + // const gbaiName = `${this.min.botId}.gbai`; + // const localName = Path.join('work', gbaiName, 'cache', `qr${GBAdminService.getRndReadableIdentifier()}.png`); + // fs.writeFileSync(localName, qrBuf); + // const url = urlJoin( + // GBServer.globals.publicAddress, + // this.min.botId, + // 'cache', + // Path.basename(localName) + // ); + // GBServer.globals.minBoot.whatsAppDirectLine.sendFileToDevice(adminNumber, url, Path.basename(localName), msg); + // s.sendEmail(adminEmail, `Check your WhatsApp for bot ${this.botId}`, msg); + }).bind(this) + ); + client.on('authenticated', async () => { + this.browserWSEndpoint = client.pupBrowser.wsEndpoint(); + GBLog.verbose(`GBWhatsApp: QR Code authenticated for ${this.botId}.`); }); - }); - - client.initialize(); + client.on('ready', async () => { + GBLog.verbose(`GBWhatsApp: Emptying chat list for ${this.botId}...`); + // Keeps the chat list cleaned. + const chats = await client.getChats(); + await CollectionUtil.asyncForEach(chats, async chat => { + const sleep = ms => { + return new Promise(resolve => { + setTimeout(resolve, ms); + }); + }; + const wait = Math.floor(Math.random() * 5000) + 1000; + await sleep(wait); + if (chat.isGroup) { + await chat.clearMessages(); + } else if (!chat.pinned) { + await chat.delete(); + } + }); + }); + client.initialize(); + }; + await createClient.bind(this)(this.browserWSEndpoint); + setUrl = false; + } + break; + case 'chatapi': + options = { + method: 'POST', + url: urlJoin(this.whatsappServiceUrl, 'webhook'), + timeout: 10000, + qs: { + token: this.whatsappServiceKey, + webhookUrl: `${GBServer.globals.publicAddress}/webhooks/whatsapp/${this.botId}`, + set: true + }, + headers: { + 'cache-control': 'no-cache' + } }; - await createClient.bind(this)(this.browserWSEndpoint); - - setUrl = false; - break; case 'chatapi': url = urlJoin(this.whatsappServiceUrl, 'webhook'); @@ -290,7 +269,7 @@ export class WhatsappDirectLine extends GBService { json: true }; break; - } + } if (setUrl && options && this.whatsappServiceUrl) { GBServer.globals.server.use(`/audios`, express.static('work')); @@ -742,7 +721,7 @@ export class WhatsappDirectLine extends GBService { let options; switch (this.provider) { case 'GeneralBots': - const attachment = await wpp.MessageMedia.fromUrl(url); + const attachment = await MessageMedia.fromUrl(url); if (to.indexOf('@') == -1) { if (to.length == 18) { to = to + '@g.us'; @@ -798,22 +777,22 @@ export class WhatsappDirectLine extends GBService { }; break; - - case 'graphapi': - url = `https://graph.facebook.com/v15.0/${phoneId}/messages` - options = { - method:'POST', - timeout: 10000, - headers: { - token: `Bearer `, - 'Content-Type': 'application/json' - }, - body:{ - messaging_product: 'whatsapp', - recipient_type: 'individual', - to: phoneId, - } + + case 'graphapi': + url = `https://graph.facebook.com/v15.0/${phoneId}/messages`; + options = { + method: 'POST', + timeout: 10000, + headers: { + token: `Bearer `, + 'Content-Type': 'application/json' + }, + body: { + messaging_product: 'whatsapp', + recipient_type: 'individual', + to: phoneId } + }; } if (options) { @@ -831,7 +810,7 @@ export class WhatsappDirectLine extends GBService { let options; switch (this.provider) { case 'GeneralBots': - const attachment = wpp.MessageMedia.fromUrl(url); + const attachment = MessageMedia.fromUrl(url); await this.customClient.sendMessage(to, attachment); break; @@ -928,7 +907,6 @@ export class WhatsappDirectLine extends GBService { }; break; case 'graphapi': - } if (options) { diff --git a/swagger.json b/swagger.json index 90c85a71..108c97db 100644 --- a/swagger.json +++ b/swagger.json @@ -13,7 +13,7 @@ "status": "Production" } }, - "host": "", + "host": "f993e4828c0d50.lhr.life", "basePath": "/", "schemes": [ "https" @@ -21,11 +21,11 @@ "consumes": [], "produces": [], "paths": { - "/api/v2//dialog/talk": { + "/api/v2/dev-perdomo/dialog/talk": { "post": { - "requestBody":{ - "content": "text/plain" - }, + "consumes":[ + "text/plain; charset=utf-8" + ], "summary": "Talk to the user.", "description": "Talk to the user.", "x-ms-no-generic-test": true, From d5ca7afe2f6b0bac78cfa75db8c0ab6478293d04 Mon Sep 17 00:00:00 2001 From: Alan Date: Tue, 14 Feb 2023 14:11:52 -0300 Subject: [PATCH 2/2] Merge branch 'main' of https://github.com/GeneralBots/BotServer --- package.json | 1 + packages/admin.gbapp/dialogs/AdminDialog.ts | 18 +++ packages/basic.gblib/services/GBVMService.ts | 28 ++-- .../services/ImageProcessingServices.ts | 32 +++++ .../services/KeywordsExpressions.ts | 7 + .../basic.gblib/services/SystemKeywords.ts | 1 + packages/core.gbapp/index.ts | 4 +- packages/core.gbapp/models/GBModel.ts | 9 +- packages/core.gbapp/services/GBCoreService.ts | 129 ++++++++++-------- packages/core.gbapp/services/GBLogEx.ts | 85 ++++++++++++ packages/kb.gbapp/dialogs/AskDialog.ts | 2 +- src/RootData.ts | 1 + 12 files changed, 238 insertions(+), 79 deletions(-) create mode 100644 packages/core.gbapp/services/GBLogEx.ts diff --git a/package.json b/package.json index 9501e49e..3e5e3dcb 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,7 @@ "google-libphonenumber": "3.2.31", "googleapis": "109.0.1", "ibm-watson": "7.1.2", + "join-images-updated": "1.1.4", "keyv": "4.5.2", "koa": "2.13.4", "koa-body": "6.0.1", diff --git a/packages/admin.gbapp/dialogs/AdminDialog.ts b/packages/admin.gbapp/dialogs/AdminDialog.ts index 66761df1..24d95a58 100644 --- a/packages/admin.gbapp/dialogs/AdminDialog.ts +++ b/packages/admin.gbapp/dialogs/AdminDialog.ts @@ -238,6 +238,24 @@ export class AdminDialog extends IGBDialog { ]) ); + + min.dialogs.add( + new WaterfallDialog('/logs', [ + async step => { + if (step.context.activity.channelId !== 'msteams' && process.env.ENABLE_AUTH) { + return await step.beginDialog('/auth'); + } else { + return await step.next(step.options); + } + }, + async step => { + const logs = await min.core['getLatestLogs'](); + await min.conversationalService.sendText(min, step, logs); + return await step.replaceDialog('/ask', { isReturning: true }); + } + ])); + + min.dialogs.add( new WaterfallDialog('/publish', [ async step => { diff --git a/packages/basic.gblib/services/GBVMService.ts b/packages/basic.gblib/services/GBVMService.ts index d0bbfa90..1b19cbba 100644 --- a/packages/basic.gblib/services/GBVMService.ts +++ b/packages/basic.gblib/services/GBVMService.ts @@ -32,7 +32,7 @@ 'use strict'; -import { GBLog, GBMinInstance, GBService, IGBCoreService, GBDialogStep } from 'botlib'; +import { GBMinInstance, GBService, IGBCoreService, GBDialogStep } from 'botlib'; import * as Fs from 'fs'; import { GBServer } from '../../../src/app.js'; import { GBDeployer } from '../../core.gbapp/services/GBDeployer.js'; @@ -50,9 +50,7 @@ import { GBAdminService } from '../../admin.gbapp/services/GBAdminService.js'; import pkg from 'swagger-client'; import { DialogKeywords } from './DialogKeywords.js'; import { KeywordsExpressions } from './KeywordsExpressions.js'; -const { Swagger } = pkg; - - +import { GBLogEx } from '../../core.gbapp/services/GBLogEx.js'; /** * @fileoverview Decision was to priorize security(isolation) and debugging, @@ -129,7 +127,7 @@ export class GBVMService extends GBService { }`; Fs.writeFileSync(urlJoin(folder, 'package.json'), packageJson); - GBLog.info(`BASIC: Installing .gbdialog node_modules for ${min.botId}...`); + GBLogEx.info(min, `BASIC: Installing .gbdialog node_modules for ${min.botId}...`); const npmPath = urlJoin(process.env.PWD, 'node_modules', '.bin', 'npm'); child_process.execSync(`${npmPath} install`, { cwd: folder }); } @@ -139,7 +137,7 @@ export class GBVMService extends GBService { const fullFilename = urlJoin(folder, filename); if (process.env.GBDIALOG_HOTSWAP) { Fs.watchFile(fullFilename, async () => { - await this.translateBASIC(fullFilename, mainName, min.botId); + await this.translateBASIC(fullFilename, mainName, min); const parsedCode: string = Fs.readFileSync(jsfile, 'utf8'); min.sandBoxMap[mainName.toLowerCase().trim()] = parsedCode; }); @@ -152,10 +150,10 @@ 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, mainName, min.botId); + await this.translateBASIC(fullFilename, mainName, min); } } else { - await this.translateBASIC(fullFilename, mainName, min.botId); + await this.translateBASIC(fullFilename, mainName, min); } const parsedCode: string = Fs.readFileSync(jsfile, 'utf8'); min.sandBoxMap[mainName.toLowerCase().trim()] = parsedCode; @@ -163,7 +161,7 @@ export class GBVMService extends GBService { }); } - public async translateBASIC(filename: any, mainName: string, botId: string) { + public async translateBASIC(filename: any, mainName: string, min:GBMinInstance) { // Converts General Bots BASIC into regular VBS let basicCode: string = Fs.readFileSync(filename, 'utf8'); @@ -204,10 +202,10 @@ export class GBVMService extends GBService { // Interprocess communication from local HTTP to the BotServer. - const dk = rest.createClient('http://localhost:1111/api/v2/${botId}/dialog'); - const sys = rest.createClient('http://localhost:1111/api/v2/${botId}/system'); - const wa = rest.createClient('http://localhost:1111/api/v2/${botId}/webautomation'); - const img = rest.createClient('http://localhost:1111/api/v2/${botId}/imagprocessing'); + const dk = rest.createClient('http://localhost:1111/api/v2/${min.botId}/dialog'); + const sys = rest.createClient('http://localhost:1111/api/v2/${min.botId}/system'); + const wa = rest.createClient('http://localhost:1111/api/v2/${min.botId}/webautomation'); + const img = rest.createClient('http://localhost:1111/api/v2/${min.botId}/imagprocessing'); // Local variables. @@ -245,7 +243,7 @@ export class GBVMService extends GBService { `; Fs.writeFileSync(jsfile, code); - GBLog.info(`[GBVMService] Finished loading of ${filename}, JavaScript from Word: \n ${code}`); + GBLogEx.info(min, `[GBVMService] Finished loading of ${filename}, JavaScript from Word: \n ${code}`); } public static getMethodNameFromVBSFilename(filename: string) { @@ -288,7 +286,7 @@ export class GBVMService extends GBService { public async convert(code: string) { // Start and End of VB2TS tags of processing. - code = process.env.ENABLE_AUTH ? `hear gbLogin as login\n${code}` : code; + code = process.env.ENABLE_AUTH ? `hear GBLogExin as login\n${code}` : code; var lines = code.split('\n'); const keywords = KeywordsExpressions.getKeywords(); let current = 41; diff --git a/packages/basic.gblib/services/ImageProcessingServices.ts b/packages/basic.gblib/services/ImageProcessingServices.ts index 63dc84c8..9bd96ba0 100644 --- a/packages/basic.gblib/services/ImageProcessingServices.ts +++ b/packages/basic.gblib/services/ImageProcessingServices.ts @@ -32,9 +32,15 @@ 'use strict'; +import Path from 'path'; import { GBLog, GBMinInstance } from 'botlib'; import { DialogKeywords } from './DialogKeywords.js'; import sharp from 'sharp'; +import joinImages from 'join-images-updated'; +import { CollectionUtil } from 'pragmatismo-io-framework'; +import { GBAdminService } from '../../admin.gbapp/services/GBAdminService.js'; +import urlJoin from 'url-join'; +import { GBServer } from '../../../src/app.js'; /** * Image processing services of conversation to be called by BASIC. @@ -95,6 +101,32 @@ export class ImageProcessingServices { return; } + /** + * SET ORIENTATION VERTICAL + * + * file = MERGE file1, file2, file3 + */ + public async mergeImage({pid, files}) + { + const { min, user } = await DialogKeywords.getProcessInfo(pid); + + let paths = []; + await CollectionUtil.asyncForEach(files, async file => { + const gbfile = DialogKeywords.getFileByHandle(file); + paths.push(gbfile.path); + }); + + const botId = this.min.instance.botId; + const gbaiName = `${botId}.gbai`; + const img = await joinImages(paths); + const localName = Path.join('work', gbaiName, 'cache', `img-mrg${GBAdminService.getRndReadableIdentifier()}.png`); + const url = urlJoin(GBServer.globals.publicAddress, min.botId, 'cache', Path.basename(localName)); + img.toFile(localName); + + return { localName: localName, url: url, data: null }; + + } + /** * Sharpen the image. * diff --git a/packages/basic.gblib/services/KeywordsExpressions.ts b/packages/basic.gblib/services/KeywordsExpressions.ts index 26bf4f65..8df1cd92 100644 --- a/packages/basic.gblib/services/KeywordsExpressions.ts +++ b/packages/basic.gblib/services/KeywordsExpressions.ts @@ -677,6 +677,13 @@ export class KeywordsExpressions { } ]; + keywords[i++] = [ + /^\s*(MERGE)(\s*)(.*)/gim, + ($0, $1, $2, $3) => { + return `await img.mergeImage({pid: pid, files: [${$3}]})`; + } + ]; + keywords[i++] = [ /^\s*PRESS\s*(.*)/gim, ($0, $1, $2) => { diff --git a/packages/basic.gblib/services/SystemKeywords.ts b/packages/basic.gblib/services/SystemKeywords.ts index c8642775..786f4d42 100644 --- a/packages/basic.gblib/services/SystemKeywords.ts +++ b/packages/basic.gblib/services/SystemKeywords.ts @@ -1412,6 +1412,7 @@ export class SystemKeywords { const images = []; let index = 0; path = Path.join(gbaiName, 'cache', `tmp${GBAdminService.getRndReadableIdentifier()}.docx`); + url = urlJoin(GBServer.globals.publicAddress, min.botId, 'cache', Path.basename(localName)); const traverseDataToInjectImageUrl = async o => { for (var i in o) { diff --git a/packages/core.gbapp/index.ts b/packages/core.gbapp/index.ts index f1a0a007..190a65fe 100644 --- a/packages/core.gbapp/index.ts +++ b/packages/core.gbapp/index.ts @@ -43,7 +43,7 @@ import { LanguageDialog } from './dialogs/LanguageDialog.js'; import { SwitchBotDialog } from './dialogs/SwitchBot.js'; import { WelcomeDialog } from './dialogs/WelcomeDialog.js'; import { WhoAmIDialog } from './dialogs/WhoAmIDialog.js'; -import { GuaribasChannel, GuaribasException, GuaribasInstance, GuaribasPackage } from './models/GBModel.js'; +import { GuaribasChannel, GuaribasInstance, GuaribasLog, GuaribasPackage } from './models/GBModel.js'; /** * Package for core.gbapp. @@ -53,7 +53,7 @@ export class GBCorePackage implements IGBPackage { public CurrentEngineName = 'guaribas-1.0.0'; public async loadPackage (core: IGBCoreService, sequelize: Sequelize): Promise { - core.sequelize.addModels([GuaribasInstance, GuaribasPackage, GuaribasChannel, GuaribasException]); + core.sequelize.addModels([GuaribasInstance, GuaribasPackage, GuaribasChannel, GuaribasLog]); } public async getDialogs (min: GBMinInstance) { diff --git a/packages/core.gbapp/models/GBModel.ts b/packages/core.gbapp/models/GBModel.ts index 203654db..76e3b17c 100644 --- a/packages/core.gbapp/models/GBModel.ts +++ b/packages/core.gbapp/models/GBModel.ts @@ -326,15 +326,18 @@ export class GuaribasChannel extends Model { */ @Table //tslint:disable-next-line:max-classes-per-file -export class GuaribasException extends Model { +export class GuaribasLog extends Model { @PrimaryKey @AutoIncrement @Column(DataType.INTEGER) - declare exceptionId: number; + declare logId: number; - @Column(DataType.STRING(255)) + @Column(DataType.STRING(1024)) declare message: string; + @Column(DataType.STRING(1)) + declare kind: string; + @ForeignKey(() => GuaribasInstance) @Column(DataType.INTEGER) declare instanceId: number; diff --git a/packages/core.gbapp/services/GBCoreService.ts b/packages/core.gbapp/services/GBCoreService.ts index 2e52de62..9f387925 100644 --- a/packages/core.gbapp/services/GBCoreService.ts +++ b/packages/core.gbapp/services/GBCoreService.ts @@ -50,7 +50,7 @@ import { GBCustomerSatisfactionPackage } from '../../customer-satisfaction.gbapp import { GBKBPackage } from '../../kb.gbapp/index.js'; import { GBSecurityPackage } from '../../security.gbapp/index.js'; import { GBWhatsappPackage } from '../../whatsapp.gblib/index.js'; -import { GuaribasInstance } from '../models/GBModel.js'; +import { GuaribasInstance, GuaribasLog} from '../models/GBModel.js'; import { GBConfigService } from './GBConfigService.js'; import { GBAzureDeployerPackage } from '../../azuredeployer.gbapp/index.js'; import { GBSharePointPackage } from '../../sharepoint.gblib/index.js'; @@ -102,16 +102,16 @@ export class GBCoreService implements IGBCoreService { /** * */ - constructor () { + constructor() { this.adminService = new GBAdminService(this); } - public async ensureInstances (instances: IGBInstance[], bootInstance: any, core: IGBCoreService) {} + public async ensureInstances(instances: IGBInstance[], bootInstance: any, core: IGBCoreService) {} /** * Gets database config and connect to storage. Currently two databases * are available: SQL Server and SQLite. */ - public async initStorage (): Promise { + public async initStorage(): Promise { this.dialect = GBConfigService.get('STORAGE_DIALECT'); let host: string | undefined; @@ -177,7 +177,7 @@ export class GBCoreService implements IGBCoreService { * Checks wheather storage is acessible or not and opens firewall * in case of any connection block. */ - public async checkStorage (installationDeployer: IGBInstallationDeployer) { + public async checkStorage(installationDeployer: IGBInstallationDeployer) { try { await this.sequelize.authenticate(); } catch (error) { @@ -195,7 +195,7 @@ export class GBCoreService implements IGBCoreService { /** * Syncronizes structure between model and tables in storage. */ - public async syncDatabaseStructure () { + public async syncDatabaseStructure() { if (GBConfigService.get('STORAGE_SYNC') === 'true') { const alter = GBConfigService.get('STORAGE_SYNC_ALTER') === 'true'; GBLog.info('Syncing database...'); @@ -213,7 +213,29 @@ export class GBCoreService implements IGBCoreService { /** * Loads all items to start several listeners. */ - public async loadInstances (): Promise { + public async getLatestLogs(instanceId: number): Promise { + const options = { + where: { + instanceId: instanceId, + state: 'active', + created: { + [Op.gt]: new Date(Date.now() - 60 * 60 * 1000 * 48) // Latest 48 hours. + } + } + }; + const list = await GuaribasLog.findAll(options); + let out = 'General Bots Log\n'; + await CollectionUtil.asyncForEach(list, async e => { + out = `${out}\n${e.createdAt} - ${e.message}`; + }); + return out; + } + + + /** + * Loads all items to start several listeners. + */ + public async loadInstances(): Promise { if (process.env.LOAD_ONLY !== undefined) { const bots = process.env.LOAD_ONLY.split(`;`); const and = []; @@ -236,7 +258,7 @@ export class GBCoreService implements IGBCoreService { /** * Loads just one Bot instance by its internal Id. */ - public async loadInstanceById (instanceId: number): Promise { + public async loadInstanceById(instanceId: number): Promise { const options = { where: { instanceId: instanceId, state: 'active' } }; return await GuaribasInstance.findOne(options); @@ -244,7 +266,7 @@ export class GBCoreService implements IGBCoreService { /** * Loads just one Bot instance. */ - public async loadInstanceByActivationCode (code: string): Promise { + public async loadInstanceByActivationCode(code: string): Promise { let options = { where: { activationCode: code, state: 'active' } }; return await GuaribasInstance.findOne(options); @@ -252,7 +274,7 @@ export class GBCoreService implements IGBCoreService { /** * Loads just one Bot instance. */ - public async loadInstanceByBotId (botId: string): Promise { + public async loadInstanceByBotId(botId: string): Promise { const options = { where: {} }; options.where = { botId: botId, state: 'active' }; @@ -264,7 +286,7 @@ export class GBCoreService implements IGBCoreService { * first startup, when user is asked some questions to create the * full base environment. */ - public async writeEnv (instance: IGBInstance) { + public async writeEnv(instance: IGBInstance) { const env = ` ADDITIONAL_DEPLOY_PATH= ADMIN_PASS=${instance.adminPass} @@ -294,7 +316,7 @@ ENDPOINT_UPDATE=true * when calling back from web services. This ensures that reverse proxy is * established. */ - public async ensureProxy (port): Promise { + public async ensureProxy(port): Promise { try { if (Fs.existsSync('node_modules/ngrok/bin/ngrok.exe') || Fs.existsSync('node_modules/ngrok/bin/ngrok')) { return await ngrok.connect({ port: port }); @@ -315,7 +337,7 @@ ENDPOINT_UPDATE=true * Setup generic web hooks so .gbapps can expose application logic * and get called on demand. */ - public installWebHook (isGet: boolean, url: string, callback: any) { + public installWebHook(isGet: boolean, url: string, callback: any) { if (isGet) { GBServer.globals.server.get(url, (req, res) => { callback(req, res); @@ -331,7 +353,7 @@ ENDPOINT_UPDATE=true * Defines the entry point dialog to be called whenever a user * starts talking to the bot. */ - public setEntryPointDialog (dialogName: string) { + public setEntryPointDialog(dialogName: string) { GBServer.globals.entryPointDialog = dialogName; } @@ -339,14 +361,14 @@ ENDPOINT_UPDATE=true * Replaces the default web application root path used to start the GB * with a custom home page. */ - public setWWWRoot (localPath: string) { + public setWWWRoot(localPath: string) { GBServer.globals.wwwroot = localPath; } /** * Removes a bot instance from storage. */ - public async deleteInstance (botId: string) { + public async deleteInstance(botId: string) { const options = { where: {} }; options.where = { botId: botId }; await GuaribasInstance.destroy(options); @@ -356,7 +378,7 @@ ENDPOINT_UPDATE=true * Saves a bot instance object to the storage handling * multi-column JSON based store 'params' field. */ - public async saveInstance (fullInstance: any) { + public async saveInstance(fullInstance: any) { const options = { where: {} }; options.where = { botId: fullInstance.botId }; let instance = await GuaribasInstance.findOne(options); @@ -377,7 +399,7 @@ ENDPOINT_UPDATE=true /** * Loads all bot instances from object storage, if it's formatted. */ - public async loadAllInstances ( + public async loadAllInstances( core: IGBCoreService, installationDeployer: IGBInstallationDeployer, proxyAddress: string @@ -431,7 +453,7 @@ ENDPOINT_UPDATE=true /** * Loads all system packages from 'packages' folder. */ - public async loadSysPackages (core: GBCoreService): Promise { + public async loadSysPackages(core: GBCoreService): Promise { // NOTE: if there is any code before this line a semicolon // will be necessary before this line. // Loads all system packages. @@ -469,7 +491,7 @@ ENDPOINT_UPDATE=true * Verifies that an complex global password has been specified * before starting the server. */ - public ensureAdminIsSecured () { + public ensureAdminIsSecured() { const password = GBConfigService.get('ADMIN_PASS'); if (!GBAdminService.StrongRegex.test(password)) { throw new Error( @@ -484,7 +506,7 @@ ENDPOINT_UPDATE=true * So a base main bot is always deployed and will act as root bot for * configuration tree with three levels: .env > root bot > all other bots. */ - public async createBootInstance ( + public async createBootInstance( core: GBCoreService, installationDeployer: IGBInstallationDeployer, proxyAddress: string @@ -519,7 +541,7 @@ ENDPOINT_UPDATE=true /** * Helper to get the web browser onpened in UI interfaces. */ - public openBrowserInDevelopment () { + public openBrowserInDevelopment() { if (process.env.NODE_ENV === 'development') { open('http://localhost:4242'); } @@ -540,35 +562,29 @@ ENDPOINT_UPDATE=true * // ' FOREIGN KEY ([groupId1], [groupId2]) REFERENCES [Group] ([groupId1], [groupId1]) ON DELETE NO ACTION,' + * // ' FOREIGN KEY ([instanceId]) REFERENCES [Instance] ([instanceId]) ON DELETE NO ACTION)' */ - private createTableQueryOverride (tableName, attributes, options): string { + private createTableQueryOverride(tableName, attributes, options): string { let sql: string = this.createTableQuery.apply(this.queryGenerator, [tableName, attributes, options]); const re1 = /CREATE\s+TABLE\s+\[([^\]]*)\]/; const matches = re1.exec(sql); if (matches !== null) { const table = matches[1]; const re2 = /PRIMARY\s+KEY\s+\(\[[^\]]*\](?:,\s*\[[^\]]*\])*\)/; - sql = sql.replace( - re2, - (match: string, ...args: any[]): string => { - return `CONSTRAINT [${table}_pk] ${match}`; - } - ); + sql = sql.replace(re2, (match: string, ...args: any[]): string => { + return `CONSTRAINT [${table}_pk] ${match}`; + }); const re3 = /FOREIGN\s+KEY\s+\((\[[^\]]*\](?:,\s*\[[^\]]*\])*)\)/g; const re4 = /\[([^\]]*)\]/g; - sql = sql.replace( - re3, - (match: string, ...args: any[]): string => { - const fkcols = args[0]; - let fkname = table; - let matches2 = re4.exec(fkcols); - while (matches2 !== null) { - fkname += `_${matches2[1]}`; - matches2 = re4.exec(fkcols); - } - - return `CONSTRAINT [${fkname}_fk] FOREIGN KEY (${fkcols})`; + sql = sql.replace(re3, (match: string, ...args: any[]): string => { + const fkcols = args[0]; + let fkname = table; + let matches2 = re4.exec(fkcols); + while (matches2 !== null) { + fkname += `_${matches2[1]}`; + matches2 = re4.exec(fkcols); } - ); + + return `CONSTRAINT [${fkname}_fk] FOREIGN KEY (${fkcols})`; + }); } return sql; @@ -582,7 +598,7 @@ ENDPOINT_UPDATE=true * ' CONSTRAINT [invalid2] FOREIGN KEY ([groupId1], [groupId2]) REFERENCES [Group] ([groupId1], [groupId2]) ON DELETE NO ACTION, ' + * ' CONSTRAINT [invalid3] FOREIGN KEY ([instanceId1]) REFERENCES [Instance] ([instanceId1]) ON DELETE NO ACTION' */ - private changeColumnQueryOverride (tableName, attributes): string { + private changeColumnQueryOverride(tableName, attributes): string { let sql: string = this.changeColumnQuery.apply(this.queryGenerator, [tableName, attributes]); const re1 = /ALTER\s+TABLE\s+\[([^\]]*)\]/; const matches = re1.exec(sql); @@ -590,20 +606,17 @@ ENDPOINT_UPDATE=true const table = matches[1]; const re2 = /(ADD\s+)?CONSTRAINT\s+\[([^\]]*)\]\s+FOREIGN\s+KEY\s+\((\[[^\]]*\](?:,\s*\[[^\]]*\])*)\)/g; const re3 = /\[([^\]]*)\]/g; - sql = sql.replace( - re2, - (match: string, ...args: any[]): string => { - const fkcols = args[2]; - let fkname = table; - let matches2 = re3.exec(fkcols); - while (matches2 !== null) { - fkname += `_${matches2[1]}`; - matches2 = re3.exec(fkcols); - } - - return `${args[0] ? args[0] : ''}CONSTRAINT [${fkname}_fk] FOREIGN KEY (${fkcols})`; + sql = sql.replace(re2, (match: string, ...args: any[]): string => { + const fkcols = args[2]; + let fkname = table; + let matches2 = re3.exec(fkcols); + while (matches2 !== null) { + fkname += `_${matches2[1]}`; + matches2 = re3.exec(fkcols); } - ); + + return `${args[0] ? args[0] : ''}CONSTRAINT [${fkname}_fk] FOREIGN KEY (${fkcols})`; + }); } return sql; @@ -612,7 +625,7 @@ ENDPOINT_UPDATE=true /** * Opens storage firewall used by the server when starting to get root bot instance. */ - private async openStorageFrontier (installationDeployer: IGBInstallationDeployer) { + private async openStorageFrontier(installationDeployer: IGBInstallationDeployer) { const group = GBConfigService.get('CLOUD_GROUP'); const serverName = GBConfigService.get('STORAGE_SERVER').split('.database.windows.net')[0]; await installationDeployer.openStorageFirewall(group, serverName); @@ -625,7 +638,7 @@ ENDPOINT_UPDATE=true * @param name Name of param to get from instance. * @param defaultValue Value returned when no param is defined in Config.xlsx. */ - public getParam (instance: IGBInstance, name: string, defaultValue?: T): any { + public getParam(instance: IGBInstance, name: string, defaultValue?: T): any { let value = null; if (instance.params) { const params = JSON.parse(instance.params); diff --git a/packages/core.gbapp/services/GBLogEx.ts b/packages/core.gbapp/services/GBLogEx.ts new file mode 100644 index 00000000..37f5a03b --- /dev/null +++ b/packages/core.gbapp/services/GBLogEx.ts @@ -0,0 +1,85 @@ +/*****************************************************************************\ +| ( )_ _ | +| _ _ _ __ _ _ __ ___ ___ _ _ | ,_)(_) ___ _ _ _ | +| ( '_`\ ( '__)/'_` ) /'_ `\/' _ ` _ `\ /'_` )| | | |/',__)/ \ /`\ /'_`\ | +| | (_) )| | ( (_| |( (_) || ( ) ( ) |( (_| || |_ | |\__, \| |*| |( (_) ) | +| | ,__/'(_) `\__,_)`\__ |(_) (_) (_)`\__,_)`\__)(_)(____/(_) (_)`\___/' | +| | | ( )_) | | +| (_) \___/' | +| | +| General Bots Copyright (c) Pragmatismo.io. All rights reserved. | +| Licensed under the AGPL-3.0. | +| | +| According to our dual licensing model, this program can be used either | +| under the terms of the GNU Affero General Public License, version 3, | +| or under a proprietary license. | +| | +| The texts of the GNU Affero General Public License with an additional | +| permission and of our proprietary license can be found at and | +| in the LICENSE file you have received along with this program. | +| | +| This program is distributed in the hope that it will be useful, | +| but WITHOUT ANY WARRANTY, without even the implied warranty of | +| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | +| GNU Affero General Public License for more details. | +| | +| "General Bots" is a registered trademark of Pragmatismo.io. | +| The licensing of the program under the AGPLv3 does not imply a | +| trademark license. Therefore any rights, title and interest in | +| our trademarks remain entirely with us. | +| | +\*****************************************************************************/ + +/** + * @fileoverview General Bots server core. + */ + +'use strict'; + +import { GBLog, IGBInstance } from "botlib"; +import { GuaribasLog } from "../models/GBModel"; + +export class GBLogEx { + public static async error(minOrInstanceId: any, message: string) { + GBLog.error(message); + if (typeof minOrInstanceId === 'object') { + minOrInstanceId = minOrInstanceId.instance.instanceId; + } + await this.log(minOrInstanceId, 'e', message); + } + + public static async debug(minOrInstanceId: any, message: string) { + GBLog.debug(message); + if (typeof minOrInstanceId === 'object') { + minOrInstanceId = minOrInstanceId.instance.instanceId; + } + await this.log(minOrInstanceId, 'd', message); + } + + public static async info(minOrInstanceId: any, message: string) { + GBLog.info(message); + if (typeof minOrInstanceId === 'object') { + minOrInstanceId = minOrInstanceId.instance.instanceId; + } + await this.log(minOrInstanceId, 'i', message); + } + + public static async verbose(minOrInstanceId: any, message: string) { + GBLog.verbose(message); + if (typeof minOrInstanceId === 'object') { + minOrInstanceId = minOrInstanceId.instance.instanceId; + } + await this.log(minOrInstanceId, 'v', message); + } + + /** + * Finds and update user agent information to a next available person. + */ + public static async log(instance: IGBInstance, kind: string, message: string): Promise { + return await GuaribasLog.create({ + instanceId: instance.instanceId, + message: message, + kind: kind + }); + } +} diff --git a/packages/kb.gbapp/dialogs/AskDialog.ts b/packages/kb.gbapp/dialogs/AskDialog.ts index 35d956d3..feaf1169 100644 --- a/packages/kb.gbapp/dialogs/AskDialog.ts +++ b/packages/kb.gbapp/dialogs/AskDialog.ts @@ -101,7 +101,7 @@ export class AskDialog extends IGBDialog { if (step.options && step.options.firstTime) { text = Messages[locale].ask_first_time; } else if (step.options && step.options.isReturning) { - text = ''; // REMOVED: Messages[locale].anything_else; + text = Messages[locale].anything_else; } else if (step.options && step.options.emptyPrompt) { text = ''; } else if (user.subjects.length > 0) { diff --git a/src/RootData.ts b/src/RootData.ts index 01ec0a32..2cc40292 100644 --- a/src/RootData.ts +++ b/src/RootData.ts @@ -29,6 +29,7 @@ | our trademarks remain entirely with us. | | | \*****************************************************************************/ + /** * @fileoverview General Bots server core. */