From ceb5f0aaf961dc1265f1cb3ec4a0667ed33523ab Mon Sep 17 00:00:00 2001 From: Rodrigo Rodriguez Date: Wed, 3 Mar 2021 16:46:18 -0300 Subject: [PATCH] new(whatsapp.gblib): New Teams and WhatsApp support. --- DATABASE-CHANGES.md | 10 +++ .../basic.gblib/services/SystemKeywords.ts | 2 +- .../services/GBConversationalService.ts | 25 ++++-- packages/core.gbapp/services/GBMinService.ts | 84 +++++++++++-------- .../dialogs/FeedbackDialog.ts | 80 ++++++++++++++++-- .../customer-satisfaction.gbapp/strings.ts | 3 +- packages/kb.gbapp/dialogs/AskDialog.ts | 2 +- packages/security.gbapp/models/index.ts | 2 +- .../security.gbapp/services/SecService.ts | 23 +++-- .../services/WhatsappDirectLine.ts | 65 ++++++++++---- 10 files changed, 223 insertions(+), 73 deletions(-) diff --git a/DATABASE-CHANGES.md b/DATABASE-CHANGES.md index 55108766..4af86281 100644 --- a/DATABASE-CHANGES.md +++ b/DATABASE-CHANGES.md @@ -80,3 +80,13 @@ GO ALTER TABLE dbo.GuaribasInstance ADD translatorEndpoint nvarchar(128) NULL GO + + +# 2.0.108 + +ALTER TABLE [dbo].[GuaribasInstance] DROP COLUMN [agentSystemId] +GO + +ALTER TABLE dbo.GuaribasUser ADD + agentSystemId nvarchar(255) NULL, +GO diff --git a/packages/basic.gblib/services/SystemKeywords.ts b/packages/basic.gblib/services/SystemKeywords.ts index d447c1a2..b19107f9 100644 --- a/packages/basic.gblib/services/SystemKeywords.ts +++ b/packages/basic.gblib/services/SystemKeywords.ts @@ -125,7 +125,7 @@ export class SystemKeywords { let sec = new SecService(); let user = await sec.getUserFromSystemId(from); if (!user) { - user = await sec.ensureUser(this.min.instance.instanceId, from, from, null, 'whatsapp', 'from'); + user = await sec.ensureUser(this.min.instance.instanceId, from, from, null, 'whatsapp', 'from', null); } await sec.updateUserHearOnDialog(user.userId, dialogName); } diff --git a/packages/core.gbapp/services/GBConversationalService.ts b/packages/core.gbapp/services/GBConversationalService.ts index 2dafc71d..e0dce88f 100644 --- a/packages/core.gbapp/services/GBConversationalService.ts +++ b/packages/core.gbapp/services/GBConversationalService.ts @@ -47,6 +47,7 @@ import { AnalyticsService } from '../../analytics.gblib/services/AnalyticsServic import { MicrosoftAppCredentials } from 'botframework-connector'; import { GBConfigService } from './GBConfigService'; import { CollectionUtil, AzureText } from 'pragmatismo-io-framework'; +import { GuaribasUser } from '../../security.gbapp/models'; const urlJoin = require('url-join'); const PasswordGenerator = require('strict-password-generator').default; const Nexmo = require('nexmo'); @@ -851,19 +852,27 @@ export class GBConversationalService { const users = await service.getAllUsers(min.instance.instanceId); await CollectionUtil.asyncForEach(users, async user => { if (user.conversationReference) { - const ref = JSON.parse(user.conversationReference); - MicrosoftAppCredentials.trustServiceUrl(ref.serviceUrl); - await min.bot['createConversation'](ref, async t1 => { - const ref2 = TurnContext.getConversationReference(t1.activity); - await min.bot.continueConversation(ref2, async t2 => { - await t2.sendActivity(message); - }); - }); + await this.sendOnConversation(min, user, message); } else { GBLog.info(`User: ${user.systemUserId} with no conversation ID while broadcasting.`); } }); } + + /** + * + * Sends a message in a user with an already started conversation (got ConversationReference set) + */ + public async sendOnConversation(min: GBMinInstance, user: GuaribasUser, message: string) { + const ref = JSON.parse(user.conversationReference); + MicrosoftAppCredentials.trustServiceUrl(ref.serviceUrl); + await min.bot['createConversation'](ref, async (t1) => { + const ref2 = TurnContext.getConversationReference(t1.activity); + await min.bot.continueConversation(ref2, async (t2) => { + await t2.sendActivity(message); + }); + }); + } public static kmpSearch(pattern, text) { pattern = pattern.toLowerCase(); diff --git a/packages/core.gbapp/services/GBMinService.ts b/packages/core.gbapp/services/GBMinService.ts index 6203cacf..44f4b48f 100644 --- a/packages/core.gbapp/services/GBMinService.ts +++ b/packages/core.gbapp/services/GBMinService.ts @@ -307,7 +307,7 @@ export class GBMinService { const sec = new SecService(); let user = await sec.getUserFromSystemId(id); if (user === null || user.hearOnDialog) { - user = await sec.ensureUser(activeMin.instance.instanceId, id, senderName, '', 'whatsapp', senderName); + user = await sec.ensureUser(activeMin.instance.instanceId, id, senderName, '', 'whatsapp', senderName, null); const startDialog = user.hearOnDialog ? user.hearOnDialog : @@ -760,10 +760,10 @@ export class GBMinService { }); const service = new KBService(min.core.sequelize); const data = await service.getFaqBySubjectArray(instance.instanceId, 'faq', undefined); - await min.conversationalService.sendEvent(min, step, 'play', { - playerType: 'bullet', - data: data.slice(0, 10) - }); + await min.conversationalService.sendEvent(min, step, 'play', { + playerType: 'bullet', + data: data.slice(0, 10) + }); // This same event is dispatched either to all participants // including the bot, that is filtered bellow. @@ -779,17 +779,10 @@ export class GBMinService { member.name, '', 'web', - member.name + member.name, + null ); - // Required for MSTEAMS handling of persisted conversations. - - if (step.context.activity.channelId === 'msteams') { - persistedUser.conversationReference = JSON.stringify( - TurnContext.getConversationReference(context.activity) - ); - await persistedUser.save(); - } // Stores conversation associated to the user to group each message. @@ -804,6 +797,18 @@ export class GBMinService { await min.userProfile.set(step.context, user); } + user.systemUser = await sec.getUserFromSystemId( user.systemUser.userSystemId); + await min.userProfile.set(step.context, user); + + // Required for MSTEAMS handling of persisted conversations. + + if (step.context.activity.channelId === 'msteams') { + const conversationReference = JSON.stringify( + TurnContext.getConversationReference(context.activity) + ); + await sec.updateConversationReferenceById(user.systemUser.userId, conversationReference); + } + GBLog.info(`User>: text:${context.activity.text} (type: ${context.activity.type}, name: ${context.activity.name}, channelId: ${context.activity.channelId}, value: ${context.activity.value})`); // Answer to specific BOT Framework event conversationUpdate to auto start dialogs. @@ -911,6 +916,8 @@ export class GBMinService { */ private async processMessageActivity(context, min: GBMinInstance, step: GBDialogStep) { + const sec = new SecService(); + // Removes Bot Id from MS Teams. context.activity.text = context.activity.text.trim(); @@ -1055,7 +1062,7 @@ export class GBMinService { if (detectLanguage || !locale) { locale = await min.conversationalService.getLanguage(min, text); if (systemUser.locale != locale) { - const sec = new SecService(); + user.systemUser = await sec.updateUserLocale(systemUser.userId, locale); await min.userProfile.set(step.context, user); } @@ -1092,28 +1099,39 @@ export class GBMinService { context.activity.originalText = originalText; GBLog.info(`Final text ready for NLP/Search/.gbapp: ${text}.`); - // If there is a dialog in course, continue to the next step. - if (step.activeDialog !== undefined) { - await step.continueDialog(); + if (user.systemUser.agentMode === 'self') { + const manualUser = await sec.getUserFromAgentSystemId(user.systemUser.userSystemId); - } else { + GBLog.info(`HUMAN AGENT (${user.systemUser.userSystemId}) TO USER ${manualUser.userSystemId}: ${text}`); + await min.whatsAppDirectLine.sendToDeviceEx(manualUser.userSystemId, `${manualUser.agentSystemId}: ${text}`, locale); - let nextDialog = null; - await CollectionUtil.asyncForEach(min.appPackages, async (e: IGBPackage) => { - nextDialog = await e.onExchangeData(min, 'handleAnswer', { - query: text, - step: step, - notTranslatedQuery: originalText, - message: message ? message['dataValues'] : null, - user: user ? user.dataValues : null + } + else { + + // If there is a dialog in course, continue to the next step. + + if (step.activeDialog !== undefined) { + await step.continueDialog(); + + } else { + + let nextDialog = null; + await CollectionUtil.asyncForEach(min.appPackages, async (e: IGBPackage) => { + nextDialog = await e.onExchangeData(min, 'handleAnswer', { + query: text, + step: step, + notTranslatedQuery: originalText, + message: message ? message['dataValues'] : null, + user: user ? user.dataValues : null + }); }); - }); - await step.beginDialog(nextDialog ? nextDialog : '/answer', { - query: text, - user: user ? user.dataValues : null, - message: message - }); + await step.beginDialog(nextDialog ? nextDialog : '/answer', { + query: text, + user: user ? user.dataValues : null, + message: message + }); + } } } } diff --git a/packages/customer-satisfaction.gbapp/dialogs/FeedbackDialog.ts b/packages/customer-satisfaction.gbapp/dialogs/FeedbackDialog.ts index c39ad7e8..618484b8 100644 --- a/packages/customer-satisfaction.gbapp/dialogs/FeedbackDialog.ts +++ b/packages/customer-satisfaction.gbapp/dialogs/FeedbackDialog.ts @@ -75,14 +75,25 @@ export class FeedbackDialog extends IGBDialog { const locale = step.context.activity.locale; const sec = new SecService(); - const from = step.context.activity.from.id; + let from = step.context.activity.from.id; await min.conversationalService.sendText(min, step, Messages[locale].please_wait_transfering); const agentSystemId = await sec.assignHumanAgent(from, min.instance.instanceId); - await min.whatsAppDirectLine.sendToDevice(agentSystemId, - Messages[locale].notify_agent(step.context.activity.from.name)); + const user = await min.userProfile.get(step.context, {}); + user.systemUser = await sec.getUserFromAgentSystemId(agentSystemId); + await min.userProfile.set(step.context, user); + if (agentSystemId.charAt(2) === ":") { // Agent is from Teams. + const agent = await sec.getUserFromSystemId(agentSystemId); + await min.conversationalService['sendOnConversation'](min, agent, + Messages[locale].notify_agent(step.context.activity.from.name)); + + } + else { + await min.whatsAppDirectLine.sendToDevice(agentSystemId, Messages[locale].notify_agent(step.context.activity.from.name)); + + } return await step.next(); } ]) @@ -95,10 +106,65 @@ export class FeedbackDialog extends IGBDialog { const locale = step.context.activity.locale; const sec = new SecService(); - const from = step.context.activity.from.id; + const userSystemId = step.context.activity.from.id; + const user = await min.userProfile.get(step.context, {}); + + if (user.systemUser.agentMode === 'self') { + const manualUser = await sec.getUserFromAgentSystemId(userSystemId); + + await min.whatsAppDirectLine.sendToDeviceEx(manualUser.userSystemId, + Messages[locale].notify_end_transfer(min.instance.botId), locale); + + if (userSystemId.charAt(2) === ":") { // Agent is from Teams. + await min.conversationalService.sendText(min, step, Messages[locale].notify_end_transfer(min.instance.botId)); + } + else { + await min.whatsAppDirectLine.sendToDeviceEx(userSystemId, + Messages[locale].notify_end_transfer(min.instance.botId), locale); + } + + await sec.updateHumanAgent(userSystemId, min.instance.instanceId, null); + await sec.updateHumanAgent(manualUser.userSystemId, min.instance.instanceId, null); + + user.systemUser = await sec.getUserFromSystemId(userSystemId); + await min.userProfile.set(step.context, user); + + } + + else if (user.systemUser.agentMode === 'human') { + const agent = await sec.getUserFromSystemId(user.systemUser.agentSystemId); + + await min.whatsAppDirectLine.sendToDeviceEx(user.systemUser.userSystemId, + Messages[locale].notify_end_transfer(min.instance.botId), locale); + + + if (user.systemUser.agentSystemId.charAt(2) === ":") { // Agent is from Teams. + await min.conversationalService.sendText(min, step, Messages[locale].notify_end_transfer(min.instance.botId)); + } + else { + await min.whatsAppDirectLine.sendToDeviceEx(user.systemUser.agentSystemId, + Messages[locale].notify_end_transfer(min.instance.botId), locale); + } + + await sec.updateHumanAgent(user.systemUser.userSystemId, min.instance.instanceId, null); + await sec.updateHumanAgent(agent.userSystemId, min.instance.instanceId, null); + + user.systemUser = await sec.getUserFromSystemId(userSystemId); + await min.userProfile.set(step.context, user); + + } + else + { + if (user.systemUser.userSystemId.charAt(2) === ":") { // Agent is from Teams. + await min.conversationalService.sendText(min, step, 'Nenhum atendimento em andamento.'); + } + else { + await min.whatsAppDirectLine.sendToDeviceEx(user.systemUser.userSystemId, + 'Nenhum atendimento em andamento.'); + } + + } - await sec.updateCurrentAgent(from, min.instance.instanceId, null); - await min.conversationalService.sendText(min, step, Messages[locale].notify_end_transfer(min.instance.botId)); return await step.next(); } @@ -149,7 +215,7 @@ export class FeedbackDialog extends IGBDialog { } else { const message = min.core.getParam(min.instance, 'Feedback Improve Message', - Messages[fixedLocale].we_will_improve); // TODO: Improve to be multi-language. + Messages[fixedLocale].we_will_improve); // TODO: Improve to be multi-language. await min.conversationalService.sendText(min, step, message); } diff --git a/packages/customer-satisfaction.gbapp/strings.ts b/packages/customer-satisfaction.gbapp/strings.ts index d006a3f2..634ee147 100644 --- a/packages/customer-satisfaction.gbapp/strings.ts +++ b/packages/customer-satisfaction.gbapp/strings.ts @@ -25,7 +25,8 @@ export const Messages = { great_thanks: 'Ótimo, obrigado por contribuir com sua resposta.', please_no_bad_words: 'Por favor, sem palavrões!', please_wait_transfering: 'Por favor, aguarde enquanto eu localizo alguém para te atender.', - notify_agent: (name) => `Existe um novo atendimento para *${name}*, por favor, responda aqui mesmo para a pessoa. Para finalizar, digite /qt.` + notify_agent: (name) => `Existe um novo atendimento para *${name}*, por favor, responda aqui mesmo para a pessoa. Para finalizar, digite /qt.`, + notify_end_transfer: (botName) => `Falando novamente com o bot ${botName}.` } }; diff --git a/packages/kb.gbapp/dialogs/AskDialog.ts b/packages/kb.gbapp/dialogs/AskDialog.ts index 5edb4896..51c4548a 100644 --- a/packages/kb.gbapp/dialogs/AskDialog.ts +++ b/packages/kb.gbapp/dialogs/AskDialog.ts @@ -113,7 +113,7 @@ export class AskDialog extends IGBDialog { 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); + const user = await sec.ensureUser(min.instance.instanceId, member.id, member.name, '', 'web', member.name, null); let handled = false; let nextDialog = null; diff --git a/packages/security.gbapp/models/index.ts b/packages/security.gbapp/models/index.ts index cc696e8e..d7bf875c 100644 --- a/packages/security.gbapp/models/index.ts +++ b/packages/security.gbapp/models/index.ts @@ -78,7 +78,7 @@ export class GuaribasUser extends Model { @BelongsTo(() => GuaribasInstance) public instance: GuaribasInstance; - @Column(DataType.STRING(16)) + @Column(DataType.STRING(255)) public agentSystemId: string; @Column(DataType.DATE) diff --git a/packages/security.gbapp/services/SecService.ts b/packages/security.gbapp/services/SecService.ts index e63912a7..1cb0b84e 100644 --- a/packages/security.gbapp/services/SecService.ts +++ b/packages/security.gbapp/services/SecService.ts @@ -39,7 +39,8 @@ export class SecService extends GBService { userName: string, address: string, channelName: string, - displayName: string + displayName: string, + email: string ): Promise { let user = await GuaribasUser.findOne({ where: { @@ -55,7 +56,7 @@ export class SecService extends GBService { user.userSystemId = userSystemId; user.userName = userName; user.displayName = displayName; - user.email = userName; + user.email = email; user.defaultChannel = channelName; return await user.save(); @@ -82,6 +83,14 @@ export class SecService extends GBService { await user.save(); } + public async updateConversationReferenceById(userId: number, conversationReference: string) { + const options = { where: { userId: userId } }; + const user = await GuaribasUser.findOne(options); + + user.conversationReference = conversationReference; + await user.save(); + } + public async updateUserLocale(userId: number, locale: any): Promise { const user = await GuaribasUser.findOne({ where: { @@ -115,14 +124,18 @@ export class SecService extends GBService { return await user.save(); } - public async updateCurrentAgent( + /** + * Finds and update user agent information to a next available person. + */ + public async updateHumanAgent( userSystemId: string, instanceId: number, agentSystemId: string ): Promise { const user = await GuaribasUser.findOne({ where: { - userSystemId: userSystemId + userSystemId: userSystemId, + instanceId: instanceId } }); @@ -190,7 +203,7 @@ export class SecService extends GBService { } }); - await this.updateCurrentAgent(userSystemId, instanceId, agentSystemId); + await this.updateHumanAgent(userSystemId, instanceId, agentSystemId); return agentSystemId; } diff --git a/packages/whatsapp.gblib/services/WhatsappDirectLine.ts b/packages/whatsapp.gblib/services/WhatsappDirectLine.ts index fe2cca1b..e4f2b826 100644 --- a/packages/whatsapp.gblib/services/WhatsappDirectLine.ts +++ b/packages/whatsapp.gblib/services/WhatsappDirectLine.ts @@ -178,7 +178,7 @@ export class WhatsappDirectLine extends GBService { const sec = new SecService(); const user = await sec.ensureUser(this.min.instance.instanceId, id, - senderName, '', 'whatsapp', senderName); + senderName, '', 'whatsapp', senderName, null); const locale = user.locale ? user.locale : 'pt'; if (message.type === 'ptt') { @@ -205,12 +205,22 @@ export class WhatsappDirectLine extends GBService { const conversationId = WhatsappDirectLine.conversationIds[from]; const client = await this.directLineClient; - if (user.agentMode === 'self') { - const manualUser = await sec.getUserFromAgentSystemId(id); + + // Check if this message is from a Human Agent itself. + + if (user.agentMode === 'self') { + + // Check if there is someone being handled by this Human Agent. + + const manualUser = await sec.getUserFromAgentSystemId(id); if (manualUser === null) { - await sec.updateCurrentAgent(id, this.min.instance.instanceId, null); + + await sec.updateHumanAgent(id, this.min.instance.instanceId, null); + } else { + const agent = await sec.getUserFromSystemId(user.agentSystemId); + const cmd = '/reply '; if (text.startsWith(cmd)) { const filename = text.substr(cmd.length); @@ -218,37 +228,60 @@ export class WhatsappDirectLine extends GBService { if (message === null) { await this.sendToDeviceEx(user.userSystemId, `File ${filename} not found in any .gbkb published. Check the name or publish again the associated .gbkb.`, - locale); + locale); } else { await this.min.conversationalService.sendMarkdownToMobile(this.min, null, user.userSystemId, message); } } else if (text === '/qt') { // TODO: Transfers only in pt-br for now. await this.sendToDeviceEx(manualUser.userSystemId, - Messages[this.locale].notify_end_transfer(this.min.instance.botId), locale); - await this.sendToDeviceEx(user.agentSystemId, - Messages[this.locale].notify_end_transfer(this.min.instance.botId), locale); + Messages[this.locale].notify_end_transfer(this.min.instance.botId), locale); - await sec.updateCurrentAgent(manualUser.userSystemId, this.min.instance.instanceId, null); + if (user.agentSystemId.charAt(2) === ":") { // Agent is from Teams. + await this.min.conversationalService['sendOnConversation'](this.min, agent, Messages[this.locale].notify_end_transfer(this.min.instance.botId)); + } + else { + await this.sendToDeviceEx(user.agentSystemId, + Messages[this.locale].notify_end_transfer(this.min.instance.botId), locale); + + } + await sec.updateHumanAgent(manualUser.userSystemId, this.min.instance.instanceId, null); + await sec.updateHumanAgent(user.agentSystemId, this.min.instance.instanceId, null); } else { GBLog.info(`HUMAN AGENT (${id}) TO USER ${manualUser.userSystemId}: ${text}`); - this.sendToDeviceEx(manualUser.userSystemId, `${manualUser.agentSystemId}: ${text}`, locale); + await this.sendToDeviceEx(manualUser.userSystemId, `${manualUser.agentSystemId}: ${text}`, locale); } } + + } else if (user.agentMode === 'human') { + const agent = await sec.getUserFromSystemId(user.agentSystemId); if (text === '/t') { await this.sendToDeviceEx(user.userSystemId, `Você já está sendo atendido por ${agent.userSystemId}.`, locale); } else if (text === '/qt' || text === 'Sair' || text === 'Fechar') { // TODO: Transfers only in pt-br for now. await this.sendToDeviceEx(id, - Messages[this.locale].notify_end_transfer(this.min.instance.botId), locale); - await this.sendToDeviceEx(user.agentSystemId, Messages[this.locale].notify_end_transfer(this.min.instance.botId), locale); + Messages[this.locale].notify_end_transfer(this.min.instance.botId), locale); - await sec.updateCurrentAgent(id, this.min.instance.instanceId, null); + if (user.agentSystemId.charAt(2) === ":") { // Agent is from Teams. + await this.min.conversationalService['sendOnConversation'](this.min, agent, Messages[this.locale].notify_end_transfer(this.min.instance.botId)); + } + else { + await this.sendToDeviceEx(user.agentSystemId, Messages[this.locale].notify_end_transfer(this.min.instance.botId), locale); + } + + await sec.updateHumanAgent(id, this.min.instance.instanceId, null); } else { - GBLog.info(`USER (${id}) TO AGENT ${user.userSystemId}: ${text}`); - this.sendToDeviceEx(user.agentSystemId, `Bot: ${this.min.instance.botId}\n${id}: ${text}`, locale); + GBLog.info(`USER (${id}) TO AGENT ${agent.userSystemId}: ${text}`); + + if (user.agentSystemId.charAt(2) === ":") { // Agent is from Teams. + await this.min.conversationalService['sendOnConversation'](this.min, agent, text); + } + else { + await this.sendToDeviceEx(user.agentSystemId, `Bot: ${this.min.instance.botId}\n${id}: ${text}`, locale); + } + } } else if (user.agentMode === 'bot' || user.agentMode === null || user.agentMode === undefined) { @@ -451,7 +484,7 @@ export class WhatsappDirectLine extends GBService { } } - private async sendToDeviceEx(to, text, locale) { + public async sendToDeviceEx(to, text, locale) { const minBoot = GBServer.globals.minBoot as any; text = await minBoot.conversationalService.translate(