This commit is contained in:
Leifer Mendez
2022-08-15 13:04:36 +02:00
parent 97226d78e0
commit 47f1ddfbb0
32 changed files with 2465 additions and 62 deletions

View File

@@ -2,4 +2,5 @@ TWILIO_SID=
TWILIO_TOKEN=
TWILIO_FROM=
META_ID_NUMBER=
META_TOKEN=
META_TOKEN=
GREET_GAP_MIN=5

3
.gitignore vendored
View File

@@ -3,4 +3,5 @@ node_modules
dist
tmp/*
!tmp/.gitkeep
.wwebjs_auth
.wwebjs_auth
.tmp

View File

@@ -0,0 +1,33 @@
import DbRepository from "../../../src/bot/domain/repositories/db.repository";
export default class DbRepositoryMock implements DbRepository {
getAnswer(): Promise<string> {
return Promise.resolve("Hola soy la respuesta que esperas");
}
findLastContact(phone: string): Promise<number> {
return Promise.resolve(1);
}
findLastMsg(
phone: string
): Promise<{ msg: string; step: string; contextId: string }> {
return Promise.resolve({
msg: "Hola de nuevo!",
step: "fisrt",
contextId: "0000000000000",
});
}
findGreetMessage(): Promise<string> {
return Promise.resolve("Hola y bienvenido soy el saludo");
}
saveRecord({
msg,
phone,
contextId,
}: {
msg: string;
phone: string;
contextId: string;
}): Promise<boolean> {
return Promise.resolve(true);
}
}

View File

@@ -0,0 +1,8 @@
import DialogRepository from "../../../src/bot/domain/repositories/dialog.repository";
export default class DialogRepositoryMock implements DialogRepository{
getAnswer({ msg, contextId }: any): Promise<string> {
return Promise.resolve('Quieres helado')
}
}

View File

@@ -0,0 +1,13 @@
import { Message } from "../../../src/bot/domain/message";
import WhatsappRepository from "../../../src/bot/domain/repositories/whatsapp.repository";
const MESSAGE_MOCK = new Message({ msg: "Hola msg!", phone: "777777777" });
export default class WhatsappRepositoryMock implements WhatsappRepository {
sendMsg({ msg, phone, contextId }: any): Promise<Message | null | undefined> {
return Promise.resolve(MESSAGE_MOCK);
}
onMsg({ msg, phone }: any): Promise<string | null | undefined> {
return Promise.resolve('MESSAGE_MOCK');
}
}

View File

@@ -0,0 +1,43 @@
import Greeting from "../../../src/bot/application/greeting";
import DbRepository from "../../../src/bot/domain/repositories/db.repository";
import DialogRepository from "../../../src/bot/domain/repositories/dialog.repository";
import WhatsappRepository from "../../../src/bot/domain/repositories/whatsapp.repository";
import WhatsBus from "../../../src/bot/infrastructure/events/whatsapp.events";
import DbRepositoryMock from "../__mocks__/db.repositoryMock";
import DialogRepositoryMock from "../__mocks__/dialog.repositoryMock";
import WhatsappRepositoryMock from "../__mocks__/whatsapp.repositoryMock";
let whatsappRepository: WhatsappRepository;
let dbRepository: DbRepository;
let dialogRepository: DialogRepository;
let greeting: Greeting;
let whatsBus: WhatsBus;
beforeEach(() => {
whatsappRepository = new WhatsappRepositoryMock();
dbRepository = new DbRepositoryMock();
dialogRepository = new DialogRepositoryMock();
whatsBus = new WhatsBus();
greeting = new Greeting([
whatsappRepository,
dbRepository,
dialogRepository,
whatsBus,
]);
});
describe(`Test de Greeting`, () => {
test(`shoud received a object`, async () => {
const { recordSave } = (await greeting.firstGreet(
"8888888888888888888"
)) as any;
expect(recordSave).toEqual(true);
});
test(`continue conversation "Quieres helado"`, async () => {
const { msgTosend } = await greeting.continueConversation(
"888888888888888"
);
expect(msgTosend.msg).toEqual("Quieres helado");
});
});

6
jest.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
cacheDirectory: '.tmp/jestCache'
};

View File

@@ -6,7 +6,8 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev:http": "nodemon ./src/http/index.ts",
"dev:bot": "nodemon ./src/http/index.ts",
"dev:bot": "nodemon ./src/bot/index.ts",
"dev:all": "nodemon ./src/app.ts",
"start": "node ./dist/app.js",
"build": "tsc -p ."
},
@@ -17,21 +18,28 @@
"axios": "^0.27.2",
"cors": "^2.8.5",
"dotenv": "^16.0.1",
"eventemitter2": "^6.4.7",
"express": "^4.18.1",
"jest": "^28.1.3",
"node-dependency-injection": "^3.0.3",
"qr-image": "^3.2.0",
"rxjs": "^7.5.6",
"twilio": "^3.80.1",
"uuid": "^8.3.2",
"whatsapp-web.js": "^1.17.1"
},
"devDependencies": {
"@babel/preset-typescript": "^7.18.6",
"@types/axios": "^0.14.0",
"@types/cors": "^2.8.12",
"@types/dotenv": "^8.2.0",
"@types/eventemitter2": "^4.1.0",
"@types/express": "^4.17.13",
"@types/jest": "^28.1.6",
"@types/qr-image": "^3.2.5",
"@types/twilio": "^3.19.3",
"@types/uuid": "^8.3.4",
"ts-jest": "^28.0.7",
"typescript": "^4.7.4"
}
}

1994
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
import Http from "./http";
import Bot from "./bot"
const http = new Http()
http.startServer()
Bot.startBot()
// Http.startServer()

View File

@@ -0,0 +1,88 @@
import "dotenv/config";
import { Message } from "../domain/message";
import DbRepository from "../domain/repositories/db.repository";
import DialogRepository from "../domain/repositories/dialog.repository";
import WhatsappRepository from "../domain/repositories/whatsapp.repository";
import WhatsBus from "../infrastructure/events/whatsapp.events";
const GREET_GAP_MIN = process.env.GREET_GAP_MIN || 2;
/**
* Saludar a contacto que escribe
*/
export default class Greeting {
private whatsappRepository: WhatsappRepository;
private dbRepository: DbRepository;
private dialogRepository: DialogRepository;
private whatsBus: WhatsBus;
constructor(
dependencies: [WhatsappRepository, DbRepository, DialogRepository, WhatsBus]
) {
const [whatsappRepository, dbRepository, dialogRepository, whatsBus] =
dependencies;
this.whatsappRepository = whatsappRepository;
this.dbRepository = dbRepository;
this.dialogRepository = dialogRepository;
this.whatsBus = whatsBus;
this.whatsBus.events$.subscribe(({ event, data }) => {
if (event === "message") {
console.log(data.body);
}
});
}
/**
* Enviar saludo
* @param phone
*/
public async checkIsGreet(phone: string, msg: string) {
const minsAgo = await this.dbRepository.findLastContact(phone);
const saveMsg = new Message({ msg, phone });
await this.dbRepository.saveRecord(saveMsg);
/**
* Continuamos conversacion o inicamos de nuevo
*/
if (!minsAgo || minsAgo > GREET_GAP_MIN) {
const { msgTosend, recordSave } = await this.firstGreet(phone);
const whatsappMsg = await this.whatsappRepository.sendMsg(msgTosend);
return { msgTosend, recordSave, whatsappMsg };
} else {
const { msgTosend, recordSave } = await this.continueConversation(phone);
const whatsappMsg = await this.whatsappRepository.sendMsg(msgTosend);
return { msgTosend, recordSave, whatsappMsg };
}
}
/**
*
* @param phone
* @param contextId
*/
public async continueConversation(
phone: string
): Promise<{ msgTosend: Message; recordSave: any }> {
const { msg, contextId } = await this.dbRepository.findLastMsg(phone);
const nextMsg = await this.dialogRepository.getAnswer({
msg,
contextId,
});
const msgTosend = new Message({ msg: nextMsg, phone });
const recordSave = await this.dbRepository.saveRecord(msgTosend);
return { msgTosend, recordSave };
}
/**
* Enviamos el mensaje de saludo
* @param phone
*/
public async firstGreet(
phone: string
): Promise<{ msgTosend: Message; recordSave: any }> {
const nextMsg = await this.dbRepository.findGreetMessage();
const msgTosend = new Message({ msg: nextMsg, phone });
const recordSave = await this.dbRepository.saveRecord(msgTosend);
return { msgTosend, recordSave };
}
}

