diff --git a/blank.docx b/blank.docx new file mode 100644 index 000000000..88beb99b1 Binary files /dev/null and b/blank.docx differ diff --git a/blank.xlsx b/blank.xlsx new file mode 100644 index 000000000..fa249fbcd Binary files /dev/null and b/blank.xlsx differ diff --git a/boot.mjs b/boot.mjs index 3749aa205..ce93db2e4 100644 --- a/boot.mjs +++ b/boot.mjs @@ -6,6 +6,7 @@ import { exec } from 'child_process'; import pjson from './package.json' assert { type: 'json' }; // Displays version of Node JS being used at runtime and others attributes. + console.log(``); console.log(``); console.log(` █████ █████ ██ █ █████ █████ ████ ██ ████ █████ █████ ███ ® `); diff --git a/packages/basic.gblib/services/DialogKeywords.ts b/packages/basic.gblib/services/DialogKeywords.ts index 0dbf1a15e..86c659842 100644 --- a/packages/basic.gblib/services/DialogKeywords.ts +++ b/packages/basic.gblib/services/DialogKeywords.ts @@ -559,7 +559,7 @@ export class DialogKeywords { // } let { min, user, params } = await DialogKeywords.getProcessInfo(pid); const sec = new SecService(); - await sec.setParam(user.userId, name, value); + await sec.setParam(user.userId, name , value); GBLog.info(`BASIC: ${name} = ${value} (botId: ${min.botId})`); return { min, user, params }; } diff --git a/packages/basic.gblib/services/GBVMService.ts b/packages/basic.gblib/services/GBVMService.ts index 20a78a616..76642e5ba 100644 --- a/packages/basic.gblib/services/GBVMService.ts +++ b/packages/basic.gblib/services/GBVMService.ts @@ -137,41 +137,13 @@ export class GBVMService extends GBService { 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 }); - - // // Hacks push-rpc to put timeout. - - // const inject1 = ` - // const { AbortController } = require("node-abort-controller"); - // var controller_1 = new AbortController(); - // var signal = controller_1.signal; - // setTimeout(function () { controller_1.abort(); }, 24 * 60 * 60 * 1000);`; - // const inject2 = `signal: signal,`; - // const js = Path.join(process.env.PWD, folder, 'node_modules/@push-rpc/http/dist/client.js'); - - // lineReplace({ - // file: js, - // line: 75, - // text: inject1, - // addNewLine: true, - // callback: ({ file, line, text, replacedText, error }) => { - // lineReplace({ - // file: js, - // line: 82, - // text: inject2, - // addNewLine: true, - // callback: ({ file, line, text, replacedText, error }) => { - // GBLogEx.info(min, `BASIC: Patching node_modules for ${min.botId} done.`); - // } - // }); - // } - // }); } // Hot swap for .vbs files. const fullFilename = urlJoin(folder, filename); if (process.env.DEV_HOTSWAP) { Fs.watchFile(fullFilename, async () => { - await this.translateBASIC(fullFilename, mainName, min); + await this.translateBASIC(fullFilename, min); const parsedCode: string = Fs.readFileSync(jsfile, 'utf8'); min.sandBoxMap[mainName.toLowerCase().trim()] = parsedCode; }); @@ -184,17 +156,17 @@ export class GBVMService extends GBService { const jsStat = Fs.statSync(jsfile); const interval = 30000; // If compiled is older 30 seconds, then recompile. if (compiledAt.isFile() && compiledAt['mtimeMs'] > jsStat['mtimeMs'] + interval) { - await this.translateBASIC(fullFilename, mainName, min); + await this.translateBASIC(fullFilename, min); } } else { - await this.translateBASIC(fullFilename, mainName, min); + await this.translateBASIC(fullFilename, min); } const parsedCode: string = Fs.readFileSync(jsfile, 'utf8'); min.sandBoxMap[mainName.toLowerCase().trim()] = parsedCode; return filename; } - public async translateBASIC(filename: any, mainName: string, min: GBMinInstance) { + public async translateBASIC(filename: any, min: GBMinInstance) { // Converts General Bots BASIC into regular VBS let basicCode: string = Fs.readFileSync(filename, 'utf8'); @@ -264,9 +236,11 @@ export class GBVMService extends GBService { let list = this.list; let httpUsername = this.httpUsername; let httpPs = this.httpPs; - let page = null; let today = this.today; let now = this.now; + let page = null; + let files = []; + let col = 1; // Transfers NLP auto variables into global object. @@ -351,18 +325,25 @@ export class GBVMService extends GBService { var lines = code.split('\n'); const keywords = KeywordsExpressions.getKeywords(); let current = 41; - const map = {}; + const map = {}; for (let i = 1; i <= lines.length; i++) { + let line = lines[i - 1]; + + // Remove lines before statments. + + line = line.replace(/^\s*\d+\s*/gi,''); + for (let j = 0; j < keywords.length; j++) { - lines[i - 1] = lines[i - 1].replace(keywords[j][0], keywords[j][1]); + line = line.replace(keywords[j][0], keywords[j][1]); } // Add additional lines returned from replacement. - let add = lines[i - 1].split(/\r\n|\r|\n/).length; + let add = line.split(/\r\n|\r|\n/).length; current = current + (add ? add : 0); map[i] = current; + lines[i - 1] = line; } code = `${lines.join('\n')}\n`; @@ -419,7 +400,8 @@ export class GBVMService extends GBService { }; const dk = new DialogKeywords(); const sys = new SystemKeywords(); - + await dk.setFilter ({pid: pid, value: null }); + sandbox['variables'] = variables; sandbox['id'] = sys.getRandomId(); sandbox['username'] = await dk.userName({ pid }); diff --git a/packages/basic.gblib/services/KeywordsExpressions.ts b/packages/basic.gblib/services/KeywordsExpressions.ts index 3985ba586..b53d1aa80 100644 --- a/packages/basic.gblib/services/KeywordsExpressions.ts +++ b/packages/basic.gblib/services/KeywordsExpressions.ts @@ -32,7 +32,9 @@ 'use strict'; -import { GBVMService } from "./GBVMService.js"; +import { GBAdminService } from '../../admin.gbapp/services/GBAdminService.js'; +import { GBVMService } from './GBVMService.js'; +import Path from 'path'; /** * Image processing services of conversation to be called by BASIC. @@ -46,7 +48,7 @@ export class KeywordsExpressions { if (accum.isConcatting) { accum.soFar[accum.soFar.length - 1] += ',' + curr; } else { - if(curr===""){ + if (curr === '') { curr = null; } accum.soFar.push(curr); @@ -67,15 +69,20 @@ export class KeywordsExpressions { names.forEach(name => { let value = items[i]; i++; - json = `${json} "${name}": ${value} ${names.length == i ? '' : ','}`; + json = `${json} "${name}": ${value === undefined ? null : value} ${names.length == i ? '' : ','}`; }); json = `${json}`; return json; }; + public static isNumber(n) { + return !isNaN(parseFloat(n)) && isFinite(n); + } + /** * Returns the list of BASIC keyword and their JS match. + * Based on https://github.com/uweg/vbscript-to-typescript. */ public static getKeywords() { // Keywords from General Bots BASIC. @@ -91,14 +98,134 @@ export class KeywordsExpressions { return result; }; + keywords[i++] = [ + /^\s*INPUT(.*)/gim, + ($0, $1, $2) => { + + let separator; + if ($1.indexOf(',') > -1){ + separator = ','; + } + else if ($1.indexOf(';') > -1){ + separator = ';'; + } + let parts; + if ( separator && (parts = $1.split(separator)) && parts.length > 1){ + return ` + TALK ${parts[0]} + HEAR ${parts[1]}`; + } + else + { + return ` + HEAR ${$1}`; + } + } + ]; + + keywords[i++] = [ + /^\s*WRITE(.*)/gim, + ($0, $1, $2) => { + return `PRINT${$1}`; + } + ]; + keywords[i++] = [/^\s*REM.*/gim, '']; + keywords[i++] = [/^\s*CLOSE.*/gim, '']; + + // Always autoclose keyword. + + keywords[i++] = [/^\s*CLOSE.*/gim, '']; + keywords[i++] = [/^\s*\'.*/gim, '']; + keywords[i++] = [ + /^\s*PRINT ([\s\S]*)/gim, + ($0, $1, $2) => { + let sessionName; + let kind = null; + let pos; + $1 = $1.trim(); + + if ($1.substr(0, 1) === '#') { + let sessionName = $1.substr(1, $1.indexOf(',') - 1); + $1 = $1.replace(/\; \"\,\"/gi, ''); + $1 = $1.substr($1.indexOf(',') + 1); + + let separator; + if ($1.indexOf(',') > -1){ + separator = ','; + } + else if ($1.indexOf(';') > -1){ + separator = ';'; + } + let items; + if (separator && (items = $1.split(separator)) && items.length > 1) { + return `await sys.save({pid: pid, file: files[${sessionName}], args:[${items.join(',')}]})`; + } else { + return `await sys.set({pid: pid, file: files[${sessionName}], address: col++, name: "${items[0]}", value: ${items[0]}})`; + } + } else { + return `await dk.talk({pid: pid, text: ${$1}})`; + } + } + ]; + + keywords[i++] = [ + /^\s*open([\s\S]*)/gim, + ($0, $1, $2) => { + let sessionName; + let kind = null; + let pos; + + $1 = $1.replace('FOR APPEND', ''); + $1 = $1.replace('FOR OUTPUT', ''); + + + + if ((pos = $1.match(/\s*AS\s*\#/gi))) { + kind = 'AS'; + } else if ((pos = $1.match(/\s*WITH\s*\#/gi))) { + kind = 'WITH'; + } + + if (pos) { + let part = $1.substr($1.lastIndexOf(pos[0])); + sessionName = `${part.substr(part.indexOf('#') + 1)}`; + $1 = $1.substr(0, $1.lastIndexOf(pos[0])); + } + $1 = $1.trim(); + if (!$1.startsWith('"') && !$1.startsWith("'")) { + $1 = `"${$1}"`; + } + const params = this.getParams($1, ['url', 'username', 'password']); + + // Checks if it is opening a file or a webpage. + + if (kind === 'AS' && KeywordsExpressions.isNumber(sessionName)) { + const jParams = JSON.parse(`{${params}}`); + const filename = `${jParams.url.substr(0, jParams.url.lastIndexOf("."))}.xlsx`; + let code = + ` + col = 1 + await sys.save({pid: pid,file: "${filename}", args: [id] }) + await dk.setFilter ({pid: pid, value: "id=" + id }) + files[${sessionName}] = "${filename}" + `; + return code; + } else { + sessionName = `"${sessionName}"`; + kind = `"${kind}"`; + return `page = await wa.openPage({pid: pid, handle: page, sessionKind: ${kind}, sessionName: ${sessionName}, ${params}})`; + } + } + ]; + keywords[i++] = [ /^\s*((?:[a-z]+.?)(?:(?:\w+).)(?:\w+)*)\s*=\s*SELECT\s*(.*)/gim, ($0, $1, $2) => { - let tableName = /\s*FROM\s*(\w+)/.exec($2)[1]; + let tableName = /\s*FROM\s*(\w+\$*)/.exec($2)[1]; let sql = `SELECT ${$2}`.replace(tableName, '?'); return `${$1} = await sys.executeSQL({pid: pid, data:${$1}, sql:"${sql}", tableName:"${tableName}"})\n`; } @@ -114,8 +241,6 @@ export class KeywordsExpressions { } ]; - // Based on https://github.com/uweg/vbscript-to-typescript. - keywords[i++] = [/^\s*else(?!{)/gim, '}\nelse {']; keywords[i++] = [/^\s*select case +(.*)/gim, 'switch ($1) {']; @@ -141,30 +266,42 @@ export class KeywordsExpressions { keywords[i++] = [/^\s*loop *$/gim, '}']; keywords[i++] = [ - /^\s*open\s*(.*)/gim, + /^\s*open([\s\S]*)/gim, ($0, $1, $2) => { let sessionName; let kind = null; let pos; - if (pos = $1.match(/\s*AS\s*\#/)) { - kind = '"AS"'; - } else if (pos = $1.match(/\s*WITH\s*\#/)) { - kind = '"WITH"'; + $1 = $1.replace(' FOR APPEND', ''); + + 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])); } - + $1 = $1.trim(); if (!$1.startsWith('"') && !$1.startsWith("'")) { $1 = `"${$1}"`; } const params = this.getParams($1, ['url', 'username', 'password']); - return `page = await wa.openPage({pid: pid, handle: page, sessionKind: ${kind}, sessionName: ${sessionName}, ${params}})`; + // Checks if it is opening a file or a webpage. + + if (kind === 'AS' && KeywordsExpressions.isNumber(sessionName)) { + const jParams = JSON.parse(`{${params}}`); + const filename = `${Path.basename(jParams.url, 'txt')}xlsx`; + return `files[${sessionName}] = "${filename}"`; + } else { + sessionName = `"${sessionName}"`; + kind = `"${kind}"`; + return `page = await wa.openPage({pid: pid, handle: page, sessionKind: ${kind}, sessionName: ${sessionName}, ${params}})`; + } } ]; @@ -176,112 +313,112 @@ export class KeywordsExpressions { ]; keywords[i++] = [ - /^\s*hear (\w+) as (\w+( \w+)*.xlsx)/gim, + /^\s*hear (\w+\$*) as (\w+( \w+)*.xlsx)/gim, ($0, $1, $2) => { return `${$1} = await dk.hear({pid: pid, kind:"sheet", arg: "${$2}"})`; } ]; keywords[i++] = [ - /^\s*hear (\w+) as\s*login/gim, + /^\s*hear (\w+\$*) as\s*login/gim, ($0, $1) => { return `${$1} = await dk.hear({pid: pid, kind:"login"})`; } ]; keywords[i++] = [ - /^\s*hear (\w+) as\s*email/gim, + /^\s*hear (\w+\$*) as\s*email/gim, ($0, $1) => { return `${$1} = await dk.hear({pid: pid, kind:"email"})`; } ]; keywords[i++] = [ - /^\s*hear (\w+) as\s*integer/gim, + /^\s*hear (\w+\$*) as\s*integer/gim, ($0, $1) => { return `${$1} = await dk.hear({pid: pid, kind:"integer"})`; } ]; keywords[i++] = [ - /^\s*hear (\w+) as\s*file/gim, + /^\s*hear (\w+\$*) as\s*file/gim, ($0, $1) => { return `${$1} = await dk.hear({pid: pid, kind:"file"})`; } ]; keywords[i++] = [ - /^\s*hear (\w+) as\s*boolean/gim, + /^\s*hear (\w+\$*) as\s*boolean/gim, ($0, $1) => { return `${$1} = await dk.hear({pid: pid, kind:"boolean"})`; } ]; keywords[i++] = [ - /^\s*hear (\w+) as\s*name/gim, + /^\s*hear (\w+\$*) as\s*name/gim, ($0, $1) => { return `${$1} = await dk.hear({pid: pid, kind:"name"})`; } ]; keywords[i++] = [ - /^\s*hear (\w+) as\s*date/gim, + /^\s*hear (\w+\$*) as\s*date/gim, ($0, $1) => { return `${$1} = await dk.hear({pid: pid, kind:"date"})`; } ]; keywords[i++] = [ - /^\s*hear (\w+) as\s*hour/gim, + /^\s*hear (\w+\$*) as\s*hour/gim, ($0, $1) => { return `${$1} = await dk.hear({pid: pid, kind:"hour"})`; } ]; keywords[i++] = [ - /^\s*hear (\w+) as\s*phone/gim, + /^\s*hear (\w+\$*) as\s*phone/gim, ($0, $1) => { return `${$1} = await dk.hear({pid: pid, kind:"phone"})`; } ]; keywords[i++] = [ - /^\s*hear (\w+) as\s*money/gim, + /^\s*hear (\w+\$*) as\s*money/gim, ($0, $1) => { return `${$1} = await dk.hear({pid: pid, kind:"money")}`; } ]; keywords[i++] = [ - /^\s*hear (\w+) as\s*qrcode/gim, + /^\s*hear (\w+\$*) as\s*qrcode/gim, ($0, $1) => { return `${$1} = await dk.hear({pid: pid, kind:"qrcode")}`; } ]; keywords[i++] = [ - /^\s*hear (\w+) as\s*language/gim, + /^\s*hear (\w+\$*) as\s*language/gim, ($0, $1) => { return `${$1} = await dk.hear({pid: pid, kind:"language")}`; } ]; keywords[i++] = [ - /^\s*hear (\w+) as\s*zipcode/gim, + /^\s*hear (\w+\$*) as\s*zipcode/gim, ($0, $1) => { return `${$1} = await dk.hear({pid: pid, kind:"zipcode")}`; } ]; keywords[i++] = [ - /^\s*hear (\w+) as\s*(.*)/gim, + /^\s*hear (\w+\$*) as\s*(.*)/gim, ($0, $1, $2) => { return `${$1} = await dk.hear({pid: pid, kind:"menu", args: [${$2}]})`; } ]; keywords[i++] = [ - /^\s*(hear)\s*(\w+)/gim, + /^\s*(hear)\s*(\w+\$*)/gim, ($0, $1, $2) => { return `${$2} = await dk.hear({pid: pid})`; } @@ -345,21 +482,21 @@ export class KeywordsExpressions { ]; keywords[i++] = [ - /^\s*((?:[a-z]+.?)(?:(?:\w+).)(?:\w+)*)\s*=\s*sort\s*(\w+)\s*by(.*)/gim, + /^\s*((?:[a-z]+.?)(?:(?:\w+).)(?:\w+)*)\s*=\s*sort\s*(\w+\$*)\s*by(.*)/gim, ($0, $1, $2, $3) => { return `${$1} = await sys.sortBy({pid: pid, array: ${$2}, memberName: "${$3}"})`; } ]; keywords[i++] = [ - /^\s*see\s*text\s*of\s*(\w+)\s*as\s*(\w+)\s*/gim, + /^\s*see\s*text\s*of\s*(\w+\$*)\s*as\s*(\w+\$*)\s*/gim, ($0, $1, $2, $3) => { return `${$2} = await sys.seeText({pid: pid, url: ${$1})`; } ]; keywords[i++] = [ - /^\s*see\s*caption\s*of\s*(\w+)\s*as(.*)/gim, + /^\s*see\s*caption\s*of\s*(\w+\$*)\s*as(.*)/gim, ($0, $1, $2, $3) => { return `${$2} = await sys.seeCaption({pid: pid, url: ${$1})`; } @@ -618,7 +755,6 @@ export class KeywordsExpressions { // Uses auto quote if this is a frase with more then one word. if (/\s/.test($3) && $3.substr(0, 1) !== '"') { - $3 = `"${$3}"`; } return `await dk.talk ({pid: pid, text: ${$3}})`; @@ -772,19 +908,19 @@ export class KeywordsExpressions { ]; keywords[i++] = [ - /^\s*save\s*(\w+)\s*as\s*(.*)/gim, + /^\s*save\s*(\w+\$*)\s*as\s*(.*)/gim, ($0, $1, $2, $3) => { return `await sys.saveFile({pid: pid, file: ${$2}, data: ${$1}})`; } ]; - + keywords[i++] = [ /^\s*(save)(\s*)(.*\.xlsx)(.*)/gim, ($0, $1, $2, $3, $4) => { - $3 = $3.replace (/\'/g, "") - $3 = $3.replace (/\"/g, "") - $4 = $4.substr(2) - return `await sys.save({pid: pid,file: "${$3}" , args: [${$4}]})`; + $3 = $3.replace(/\'/g, ''); + $3 = $3.replace(/\"/g, ''); + $4 = $4.substr(2); + return `await sys.save({pid: pid, file: "${$3}", args: [${$4}]})`; } ]; diff --git a/packages/basic.gblib/services/SystemKeywords.ts b/packages/basic.gblib/services/SystemKeywords.ts index 1e8719d26..bfc60102f 100644 --- a/packages/basic.gblib/services/SystemKeywords.ts +++ b/packages/basic.gblib/services/SystemKeywords.ts @@ -58,6 +58,7 @@ import DynamicsWebApi from 'dynamics-web-api'; import * as MSAL from '@azure/msal-node'; import { GBConversationalService } from '../../core.gbapp/services/GBConversationalService.js'; import { WebAutomationServices } from './WebAutomationServices.js'; +import { KeywordsExpressions } from './KeywordsExpressions.js'; /** * @fileoverview General Bots server core. @@ -471,7 +472,7 @@ export class SystemKeywords { * @example SET page, "elementHTMLSelector", "text" * */ - public async set({ pid, handle, file, address, value }): Promise { + public async set({ pid, handle, file, address, value, name = null }): Promise { const { min, user } = await DialogKeywords.getProcessInfo(pid); // Handles calls for HTML stuff @@ -485,14 +486,6 @@ export class SystemKeywords { // TODO: Add a semaphore between FILTER and SET. - // Processes FILTER option to ensure parallel SET calls. - - const filter = await DialogKeywords.getOption({ pid, name }); - if (filter) { - const row = this.find({ pid, handle: null, args: [filter] }); - address += row['line']; - } - // Handles calls for BASIC persistence on sheet files. GBLog.info(`BASIC: Defining '${address}' in '${file}' to '${value}' (SET). `); @@ -501,16 +494,53 @@ export class SystemKeywords { const botId = min.instance.botId; const path = DialogKeywords.getGBAIPath(botId, 'gbdata'); + let document = await this.internalGetDocument(client, baseUrl, path, file); + let sheets = await client.api(`${baseUrl}/drive/items/${document.id}/workbook/worksheets`).get(); + let body = { values: [[]] }; + // Processes FILTER option to ensure parallel SET calls. + + const filter = await DialogKeywords.getOption({ pid, name: 'filter' }); + let titleAddress; + + if (filter) { + // Transforms address number (col index) to letter based. + // Eg.: REM This is A column and index automatically specified by filter. + // SET file.xlsx, 1, 4000 + + if (KeywordsExpressions.isNumber(address)) { + address = `${this.numberToLetters(address)}`; + titleAddress = `${address}1:${address}1`; + } + + // Processes SET FILTER directive to calculate address. + + body.values[0][0] = 'id'; + const addressId = 'A1:A1'; + await client + .api( + `${baseUrl}/drive/items/${document.id}/workbook/worksheets('${sheets.value[0].name}')/range(address='${addressId}')` + ) + .patch(body); + + const row = await this.find({ pid, handle: null, args: [file, filter] }); + if (row) { + address += row['line']; // Eg.: "A" + 1 = "A1". + } + } address = address.indexOf(':') !== -1 ? address : address + ':' + address; - let document = await this.internalGetDocument(client, baseUrl, path, file); + if (titleAddress) { + body.values[0][0] = name.trim().replace(/[^a-zA-Z]/gi, ''); + + await client + .api( + `${baseUrl}/drive/items/${document.id}/workbook/worksheets('${sheets.value[0].name}')/range(address='${titleAddress}')` + ) + .patch(body); + } - let body = { values: [[]] }; body.values[0][0] = value; - - let sheets = await client.api(`${baseUrl}/drive/items/${document.id}/workbook/worksheets`).get(); - await client .api( `${baseUrl}/drive/items/${document.id}/workbook/worksheets('${sheets.value[0].name}')/range(address='${address}')` @@ -529,7 +559,10 @@ export class SystemKeywords { }); if (!documents || documents.length === 0) { - throw `File '${file}' specified on GBasic command not found. Check the .gbdata or the .gbdialog associated.`; + throw new Error( + `File '${file}' specified on GBasic command not found. Check the .gbdata or the .gbdialog associated.`, + { cause: 404 } + ); } return documents[0]; @@ -575,36 +608,84 @@ export class SystemKeywords { */ public async save({ pid, file, args }): Promise { const { min, user } = await DialogKeywords.getProcessInfo(pid); - args.shift(); + GBLog.info(`BASIC: Saving '${file}' (SAVE). Args: ${args.join(',')}.`); let { baseUrl, client } = await GBDeployer.internalGetDriveClient(min); const botId = min.instance.botId; const path = DialogKeywords.getGBAIPath(botId, 'gbdata'); - let document = await this.internalGetDocument(client, baseUrl, path, file); - let sheets = await client.api(`${baseUrl}/drive/items/${document.id}/workbook/worksheets`).get(); + let sheets; + let document; + try { + document = await this.internalGetDocument(client, baseUrl, path, file); + sheets = await client.api(`${baseUrl}/drive/items/${document.id}/workbook/worksheets`).get(); + } catch (e) { + if (e.cause === 404) { + // Creates the file. - await client - .api( - `${baseUrl}/drive/items/${document.id}/workbook/worksheets('${sheets.value[0].name}')/range(address='A2:DX2')/insert` - ) - .post({}); + const blank = Path.join(process.env.PWD, 'blank.xlsx'); + const data = Fs.readFileSync(blank); + await client.api(`${baseUrl}/drive/root:/${path}/${file}:/content`).put(data); - if (args.length > 128) { - throw `File '${file}' has a SAVE call with more than 128 arguments. Check the .gbdialog associated.`; + // Tries to open again. + + document = await this.internalGetDocument(client, baseUrl, path, file); + sheets = await client.api(`${baseUrl}/drive/items/${document.id}/workbook/worksheets`).get(); + } else { + throw e; + } } + let address; let body = { values: [[]] }; - const address = `A2:${this.numberToLetters(args.length - 1)}2`; + // Processes FILTER option to ensure parallel SET calls. + + const filter = await DialogKeywords.getOption({ pid, name: 'filter' }); + if (filter) { + + // Creates id row. + + body.values[0][0] = 'id'; + const addressId = 'A1:A1'; + await client + .api( + `${baseUrl}/drive/items/${document.id}/workbook/worksheets('${sheets.value[0].name}')/range(address='${addressId}')` + ) + .patch(body); + body.values[0][0] = undefined ; + + // FINDs the filtered row to be updated. + + const row = await this.find({ pid, handle: null, args: [file, filter] }); + if (row) { + address = `A${row['line']}:${this.numberToLetters(args.length)}${row['line']}`; + } + } + + // Editing or saving detection. + + if (!address) { + await client + .api( + `${baseUrl}/drive/items/${document.id}/workbook/worksheets('${sheets.value[0].name}')/range(address='A2:DX2')/insert` + ) + .post({}); + address = `A2:${this.numberToLetters(args.length - 1)}2`; + } + + // Fills rows object to call sheet API. + for (let index = 0; index < args.length; index++) { let value = args[index]; if (value && (await this.isValidDate({ pid, dt: value }))) { value = `'${value}`; } - body.values[0][index] = value; - } + // If filter is defined, skips id column. + + body.values[0][filter ? index + 1 : index] = value; + } await client .api( `${baseUrl}/drive/items/${document.id}/workbook/worksheets('${sheets.value[0].name}')/range(address='${address}')` @@ -675,11 +756,7 @@ export class SystemKeywords { } public async isValidNumber({ pid, number }) { - const { min, user } = await DialogKeywords.getProcessInfo(pid); - if (number === '') { - return false; - } - return !isNaN(number); + return KeywordsExpressions.isNumber(number); } public isValidHour({ pid, value }) { @@ -1179,6 +1256,33 @@ export class SystemKeywords { await client.api(`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/invite`).post(body); } + public async internalCreateDocument(min, path, content) { + GBLog.info(`BASIC: CREATE DOCUMENT '${path}...'`); + let { baseUrl, client } = await GBDeployer.internalGetDriveClient(min); + const gbaiName = DialogKeywords.getGBAIPath(min.botId); + const tmpDocx = urlJoin(gbaiName, path); + + // Templates a blank {content} tag inside the blank.docx. + + const blank = Path.join(process.env.PWD, 'blank.docx'); + let buf = Fs.readFileSync(blank); + let zip = new PizZip(buf); + let doc = new Docxtemplater(); + doc.setOptions({ linebreaks: true }); + doc.loadZip(zip); + doc.setData({ content: content }).render(); + buf = doc.getZip().generate({ type: 'nodebuffer', compression: 'DEFLATE' }); + + // Performs the upload. + + await client.api(`${baseUrl}/drive/root:/${tmpDocx}:/content`).put(buf); + } + + public async createDocument({ pid, path, content }) { + const { min, user, params } = await DialogKeywords.getProcessInfo(pid); + this.internalCreateDocument(min, path, content); + } + /** * Copies a drive file from a place to another . * @@ -1224,8 +1328,9 @@ export class SystemKeywords { parentReference: { driveId: folder.parentReference.driveId, id: folder.id }, name: `${Path.basename(dest)}` }; - - return await client.api(`${baseUrl}/drive/items/${srcFile.id}/copy`).post(destFile); + const file = await client.api(`${baseUrl}/drive/items/${srcFile.id}/copy`).post(destFile); + GBLog.info(`BASIC: FINISHED COPY '${src}' to '${dest}'`); + return file; } catch (error) { if (error.code === 'itemNotFound') { GBLog.info(`BASIC: COPY source file not found: ${srcPath}.`); @@ -1234,7 +1339,6 @@ export class SystemKeywords { } throw error; } - GBLog.info(`BASIC: FINISHED COPY '${src}' to '${dest}'`); } /** @@ -1468,10 +1572,6 @@ export class SystemKeywords { localName = Path.join('work', gbaiName, 'cache', `tmp${GBAdminService.getRndReadableIdentifier()}.docx`); Fs.writeFileSync(localName, buf, { encoding: null }); - // Loads the file as binary content. - - let zip = new PizZip(buf); - // Replace image path on all elements of data. const images = []; @@ -1537,6 +1637,9 @@ export class SystemKeywords { } }; + // Loads the file as binary content. + + let zip = new PizZip(buf); let doc = new Docxtemplater(); doc.setOptions({ paragraphLoop: true, linebreaks: true }); doc.loadZip(zip); diff --git a/packages/core.gbapp/services/GBMinService.ts b/packages/core.gbapp/services/GBMinService.ts index 6cf635c74..a15100020 100644 --- a/packages/core.gbapp/services/GBMinService.ts +++ b/packages/core.gbapp/services/GBMinService.ts @@ -166,7 +166,6 @@ export class GBMinService { // Calls mountBot event to all bots. let i = 1; - if (instances.length > 1) { this.bar1 = new cliProgress.SingleBar( { @@ -179,19 +178,20 @@ export class GBMinService { this.bar1.start(instances.length, i, { botId: 'Boot' }); } - - await CollectionUtil.asyncForEach(instances, (async instance => { - try { - await this['mountBot'](instance); - } catch (error) { - GBLog.error(`Error mounting bot ${instance.botId}: ${error.message}\n${error.stack}`); - } - finally { - if (this.bar1) { - this.bar1.update(i++, { botId: instance.botId }); + await CollectionUtil.asyncForEach( + instances, + (async instance => { + try { + await this['mountBot'](instance); + } catch (error) { + GBLog.error(`Error mounting bot ${instance.botId}: ${error.message}\n${error.stack}`); + } finally { + if (this.bar1) { + this.bar1.update(i++, { botId: instance.botId }); + } } - } - }).bind(this)); + }).bind(this) + ); if (this.bar1) { this.bar1.stop(); @@ -200,10 +200,10 @@ export class GBMinService { pingSendTimeout: null, keepAliveTimeout: null, listeners: { - unsubscribed(subscriptions: number): void { }, - subscribed(subscriptions: number): void { }, - disconnected(remoteId: string, connections: number): void { }, - connected(remoteId: string, connections: number): void { }, + unsubscribed(subscriptions: number): void {}, + subscribed(subscriptions: number): void {}, + disconnected(remoteId: string, connections: number): void {}, + connected(remoteId: string, connections: number): void {}, messageIn(...params): void { GBLogEx.info(0, '[IN] ' + params); }, @@ -237,8 +237,6 @@ export class GBMinService { GBLogEx.info(0, 'API RPC HTTP Server started.'); - - // // Loads schedules. // GBLog.info(`Preparing SET SCHEDULE dialog calls...`); @@ -286,7 +284,7 @@ export class GBMinService { /** * Unmounts the bot web site (default.gbui) secure domain, if any. */ - public async unloadDomain(instance: IGBInstance) { } + public async unloadDomain(instance: IGBInstance) {} /** * Mount the instance by creating an BOT Framework bot object, @@ -331,7 +329,7 @@ export class GBMinService { const gbai = DialogKeywords.getGBAIPath(min.botId); let dir = `work/${gbai}/cache`; - const botId = gbai.replace(/\.[^/.]+$/, ""); + const botId = gbai.replace(/\.[^/.]+$/, ''); if (!Fs.existsSync(dir)) { mkdirp.sync(dir); @@ -563,8 +561,9 @@ export class GBMinService { min.instance.authenticatorTenant, '/oauth2/authorize' ); - authorizationUrl = `${authorizationUrl}?response_type=code&client_id=${min.instance.marketplaceId - }&redirect_uri=${urlJoin(min.instance.botEndpoint, min.instance.botId, 'token')}`; + authorizationUrl = `${authorizationUrl}?response_type=code&client_id=${ + min.instance.marketplaceId + }&redirect_uri=${urlJoin(min.instance.botEndpoint, min.instance.botId, 'token')}`; GBLog.info(`HandleOAuthRequests: ${authorizationUrl}.`); res.redirect(authorizationUrl); }); @@ -1065,8 +1064,9 @@ export class GBMinService { await this.processEventActivity(min, user, context, step); } } catch (error) { - const msg = `ERROR: ${error.message} ${error.error ? error.error.body : ''} ${error.error ? (error.error.stack ? error.error.stack : '') : '' - }`; + const msg = `ERROR: ${error.message} ${error.error ? error.error.body : ''} ${ + error.error ? (error.error.stack ? error.error.stack : '') : '' + }`; GBLog.error(msg); await min.conversationalService.sendText( @@ -1085,8 +1085,7 @@ export class GBMinService { if (error.code === 401) { GBLog.error('Calling processActivity due to Signing Key could not be retrieved error.'); await adapter['processActivity'](req, res, handler); - } - else { + } else { throw error; } } @@ -1275,7 +1274,9 @@ export class GBMinService { // Files in .gbdialog can be called directly by typing its name normalized into JS . const isVMCall = Object.keys(min.scriptMap).find(key => min.scriptMap[key] === context.activity.text) !== undefined; - if (isVMCall) { + if (/create dialog|creative dialog|create a dialog|criar diálogo|criar diálogo/gi.test(context.activity.text)) { + await step.beginDialog('/dialog'); + } else if (isVMCall) { await GBVMService.callVM(context.activity.text, min, step, user, this.deployer, false); } else if (context.activity.text.charAt(0) === '/') { const text = context.activity.text; diff --git a/packages/default.gbui/src/GBUIApp.js b/packages/default.gbui/src/GBUIApp.js index ade11eebf..a444c7bec 100644 --- a/packages/default.gbui/src/GBUIApp.js +++ b/packages/default.gbui/src/GBUIApp.js @@ -326,7 +326,7 @@ class GBUIApp extends React.Component { ref={chat => { this.chat = chat; }} - locale={'pt-br'} + locale={'en-us'} directLine={this.state.line} webSpeechPonyfillFactory={window.WebChat.createCognitiveServicesSpeechServicesPonyfillFactory({ credentials: { authorizationToken: token, region: 'westus' } diff --git a/packages/kb.gbapp/dialogs/AskDialog.ts b/packages/kb.gbapp/dialogs/AskDialog.ts index 5ed1df231..20ef55adc 100644 --- a/packages/kb.gbapp/dialogs/AskDialog.ts +++ b/packages/kb.gbapp/dialogs/AskDialog.ts @@ -50,6 +50,9 @@ import { GBVMService } from '../../basic.gblib/services/GBVMService.js'; import { GBImporter } from '../../core.gbapp/services/GBImporterService.js'; import { GBDeployer } from '../../core.gbapp/services/GBDeployer.js'; import { GBConfigService } from '../../core.gbapp/services/GBConfigService.js'; +import Fs from 'fs'; +import urlJoin from 'url-join'; +import { SystemKeywords } from '../../basic.gblib/services/SystemKeywords.js'; /** * Dialog arguments. @@ -78,6 +81,7 @@ export class AskDialog extends IGBDialog { min.dialogs.add(new WaterfallDialog('/answerEvent', AskDialog.getAnswerEventDialog(service, min))); min.dialogs.add(new WaterfallDialog('/answer', AskDialog.getAnswerDialog(min, service))); min.dialogs.add(new WaterfallDialog('/ask', AskDialog.getAskDialog(min))); + min.dialogs.add(new WaterfallDialog('/dialog', AskDialog.getLLVMDialog(min, service))); } private static getAskDialog(min: GBMinInstance) { @@ -234,7 +238,7 @@ export class AskDialog extends IGBDialog { } // TODO: https://github.com/GeneralBots/BotServer/issues/9 // else if (user.subjects && user.subjects.length > 0) { - // // ...second time running Search, now with no filter. + // // ..second time running Search, now with no filter. // const resultsB = await service.ask(min.instance, text, searchScore, undefined); @@ -304,6 +308,7 @@ export class AskDialog extends IGBDialog { } const CHATGPT_TIMEOUT = 60 * 1000; GBLog.info(`ChatGPT being used...`); + const response = await GBServer.globals.chatGPT.sendMessage(text, { timeoutMs: CHATGPT_TIMEOUT }); if (!response) { @@ -363,4 +368,77 @@ export class AskDialog extends IGBDialog { } ]; } + + private static getLLVMDialog(min: GBMinInstance, service: KBService) { + return [ + 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 => { + return await min.conversationalService.prompt(min, step, 'Please, describe the dialog scene.'); + }, + async step => { + step.options.dialog = step.result; + return await min.conversationalService.prompt(min, step, 'How would you call this?'); + }, + async step => { + if (GBServer.globals.chatGPT) { + let input = `Write a BASIC program that ${step.options.dialog.toLowerCase()}. And does not explain.`; + + await min.conversationalService.sendText(min, step, 'Thank you. The dialog is being written right now...'); + + const CHATGPT_TIMEOUT = 3 * 60 * 1000; + GBLog.info(`ChatGPT Code: ${input}`); + let response = await GBServer.globals.chatGPT.sendMessage(input, { + timeoutMs: CHATGPT_TIMEOUT + }); + + // Removes instructions, just code. + + response = response.replace(/Copy code/gim, '\n'); + let lines = response.split('\n') + let filteredLines = lines.filter(line => /\s*\d+\s*.*/.test(line)) + response = filteredLines.join('\n'); + + // Gets dialog name and file handling + + let dialogName = step.result.replace('.', ''); + const docx = urlJoin(`${min.botId}.gbdialog`, `${dialogName}.docx`); + const sys = new SystemKeywords(); + const document = await sys.internalCreateDocument(min, docx, response); + await service.addQA(min, dialogName, dialogName); + + let message = `Waiting for publishing...`; + await min.conversationalService.sendText(min, step, message); + + await step.replaceDialog('/publish', { confirm: true }); + + message = `Dialog is ready! Let's run:`; + await min.conversationalService.sendText(min, step, message); + + + let sec = new SecService(); + const member = step.context.activity.from; + const user = await sec.ensureUser( + min.instance.instanceId, + member.id, + member.name, + '', + 'web', + member.name, + null + ); + + await step.endDialog(); + + await GBVMService.callVM(dialogName.toLowerCase(), + min, step, user, this.deployer, false); + } + } + ]; + } } diff --git a/packages/kb.gbapp/services/KBService.ts b/packages/kb.gbapp/services/KBService.ts index 691731249..0219f40e4 100644 --- a/packages/kb.gbapp/services/KBService.ts +++ b/packages/kb.gbapp/services/KBService.ts @@ -529,7 +529,7 @@ export class KBService implements IGBKBService { const isBasic = answer.toLowerCase().startsWith('/basic'); if (/TALK\s*\".*\"/gi.test(answer) || isBasic) { const code = isBasic ? answer.substr(6) : answer; - const path = DialogKeywords.getGBAIPath(min.botId,`gbdialog`); + const path = DialogKeywords.getGBAIPath(min.botId, `gbdialog`); const scriptName = `tmp${GBAdminService.getRndReadableIdentifier()}.docx`; const localName = Path.join('work', path, `${scriptName}`); Fs.writeFileSync(localName, code, { encoding: null }); @@ -604,17 +604,11 @@ export class KBService implements IGBKBService { answer.content.endsWith('.xlsx') ) { const path = DialogKeywords.getGBAIPath(min.botId, `gbkb`); - const doc = urlJoin( - GBServer.globals.publicAddress, - 'kb', - path, - 'assets', - answer.content - ); + const doc = urlJoin(GBServer.globals.publicAddress, 'kb', path, 'assets', answer.content); const url = `http://view.officeapps.live.com/op/view.aspx?src=${doc}`; await this.playUrl(min, min.conversationalService, step, url, channel); } else if (answer.content.endsWith('.pdf')) { - const path = DialogKeywords.getGBAIPath(min.botId,`gbkb`); + const path = DialogKeywords.getGBAIPath(min.botId, `gbkb`); const url = urlJoin('kb', path, 'assets', answer.content); await this.playUrl(min, min.conversationalService, step, url, channel); } else if (answer.format === '.md') { @@ -627,6 +621,37 @@ export class KBService implements IGBKBService { } } + public async addQA(min, questionText, answerText) { + const pkg = await GuaribasPackage.findOne({ + where: { instanceId: min.instance.instanceId } + }); + + const question = { + from: 'autodialog', + to: '', + subject1: '', + subject2: '', + subject3: '', + subject4: '', + content: questionText.replace(/["]+/g, ''), + instanceId: min.instance.instanceId, + skipIndex: false, + packageId: pkg.packageId + }; + const answer = { + instanceId: min.instance.instanceId, + content: answerText, + format: '.txt', + media: null, + packageId: pkg.packageId, + prevId: 0 + }; + const a =await GuaribasAnswer.create(answer); + question['answerId'] = a.answerId; + const q = await GuaribasQuestion.create(question); + + } + public async importKbPackage( min: GBMinInstance, localPath: string, @@ -692,9 +717,12 @@ export class KBService implements IGBKBService { 'cache', `img-docx${GBAdminService.getRndReadableIdentifier()}.png` ); - const url = urlJoin(GBServer.globals.publicAddress, - DialogKeywords.getGBAIPath(instance.botId).replace(/\.[^/.]+$/, "") - , 'cache', Path.basename(localName)); + const url = urlJoin( + GBServer.globals.publicAddress, + DialogKeywords.getGBAIPath(instance.botId).replace(/\.[^/.]+$/, ''), + 'cache', + Path.basename(localName) + ); const buffer = await image.read(); Fs.writeFileSync(localName, buffer, { encoding: null }); return { src: url }; @@ -965,7 +993,7 @@ export class KBService implements IGBKBService { let category = categoryReg[1]; if (category === 'number') { - min['nerEngine'].addRegexEntity('number','pt', '/d+/gi'); + min['nerEngine'].addRegexEntity('number', 'pt', '/d+/gi'); } if (nameReg) { let name = nameReg[1]; @@ -996,13 +1024,8 @@ export class KBService implements IGBKBService { GBLog.info(`[GBDeployer] Start Bot Server Side Rendering... ${localPath}`); const html = await GBSSR.getHTML(min); - let path = DialogKeywords.getGBAIPath(min.botId,`gbui`); - path = Path.join( - process.env.PWD, - 'work', - path, - 'index.html' - ); + let path = DialogKeywords.getGBAIPath(min.botId, `gbui`); + path = Path.join(process.env.PWD, 'work', path, 'index.html'); GBLogEx.info(min, `[GBDeployer] Saving SSR HTML in ${path}.`); Fs.writeFileSync(path, html, 'utf8'); diff --git a/packages/security.gbapp/services/SecService.ts b/packages/security.gbapp/services/SecService.ts index 18e2b3ad3..510b9c580 100644 --- a/packages/security.gbapp/services/SecService.ts +++ b/packages/security.gbapp/services/SecService.ts @@ -279,7 +279,7 @@ export class SecService extends GBService { { obj = {}; } - obj['name'] = value; + obj[name] = value; user.params = JSON.stringify(obj); return await user.save(); } diff --git a/src/app.ts b/src/app.ts index 30bf8d4e0..6fd7e8a1e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -52,11 +52,13 @@ import { GBDeployer } from '../packages/core.gbapp/services/GBDeployer.js'; import { GBImporter } from '../packages/core.gbapp/services/GBImporterService.js'; import { GBMinService } from '../packages/core.gbapp/services/GBMinService.js'; import auth from 'basic-auth'; +import { ChatGPTAPIBrowser } from 'chatgpt'; import child_process from 'child_process'; import * as winston from 'winston-logs-display'; import { RootData } from './RootData.js'; import { GBSSR } from '../packages/core.gbapp/services/GBSSR.js'; import { Mutex } from 'async-mutex'; +import { GBVMService } from '../packages/basic.gblib/services/GBVMService.js'; /** * General Bots open-core entry point. @@ -82,7 +84,7 @@ export class GBServer { GBLog.error(`Running TEST_SHELL ERROR: ${error}...`); } } - + const server = express(); GBServer.globals.server = server; @@ -215,6 +217,22 @@ export class GBServer { GBServer.globals.minService = minService; await minService.buildMin(instances); + if (process.env.OPENAI_EMAIL) { + if (!GBServer.globals.chatGPT) { + GBServer.globals.chatGPT = new ChatGPTAPIBrowser({ + email: process.env.OPENAI_EMAIL, + password: process.env.OPENAI_PASSWORD, + markdown: false + }); + await GBServer.globals.chatGPT.init(); + } + } + + // let s = new GBVMService(); + // await s.translateBASIC('work/gptA.vbs', GBServer.globals.minBoot ); + // await s.translateBASIC('work/gptB.vbs', GBServer.globals.minBoot ); + // await s.translateBASIC('work/gptC.vbs', GBServer.globals.minBoot ); + // process.exit(9); if (process.env.ENABLE_WEBLOG) { // If global log enabled, reorders transports adding web logging. @@ -257,8 +275,6 @@ export class GBServer { })(); }; - // - if (process.env.CERTIFICATE_PFX) { const options1 = { passphrase: process.env.CERTIFICATE_PASSPHRASE,