chore: pre-chore

This commit is contained in:
Leifer Mendez
2023-02-04 17:59:45 +01:00
11 changed files with 239 additions and 155 deletions

View File

@@ -1,3 +1,3 @@
{
"recommendations": ["xyc.vscode-mdx-preview"]
"recommendations": ["xyc.vscode-mdx-preview", "vivaxy.vscode-conventional-commits", "mhutchie.git-graph"]
}

View File

@@ -2,12 +2,7 @@ const { test } = require('uvu')
const assert = require('uvu/assert')
const MOCK_DB = require('../packages/database/src/mock')
const PROVIDER_DB = require('../packages/provider/src/mock')
const {
addKeyword,
createBot,
createFlow,
createProvider,
} = require('../packages/bot/index')
const { addKeyword, createBot, createFlow, createProvider } = require('../packages/bot/index')
/**
* Falsear peticion async
@@ -21,11 +16,7 @@ const fakeHTTP = async (fakeData = []) => {
}
test(`[Caso - 05] Continuar Flujo (continueFlow)`, async () => {
const MOCK_VALUES = [
'¿CUal es tu email?',
'Continuamos....',
'¿Cual es tu edad?',
]
const MOCK_VALUES = ['¿CUal es tu email?', 'Continuamos....', '¿Cual es tu edad?']
const provider = createProvider(PROVIDER_DB)
const database = new MOCK_DB()
@@ -39,26 +30,20 @@ test(`[Caso - 05] Continuar Flujo (continueFlow)`, async () => {
const validation = ctx.body.includes('@')
if (validation) {
const getDataFromApi = await fakeHTTP([
'Gracias por tu email se ha validado de manera correcta',
])
const getDataFromApi = await fakeHTTP(['Gracias por tu email se ha validado de manera correcta'])
return flowDynamic(getDataFromApi)
}
return fallBack(validation)
}
)
.addAnswer(MOCK_VALUES[1])
.addAnswer(
MOCK_VALUES[2],
{ capture: true },
async (ctx, { flowDynamic, fallBack }) => {
if (ctx.body !== '18') {
await delay(50)
return fallBack(false, 'Ups creo que no eres mayor de edad')
}
return flowDynamic('Bien tu edad es correcta!')
.addAnswer(MOCK_VALUES[2], { capture: true }, async (ctx, { flowDynamic, fallBack }) => {
if (ctx.body !== '18') {
await delay(50)
return fallBack(false, 'Ups creo que no eres mayor de edad')
}
)
return flowDynamic('Bien tu edad es correcta!')
})
.addAnswer('Puedes pasar')
createBot({
@@ -98,10 +83,7 @@ test(`[Caso - 05] Continuar Flujo (continueFlow)`, async () => {
assert.is('this is not email value', getHistory[1])
assert.is(MOCK_VALUES[0], getHistory[2])
assert.is('test@test.com', getHistory[3])
assert.is(
'1 Gracias por tu email se ha validado de manera correcta',
getHistory[4]
)
assert.is('1 Gracias por tu email se ha validado de manera correcta', getHistory[4])
assert.is(MOCK_VALUES[1], getHistory[5])
assert.is(MOCK_VALUES[2], getHistory[6])
assert.is('20', getHistory[7])

92
__test__/07-case.test.js Normal file
View File

@@ -0,0 +1,92 @@
const { test } = require('uvu')
const assert = require('uvu/assert')
const MOCK_DB = require('../packages/database/src/mock')
const PROVIDER_DB = require('../packages/provider/src/mock')
const { addKeyword, createBot, createFlow, createProvider } = require('../packages/bot/index')
/**
* Falsear peticion async
* @param {*} fakeData
* @returns
*/
const fakeHTTP = async (fakeData = []) => {
await delay(5)
const data = fakeData.map((u, i) => ({ body: `${i + 1} ${u}` }))
return Promise.resolve(data)
}
let STATE_APP = {}
test(`[Caso - 07] Retornar estado`, async () => {
const MOCK_VALUES = ['¿Cual es tu nombre?', '¿Cual es tu edad?', 'Tu datos son:']
const provider = createProvider(PROVIDER_DB)
const database = new MOCK_DB()
const flujoPrincipal = addKeyword(['hola'])
.addAnswer(
MOCK_VALUES[0],
{
capture: true,
},
async (ctx, { flowDynamic, fallBack }) => {
STATE_APP[ctx.from] = { ...STATE_APP[ctx.from], name: ctx.body }
flowDynamic('Gracias por tu nombre!')
}
)
.addAnswer(
MOCK_VALUES[1],
{
capture: true,
},
async (ctx, { flowDynamic, endFlow }) => {
STATE_APP[ctx.from] = { ...STATE_APP[ctx.from], age: ctx.body }
await flowDynamic('Gracias por tu edad!')
}
)
.addAnswer(MOCK_VALUES[2], null, async (ctx, { flowDynamic }) => {
flowDynamic(`Nombre: ${STATE_APP[ctx.from].name} Edad: ${STATE_APP[ctx.from].age}`)
})
.addAnswer('🤖🤖 Gracias por tu participacion')
createBot({
database,
flow: createFlow([flujoPrincipal]),
provider,
})
provider.delaySendMessage(0, 'message', {
from: '000',
body: 'hola',
})
provider.delaySendMessage(20, 'message', {
from: '000',
body: 'Leifer',
})
provider.delaySendMessage(40, 'message', {
from: '000',
body: '90',
})
await delay(1200)
const getHistory = database.listHistory.map((i) => i.answer)
assert.is(MOCK_VALUES[0], getHistory[0])
assert.is('Leifer', getHistory[1])
assert.is('Gracias por tu nombre!', getHistory[2])
assert.is('¿Cual es tu edad?', getHistory[3])
assert.is('90', getHistory[4])
assert.is('Gracias por tu edad!', getHistory[5])
assert.is('Tu datos son:', getHistory[6])
assert.is('Nombre: Leifer Edad: 90', getHistory[7])
assert.is('🤖🤖 Gracias por tu participacion', getHistory[8])
assert.is(undefined, getHistory[9])
})
test.run()
function delay(ms) {
return new Promise((res) => setTimeout(res, ms))
}

