diff --git a/server/chat-bot-state/chat-bot-state.module.ts b/server/chat-bot-state/chat-bot-state.module.ts new file mode 100644 index 000000000..4240fcd0f --- /dev/null +++ b/server/chat-bot-state/chat-bot-state.module.ts @@ -0,0 +1,18 @@ +import { Module } from "@nestjs/common"; +import { MongooseModule } from "@nestjs/mongoose"; +import { ChatBotState, ChatBotStateSchema } from "./chat-bot-state.schema"; +import { ChatBotStateService } from "./chat-bot-state.service"; + +const ChatBotStateModel = MongooseModule.forFeature([ + { + name: ChatBotState.name, + schema: ChatBotStateSchema, + }, +]); + +@Module({ + imports: [ChatBotStateModel], + exports: [ChatBotStateService], + providers: [ChatBotStateService], +}) +export class ChatBotStateModule {} diff --git a/server/chat-bot-state/chat-bot-state.schema.ts b/server/chat-bot-state/chat-bot-state.schema.ts new file mode 100644 index 000000000..c324142b4 --- /dev/null +++ b/server/chat-bot-state/chat-bot-state.schema.ts @@ -0,0 +1,17 @@ +import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; +import * as mongoose from "mongoose"; + +export type ChatBotStateDocument = ChatBotState & mongoose.Document; + +@Schema() +export class ChatBotState { + @Prop({ required: true }) + _id: string; + + @Prop({ required: true, type: mongoose.Schema.Types.Mixed }) + state: any; +} + +const ChatBotStateSchemaRaw = SchemaFactory.createForClass(ChatBotState); + +export const ChatBotStateSchema = ChatBotStateSchemaRaw; diff --git a/server/chat-bot-state/chat-bot-state.service.ts b/server/chat-bot-state/chat-bot-state.service.ts new file mode 100644 index 000000000..a7bf0f9b6 --- /dev/null +++ b/server/chat-bot-state/chat-bot-state.service.ts @@ -0,0 +1,36 @@ +import { Injectable } from "@nestjs/common"; +import { InjectModel } from "@nestjs/mongoose"; +import { Model } from "mongoose"; +import { ChatBotState, ChatBotStateDocument } from "./chat-bot-state.schema"; + +@Injectable() +export class ChatBotStateService { + constructor( + @InjectModel(ChatBotState.name) + private ChatBotStateModel: Model + ) {} + + async create(state, id: string) { + const newChatBotState = new this.ChatBotStateModel({ + _id: id, + state: state, + }); + return await newChatBotState.save(); + } + + async updateState( + id: string, + state: any + ): Promise { + return await this.ChatBotStateModel.findByIdAndUpdate( + id, + { state: state }, + { new: true, useFindAndModify: false } + ).exec(); + } + + async getById(id: string) { + const chatBotState = this.ChatBotStateModel.findById(id); + return chatBotState; + } +} diff --git a/server/chat-bot/chat-bot-actions.ts b/server/chat-bot/chat-bot-actions.ts index 53b5e5844..a4957ddd4 100644 --- a/server/chat-bot/chat-bot-actions.ts +++ b/server/chat-bot/chat-bot-actions.ts @@ -14,7 +14,7 @@ const MESSAGES = { "Desculpe, não entendi sua resposta. Para continuar, preciso que você digite SIM se deseja fazer uma denúncia, ou NÃO se não deseja.\n\nVocê gostaria de fazer uma denúncia agora?", askForVerificationRequest: "Por favor, me conte com detalhes o que você gostaria de denunciar.\n\nPor favor, inclua todas as informações que considerar relevantes para que possamos verificar a denúncia de forma eficiente 👀", - thanks: "Muito obrigada por sua contribuição!\n\nSua informação será analisada pela nossa equipe ✅Para saber mais, visite nosso site: https://aletheiafact.org.\n\nDeseja relatar outra denúncia? Responda SIM para continuar ou NÃO para encerrar.", + thanks: "Muito obrigada por sua contribuição!\n\nSua informação será analisada pela nossa equipe ✅Para saber mais, visite nosso site: https://aletheiafact.org.\n\nDeseja relatar outra denúncia? Responda SIM para continuar.", noTextMessageGreeting: "Desculpe, só podemos processar mensagens de texto. Por favor, envie sua mensagem em formato de texto.\n\nOlá! Sou o assistente virtual da AletheiaFact.org, estou aqui para ajudá-lo(a) a combater desinformações 🙂 Você gostaria de fazer uma denúncia agora?\n\nResponda SIM para continuar ou NÃO se não deseja denunciar.", noTextMessageAskForVerificationRequest: diff --git a/server/chat-bot/chat-bot.machine.ts b/server/chat-bot/chat-bot.machine.ts index d9e921597..55ff88489 100644 --- a/server/chat-bot/chat-bot.machine.ts +++ b/server/chat-bot/chat-bot.machine.ts @@ -66,7 +66,7 @@ export const createChatBotMachine = ( askingForVerificationRequest: { on: { RECEIVE_REPORT: { - target: "askingIfVerificationRequest", + target: "finishedReport", actions: [ "saveVerificationRequest", "sendThanks", @@ -83,6 +83,21 @@ export const createChatBotMachine = ( }, }, }, + finishedReport: { + on: { + RECEIVE_YES: { + target: "askingForVerificationRequest", + actions: [ + "askForVerificationRequest", + "setResponseMessage", + ], + }, + ANY_TEXT_MESSAGE: { + target: "askingIfVerificationRequest", + actions: ["sendGreeting", "setResponseMessage"], + }, + }, + }, sendingNoMessage: { on: { ASK_TO_REPORT: { diff --git a/server/chat-bot/chat-bot.module.ts b/server/chat-bot/chat-bot.module.ts index 95487d163..6d2423592 100644 --- a/server/chat-bot/chat-bot.module.ts +++ b/server/chat-bot/chat-bot.module.ts @@ -10,9 +10,15 @@ import { HttpModule } from "@nestjs/axios"; import { VerificationRequestModule } from "../verification-request/verification-request.module"; import { ConfigModule } from "@nestjs/config"; import { AuthZenviaWebHookMiddleware } from "../middleware/auth-zenvia-webhook.middleware"; +import { ChatBotStateModule } from "../chat-bot-state/chat-bot-state.module"; @Module({ - imports: [HttpModule, VerificationRequestModule, ConfigModule], + imports: [ + HttpModule, + VerificationRequestModule, + ConfigModule, + ChatBotStateModule, + ], providers: [ChatbotService], controllers: [ChatbotController], }) diff --git a/server/chat-bot/chat-bot.service.ts b/server/chat-bot/chat-bot.service.ts index aaf8b9bed..6ad57b13d 100644 --- a/server/chat-bot/chat-bot.service.ts +++ b/server/chat-bot/chat-bot.service.ts @@ -6,6 +6,7 @@ import { Observable, throwError } from "rxjs"; import { createChatBotMachine } from "./chat-bot.machine"; import { VerificationRequestService } from "../verification-request/verification-request.service"; import { ConfigService } from "@nestjs/config"; +import { ChatBotStateService } from "../chat-bot-state/chat-bot-state.service"; const diacriticsRegex = /[\u0300-\u036f]/g; const MESSAGE_MAP = { @@ -15,98 +16,149 @@ const MESSAGE_MAP = { @Injectable() export class ChatbotService { - private chatBotMachineService; - constructor( private configService: ConfigService, private readonly httpService: HttpService, - private verificationService: VerificationRequestService + private verificationService: VerificationRequestService, + private chatBotStateService: ChatBotStateService ) {} - onModuleInit() { - this.initializeChatBotMachine(); - } + private async getOrCreateChatBotMachine(from: string, channel: string) { + const id = `${channel}-${from}`; + let chatbotState = await this.chatBotStateService.getById(id); - private initializeChatBotMachine() { - this.chatBotMachineService = createChatBotMachine( - this.verificationService - ); - this.chatBotMachineService.start(); - } + if (!chatbotState) { + const newMachine = createChatBotMachine(this.verificationService); + newMachine.start(); + chatbotState = await this.chatBotStateService.create( + newMachine.getSnapshot().value, + id + ); + } else { + const rehydratedMachine = createChatBotMachine( + this.verificationService + ); + rehydratedMachine.start(chatbotState.state); + chatbotState.state = rehydratedMachine.getSnapshot(); + } - //TODO: Find a better way to interpret the user's message. - private normalizeAndLowercase(message: string): string { - return message - .normalize("NFD") - .replace(diacriticsRegex, "") - .toLowerCase(); + return chatbotState; } - private handleMachineEventSend(parsedMessage: string): void { - this.chatBotMachineService.send( - MESSAGE_MAP[parsedMessage] || "NOT_UNDERSTOOD" + private async updateChatBotState(chatbotState) { + await this.chatBotStateService.updateState( + chatbotState._id, + chatbotState.state ); } - private handleSendingNoMessage(parsedMessage: string): void { - this.chatBotMachineService.send( - parsedMessage === "denuncia" ? "ASK_TO_REPORT" : "RECEIVE_NO" + public async sendMessage(message): Promise>> { + const { api_url, api_token } = this.configService.get("zenvia"); + const { from, to, channel, contents } = message; + + const chatbotState = await this.getOrCreateChatBotMachine( + from, + channel ); - } + const chatBotMachineService = createChatBotMachine( + this.verificationService + ); + chatBotMachineService.start(chatbotState.state); - private handleUserMessage(message): void { - const messageType = message.contents[0].type; - const userMessage = message.contents[0].text; + const userMessage = contents[0].text; + this.handleUserMessage(userMessage, chatBotMachineService); - if (messageType !== "text") { - this.chatBotMachineService.send("NON_TEXT_MESSAGE"); - return; - } + const snapshot = chatBotMachineService.getSnapshot(); + chatbotState.state = snapshot.value; - const parsedMessage = this.normalizeAndLowercase(userMessage); - const currentState = this.chatBotMachineService.getSnapshot().value; + await this.updateChatBotState(chatbotState); + + const responseMessage = snapshot.context.responseMessage; + + const body = { + from: to, + to: from, + contents: [{ type: "text", text: responseMessage }], + }; + + return this.httpService + .post(api_url, body, { + headers: { "X-API-TOKEN": api_token }, + }) + .pipe( + map((response) => response), + catchError((error) => throwError(() => new Error(error))) + ); + } + + private handleUserMessage(message: string, chatBotMachineService) { + const parsedMessage = this.normalizeAndLowercase(message); + const currentState = chatBotMachineService.getSnapshot().value; switch (currentState) { case "greeting": - this.chatBotMachineService.send("ASK_IF_VERIFICATION_REQUEST"); + chatBotMachineService.send("ASK_IF_VERIFICATION_REQUEST"); break; case "askingIfVerificationRequest": - this.handleMachineEventSend(parsedMessage); + this.handleMachineEventSend( + parsedMessage, + chatBotMachineService + ); break; case "askingForVerificationRequest": - this.chatBotMachineService.send({ + chatBotMachineService.send({ type: "RECEIVE_REPORT", - verificationRequest: userMessage, + verificationRequest: message, }); break; case "sendingNoMessage": - this.handleSendingNoMessage(parsedMessage); + this.handleSendingNoMessage( + parsedMessage, + chatBotMachineService + ); + break; + case "finishedReport": + this.handleMachineFinishEventSend( + parsedMessage, + chatBotMachineService + ); break; default: console.warn(`Unhandled state: ${currentState}`); } } - public sendMessage(message): Observable> { - const { api_url, api_token } = this.configService.get("zenvia"); - this.handleUserMessage(message); + private normalizeAndLowercase(message: string): string { + return message + .normalize("NFD") + .replace(diacriticsRegex, "") + .toLowerCase(); + } - const snapshot = this.chatBotMachineService.getSnapshot(); - const responseMessage = snapshot.context.responseMessage; + private handleMachineEventSend( + parsedMessage: string, + chatBotMachineService + ): void { + chatBotMachineService.send( + MESSAGE_MAP[parsedMessage] || "NOT_UNDERSTOOD" + ); + } - const body = { - from: message.to, - to: message.from, - contents: [{ type: "text", text: responseMessage }], - }; + private handleMachineFinishEventSend( + parsedMessage: string, + chatBotMachineService + ): void { + chatBotMachineService.send( + parsedMessage === "sim" ? "RECEIVE_YES" : "ANY_TEXT_MESSAGE" + ); + } - return this.httpService - .post(api_url, body, { - headers: { "X-API-TOKEN": api_token }, - }) - .pipe( - map((response) => response), - catchError((error) => throwError(() => new Error(error))) - ); + private handleSendingNoMessage( + parsedMessage: string, + chatBotMachineService + ): void { + chatBotMachineService.send( + parsedMessage === "denuncia" ? "ASK_TO_REPORT" : "RECEIVE_NO" + ); } }