diff --git a/__test__/0.1.6-case.test.js b/__test__/0.1.6-case.test.js index 3473aab80..72f5f4c40 100644 --- a/__test__/0.1.6-case.test.js +++ b/__test__/0.1.6-case.test.js @@ -9,7 +9,6 @@ suiteCase.before.each(setup) suiteCase.after.each(clear) suiteCase(`Debe retornar un mensaje resumen`, async ({ database, provider }) => { - let STATE_APP = {} const MOCK_VALUES = ['¿Cual es tu nombre?', '¿Cual es tu edad?', 'Tu datos son:'] const flujoPrincipal = addKeyword(['hola']) @@ -18,9 +17,8 @@ suiteCase(`Debe retornar un mensaje resumen`, async ({ database, provider }) => { capture: true, }, - async (ctx, { flowDynamic }) => { - STATE_APP[ctx.from] = { ...STATE_APP[ctx.from], name: ctx.body } - + async (ctx, { flowDynamic, state }) => { + state.update({ name: ctx.body }) flowDynamic('Gracias por tu nombre!') } ) @@ -29,14 +27,15 @@ suiteCase(`Debe retornar un mensaje resumen`, async ({ database, provider }) => { capture: true, }, - async (ctx, { flowDynamic }) => { - STATE_APP[ctx.from] = { ...STATE_APP[ctx.from], age: ctx.body } - - await flowDynamic(`Gracias por tu edad! ${STATE_APP[ctx.from].name}`) + async (ctx, { flowDynamic, state }) => { + state.update({ age: ctx.body }) + const myState = state.getMyState() + await flowDynamic(`Gracias por tu edad! ${myState.name}`) } ) - .addAnswer(MOCK_VALUES[2], null, async (ctx, { flowDynamic }) => { - flowDynamic(`Nombre: ${STATE_APP[ctx.from].name} Edad: ${STATE_APP[ctx.from].age}`) + .addAnswer(MOCK_VALUES[2], null, async (_, { flowDynamic, state }) => { + const myState = state.getMyState() + flowDynamic(`Nombre: ${myState.name} Edad: ${myState.age}`) }) .addAnswer('🤖🤖 Gracias por tu participacion') diff --git a/package.json b/package.json index 61cb64121..91998bc6c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bot-whatsapp/root", - "version": "0.1.28", + "version": "0.1.29", "description": "Bot de wahtsapp open source para MVP o pequeños negocios", "main": "app.js", "private": true, diff --git a/packages/bot/context/state.class.js b/packages/bot/context/state.class.js new file mode 100644 index 000000000..2b1cdad0f --- /dev/null +++ b/packages/bot/context/state.class.js @@ -0,0 +1,39 @@ +class GlobalState { + STATE = new Map() + constructor() {} + + /** + * + * @param {*} ctx + * @returns + */ + updateState = (ctx = {}) => { + const currentStateByFrom = this.STATE.get(ctx.from) + return (keyValue) => this.STATE.set(ctx.from, { ...currentStateByFrom, ...keyValue }) + } + + /** + * + * @returns + */ + getMyState = (from) => { + return () => this.STATE.get(from) + } + + /** + * + * @returns + */ + getAllState = () => this.STATE.values() + + /** + * + * @param {*} from + * @returns + */ + clear = (from) => { + return () => this.STATE.delete(from) + } +} + +module.exports = GlobalState diff --git a/packages/bot/core/core.class.js b/packages/bot/core/core.class.js index 953ba2e6d..4b2a76582 100644 --- a/packages/bot/core/core.class.js +++ b/packages/bot/core/core.class.js @@ -5,12 +5,14 @@ const Queue = require('../utils/queue') const { Console } = require('console') const { createWriteStream } = require('fs') const { LIST_REGEX } = require('../io/events') +const GlobalState = require('../context/state.class') const logger = new Console({ stdout: createWriteStream(`${process.cwd()}/core.class.log`), }) const QueuePrincipal = new Queue() +const StateHandler = new GlobalState() /** * [ ] Escuchar eventos del provider asegurarte que los provider emitan eventos @@ -91,6 +93,14 @@ class CoreClass { await this.databaseClass.save(ctxByNumber) } + // 📄 Mantener estado de conversacion por numero + const state = { + getMyState: StateHandler.getMyState(messageCtxInComming.from), + getAllState: StateHandler.getAllState, + update: StateHandler.updateState(messageCtxInComming), + clear: StateHandler.clear(messageCtxInComming.from), + } + // 📄 Crar CTX de mensaje (uso private) const createCtxMessage = (payload = {}, index = 0) => { const body = typeof payload === 'string' ? payload : payload?.body ?? payload?.answer @@ -232,6 +242,7 @@ class CoreClass { const argsCb = { provider, + state, fallBack: fallBack(flags), flowDynamic: flowDynamic(flags), endFlow: endFlow(flags), diff --git a/packages/bot/io/events/eventAction.js b/packages/bot/io/events/eventAction.js new file mode 100644 index 000000000..260fe700a --- /dev/null +++ b/packages/bot/io/events/eventAction.js @@ -0,0 +1,7 @@ +const { generateRef } = require('../../utils/hash') + +const eventAction = () => { + return generateRef('_event_action_') +} + +module.exports = { eventAction } diff --git a/packages/bot/io/events/index.js b/packages/bot/io/events/index.js index dfeb6ab98..7a51877f6 100644 --- a/packages/bot/io/events/index.js +++ b/packages/bot/io/events/index.js @@ -3,6 +3,7 @@ const { eventLocation, REGEX_EVENT_LOCATION } = require('./eventLocation') const { eventMedia, REGEX_EVENT_MEDIA } = require('./eventMedia') const { eventVoiceNote, REGEX_EVENT_VOICE_NOTE } = require('./eventVoiceNote') const { eventWelcome } = require('./eventWelcome') +const { eventAction } = require('./eventAction') const LIST_ALL = { WELCOME: eventWelcome(), @@ -10,6 +11,7 @@ const LIST_ALL = { LOCATION: eventLocation(), DOCUMENT: eventDocument(), VOICE_NOTE: eventVoiceNote(), + ACTION: eventAction(), } const LIST_REGEX = { diff --git a/packages/bot/io/flow.class.js b/packages/bot/io/flow.class.js index 812e008db..1f94866f9 100644 --- a/packages/bot/io/flow.class.js +++ b/packages/bot/io/flow.class.js @@ -26,8 +26,10 @@ class FlowClass { overFlow = overFlow ?? this.flowSerialize const mapSensitive = (str, mapOptions = { sensitive: false, regex: false }) => { - if (mapOptions.regex) return new RegExp(str) - const regexSensitive = mapOptions.sensitive ? 'g' : 'gi' + + if (mapOptions.regex) return new Function(`return ${str}`)(); + const regexSensitive = mapOptions.sensitive ? 'g' : 'i' + if (Array.isArray(str)) { const patterns = mapOptions.sensitive ? str.map((item) => `\\b${item}\\b`) : str return new RegExp(patterns.join('|'), regexSensitive) diff --git a/packages/docs/src/routes/docs/install/index.mdx b/packages/docs/src/routes/docs/install/index.mdx index 4b64e1169..d5354524c 100644 --- a/packages/docs/src/routes/docs/install/index.mdx +++ b/packages/docs/src/routes/docs/install/index.mdx @@ -45,7 +45,7 @@ Cada plantilla tiene sus dependencias necesarias basadas en tu previa selección "@bot-whatsapp/cli": "latest", "@bot-whatsapp/database": "latest", "@bot-whatsapp/provider": "latest", - "@adiwajshing/baileys": "github:WhiskeySockets/Baileys", + "@whiskeysockets/baileys": "^6.4.0", "mysql2": "^2.3.3", 👈 }, ``` diff --git a/packages/docs/src/routes/docs/providers/index.mdx b/packages/docs/src/routes/docs/providers/index.mdx index 5d66e8b6c..18d07cfaf 100644 --- a/packages/docs/src/routes/docs/providers/index.mdx +++ b/packages/docs/src/routes/docs/providers/index.mdx @@ -30,7 +30,7 @@ Los proveedores disponibles hasta el momento son los siguientes: [Meta Official](https://developers.facebook.com/docs/whatsapp/cloud-api/reference/messages) `require('@bot-whatsapp/provider/meta')` -[Twilio Official](https://www.twilio.com/es-mx/messaging/whatsapp) `require('@bot-whatsapp/provider/twilio')` +[Twilio Official](https://www.twilio.com/es-mx/messaging/channels/whatsapp) `require('@bot-whatsapp/provider/twilio')` --- diff --git a/packages/provider/src/baileys/index.js b/packages/provider/src/baileys/index.js index 4f044f00a..d0dd7d596 100644 --- a/packages/provider/src/baileys/index.js +++ b/packages/provider/src/baileys/index.js @@ -4,10 +4,19 @@ const pino = require('pino') const rimraf = require('rimraf') const mime = require('mime-types') const { join } = require('path') -const { createWriteStream, readFileSync } = require('fs') +const { createWriteStream, readFileSync, existsSync } = require('fs') const { Console } = require('console') -const { default: makeWASocket, useMultiFileAuthState, Browsers, DisconnectReason } = require('@adiwajshing/baileys') +const { + default: makeWASocket, + useMultiFileAuthState, + Browsers, + DisconnectReason, + proto, + makeInMemoryStore, + makeCacheableSignalKeyStore, + getAggregateVotesInPollMessage, +} = require('@whiskeysockets/baileys') const { baileyGenerateImage, baileyCleanNumber, baileyIsValidNumber } = require('./utils') @@ -25,11 +34,13 @@ const logger = new Console({ * https://github.com/whiskeysockets/Baileys */ class BaileysProvider extends ProviderClass { - globalVendorArgs = { name: `bot`, gifPlayback: false } + globalVendorArgs = { name: `bot`, gifPlayback: false, usePairingCode: false, phoneNumber: null } vendor + store saveCredsGlobal = null constructor(args) { super() + this.store = null this.globalVendorArgs = { ...this.globalVendorArgs, ...args } this.initBailey().then() } @@ -40,17 +51,57 @@ class BaileysProvider extends ProviderClass { initBailey = async () => { const NAME_DIR_SESSION = `${this.globalVendorArgs.name}_sessions` const { state, saveCreds } = await useMultiFileAuthState(NAME_DIR_SESSION) + const loggerBaileys = pino({ level: 'fatal' }) + this.saveCredsGlobal = saveCreds + this.store = makeInMemoryStore({ loggerBaileys }) + this.store.readFromFile(`${NAME_DIR_SESSION}/baileys_store.json`) + setInterval(() => { + const path = `${this.NAME_DIR_SESSION}/baileys_store.json` + if (existsSync(path)) { + this.store.writeToFile(path) + } + }, 10_000) + try { const sock = makeWASocket({ + logger: loggerBaileys, printQRInTerminal: false, - auth: state, + auth: { + creds: state.creds, + keys: makeCacheableSignalKeyStore(state.keys, loggerBaileys), + }, browser: Browsers.macOS('Desktop'), syncFullHistory: false, - logger: pino({ level: 'fatal' }), + generateHighQualityLinkPreview: true, + getMessage: this.getMessage, }) + this.store?.bind(sock.ev) + + if (this.globalVendorArgs.usePairingCode && !sock.authState.creds.registered) { + if (this.globalVendorArgs.phoneNumber) { + await sock.waitForConnectionUpdate((update) => !!update.qr) + const code = await sock.requestPairingCode(this.globalVendorArgs.phoneNumber) + this.emit('require_action', { + instructions: [ + `Acepta la notificación del WhatsApp ${this.globalVendorArgs.phoneNumber} en tu celular 👌`, + `El token para la vinculación es: ${code}`, + `Necesitas ayuda: https://link.codigoencasa.com/DISCORD`, + ], + }) + } else { + this.emit('auth_failure', [ + `No se ha definido el numero de telefono agregalo`, + `Reinicia el BOT`, + `Tambien puedes mirar un log que se ha creado baileys.log`, + `Necesitas ayuda: https://link.codigoencasa.com/DISCORD`, + `(Puedes abrir un ISSUE) https://github.com/codigoencasa/bot-whatsapp/issues/new/choose`, + ]) + } + } + sock.ev.on('connection.update', async (update) => { const { connection, lastDisconnect, qr } = update @@ -79,7 +130,7 @@ class BaileysProvider extends ProviderClass { } /** QR Code */ - if (qr) { + if (qr && !this.globalVendorArgs.usePairingCode) { this.emit('require_action', { instructions: [ `Debes escanear el QR Code 👌 ${this.globalVendorArgs.name}.qr.png`, @@ -165,6 +216,41 @@ class BaileysProvider extends ProviderClass { this.emit('message', payload) }, }, + { + event: 'messages.update', + func: async (message) => { + for (const { key, update } of message) { + if (update.pollUpdates) { + const pollCreation = await this.getMessage(key) + if (pollCreation) { + const pollMessage = await getAggregateVotesInPollMessage({ + message: pollCreation, + pollUpdates: update.pollUpdates, + }) + const [messageCtx] = message + + const messageOriginalKey = messageCtx?.update?.pollUpdates[0]?.pollUpdateMessageKey + const messageOriginal = await this.store.loadMessage( + messageOriginalKey.remoteJid, + messageOriginalKey.id + ) + + let payload = { + ...messageCtx, + body: pollMessage.find((poll) => poll.voters.length > 0)?.name || '', + from: baileyCleanNumber(key.remoteJid), + pushName: messageOriginal?.pushName, + broadcast: messageOriginal?.broadcast, + messageTimestamp: messageOriginal?.messageTimestamp, + voters: pollCreation, + type: 'poll', + } + this.emit('message', payload) + } + } + } + }, + }, ] initBusEvents = (_sock) => { @@ -176,6 +262,15 @@ class BaileysProvider extends ProviderClass { } } + getMessage = async (key) => { + if (this.store) { + const msg = await this.store.loadMessage(key.remoteJid, key.id) + return msg?.message || undefined + } + // only if store is present + return proto.Message.fromObject({}) + } + /** * Funcion SendRaw envia opciones directamente del proveedor * @example await sendMessage('+XXXXXXXXXXX', 'Hello World') @@ -302,6 +397,29 @@ class BaileysProvider extends ProviderClass { return this.vendor.sendMessage(numberClean, buttonMessage) } + /** + * + * @param {string} number + * @param {string} text + * @param {string} footer + * @param {Array} poll + * @example await sendMessage("+XXXXXXXXXXX", { poll: { "name": "You accept terms", "values": [ "Yes", "Not"], "selectableCount": 1 }) + */ + + sendPoll = async (numberIn, text, poll) => { + const numberClean = baileyCleanNumber(numberIn) + + if (poll.options.length < 2) return false + + const pollMessage = { + name: text, + values: poll.options, + selectableCount: poll?.multiselect === undefined ? 1 : poll?.multiselect ? 1 : 0, + } + + return this.vendor.sendMessage(numberClean, { poll: pollMessage }) + } + /** * TODO: Necesita terminar de implementar el sendMedia y sendButton guiarse: * https://github.com/leifermendez/bot-whatsapp/blob/4e0fcbd8347f8a430adb43351b5415098a5d10df/packages/provider/src/web-whatsapp/index.js#L165 diff --git a/packages/provider/src/baileys/package.json b/packages/provider/src/baileys/package.json index fb46b235d..aa31366b9 100644 --- a/packages/provider/src/baileys/package.json +++ b/packages/provider/src/baileys/package.json @@ -1,7 +1,6 @@ { "dependencies": { - "@adiwajshing/baileys": "github:WhiskeySockets/Baileys", - "@adiwajshing/keyed-db": "^0.2.4", + "@whiskeysockets/baileys": "^6.4.0", "wa-sticker-formatter": "4.3.2" } } diff --git a/packages/provider/src/wppconnect/index.js b/packages/provider/src/wppconnect/index.js index caaf3331f..693502cbf 100644 --- a/packages/provider/src/wppconnect/index.js +++ b/packages/provider/src/wppconnect/index.js @@ -43,6 +43,10 @@ class WPPConnectProviderClass extends ProviderClass { }) WppConnectGenerateImage(base64Qrimg, `${this.globalVendorArgs.name}.qr.png`) }, + puppeteerOptions: { + headless: true, + args: ['--no-sandbox'], + }, }) this.vendor = session diff --git a/starters/apps/base-baileys-json/package.json b/starters/apps/base-baileys-json/package.json index 3319b212e..6f85ee56f 100644 --- a/starters/apps/base-baileys-json/package.json +++ b/starters/apps/base-baileys-json/package.json @@ -15,8 +15,7 @@ "@bot-whatsapp/provider": "latest", "@bot-whatsapp/portal": "latest", "mime-types": "2.1.35", - "@adiwajshing/baileys": "github:WhiskeySockets/Baileys", - "@adiwajshing/keyed-db": "^0.2.4", + "@whiskeysockets/baileys": "^6.4.0", "wa-sticker-formatter": "4.3.2" }, "author": "", diff --git a/starters/apps/base-baileys-memory/package.json b/starters/apps/base-baileys-memory/package.json index 1cf8e7f95..5c64e3537 100644 --- a/starters/apps/base-baileys-memory/package.json +++ b/starters/apps/base-baileys-memory/package.json @@ -14,8 +14,7 @@ "@bot-whatsapp/database": "latest", "@bot-whatsapp/provider": "latest", "@bot-whatsapp/portal": "latest", - "@adiwajshing/baileys": "github:WhiskeySockets/Baileys", - "@adiwajshing/keyed-db": "^0.2.4", + "@whiskeysockets/baileys": "^6.4.0", "wa-sticker-formatter": "4.3.2" }, "author": "", diff --git a/starters/apps/base-baileys-mongo/package.json b/starters/apps/base-baileys-mongo/package.json index 473bd1198..305261b0d 100644 --- a/starters/apps/base-baileys-mongo/package.json +++ b/starters/apps/base-baileys-mongo/package.json @@ -16,8 +16,7 @@ "@bot-whatsapp/portal": "latest", "mime-types": "2.1.35", "mongodb": "^4.12.1", - "@adiwajshing/baileys": "github:WhiskeySockets/Baileys", - "@adiwajshing/keyed-db": "^0.2.4", + "@whiskeysockets/baileys": "^6.4.0", "wa-sticker-formatter": "4.3.2" }, "author": "", diff --git a/starters/apps/base-baileys-mysql/package.json b/starters/apps/base-baileys-mysql/package.json index 0b9c67e3e..b1284ff69 100644 --- a/starters/apps/base-baileys-mysql/package.json +++ b/starters/apps/base-baileys-mysql/package.json @@ -15,8 +15,7 @@ "@bot-whatsapp/provider": "latest", "@bot-whatsapp/portal": "latest", "mysql2": "^2.3.3", - "@adiwajshing/baileys": "github:WhiskeySockets/Baileys", - "@adiwajshing/keyed-db": "^0.2.4", + "@whiskeysockets/baileys": "^6.4.0", "wa-sticker-formatter": "4.3.2" }, "author": "",