View File

15
src/bot/domain/message.ts Normal file
View File

@@ -0,0 +1,15 @@
import {v4 as uuid} from "uuid"
export class Message {
readonly uuid: string;
readonly msg: string;
readonly phone: string;
readonly contextId:string
constructor({ msg, phone }: { msg: string; phone: string }) {
this.uuid = uuid()
this.contextId = `context_${uuid()}`
this.msg = msg
this.phone = phone
}
}

View File

@@ -0,0 +1,7 @@
export default interface DbRepository {
getAnswer(): Promise<string>;
findLastContact(phone: string): Promise<number>;
findLastMsg(phone: string): Promise<{ msg: string; step:string, contextId: string }>;
findGreetMessage(): Promise<string>;
saveRecord({msg, phone, contextId}:{msg:string, phone:string, contextId:string}):Promise<boolean>
}

View File

@@ -0,0 +1,8 @@
export default interface DialogRepository {
getAnswer({ msg, contextId }: MsgInput): Promise<string>;
}
interface MsgInput {
msg: string;
contextId?: string;
}

View File

@@ -0,0 +1,4 @@
export default interface EventRepository {
onMsg(payload: any): Event;
on(event: string, payload: any): any;
}

View File

@@ -0,0 +1,17 @@
import { Message } from "../message";
export default interface WhatsappRepository {
sendMsg({
msg,
phone,
contextId,
}: MsgInput): Promise<Message | null | undefined>;
onMsg({msg, phone}:{msg:string, phone:string}): Promise<string | null | undefined>;
}
interface MsgInput {
msg: string;
phone: string;
contextId: string;
}

