From 5d2e801c12ce3aebfdf61a97102e789107b16ef9 Mon Sep 17 00:00:00 2001 From: Alan Date: Thu, 9 Feb 2023 19:40:16 -0300 Subject: [PATCH 01/29] feat(whatsapp.gblib): add 'graphapi' option. --- packages/basic.gblib/index.ts | 9 +++- .../services/WhatsappDirectLine.ts | 41 +++++++++++++++---- src/app.ts | 10 ++++- swagger.json | 7 +--- 4 files changed, 50 insertions(+), 17 deletions(-) diff --git a/packages/basic.gblib/index.ts b/packages/basic.gblib/index.ts index b78ba356..0a4160b5 100644 --- a/packages/basic.gblib/index.ts +++ b/packages/basic.gblib/index.ts @@ -60,7 +60,8 @@ 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); } @@ -97,5 +98,11 @@ export class GBBasicPackage implements IGBPackage { app.use(waRouter.routes()); app.use(dbgRouter.routes()); app.use(imgRouter.routes()); + app.use(async (ctx, next) => { + if(ctx['status'] === 404){ + ctx.status = 404 + ctx.body = {msg:'emmmmmmm, seems 404'}; + } + }); } } diff --git a/packages/whatsapp.gblib/services/WhatsappDirectLine.ts b/packages/whatsapp.gblib/services/WhatsappDirectLine.ts index f207b26d..99699627 100644 --- a/packages/whatsapp.gblib/services/WhatsappDirectLine.ts +++ b/packages/whatsapp.gblib/services/WhatsappDirectLine.ts @@ -48,6 +48,7 @@ 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'; /** * Support for Whatsapp. @@ -96,11 +97,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' : 'chatapi'; this.groupId = groupId; } @@ -125,6 +128,7 @@ export class WhatsappDirectLine extends GBService { new Swagger.ApiKeyAuthorization('Authorization', `Bearer ${this.directLineSecret}`, 'header') ); let options: any; + const phoneId = this.whatsappServiceNumber.split(';')[0]; switch (this.provider) { case 'GeneralBots': @@ -203,7 +207,7 @@ export class WhatsappDirectLine extends GBService { msg ); - s.sendEmail({pid: 0, to: adminEmail, subject: `Check your WhatsApp for bot ${this.botId}`, body: msg }); + s.sendEmail({ pid: 0, to: adminEmail, subject: `Check your WhatsApp for bot ${this.botId}`, body: msg }); }).bind(this) ); @@ -249,7 +253,6 @@ export class WhatsappDirectLine extends GBService { setUrl = false; break; - case 'chatapi': url = urlJoin(this.whatsappServiceUrl, 'webhook'); options = { @@ -268,7 +271,6 @@ export class WhatsappDirectLine extends GBService { break; case 'maytapi': - let phoneId = this.whatsappServiceNumber.split(';')[0]; let productId = this.whatsappServiceNumber.split(';')[1]; url = `${this.INSTANCE_URL}/${productId}/${phoneId}/config`; body = { @@ -288,7 +290,7 @@ export class WhatsappDirectLine extends GBService { json: true }; break; - } + } if (setUrl && options && this.whatsappServiceUrl) { GBServer.globals.server.use(`/audios`, express.static('work')); @@ -327,7 +329,7 @@ export class WhatsappDirectLine extends GBService { } public static providerFromRequest(req: any) { - return req.body.messages ? 'chatapi' : req.body.message ? 'maytapi' : 'GeneralBots'; + return req.body.messages ? 'chatapi' : req.body.message ? 'maytapi' : req.body.message ? 'graphapi' : 'GeneralBots'; } public async received(req, res) { @@ -379,6 +381,8 @@ export class WhatsappDirectLine extends GBService { } break; + case 'graphapi': + break; case 'maytapi': message = req.body.message; @@ -621,7 +625,7 @@ export class WhatsappDirectLine extends GBService { } } - private async endTransfer(id: any, locale: string, user: GuaribasUser, agent: GuaribasUser, sec: SecService) { + 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.charAt(2) === ':') { @@ -643,7 +647,7 @@ export class WhatsappDirectLine extends GBService { await sec.updateHumanAgent(id, this.min.instance.instanceId, null); } - public inputMessage(client, conversationId, text, from, fromName, group, attachments) { + public inputMessage(client, conversationId: string, text: string, from, fromName: string, group, attachments: File) { return client.Conversations.Conversations_PostActivity({ conversationId: conversationId, activity: { @@ -794,7 +798,24 @@ 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, + } + } } + if (options) { try { // tslint:disable-next-line: await-promise @@ -845,7 +866,7 @@ export class WhatsappDirectLine extends GBService { } } - public async sendTextAsAudioToDevice(to, msg, chatId) { + public async sendTextAsAudioToDevice(to, msg: string, chatId) { const url = await GBConversationalService.getAudioBufferFromText(msg); await this.sendFileToDevice(to, url, 'Audio', msg, chatId); @@ -906,6 +927,8 @@ export class WhatsappDirectLine extends GBService { } }; break; + case 'graphapi': + } if (options) { diff --git a/src/app.ts b/src/app.ts index b0b9d41c..eb4c41f8 100644 --- a/src/app.ts +++ b/src/app.ts @@ -96,7 +96,7 @@ export class GBServer { server.use(bodyParser.json()); server.use(bodyParser.urlencoded({ extended: true })); - process.on('unhandledRejection', (err, p) => { + process.on('unhandledRejection', (err, p) => { GBLog.error(`UNHANDLED_REJECTION(promises): ${p} ${err.toString()}`); }); @@ -126,7 +126,7 @@ export class GBServer { const azureDeployer: AzureDeployerService = await AzureDeployerService.createInstance(deployer); const adminService: GBAdminService = new GBAdminService(core); - if (process.env.NODE_ENV === 'development' ) { + if (process.env.NODE_ENV === 'development') { const proxy = GBConfigService.get('BOT_URL'); if (proxy !== undefined) { GBServer.globals.publicAddress = proxy; @@ -228,6 +228,12 @@ export class GBServer { winston.default(server, loggers[1]); } + server.get('*', function(req, res){ + GBLog.info(`HTTP 404: ${req.url}.`); + res.status(404); + res.end(); + }); + GBLog.info(`The Bot Server is in RUNNING mode...`); // Opens Navigator. diff --git a/swagger.json b/swagger.json index 0499bef4..2f7b8320 100644 --- a/swagger.json +++ b/swagger.json @@ -13,7 +13,7 @@ "status": "Production" } }, - "host": "generalbots.ai", + "host": "", "basePath": "/", "schemes": [ "https" @@ -21,7 +21,7 @@ "consumes": [], "produces": [], "paths": { - "/api/talkTo": { + "/api/v2//dialog/talk": { "post": { "summary": "Talk to the user.", "description": "Talk to the user.", @@ -58,9 +58,6 @@ "properties": { "pid": { "type": "string" - }, - "mobile": { - "type": "string" }, "text": { "type": "string" From 954eb8c94e1ab8f390e8990d26a9cb0e91a40ea3 Mon Sep 17 00:00:00 2001 From: rodrigorodriguez Date: Fri, 10 Feb 2023 08:52:04 -0300 Subject: [PATCH 02/29] new(basic.gblib): Power Platform integration. --- swagger.json | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/swagger.json b/swagger.json index 2f7b8320..90c85a71 100644 --- a/swagger.json +++ b/swagger.json @@ -23,32 +23,18 @@ "paths": { "/api/v2//dialog/talk": { "post": { + "requestBody":{ + "content": "text/plain" + }, "summary": "Talk to the user.", "description": "Talk to the user.", "x-ms-no-generic-test": true, - "operationId": "talkTo", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "name": "content", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/TalkRequest" - } - } - ], + "operationId": "talk", "responses": { "200": { "description": "OK" } - }, - "deprecated": false + } } } }, From e2e17c3bc233aab62898761a1b63003298e27bdd Mon Sep 17 00:00:00 2001 From: rodrigorodriguez Date: Sun, 12 Feb 2023 14:31:21 -0300 Subject: [PATCH 03/29] new(all): Log per bot started. #299. --- packages/admin.gbapp/dialogs/AdminDialog.ts | 18 +++ packages/basic.gblib/services/GBVMService.ts | 28 ++-- 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 + 8 files changed, 197 insertions(+), 79 deletions(-) create mode 100644 packages/core.gbapp/services/GBLogEx.ts 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/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. */ From b5dcbe41eb5c155cf3505bba51135cd8dfebc23f Mon Sep 17 00:00:00 2001 From: rodrigorodriguez Date: Mon, 13 Feb 2023 17:31:38 -0300 Subject: [PATCH 04/29] new(all): #329 MERGE keyword added. --- package.json | 1 + .../services/ImageProcessingServices.ts | 32 +++++++++++++++++++ .../services/KeywordsExpressions.ts | 7 ++++ .../basic.gblib/services/SystemKeywords.ts | 1 + 4 files changed, 41 insertions(+) diff --git a/package.json b/package.json index 835737bb..033f81bf 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/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) { From 2e83f2b86d1344d930623a57d6f06ede2ba49f0d Mon Sep 17 00:00:00 2001 From: Alan Date: Tue, 14 Feb 2023 13:58:17 -0300 Subject: [PATCH 05/29] 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 ad607a967f065207b43142f231985f3399ce0e42 Mon Sep 17 00:00:00 2001 From: Alan Date: Tue, 14 Feb 2023 14:11:52 -0300 Subject: [PATCH 06/29] 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. */ From f4aa2d6ed179223a5a70a2348d486055f217b796 Mon Sep 17 00:00:00 2001 From: rodrigorodriguez Date: Wed, 15 Feb 2023 22:12:24 -0300 Subject: [PATCH 07/29] new(all): #327 OPEN AS web automation. --- .travis.yml | 2 +- package.json | 3 +- .../services/KeywordsExpressions.ts | 13 +++- .../services/WebAutomationServices.ts | 67 ++++++++++++------- packages/core.gbapp/services/GBLogEx.ts | 3 +- packages/core.gbapp/services/GBSSR.ts | 15 +++-- src/RootData.ts | 1 + src/app.ts | 1 + 8 files changed, 70 insertions(+), 35 deletions(-) diff --git a/.travis.yml b/.travis.yml index 158ca137..8b252f2a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ dist: focal language: node_js node_js: - - 19.5.0 + - 19.6.0 notifications: diff --git a/package.json b/package.json index 033f81bf..f6a40293 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "Dário Vieira " ], "engines": { - "node": "=19.5.0" + "node": "=19.6.0" }, "license": "AGPL-3.0", "preferGlobal": true, @@ -76,6 +76,7 @@ "alasql": "2.1.6", "any-shell-escape": "0.1.1", "arraybuffer-to-buffer": "^0.0.7", + "async-mutex": "^0.4.0", "async-promises": "0.2.3", "basic-auth": "2.0.1", "billboard.js": "3.6.3", diff --git a/packages/basic.gblib/services/KeywordsExpressions.ts b/packages/basic.gblib/services/KeywordsExpressions.ts index 8df1cd92..e5241c31 100644 --- a/packages/basic.gblib/services/KeywordsExpressions.ts +++ b/packages/basic.gblib/services/KeywordsExpressions.ts @@ -140,12 +140,21 @@ export class KeywordsExpressions { keywords[i++] = [ /^\s*open\s*(.*)/gim, ($0, $1, $2) => { + + let pos; + let sessionName; + if (pos = $1.match(/\s*AS\s*\#/)){ + let part = $1.substr($1.lastIndexOf(pos[0])); + sessionName = `"${part.substr(part.indexOf("#") + 1)}"`; + $1 = $1.substr(0, $1.lastIndexOf(pos[0])); + } + if (!$1.startsWith('"') && !$1.startsWith("'")) { $1 = `"${$1}"`; } const params = this.getParams($1, ['url', 'username', 'password']); - - return `page = await wa.getPage({pid: pid,${params}})`; + + return `page = await wa.getPage({pid: pid, sessionName: ${sessionName}, ${params}})`; } ]; diff --git a/packages/basic.gblib/services/WebAutomationServices.ts b/packages/basic.gblib/services/WebAutomationServices.ts index d2e873fb..dae6dad5 100644 --- a/packages/basic.gblib/services/WebAutomationServices.ts +++ b/packages/basic.gblib/services/WebAutomationServices.ts @@ -44,12 +44,15 @@ import urlJoin from 'url-join'; import Fs from 'fs'; import Path from 'path'; import url from 'url'; -import { pid } from 'process'; +import {Mutex, Semaphore, withTimeout} from 'async-mutex'; /** * Web Automation services of conversation to be called by BASIC. */ export class WebAutomationServices { + + static semaphoreWithTimeout = withTimeout(new Semaphore(5), 60 * 1000, new Error('new fancy error')); + /** * Reference to minimal bot instance. */ @@ -107,7 +110,7 @@ export class WebAutomationServices { * When creating this keyword facade,a bot instance is * specified among the deployer service. */ - constructor (min: GBMinInstance, user, dk) { + constructor(min: GBMinInstance, user, dk) { this.min = min; this.user = user; this.dk = dk; @@ -120,25 +123,37 @@ export class WebAutomationServices { * * @example OPEN "https://wikipedia.org" */ - public async getPage ({ pid, url, username, password }) { - GBLog.info(`BASIC: Web Automation GET PAGE ${url}.`); - if (!this.browser) { - this.browser = await createBrowser(null); - } - const page = (await this.browser.pages())[0]; - if (username || password) { - await page.authenticate({pid, username: username, password: password }); + + public async getPage({ pid, sessionName, url, username, password }) { + GBLog.info(`BASIC: Web Automation GET PAGE ${sessionName ? sessionName : ''} ${url}.`); + + let page; + if (url.startsWith('#')) { + const [value, release] = await WebAutomationServices.semaphoreWithTimeout.acquire(); + try { + page = GBServer.globals.webSessions[url.substr(1)]; + } finally { + release(); + } + } else { + if (!this.browser) { + this.browser = await createBrowser(null); + } + page = (await this.browser.pages())[0]; + if (sessionName) { + GBServer.globals.webSessions[sessionName] = page; + } + if (username || password) { + await page.authenticate({ pid, username: username, password: password }); + } } await page.goto(url); - const handle = WebAutomationServices.cyrb53(this.min.botId + url); - this.pageMap[handle] = page; - return handle; } - public getPageByHandle (hash) { + public getPageByHandle(hash) { return this.pageMap[hash]; } @@ -147,7 +162,7 @@ export class WebAutomationServices { * * @example GET "selector" */ - public async getBySelector ({ handle, selector }) { + public async getBySelector({ handle, selector }) { const page = this.getPageByHandle(handle); GBLog.info(`BASIC: Web Automation GET element: ${selector}.`); await page.waitForSelector(selector); @@ -170,7 +185,7 @@ export class WebAutomationServices { * * @example GET page,"frameSelector,"elementSelector" */ - public async getByFrame ({ handle, frame, selector }) { + public async getByFrame({ handle, frame, selector }) { const page = this.getPageByHandle(handle); GBLog.info(`BASIC: Web Automation GET element by frame: ${selector}.`); await page.waitForSelector(frame); @@ -190,7 +205,7 @@ export class WebAutomationServices { /** * Simulates a mouse hover an web page element. */ - public async hover ({ pid, handle, selector }) { + public async hover({ pid, handle, selector }) { const page = this.getPageByHandle(handle); GBLog.info(`BASIC: Web Automation HOVER element: ${selector}.`); await this.getBySelector({ handle, selector: selector }); @@ -203,7 +218,7 @@ export class WebAutomationServices { * * @example CLICK page,"#idElement" */ - public async click ({ pid, handle, frameOrSelector, selector }) { + public async click({ pid, handle, frameOrSelector, selector }) { const page = this.getPageByHandle(handle); GBLog.info(`BASIC: Web Automation CLICK element: ${frameOrSelector}.`); if (selector) { @@ -219,7 +234,7 @@ export class WebAutomationServices { await this.debugStepWeb(pid, page); } - private async debugStepWeb (pid, page) { + private async debugStepWeb(pid, page) { let refresh = true; if (this.lastDebugWeb) { refresh = new Date().getTime() - this.lastDebugWeb.getTime() > 5000; @@ -229,7 +244,7 @@ export class WebAutomationServices { const mobile = this.min.core.getParam(this.min.instance, 'Bot Admin Number', null); const filename = page; if (mobile) { - await this.dk.sendFileTo({pid: pid, mobile, filename, caption: 'General Bots Debugger' }); + await this.dk.sendFileTo({ pid: pid, mobile, filename, caption: 'General Bots Debugger' }); } this.lastDebugWeb = new Date(); } @@ -240,7 +255,7 @@ export class WebAutomationServices { * * @example PRESS ENTER ON page */ - public async pressKey ({ handle, char, frame }) { + public async pressKey({ handle, char, frame }) { const page = this.getPageByHandle(handle); GBLog.info(`BASIC: Web Automation PRESS ${char} ON element: ${frame}.`); if (char.toLowerCase() === 'enter') { @@ -256,7 +271,7 @@ export class WebAutomationServices { } } - public async linkByText ({ pid, handle, text, index }) { + public async linkByText({ pid, handle, text, index }) { const page = this.getPageByHandle(handle); GBLog.info(`BASIC: Web Automation CLICK LINK TEXT: ${text} ${index}.`); if (!index) { @@ -272,7 +287,7 @@ export class WebAutomationServices { * * @example file = SCREENSHOT page */ - public async screenshot ({ handle, selector }) { + public async screenshot({ handle, selector }) { const page = this.getPageByHandle(handle); GBLog.info(`BASIC: Web Automation SCREENSHOT ${selector}.`); @@ -292,7 +307,7 @@ export class WebAutomationServices { * * @example SET page,"selector","text" */ - public async setElementText ({ pid, handle, selector, text }) { + public async setElementText({ pid, handle, selector, text }) { const page = this.getPageByHandle(handle); GBLog.info(`BASIC: Web Automation TYPE on ${selector}: ${text}.`); const e = await this.getBySelector({ handle, selector }); @@ -307,9 +322,9 @@ export class WebAutomationServices { * * @example file = DOWNLOAD element, folder */ - public async download ({ handle, selector, folder }) { + public async download({ handle, selector, folder }) { const page = this.getPageByHandle(handle); - + const element = await this.getBySelector({ handle, selector }); // https://github.com/GeneralBots/BotServer/issues/311 const container = element['_frame'] ? element['_frame'] : element['_page']; diff --git a/packages/core.gbapp/services/GBLogEx.ts b/packages/core.gbapp/services/GBLogEx.ts index 37f5a03b..5a1b0fc8 100644 --- a/packages/core.gbapp/services/GBLogEx.ts +++ b/packages/core.gbapp/services/GBLogEx.ts @@ -37,7 +37,7 @@ 'use strict'; import { GBLog, IGBInstance } from "botlib"; -import { GuaribasLog } from "../models/GBModel"; +import { GuaribasLog } from "../models/GBModel.js"; export class GBLogEx { public static async error(minOrInstanceId: any, message: string) { @@ -76,6 +76,7 @@ export class GBLogEx { * Finds and update user agent information to a next available person. */ public static async log(instance: IGBInstance, kind: string, message: string): Promise { + message = message?message.substring(0,1023):null; return await GuaribasLog.create({ instanceId: instance.instanceId, message: message, diff --git a/packages/core.gbapp/services/GBSSR.ts b/packages/core.gbapp/services/GBSSR.ts index 3148c4c7..8f361f13 100644 --- a/packages/core.gbapp/services/GBSSR.ts +++ b/packages/core.gbapp/services/GBSSR.ts @@ -36,11 +36,17 @@ 'use strict'; -import puppeteer from 'puppeteer-extra'; + +import {createRequire} from "module"; +const require = createRequire(import.meta.url); + +const puppeteer = require('puppeteer-extra'); +const hidden = require('puppeteer-extra-plugin-stealth') + +// require executablePath from puppeteer +const {executablePath} = require('puppeteer') import Fs from 'fs'; -// const StealthPlugin from 'puppeteer-extra-plugin-stealth') -// puppeteer.use(StealthPlugin()); import { NextFunction, Request, Response } from 'express'; import urljoin from 'url-join'; @@ -96,12 +102,13 @@ async function createBrowser (profilePath): Promise { Fs.writeFileSync(preferences, JSON.stringify(data)); } } - + puppeteer.use(hidden()) const browser = await puppeteer.launch({ args: args, ignoreHTTPSErrors: true, headless: false, defaultViewport: null, + executablePath:executablePath(), ignoreDefaultArgs: ['--enable-automation', '--enable-blink-features=IdleDetection'] }); return browser; diff --git a/src/RootData.ts b/src/RootData.ts index 2cc40292..bf89456f 100644 --- a/src/RootData.ts +++ b/src/RootData.ts @@ -42,6 +42,7 @@ import { GBMinService } from '../packages/core.gbapp/services/GBMinService.js'; */ export class RootData { + public webSessions: {} // List of Web Automation sessions. public processes: {}; // List of .gbdialog active executions. public files: {}; // List of uploaded files handled. public publicAddress: string; // URI for BotServer. diff --git a/src/app.ts b/src/app.ts index eb4c41f8..ba141904 100644 --- a/src/app.ts +++ b/src/app.ts @@ -84,6 +84,7 @@ export class GBServer { const server = express(); GBServer.globals.server = server; + GBServer.globals.webSessions = {}; GBServer.globals.processes = {}; GBServer.globals.files = {}; GBServer.globals.appPackages = []; From a6f705f823b6eb9793fd016793a9a65fe2f613f0 Mon Sep 17 00:00:00 2001 From: rodrigorodriguez Date: Thu, 16 Feb 2023 10:27:18 -0300 Subject: [PATCH 08/29] new(all): #327 OPEN AS web automation. --- packages/basic.gblib/services/GBVMService.ts | 66 ++++++++++--------- .../services/KeywordsExpressions.ts | 23 ++++--- .../services/WebAutomationServices.ts | 48 +++++++++----- 3 files changed, 82 insertions(+), 55 deletions(-) diff --git a/packages/basic.gblib/services/GBVMService.ts b/packages/basic.gblib/services/GBVMService.ts index 1b19cbba..a7470a8e 100644 --- a/packages/basic.gblib/services/GBVMService.ts +++ b/packages/basic.gblib/services/GBVMService.ts @@ -32,7 +32,7 @@ 'use strict'; -import { 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'; @@ -61,10 +61,8 @@ import { GBLogEx } from '../../core.gbapp/services/GBLogEx.js'; * Basic services for BASIC manipulation. */ export class GBVMService extends GBService { - private static DEBUGGER_PORT = 9222; - public async loadDialogPackage(folder: string, min: GBMinInstance, core: IGBCoreService, deployer: GBDeployer) { const files = await walkPromise(folder); @@ -161,7 +159,7 @@ export class GBVMService extends GBService { }); } - public async translateBASIC(filename: any, mainName: string, min:GBMinInstance) { + public async translateBASIC(filename: any, mainName: string, min: GBMinInstance) { // Converts General Bots BASIC into regular VBS let basicCode: string = Fs.readFileSync(filename, 'utf8'); @@ -276,7 +274,6 @@ export class GBVMService extends GBService { }); } - /** * Converts General Bots BASIC * @@ -312,7 +309,6 @@ export class GBVMService extends GBService { * Executes the converted JavaScript from BASIC code inside execution context. */ public static async callVM(text: string, min: GBMinInstance, step, deployer: GBDeployer, debug: boolean) { - // Creates a class DialogKeywords which is the *this* pointer // in BASIC. @@ -351,9 +347,9 @@ export class GBVMService extends GBService { }; sandbox['id'] = dk.sys().getRandomId(); - sandbox['username'] = await dk.userName({pid}); - sandbox['mobile'] = await dk.userMobile({pid}); - sandbox['from'] = await dk.userMobile({pid}); + sandbox['username'] = await dk.userName({ pid }); + sandbox['mobile'] = await dk.userMobile({ pid }); + sandbox['from'] = await dk.userMobile({ pid }); sandbox['ENTER'] = String.fromCharCode(13); sandbox['headers'] = {}; sandbox['data'] = {}; @@ -362,8 +358,10 @@ export class GBVMService extends GBService { sandbox['httpPs'] = ''; sandbox['pid'] = pid; - if (GBConfigService.get('GBVM') === 'false') { - try { + let result; + + try { + if (GBConfigService.get('GBVM') === 'false') { const vm1 = new NodeVM({ allowAsync: true, sandbox: sandbox, @@ -377,23 +375,18 @@ export class GBVMService extends GBService { } }); const s = new VMScript(code, { filename: scriptPath }); - let x = vm1.run(s); - return x; - } catch (error) { - throw new Error(`BASIC RUNTIME ERR: ${error.message ? error.message : error}\n Stack:${error.stack}`); - } - } else { - const runnerPath = urlJoin( - process.cwd(), - 'dist', - 'packages', - 'basic.gblib', - 'services', - 'vm2-process', - 'vm2ProcessRunner.js' - ); + result = vm1.run(s); + } else { + const runnerPath = urlJoin( + process.cwd(), + 'dist', + 'packages', + 'basic.gblib', + 'services', + 'vm2-process', + 'vm2ProcessRunner.js' + ); - try { const { run } = createVm2Pool({ min: 0, max: 0, @@ -407,12 +400,23 @@ export class GBVMService extends GBService { script: runnerPath }); - const result = await run(code, { filename: scriptPath, sandbox: sandbox }); + result = await run(code, { filename: scriptPath, sandbox: sandbox }); + } + } catch (error) { + throw new Error(`BASIC RUNTIME ERR: ${error.message ? error.message : error}\n Stack:${error.stack}`); + } finally { + + // Releases previous allocated OPEN semaphores. - return result; - } catch (error) { - throw new Error(`BASIC RUNTIME ERR: ${error.message ? error.message : error}\n Stack:${error.stack}`); + let keys = Object.keys(GBServer.globals.webSessions); + for (let i = 0; i < keys.length; i++) { + const session = GBServer.globals.webSessions[keys[i]]; + if (session.pid === pid) { + session.semaphore.release(); + } } } + + return result; } } diff --git a/packages/basic.gblib/services/KeywordsExpressions.ts b/packages/basic.gblib/services/KeywordsExpressions.ts index e5241c31..89d056e2 100644 --- a/packages/basic.gblib/services/KeywordsExpressions.ts +++ b/packages/basic.gblib/services/KeywordsExpressions.ts @@ -36,8 +36,7 @@ * Image processing services of conversation to be called by BASIC. */ export class KeywordsExpressions { - - private static getParams = (text: string, names) => { + private static getParams = (text: string, names) => { let ret = {}; const splitParamsButIgnoreCommasInDoublequotes = (str: string) => { return str.split(',').reduce( @@ -74,7 +73,6 @@ export class KeywordsExpressions { * Returns the list of BASIC keyword and their JS match. */ public static getKeywords() { - // Keywords from General Bots BASIC. let keywords = []; @@ -140,12 +138,19 @@ export class KeywordsExpressions { keywords[i++] = [ /^\s*open\s*(.*)/gim, ($0, $1, $2) => { - - let pos; let sessionName; - if (pos = $1.match(/\s*AS\s*\#/)){ + let kind = ''; + let pos; + + if (pos = $1.match(/\s*AS\s*\#/)) { + kind = '"AS"'; + } else if (pos = $1.match(/\s*WITH\s*\#/)) { + kind = '"WITH"'; + } + + if (pos) { let part = $1.substr($1.lastIndexOf(pos[0])); - sessionName = `"${part.substr(part.indexOf("#") + 1)}"`; + sessionName = `"${part.substr(part.indexOf('#') + 1)}"`; $1 = $1.substr(0, $1.lastIndexOf(pos[0])); } @@ -153,8 +158,8 @@ export class KeywordsExpressions { $1 = `"${$1}"`; } const params = this.getParams($1, ['url', 'username', 'password']); - - return `page = await wa.getPage({pid: pid, sessionName: ${sessionName}, ${params}})`; + + return `page = await wa.getPage({pid: pid, sessionKind: ${kind}, sessionName: ${sessionName}, ${params}})`; } ]; diff --git a/packages/basic.gblib/services/WebAutomationServices.ts b/packages/basic.gblib/services/WebAutomationServices.ts index dae6dad5..303da106 100644 --- a/packages/basic.gblib/services/WebAutomationServices.ts +++ b/packages/basic.gblib/services/WebAutomationServices.ts @@ -44,15 +44,12 @@ import urlJoin from 'url-join'; import Fs from 'fs'; import Path from 'path'; import url from 'url'; -import {Mutex, Semaphore, withTimeout} from 'async-mutex'; +import { Mutex, Semaphore, withTimeout } from 'async-mutex'; /** * Web Automation services of conversation to be called by BASIC. */ export class WebAutomationServices { - - static semaphoreWithTimeout = withTimeout(new Semaphore(5), 60 * 1000, new Error('new fancy error')); - /** * Reference to minimal bot instance. */ @@ -123,26 +120,47 @@ export class WebAutomationServices { * * @example OPEN "https://wikipedia.org" */ - - public async getPage({ pid, sessionName, url, username, password }) { + + public async getPage({ pid, sessionKind, sessionName, url, username, password }) { GBLog.info(`BASIC: Web Automation GET PAGE ${sessionName ? sessionName : ''} ${url}.`); + // Semaphore logic to block multiple entries on the same session. + let page; - if (url.startsWith('#')) { - const [value, release] = await WebAutomationServices.semaphoreWithTimeout.acquire(); + let session = GBServer.globals.webSessions[sessionName]; + + if (session) { + const [value, release] = await session.semaphore.acquire(); try { + GBServer.globals.webSessions[sessionName].release = release; page = GBServer.globals.webSessions[url.substr(1)]; - } finally { - release(); - } + } catch { + release(); + } + } + + // There is no session yet, + + if (!session && sessionKind === 'AS') { + + // A new web session is being created. + + GBServer.globals.webSessions[sessionName] = {}; + GBServer.globals.webSessions[sessionName].pid = pid; + GBServer.globals.webSessions[sessionName].page = page; + GBServer.globals.webSessions[sessionName].semaphore = withTimeout( + new Semaphore(5), + 60 * 1000, + new Error('Error waiting for OPEN keyword.') + ); + } + + if (url.startsWith('#') && sessionKind == 'WITH') { } else { if (!this.browser) { this.browser = await createBrowser(null); - } - page = (await this.browser.pages())[0]; - if (sessionName) { - GBServer.globals.webSessions[sessionName] = page; } + page = (await this.browser.pages())[0]; if (username || password) { await page.authenticate({ pid, username: username, password: password }); } From 72453049adc699c16e00649fa075a41ed4bb0d7b Mon Sep 17 00:00:00 2001 From: rodrigorodriguez Date: Thu, 16 Feb 2023 18:12:21 -0300 Subject: [PATCH 09/29] new(all): #327 OPEN AS web automation. --- .../services/WebAutomationServices.ts | 63 ++++++++++++------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/packages/basic.gblib/services/WebAutomationServices.ts b/packages/basic.gblib/services/WebAutomationServices.ts index 303da106..2e4d1daa 100644 --- a/packages/basic.gblib/services/WebAutomationServices.ts +++ b/packages/basic.gblib/services/WebAutomationServices.ts @@ -45,6 +45,7 @@ import Fs from 'fs'; import Path from 'path'; import url from 'url'; import { Mutex, Semaphore, withTimeout } from 'async-mutex'; +import { GBLogEx } from '../../core.gbapp/services/GBLogEx.js'; /** * Web Automation services of conversation to be called by BASIC. @@ -86,8 +87,6 @@ export class WebAutomationServices { */ maxLines: number = 2000; - pageMap = {}; - public static cyrb53 = (str, seed = 0) => { let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed; @@ -123,12 +122,25 @@ export class WebAutomationServices { public async getPage({ pid, sessionKind, sessionName, url, username, password }) { GBLog.info(`BASIC: Web Automation GET PAGE ${sessionName ? sessionName : ''} ${url}.`); + const { min, user } = await DialogKeywords.getProcessInfo(pid); + + let handle; + // Try to find an existing handle. + + let session = GBServer.globals.webSessions[sessionName]; + let keys = Object.keys(GBServer.globals.webSessions); + for (let i = 0; i < keys.length; i++) { + const session = GBServer.globals.webSessions[keys[i]]; + if (session.sessionName === sessionName) { + handle = keys[i]; + break; + } + } + // Semaphore logic to block multiple entries on the same session. let page; - let session = GBServer.globals.webSessions[sessionName]; - if (session) { const [value, release] = await session.semaphore.acquire(); try { @@ -139,40 +151,49 @@ export class WebAutomationServices { } } + // Creates the page if it is the first time. + + let browser; + if (!page) { + browser = await createBrowser(null); + page = (await this.browser.pages())[0]; + if (username || password) { + await page.authenticate({ pid, username: username, password: password }); + } + } + // There is no session yet, if (!session && sessionKind === 'AS') { // A new web session is being created. - GBServer.globals.webSessions[sessionName] = {}; - GBServer.globals.webSessions[sessionName].pid = pid; - GBServer.globals.webSessions[sessionName].page = page; - GBServer.globals.webSessions[sessionName].semaphore = withTimeout( + handle = WebAutomationServices.cyrb53(this.min.botId + url); + GBServer.globals.webSessions[handle] = {}; + GBServer.globals.webSessions[handle].sessionName = sessionName; + GBServer.globals.webSessions[handle].pid = pid; + GBServer.globals.webSessions[handle].page = page; + GBServer.globals.webSessions[handle].browser = browser; + GBServer.globals.webSessions[handle].semaphore = withTimeout( new Semaphore(5), 60 * 1000, new Error('Error waiting for OPEN keyword.') ); } - if (url.startsWith('#') && sessionKind == 'WITH') { - } else { - if (!this.browser) { - this.browser = await createBrowser(null); - } - page = (await this.browser.pages())[0]; - if (username || password) { - await page.authenticate({ pid, username: username, password: password }); - } + // WITH is only valid in a previously defined session. + + if (!session && sessionKind == 'WITH') { + GBLogEx.error(min, `NULL session for OPEN WITH #${sessionName}.`); } + await page.goto(url); - const handle = WebAutomationServices.cyrb53(this.min.botId + url); - this.pageMap[handle] = page; + return handle; } - public getPageByHandle(hash) { - return this.pageMap[hash]; + public getPageByHandle(handle) { + return GBServer.globals.webSessions[handle].page; } /** From 87cf9d1b00c86bead09c3e388fe08a05af5bc6d7 Mon Sep 17 00:00:00 2001 From: rodrigorodriguez Date: Fri, 17 Feb 2023 06:32:30 -0300 Subject: [PATCH 10/29] new(all): #327 OPEN AS web automation. --- packages/basic.gblib/services/GBVMService.ts | 12 +++------ .../services/WebAutomationServices.ts | 26 ++++++++++++++----- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/packages/basic.gblib/services/GBVMService.ts b/packages/basic.gblib/services/GBVMService.ts index a7470a8e..d5b88784 100644 --- a/packages/basic.gblib/services/GBVMService.ts +++ b/packages/basic.gblib/services/GBVMService.ts @@ -237,6 +237,8 @@ export class GBVMService extends GBService { ${code} + await wa.closeHandles({pid: pid}); + })(); `; @@ -406,15 +408,7 @@ export class GBVMService extends GBService { throw new Error(`BASIC RUNTIME ERR: ${error.message ? error.message : error}\n Stack:${error.stack}`); } finally { - // Releases previous allocated OPEN semaphores. - - let keys = Object.keys(GBServer.globals.webSessions); - for (let i = 0; i < keys.length; i++) { - const session = GBServer.globals.webSessions[keys[i]]; - if (session.pid === pid) { - session.semaphore.release(); - } - } + } return result; diff --git a/packages/basic.gblib/services/WebAutomationServices.ts b/packages/basic.gblib/services/WebAutomationServices.ts index 2e4d1daa..0581b2fd 100644 --- a/packages/basic.gblib/services/WebAutomationServices.ts +++ b/packages/basic.gblib/services/WebAutomationServices.ts @@ -114,6 +114,18 @@ export class WebAutomationServices { this.debugWeb = this.min.core.getParam(this.min.instance, 'Debug Web Automation', false); } + public async closeHandles({pid}){ + // Releases previous allocated OPEN semaphores. + + let keys = Object.keys(GBServer.globals.webSessions); + for (let i = 0; i < keys.length; i++) { + const session = GBServer.globals.webSessions[keys[i]]; + if (session.pid === pid) { + session.semaphore.release(); + } + } + } + /** * Returns the page object. * @@ -128,11 +140,11 @@ export class WebAutomationServices { // Try to find an existing handle. - let session = GBServer.globals.webSessions[sessionName]; + let session; let keys = Object.keys(GBServer.globals.webSessions); for (let i = 0; i < keys.length; i++) { - const session = GBServer.globals.webSessions[keys[i]]; - if (session.sessionName === sessionName) { + if (GBServer.globals.webSessions[keys[i]].sessionName === sessionName) { + session = GBServer.globals.webSessions[keys[i]]; handle = keys[i]; break; } @@ -144,8 +156,8 @@ export class WebAutomationServices { if (session) { const [value, release] = await session.semaphore.acquire(); try { - GBServer.globals.webSessions[sessionName].release = release; - page = GBServer.globals.webSessions[url.substr(1)]; + GBServer.globals.webSessions[handle].release = release; + page = session.page; } catch { release(); } @@ -156,7 +168,7 @@ export class WebAutomationServices { let browser; if (!page) { browser = await createBrowser(null); - page = (await this.browser.pages())[0]; + page = (await browser.pages())[0]; if (username || password) { await page.authenticate({ pid, username: username, password: password }); } @@ -169,7 +181,7 @@ export class WebAutomationServices { // A new web session is being created. handle = WebAutomationServices.cyrb53(this.min.botId + url); - GBServer.globals.webSessions[handle] = {}; + GBServer.globals.webSessions[handle] = session= {}; GBServer.globals.webSessions[handle].sessionName = sessionName; GBServer.globals.webSessions[handle].pid = pid; GBServer.globals.webSessions[handle].page = page; From 4b880ecf5e8b76ef915df5eb60fd375a0de12b54 Mon Sep 17 00:00:00 2001 From: rodrigorodriguez Date: Fri, 17 Feb 2023 15:34:29 -0300 Subject: [PATCH 11/29] new(all): #327 OPEN AS web automation. --- packages/basic.gblib/services/GBVMService.ts | 2 +- .../services/WebAutomationServices.ts | 59 +++++++++++-------- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/packages/basic.gblib/services/GBVMService.ts b/packages/basic.gblib/services/GBVMService.ts index d5b88784..8558ec2a 100644 --- a/packages/basic.gblib/services/GBVMService.ts +++ b/packages/basic.gblib/services/GBVMService.ts @@ -237,7 +237,7 @@ export class GBVMService extends GBService { ${code} - await wa.closeHandles({pid: pid}); + await wa.getCloseHandles({pid: pid}); })(); diff --git a/packages/basic.gblib/services/WebAutomationServices.ts b/packages/basic.gblib/services/WebAutomationServices.ts index 0581b2fd..0b709c33 100644 --- a/packages/basic.gblib/services/WebAutomationServices.ts +++ b/packages/basic.gblib/services/WebAutomationServices.ts @@ -114,16 +114,18 @@ export class WebAutomationServices { this.debugWeb = this.min.core.getParam(this.min.instance, 'Debug Web Automation', false); } - public async closeHandles({pid}){ - // Releases previous allocated OPEN semaphores. + public async getCloseHandles({ pid }) { + const { min, user } = await DialogKeywords.getProcessInfo(pid); + // Releases previous allocated OPEN semaphores. - let keys = Object.keys(GBServer.globals.webSessions); - for (let i = 0; i < keys.length; i++) { - const session = GBServer.globals.webSessions[keys[i]]; - if (session.pid === pid) { - session.semaphore.release(); - } + let keys = Object.keys(GBServer.globals.webSessions); + for (let i = 0; i < keys.length; i++) { + const session = GBServer.globals.webSessions[keys[i]]; + if (session.activePid === pid) { + session.semaphore.release(); + GBLogEx.info(min, `Release for PID: ${pid} done.`); } + } } /** @@ -135,7 +137,7 @@ export class WebAutomationServices { public async getPage({ pid, sessionKind, sessionName, url, username, password }) { GBLog.info(`BASIC: Web Automation GET PAGE ${sessionName ? sessionName : ''} ${url}.`); const { min, user } = await DialogKeywords.getProcessInfo(pid); - + let handle; // Try to find an existing handle. @@ -149,14 +151,17 @@ export class WebAutomationServices { break; } } - + // Semaphore logic to block multiple entries on the same session. let page; if (session) { - const [value, release] = await session.semaphore.acquire(); + GBLogEx.info(min, `Acquiring (1) for PID: ${pid}...`); + const release = await session.semaphore.acquire(); + GBLogEx.info(min, `Acquire (1) for PID: ${pid} done.`); try { - GBServer.globals.webSessions[handle].release = release; + session.activePid = pid; + session.release = release; page = session.page; } catch { release(); @@ -164,7 +169,7 @@ export class WebAutomationServices { } // Creates the page if it is the first time. - + let browser; if (!page) { browser = await createBrowser(null); @@ -177,28 +182,30 @@ export class WebAutomationServices { // There is no session yet, if (!session && sessionKind === 'AS') { - // A new web session is being created. handle = WebAutomationServices.cyrb53(this.min.botId + url); - GBServer.globals.webSessions[handle] = session= {}; - GBServer.globals.webSessions[handle].sessionName = sessionName; - GBServer.globals.webSessions[handle].pid = pid; - GBServer.globals.webSessions[handle].page = page; - GBServer.globals.webSessions[handle].browser = browser; - GBServer.globals.webSessions[handle].semaphore = withTimeout( - new Semaphore(5), - 60 * 1000, - new Error('Error waiting for OPEN keyword.') - ); + GBServer.globals.webSessions[handle] = session = {}; + session.sessionName = sessionName; + + session.page = page; + session.browser = browser; + session.semaphore = new Mutex(); + GBLogEx.info(min, `Acquiring (2) for PID: ${pid}...`); + const release = await session.semaphore.acquire(); + GBLogEx.info(min, `Acquire (2) for PID: ${pid} done.`); + session.release = release; + session.activePid = pid; + } // WITH is only valid in a previously defined session. if (!session && sessionKind == 'WITH') { - GBLogEx.error(min, `NULL session for OPEN WITH #${sessionName}.`); + const error = `NULL session for OPEN WITH #${sessionName}.`; + GBLogEx.error(min, error); } - + await page.goto(url); return handle; From 6605de5c5db5552e24e4c220db984f2085aa6691 Mon Sep 17 00:00:00 2001 From: rodrigorodriguez Date: Sat, 18 Feb 2023 16:48:40 -0300 Subject: [PATCH 12/29] fix(basic.gblib): #282 Fix SSR for Bots 3.0. --- .../basic.gblib/services/ChartServices.ts | 4 +- .../basic.gblib/services/SystemKeywords.ts | 4 +- .../services/WebAutomationServices.ts | 39 +- packages/core.gbapp/services/GBMinService.ts | 16 +- packages/core.gbapp/services/GBSSR.ts | 467 ++++++++---------- src/app.ts | 5 +- tsconfig.json | 1 + 7 files changed, 253 insertions(+), 283 deletions(-) diff --git a/packages/basic.gblib/services/ChartServices.ts b/packages/basic.gblib/services/ChartServices.ts index c2a2280a..25d6f686 100644 --- a/packages/basic.gblib/services/ChartServices.ts +++ b/packages/basic.gblib/services/ChartServices.ts @@ -32,7 +32,7 @@ 'use strict'; -import { createBrowser } from '../../core.gbapp/services/GBSSR.js'; +import { GBSSR }from '../../core.gbapp/services/GBSSR.js'; export class ChartServices { /** @@ -41,7 +41,7 @@ export class ChartServices { * @param {string} path screenshot image full path with file name */ public static async screenshot (args, path) { - const browser = await createBrowser(null); + const browser = await GBSSR.createBrowser(null); const page = await browser.newPage(); // load billboard.js assets from CDN. diff --git a/packages/basic.gblib/services/SystemKeywords.ts b/packages/basic.gblib/services/SystemKeywords.ts index 786f4d42..20d7ab72 100644 --- a/packages/basic.gblib/services/SystemKeywords.ts +++ b/packages/basic.gblib/services/SystemKeywords.ts @@ -39,7 +39,7 @@ import { DialogKeywords } from './DialogKeywords.js'; import { GBServer } from '../../../src/app.js'; import { GBVMService } from './GBVMService.js'; import Fs from 'fs'; -import { createBrowser } from '../../core.gbapp/services/GBSSR.js'; +import { GBSSR }from '../../core.gbapp/services/GBSSR.js'; import urlJoin from 'url-join'; import Excel from 'exceljs'; import { TwitterApi } from 'twitter-api-v2'; @@ -257,7 +257,7 @@ export class SystemKeywords { const { min, user } = await DialogKeywords.getProcessInfo(pid); const gbaiName = `${min.botId}.gbai`; - const browser = await createBrowser(null); + const browser = await GBSSR.createBrowser(null); const page = await browser.newPage(); // Includes the associated CSS related to current theme. diff --git a/packages/basic.gblib/services/WebAutomationServices.ts b/packages/basic.gblib/services/WebAutomationServices.ts index 0b709c33..c9aa7b66 100644 --- a/packages/basic.gblib/services/WebAutomationServices.ts +++ b/packages/basic.gblib/services/WebAutomationServices.ts @@ -32,19 +32,19 @@ 'use strict'; -import { GBLog, GBMinInstance } from 'botlib'; -import { GBServer } from '../../../src/app.js'; -import { GBAdminService } from '../../admin.gbapp/services/GBAdminService.js'; -import { createBrowser } from '../../core.gbapp/services/GBSSR.js'; -import { GuaribasUser } from '../../security.gbapp/models/index.js'; -import { DialogKeywords } from './DialogKeywords.js'; - -import { GBDeployer } from '../../core.gbapp/services/GBDeployer.js'; import urlJoin from 'url-join'; import Fs from 'fs'; import Path from 'path'; import url from 'url'; -import { Mutex, Semaphore, withTimeout } from 'async-mutex'; + +import { GBLog, GBMinInstance } from 'botlib'; +import { GBServer } from '../../../src/app.js'; +import { GBAdminService } from '../../admin.gbapp/services/GBAdminService.js'; +import { GBSSR }from '../../core.gbapp/services/GBSSR.js'; +import { GuaribasUser } from '../../security.gbapp/models/index.js'; +import { DialogKeywords } from './DialogKeywords.js'; +import { GBDeployer } from '../../core.gbapp/services/GBDeployer.js'; +import { Mutex } from 'async-mutex'; import { GBLogEx } from '../../core.gbapp/services/GBLogEx.js'; /** @@ -172,16 +172,17 @@ export class WebAutomationServices { let browser; if (!page) { - browser = await createBrowser(null); + browser = await GBSSR.createBrowser(null); page = (await browser.pages())[0]; if (username || password) { await page.authenticate({ pid, username: username, password: password }); } } - // There is no session yet, + // There is no session yet. if (!session && sessionKind === 'AS') { + // A new web session is being created. handle = WebAutomationServices.cyrb53(this.min.botId + url); @@ -459,4 +460,20 @@ export class WebAutomationServices { return file; } + + private async recursiveFindInFrames (inputFrame, selector) { + const frames = inputFrame.childFrames(); + const results = await Promise.all( + frames.map(async frame => { + const el = await frame.$(selector); + if (el) return el; + if (frame.childFrames().length > 0) { + return await this.recursiveFindInFrames(frame, selector); + } + return null; + }) + ); + return results.find(Boolean); + } + } diff --git a/packages/core.gbapp/services/GBMinService.ts b/packages/core.gbapp/services/GBMinService.ts index 09f9c3d5..7a77c0a6 100644 --- a/packages/core.gbapp/services/GBMinService.ts +++ b/packages/core.gbapp/services/GBMinService.ts @@ -87,6 +87,7 @@ import { GoogleChatDirectLine } from '../../google-chat.gblib/services/GoogleCha import { SystemKeywords } from '../../basic.gblib/services/SystemKeywords.js'; import * as nlp from 'node-nlp'; import Path from 'path'; +import { GBSSR } from './GBSSR.js'; /** * Minimal service layer for a bot and encapsulation of BOT Framework calls. @@ -236,6 +237,7 @@ export class GBMinService { const url = `/api/messages/${botId}`; removeRoute(GBServer.globals.server, url); + const uiUrl = `/${botId}`; removeRoute(GBServer.globals.server, uiUrl); @@ -295,6 +297,10 @@ export class GBMinService { if (!Fs.existsSync(dir)) { mkdirp.sync(dir); } + dir = `work/${min.botId}.gbai/${min.botId}.gbui`; + if (!Fs.existsSync(dir)) { + mkdirp.sync(dir); + } // Loads Named Entity data for this bot. @@ -370,15 +376,17 @@ export class GBMinService { if (process.env.DISABLE_WEB !== 'true') { const uiUrl = `/${instance.botId}`; + let staticHandler = express.static(urlJoin(GBDeployer.deployFolder, GBMinService.uiPackage, 'build')); + + GBServer.globals.server.get(uiUrl, async (req, res, next)=> { + await GBSSR.ssrFilter(req, res, staticHandler as any); + }); const uiUrlAlt = `/${instance.activationCode}`; - GBServer.globals.server.use( - uiUrl, - express.static(urlJoin(GBDeployer.deployFolder, GBMinService.uiPackage, 'build')) - ); GBServer.globals.server.use( uiUrlAlt, express.static(urlJoin(GBDeployer.deployFolder, GBMinService.uiPackage, 'build')) ); + const domain = min.core.getParam(min.instance, 'Domain', null); if (domain) { GBServer.globals.server.use( diff --git a/packages/core.gbapp/services/GBSSR.ts b/packages/core.gbapp/services/GBSSR.ts index 8f361f13..b353b10d 100644 --- a/packages/core.gbapp/services/GBSSR.ts +++ b/packages/core.gbapp/services/GBSSR.ts @@ -36,297 +36,240 @@ 'use strict'; - -import {createRequire} from "module"; -const require = createRequire(import.meta.url); - -const puppeteer = require('puppeteer-extra'); -const hidden = require('puppeteer-extra-plugin-stealth') - -// require executablePath from puppeteer -const {executablePath} = require('puppeteer') +import Path from 'path'; import Fs from 'fs'; - - import { NextFunction, Request, Response } from 'express'; import urljoin from 'url-join'; +import { GBMinInstance } from 'botlib'; +import { GBServer } from '../../../src/app.js'; +import { GBLogEx } from './GBLogEx.js'; +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); +const puppeteer = require('puppeteer-extra'); +const hidden = require('puppeteer-extra-plugin-stealth'); +const { executablePath } = require('puppeteer'); -// https://hackernoon.com/tips-and-tricks-for-web-scraping-with-puppeteer-ed391a63d952 -// Dont download all resources, we just need the HTML -// Also, this is huge performance/response time boost -const blockedResourceTypes = ['image', 'media', 'font', 'texttrack', 'object', 'beacon', 'csp_report', 'imageset']; -// const whitelist = ["document", "script", "xhr", "fetch"]; -const skippedResources = [ - 'quantserve', - 'adzerk', - 'doubleclick', - 'adition', - 'exelator', - 'sharethrough', - 'cdn.api.twitter', - 'google-analytics', - 'googletagmanager', - 'google', - 'fontawesome', - 'facebook', - 'analytics', - 'optimizely', - 'clicktale', - 'mixpanel', - 'zedo', - 'clicksor', - 'tiqcdn' -]; +export class GBSSR { + // https://hackernoon.com/tips-and-tricks-for-web-scraping-with-puppeteer-ed391a63d952 + // Dont download all resources, we just need the HTML + // Also, this is huge performance/response time boost + private blockedResourceTypes = ['image', 'media', 'font', 'texttrack', 'object', 'beacon', 'csp_report', 'imageset']; -const RENDER_CACHE = new Map(); - -async function createBrowser (profilePath): Promise { - let args = [ - '--check-for-update-interval=2592000', - '--disable-accelerated-2d-canvas', - '--disable-dev-shm-usage', - '--disable-features=site-per-process', - '--disable-gpu', - '--no-first-run', - '--no-default-browser-check' + // const whitelist = ["document", "script", "xhr", "fetch"]; + private skippedResources = [ + 'quantserve', + 'adzerk', + 'doubleclick', + 'adition', + 'exelator', + 'sharethrough', + 'cdn.api.twitter', + 'google-analytics', + 'googletagmanager', + 'google', + 'fontawesome', + 'facebook', + 'analytics', + 'optimizely', + 'clicktale', + 'mixpanel', + 'zedo', + 'clicksor', + 'tiqcdn' ]; - if (profilePath) { - args.push(`--user-data-dir=${profilePath}`); + public static async createBrowser(profilePath): Promise { + let args = [ + '--check-for-update-interval=2592000', + '--disable-accelerated-2d-canvas', + '--disable-dev-shm-usage', + '--disable-features=site-per-process', + '--disable-gpu', + '--no-first-run', + '--no-default-browser-check' + ]; - const preferences = urljoin(profilePath, 'Default', 'Preferences'); - if (Fs.existsSync(preferences)) { - const file = Fs.readFileSync(preferences, 'utf8'); - const data = JSON.parse(file); - data['profile']['exit_type'] = 'none'; - Fs.writeFileSync(preferences, JSON.stringify(data)); + if (profilePath) { + args.push(`--user-data-dir=${profilePath}`); + + const preferences = urljoin(profilePath, 'Default', 'Preferences'); + if (Fs.existsSync(preferences)) { + const file = Fs.readFileSync(preferences, 'utf8'); + const data = JSON.parse(file); + data['profile']['exit_type'] = 'none'; + Fs.writeFileSync(preferences, JSON.stringify(data)); + } } + puppeteer.use(hidden()); + const browser = await puppeteer.launch({ + args: args, + ignoreHTTPSErrors: true, + headless: false, + defaultViewport: null, + executablePath: executablePath(), + ignoreDefaultArgs: ['--enable-automation', '--enable-blink-features=IdleDetection'] + }); + return browser; } - puppeteer.use(hidden()) - const browser = await puppeteer.launch({ - args: args, - ignoreHTTPSErrors: true, - headless: false, - defaultViewport: null, - executablePath:executablePath(), - ignoreDefaultArgs: ['--enable-automation', '--enable-blink-features=IdleDetection'] - }); - return browser; -} -async function recursiveFindInFrames (inputFrame, selector) { - const frames = inputFrame.childFrames(); - const results = await Promise.all( - frames.map(async frame => { - const el = await frame.$(selector); - if (el) return el; - if (frame.childFrames().length > 0) { - return await recursiveFindInFrames(frame, selector); - } - return null; - }) - ); - return results.find(Boolean); -} + /** + * Return the HTML of bot default.gbui. + */ + public async getHTML(min: GBMinInstance) { + const url = urljoin(GBServer.globals.publicAddress, min.botId); + const browser = await GBSSR.createBrowser(null); + const stylesheetContents = {}; -/** - * https://developers.google.com/web/tools/puppeteer/articles/ssr#reuseinstance - * @param {string} url URL to prerender. - */ -async function ssr (url: string, useCache: boolean, cacheRefreshRate: number) { - if (RENDER_CACHE.has(url) && useCache) { - const cached = RENDER_CACHE.get(url); - if (Date.now() - cached.renderedAt > cacheRefreshRate && !(cacheRefreshRate <= 0)) { - RENDER_CACHE.delete(url); - } else { - return { - html: cached.html, - status: 200 - }; - } - } - const browser = await createBrowser(null); - const stylesheetContents = {}; - - try { - const page = await browser.newPage(); - await page.setUserAgent( - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36' - ); - await page.setRequestInterception(true); - page.on('request', request => { - const requestUrl = request - .url() - .split('?')[0] - .split('#')[0]; - if ( - blockedResourceTypes.indexOf(request.resourceType()) !== -1 || - skippedResources.some(resource => requestUrl.indexOf(resource) !== -1) - ) { - request.abort(); - } else { - request.continue(); - } - }); - - page.on('response', async resp => { - const responseUrl = resp.url(); - const sameOrigin = new URL(responseUrl).origin === new URL(url).origin; - const isStylesheet = resp.request().resourceType() === 'stylesheet'; - if (sameOrigin && isStylesheet) { - stylesheetContents[responseUrl] = await resp.text(); - } - }); - - const response = await page.goto(url, { - timeout: 120000, - waitUntil: 'networkidle0' - }); - - const sleep = ms => { - return new Promise(resolve => { - setTimeout(resolve, ms); + try { + const page = await browser.newPage(); + await page.setUserAgent( + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36' + ); + await page.setRequestInterception(true); + page.on('request', request => { + const requestUrl = request.url().split('?')[0].split('#')[0]; + if ( + this.blockedResourceTypes.indexOf(request.resourceType()) !== -1 || + this.skippedResources.some(resource => requestUrl.indexOf(resource) !== -1) + ) { + request.abort(); + } else { + request.continue(); + } }); - }; - await sleep(45000); - // Inject on page to relative resources load properly. - await page.evaluate(url => { - const base = document.createElement('base'); - base.href = url; - // Add to top of head, before all other resources. - document.head.prepend(base); - }, url); - - // Remove scripts and html imports. They've already executed. - await page.evaluate(() => { - const elements = document.querySelectorAll('script, link[rel="import"]'); - elements.forEach(e => { - e.remove(); + page.on('response', async resp => { + const responseUrl = resp.url(); + const sameOrigin = new URL(responseUrl).origin === new URL(url).origin; + const isStylesheet = resp.request().resourceType() === 'stylesheet'; + if (sameOrigin && isStylesheet) { + stylesheetContents[responseUrl] = await resp.text(); + } }); - }); - // Replace stylesheets in the page with their equivalent