diff --git a/package.json b/package.json index 734cbaa81..1bfd603c6 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "adm-zip": "0.5.9", "alasql": "2.1.6", "any-shell-escape": "0.1.1", + "arraybuffer-to-buffer": "^0.0.7", "async-promises": "0.2.3", "basic-auth": "2.0.1", "billboard.js": "3.6.3", diff --git a/packages/analytics.gblib/services/AnalyticsService.ts b/packages/analytics.gblib/services/AnalyticsService.ts index b904eccd8..3be6af745 100644 --- a/packages/analytics.gblib/services/AnalyticsService.ts +++ b/packages/analytics.gblib/services/AnalyticsService.ts @@ -83,15 +83,16 @@ export class AnalyticsService { public async createMessage ( instanceId: number, - conversation: GuaribasConversation, + conversationId: number, userId: number, content: string ): Promise { + const message = GuaribasConversationMessage.build(); message.content = typeof content === 'object' ? JSON.stringify(content) : content; message.instanceId = instanceId; message.userId = userId; - message.conversationId = conversation.conversationId; + message.conversationId = conversationId; return await message.save(); } diff --git a/packages/basic.gblib/services/DialogKeywords.ts b/packages/basic.gblib/services/DialogKeywords.ts index 321a478cf..259852dde 100644 --- a/packages/basic.gblib/services/DialogKeywords.ts +++ b/packages/basic.gblib/services/DialogKeywords.ts @@ -588,8 +588,29 @@ export class DialogKeywords { this['id'] = this.sys().getRandomId(); } + private isUserSystemParam(name: string): Boolean { + const names = [ + 'welcomed', + 'loaded', + 'subjects', + 'cb', + 'welcomed', + 'maxLines', + 'translatorOn', + 'wholeWord', + 'theme', + 'maxColumns' + ]; + + return names.indexOf(name) > -1; + } + + private async setOption({pid, name, value}) { + if (this.isUserSystemParam(name)){ + throw new Error(`Not possible to define ${name} as it is a reserved system param name.`); + } const process = GBServer.globals.processes[pid]; let { min, user, params } = await DialogKeywords.getProcessInfo(pid); const sec = new SecService(); @@ -598,6 +619,17 @@ export class DialogKeywords { return { min, user, params }; } + private async getOption({pid, name}) + { + if (this.isUserSystemParam(name)){ + throw new Error(`Not possible to retrieve ${name} system param.`); + } + const process = GBServer.globals.processes[pid]; + let { min, user, params } = await DialogKeywords.getProcessInfo(pid); + const sec = new SecService(); + return await sec.getParam(user, name); + } + /** * Defines the maximum lines to scan in spreedsheets. * @@ -608,6 +640,27 @@ export class DialogKeywords { await this.setOption({pid, name: "maxLines", value: count}); } + /** + * Defines a custom user param to be persisted to storage. + * + * @example SET PARAM name AS value + * + */ + public async setUserParam({ pid, name, value }) { + await this.setOption({pid, name, value}); + } + + /** + * Returns a custom user param persisted on storage. + * + * @example GET PARAM name + * + */ + public async getUserParam({ pid, name }) { + await this.getOption({pid, name}); + } + + /** * Defines the maximum lines to scan in spreedsheets. * @@ -772,12 +825,14 @@ export class DialogKeywords { await sleep(DEFAULT_HEAR_POLL_INTERVAL); } - const text = min.cbMap[userId].promise; + const answer = min.cbMap[userId].promise; if (kind === 'file') { + GBLog.info(`BASIC (${min.botId}): Upload done for ${answer.filename}.`); + // TODO: answer.filename, answer.data. } else if (kind === 'boolean') { - if (isIntentYes('pt-BR', text)) { + if (isIntentYes('pt-BR', answer)) { result = true; } else { result = false; @@ -787,7 +842,7 @@ export class DialogKeywords { return text.match(/([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)/gi); }; - const value = extractEntity(text); + const value = extractEntity(answer); if (value === null) { await this.talk({ pid, text: 'Por favor, digite um e-mail válido.' }); @@ -800,7 +855,7 @@ export class DialogKeywords { return text.match(/[_a-zA-Z][_a-zA-Z0-9]{0,16}/gi); }; - const value = extractEntity(text); + const value = extractEntity(answer); if (value === null || value.length != 1) { await this.talk({ pid, text: 'Por favor, digite um nome válido.' }); @@ -813,7 +868,7 @@ export class DialogKeywords { return text.match(/\d+/gi); }; - const value = extractEntity(text); + const value = extractEntity(answer); if (value === null || value.length != 1) { await this.talk({ pid, text: 'Por favor, digite um número válido.' }); @@ -828,7 +883,7 @@ export class DialogKeywords { ); }; - const value = extractEntity(text); + const value = extractEntity(answer); if (value === null || value.length != 1) { await this.talk({ pid, text: 'Por favor, digite uma data no formato 12/12/2020.' }); @@ -841,7 +896,7 @@ export class DialogKeywords { return text.match(/^([0-1]?[0-9]|2[0-4]):([0-5][0-9])(:[0-5][0-9])?$/gi); }; - const value = extractEntity(text); + const value = extractEntity(answer); if (value === null || value.length != 1) { await this.talk({ pid, text: 'Por favor, digite um horário no formato hh:ss.' }); @@ -860,7 +915,7 @@ export class DialogKeywords { return []; }; - const value = extractEntity(text); + const value = extractEntity(answer); if (value === null || value.length != 1) { await this.talk({ pid, text: 'Por favor, digite um valor monetário.' }); @@ -872,7 +927,7 @@ export class DialogKeywords { let phoneNumber; try { // https://github.com/GeneralBots/BotServer/issues/307 - phoneNumber = phone(text, { country: 'BRA' })[0]; + phoneNumber = phone(answer, { country: 'BRA' })[0]; phoneNumber = phoneUtil.parse(phoneNumber); } catch (error) { await this.talk({ pid, text: Messages[locale].validation_enter_valid_mobile }); @@ -897,7 +952,7 @@ export class DialogKeywords { } }; - const value = extractEntity(text); + const value = extractEntity(answer); if (value === null || value.length != 1) { await this.talk({ pid, text: 'Por favor, digite um CEP válido.' }); @@ -909,7 +964,7 @@ export class DialogKeywords { const list = args; result = null; await CollectionUtil.asyncForEach(list, async item => { - if (GBConversationalService.kmpSearch(text, item) != -1) { + if (GBConversationalService.kmpSearch(answer, item) != -1) { result = item; } }); @@ -939,8 +994,8 @@ export class DialogKeywords { await CollectionUtil.asyncForEach(list, async item => { if ( - GBConversationalService.kmpSearch(text.toLowerCase(), item.name.toLowerCase()) != -1 || - GBConversationalService.kmpSearch(text.toLowerCase(), item.code.toLowerCase()) != -1 + GBConversationalService.kmpSearch(answer.toLowerCase(), item.name.toLowerCase()) != -1 || + GBConversationalService.kmpSearch(answer.toLowerCase(), item.code.toLowerCase()) != -1 ) { result = item.code; } diff --git a/packages/basic.gblib/services/GBVMService.ts b/packages/basic.gblib/services/GBVMService.ts index 7bb2f324e..9b4887d11 100644 --- a/packages/basic.gblib/services/GBVMService.ts +++ b/packages/basic.gblib/services/GBVMService.ts @@ -659,6 +659,20 @@ export class GBVMService extends GBService { } ]; + keywords[i++] = [ + /^\s*set param \s*(.*)\s*as\s*(.*)/gim, + ($0, $1, $2) => { + return `await dk.setUserParam ({pid: pid, ${$1}}, ${$2})`; + } + ]; + + keywords[i++] = [ + /^\s*get param \s*(.*)/gim, + ($0, $1, $2) => { + return `await dk.getUserParam ({pid: pid, ${$1}})`; + } + ]; + keywords[i++] = [ /^\s*set header\s*(.*)\s*as\s*(.*)/gim, ($0, $1, $2) => { diff --git a/packages/core.gbapp/services/GBMinService.ts b/packages/core.gbapp/services/GBMinService.ts index d65165e80..1b0eac762 100644 --- a/packages/core.gbapp/services/GBMinService.ts +++ b/packages/core.gbapp/services/GBMinService.ts @@ -46,6 +46,8 @@ import { FacebookAdapter } from 'botbuilder-adapter-facebook'; import path from 'path'; import mkdirp from 'mkdirp'; import Fs from 'fs'; +import arrayBufferToBuffer from 'arraybuffer-to-buffer'; + import { AutoSaveStateMiddleware, BotFrameworkAdapter, @@ -826,11 +828,11 @@ export class GBMinService { // Get loaded user state - const member = context.activity.from; const step = await min.dialogs.createContext(context); step.context.activity.locale = 'pt-BR'; let firstTime = false; + const member = context.activity.from; const sec = new SecService(); const user = await sec.ensureUser(instance.instanceId, member.id, member.name, '', 'web', member.name, null); const userId = user.userId; @@ -1061,36 +1063,28 @@ export class GBMinService { private static async downloadAttachmentAndWrite(attachment) { const url = attachment.contentUrl; - // https://github.com/GeneralBots/BotServer/issues/195 - '${botId}','uploads'); + // TODO: https://github.com/GeneralBots/BotServer/issues/195 - '${botId}','uploads'); const localFolder = Path.join('work'); - const localFileName = Path.join(localFolder, this['botId'],'uploads', attachment.name); - - try { - let response; - if (url.startsWith('data:')) { - var regex = /^data:.+\/(.+);base64,(.*)$/; - var matches = url.match(regex); - var ext = matches[1]; - var data = matches[2]; - response = Buffer.from(data, 'base64'); - } else { - // arraybuffer is necessary for images - const options = { - method: 'GET', - encoding: 'binary' - }; - response = await fetch(url, options); - } - - Fs.writeFile(localFileName, response, fsError => { - if (fsError) { - throw fsError; - } - }); - } catch (error) { - console.error(error); - return undefined; + const localFileName = Path.join(localFolder, `${this['min'].botId}.gbai`, 'uploads', attachment.name); + + let res; + if (url.startsWith('data:')) { + var regex = /^data:.+\/(.+);base64,(.*)$/; + var matches = url.match(regex); + var ext = matches[1]; + var data = matches[2]; + res = Buffer.from(data, 'base64'); + } else { + // arraybuffer is necessary for images + const options = { + method: 'GET', + encoding: 'binary' + }; + res = await fetch(url, options); + const buffer = arrayBufferToBuffer(await res.arrayBuffer()); + Fs.writeFileSync(localFileName, buffer); } + // If no error was thrown while writing to disk,return the attachment's name // and localFilePath for the response back to the user. return { @@ -1133,48 +1127,62 @@ export class GBMinService { context.activity.text = context.activity.text.trim(); - const user = await min.userProfile.get(context, {}); + const member = context.activity.from; + + let user = await sec.ensureUser(min.instance.instanceId, member.id, member.name, '', 'web', member.name, null); + const userId = user.userId; + const params = user.params ? JSON.parse(user.params) : {}; + let message: GuaribasConversationMessage; if (process.env.PRIVACY_STORE_MESSAGES === 'true') { // Adds message to the analytics layer. const analytics = new AnalyticsService(); + if (user) { - if (!user.conversation) { - user.conversation = await analytics.createConversation(user.systemUser); + let conversation; + if (!user.conversationId) { + conversation = await analytics.createConversation(user); + user.conversationId = conversation.Id; } message = await analytics.createMessage( min.instance.instanceId, - user.conversation, - user.systemUser.userId, + user.conversationId, + userId, context.activity.text ); } } - if (process.env.ENABLE_DOWNLOAD) { - // Prepare Promises to download each attachment and then execute each Promise. + // Prepare Promises to download each attachment and then execute each Promise. - const promises = step.context.activity.attachments.map(GBMinService.downloadAttachmentAndWrite.bind(min)); - const successfulSaves = await Promise.all(promises); - async function replyForReceivedAttachments(localAttachmentData) { - if (localAttachmentData) { - // Because the TurnContext was bound to this function,the bot can call - // `TurnContext.sendActivity` via `this.sendActivity`; - await this.sendActivity(`Upload OK.`); - } else { - await this.sendActivity('Error uploading file. Please,start again.'); - } + const promises = step.context.activity.attachments.map( + GBMinService.downloadAttachmentAndWrite.bind({ min, user, params }) + ); + const successfulSaves = await Promise.all(promises); + async function replyForReceivedAttachments(localAttachmentData) { + if (localAttachmentData) { + // Because the TurnContext was bound to this function,the bot can call + // `TurnContext.sendActivity` via `this.sendActivity`; + await this.sendActivity(`Upload OK.`); + } else { + await this.sendActivity('Error uploading file. Please,start again.'); } - // Prepare Promises to reply to the user with information about saved attachments. - // The current TurnContext is bound so `replyForReceivedAttachments` can also send replies. - const replyPromises = successfulSaves.map(replyForReceivedAttachments.bind(step.context)); - await Promise.all(replyPromises); + } + // Prepare Promises to reply to the user with information about saved attachments. + // The current TurnContext is bound so `replyForReceivedAttachments` can also send replies. + const replyPromises = successfulSaves.map(replyForReceivedAttachments.bind(step.context)); + await Promise.all(replyPromises); + if (successfulSaves.length > 0) { const result = { data: Fs.readFileSync(successfulSaves[0]['localPath']), filename: successfulSaves[0]['fileName'] }; + + if (min.cbMap[userId] && min.cbMap[userId].promise == '!GBHEAR') { + min.cbMap[userId].promise = result; + } } // Files in .gbdialog can be called directly by typing its name normalized into JS . @@ -1298,13 +1306,11 @@ export class GBMinService { 'Language Detector', GBConfigService.getBoolean('LANGUAGE_DETECTOR') ) === 'true'; - const systemUser = user.systemUser; - locale = systemUser.locale; + locale = user.locale; if (text != '' && detectLanguage && !locale) { locale = await min.conversationalService.getLanguage(min, text); - if (systemUser.locale != locale) { - user.systemUser = await sec.updateUserLocale(systemUser.userId, locale); - await min.userProfile.set(step.context, user); + if (user.locale != locale) { + user = await sec.updateUserLocale(user.userId, locale); } } @@ -1340,10 +1346,10 @@ export class GBMinService { GBLog.info(`Text>: ${text}.`); - if (user.systemUser.agentMode === 'self') { - const manualUser = await sec.getUserFromAgentSystemId(user.systemUser.userSystemId); + if (user.agentMode === 'self') { + const manualUser = await sec.getUserFromAgentSystemId(user.userSystemId); - GBLog.info(`HUMAN AGENT (${user.systemUser.userSystemId}) TO USER ${manualUser.userSystemId}: ${text}`); + GBLog.info(`HUMAN AGENT (${user.userId}) TO USER ${manualUser.userSystemId}: ${text}`); const cmd = 'SEND FILE '; if (text.startsWith(cmd)) { @@ -1366,8 +1372,8 @@ export class GBMinService { ); } } else { - if (min.cbMap[user.systemUser.userId] && min.cbMap[user.systemUser.userId].promise == '!GBHEAR') { - min.cbMap[user.systemUser.userId].promise = text; + if (min.cbMap[userId] && min.cbMap[userId].promise == '!GBHEAR') { + min.cbMap[userId].promise = text; } // If there is a dialog in course, continue to the next step. diff --git a/packages/security.gbapp/models/index.ts b/packages/security.gbapp/models/index.ts index 03ec59f79..1c92a9f35 100644 --- a/packages/security.gbapp/models/index.ts +++ b/packages/security.gbapp/models/index.ts @@ -36,6 +36,7 @@ 'use strict'; +import { GuaribasConversation } from '../../analytics.gblib/models/index.js'; import { AutoIncrement, BelongsTo, @@ -97,11 +98,14 @@ export class GuaribasUser extends Model { @Column(DataType.TEXT) declare conversationReference: string; + @Column(DataType.INTEGER) + declare conversationId: number; + @Column(DataType.STRING(64)) declare hearOnDialog: string; @Column(DataType.STRING(4000)) - declare params: string; + declare params: string; } /**