26
src/bot/index.ts Normal file
View File

@@ -0,0 +1,26 @@
import Greeting from "./application/greeting";
import container from "../ioc";
import WebWhatsappRepository from "./infrastructure/repositories/web-whatsapp.repository";
import parseNumber from "./infrastructure/utils/parse.number";
const greetingCase = container.get("greeting.case");
const whatsProvider: WebWhatsappRepository = container.get("ws.provider");
class BotWs {
constructor(
private whatsProvider: WebWhatsappRepository,
private greetingCase: Greeting
) {
this.whatsProvider.on("message", ({ body, from }) => {
const phone = parseNumber(from);
this.greetingCase.checkIsGreet(phone, body);
});
}
public startBot() {
this.whatsProvider.initialize();
}
}
const bot = new BotWs(whatsProvider, greetingCase);
export default bot;

View File

@@ -0,0 +1,10 @@
import { BehaviorSubject } from "rxjs";
export default class WhatsBus {
private _events = new BehaviorSubject<any>({event:null, data:null});
public events$ = this._events.asObservable();
public sendEvent(event: string, data: any) {
this._events.next({ event, data });
}
}

View File

@@ -0,0 +1,8 @@
import DialogRepository from "../../domain/repositories/dialog.repository";
export default class DialogFlowRepository implements DialogRepository{
getAnswer({ msg, contextId }: any): Promise<string> {
return Promise.resolve('Quieres helado')
}
}

View File

@@ -0,0 +1,9 @@
import EventEmitter2, { event, Listener, ListenerFn, OnOptions } from "eventemitter2";
import EventRepository from "../../domain/repositories/events.repository";
export default class Event2 extends EventEmitter2 implements EventRepository {
onMsg(payload: any): Event {
throw new Error("Method not implemented.");
}
}

View File

@@ -0,0 +1,22 @@
import DbRepository from "../../domain/repositories/db.repository";
class MySqlRepository implements DbRepository {
getAnswer(): Promise<string> {
return Promise.resolve('Hola y bienvenido!')
}
findLastContact(phone: string): Promise<number> {
return Promise.resolve(0)
}
findLastMsg(phone: string): Promise<{ msg: string; step: string; contextId: string; }> {
return Promise.resolve({msg:'Como estas', step:'1', contextId:'00000000'})
}
findGreetMessage(): Promise<string> {
return Promise.resolve('Bienvenido!');
}
saveRecord({ msg, phone, contextId }: { msg: string; phone: string; contextId: string; }): Promise<boolean> {
return Promise.resolve(true)
}
}
export default MySqlRepository

View File

@@ -0,0 +1,62 @@
import { Client, LocalAuth } from "whatsapp-web.js";
import WhatsappRepository from "../../domain/repositories/whatsapp.repository";
/**
* Extendemos los super poderes de whatsapp-web
*/
class WebWhatsappRepository extends Client implements WhatsappRepository {
private status = false;
constructor() {
super({
authStrategy: new LocalAuth(),
puppeteer: { headless: true },
});
this.on('ready',() => {
this.status = true
console.log('LOGIN_READY')
})
this.on('auth_failure',() => {
this.status = false
console.log('LOGIN_FAIL')
})
}
onMsg({
msg,
phone,
}: {
msg: string;
phone: string;
}): Promise<string | null | undefined> {
throw new Error("Method not implemented.");
}
/**
* Enviar mensaje de WS
* @param lead
* @returns
*/
async sendMsg({ msg, phone }: { msg: string; phone: string }): Promise<any> {
try {
console.log('WS:Enviar', msg, phone)
if (!this.status) return Promise.resolve({ error: "WAIT_LOGIN" });
const response = await this.sendMessage(`${phone}@c.us`, msg);
return { id: response.id.id };
} catch (e: any) {
return Promise.resolve({ error: e.message });
}
}
getStatus(): boolean {
return this.status;
}
}
export default WebWhatsappRepository;

View File

@@ -0,0 +1,5 @@
const parseBody = () => {
}
export default parseBody

View File