43
__test__/08-case.test.js Normal file
View File

@@ -0,0 +1,43 @@
const { test } = require('uvu')
const assert = require('uvu/assert')
const MOCK_DB = require('../packages/database/src/mock')
const PROVIDER_DB = require('../packages/provider/src/mock')
const { addKeyword, createBot, createFlow, createProvider } = require('../packages/bot/index')
test(`[Caso - 08] Regular expression on keyword`, async () => {
const provider = createProvider(PROVIDER_DB)
const database = new MOCK_DB()
const REGEX_CREDIT_NUMBER = `/(^4[0-9]{12}(?:[0-9]{3})?$)|(^(?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}$)|(3[47][0-9]{13})|(^3(?:0[0-5]|[68][0-9])[0-9]{11}$)|(^6(?:011|5[0-9]{2})[0-9]{12}$)|(^(?:2131|1800|35\d{3})\d{11}$)/gm`
const flujoPrincipal = addKeyword(REGEX_CREDIT_NUMBER, { regex: true })
.addAnswer(`Gracias por proporcionar un numero de tarjeta valido`)
.addAnswer('Fin!')
createBot({
database,
flow: createFlow([flujoPrincipal]),
provider,
})
provider.delaySendMessage(0, 'message', {
from: '000',
body: 'hola',
})
provider.delaySendMessage(20, 'message', {
from: '000',
body: '374245455400126',
})
await delay(40)
const getHistory = database.listHistory.map((i) => i.answer)
assert.is('Gracias por proporcionar un numero de tarjeta valido', getHistory[0])
assert.is('Fin!', getHistory[1])
assert.is(undefined, getHistory[2])
})
test.run()
function delay(ms) {
return new Promise((res) => setTimeout(res, ms))
}

View File