@@ -0,0 +1,6 @@
const parseNumber = (phone: string) => {
const parseFrom = phone.split("@").shift() || '';
return parseFrom;
};
export default parseNumber;

View File

@@ -0,0 +1,11 @@
import { image as imageQr } from "qr-image";
const generateImage = (base64: string) => {
const path = `${process.cwd()}/tmp`;
let qr_svg = imageQr(base64, { type: "svg", margin: 4 });
qr_svg.pipe(require("fs").createWriteStream(`${path}/qr.svg`));
console.log(`⚡ Recuerda que el QR se actualiza cada minuto ⚡'`);
console.log(`⚡ Actualiza F5 el navegador para mantener el mejor QR⚡`);
};
export default generateImage;

View File

@@ -10,14 +10,19 @@ export class LeadCreate {
this.leadExternal = leadExternal;
}
/**
* Se debe revisar para separar
* @param param
* @returns
*/
public async sendMessageAndSave({
message,
msg,
phone,
}: {
message: string;
msg: string;
phone: string;
}) {
const responseDbSave = await this.leadRepository.save({ message, phone });//TODO DB
const responseDbSave = await this.leadRepository.saveRecord({ msg, phone });//TODO DB
const responseExSave = await this.leadExternal.sendMsg({ message, phone });//TODO enviar a ws
return {responseDbSave, responseExSave};
}

View File

@@ -1,16 +1,3 @@
import { Lead } from "./lead";
/**
* Esta la interfaz que debe de cumplir el repositorio de infraestructura
* mysql o mongo o etc
*/
export default interface LeadRepository {
save({
message,
phone,
}: {
message: string;
phone: string;
}): Promise<Lead | undefined | null>;
getDetail(id:string):Promise<Lead | null | undefined>
export default interface DbRepository {
saveRecord({msg, phone, contextId}:{msg:string, phone:string, contextId:string}):Promise<boolean>
}

View File

@@ -8,11 +8,14 @@ app.use(cors());
app.use(express.json());
app.use(`/`, routes);
export default class Http {
class Http {
constructor() {}
public startServer() {
const server = app.listen(port, () => console.log(`Ready...${port}`));
return server
return server;
}
}
const http = new Http();
export default http;

View File

@@ -1,28 +0,0 @@
import { ContainerBuilder } from "node-dependency-injection";
import { LeadCreate } from "../application/lead.create";
import LeadCtrl from "./controller/lead.ctrl";
import MetaRepository from "./repositories/meta.repository";
import MockRepository from "./repositories/mock.repository";
import TwilioService from "./repositories/twilio.repository";
import WsTransporter from "./repositories/ws.external";
const container = new ContainerBuilder();
/**
* Inicamos servicio de WS / Bot / Twilio
*/
container.register("ws.transporter", MetaRepository);
const wsTransporter = container.get("ws.transporter");
container.register("db.repository", MockRepository);
const dbRepository = container.get("db.repository");
container
.register("lead.creator", LeadCreate)
.addArgument([dbRepository, wsTransporter]);
const leadCreator = container.get("lead.creator");
container.register("lead.ctrl", LeadCtrl).addArgument(leadCreator);
export default container;

View File

@@ -1,6 +1,7 @@
import express, { Router } from "express";
import container from "../../../ioc";
import LeadCtrl from "../controller/lead.ctrl";
import container from "../ioc";
const router: Router = Router();
/**

45
src/ioc.ts Normal file
View File

@@ -0,0 +1,45 @@
import { ContainerBuilder } from "node-dependency-injection";
import Greeting from "./bot/application/greeting";
import WhatsBus from "./bot/infrastructure/events/whatsapp.events";
import DialogFlowRepository from "./bot/infrastructure/repositories/dialog-flow.repository";
import MySqlRepository from "./bot/infrastructure/repositories/mysql.repository";
import WebWhatsappRepository from "./bot/infrastructure/repositories/web-whatsapp.repository";
import { LeadCreate } from "./http/application/lead.create";
import LeadCtrl from "./http/infrastructure/controller/lead.ctrl";
const container = new ContainerBuilder();
/**
* BOT
*/
container.register("events.bus", WhatsBus);
const eventBus = container.get("events.bus");
container.register("ws.provider", WebWhatsappRepository);
const wsProvider = container.get("ws.provider");
container.register("db.provider", MySqlRepository);
const dbProvider = container.get("db.provider");
container.register("dialog.provider", DialogFlowRepository);
const dialogProvider = container.get("dialog.provider");
container
.register("greeting.case", Greeting)
.addArgument([wsProvider, dbProvider, dialogProvider, eventBus]);
/**
* HTTP
*/
container
.register("lead.creator", LeadCreate)
.addArgument([dbProvider, wsProvider]);
const leadCreator = container.get("lead.creator");
container.register("lead.ctrl", LeadCtrl).addArgument(leadCreator);
export default container;