@@ -43,8 +43,7 @@ class CoreClass {
},
{
event: 'require_action',
func: ({ instructions, title = '⚡⚡ ACCIÓN REQUERIDA ⚡⚡' }) =>
printer(instructions, title),
func: ({ instructions, title = '⚡⚡ ACCIÓN REQUERIDA ⚡⚡' }) => printer(instructions, title),
},
{
event: 'ready',
@@ -52,8 +51,7 @@ class CoreClass {
},
{
event: 'auth_failure',
func: ({ instructions }) =>
printer(instructions, '⚡⚡ ERROR AUTH ⚡⚡'),
func: ({ instructions }) => printer(instructions, '⚡⚡ ERROR AUTH ⚡⚡'),
},
{
@@ -78,9 +76,7 @@ class CoreClass {
if (!body.length) return
let prevMsg = await this.databaseClass.getPrevByNumber(from)
const refToContinue = this.flowClass.findBySerialize(
prevMsg?.refSerialize
)
const refToContinue = this.flowClass.findBySerialize(prevMsg?.refSerialize)
if (prevMsg?.ref) {
const ctxByNumber = toCtx({
@@ -93,10 +89,7 @@ class CoreClass {
// 📄 Crar CTX de mensaje (uso private)
const createCtxMessage = (payload = {}, index = 0) => {
const body =
typeof payload === 'string'
? payload
: payload?.body ?? payload?.answer
const body = typeof payload === 'string' ? payload : payload?.body ?? payload?.answer
const media = payload?.media ?? null
const buttons = payload?.buttons ?? []
const capture = payload?.capture ?? false
@@ -118,30 +111,16 @@ class CoreClass {
// 📄 Finalizar flujo
const endFlow = async (message = null) => {
prevMsg = null
endFlowFlag = true
if (message)
this.sendProviderAndSave(from, createCtxMessage(message))
if (message) this.sendProviderAndSave(from, createCtxMessage(message))
clearQueue()
return
}
// 📄 Continuar con el siguiente flujo
const continueFlow = async () => {
const cotinueMessage =
this.flowClass.find(refToContinue?.ref, true) || []
sendFlow(cotinueMessage, from, { continue: true })
sendFlow([])
return
}
// 📄 Esta funcion se encarga de enviar un array de mensajes dentro de este ctx
const sendFlow = async (
messageToSend,
numberOrId,
options = { continue: false }
) => {
if (!options.continue && prevMsg?.options?.capture)
await cbEveryCtx(prevMsg?.ref)
const sendFlow = async (messageToSend, numberOrId, options = { prev: prevMsg }) => {
if (options.prev?.options?.capture) await cbEveryCtx(options.prev?.ref)
const queue = []
for (const ctxMessage of messageToSend) {
@@ -150,9 +129,7 @@ class CoreClass {
if (delayMs) await delay(delayMs)
QueuePrincipal.enqueue(() =>
Promise.all([
this.sendProviderAndSave(numberOrId, ctxMessage).then(
() => resolveCbEveryCtx(ctxMessage)
),
this.sendProviderAndSave(numberOrId, ctxMessage).then(() => resolveCbEveryCtx(ctxMessage)),
])
)
}
@@ -160,20 +137,26 @@ class CoreClass {
}
// 📄 [options: fallBack]: esta funcion se encarga de repetir el ultimo mensaje
const fallBack = async (next = false, message = null) => {
const fallBack = async (validation = false, message = null) => {
QueuePrincipal.queue = []
if (next) return continueFlow()
return this.sendProviderAndSave(from, {
if (validation) {
const currentPrev = await this.databaseClass.getPrevByNumber(from)
const nextFlow = await this.flowClass.find(refToContinue?.ref, true)
const filterNextFlow = nextFlow.filter((msg) => msg.refSerialize !== currentPrev?.refSerialize)
return sendFlow(filterNextFlow, from, { prev: undefined })
}
await this.sendProviderAndSave(from, {
...prevMsg,
answer:
typeof message === 'string'
? message
: message?.body ?? prevMsg.answer,
answer: typeof message === 'string' ? message : message?.body ?? prevMsg.answer,
options: {
...prevMsg.options,
buttons: message?.buttons ?? prevMsg.options?.buttons,
buttons: prevMsg.options?.buttons,
},
})
return
}
// 📄 [options: flowDynamic]: esta funcion se encarga de responder un array de respuesta esta limitado a 5 mensajes
@@ -182,21 +165,33 @@ class CoreClass {
const flowDynamic = async (listMsg = []) => {
if (!Array.isArray(listMsg)) listMsg = [listMsg]
const parseListMsg = listMsg.map((opt, index) =>
createCtxMessage(opt, index)
)
const parseListMsg = listMsg.map((opt, index) => createCtxMessage(opt, index))
const currentPrev = await this.databaseClass.getPrevByNumber(from)
const skipContinueFlow = async () => {
const nextFlow = await this.flowClass.find(refToContinue?.ref, true)
const filterNextFlow = nextFlow.filter((msg) => msg.refSerialize !== currentPrev?.refSerialize)
const isContinueFlow = filterNextFlow.map((i) => i.keyword).includes(currentPrev?.ref)
return {
continue: !isContinueFlow,
contexts: filterNextFlow,
}
}
if (endFlowFlag) return
for (const msg of parseListMsg) {
await this.sendProviderAndSave(from, msg)
}
return continueFlow()
const continueFlowData = await skipContinueFlow()
if (continueFlowData.continue) return sendFlow(continueFlowData.contexts, from, { prev: undefined })
return
}
// 📄 Se encarga de revisar si el contexto del mensaje tiene callback o fallback
const resolveCbEveryCtx = async (ctxMessage) => {
if (!ctxMessage?.options?.capture)
return await cbEveryCtx(ctxMessage?.ref)
if (!ctxMessage?.options?.capture) return await cbEveryCtx(ctxMessage?.ref)
}
// 📄 Se encarga de revisar si el contexto del mensaje tiene callback y ejecutarlo
@@ -206,7 +201,6 @@ class CoreClass {
fallBack,
flowDynamic,
endFlow,
continueFlow,
})
}
@@ -246,10 +240,9 @@ class CoreClass {
*/
sendProviderAndSave = (numberOrId, ctxMessage) => {
const { answer } = ctxMessage
return Promise.all([
this.providerClass.sendMessage(numberOrId, answer, ctxMessage),
this.databaseClass.save({ ...ctxMessage, from: numberOrId }),
])
return this.providerClass
.sendMessage(numberOrId, answer, ctxMessage)
.then(() => this.databaseClass.save({ ...ctxMessage, from: numberOrId }))
}
/**
@@ -279,9 +272,7 @@ class CoreClass {
for (const ctxMessage of messageToSend) {
const delayMs = ctxMessage?.options?.delay || 0
if (delayMs) await delay(delayMs)
QueuePrincipal.enqueue(() =>
this.sendProviderAndSave(numberOrId, ctxMessage)
)
QueuePrincipal.enqueue(() => this.sendProviderAndSave(numberOrId, ctxMessage))
}
return Promise.all(queue)
}

View File

@@ -25,9 +25,17 @@ class FlowClass {
let refSymbol = null
overFlow = overFlow ?? this.flowSerialize
const customRegex = (str = null) => {
if (typeof str !== 'string') return
const instanceRegex = new RegExp(str)
return instanceRegex.test(str)
}
/** Retornar expresion regular para buscar coincidencia */
const mapSensitive = (str, flag = false) => {
const regexSensitive = flag ? 'g' : 'i'
const mapSensitive = (str, mapOptions = { sensitive: false, regex: false }) => {
if (mapOptions.regex) return customRegex(str)
const regexSensitive = mapOptions.sensitive ? 'g' : 'i'
if (Array.isArray(str)) {
return new RegExp(str.join('|'), regexSensitive)
}
@@ -36,6 +44,7 @@ class FlowClass {
const findIn = (keyOrWord, symbol = false, flow = overFlow) => {
const sensitive = refSymbol?.options?.sensitive || false
const regex = refSymbol?.options?.regex || false
capture = refSymbol?.options?.capture || false
if (capture) return messages
@@ -46,7 +55,7 @@ class FlowClass {
if (refSymbol?.ref) findIn(refSymbol.ref, true)
} else {
refSymbol = flow.find((c) => {
return mapSensitive(c.keyword, sensitive).test(keyOrWord)
return mapSensitive(c.keyword, { sensitive, regex }).test(keyOrWord)
})
if (refSymbol?.ref) findIn(refSymbol.ref, true)
return messages
@@ -56,8 +65,7 @@ class FlowClass {
return messages
}
findBySerialize = (refSerialize) =>
this.flowSerialize.find((r) => r.refSerialize === refSerialize)
findBySerialize = (refSerialize) => this.flowSerialize.find((r) => r.refSerialize === refSerialize)
findIndexByRef = (ref) => this.flowSerialize.findIndex((r) => r.ref === ref)
}

View File

@@ -8,12 +8,14 @@ const { toJson } = require('./toJson')
* @param {*} options {sensitive:boolean} default false
*/
const addKeyword = (keyword, options) => {
if (typeof keyword !== 'string' && !Array.isArray(keyword)) {
throw new Error('DEBE_SER_STRING_ARRAY_REGEX')
}
const parseOptions = () => {
const defaultProperties = {
sensitive:
typeof options?.sensitive === 'boolean'
? options?.sensitive
: false,
sensitive: typeof options?.sensitive === 'boolean' ? options?.sensitive : false,
regex: typeof options?.regex === 'boolean' ? options?.regex : false,
}
return defaultProperties

View File

@@ -1,6 +1,6 @@
{
"name": "@bot-whatsapp/bot",
"version": "0.0.91-alpha.0",
"version": "0.0.96-alpha.0",
"description": "",
"main": "./lib/bundle.bot.cjs",
"scripts": {

View File

@@ -10,7 +10,10 @@ class MockDatabase {
constructor() {}
getPrevByNumber = (from) => {
const history = this.listHistory.slice().reverse()
const history = this.listHistory
.slice()
.reverse()
.filter((i) => !!i.keyword)
return history.find((a) => a.from === from)
}

View File

@@ -7,18 +7,9 @@ const { join } = require('path')
const { createWriteStream, readFileSync } = require('fs')
const { Console } = require('console')
const {
default: makeWASocket,
useMultiFileAuthState,
Browsers,
DisconnectReason,
} = require('@adiwajshing/baileys')
const { default: makeWASocket, useMultiFileAuthState, Browsers, DisconnectReason } = require('@adiwajshing/baileys')
const {
baileyGenerateImage,
baileyCleanNumber,
baileyIsValidNumber,
} = require('./utils')
const { baileyGenerateImage, baileyCleanNumber, baileyIsValidNumber } = require('./utils')
const { generalDownload } = require('../../common/download')
@@ -46,9 +37,7 @@ class BaileysProvider extends ProviderClass {
*/
initBailey = async () => {
const NAME_DIR_SESSION = `${this.globalVendorArgs.name}_sessions`
const { state, saveCreds } = await useMultiFileAuthState(
NAME_DIR_SESSION
)
const { state, saveCreds } = await useMultiFileAuthState(NAME_DIR_SESSION)
this.saveCredsGlobal = saveCreds
try {
@@ -57,7 +46,7 @@ class BaileysProvider extends ProviderClass {
auth: state,
browser: Browsers.macOS('Desktop'),
syncFullHistory: false,
logger: pino({ level: 'error' }),
logger: pino({ level: 'fatal' }),
})
sock.ev.on('connection.update', async (update) => {
@@ -96,10 +85,7 @@ class BaileysProvider extends ProviderClass {
`Necesitas ayuda: https://link.codigoencasa.com/DISCORD`,
],
})
await baileyGenerateImage(
qr,
`${this.globalVendorArgs.name}.qr.png`
)
await baileyGenerateImage(qr, `${this.globalVendorArgs.name}.qr.png`)
}
})
@@ -131,9 +117,10 @@ class BaileysProvider extends ProviderClass {
const [messageCtx] = messages
let payload = {
...messageCtx,
body: messageCtx?.message?.conversation,
body: messageCtx?.message?.extendedTextMessage?.text ?? messageCtx?.message?.conversation,
from: messageCtx?.key?.remoteJid,
}
if (payload.from === 'status@broadcast') return
if (payload?.key?.fromMe) return
@@ -142,9 +129,7 @@ class BaileysProvider extends ProviderClass {
return
}
const btnCtx =
payload?.message?.buttonsResponseMessage
?.selectedDisplayText
const btnCtx = payload?.message?.buttonsResponseMessage?.selectedDisplayText
if (btnCtx) payload.body = btnCtx
@@ -174,12 +159,9 @@ class BaileysProvider extends ProviderClass {
const fileDownloaded = await generalDownload(imageUrl)
const mimeType = mime.lookup(fileDownloaded)
if (mimeType.includes('image'))
return this.sendImage(number, fileDownloaded, text)
if (mimeType.includes('video'))
return this.sendVideo(number, fileDownloaded, text)
if (mimeType.includes('audio'))
return this.sendAudio(number, fileDownloaded, text)
if (mimeType.includes('image')) return this.sendImage(number, fileDownloaded, text)
if (mimeType.includes('video')) return this.sendVideo(number, fileDownloaded, text)
if (mimeType.includes('audio')) return this.sendAudio(number, fileDownloaded, text)
return this.sendFile(number, fileDownloaded)
}
@@ -294,10 +276,8 @@ class BaileysProvider extends ProviderClass {
sendMessage = async (numberIn, message, { options }) => {
const number = baileyCleanNumber(numberIn)
if (options?.buttons?.length)
return this.sendButtons(number, message, options.buttons)
if (options?.media)
return this.sendMedia(number, options.media, message)
if (options?.buttons?.length) return this.sendButtons(number, message, options.buttons)
if (options?.media) return this.sendMedia(number, options.media, message)
return this.sendText(number, message)
}
@@ -332,12 +312,7 @@ class BaileysProvider extends ProviderClass {
* @example await sendContact("xxxxxxxxxxx@c.us" || "xxxxxxxxxxxxxxxxxx@g.us", "+xxxxxxxxxxx", "Robin Smith", messages)
*/
sendContact = async (
remoteJid,
contactNumber,
displayName,
messages = null
) => {
sendContact = async (remoteJid, contactNumber, displayName, messages = null) => {
const cleanContactNumber = contactNumber.replaceAll(' ', '')
const waid = cleanContactNumber.replace('+', '')

View File

@@ -2,11 +2,7 @@ const { Client, LocalAuth, MessageMedia, Buttons } = require('whatsapp-web.js')
const { ProviderClass } = require('@bot-whatsapp/bot')
const { Console } = require('console')
const { createWriteStream, readFileSync } = require('fs')
const {
wwebCleanNumber,
wwebGenerateImage,
wwebIsValidNumber,
} = require('./utils')
const { wwebCleanNumber, wwebGenerateImage, wwebIsValidNumber } = require('./utils')
const logger = new Console({
stdout: createWriteStream('./log'),
@@ -32,11 +28,7 @@ class WebWhatsappProvider extends ProviderClass {
}),
puppeteer: {
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--unhandled-rejections=strict',
],
args: ['--no-sandbox', '--disable-setuid-sandbox', '--unhandled-rejections=strict'],
//executablePath: 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
},
})
@@ -80,10 +72,7 @@ class WebWhatsappProvider extends ProviderClass {
`Necesitas ayuda: https://link.codigoencasa.com/DISCORD`,
],
})
await wwebGenerateImage(
qr,
`${this.globalVendorArgs.name}.qr.png`
)
await wwebGenerateImage(qr, `${this.globalVendorArgs.name}.qr.png`)
},
},
{
@@ -116,6 +105,9 @@ class WebWhatsappProvider extends ProviderClass {
* @returns
*/
sendButtons = async (number, message, buttons = []) => {
console.log(`🚩 ¿No te funciona los botones? Intenta instalar`)
console.log(`npm i github:pedroslopez/whatsapp-web.js#fix-buttons-list`)
const buttonMessage = new Buttons(message, buttons, '', '')
return this.vendor.sendMessage(number, buttonMessage)
}
@@ -226,12 +218,9 @@ class WebWhatsappProvider extends ProviderClass {
const fileDownloaded = await generalDownload(mediaUrl)
const mimeType = mime.lookup(fileDownloaded)
if (mimeType.includes('image'))
return this.sendImage(number, fileDownloaded, text)
if (mimeType.includes('video'))
return this.sendVideo(number, fileDownloaded)
if (mimeType.includes('audio'))
return this.sendAudio(number, fileDownloaded, text)
if (mimeType.includes('image')) return this.sendImage(number, fileDownloaded, text)
if (mimeType.includes('video')) return this.sendVideo(number, fileDownloaded)
if (mimeType.includes('audio')) return this.sendAudio(number, fileDownloaded)
return this.sendFile(number, fileDownloaded)
}
@@ -245,8 +234,7 @@ class WebWhatsappProvider extends ProviderClass {
*/
sendMessage = async (userId, message, { options }) => {
const number = wwebCleanNumber(userId)
if (options?.buttons?.length)
return this.sendButtons(number, message, options.buttons)
if (options?.buttons?.length) return this.sendButtons(number, message, options.buttons)
if (options?.media) return this.sendMedia(number, options.media)
return this.sendText(number, message)
}