diff --git a/.eslintrc.json b/.eslintrc.json index cf8961c..1adf73a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,6 +3,12 @@ "rules": { "no-useless-escape": [ "off" + ], + "no-unmodified-loop-condition": [ + "off" + ], + "no-irregular-whitespace": [ + "off" ] } } \ No newline at end of file diff --git a/.gitignore b/.gitignore index 355f242..7b15b96 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,10 @@ node_modules -.wwebjs_auth -.wwebjs_cache +auth_info_baileys .env package-lock.json errors.log -temp/* -!temp/.gitkeep \ No newline at end of file +src/temp/* +!src/temp/.gitkeep \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 36cc1d0..7a73a41 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,22 +1,2 @@ { - "workbench.colorCustomizations": { - "activityBar.activeBackground": "#14e8a2", - "activityBar.background": "#14e8a2", - "activityBar.foreground": "#15202b", - "activityBar.inactiveForeground": "#15202b99", - "activityBarBadge.background": "#bb4ff0", - "activityBarBadge.foreground": "#15202b", - "commandCenter.border": "#15202b99", - "sash.hoverBorder": "#14e8a2", - "statusBar.background": "#10b981", - "statusBar.foreground": "#15202b", - "statusBarItem.hoverBackground": "#0c8a60", - "statusBarItem.remoteBackground": "#10b981", - "statusBarItem.remoteForeground": "#15202b", - "titleBar.activeBackground": "#10b981", - "titleBar.activeForeground": "#15202b", - "titleBar.inactiveBackground": "#10b98199", - "titleBar.inactiveForeground": "#15202b99" - }, - "peacock.color": "#10b981" } \ No newline at end of file diff --git a/package.json b/package.json index 703c0c8..fb841d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "deadbyte-v3", - "version": "3.0.0", + "version": "3.5.0", "description": "deadbyte 3rd generation", "author": "sergiooak", "main": "src/index.js", @@ -15,21 +15,28 @@ }, "license": "unlicense", "dependencies": { - "change-case": "^5.3.0", + "@whiskeysockets/baileys": "^6.6.0", + "change-case": "^5.4.2", + "cheerio": "^1.0.0-rc.12", "citty": "^0.1.5", "dayjs": "^1.11.10", - "dotenv": "^16.3.1", + "dotenv": "^16.4.1", + "file-type": "^19.0.0", "filenamify": "^6.0.0", + "fluent-ffmpeg": "^2.1.2", "form-data": "^4.0.0", + "link-preview-js": "^3.0.5", "mime-types": "^2.1.35", + "node-cache": "^5.1.2", "node-cron": "^3.0.3", - "openai": "^4.24.1", + "node-webpmux": "^3.2.0", + "openai": "^4.25.0", "pino": "^8.17.2", "pino-pretty": "^10.3.1", "qrcode-terminal": "^0.12.0", "qs": "^6.11.2", + "read-chunk": "^4.0.3", "sharp": "^0.32.6", - "whatsapp-web.js": "^1.23.0", "yargs": "^17.7.2" }, "devDependencies": { diff --git a/src/db.js b/src/db.js index 9cd66ab..47cb5ec 100644 --- a/src/db.js +++ b/src/db.js @@ -1,6 +1,6 @@ -import fetch from 'node-fetch' -import logger from './logger.js' import { kebabCase } from 'change-case' +import logger from './logger.js' +import fetch from 'node-fetch' import qs from 'qs' // // ===================================== Variables ====================================== @@ -48,7 +48,6 @@ doLogin() // /** * Check if the API is online - * * @returns {boolean} true if the API is online */ export function isOnline () { @@ -57,7 +56,6 @@ export function isOnline () { /** * Get the token - * * @returns {string} token */ export function getToken () { @@ -66,7 +64,6 @@ export function getToken () { /** * Load the commands from the database - * * @returns {object} commands */ export async function loadCommands () { @@ -98,7 +95,6 @@ export async function loadCommands () { /** * Get the commands - * * @returns {object} commands */ export function getCommands () { @@ -129,14 +125,16 @@ export function getBot () { * @param {import('whatsapp-web.js').Contact} contact */ export async function findOrCreateContact (contact) { - // 1 - Check if contact.id._serialized is on the cache - if (contactsCache[contact.id._serialized]) { - contactsCache[contact.id._serialized].lastSeen = new Date() - return contactsCache[contact.id._serialized] + const id = contact.id.replace('@s.whatsapp.net', '@c.us') + + // 1 - Check if contact is on the cache + if (contactsCache[id]) { + contactsCache[id].lastSeen = new Date() + return contactsCache[id] } // 2 - If not, fetch from the database - const response = await fetch(`${dbUrl}/contacts/${contact.id._serialized}`, { + const response = await fetch(`${dbUrl}/contacts/${id}`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -144,20 +142,18 @@ export async function findOrCreateContact (contact) { }, body: JSON.stringify({ data: { - name: contact.name, - number: contact.id.user, + number: id.split('@')[0], pushname: contact.pushname, - isMyContact: contact.isMyContact, - wid: contact.id._serialized + wid: id } }) }) const data = await response.json() // 3 - Save on the cache - contactsCache[contact.id._serialized] = data - contactsCache[contact.id._serialized].lastSeen = new Date() - return contactsCache[contact.id._serialized] + contactsCache[id] = data + contactsCache[id].lastSeen = new Date() + return contactsCache[id] } // mini cache system for contacts, every minute filter out the contacts that haven't been seen in the last 5 minutes @@ -171,20 +167,33 @@ setInterval(() => { }) }, 60_000) +/** + * Force a contact update on the database + * @param {import('whatsapp-web.js').Contact} contact + * @returns {Promise} - The updated contact + */ +export async function forceContactUpdate (contact) { + const id = contact.id.replace('@s.whatsapp.net', '@c.us') + delete contactsCache[id] + return await findOrCreateContact(contact) +} + /** * Find or create a chat on the database - * - * @param {import('whatsapp-web.js').Chat} chat */ -export async function findOrCreateChat (chat) { +export async function findOrCreateChat (msg) { // 1 - Check if chat.id._serialized is on the cache - if (chatsCache[chat.id._serialized]) { - chatsCache[chat.id._serialized].lastSeen = new Date() - return chatsCache[chat.id._serialized] + const id = msg.isGroup + ? msg.aux.group.id + : msg.contact.id.replace('@s.whatsapp.net', '@c.us') + + if (chatsCache[id]) { + chatsCache[id].lastSeen = new Date() + return chatsCache[id] } // 2 - If not, fetch from the database - const response = await fetch(`${dbUrl}/chats/${chat.id._serialized}`, { + const response = await fetch(`${dbUrl}/chats/${id}`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -192,17 +201,17 @@ export async function findOrCreateChat (chat) { }, body: JSON.stringify({ data: { - name: chat.name, - isGroup: chat.isGroup, - wid: chat.id._serialized + name: msg.isGroup ? msg.aux.group.subject : msg.pushname, + isGroup: msg.isGroup, + wid: id } }) }) const data = await response.json() // 3 - Save on the cache - chatsCache[chat.id._serialized] = data - chatsCache[chat.id._serialized].lastSeen = new Date() + chatsCache[id] = data + chatsCache[id].lastSeen = new Date() return data } @@ -274,30 +283,32 @@ export async function saveActionToDB (moduleName, functionName, msg) { const commandGroupID = commandGroup?.id const command = commandGroup.commands.find((command) => command.slug === kebabCase(functionName)) const commandID = command?.id - const contact = await findOrCreateContact(msg.aux.sender) + const contact = await findOrCreateContact(msg.contact) const contactID = contact.id - const chat = await findOrCreateChat(msg.aux.chat) + const chat = await findOrCreateChat(msg) const chatID = chat.id - - const action = await createAction(commandGroupID, commandID, chatID, contactID) - + const action = createAction(commandGroupID, commandID, chatID, contactID) try { - const actionID = action.id - return { action, actionID, commandGroup, commandGroupID, command, commandID, contact, contactID, chat, chatID } + return { action, commandGroup, commandGroupID, command, commandID, contact, contactID, chat, chatID } } catch (error) { logger.error('Error saving action to database', error) logger.error('Action:', action) } } -export async function findCurrentBot (client) { +export async function findCurrentBot (socket) { + if (!socket.user) { // TODO: auto-restart + logger.warn('Bot never connected before') + logger.warn('Remember to restart after reading the QR code') + return + } + const id = socket.user.id.split(':')[0] + '@c.us' // 1 - Check if bot already exists on db - const findQuery = qs.stringify( { filters: { wid: { - $eq: client.info.wid._serialized + $eq: id } } }, @@ -305,6 +316,9 @@ export async function findCurrentBot (client) { encodeValuesOnly: true // prettify URL } ) + while (!token) { // wait token to be populated + await new Promise(resolve => setTimeout(resolve, 100)) + } const find = await fetch(`${dbUrl}/bots?${findQuery}`, { method: 'GET', headers: { @@ -312,7 +326,6 @@ export async function findCurrentBot (client) { Authorization: `Bearer ${token}` } }) - const { data: findData } = await find.json() if (findData.length) { @@ -321,7 +334,6 @@ export async function findCurrentBot (client) { } // 2 - If not, create it - const create = await fetch(`${dbUrl}/bots`, { method: 'POST', headers: { @@ -330,12 +342,12 @@ export async function findCurrentBot (client) { }, body: JSON.stringify({ data: { - wid: client.info.wid._serialized, - pushname: client.info.pushname, - platform: client.info.platform + wid: id, + pushname: socket.user.name } }) }) + const { data: createData } = await create.json() bot = createData.id } diff --git a/src/index.js b/src/index.js index 4015d3d..ef22b7b 100644 --- a/src/index.js +++ b/src/index.js @@ -1,15 +1,16 @@ import 'dotenv/config' +import importFresh from './utils/importFresh.js' +import * as baileys from '@whiskeysockets/baileys' import { defineCommand, runMain } from 'citty' import { apiKey } from './config/api.js' -import { snakeCase } from 'change-case' -import wwebjs from 'whatsapp-web.js' +import { dotCase } from 'change-case' +import NodeCache from 'node-cache' import bot from './config/bot.js' import logger from './logger.js' +import * as db from './db.js' import fs from 'fs/promises' -import './db.js' - -let globalArgs = {} +import pino from 'pino' const main = defineCommand({ meta: { @@ -22,132 +23,148 @@ const main = defineCommand({ type: 'positional', description: 'Bot name unique per session' }, - stickerOnly: { + sticker: { type: 'boolean', description: 'Deactivate all commands and only listen to stickers' }, - showBrowser: { + 'no-store': { type: 'boolean', - description: 'Deactivate headless mode and show the browser' + description: 'Do not store session data' }, - dummy: { + 'no-reply': { type: 'boolean', description: 'Do not reply to messages' + }, + 'use-pairing-code': { + type: 'boolean', + description: 'Use pairing code instead of QR code' + }, + mobile: { + type: 'boolean', + description: 'Use mobile user agent' } }, run ({ args }) { - globalArgs = args + global.args = args bot.name = args.name logger.info(`Starting bot "${args.name}"`) - bot.headless = !args.showBrowser ? 'new' : false - logger.info(`Headless mode: ${bot.headless ? 'on' : 'off'}`) + bot.useStore = !args['no-store'] + logger.info(`Store mode: ${bot.useStore ? 'on' : 'off'}`) + bot.doReplies = !args['no-reply'] + logger.info(`Reply messages: ${bot.doReplies ? 'on' : 'off'}`) + + bot.usePairingCode = args['use-pairing-code'] + bot.useMobile = args.mobile + bot.mode = 'qr' + if (bot.usePairingCode) bot.mode = 'pairing' + if (bot.useMobile) bot.mode = 'mobile' + logger.info(`Mode: ${bot.mode}`) + bot.stickerOnly = args.stickerOnly logger.info(`Sticker only mode: ${bot.stickerOnly ? 'on' : 'off'}`) - bot.dummy = args.dummy - logger.info(`Dummy mode: ${bot.dummy ? 'on' : 'off'}`) - loadEvents() + const store = bot.useStore + ? baileys.makeInMemoryStore({ logger: pino().child({ level: 'fatal', stream: 'store' }) }) + : undefined + + const storePath = `./src/temp/${bot.name}.json` + store.readFromFile(storePath) + // save every 10s + setInterval(() => { + store.writeToFile(storePath) + }, 10_000) + + connectToWhatsApp() } }) +export const store = undefined runMain(main) /** - * Grabs CLI args - * @returns {object} - */ -export function getArgs () { - return globalArgs + * Grabs the socket + * @returns {import('./types').WSocket} +*/ +export function getSocket () { + return socket } +let socket = null -/** - * Whatsapp Web Client - * @type {wwebjs.Client} - */ -let client = null +// external map to store retry counts of messages when decryption/encryption fails +// keep this out of the socket itself, so as to prevent a message decryption/encryption loop across socket restarts +const msgRetryCounterCache = new NodeCache() -/** - * Grabs Whatsapp Web Client - * @returns {wwebjs.Client} - */ -export function getClient () { - return client -} +export async function connectToWhatsApp () { + // if no API KEY, kill the process + if (!apiKey) { + logger.fatal('API_KEY not found! Grab one at https://api.deadbyte.com.br') + process.exit(1) + } -async function loadEvents () { - logger.info('Loading events...', bot) - client = new wwebjs.Client({ - authStrategy: new wwebjs.LocalAuth({ - clientId: bot.name - }), - - puppeteer: { - headless: bot.headless, - executablePath: bot.chromePath, - args: [ - '--lang=pt-BR,pt', - '--autoplay-policy=user-gesture-required', - '--disable-background-timer-throttling', - '--disable-backgrounding-occluded-windows', - '--disable-breakpad', - '--disable-client-side-phishing-detection', - '--disable-component-update', - '--disable-default-apps', - '--disable-dev-shm-usage', - '--disable-domain-reliability', - '--disable-extensions', - '--disable-features=AudioServiceOutOfProcess', - '--disable-hang-monitor', - '--disable-ipc-flooding-protection', - '--disable-notifications', - '--disable-offer-store-unmasked-wallet-cards', - '--disable-popup-blocking', - '--disable-print-preview', - '--disable-prompt-on-repost', - '--disable-renderer-backgrounding', - '--disable-setuid-sandbox', - '--disable-speech-api', - '--disable-sync', - '--hide-scrollbars', - '--ignore-gpu-blacklist', - '--metrics-recording-only', - '--no-default-browser-check', - '--no-first-run', - '--no-pings', - '--no-sandbox', - '--no-zygote', - '--password-store=basic', - '--use-gl=swiftshader', - '--use-mock-keychain', - '--disable-web-security', - '--disable-accelerated-2d-canvas', - '--disable-accelerated-jpeg-decoding', - '--disable-features=Translate', - '--disable-features=site-per-process', - '--disable-features=IsolateOrigins', - '--disable-site-isolation-trials', - '--disable-software-rasterizer' - ] - } + logger.info('Connecting to WhatsApp...') + + const { state, saveCreds } = await baileys.useMultiFileAuthState(`./src/temp/${bot.name}`) + const { version, isLatest } = await baileys.fetchLatestBaileysVersion() + logger.info(`Baileys version: v${version.join('.')} (latest: ${isLatest})`) + + socket = baileys.makeWASocket({ + version, + logger: pino({ level: 'fatal' }), + printQRInTerminal: true, + auth: { + creds: state.creds, + keys: baileys.makeCacheableSignalKeyStore(state.keys, logger) + }, + msgRetryCounterCache, + markOnlineOnConnect: true, + browser: ['DeadByte', 'Safari', '3.0'], + generateHighQualityLinkPreview: true, + shouldIgnoreJid: jid => baileys.isJidBroadcast(jid), // TODO: make a stories downloader, + getMessage }) + + store?.bind(socket.ev) + + logger.info('Loading events...', bot) + const events = await fs.readdir('./src/services/events') - // if not in dummy mode, load all events - if (!bot.dummy) { - events.forEach(async event => { - const eventModule = await import(`./services/events/${event}`) - const eventName = snakeCase(event.split('.')[0]) - logger.info(`Loading event ${eventName} from file ${event}`) - client.on(eventName, eventModule.default) + events.forEach(async event => { + if (!bot.doReplies) { + const ignoreEvents = ['call.js', 'messagesUpsert.js'] + if (ignoreEvents.includes(event)) return + } + const eventPath = `services/events/${event}` + const eventName = dotCase(event.split('.')[0]) + logger.trace(`Loading event ${eventName} from file ${event}`) + socket.ev.on(eventName, async (event) => { + const module = await importFresh(eventPath) + module.default(event) }) - } - client.initialize() + }) + socket.ev.on('creds.update', saveCreds) + socket.ev.on('messaging-history.set', async (history) => { + const { chats, contacts, messages, isLatest } = history + logger.info(`Loaded ${chats.length} chats, ${contacts.length} contacts and ${messages.length} messages (latest: ${isLatest})`) + }) + logger.info('Client initialized!') + await db.findCurrentBot(socket) // find the current bot on the database + return socket +} - // if no API KEY, kill the process - if (!apiKey) { - logger.fatal('API_KEY not found! Grab one at https://api.deadbyte.com.br') - process.exit(1) +/** + * Retrieves a message from the store based on the provided key. + * @param {import('@whiskeysockets/baileys').WAMessageKey} key - The key of the message to retrieve. + * @returns {Promise} The retrieved message content, or undefined if not found. + */ +export async function getMessage (key) { + if (store) { + const msg = await store.loadMessage(key.remoteJid, key.id) + return msg?.message || undefined } + + // only if store is present + return baileys.proto.Message.fromObject({}) } // clear terminal @@ -155,8 +172,12 @@ process.stdout.write('\x1B[2J\x1B[0f') // catch unhandled rejections and errors to avoid crashing process.on('unhandledRejection', (err) => { - logger.fatal(err) -}) -process.on('uncaughtException', (err) => { - logger.fatal(err) + // Connection Closed try connectToWhatsApp + if (err.message.includes('Connection Closed')) { + logger.fatal('Connection Closed AAAAAAAAAAA') + console.error(err) + process.exit(0) // kill the process and pm2 will restart it + } else { + logger.fatal(err) + } }) diff --git a/src/logger.js b/src/logger.js index e46362f..4784bc2 100644 --- a/src/logger.js +++ b/src/logger.js @@ -1,29 +1,14 @@ import pino from 'pino' -const transport = pino.transport({ - targets: [ - { - level: 'warn', - target: 'pino/file', - options: { - destination: 'errors.log' - } - }, - { - level: 'trace', - target: 'pino-pretty', - options: {} - } - ] -}) - /** * @type {import('pino').Logger} * @see https://getpino.io/#/ */ export default pino( { - level: 'info' - }, - transport + level: 'info', + transport: { + target: 'pino-pretty' + } + } ) diff --git a/src/meta/message.js b/src/meta/message.js new file mode 100644 index 0000000..47081f7 --- /dev/null +++ b/src/meta/message.js @@ -0,0 +1,342 @@ +import { downloadContentFromMessage } from '@whiskeysockets/baileys' +import messageTypeValidator from '../validators/messageType.js' +import relativeTime from 'dayjs/plugin/relativeTime.js' +import { MessageMedia } from './messageMedia.js' +import spintax from '../utils/spintax.js' +import { getSocket } from '../index.js' +import logger from '../logger.js' +import fetch from 'node-fetch' +import 'dayjs/locale/pt-br.js' +import fs from 'fs/promises' +import dayjs from 'dayjs' + +// +// ================================ Variables ================================= +// +dayjs.locale('pt-br') +dayjs.extend(relativeTime) + +const socket = getSocket() +// +// ================================ Main Functions ================================= +// +/** + * Inject functions into the message object to be drop in replacement for wwebjs + * @param {import('@whiskeysockets/baileys').proto.IWebMessageInfo} msg + */ +const serializeMessage = (msg) => { + const raw = structuredClone(msg) + const newMsgObject = {} + const { type } = messageTypeValidator(msg) + + newMsgObject.type = type + + const berak = Object.keys(msg.message)[0] + newMsgObject.originalType = berak + + const firstItem = msg.message[newMsgObject.originalType] + newMsgObject.body = typeof firstItem === 'string' + ? firstItem + : firstItem.caption || firstItem.text || '' + + newMsgObject.hasQuotedMsg = false + newMsgObject.quotedMsg = firstItem.contextInfo?.quotedMessage?.ephemeralMessage + ? firstItem.contextInfo.quotedMessage.ephemeralMessage.message + : firstItem.contextInfo?.quotedMessage + + if (newMsgObject.quotedMsg) { + newMsgObject.hasQuotedMsg = true + newMsgObject.quotedMsg.id = msg.message[newMsgObject.originalType].contextInfo?.stanzaId + + newMsgObject.quotedMsg.type = Object.keys(newMsgObject.quotedMsg)[0] + const firstItem = newMsgObject.quotedMsg[newMsgObject.quotedMsg.type] + newMsgObject.quotedMsg.hasMedia = Object.keys(firstItem).includes('mediaKey') + newMsgObject.quotedMsg.body = typeof firstItem === 'string' + ? firstItem + : firstItem.caption || firstItem.text || '' + newMsgObject.quotedMsg.sender = msg.message[newMsgObject.originalType].contextInfo?.participant + newMsgObject.quotedMsg.fromMe = newMsgObject.quotedMsg.sender === socket.user.id.split(':')[0] + '@s.whatsapp.net' + } + + try { + const mention = msg.message[msg.originalType].contextInfo.mentionedJid + newMsgObject.mentioned = mention + } catch { + newMsgObject.mentioned = [] + } + + newMsgObject.isGroup = msg.key.remoteJid.endsWith('@g.us') + if (newMsgObject.isGroup) { + newMsgObject.sender = msg.participant + } else { + newMsgObject.sender = msg.key.remoteJid + } + if (msg.key.fromMe) { + newMsgObject.sender = socket.user.id.split(':')[0] + '@s.whatsapp.net' + } + + newMsgObject.isBaileys = msg.key.id.startsWith('BAE5') || msg.key.id.startsWith('3EB0') + + const properties = { + id: msg.key.id, + pushname: msg.pushName, + contact: { + id: newMsgObject.sender || msg.key.participant, + pushname: msg.pushName + }, + author: newMsgObject.isGroup ? msg.key.participant : undefined, + duration: firstItem.seconds, + from: msg.key.remoteJid, + fromMe: msg.key.fromMe, + // ack: undefined, + broadcast: msg.broadcast, + bot: socket.user, + // deviceType: undefined, + fowardScore: firstItem.contextInfo?.forwardingScore, + isForwarded: firstItem.contextInfo?.isForwarded, + hasMedia: Object.keys(firstItem).includes('mediaKey'), + mediaKey: Object.keys(firstItem).includes('mediaKey') ? firstItem.mediaKey : undefined, + // hasReaction: undefined, + inviteV4: type === 'groups_v4_invite' ? firstItem : undefined, + isEphemeral: !!firstItem.contextInfo?.expiration, + isGif: !!firstItem.gifPlayback, + // isStarred: undefined, + // isStatus: undefined, + links: extractLinks(newMsgObject.body), + location: ['location', 'live_location'].includes(type) + ? firstItem + : undefined, + mentionedIds: firstItem.contextInfo?.mentionedJid, + mentionedGroups: firstItem.contextInfo?.groupMentions, + // orderId: undefined, + startedAt: Date.now(), + timestamp: typeof msg.messageTimestamp === 'number' + ? msg.messageTimestamp + : msg.messageTimestamp.toInt(), + timestampIso: dayjs(msg.messageTimestamp * 1000).toISOString(), + // lag is the difference between local time and the time of the sender in ms + // using dayjs to convert + lag: dayjs().diff(dayjs(msg.messageTimestamp * 1000), 'second'), + // to: msg.key.fromMe ? msg.key.remoteJid : botId, + vCards: type === 'multi_vcard' ? firstItem.contacts : type === 'vcard' ? [firstItem] : undefined, + raw + } + + for (const property in properties) { + newMsgObject[property] = properties[property] + } + + const methods = { + react, + reply, + sendSeen, + downloadMedia + } + + for (const method in methods) { + newMsgObject[method] = methods[method].bind(msg) + } + + // remove undefined properties + for (const property in newMsgObject) { + if (newMsgObject[property] === undefined) delete newMsgObject[property] + } + + logger.trace('newMsgObject', newMsgObject) + return newMsgObject +} + +export default serializeMessage + +// +// ================================== Methods ================================== +// +/** + * React to this message with an emoji + * @param {string} reaction Emoji to react with. Send an empty string to remove the reaction. + * @returns {Promise} + */ +async function react (reaction) { + /** + * Message object itself + * @type {import('@whiskeysockets/baileys').proto.WebMessageInfo} + */ + const msg = this + await wait(Math.floor(Math.random() * 1000)) // 0-1000ms delay + await socket.sendMessage(msg.key.remoteJid, { + react: { + text: spintax(reaction), + key: msg.key + } + }) + await wait(Math.floor(Math.random() * 1000)) // 0-1000ms delay +} + +/** + * Sends a message as a reply to this message. If chatId is specified, it will be sent through the specified Chat. If not, it will send the message in the same Chat as the original message was sent. + * @param {string} content The message to send + * @param {string} [chatId] The chat to send the message in + * @param {import('@whiskeysockets/baileys').proto.IMessageOptions} [options] Additional options + * @returns {import('@whiskeysockets/baileys').proto.WebMessageInfo} + */ +async function reply (content, chatId, options) { + const mode = typeof content === 'string' ? 'text' : 'media' + let messageObject = {} + let tempPath = '' + if (mode === 'text') messageObject.text = spintax(content) + if (mode === 'media') { + messageObject = content + + if (messageObject.caption) { + messageObject.caption = spintax(messageObject.caption) + } + + if (messageObject.media) { + const media = messageObject.media + delete messageObject.media + + tempPath = `./src/temp/${media.filename}` + await fs.writeFile(tempPath, media.data, 'base64') + + let type = 'document' + if (media.mimetype.split('/')[0] === 'image') type = 'image' + if (media.mimetype.split('/')[0] === 'video') type = 'video' + if (media.mimetype.split('/')[0] === 'audio') type = 'audio' + + if (type === 'image' && media.mimetype === 'image/webp') { + // if is a webp iamge send as documment + type = 'document' + } + + messageObject[type] = { url: tempPath } + messageObject.mimetype = media.mimetype + messageObject.fileName = media.filename + } + } + /** + * Message object itself + * @type {import('@whiskeysockets/baileys').proto.WebMessageInfo} + */ + const msg = this + + const message = await socket.sendMessage(chatId || msg.key.remoteJid, messageObject, { + quoted: msg, + ephemeralExpiration: msg.message[Object.keys(msg.message)[0]].contextInfo?.expiration || undefined + }) + if (tempPath) await fs.unlink(tempPath) + return message +} + +/** + * Marks this message as seen + * @returns {Promise} + */ +async function sendSeen () { + /** + * Message object itself + * @type {import('@whiskeysockets/baileys').proto.WebMessageInfo} + */ + // const msg = this + // await socket.readMessages([msg.key]) + // temp disable +} + +/** + * Downloads the media of this message + * @returns {Promise} + */ +async function downloadMedia (quoted = false) { + /** + * Message object itself + * @type {import('@whiskeysockets/baileys').proto.WebMessageInfo} + */ + const msg = this + const firstKey = Object.keys(msg.message)[0] + const firstItem = msg.message[firstKey] + const firstKeyFromQuoted = quoted ? Object.keys(firstItem.contextInfo.quotedMessage)[0] : undefined + const firstItemFromQuoted = quoted ? firstItem.contextInfo.quotedMessage[firstKeyFromQuoted] : undefined + const downloadType = quoted + ? firstKeyFromQuoted.replace('Message', '') + : firstKey.replace('Message', '') + + try { + const stream = await downloadContentFromMessage(!quoted ? firstItem : firstItemFromQuoted, downloadType) + let buffer = Buffer.from([]) + for await (const chunk of stream) { + buffer = Buffer.concat([buffer, chunk]) + } + + // const buffer = await downloadMediaMessage(msg, 'buffer', {}, { + // logger, + // reuploadRequest: sock.updateMediaMessage + // }) + const media = await MessageMedia.fromBuffer(buffer) + const metadataSource = quoted ? firstItemFromQuoted : firstItem + media.metadata = { + width: metadataSource.width, + height: metadataSource.height, + ratio: metadataSource.width / metadataSource.height, + duration: metadataSource.seconds + } + return media + } catch (error) { + logger.error('downloadMedia ERROR', { error }) + throw error + } +} + +// +// ================================== Helper Functions ================================== +// +/** + * Extracts valid links from a string + * @param {string} string String to extract links from + * @returns {{link: string, isSuspicious: boolean, isValid: Promise}[]} + */ +function extractLinks (string) { + const regex = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g + + const links = string.match(regex) + + const responseArray = [] + + if (links) { + for (const link of links) { + if (link.includes('@')) continue + // regex with suspicious characters for a url + const suspiciousCharacters = /[<>{}|\\^~\[\]`]/g + const isSuspicious = suspiciousCharacters.test(link) + + const isValid = new Promise((resolve, reject) => { + setTimeout(() => { + fetch('https://' + link, { + method: 'HEAD' + }) + .then(res => resolve(res.ok)) + .catch(error => { + logger.trace('extractLinks ERROR', { error }) + resolve(false) + }) + }, 2000) + }) + + responseArray.push({ + link, + isSuspicious, + isValid + }) + } + } + + if (!responseArray.length) return undefined + return responseArray +} + +/** + * Wait for the given amount of time + * @param {number} ms + * @returns {Promise} + */ +async function wait (ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} diff --git a/src/meta/messageMedia.js b/src/meta/messageMedia.js new file mode 100644 index 0000000..6b1a797 --- /dev/null +++ b/src/meta/messageMedia.js @@ -0,0 +1,70 @@ +import { fileTypeFromBuffer } from 'file-type' +import fetch from 'node-fetch' + +/** + * @class MessageMedia + * @property {string} mimetype - MIME type of the attachment + * @property {string} data - Base64-encoded data of the file + * @property {string|null} [filename] - Document file name. Value can be null + * @property {number|null} [filesize] - Document file size in bytes. Value can be null. + */ +export class MessageMedia { + constructor (mimetype, data, filename, filesize) { + this.mimetype = mimetype + this.data = data + this.filename = filename + this.filesize = filesize + } + + /** + * Creates a MessageMedia instance from a local file path + * @param {string} filePath - The path of the file + * @returns {MessageMedia} - The created MessageMedia instance + */ + static fromFilePath (filePath) { + // implementation here + } + + /** + * Creates a MessageMedia instance from a URL + * @param {string} url - The URL of the media + * @param {MediaFromURLOptions} [options] - The options for the media from URL + * @param {number} [maxRetries=3] - The max number of retries + * @returns {Promise} - The Promise which resolves to a MessageMedia instance + */ + static async fromUrl (url, options, maxRetries = 3) { + let retries = 0 + while (retries < maxRetries) { + try { + const buffer = await fetch(url).then((res) => { + if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`) + return res.buffer() + }) + return MessageMedia.fromBuffer(buffer) + } catch (error) { + if (retries === maxRetries - 1) throw new Error(`Failed to fetch from URL after ${maxRetries} attempts.`) + retries++ + } + } + } + + /** + * Creates a MessageMedia instance from a raw buffer + * @param {Buffer} buffer - The raw buffer + * @returns {Promise} - The Promise which resolves to a MessageMedia instance + */ + static async fromBuffer (buffer) { + const type = await fileTypeFromBuffer(buffer) + + if (!type) { + throw new Error('Unsupported buffer') + } + + return new MessageMedia( + type.mime, + buffer.toString('base64'), + `DeadByte-${Math.random().toString(36).substring(7)}.${type.ext}`, + buffer.byteLength + ) + } +} diff --git a/src/services/commands/groups.js b/src/services/commands/groups.js index ab4d939..042a745 100644 --- a/src/services/commands/groups.js +++ b/src/services/commands/groups.js @@ -5,14 +5,15 @@ */ export default (msg) => { return { - ban: msg.aux.chat.isGroup && /^(ban)$/.test(msg.aux.function), - promote: msg.aux.chat.isGroup && /^(promote|promove|promover)$/.test(msg.aux.function), - demote: msg.aux.chat.isGroup && /^(demote|rebaixa|rebaixar)$/.test(msg.aux.function), - giveaway: msg.aux.chat.isGroup && /^(sorteio|sortear)$/.test(msg.aux.function), - 'giveaway-admins-only': msg.aux.chat.isGroup && /^(sorteioadm|sortearadm)$/.test(msg.aux.function), - 'mark-all-members': msg.aux.chat.isGroup && /^(todos|all|hiddenmention)$/.test(msg.aux.function), - 'call-admins': msg.aux.chat.isGroup && /^(adm|adms|admins)$/.test(msg.aux.function), - 'close-group': msg.aux.chat.isGroup && /^(close|fechar)$/.test(msg.aux.function), - 'open-group': msg.aux.chat.isGroup && /^(open|abrir)$/.test(msg.aux.function) + ban: msg.isGroup && /^(ban)$/.test(msg.aux.function), + unban: msg.isGroup && /^(unban|desban|add)$/.test(msg.aux.function), + promote: msg.isGroup && /^(promote|promove|promover)$/.test(msg.aux.function), + demote: msg.isGroup && /^(demote|rebaixa|rebaixar)$/.test(msg.aux.function), + giveaway: msg.isGroup && /^(sorteio|sortear)$/.test(msg.aux.function), + 'giveaway-admins-only': msg.isGroup && /^(sorteioadm|sortearadm)$/.test(msg.aux.function), + 'mark-all-members': msg.isGroup && /^(todos|all|hiddenmention)$/.test(msg.aux.function), + 'call-admins': msg.isGroup && /^(adm|adms|admins)$/.test(msg.aux.function), + 'close-group': msg.isGroup && /^(close|fechar)$/.test(msg.aux.function), + 'open-group': msg.isGroup && /^(open|abrir)$/.test(msg.aux.function) } } diff --git a/src/services/commands/meta.js b/src/services/commands/meta.js new file mode 100644 index 0000000..2b2d365 --- /dev/null +++ b/src/services/commands/meta.js @@ -0,0 +1,11 @@ +/** + * Miscelanius Bot Commands + * @param {import('../../types.d.ts').WWebJSMessage} msg + * @returns {Object} + */ +export default (msg) => { + return { + activate: /^(ativa|ativar|active|activate)$/.test(msg.aux.function), + set: /^(set|definir|defina)$/.test(msg.aux.function) + } +} diff --git a/src/services/commands/miscellaneous.js b/src/services/commands/miscellaneous.js index 50815a9..ca3c826 100644 --- a/src/services/commands/miscellaneous.js +++ b/src/services/commands/miscellaneous.js @@ -8,7 +8,7 @@ export default (msg) => { uptime: /^(uptime|online|up|tempo)$/.test(msg.aux.function), react: /^(react|reacao)$/.test(msg.aux.function) || msg.aux.function === '', dice: /^\d*d\d+([\+\-\*\/]\d+)?$/.test(msg.aux.function), - toFile: /^(tofile|file|arquivo|imagem|img|togif|image)$/.test(msg.aux.function), + toFile: /^(tofile|revert|file|arquivo|imagem|img|togif|image)$/.test(msg.aux.function), toUrl: /^(tourl|url)$/.test(msg.aux.function), ping: /^(ping|pong)$/.test(msg.aux.function), speak: /^(speak|fale|falar|voz|diga|dizer|fala)\d?$/.test(msg.aux.function), diff --git a/src/services/commands/tools.js b/src/services/commands/tools.js index 716a67e..fe52152 100644 --- a/src/services/commands/tools.js +++ b/src/services/commands/tools.js @@ -5,7 +5,7 @@ */ export default (msg) => { return { - 'qr-reader': /^(qrl|lerqr|readqr)$/.test(msg.aux.function) || (/^(qr)$/.test(msg.aux.function) && (msg.hasMedia || msg.aux.quotedMsg?.hasMedia)), + 'qr-reader': /^(qrl|lerqr|readqr)$/.test(msg.aux.function) || (/^(qr)$/.test(msg.aux.function) && (msg.hasMedia || msg.quotedMsg?.hasMedia)), 'qr-image-creator': /^(qr|qrimg|createqr)$/.test(msg.aux.function), 'qr-text-creator': /^(qrt|qrtexto|textqr)$/.test(msg.aux.function) } diff --git a/src/services/events.old/call.js b/src/services/events.old/call.js new file mode 100644 index 0000000..4439263 --- /dev/null +++ b/src/services/events.old/call.js @@ -0,0 +1,57 @@ +import logger from '../../logger.js' +import { getClient } from '../../index.js' +import spintax from '../../utils/spintax.js' + +// user is key, value is an object with the time of the warning and the number of warnings +const warnings = {} +const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms)) + +/** + * Emitted when a call is received + * @param {import('whatsapp-web.js').Call} call + * https://docs.wwebjs.dev/Client.html#event:incoming_call + */ + +export default async (call) => { + const emoji = '📞' + logger.info(`${emoji} - Incoming call`, call) + await call.reject() + + if (call.isGroup) return // Ignore group calls + + const client = getClient() + + if (!warnings[call.from]) { + warnings[call.from] = { + time: Date.now(), + count: 1 + } + let message = '⚠️ - ' + message += '{Por favor, não ligue|Por favor, evite ligar|Peço que não ligue} para o bot!\n' + message += ' {Desculpe|Peço desculpas} se {você ligou por|se foi} engano, {irei|vou} {relevar|deixar passar|não irei fazer nada} {desta|dessa} vez, ' + message += '{mas|porém} {da|na} próxima vez, você {será bloqueado(a)|levará block}!' + return await client.sendMessage(call.from, spintax(message)) + } + + if (warnings[call.from].count === 1) { + warnings[call.from].count++ + let message = '🚨 - ' + message += '{{Já|Eu já} {te|lhe} avisei uma vez|Você já foi avisado(a)|Mais uma vez}, não ligue para o bot!!!\n' + message += '\n{{Este|Esse} é o seu *último aviso*|Essa é a sua *última chance*}, {da próxima vez|na próxima|se ligar novamente} *você {será bloqueado|levará block}*!' + return await client.sendMessage(call.from, spintax(message)) + } + + if (warnings[call.from].count === 2) { + warnings[call.from].count++ + let message = '🚫 - ' + message += '{Atenção!|Você} {foi *bloqueado*|levou um *block*}!\n' + message += '{Se|Caso} você {acha|acredita|acredite} que {foi {um|algum}|tenha ocorrido algum} {erro|engano|equívoco} ' + message += 'entre em contato com o desenvolvedor do bot' + await client.sendMessage(call.from, spintax(message)) + await wait(5000) // wait 5 seconds to garantee the message is sent before blocking + + warnings[call.from] = undefined // remove from warnings + const contact = await client.getContactById(call.from) + await contact.block() + } +} diff --git a/src/services/events/message.js b/src/services/events.old/message.js similarity index 100% rename from src/services/events/message.js rename to src/services/events.old/message.js diff --git a/src/services/events/qr.js b/src/services/events.old/qr.js similarity index 100% rename from src/services/events/qr.js rename to src/services/events.old/qr.js diff --git a/src/services/events/ready.js b/src/services/events.old/ready.js similarity index 100% rename from src/services/events/ready.js rename to src/services/events.old/ready.js diff --git a/src/services/events/call.js b/src/services/events/call.js index 4439263..ad010f6 100644 --- a/src/services/events/call.js +++ b/src/services/events/call.js @@ -1,5 +1,5 @@ import logger from '../../logger.js' -import { getClient } from '../../index.js' +import { getSocket } from '../../index.js' import spintax from '../../utils/spintax.js' // user is key, value is an object with the time of the warning and the number of warnings @@ -7,51 +7,50 @@ const warnings = {} const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms)) /** - * Emitted when a call is received - * @param {import('whatsapp-web.js').Call} call - * https://docs.wwebjs.dev/Client.html#event:incoming_call + * Receive an update on a call, including when the call was received, rejected, accepted + * @param {import('@whiskeysockets/baileys').BaileysEventMap['call']} event */ - -export default async (call) => { +export default async (event) => { const emoji = '📞' - logger.info(`${emoji} - Incoming call`, call) - await call.reject() - - if (call.isGroup) return // Ignore group calls - - const client = getClient() - - if (!warnings[call.from]) { - warnings[call.from] = { - time: Date.now(), - count: 1 + const call = event[0] + if (call.status === 'offer') { + logger.info(`${emoji} - Incoming call\n` + JSON.stringify(call, null, 2)) + const sock = getSocket() + await sock.rejectCall(call.id, call.from) + + if (call.isGroup) return // Ignore group calls + + if (!warnings[call.from]) { + warnings[call.from] = { + time: Date.now(), + count: 1 + } + let message = '⚠️ - ' + message += '{Por favor, não ligue|Por favor, evite ligar|Peço que não ligue} para o bot!\n' + message += ' {Desculpe|Peço desculpas} se {você ligou por|se foi} engano, {irei|vou} {relevar|deixar passar|não irei fazer nada} {desta|dessa} vez, ' + message += '{mas|porém} {da|na} próxima vez, você {será bloqueado(a)|levará block}!' + return await sock.sendMessage(call.from, { text: spintax(message) }) } - let message = '⚠️ - ' - message += '{Por favor, não ligue|Por favor, evite ligar|Peço que não ligue} para o bot!\n' - message += ' {Desculpe|Peço desculpas} se {você ligou por|se foi} engano, {irei|vou} {relevar|deixar passar|não irei fazer nada} {desta|dessa} vez, ' - message += '{mas|porém} {da|na} próxima vez, você {será bloqueado(a)|levará block}!' - return await client.sendMessage(call.from, spintax(message)) - } - if (warnings[call.from].count === 1) { - warnings[call.from].count++ - let message = '🚨 - ' - message += '{{Já|Eu já} {te|lhe} avisei uma vez|Você já foi avisado(a)|Mais uma vez}, não ligue para o bot!!!\n' - message += '\n{{Este|Esse} é o seu *último aviso*|Essa é a sua *última chance*}, {da próxima vez|na próxima|se ligar novamente} *você {será bloqueado|levará block}*!' - return await client.sendMessage(call.from, spintax(message)) - } - - if (warnings[call.from].count === 2) { - warnings[call.from].count++ - let message = '🚫 - ' - message += '{Atenção!|Você} {foi *bloqueado*|levou um *block*}!\n' - message += '{Se|Caso} você {acha|acredita|acredite} que {foi {um|algum}|tenha ocorrido algum} {erro|engano|equívoco} ' - message += 'entre em contato com o desenvolvedor do bot' - await client.sendMessage(call.from, spintax(message)) - await wait(5000) // wait 5 seconds to garantee the message is sent before blocking + if (warnings[call.from].count === 1) { + warnings[call.from].count++ + let message = '🚨 - ' + message += '{{Já|Eu já} {te|lhe} avisei uma vez|Você já foi avisado(a)|Mais uma vez}, não ligue para o bot!!!\n' + message += '\n{{Este|Esse} é o seu *último aviso*|Essa é a sua *última chance*}, {da próxima vez|na próxima|se ligar novamente} *você {será bloqueado|levará block}*!' + return await sock.sendMessage(call.from, { text: spintax(message) }) + } - warnings[call.from] = undefined // remove from warnings - const contact = await client.getContactById(call.from) - await contact.block() + if (warnings[call.from].count === 2) { + warnings[call.from].count++ + let message = '🚫 - ' + message += '{Atenção!|Você} {foi *bloqueado*|levou um *block*}!\n' + message += '{Se|Caso} você {acha|acredita|acredite} que {foi {um|algum}|tenha ocorrido algum} {erro|engano|equívoco} ' + message += 'entre em contato com o desenvolvedor do bot' + await sock.sendMessage(call.from, { text: spintax(message) }) + await wait(5000) // wait 5 seconds to garantee the message is sent before blocking + + warnings[call.from] = undefined // remove from warnings + await sock.updateBlockStatus(call.from, 'block') + } } } diff --git a/src/services/events/connectionUpdate.js b/src/services/events/connectionUpdate.js new file mode 100644 index 0000000..0bf8ce0 --- /dev/null +++ b/src/services/events/connectionUpdate.js @@ -0,0 +1,20 @@ +import logger from '../../logger.js' +import { DisconnectReason } from '@whiskeysockets/baileys' +import { connectToWhatsApp } from '../../index.js' + +/** + * Connection state has been updated -- WS closed, opened, connecting etc. + * @param {import('@whiskeysockets/baileys').BaileysEventMap['connection.update']} update + */ +export default async (update) => { + logger.trace('Connection updated\n' + JSON.stringify(update)) + if (global.qr !== update.qr) { + global.qr = update.qr + } + const { connection, lastDisconnect } = update + if (connection === 'close') { + lastDisconnect.error?.output?.statusCode !== DisconnectReason.loggedOut + ? connectToWhatsApp() + : logger.fatal('connection logged out...') + } +} diff --git a/src/services/events/messagesUpdate.js b/src/services/events/messagesUpdate.js new file mode 100644 index 0000000..e5b8aff --- /dev/null +++ b/src/services/events/messagesUpdate.js @@ -0,0 +1,25 @@ +import logger from '../../logger.js' +import { getAggregateVotesInPollMessage } from '@whiskeysockets/baileys' +import { getMessage } from '../../index.js' + +/** + * Connection state has been updated -- WS closed, opened, connecting etc. + * @param {import('@whiskeysockets/baileys').BaileysEventMap['messages.update']} event + */ +export default async (event) => { + logger.trace('Messages updated\n' + JSON.stringify(event, null, 2)) + for (const { key, update } of event) { + if (update.pollUpdates) { + const pollCreation = await getMessage(key) + if (pollCreation) { + console.log( + 'got poll update, aggregation: ', + getAggregateVotesInPollMessage({ + message: pollCreation, + pollUpdates: update.pollUpdates + }) + ) + } + } + } +} diff --git a/src/services/events/messagesUpsert.js b/src/services/events/messagesUpsert.js new file mode 100644 index 0000000..f488cdd --- /dev/null +++ b/src/services/events/messagesUpsert.js @@ -0,0 +1,95 @@ +import importFresh from '../../utils/importFresh.js' +import { saveActionToDB } from '../../db.js' +import { getSocket } from '../../index.js' +import { addToQueue } from '../queue.js' +import logger from '../../logger.js' +// +// ================================ Variables ================================= +// + +// +// ================================ Main Function ============================= +// +/** + * Add/update the given messages. If they were received while the connection was online, the update will have type: "notify" + * @param {import('@whiskeysockets/baileys').BaileysEventMap['messages.upsert']} upsert + */ +export default async (upsert) => { + // console.log('messages.upsert\n' + JSON.stringify(upsert, null, 2)) + logger.trace('messages.upsert\n' + JSON.stringify(upsert, null, 2)) + if (upsert.type === 'append') return // TODO: handle unread messages + + for (let msg of upsert.messages) { + if (msg.key.fromMe) return // ignore self messages + + const meta = await importFresh('meta/message.js') + msg = meta.default(msg) + + if (msg.type === 'revoked') { + // TODO: send random "Deus viu o que você apagou" sticker + return + } + if (msg.type === 'edited') { + await msg.sendSeen() + return await msg.react('✏️') + } + + // const socket = getSocket() + // await socket.sendPresenceUpdate('available') + const messageParser = await importFresh('validators/message.js') + const handlerModule = await messageParser.default(msg) + logger.trace('handlerModule: ', handlerModule) + + if (!handlerModule) return logger.debug('handlerModule is undefined') + + try { + msg.aux.db = await saveActionToDB(handlerModule.type, handlerModule.command, msg) + } catch (error) { + logger.trace('Error saving action to DB', error) + } + + // TODO: improve bot vip system + // const vipBots = ['DeadByte - 5852', 'DeadByte - 7041', 'DeadByte - VIP'] + // if (!msg.isGroup && vipBots.includes(msg.bot.name) && msg.aux.db) { + if (!msg.isGroup && msg.aux.db) { + const sender = msg.aux.db.contact.attributes + if (!sender.queue?.data && msg.aux.db.command.slug !== 'activate') { + console.warn(`⛔ - ${msg.from} - ${handlerModule.command} - Not queued`) + return // user not passed through the queue + } + // const hasDonated = sender?.hasDonated === true + // if (!hasDonated) { + // // await msg.react('💎') + // // let message = '❌ - Você não é um VIP! 😢\n\n' + // // message += 'Desculpe, não localizei nenhuma doação em seu nome.\n\n' + // // message += '*Se isso for um erro ou se você deseja se tornar um VIP, entre em contato no grupo de suporte:*\n' + // // message += 'https://chat.whatsapp.com/CBlkOiMj4fM3tJoFeu2WpR' + // // await msg.reply(message) + + // // // wait 3 seconds and block the user + // // setTimeout(async () => { + // // await socket.updateBlockStatus(msg.from, 'block') + // // }, 5000) + + // return + // } + } + + const checkDisabled = await importFresh('validators/checkDisabled.js') + const isEnabled = await checkDisabled.default(msg) + if (!isEnabled) return logger.info(`⛔ - ${msg.from} - ${handlerModule.command} - Disabled`) + + const checkOwnerOnly = await importFresh('validators/checkOwnerOnly.js') + const isOwnerOnly = await checkOwnerOnly.default(msg) + if (isOwnerOnly) return logger.info(`🛂 - ${msg.from} - ${handlerModule.command} - Restricted to admins`) + + // TODO: implement queue system + const moduleName = handlerModule.type + const functionName = handlerModule.command + await addToQueue(moduleName, functionName, msg) + // const { isSpam, messagesOnQueue } = await addToQueue(moduleName, functionName, msg) + // if (isSpam) return logger.warn(`${msg.from} - ${handlerModule.command} - Spam detected`) + // if (messagesOnQueue > 1) return logger.info(`${msg.from} - ${handlerModule.command} - Queued`) + // logger.info(`${msg.from} - ${handlerModule.command} - Processing`) + } +} diff --git a/src/services/functions/admin-commands.js b/src/services/functions/admin-commands.js index 9535b1e..78838fa 100644 --- a/src/services/functions/admin-commands.js +++ b/src/services/functions/admin-commands.js @@ -14,13 +14,8 @@ dayjs.extend(relativeTime) * @param {import('../../types').WWebJSMessage} msg */ export async function debug (msg) { - const debugEmoji = '🐛' - await msg.react(debugEmoji) + // Debug code goes here - const announceGroup = '120363094244463491@g.us' - const chat = await msg.aux.client.getChatById(announceGroup) - const admins = chat.participants.filter(p => p.isAdmin || p.isSuperAdmin).map((p) => p.id._serialized) - const botIsAdmin = admins.includes(msg.aux.me) - - await msg.reply(JSON.stringify(botIsAdmin, null, 2)) + // Then react to the message + await msg.react(msg.aux.db.command.emoji) } diff --git a/src/services/functions/artificial-intelligence.js b/src/services/functions/artificial-intelligence.js index c236ceb..fcdfef6 100644 --- a/src/services/functions/artificial-intelligence.js +++ b/src/services/functions/artificial-intelligence.js @@ -1,50 +1,50 @@ -import reactions from '../../config/reactions.js' -import { createUrl } from '../../config/api.js' -import fetch from 'node-fetch' +// import reactions from '../../config/reactions.js' +// import { createUrl } from '../../config/api.js' +// import fetch from 'node-fetch' /** * Use chat gpt * @param {import('../../types.d.ts').WWebJSMessage} msg */ export async function gpt (msg) { - await msg.react(reactions.wait) - if (!msg.body) { - return msg.reply('Para utilizar o *!gpt* mande uma mensagem junto com o comando.') - } - - const messages = msg.aux.history.map(msg => { - return { - role: msg._data.self === 'out' ? 'assistant' : 'user', - content: msg.body - } - }) - - await msg.aux.chat.sendStateTyping() - const url = await createUrl('artificial-intelligence', 'gpt', {}) - // POST request to the API with messages on json body - try { - const timeout = setTimeout(() => { - throw new Error('Timeout') - }, 30_000) - - const res = await fetch(url, { - method: 'POST', - body: JSON.stringify({ messages }), - headers: { 'Content-Type': 'application/json' } - }) - - clearTimeout(timeout) - - const data = await res.json() - - await msg.reply(data.result) - await msg.aux.chat.clearState() - await msg.react('🧠') - } catch (error) { - await msg.reply('❌ - Aconteceu um erro inesperado, tente novamente mais tarde.\nznSe possivel, reporte o erro para o desenvolvedor no grupo:\nhttps://chat.whatsapp.com/CBlkOiMj4fM3tJoFeu2WpR') - await msg.aux.chat.clearState() - await msg.react('❌') - } + // await msg.react(reactions.wait) + // if (!msg.body) { + // return msg.reply('Para utilizar o *!gpt* mande uma mensagem junto com o comando.') + // } + + // const messages = msg.aux.history.map(msg => { + // return { + // role: msg._data.self === 'out' ? 'assistant' : 'user', + // content: msg.body + // } + // }) + + // await msg.aux.chat.sendStateTyping() + // const url = await createUrl('artificial-intelligence', 'gpt', {}) + // // POST request to the API with messages on json body + // try { + // const timeout = setTimeout(() => { + // throw new Error('Timeout') + // }, 30_000) + + // const res = await fetch(url, { + // method: 'POST', + // body: JSON.stringify({ messages }), + // headers: { 'Content-Type': 'application/json' } + // }) + + // clearTimeout(timeout) + + // const data = await res.json() + + // await msg.reply(data.result) + // await msg.aux.chat.clearState() + // await msg.react(msg.aux.db.command.emoji) + // } catch (error) { + // await msg.reply('❌ - Aconteceu um erro inesperado, tente novamente mais tarde.\nznSe possivel, reporte o erro para o desenvolvedor no grupo:\nhttps://chat.whatsapp.com/CBlkOiMj4fM3tJoFeu2WpR') + // await msg.aux.chat.clearState() + // await msg.react(reactions.error) + // } } /** @@ -52,44 +52,44 @@ export async function gpt (msg) { * @param {import('../../types.d.ts').WWebJSMessage} msg */ export async function bot (msg) { - await msg.react(reactions.wait) - if (!msg.body) { - return msg.reply('Para utilizar o *!gpt* mande uma mensagem junto com o comando.') - } - - const messages = msg.aux.history.map(msg => { - return { - role: msg._data.self === 'out' ? 'assistant' : 'user', - content: msg.body - } - }) - - await msg.aux.chat.sendStateTyping() - const url = await createUrl('artificial-intelligence', 'bot', {}) - // POST request to the API with messages on json body - try { - const timeout = setTimeout(() => { - throw new Error('Timeout') - }, 30_000) - - const res = await fetch(url, { - method: 'POST', - body: JSON.stringify({ messages }), - headers: { 'Content-Type': 'application/json' } - }) - - clearTimeout(timeout) - - const data = await res.json() - - await msg.reply(data.result) - await msg.aux.chat.clearState() - await msg.react('🧠') - } catch (error) { - await msg.reply('❌ - Aconteceu um erro inesperado, tente novamente mais tarde.\nznSe possivel, reporte o erro para o desenvolvedor no grupo:\nhttps://chat.whatsapp.com/CBlkOiMj4fM3tJoFeu2WpR') - await msg.aux.chat.clearState() - await msg.react('❌') - } + // await msg.react(reactions.wait) + // if (!msg.body) { + // return msg.reply('Para utilizar o *!gpt* mande uma mensagem junto com o comando.') + // } + + // const messages = msg.aux.history.map(msg => { + // return { + // role: msg._data.self === 'out' ? 'assistant' : 'user', + // content: msg.body + // } + // }) + + // await msg.aux.chat.sendStateTyping() + // const url = await createUrl('artificial-intelligence', 'bot', {}) + // // POST request to the API with messages on json body + // try { + // const timeout = setTimeout(() => { + // throw new Error('Timeout') + // }, 30_000) + + // const res = await fetch(url, { + // method: 'POST', + // body: JSON.stringify({ messages }), + // headers: { 'Content-Type': 'application/json' } + // }) + + // clearTimeout(timeout) + + // const data = await res.json() + + // await msg.reply(data.result) + // await msg.aux.chat.clearState() + // await msg.react(msg.aux.db.command.emoji) + // } catch (error) { + // await msg.reply('❌ - Aconteceu um erro inesperado, tente novamente mais tarde.\nznSe possivel, reporte o erro para o desenvolvedor no grupo:\nhttps://chat.whatsapp.com/CBlkOiMj4fM3tJoFeu2WpR') + // await msg.aux.chat.clearState() + // await msg.react(reactions.error) + // } } /** @@ -97,35 +97,35 @@ export async function bot (msg) { * @param {import('../../types.d.ts').WWebJSMessage} msg */ export async function emojify (msg) { - await msg.react(reactions.wait) + // await msg.react(reactions.wait) - if (!msg.body && !msg.hasQuotedMsg) return msg.reply('Para utilizar o *!emojify* mande uma mensagem junto com o comando.\nOu responda a uma mensagem com o comando.') + // if (!msg.body && !msg.hasQuotedMsg) return msg.reply('Para utilizar o *!emojify* mande uma mensagem junto com o comando.\nOu responda a uma mensagem com o comando.') - const messages = [ - { - role: 'user', - content: msg.hasQuotedMsg ? msg.aux.quotedMsg.body : msg.body - } - ] + // const messages = [ + // { + // role: 'user', + // content: msg.hasQuotedMsg ? msg.aux.quotedMsg.body : msg.body + // } + // ] - const prompt = { - role: 'system', - content: 'I want you to translate the sentences I wrote into emojis. The use will write the sentence, and you will express it with emojis. I just want you to express it with emojis. I don\'t want you to reply with anything but emoji tranlation' - } + // const prompt = { + // role: 'system', + // content: 'I want you to translate the sentences I wrote into emojis. The use will write the sentence, and you will express it with emojis. I just want you to express it with emojis. I don\'t want you to reply with anything but emoji tranlation' + // } - messages.unshift(prompt) // Add prompt object at the beginning of messages array + // messages.unshift(prompt) // Add prompt object at the beginning of messages array - const completion = await openai.chat.completions.create({ - model: 'gpt-3.5-turbo', - max_tokens: 4096 / 8, - temperature: 0, - messages - }) + // const completion = await openai.chat.completions.create({ + // model: 'gpt-3.5-turbo', + // max_tokens: 4096 / 8, + // temperature: 0, + // messages + // }) - const response = completion.choices[0]?.message?.content + // const response = completion.choices[0]?.message?.content - await msg.reply(response) - await msg.react('😀') + // await msg.reply(response) + // await msg.react(msg.aux.db.command.emoji) } /** @@ -133,40 +133,40 @@ export async function emojify (msg) { * @param {import('../../types.d.ts').WWebJSMessage} msg */ export async function translate (msg) { - await msg.react(reactions.wait) - const prompt = { - role: 'system', - content: `Returns the sentence translated into the output language. - - Automatically detect the input language. - The output language will be english if the input language is portuguese, and portuguese if the input language is english. - Except if the user specify the output language, saying something like "translate es" or "translate chinese "something"". - - Do not interact with the user, just return the translations of what the user said. - Localize the translations, adapt slang and other things to the language feel natural. - - Prefix the response with a flag representing th output language, like "🇪🇸" or or "🇧🇷" or "🇺🇸" etc.. - Example: '🇪🇸 - "Hola, ¿cómo estás?"' or '🇧🇷 - "Oi, tudo bem?"' or '🇺🇸 - "Hi, how are you?"' - ` - } - - const messageToTranslate = msg.hasQuotedMsg ? msg.aux.quotedMsg.body : msg.body - - const messages = [prompt, { - role: 'user', - content: `translate ${msg.body ? msg.body + ' ' : ''}"${messageToTranslate}"` - }] - - const completion = await openai.chat.completions.create({ - model: 'gpt-3.5-turbo', - max_tokens: 4096 / 4, - temperature: 0, - messages - }) - - const response = completion.choices[0]?.message?.content - await msg.reply(response) - await msg.react('🌐') + // await msg.react(reactions.wait) + // const prompt = { + // role: 'system', + // content: `Returns the sentence translated into the output language. + + // Automatically detect the input language. + // The output language will be english if the input language is portuguese, and portuguese if the input language is english. + // Except if the user specify the output language, saying something like "translate es" or "translate chinese "something"". + + // Do not interact with the user, just return the translations of what the user said. + // Localize the translations, adapt slang and other things to the language feel natural. + + // Prefix the response with a flag representing th output language, like "🇪🇸" or or "🇧🇷" or "🇺🇸" etc.. + // Example: '🇪🇸 - "Hola, ¿cómo estás?"' or '🇧🇷 - "Oi, tudo bem?"' or '🇺🇸 - "Hi, how are you?"' + // ` + // } + + // const messageToTranslate = msg.hasQuotedMsg ? msg.aux.quotedMsg.body : msg.body + + // const messages = [prompt, { + // role: 'user', + // content: `translate ${msg.body ? msg.body + ' ' : ''}"${messageToTranslate}"` + // }] + + // const completion = await openai.chat.completions.create({ + // model: 'gpt-3.5-turbo', + // max_tokens: 4096 / 4, + // temperature: 0, + // messages + // }) + + // const response = completion.choices[0]?.message?.content + // await msg.reply(response) + // await msg.react(msg.aux.db.command.emoji) } /** @@ -174,44 +174,44 @@ export async function translate (msg) { * @param {import('../../types.d.ts').WWebJSMessage} msg */ export async function calculate (msg) { - await msg.react(reactions.wait) - - const messages = msg.aux.history.map(msg => { - return { - role: msg._data.self === 'out' ? 'assistant' : 'user', - content: msg.body - } - }) - - const prompt = { - role: 'system', - content: `I want you to act like a mathematician - I will type mathematical expressions and you will respond with the result of calculating the expression - I want you to answer the line by line calculations - Do not write explanations - Always wrap the result in * like *18* or *x = 2* - If you need to explain something, always do it in portuguese, but avoid it if possible - - Example: - 2 + 2 * 8 - 2 + (2 * 8) - 2 + 16 - ---------- - *18* - ` - } - - messages.unshift(prompt) // Add prompt object at the beginning of messages array - - const completion = await openai.chat.completions.create({ - model: 'gpt-3.5-turbo', - max_tokens: 4096 / 8, - temperature: 0, - messages - }) - - const response = completion.choices[0]?.message?.content - - await msg.reply(response) - await msg.react('😀') + // await msg.react(reactions.wait) + + // const messages = msg.aux.history.map(msg => { + // return { + // role: msg._data.self === 'out' ? 'assistant' : 'user', + // content: msg.body + // } + // }) + + // const prompt = { + // role: 'system', + // content: `I want you to act like a mathematician + // I will type mathematical expressions and you will respond with the result of calculating the expression + // I want you to answer the line by line calculations + // Do not write explanations + // Always wrap the result in * like *18* or *x = 2* + // If you need to explain something, always do it in portuguese, but avoid it if possible + + // Example: + // 2 + 2 * 8 + // 2 + (2 * 8) + // 2 + 16 + // ---------- + // *18* + // ` + // } + + // messages.unshift(prompt) // Add prompt object at the beginning of messages array + + // const completion = await openai.chat.completions.create({ + // model: 'gpt-3.5-turbo', + // max_tokens: 4096 / 8, + // temperature: 0, + // messages + // }) + + // const response = completion.choices[0]?.message?.content + + // await msg.reply(response) + // await msg.react(msg.aux.db.command.emoji) } diff --git a/src/services/functions/groups.js b/src/services/functions/groups.js index 3f80683..3dba3a9 100644 --- a/src/services/functions/groups.js +++ b/src/services/functions/groups.js @@ -1,11 +1,16 @@ import spintax from '../../utils/spintax.js' +import { getSocket } from '../../index.js' +const socket = getSocket() /** * Ban user from group * @param {import('../../types.d.ts').WWebJSMessage} msg */ export async function ban (msg) { - if (!msg.hasQuotedMsg) { - return await msg.reply('para usar o !ban você precisa responder a mensagem da pessoa que deseja banir') + const hasMentions = msg.aux.mentions.length > 0 + const hasQuotedMsg = msg.hasQuotedMsg + + if (!hasMentions && !hasQuotedMsg) { + return await msg.reply('para usar o !ban você precisa responder a mensagem da pessoa que deseja banir ou *mensionar* o @ da pessoa que deseja banir') } if (!msg.aux.isBotAdmin) { @@ -16,11 +21,44 @@ export async function ban (msg) { return await msg.reply('para usar o !ban *você* precisa ser admin') } - const quotedMsg = await msg.getQuotedMessage() - const author = await quotedMsg.getContact() + let target = [] + if (hasMentions) { target.push(...msg.aux.mentions) } + if (hasQuotedMsg) { target.push(await msg.quotedMsg.sender) } + const admins = msg.aux.admins + + target = target.filter((t) => !admins.includes(t)) + if (target.length === 0) { + await msg.react('🤡') + return await msg.reply('Desculpe, mas eu não posso banir administradores') + } - await msg.aux.chat.removeParticipants([author.id._serialized]) - await msg.react('🔨') + await socket.groupParticipantsUpdate(msg.from, target, 'remove') + await msg.react(msg.aux.db.command.emoji) +} + +export async function unban (msg) { + // TODO: Verify if can add to avoid ban + const hasMentions = msg.aux.mentions.length > 0 + const hasQuotedMsg = msg.hasQuotedMsg + + if (!hasMentions && !hasQuotedMsg) { + return await msg.reply('para usar o !unban você precisa responder a mensagem da pessoa que deseja banir ou *mensionar* o @ da pessoa que deseja banir') + } + + if (!msg.aux.isBotAdmin) { + return await msg.reply('para usar o !unban *o bot* precisa ser admin') + } + + if (!msg.aux.isSenderAdmin) { + return await msg.reply('para usar o !unban *você* precisa ser admin') + } + + const target = [] + if (hasMentions) { target.push(...msg.aux.mentions) } + if (hasQuotedMsg) { target.push(await msg.quotedMsg.sender) } + + await socket.groupParticipantsUpdate(msg.from, target, 'add') + await msg.react(msg.aux.db.command.emoji) } /** @@ -39,9 +77,8 @@ export async function promote (msg) { if (!msg.aux.isSenderAdmin) { return await msg.reply('para usar o !promove *você* precisa ser admin') } - - await msg.aux.chat.promoteParticipants(msg.aux.mentions) - await msg.react('↗️') + await socket.groupParticipantsUpdate(msg.from, msg.aux.mentions, 'promote') + await msg.react(msg.aux.db.command.emoji) } /** @@ -49,8 +86,8 @@ export async function promote (msg) { * @param {import('../../types.d.ts').WWebJSMessage} msg */ export async function demote (msg) { - if (!msg.aux.mentions.length === 0) { - return await msg.reply('para usar o !demote você precisa *mensionar* o @ da pessoa que deseja rebaixar') + if (msg.aux.mentions.length === 0) { + return await msg.reply('Para usar o !demote você precisa *mensionar* o @ da pessoa que deseja rebaixar') } if (!msg.aux.isBotAdmin) { @@ -61,8 +98,8 @@ export async function demote (msg) { return await msg.reply('para usar o !demote *você* precisa ser admin') } - await msg.aux.chat.demoteParticipants(msg.aux.mentions) - await msg.react('↘️') + await socket.groupParticipantsUpdate(msg.from, msg.aux.mentions, 'demote') + await msg.react(msg.aux.db.command.emoji) } /** @@ -73,21 +110,17 @@ export async function giveaway (msg) { const hasText = !!msg.body.trim() let participants = await msg.aux.participants - const botId = msg.aux.client.info.wid._serialized - participants = participants.filter((p) => p.id._serialized !== botId) + const botId = msg.aux.me + participants = participants.filter((p) => p.id !== botId) const random = Math.floor(Math.random() * (participants.length)) const winner = participants[random] - const winnerContact = await msg.aux.client.getContactById(winner.id._serialized) - - let message = `{🎉|🎊|🥳|✨|🌟} - {@${winnerContact.id.user} parabéns| {Meus p|P}arabéns @${winnerContact.id.user}}! {Você|Tu|Vc} {ganhou|venceu|acaba de ganhar} o {incrível |super |magnífico |maravilhoso |fantástico |excepcional |}{sorteio|concurso|prêmio}` + let message = `{🎉|🎊|🥳|✨|🌟} - {@${winner.id.split('@')[0]} parabéns|Meus parabéns @${winner.id.split('@')[0]}}! {Você|Tu|Vc} {ganhou|venceu|acaba de ganhar} o {incrível |super |magnífico |maravilhoso |fantástico |excepcional |}{sorteio|concurso|prêmio}` message = hasText ? `${message} de *${msg.body.trim()}*!` : message + '!' - await msg.aux.chat.sendMessage(spintax(message), { - mentions: [winnerContact] - }) + await socket.sendMessage(msg.from, { text: spintax(message), mentions: [winner.id] }, { ephemeralExpiration: msg.raw.message[Object.keys(msg.raw.message)[0]].contextInfo?.expiration || undefined }) - await msg.react(spintax('{🎉|🎊|🥳}')) + await msg.react(msg.aux.db.command.emoji) } /** @@ -98,22 +131,18 @@ export async function giveawayAdminsOnly (msg) { const hasText = msg.body.split(' ').length > 1 const text = hasText ? msg.body : '' - let participants = await msg.aux.participants.filter((p) => p.isAdmin || p.isSuperAdmin) - const botId = msg.aux.client.info.wid._serialized - participants = participants.filter((p) => p.id._serialized !== botId) + let participants = await msg.aux.participants.filter((p) => p.admin) + const botId = msg.aux.me + participants = participants.filter((p) => p.id !== botId) const random = Math.floor(Math.random() * (participants.length)) const winner = participants[random] - const winnerContact = await msg.aux.client.getContactById(winner.id._serialized) - - let message = `🎉 - @${winnerContact.id.user} parabéns! Você ganhou o sorteio` + let message = `🎉 - @${winner.id.split('@')[0]} parabéns! Você ganhou o sorteio` message = hasText ? `${message} *${text.trim()}*!` : message + '!' - await msg.aux.chat.sendMessage(message, { - mentions: [winnerContact] - }) + await socket.sendMessage(msg.from, { text: spintax(message), mentions: [winner.id] }, { ephemeralExpiration: msg.raw.message[Object.keys(msg.raw.message)[0]].contextInfo?.expiration || undefined }) - await msg.react('🎉') + await msg.react(msg.aux.db.command.emoji) } /** @@ -127,14 +156,13 @@ export async function markAllMembers (msg) { msg.body = msg.body.charAt(0).toUpperCase() + msg.body.slice(1) - const participants = msg.aux.participants.map((p) => p.id._serialized) - const contactArray = [] - for (let i = 0; i < participants.length; i++) { - contactArray.push(await msg.aux.client.getContactById(participants[i])) - } + const participants = msg.aux.participants.map((p) => p.id) - await msg.aux.chat.sendMessage(msg.body ? `📣 - ${msg.body}` : '📣', { mentions: contactArray }) - await msg.react('📣') + await socket.sendMessage(msg.from, { + text: msg.body ? `📣 - ${msg.body}` : '📣', + mentions: participants + }, { ephemeralExpiration: msg.raw.message[Object.keys(msg.raw.message)[0]].contextInfo?.expiration || undefined }) + await msg.react(msg.aux.db.command.emoji) } /** @@ -143,12 +171,11 @@ export async function markAllMembers (msg) { */ export async function callAdmins (msg) { const admins = msg.aux.admins - const contactArray = [] - for (let i = 0; i < admins.length; i++) { - contactArray.push(await msg.aux.client.getContactById(admins[i])) - } - await msg.aux.chat.sendMessage('👑 - Atenção administradores!', { mentions: contactArray }) - await msg.react('👑') + await socket.sendMessage(msg.from, { + text: '👑 - Atenção administradores!', + mentions: admins + }, { ephemeralExpiration: msg.raw.message[Object.keys(msg.raw.message)[0]].contextInfo?.expiration || undefined }) + await msg.react(msg.aux.db.command.emoji) } /** @@ -163,9 +190,8 @@ export async function closeGroup (msg) { if (!msg.aux.isSenderAdmin) { return await msg.reply('para usar o !fechar *você* precisa ser admin') } - - await msg.aux.chat.setMessagesAdminsOnly(true) - await msg.react('🔒') + await socket.groupSettingUpdate(msg.from, 'announcement') + await msg.react(msg.aux.db.command.emoji) } /** @@ -181,6 +207,6 @@ export async function openGroup (msg) { return await msg.reply('para usar o !abrir *você* precisa ser admin') } - await msg.aux.chat.setMessagesAdminsOnly(false) - await msg.react('🔓') + await socket.groupSettingUpdate(msg.from, 'not_announcement') + await msg.react(msg.aux.db.command.emoji) } diff --git a/src/services/functions/menu.js b/src/services/functions/menu.js index 9b7dc39..f953f8f 100644 --- a/src/services/functions/menu.js +++ b/src/services/functions/menu.js @@ -1,7 +1,8 @@ +import { MessageMedia } from '../../meta/messageMedia.js' import relativeTime from 'dayjs/plugin/relativeTime.js' +import reactions from '../../config/reactions.js' import spintax from '../../utils/spintax.js' import { getCommands } from '../../db.js' -import wwebjs from 'whatsapp-web.js' import 'dayjs/locale/pt-br.js' import dayjs from 'dayjs' @@ -16,7 +17,7 @@ dayjs.extend(relativeTime) * @param {import('../../types.d.ts').WWebJSMessage} msg */ export async function menu (msg) { - await msg.react('📜') + await msg.react(reactions.wait) let commandGroups = await getCommands() @@ -36,7 +37,7 @@ export async function menu (msg) { ] const randomImage = menuImages[Math.floor(Math.random() * menuImages.length)] - const media = await wwebjs.MessageMedia.fromUrl(randomImage, { unsafeMime: true }) + const media = await MessageMedia.fromUrl(randomImage) if (!media) throw new Error('Error downloading media') // remove CommandGroups where hideFromMenu is true @@ -69,13 +70,15 @@ export async function menu (msg) { // Tell About prefix message += 'Os seguintes prefixos são aceitos para os comandos: *! . # /*\n\n' + // const readMore = '​'.repeat(783) + // message += readMore // const menuEmojis = '{📋|🗒️|📜}' - // message += '```━━━━━━━━━━ ' + menuEmojis + ' ━━━━━━━━━━```\n\n' + // message += '```•·········• ' + menuEmojis + ' •·········•```\n\n' // await msg.reply(JSON.stringify(commands, null, 2)) commandGroups.forEach((commandGroup, i) => { - message += '```━━━━━━━━━ ' + commandGroup.emoji + ' ━━━━━━━━━```\n\n' + message += '```•·········• ' + commandGroup.emoji + ' •·········•```\n\n' message += `*${commandGroup.description}*\n\n` commandGroup.commands.forEach(command => { command = structuredClone(command) @@ -95,21 +98,22 @@ export async function menu (msg) { // remove the last \n message = message.trim().replace(/\n$/, '').trim() // await msg.reply(JSON.stringify(msg.aux, null, 2)) - await msg.reply(spintax(message), undefined, { media }) + await msg.reply({ media, caption: spintax(message) }, undefined) + await msg.react(reactions.success) } export async function menuGroup (msg) { + await msg.react(reactions.wait) let commandGroups = await getCommands() // pick only the command group where slug == groups commandGroups = commandGroups.filter(c => c.slug === 'groups') - - await msg.react(commandGroups[0].emoji) + const emoji = commandGroups[0].emoji // if there is image, send it let media if (commandGroups[0].menuImageUrl) { - media = await wwebjs.MessageMedia.fromUrl(commandGroups[0].menuImageUrl, { unsafeMime: true }) + media = await MessageMedia.fromUrl(commandGroups[0].menuImageUrl) if (!media) throw new Error('Error downloading media') } @@ -130,12 +134,12 @@ export async function menuGroup (msg) { message += 'Os seguintes prefixos são aceitos para os comandos: *! . # /*\n\n' // const menuEmojis = '{📋|🗒️|📜}' - // message += '```━━━━━━━━━━ ' + menuEmojis + ' ━━━━━━━━━━```\n\n' + // message += '```•·········• ' + menuEmojis + ' •·········•```\n\n' // await msg.reply(JSON.stringify(commands, null, 2)) commandGroups.forEach((commandGroup, i) => { - message += '```━━━━━━━━━ ' + commandGroup.emoji + ' ━━━━━━━━━━```\n\n' + message += '```•·········• ' + commandGroup.emoji + ' •·········•```\n\n' message += `*${commandGroup.description}*\n\n` commandGroup.commands.forEach(command => { command = structuredClone(command) @@ -157,6 +161,7 @@ export async function menuGroup (msg) { // await msg.reply(JSON.stringify(msg.aux, null, 2)) // await msg.reply(spintax(message)) await msg.reply(spintax(message), undefined, { media }) + await msg.react(emoji) } // diff --git a/src/services/functions/meta.js b/src/services/functions/meta.js new file mode 100644 index 0000000..78f28d1 --- /dev/null +++ b/src/services/functions/meta.js @@ -0,0 +1,175 @@ +import { getDBUrl, getToken, forceContactUpdate } from '../../db.js' +import relativeTime from 'dayjs/plugin/relativeTime.js' +import reactions from '../../config/reactions.js' +import spintax from '../../utils/spintax.js' +import { textSticker } from './stickers.js' +import 'dayjs/locale/pt-br.js' +import dayjs from 'dayjs' + +dayjs.locale('pt-br') +dayjs.extend(relativeTime) + +const obfuscateMap = [ + 'a', 'D', 'e', 'A', 'd', 'F', 'g', 'H', 'i', 'J', 'k', 'L', 'm', 'N', 'o', 'P', + 'q', 'R', 'S', 't', 'u', 'V', 'w', 'X', 'y', 'Z', 'B', 'Y', 'T', 'E', 'c', 'C' +] + +// +// ================================ Main Functions ================================= +// +/** + * Send the menu + * @param {import('../../types.d.ts').WWebJSMessage} msg + */ +export async function set (msg) { + const preferences = { + pack: 'stickerName', + pacote: 'stickerName', + author: 'stickerAuthor', + autor: 'stickerAuthor', + nome: 'stickerAuthor', + name: 'stickerAuthor' + } + + const examples = { + pacote: 'DeadByte.com.br', + nome: 'bot de figurinhas' + } + + const avaliablePreferences = Object.keys(preferences) + const examplesPrefences = Object.keys(examples) + + const prefence = msg.body.split(' ')[0] + const value = msg.body.split(' ').slice(1).join(' ') + + if (!prefence) { + let message = '❌ - Para usar este comando você deve informar *o que* ' + message += 'você está definindo e *o valor* que você quer definir\n\n' + message += '*Exemplos:*' + const mono = '```' + for (const pref of examplesPrefences) { + message += `\n${mono}${msg.aux.prefix}set ${pref} ${examples[pref]}${mono}` + } + return msg.reply(message) + } + + if (!avaliablePreferences.includes(prefence)) { + let message = '❌ - Preferência inválida\n\n' + message += '*Preferências disponíveis:*' + const mono = '```' + for (const pref of examplesPrefences) { + message += `\n${mono}${pref}${mono}` + } + return msg.reply(message) + } + + await msg.react(reactions.wait) + const id = msg.aux.db.contact.id + const preferenceObject = msg.aux.db.contact.attributes.preferences ?? {} + preferenceObject[preferences[prefence]] = value || 'undefined' + // PUT /api/contacts/:id + await fetch(`${getDBUrl()}/contacts/${id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${getToken()}` + }, + body: JSON.stringify({ + data: { + preferences: preferenceObject + } + }) + }) + msg.aux.db.contact = await forceContactUpdate(msg.contact) + msg.body = spintax('{Clique|Clica} {{nessa|nesta} figurinha|{nesse|neste} sticker} para {você |vc |}ver {{o que|oq} {mudou|alterou}|como ficou|o resultado}') + await textSticker(msg) + await msg.react(msg.aux.db.command.emoji) +} + +export async function activate (msg) { +// msg.body = DeAdFR\n\nSe o dead não responder em até 1 minuto envie a mensagem novamente!\n\n Pra facilitar é só clicar no botão de novo: DeadByte.com.br/fila-de-acesso +// code = DeAdFR + + const code = msg.body.split('\n')[0].trim() + const queueId = deobfuscateQueueId(code) + if (queueId === false) { + await msg.react('❌') + return msg.reply('❌ - Código inválido') + } + + // GET /api/queues/:id + const response = await fetch(`${getDBUrl()}/queues/${queueId}?populate=*`, { + headers: { + Authorization: `Bearer ${getToken()}` + } + }) + const queue = await response.json() + if (queue.data.contact) { + // TODO: if the current contact give sucessful feedback + // If is another contact, tell the user that the code is already used + // And send then to the queue again + await msg.react('⚠️') + return msg.reply('⚠️ - Código de ativação já utilizado') + } + + // PUT /api/queues/:id + await fetch(`${getDBUrl()}/queues/${queueId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${getToken()}` + }, + body: JSON.stringify({ + data: { + contact: msg.aux.db.contact.id + } + }) + }) + + msg.aux.db.contact = await forceContactUpdate(msg.contact) + await msg.reply(`⚡ - {Prontinho|Pronto|Tudo pronto} ${msg.pushname}{, o|!\n\nO|!!!\n\nO} {DeadByte|dead|bot} {{já esta|tá} ativo|{já foi|foi} ativado} {para você|pra vc|pra tu}{!|!!|!!!}`) + await msg.react(msg.aux.db.command.emoji) +} + +// +// ================================== Helper Functions ================================== +// +/** + * Obfuscate a number to a string + * @param {number} number + * @returns {string} + */ +function obfuscateQueueId (number) { + return parseInt(number).toString(16).padStart(6, '0').split('').map((char, index) => { + const shift = (parseInt(char, 16) + index + 1) % obfuscateMap.length + return obfuscateMap[shift] + }).join('') +} + +/** + * Deobfuscate a string to a number + * @param {string} encodedStr + * @returns {number | boolean} + */ +function deobfuscateQueueId (encodedStr) { + let foundInt + try { + // Check for invalid input + if (!encodedStr.split('').every(char => obfuscateMap.includes(char))) { + return 'Invalid input' + } + + const hexStr = encodedStr.split('').map((char, index) => { + let shift = obfuscateMap.indexOf(char) - (index + 1) + while (shift < 0) { + shift += obfuscateMap.length // Correct negative shift + } + return shift.toString(16) + }).join('') + + foundInt = parseInt(hexStr, 16) + } catch (error) { + return false + } + return obfuscateQueueId(foundInt) === encodedStr ? foundInt : false +} diff --git a/src/services/functions/miscellaneous.js b/src/services/functions/miscellaneous.js index c97f375..eb532f8 100644 --- a/src/services/functions/miscellaneous.js +++ b/src/services/functions/miscellaneous.js @@ -1,9 +1,11 @@ -import { getQueueLength } from '../queue.js' +// import { getQueueLength } from '../queue.js' +import { MessageMedia } from '../../meta/messageMedia.js' import relativeTime from 'dayjs/plugin/relativeTime.js' +import { webpToMp4 } from '../../utils/converters.js' import reactions from '../../config/reactions.js' import { createUrl } from '../../config/api.js' import spintax from '../../utils/spintax.js' -import wwebjs from 'whatsapp-web.js' +// import wwebjs from 'whatsapp-web.js' import logger from '../../logger.js' import FormData from 'form-data' import 'dayjs/locale/pt-br.js' @@ -31,13 +33,13 @@ export async function uptime (msg) { const uptimeString = secondsToDhms(uptime) const clock = '{⏳|⌚|⏰|⏱️|⏲️|🕰️|🕛|🕧|🕐|🕜|🕑|🕝}' - await msg.react(spintax(clock)) // react with random clock emoji - const saudation = `{${spintax(clock)}} - {Olá|Oi|Oie|E aí} ${msg.aux.sender.pushname || 'usuário'} tudo {jóia|bem}?` + const saudation = `{${spintax(clock)}} - {Olá|Oi|Oie|E aí} ${msg.pushname || 'usuário'} tudo {jóia|bem}?` const part1 = '{Eu estou|Estou|O bot {está|ta|tá}|O DeadByte {está|ta|tá}} {online|on|ligado}{ direto|} {a|á|tem}{:|} ' - const message = spintax(`${saudation}\n\n${part1}*${uptimeString}*`) + const message = `${saudation}\n\n${part1}*${uptimeString}*` await msg.reply(message) + await msg.react(msg.aux.db.command.emoji) } /** @@ -49,7 +51,6 @@ export async function react (msg) { const json = await response.json() const emoji = String.fromCodePoint(...json.unicode.map(u => parseInt(u.replace('U+', '0x'), 16))) await msg.react(emoji) - await msg.aux.chat.sendSeen() } /** @@ -57,8 +58,6 @@ export async function react (msg) { * @param {import('../../types.d.ts').WWebJSMessage} msg */ export async function dice (msg) { - await msg.react('🎲') - const fullCommand = msg.aux.function const regex = /(?\d*)d(?\d+)(?[\+\-\*\/]\d+)?/i @@ -84,22 +83,7 @@ export async function dice (msg) { }) } await msg.reply(message) -} - -/** - * Tests functions - * @param {import('../../types.d.ts').WWebJSMessage} msg - */ -export async function debug (msg) { - const debugEmoji = '🐛' - await msg.react(debugEmoji) - - const announceGroup = '120363094244463491@g.us' - const chat = await msg.aux.client.getChatById(announceGroup) - const admins = chat.participants.filter(p => p.isAdmin || p.isSuperAdmin).map((p) => p.id._serialized) - const botIsAdmin = admins.includes(msg.aux.me) - - await msg.reply(JSON.stringify(botIsAdmin, null, 2)) + await msg.react(msg.aux.db.command.emoji) } /** @@ -107,7 +91,7 @@ export async function debug (msg) { * @param {import('../../types.d.ts').WWebJSMessage} msg */ export async function toFile (msg) { - if ((!msg.hasQuotedMsg && !msg.hasMedia) || (msg.hasQuotedMsg && !msg.aux.quotedMsg.hasMedia)) { + if (!msg.hasMedia && (msg.hasQuotedMsg && !msg.quotedMsg.hasMedia)) { await msg.react(reactions.error) const header = '☠️🤖' @@ -118,8 +102,7 @@ export async function toFile (msg) { const message = spintax(`${header} - ${part1} ${part2}${end}`) return await msg.reply(message) } - await msg.react('🗂️') - const media = msg.hasQuotedMsg ? await msg.aux.quotedMsg.downloadMedia() : await msg.downloadMedia() + let media = msg.hasQuotedMsg ? await msg.downloadMedia(true) : await msg.downloadMedia() if (!media) throw new Error('Error downloading media') // the last 10 chars of the timestamp @@ -129,30 +112,34 @@ export async function toFile (msg) { const buffer = Buffer.from(media.data, 'base64') - let message = '' - message += '{Aqui está|Toma ai|Confira aqui|Veja só|Prontinho ta aí} ' - message += 'o arquivo{ que você {me |}{pediu|enviou}|}!\n\n' - const isImage = media.mimetype.includes('image') const isVideo = media.mimetype.includes('video') // use sharp to check if the image is animated const isAnimated = isImage ? await sharp(buffer).metadata().then(m => parseInt(m.pages) > 1) : false - const finalExtension = isImage ? isAnimated ? 'webp' : 'png' : mime.extension(media.mimetype) - - message += `É ${isImage ? 'uma imagem' : isVideo ? 'um vídeo' : 'um arquivo'} ${finalExtension.toUpperCase()}` - message += isImage && isAnimated ? ' animada' : '' - if (isImage && !isAnimated) { const converted = await sharp(buffer).toFormat('png').toBuffer() media.data = converted.toString('base64') media.mimetype = 'image/png' media.filename = media.filename.split('.').slice(0, -1).join('.') + '.png' - return await msg.reply(media, undefined, { sendMediaAsDocument: false, caption: spintax(message) }) + await msg.reply({ media, caption: 'DeadByte.com.br - bot de figurinhas' }) + } + + if (isImage && isAnimated) { + await msg.reply({ media, caption: 'Espere um pouco que vou converter esse *webp* para um formato melhor para você...' }) + await msg.react(reactions.wait) + + const tempUrl = await getTempUrl(media) + const url = await webpToMp4(tempUrl) + media = await MessageMedia.fromUrl(url) + await msg.reply({ media, gifPlayback: true, caption: 'DeadByte.com.br - bot de figurinhas' }) + } + + if (isVideo) { + await msg.reply({ media, caption: 'DeadByte.com.br - bot de figurinhas' }) } - await msg.reply(media, undefined, { sendMediaAsDocument: true, caption: spintax(message) }) - // TODO convert to webp if animated to mp4 and send as "gif" + await msg.react(msg.aux.db.command.emoji) } /** @@ -160,7 +147,7 @@ export async function toFile (msg) { * @param {import('../../types.d.ts').WWebJSMessage} msg */ export async function toUrl (msg) { - if ((!msg.hasQuotedMsg && !msg.hasMedia) || (msg.hasQuotedMsg && !msg.aux.quotedMsg.hasMedia)) { + if (!msg.hasMedia && (msg.hasQuotedMsg && !msg.quotedMsg.hasMedia)) { await msg.react(reactions.error) const header = '☠️🤖' @@ -172,18 +159,19 @@ export async function toUrl (msg) { return await msg.reply(message) } - await msg.react('🔗') - const media = msg.hasQuotedMsg ? await msg.aux.quotedMsg.downloadMedia() : await msg.downloadMedia() + const media = msg.hasQuotedMsg ? await msg.downloadMedia(true) : await msg.downloadMedia() if (!media) throw new Error('Error downloading media') - const tempUrl = (await getTempUrl(media)).replace('http://', 'https://') + const tempUrl = (await getTempUrl(media)) + await msg.react(reactions.wait) let message = '🔗 - ' message += '{Aqui está|Toma ai|Confira aqui|Veja só|Prontinho ta aí} ' message += '{a url temporária|o link temporário|o endereço temporário} ' message += '{para {o|esse}|desse} arquivo: ' message += `${tempUrl}\n\n` message += '{Válido por {apenas|}|Com {validade|vigência} de|Por um período de} {3|03|três} dias' - await msg.reply(spintax(message)) + await msg.reply(message) + await msg.react(msg.aux.db.command.emoji) } /** @@ -191,31 +179,35 @@ export async function toUrl (msg) { * @param {import('../../types.d.ts').WWebJSMessage} msg */ export async function ping (msg) { - await msg.react('🏓') - - let message = '🏓 - Pong!\n\n' - - const usersInQueue = getQueueLength('user') - const messagesInQueue = getQueueLength('messages') - if (usersInQueue || messagesInQueue) { - message += `{Atualmente|No momento|{Nesse|Neste}{ exato|} momento} tem *${usersInQueue} ${usersInQueue > 1 ? 'usuários' : 'usuário'}* na fila com *${messagesInQueue} ${messagesInQueue > 1 ? 'mensagens' : 'mensagem'}* ao todo!\n\n` - } - - let lag = msg.lag + let lag = msg.lag / 1000 lag = Math.max(lag, 0) // if lag is negative, set it to 0 - lag = lag < 5 ? 0 : lag // ignore lag if it is less than 5 seconds lag = isNaN(lag) ? 0 : lag const ping = Date.now() - msg.startedAt const delayString = convertToHumanReadable(ping, lag, 'ms') - message += `Essa mensagem demorou *${delayString}* para ser respondida` + let message = `Essa mensagem demorou *${delayString}* para ser respondida` + + message += '\n\n```━━━━━━━━━━ 🏓 ━━━━━━━━━━```\n\n' + + // TODO: get medium speed for each message from server + // message += '📶 - Velocidade atual: 0\n' + // message += '👥 - Usuários esta semana: 0\n' + // message += '💬 - Chats na última hora: 0\n' + // message += '📩 - Mensagens na última hora: 0\n' + + message += '🕒 - Online direto há: ' + const uptime = process.uptime() + const uptimeString = secondsToDhms(uptime) + message += uptimeString - if (lag > 0) { + if (lag >= 1) { + message += '\n\n```━━━━━━━━━━ ⚠️ ━━━━━━━━━━```\n\n' const lagString = convertToHumanReadable(lag, 0, 's') - message += `\n\nO WhatsApp demorou *${lagString}* para entregar essa mensagem pra mim!` + message += `O WhatsApp demorou *${lagString}* para entregar essa mensagem pra mim!` } await msg.reply(spintax(message)) + await msg.react(msg.aux.db.command.emoji) } /** @@ -223,12 +215,14 @@ export async function ping (msg) { * @param {import('../../types.d.ts').WWebJSMessage} msg */ export async function speak (msg) { - let msgToReply = msg let input = msg.body if (msg.hasQuotedMsg && !msg.body) { - const quotedMsg = await msg.getQuotedMessage() - input = quotedMsg.body - msgToReply = quotedMsg + const quotedMsg = msg.quotedMsg + const keys = Object.keys(quotedMsg) + const firstItem = quotedMsg[keys[0]] + input = typeof firstItem === 'string' + ? firstItem + : firstItem.caption || firstItem.text || '' } if (!input) { @@ -247,8 +241,8 @@ export async function speak (msg) { input = input.slice(0, inputLimit) } - await msg.react('🗣️') - await msg.aux.chat.sendStateRecording() + await msg.react(reactions.wait) + // await msg.aux.chat.sendStateRecording() const voices = ['onyx', 'echo', 'fable', 'nova', 'shimmer'] @@ -272,8 +266,10 @@ export async function speak (msg) { const buffer = Buffer.from(await opus.arrayBuffer()) // hand make the media object - const media = new wwebjs.MessageMedia('audio/ogg; codecs=opus', buffer.toString('base64'), 'DeadByte.opus') - await msgToReply.reply(media, undefined, { sendAudioAsVoice: true }) + const media = new MessageMedia('audio/ogg; codecs=opus', buffer.toString('base64'), 'DeadByte' + Date.now() + '.opus' + , buffer.length) + await msg.reply({ media }, undefined, { ptt: true }) + await msg.react(msg.aux.db.command.emoji) } /** @@ -281,12 +277,9 @@ export async function speak (msg) { * @param {import('../../types.d.ts').WWebJSMessage} msg */ export async function transcribe (msg) { - let msgToReply = msg let media = msg.hasMedia ? await msg.downloadMedia() : null if (msg.hasQuotedMsg && !msg.hasMedia) { - const quotedMsg = await msg.getQuotedMessage() - media = await quotedMsg.downloadMedia() - msgToReply = quotedMsg + media = await msg.downloadMedia(true) } if (!media) { @@ -297,22 +290,24 @@ export async function transcribe (msg) { return } - await msg.react('🎙️') - await msg.aux.chat.sendStateTyping() + await msg.react(reactions.wait) + // await msg.aux.chat.sendStateTyping() // save file to temp folder const timestampish = Date.now().toString().slice(-10) - const filePath = `./temp/${timestampish}.mp3` + const filePath = `./src/temp/${timestampish}.mp3` const nomalizedFilePath = path.resolve(filePath) fs.writeFileSync(nomalizedFilePath, media.data, { encoding: 'base64' }) const transcription = await openai.audio.transcriptions.create({ file: fs.createReadStream(nomalizedFilePath), model: 'whisper-1', - response_format: 'text' + response_format: 'text', + prompt: '"DeadByte" sticker.ly' }) fs.unlinkSync(nomalizedFilePath) - await msgToReply.reply(`🎙️ - ${transcription.trim()}`) + await msg.reply(`🎙️ - ${transcription.trim()}`) + await msg.react(msg.aux.db.command.emoji) } // @@ -339,14 +334,14 @@ function secondsToDhms (seconds) { // add suffixe "dia" or "dias" if days > 0 and singular or plural // "hora" or "horas" if hours > 0 and singular or plural etc... - const days = d > 0 ? `${d === 1 ? 'dia' : 'dias'}` : '' - const hours = h > 0 ? `${h === 1 ? 'hora' : 'horas'}` : '' - const minutes = m > 0 ? `${m === 1 ? 'minuto' : 'minutos'}` : '' - const secondsString = `${s === 1 ? 'segundo' : 'segundos'}` - // from left to right, get the first non empty string - const array = [days, hours, minutes, secondsString].filter(s => s !== '') - const suffix = array[0] - string += ` ${suffix}` + // const days = d > 0 ? `${d === 1 ? 'dia' : 'dias'}` : '' + // const hours = h > 0 ? `${h === 1 ? 'hora' : 'horas'}` : '' + // const minutes = m > 0 ? `${m === 1 ? 'minuto' : 'minutos'}` : '' + // const secondsString = `${s === 1 ? 'segundo' : 'segundos'}` + // // from left to right, get the first non empty string + // const array = [days, hours, minutes, secondsString].filter(s => s !== '') + // const suffix = array[0] + // string += ` ${suffix}` return string } @@ -375,7 +370,7 @@ async function getTempUrl (media) { const json = await response.json() const tempUrl = json.result - return tempUrl + return tempUrl.replace('http://', 'https://') } /** diff --git a/src/services/functions/statistics.js b/src/services/functions/statistics.js index 9ca452b..3a68d55 100644 --- a/src/services/functions/statistics.js +++ b/src/services/functions/statistics.js @@ -30,7 +30,7 @@ export async function stats (msg) { const stats = await fetchStats(contactID) const emojis = '{📊|📈|📉|🔍|🔬|📚}' - let message = `${emojis} - {Olá|Oi|Oie|${saudation}} ${msg.aux.sender.pushname}!\n\n` + let message = `${emojis} - {Olá|Oi|Oie|${saudation}} ${msg.pushname}!\n\n` // 📊 - Olá, Sergio Carvalho! message += '```━━━━━━━━━━ {📊|📈|📉|🔍|🔬|📚} ━━━━━━━━━━```\n\n' @@ -59,7 +59,7 @@ export async function stats (msg) { message = formatCommands(commands, msg, message) await waitForMinimumTime(startedAt) - await reactAndReply(msg, emojis, reply, message) + await reactAndReply(msg, msg.aux.db.command.emoji, reply, message) } /** @@ -73,7 +73,7 @@ export async function botStats (msg) { const stats = await fetchStats() const emojis = '{🤖|👾|💀}' - let message = `${emojis} - {Olá|Oi|Oie|${saudation}} ${msg.aux.sender.pushname}!` + let message = `${emojis} - {Olá|Oi|Oie|${saudation}} ${msg.pushname}!` // 🤖 - Olá, Sergio Carvalho! message += '\n\n```━━━━━━━━━━ {📊|📈|📉|🔍|🔬|📚} ━━━━━━━━━━```\n\n' @@ -107,7 +107,7 @@ export async function botStats (msg) { message += 'Veja as estatísticas completas em tempo real no site:\ndeadbyte.com.br/stats\n\n' await waitForMinimumTime(startedAt) - await reactAndReply(msg, emojis, reply, message) + await reactAndReply(msg, msg.aux.db.command.emoji, reply, message) } /** @@ -122,7 +122,7 @@ export async function weekStats (msg) { const stats = await fetchStats(contactID, 'week') const emojis = '{📊|📈|📉|🔍|🔬|📚}' - let message = `${emojis} - {Olá|Oi|Oie|${saudation}} ${msg.aux.sender.pushname}!\n\n` + let message = `${emojis} - {Olá|Oi|Oie|${saudation}} ${msg.pushname}!\n\n` // 📊 - Olá, Sergio Carvalho! message += '```━━━━━━━━━━ {📊|📈|📉|🔍|🔬|📚} ━━━━━━━━━━```\n\n' @@ -151,7 +151,7 @@ export async function weekStats (msg) { message = formatCommands(commands, msg, message) await waitForMinimumTime(startedAt) - await reactAndReply(msg, emojis, reply, message) + await reactAndReply(msg, msg.aux.db.command.emoji, reply, message) } /** @@ -166,7 +166,7 @@ export async function dayStats (msg) { const stats = await fetchStats(contactID, 'day') const emojis = '{📊|📈|📉|🔍|🔬|📚}' - let message = `${emojis} - {Olá|Oi|Oie|${saudation}} ${msg.aux.sender.pushname}!\n\n` + let message = `${emojis} - {Olá|Oi|Oie|${saudation}} ${msg.pushname}!\n\n` // 📊 - Olá, Sergio Carvalho! message += '```━━━━━━━━━━ {📊|📈|📉|🔍|🔬|📚} ━━━━━━━━━━```\n\n' @@ -195,7 +195,7 @@ export async function dayStats (msg) { message = formatCommands(commands, msg, message) await waitForMinimumTime(startedAt) - await reactAndReply(msg, emojis, reply, message) + await reactAndReply(msg, msg.aux.db.command.emoji, reply, message) } /** @@ -210,7 +210,7 @@ export async function hourStats (msg) { const stats = await fetchStats(contactID, 'hour') const emojis = '{📊|📈|📉|🔍|🔬|📚}' - let message = `${emojis} - {Olá|Oi|Oie|${saudation}} ${msg.aux.sender.pushname}!\n\n` + let message = `${emojis} - {Olá|Oi|Oie|${saudation}} ${msg.pushname}!\n\n` // 📊 - Olá, Sergio Carvalho! message += '```━━━━━━━━━━ {📊|📈|📉|🔍|🔬|📚} ━━━━━━━━━━```\n\n' @@ -236,7 +236,7 @@ export async function hourStats (msg) { message = formatCommands(commands, msg, message) await waitForMinimumTime(startedAt) - await reactAndReply(msg, emojis, reply, message) + await reactAndReply(msg, msg.aux.db.command.emoji, reply, message) } // // ================================== Helper Functions ================================== @@ -288,7 +288,7 @@ async function waitForMinimumTime (startedAt) { */ async function reactAndReply (msg, emojis, reply, message) { await msg.react(spintax(emojis)) - if (reply) { return await reply.edit(spintax(message)) } + // if (reply) { return await reply.edit(spintax(message)) } await msg.reply(spintax(message)) } @@ -325,17 +325,19 @@ export async function fetchStats (contact = undefined, mode = undefined, bot = u * @returns {Promise} */ async function sendInitialReply (msg, sufix) { + await msg.react(reactions.wait) const saudation = getSaudation() - let initialMessage = `{⏳|⌛|🕰️|🕛|🕒|🕞} - {Olá|Oi|Oie|${saudation}} ${msg.aux.sender.pushname}!\n\n` - // ⏳ - Olá, Sergio Carvalho! - initialMessage += `{Espere|Espera|Péra} um {pouco|pouquinho|momento|segundo} enquanto eu {pego|busco|procuro} as {suas |}estatísticas${sufix ? ' ' + sufix : ''}...` - // Espere um pouco enquanto eu pego as suas estatísticas... - let reply = null - if (!msg.aux.chat.isGroup) { - reply = await msg.reply(spintax(initialMessage)) - } const startedAt = Date.now() - return { saudation, startedAt, reply } + return { saudation, startedAt, reply: null } + // let initialMessage = `{⏳|⌛|🕰️|🕛|🕒|🕞} - {Olá|Oi|Oie|${saudation}} ${msg.pushname}!\n\n` + // // ⏳ - Olá, Sergio Carvalho! + // initialMessage += `{Espere|Espera|Péra} um {pouco|pouquinho|momento|segundo} enquanto eu {pego|busco|procuro} as {suas |}estatísticas${sufix ? ' ' + sufix : ''}...` + // // Espere um pouco enquanto eu pego as suas estatísticas... + // let reply = null + // if (!msg.aux.chat.isGroup) { + // reply = await msg.reply(spintax(initialMessage)) + // } + // return { saudation, startedAt, reply } } /** diff --git a/src/services/functions/stickers.js b/src/services/functions/stickers.js index 99ffe30..ff16fc9 100644 --- a/src/services/functions/stickers.js +++ b/src/services/functions/stickers.js @@ -1,36 +1,47 @@ -import wwebjs from 'whatsapp-web.js' -import Util from '../../utils/sticker.js' -import sharp from 'sharp' -import { createUrl } from '../../config/api.js' +import { MessageMedia } from '../../meta/messageMedia.js' import reactions from '../../config/reactions.js' -import logger from '../../logger.js' +import { createUrl } from '../../config/api.js' import spintax from '../../utils/spintax.js' -import fetch from 'node-fetch' +import { getSocket } from '../../index.js' +import Util from '../../utils/sticker.js' +import logger from '../../logger.js' import FormData from 'form-data' +import fetch from 'node-fetch' +import sharp from 'sharp' + +const sock = getSocket() /** * Make sticker from media (image, video, gif) * @param {import('../../types.d.ts').WWebJSMessage} msg - * @param {boolean} [crop=false] - crop the image to a square - * @param {string} StickerAuthor - sticker author name - * @param {string} StickerPack - sticker pack name + * @param {string} stickerName + * @param {string} stickerAuthor */ -export async function stickerCreator (msg, crop = false, stickerAuthor, stickerPack) { +export async function stickerCreator (msg, stickerName, stickerAuthor, overwrite = false) { await msg.react(reactions.wait) - const media = msg.hasQuotedMsg ? await msg.aux.quotedMsg.downloadMedia() : await msg.downloadMedia() - if (!media) throw new Error('Error downloading media') + const media = msg.hasQuotedMsg ? await msg.downloadMedia(true) : await msg.downloadMedia() + const metadata = media.metadata - let stickerMedia = await Util.formatToWebpSticker(media, {}, crop) - if (msg.type === 'document') msg.body = '' // remove file name from caption - if (msg.body) stickerMedia = await overlaySubtitle(msg.body, stickerMedia).catch((e) => logger.error(e)) || stickerMedia + const promises = [] + promises.push(Util.formatToWebpSticker(media, {}, false)) + + const ratioDistanceFromSquare = Math.abs(metadata.ratio - 1) + const needToCrop = ratioDistanceFromSquare > 0.1 + + if (needToCrop) { + promises.push(Util.formatToWebpSticker(media, {}, true)) + } + + for (const promise of promises) { + let stickerMedia = await promise - await sendMediaAsSticker(msg.aux.chat, stickerMedia, stickerAuthor, stickerPack) + if (msg.type === 'document') msg.body = '' // remove file name from caption + if (msg.body) stickerMedia = await overlaySubtitle(msg.body, stickerMedia).catch((e) => logger.error(e)) || stickerMedia - if (!crop) { - await stickerCreator(msg, true) // make cropped version - await msg.react(reactions.success) + await sendMediaAsSticker(msg, stickerMedia, stickerName, stickerAuthor, overwrite) } + await msg.react() } /** @@ -42,11 +53,11 @@ export async function textSticker (msg) { const url = await createUrl('image-creator', 'ttp', { message: msg.body }) - const media = await wwebjs.MessageMedia.fromUrl(url, { unsafeMime: true }) - if (!media) throw new Error('Error downloading media') + let media = await MessageMedia.fromUrl(url) + media = await Util.formatToWebpSticker(media, {}, false) - await sendMediaAsSticker(msg.aux.chat, media) - await msg.react(reactions.success) + await sendMediaAsSticker(msg, media) + await msg.react() } /** @@ -57,12 +68,11 @@ export async function textSticker2 (msg) { await msg.react(reactions.wait) const url = await createUrl('image-creator', 'ttp2', { message: msg.body }) + let media = await MessageMedia.fromUrl(url) + media = await Util.formatToWebpSticker(media, {}, false) - const media = await wwebjs.MessageMedia.fromUrl(url, { unsafeMime: true }) - if (!media) throw new Error('Error downloading media') - - await sendMediaAsSticker(msg.aux.chat, media) - await msg.react(reactions.success) + await sendMediaAsSticker(msg, media) + await msg.react() } /** @@ -73,12 +83,11 @@ export async function textSticker3 (msg) { await msg.react(reactions.wait) const url = await createUrl('image-creator', 'ttp3', { message: msg.body }) + let media = await MessageMedia.fromUrl(url) + media = await Util.formatToWebpSticker(media, {}, false) - const media = await wwebjs.MessageMedia.fromUrl(url, { unsafeMime: true }) - if (!media) throw new Error('Error downloading media') - - await sendMediaAsSticker(msg.aux.chat, media) - await msg.react(reactions.success) + await sendMediaAsSticker(msg, media) + await msg.react() } /** @@ -88,8 +97,7 @@ export async function textSticker3 (msg) { */ export async function removeBg (msg) { await msg.react(reactions.wait) - - if (!msg.hasMedia && (msg.hasQuotedMsg && !msg.aux.quotedMsg.hasMedia)) { + if (!msg.hasMedia && (msg.hasQuotedMsg && !msg.quotedMsg.hasMedia)) { await msg.react(reactions.error) const header = '☠️🤖' @@ -101,14 +109,14 @@ export async function removeBg (msg) { return await msg.reply(message) } - const media = msg.hasQuotedMsg ? await msg.aux.quotedMsg.downloadMedia() : await msg.downloadMedia() + const media = msg.hasQuotedMsg ? await msg.downloadMedia(true) : await msg.downloadMedia() if (!media) throw new Error('Error downloading media') if (!media.mimetype.includes('image')) { await msg.react(reactions.error) return await msg.reply('❌ Só consigo remover o fundo de imagens') } - // use shapr to convert to a max 512 (bigger side) jpg image, crank up the contrast + // use sharp to convert to a max 512 (bigger side) jpg image, crank up the contrast const buffer = Buffer.from(media.data, 'base64') const resizedBuffer = await sharp(buffer) .resize(1024, 1024, { fit: 'inside' }) @@ -117,13 +125,13 @@ export async function removeBg (msg) { const tempUrl = await getTempUrl(resizedBuffer) const url = await createUrl('image-processing', 'removebg', { img: tempUrl, trim: true }) - const bgMedia = await wwebjs.MessageMedia.fromUrl(url, { unsafeMime: true }) + const bgMedia = await MessageMedia.fromUrl(url, { unsafeMime: true }) let stickerMedia = await Util.formatToWebpSticker(bgMedia, {}) if (msg.body) stickerMedia = await overlaySubtitle(msg.body, stickerMedia).catch((e) => logger.error(e)) || stickerMedia - await sendMediaAsSticker(msg.aux.chat, stickerMedia) - await msg.react(reactions.success) + await sendMediaAsSticker(msg, stickerMedia) + await msg.react(msg.aux.db.command.emoji) } /** @@ -132,23 +140,25 @@ export async function removeBg (msg) { * */ export async function stealSticker (msg) { + if (!msg.hasQuotedMsg && (msg.hasQuotedMsg && !msg.quotedMsg.hasMedia) && !msg.hasMedia) { + await msg.react(reactions.error) + return await msg.reply('❌ - Você precisa responder a uma figurinha para usar esse comando') + } await msg.react(reactions.wait) - const quotedMsg = await msg.getQuotedMessage() - const delimiters = ['|', '/', '\\'] let messageParts = [msg.body] // default to the whole message for (const delimiter of delimiters) { if (messageParts.length > 1) break messageParts = msg.body.split(delimiter) } - const stickerName = messageParts[0]?.trim() || msg.aux.sender.pushname - const stickerAuthor = messageParts[1]?.trim() || 'DeadByte.com.br' + const stickerName = messageParts[0]?.trim() || undefined + const stickerAuthor = messageParts[1]?.trim() || undefined - if (!msg.hasQuotedMsg || !quotedMsg.hasMedia) { + if (!msg.hasQuotedMsg || !msg.quotedMsg.hasMedia) { if (msg.hasMedia && (msg.type === 'image' || msg.type === 'video' || msg.type === 'sticker')) { msg.body = '' - return await stickerCreator(msg, undefined, stickerName, stickerAuthor) + return await stickerCreator(msg, stickerName, stickerAuthor, true) } await msg.react(reactions.error) @@ -162,11 +172,11 @@ export async function stealSticker (msg) { return await msg.reply(message) } - const media = await quotedMsg.downloadMedia() + const media = await msg.downloadMedia(true) if (!media) throw new Error('Error downloading media') - await sendMediaAsSticker(msg.aux.chat, media, stickerName, stickerAuthor) - await msg.react(reactions.success) + await sendMediaAsSticker(msg, media, stickerName, stickerAuthor, true) + await msg.react(msg.aux.db.command.emoji) } /** @@ -174,7 +184,8 @@ export async function stealSticker (msg) { * @param {import('../../types.d.ts').WWebJSMessage} msg */ export async function stickerLySearch (msg) { - const isStickerGroup = checkStickerGroup(msg.aux.chat.id) + // const isStickerGroup = checkStickerGroup(msg.aux.chat.id) + const isStickerGroup = false const limit = getStickerLimit(isStickerGroup) if (!msg.body) { @@ -216,8 +227,8 @@ export async function stickerLySearch (msg) { await msg.reply(spintax(message)) } - await sendStickers(stickersPaginated, msg.aux.chat) - await msg.react(reactions.success) + await sendStickers(stickersPaginated, msg) + await msg.react(msg.aux.db.command.emoji) } /** @@ -228,12 +239,12 @@ export async function stickerLyPack (msg) { // remove https://sticker.ly/s/ from the beginning of the message if it exists msg.body = msg.body.replace('https://sticker.ly/s/', '') - const isStickerGroup = checkStickerGroup(msg.aux.chat.id) + // const isStickerGroup = checkStickerGroup(msg.aux.chat.id) + const isStickerGroup = false const limit = getStickerLimit(isStickerGroup) if (!msg.body) { - await msg.reply('🤖 - Para usar o *!pack* você precisa enviar um código de pacote do sticker.ly.\nEx: *!pack 2RY2AQ*') - throw new Error('No search term') + return await msg.reply('🤖 - Para usar o *!pack* você precisa enviar um código de pacote do sticker.ly.\nEx: *!pack 2RY2AQ*') } await msg.react(reactions.wait) @@ -241,7 +252,7 @@ export async function stickerLyPack (msg) { // if the term is a pack id, send the pack const packRegex = /^[a-zA-Z0-9]{6}$/ if (!packRegex.test(msg.body)) { - await msg.reply('🤖 - Para usar o *!pack* você precisa enviar um código de pacote do sticker.ly.\nEx: *!pack 2RY2AQ*') + return await msg.reply('🤖 - Para usar o *!pack* você precisa enviar um código de pacote do sticker.ly.\nEx: *!pack 2RY2AQ*') } const packId = msg.body.toUpperCase() @@ -273,8 +284,8 @@ export async function stickerLyPack (msg) { await msg.reply(spintax(message)) } - await sendStickers(stickersPaginated, msg.aux.chat) - await msg.react(reactions.success) + await sendStickers(stickersPaginated, msg) + await msg.react(msg.aux.db.command.emoji) } // @@ -289,24 +300,45 @@ export async function stickerLyPack (msg) { * @param {import ('whatsapp-web.js').MessageMedia} media - The media to send as a sticker. * @param {string} [stickerName='DeadByte.com.br'] - The name of the sticker. * @param {string} [stickerAuthor='bot de figurinhas'] - The author of the sticker. + * @param {boolean} [overwrite=false] - Whether to overwrite the sticker pack or not. * @returns {Promise} A Promise that resolves with the Message object of the sent sticker. */ -async function sendMediaAsSticker (chat, media, stickerName, stickerAuthor) { - const buffer = Buffer.from(media.data, 'base64') +async function sendMediaAsSticker (msg, media, author, pack, overwrite = false) { + // parse sticker name and author + // author = author || (overwrite ? 'DeadByte.com.br' : undefined) + // pack = pack || (overwrite ? 'bot de figurinhas' : undefined) + const authorFromDb = msg.aux.db.contact.attributes?.preferences?.stickerAuthor + const packFromDb = msg.aux.db.contact.attributes?.preferences?.stickerName + if (!overwrite) { + author = authorFromDb || 'DeadByte.com.br' + pack = packFromDb || 'bot de figurinhas' + } + author = author === 'undefined' ? undefined : author + pack = pack === 'undefined' ? undefined : pack + + const stickerMedia = await Util.formatToWebpSticker(media, { + author, + pack + }, false) + const buffer = Buffer.from(stickerMedia.data, 'base64') // if heavier than 1MB, compress it if (buffer.byteLength > 1_000_000) { - media = await compressMediaBuffer(buffer) + media = await compressMediaBuffer(buffer, media) + media = await Util.formatToWebpSticker(media, { + author, + pack + }, false) } - - media = new wwebjs.MessageMedia(media.mimetype || 'image/webp', media.data, media.filename || 'sticker.webp') + const firstKey = Object.keys(msg.raw.message)[0] + const firstItem = msg.raw.message[firstKey] + const isEphemeral = !!firstItem.contextInfo?.expiration try { - return await chat.sendMessage(media, { - sendMediaAsSticker: true, - stickerName: stickerName || 'DeadByte.com.br', - stickerAuthor: stickerAuthor || 'bot de figurinhas', - stickerCategories: ['💀', '🤖'] + return await sock.sendMessage(msg.from, { + sticker: buffer + }, { + ephemeralExpiration: isEphemeral ? firstItem.contextInfo?.expiration : undefined }) } catch (error) { logger.error(error) @@ -329,12 +361,8 @@ async function overlaySubtitle (text, stickerMedia) { message: text, subtitle: true }) - const subtitleMedia = await wwebjs.MessageMedia.fromUrl(url, { - unsafeMime: true - }) - if (!subtitleMedia) throw new Error('Error downloading subtitle media') - const subtitleBuffer = Buffer.from(subtitleMedia.data, 'base64') + const subtitleBuffer = await fetch(url).then((res) => res.buffer()) const finalBuffer = await sharp(mediaBuffer, { animated: true }) .composite([{ input: subtitleBuffer, @@ -345,8 +373,9 @@ async function overlaySubtitle (text, stickerMedia) { .webp() .toBuffer() - // replace media data with the new data from sharp - return new wwebjs.MessageMedia('image/webp', finalBuffer.toString('base64'), 'deadbyte.webp', true) + stickerMedia.data = finalBuffer.toString('base64') + stickerMedia.filesize = finalBuffer.byteLength + return stickerMedia } /** @@ -357,7 +386,7 @@ async function overlaySubtitle (text, stickerMedia) { * @returns {Promise} A Promise that resolves with a compressed MessageMedia object. * @throws {Error} If the compressed buffer is still too heavy. */ -async function compressMediaBuffer (mediaBuffer) { +async function compressMediaBuffer (mediaBuffer, media) { logger.debug('compressing sticker...', mediaBuffer.byteLength) const compressedBuffer = await sharp(mediaBuffer, { animated: true }) .webp({ quality: 33 }) @@ -366,7 +395,10 @@ async function compressMediaBuffer (mediaBuffer) { if (compressedBuffer.byteLength > 1_000_000) throw new Error('Sticker is still too heavy!', mediaBuffer.byteLength) - return new wwebjs.MessageMedia('image/webp', compressedBuffer.toString('base64'), 'deadbyte.webp', true) + media.data = compressedBuffer.toString('base64') + media.filesize = compressedBuffer.byteLength + + return media } /** @@ -514,10 +546,10 @@ function addPaginationToTheMessage (message, prefix, command, term, limit, total * @param {string} chatId * @returns {boolean} */ -function checkStickerGroup (chatId) { - const stickerGroup = '120363187692992289@g.us' - return chatId._serialized === stickerGroup -} +// function checkStickerGroup (chatId) { +// const stickerGroup = '120363187692992289@g.us' +// return chatId._serialized === stickerGroup +// } /** * Get the limit of stickers based on the chat type @@ -557,21 +589,23 @@ function paginateStickers (stickers, cursor, limit) { * @param {Object} chat * @returns {Promise} */ -async function sendStickers (stickersPaginated, chat) { +async function sendStickers (stickersPaginated, msg) { for (const s of stickersPaginated) { - const media = await wwebjs.MessageMedia.fromUrl(s.url) - await sendMediaAsSticker(chat, media) + // const media = await wwebjs.MessageMedia.fromUrl(s.url) + let media = await MessageMedia.fromUrl(s.url) + media = await Util.formatToWebpSticker(media, {}, false) + await sendMediaAsSticker(msg, media) await waitRandomTime() } } /** * Wait random time - * @param {number} min @default 50 - * @param {number} max @default 500 + * @param {number} min @default 25 + * @param {number} max @default 250 * @returns {Promise} */ -function waitRandomTime (min = 50, max = 500) { +function waitRandomTime (min = 25, max = 250) { return new Promise((resolve) => { setTimeout(resolve, Math.random() * (max - min) + min) }) diff --git a/src/services/functions/tools.js b/src/services/functions/tools.js index dbaab83..4d8dd59 100644 --- a/src/services/functions/tools.js +++ b/src/services/functions/tools.js @@ -1,18 +1,17 @@ -import wwebjs from 'whatsapp-web.js' -import fetch from 'node-fetch' -import reactions from '../../config/reactions.js' -import dayjs from 'dayjs' -import 'dayjs/locale/pt-br.js' import relativeTime from 'dayjs/plugin/relativeTime.js' +import reactions from '../../config/reactions.js' import { createUrl } from '../../config/api.js' +import logger from '../../logger.js' import FormData from 'form-data' +import 'dayjs/locale/pt-br.js' +import fetch from 'node-fetch' +import dayjs from 'dayjs' import sharp from 'sharp' -import logger from '../../logger.js' dayjs.locale('pt-br') dayjs.extend(relativeTime) -const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) +// const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) // // ================================ Main Functions ================================= @@ -33,9 +32,12 @@ export async function qrImageCreator (msg) { const url = await createUrl('image-creator', 'qr', { text: msg.body }) try { - const media = await wwebjs.MessageMedia.fromUrl(url, { unsafeMime: true }) - await msg.reply(media) - await msg.react(reactions.success) + await msg.reply({ + image: { + url + } + }) + await msg.react(msg.aux.db.command.emoji) } catch (error) { logger.error(error) await msg.reply('Erro ao criar QR Code') @@ -62,7 +64,7 @@ export async function qrTextCreator (msg) { const response = await fetch(url) const data = await response.json() await msg.reply('```' + data.result.string + '```') - await msg.react(reactions.success) + await msg.react(msg.aux.db.command.emoji) } catch (error) { logger.error(error) await msg.reply('Erro ao criar QR Code') @@ -76,7 +78,7 @@ export async function qrTextCreator (msg) { */ export async function qrReader (msg) { // if is not replying to a image - if (!msg.hasMedia && (msg.hasQuotedMsg && !msg.aux.quotedMsg.hasMedia)) { + if (!msg.hasMedia && (msg.hasQuotedMsg && !msg.quotedMsg.hasMedia)) { await msg.reply('Para ler um QR Code, responda uma imagem com !qr') await msg.react(reactions.error) return @@ -84,30 +86,31 @@ export async function qrReader (msg) { await msg.react(reactions.wait) - const media = msg.hasQuotedMsg ? await msg.aux.quotedMsg.downloadMedia() : await msg.downloadMedia() + const media = msg.hasQuotedMsg ? await msg.downloadMedia(true) : await msg.downloadMedia() if (!media) throw new Error('Error downloading media') if (!media.mimetype.includes('image')) { await msg.react(reactions.error) return await msg.reply('❌ Só consigo ler QR Codes em imagens') } - + // await msg.react(reactions.wait) // a clock doing a full circle - const spinner = ['🕛', '🕐', '🕑', '🕒', '🕓', '🕔', '🕕', '🕖', '🕗', '🕘', '🕙', '🕚'] - let spinnerIndex = 0 + // const spinner = ['🕛', '🕐', '🕑', '🕒', '🕓', '🕔', '🕕', '🕖', '🕗', '🕘', '🕙', '🕚'] + // let spinnerIndex = 0 - const reply = await msg.reply(`${spinner[spinnerIndex]} - Lendo QR code da imagem`) // send the first message - const interval = setInterval(async () => { - spinnerIndex++ - if (spinnerIndex === spinner.length) spinnerIndex = 0 + // const reply = await msg.reply(`${spinner[spinnerIndex]} - Lendo QR code da imagem`) // send the first message + // console.log('reply', reply) + // const interval = setInterval(async () => { + // spinnerIndex++ + // if (spinnerIndex === spinner.length) spinnerIndex = 0 - await reply.edit(`${spinner[spinnerIndex]} - Lendo QR code da imagem`) // edit the message - }, 1000) + // await reply.edit(`${spinner[spinnerIndex]} - Lendo QR code da imagem`) // edit the message + // }, 1000) // auto cancel if the process takes more than 30 seconds const timeout = setTimeout(async () => { - clearInterval(interval) + // clearInterval(interval) await msg.react(reactions.error) - await reply.edit('❌ - Tempo limite excedido') + await msg.reply('❌ - Tempo limite excedido') throw new Error('Timeout') }, 30_000) @@ -129,11 +132,11 @@ export async function qrReader (msg) { const qrResponse = await fetch(qrUrl) const qrData = await qrResponse.json() - clearInterval(interval) // stop the interval + // clearInterval(interval) // stop the interval clearTimeout(timeout) // stop the timeout - await reply.edit(`✅ - ${qrData.result}`) - await wait(1000) - await reply.edit(`✅ - ${qrData.result}`) - await msg.react(reactions.success) + await msg.reply(`✅ - ${qrData.result}`) + // await wait(1000) + // await reply.edit(`✅ - ${qrData.result}`) + await msg.react(msg.aux.db.command.emoji) } diff --git a/src/services/queue.js b/src/services/queue.js index c173846..44e311e 100644 --- a/src/services/queue.js +++ b/src/services/queue.js @@ -1,128 +1,250 @@ import importFresh from '../utils/importFresh.js' import logger from '../logger.js' -import { getClient } from '../index.js' +import { getSocket } from '../index.js' import { camelCase } from 'change-case' // // ===================================== Variables ====================================== // +/** + * @typedef {Object} Message + * @property {any} message - The message content. + */ + +/** + * @typedef {Object} QueueItem + * @property {number} waitUntil - The timestamp to wait until. + * @property {string} moduleName - The name of the module. + * @property {string} functionName - The name of the function. + * @property {Message} message - The message object. + */ + +/** + * @typedef {Object} Chat + * @property {boolean} isBusy - Indicates if the chat is busy. + * @property {number} lastEvent - The timestamp of the last event. + * @property {Array} queue - The queue of items. + * @property {number} spamWarning - The number of spam warnings. + * @property {number} lastSpamWarning - The timestamp of the last spam warning. + */ -const client = getClient() -const queue = [] -const waitTimeMax = 300 -const waitTimeMin = 0 -const waitTimeMultiplier = 100 -let waitTime = waitTimeMax // initial wait time +/** + * @typedef {Object.} Queue + */ +/** + * The queue of messages. + * @type {Queue} + */ +const queue = {} +const socket = getSocket() // // ==================================== Main Function ==================================== // +async function processQueue () { + if (Object.keys(queue).length > 0) { + // pick the first key from the queue, pick the first item from the queue + // reconstruct the message object with the current key at the end + const firstChatInQueueKey = Object.keys(queue)[0] + const firstChatInQueue = queue[firstChatInQueueKey] + delete queue[firstChatInQueueKey] + queue[firstChatInQueueKey] = firstChatInQueue // add it back to the queue + if (firstChatInQueue.queue.length === 0) { + return waitAndProcessQueue() // instantly process the next message + } + if (firstChatInQueue.isBusy) { + return waitAndProcessQueue(0, 0) // instantly process the next message + } + const firstMessageInQueue = firstChatInQueue.queue[0] + firstChatInQueue.queue = firstChatInQueue.queue.slice(1) + if (Date.now() < firstMessageInQueue.waitUntil) { + firstChatInQueue.queue.unshift(firstMessageInQueue) + return waitAndProcessQueue(0, 0) // instantly process the next message + } + firstChatInQueue.isBusy = true + // do not await this, so that we can process the next message in the queue + executeQueueItem(firstMessageInQueue.moduleName, firstMessageInQueue.functionName, firstMessageInQueue.message).then(() => { + queue[firstChatInQueueKey].isBusy = false + queue[firstChatInQueueKey].lastEvent = Date.now() + }) + } + + // // await random time between 0,5 and 1,5 seconds + return await waitAndProcessQueue() +} +processQueue() // start the queue processing +// +// ================================== Helper Functions ================================== +// /** - * Add a message to queue and return an array with the queue length and user queue length - * @param {import('whatsapp-web.js').ClientInfo} userId - * @param {string} moduleName - Name of the module to be imported e.g. 'sticker' - * @param {string} functionName - Name of the function to be called e.g. 'stickerText' - * @param {import('whatsapp-web.js').Message} msg - Message object - * @returns {Array} [queueLength, userQueueLength] - * + * Add the message to the queue + * @param {string} moduleName + * @param {string} functionName + * @param {Message} msg + * @returns {Promise<{waitUntil: number, messagesOnQueue: number, isSpam: boolean}>} */ -function addToQueue (userId, moduleName, functionName, msg) { - // if is a group, bypass the queue - if (msg.aux.chat.isGroup) { - bypassQueue(moduleName, functionName, msg) - return [0, 0] +export async function addToQueue (moduleName, functionName, msg) { + const id = msg.from + if (!queue[id]) { + initializeQueueForUser(id) } - const userIndex = queue.findIndex((user) => user.wid === userId) - if (userIndex !== -1) { - queue[userIndex].messagesQueue.push({ moduleName, functionName, message: msg }) - return [getQueueLength(), queue[userIndex].messagesQueue.length] + + const messagesOnQueue = queue[id].queue.length + const waitUntil = Date.now() + (messagesOnQueue * 3000) + const spamThreshold = 6 + + if (messagesOnQueue >= spamThreshold) { + const spamWarningResult = await handleSpamWarning(id, spamThreshold, msg) + if (spamWarningResult.isSpam) { + return spamWarningResult + } } - queue.push({ - wid: userId, - messagesQueue: [{ moduleName, functionName, message: msg }] - }) - return [getQueueLength(), 1] + return addMessageToQueue(id, waitUntil, moduleName, functionName, msg) } -async function bypassQueue (moduleName, functionName, msg) { - const module = await importFresh(`services/functions/${moduleName}.js`) - const camelCaseFunctionName = camelCase(functionName) - module[camelCaseFunctionName](msg) +/** + * Initialize the queue for the user + * @param {string} id + * @returns {void} + */ +function initializeQueueForUser (id) { + queue[id] = { + isBusy: false, + lastEvent: Date.now(), + queue: [], + spamWarning: 0, + lastSpamWarning: 0 + } } -async function processQueue () { - // set the wait time for the next round based on the queue length - const newWaitTime = Math.max(waitTimeMax - (queue.length * waitTimeMultiplier), waitTimeMin) - // noise between -150ms and 150ms to make the wait time more human-like - const noise = Math.floor(Math.random() * 300) - 150 - setWaitTime(newWaitTime + noise) - - if (queue.length === 0) return setTimeout(processQueue, waitTime) // if the queue is empty, wait and try again - - const user = queue.shift() // get the first user on the queue - - const currentMessage = user.messagesQueue.shift() // get the first message of that user - - /** @type {{moduleName: string, functionName: string, message: import('whatsapp-web.js').Message}} */ - const { moduleName, functionName, message: msg } = currentMessage - - const number = await client.getFormattedNumber(msg.from) - const camelCaseFunctionName = camelCase(functionName) - logger.info(`🛫 - ${number} - ${moduleName}.${camelCaseFunctionName}()`) - - try { - const module = await importFresh(`services/functions/${moduleName}.js`) // import the module - logger.debug(module) - - const fnPromisse = module[camelCaseFunctionName](msg) - fnPromisse.then((_result) => { - if (user.messagesQueue.length > 0) { - queue.push(user) // if there are more messages on the user queue, push it back to the queue - } else { - // mark the chat as read after 5 seconds - setTimeout(() => { - msg.aux.chat.sendSeen() - }, 5_000) - } - }).catch((err) => { - logger.error(err) - msg.react('❌') - }) - } catch (err) { - logger.fatal('Error executing module', moduleName, camelCaseFunctionName) - logger.error(err) +/** + * Handle the spam warning + * @param {string} id + * @param {number} spamThreshold + * @param {Message} msg + * @returns {Promise<{isSpam: boolean, messagesOnQueue: number}>} + */ +async function handleSpamWarning (id, spamThreshold, msg) { + if (Date.now() - queue[id].lastSpamWarning > 30_000) { + queue[id].spamWarning++ + queue[id].lastSpamWarning = Date.now() + + const { emoji, message } = getSpamWarningMessage(queue[id].spamWarning, spamThreshold) + + await wait(15_000) + await msg.react(emoji) + await msg.reply(message) + + if (queue[id].spamWarning >= 4) { + await wait(5_000) + await socket.updateBlockStatus(id, 'block') + } + return { isSpam: true, messagesOnQueue: queue[id].queue.length } } + return { isSpam: false } +} - setTimeout(processQueue, waitTime) // wait and process the next item on the queue +/** + * Get the spam warning message + * @param {number} spamWarning + * @param {number} spamThreshold + * @returns {{emoji: string, message: string}} + */ +function getSpamWarningMessage (spamWarning, spamThreshold) { + let emoji = '⚠️' + let message = '' + switch (spamWarning) { + case 4: + emoji = '🚫' + message = '🚫 - Você foi bloqueado por _spamming_ o bot! 😡' + break + case 3: + emoji = '🚨' + message = '🚨 - *ÚLTIMO AVISO!!!*\n\nNa próxima vez que você _spammar_ o bot te darei block!' + break + case 2: + message = `⚠️ - *ATENÇÃO!!!*\n\nJá avisei uma vez 😡, não _spamme_ o bot, máximo de ${spamThreshold} mensagens por minuto.\n\nSe continuar, você será bloqueado!` + break + default: + message = `⚠️ - *ATENÇÃO!!!*\n\nNão _spamme_ o bot, máximo de ${spamThreshold} mensagens por minuto.\n\nSe continuar, você será bloqueado!` + } + return { emoji, message } } -processQueue() +/** + * Add the message to the queue + * @param {string} id + * @param {number} waitUntil + * @param {string} moduleName + * @param {string} functionName + * @param {Message} msg + * @returns {{waitUntil: number, messagesOnQueue: number}} + */ +function addMessageToQueue (id, waitUntil, moduleName, functionName, msg) { + queue[id].queue.push({ + waitUntil, + moduleName, + functionName, + message: msg + }) -// -// ================================== Helper Functions ================================== -// -export function setWaitTime (time) { - // make sure the wait time is not lower than 1ms - const safeTime = Math.max(time, 1) - waitTime = safeTime + return { waitUntil, messagesOnQueue: queue[id].queue.length } } -export function getWaitTime () { - return waitTime +/** + * Wait for the given amount of time + * @param {number} ms + * @returns {Promise} + */ +async function wait (ms) { + return new Promise(resolve => setTimeout(resolve, ms)) } /** - * Get the number of messages on the queue, by user or total messages - * @returns {number} - * @param {string} by - 'user' or 'messages' - * @throws {Error} Invalid parameter + * Wait for a random amount of time and process the queue + * @param {number} min + * @param {number} max + * @returns {Promise} */ -export function getQueueLength (by = 'messages') { - if (by === 'user') return queue.length - if (by === 'messages') return queue.reduce((acc, user) => acc + user.messagesQueue.length, 0) - - throw new Error('Invalid parameter') +async function waitAndProcessQueue (min = 1000, max = 3000) { + const waitTime = Math.floor(Math.random() * (max - min + 1)) + min + await wait(waitTime) + processQueue() } -export { addToQueue, processQueue } +/** + * Execute the queue item + * @param {string} moduleName + * @param {string} functionName + * @param {Message} msg + * @returns {Promise} + */ +export async function executeQueueItem (moduleName, functionName, msg) { + // console.log('Executing queue item', moduleName, functionName, msg) + await msg.sendSeen() + const checkDisabled = await importFresh('validators/checkDisabled.js') + const isEnabled = await checkDisabled.default(msg) + if (!isEnabled) return logger.info(`⛔ - ${msg.from} - ${functionName} - Disabled`) + + const checkOwnerOnly = await importFresh('validators/checkOwnerOnly.js') + const isOwnerOnly = await checkOwnerOnly.default(msg) + if (isOwnerOnly) return logger.info(`🛂 - ${msg.from} - ${functionName} - Restricted to admins`) + + const module = await importFresh(`services/functions/${moduleName}.js`) + const camelCaseFunctionName = camelCase(functionName) + try { + await module[camelCaseFunctionName](msg) + } catch (error) { + logger.error(`Error with command ${camelCaseFunctionName}`) + console.error(error) + // const readMore = '​'.repeat(783) + const prefix = msg.aux.prefix ?? '!' + msg.react('❌') + let message = `❌ - Ocorreu um erro inesperado com o comando *${prefix}${msg.aux.function}*\n\n` + message += 'Se for possível, tira um print e manda para meu administrador nesse grupo aqui: ' + message += 'https://chat.whatsapp.com/CBlkOiMj4fM3tJoFeu2WpR\n\n' + message += JSON.stringify(error, null, 2) + msg.reply(message) + } +} diff --git a/temp/.gitkeep b/src/temp/.gitkeep similarity index 100% rename from temp/.gitkeep rename to src/temp/.gitkeep diff --git a/src/types.d.ts b/src/types.d.ts index 283c33a..0233002 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,51 +1,6 @@ -import { Client, Chat, Contact, Message, GroupParticipant } from 'whatsapp-web.js'; +import { makeWASocket } from '@whiskeysockets/baileys'; /** - * Auxiliar message data + * Baileys socket */ -export interface AuxiliarMessageData { - /** The client */ - client: Client; - /** The chat */ - chat: Chat; - /** The sender */ - sender: Contact; - /** If the sender is the bot */ - senderIsMe: boolean; - /** If the message mentions the bot */ - mentionedMe: boolean; - /** The original message */ - originalMsg: Message; - /** The message history */ - history: Message[]; - /** The original message body */ - originalBody: string; - /** If the message is a function */ - isFunction: boolean; - /** The message prefix */ - prefix: string; - /** The message function */ - function: string; - /** If the original message is a function */ - hasOriginalFunction: boolean; - /** The original message function */ - originalFunction: string; - /** The bot id */ - me: string; - /** The mentions */ - mentions: string[]; - /** If the bot is mentioned */ - amIMentioned: boolean; - /** The chat participants */ - participants: GroupParticipant[]; - /** The chat admins */ - admins: string[]; - /** If the sender is admin */ - isSenderAdmin: boolean; - /** If the bot is admin */ - isBotAdmin: boolean; - /** If the chat is the sticker group */ - isStickerGroup: boolean; -} - -export type WWebJSMessage = Message & { aux: AuxiliarMessageData }; \ No newline at end of file +export interface WSocket extends ReturnType { } \ No newline at end of file diff --git a/src/utils/converters.js b/src/utils/converters.js new file mode 100644 index 0000000..b4452f4 --- /dev/null +++ b/src/utils/converters.js @@ -0,0 +1,75 @@ +import FormData from 'form-data' +import fetch from 'node-fetch' +import { load } from 'cheerio' + +// +// ===================================== Variables ====================================== +// + +const EZ_GIF_URL = 'https://ezgif.com/webp-to-mp4?url=' + +// +// ==================================== Main Function ==================================== +// +/** + * Convert Webp to Mp4 + * @param {String} path + * @returns {Promise<{status: Boolean, message: String, result: String}>} + */ +async function webpToMp4 (url) { + const response = await fetchWithRetry(EZ_GIF_URL + url) + const { action, file } = await parseHtml(response) + + const formData = new FormData() + formData.append('file', file) + + const responseThen = await fetchWithRetry(action + '?ajax=true', { + method: 'POST', + body: formData, + headers: formData.getHeaders() + }) + + const $then = load(responseThen) + const resultFile = $then('video > source').attr('src') + + return resultFile.startsWith('//') ? 'https:' + resultFile : resultFile +} + +export { webpToMp4 } + +// +// ================================== Helper Functions ================================== +// + +/** + * Fetch with retry + * @param {String} url + * @param {Object} options + * @param {Number} retries + * @returns {Promise} + */ +async function fetchWithRetry (url, options = {}, retries = 3) { + for (let i = 0; i < retries; i++) { + try { + const response = await fetch(url, options) + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`) + return response + } catch (error) { + if (i < retries - 1) continue + throw error + } + } +} + +/** + * Parse HTML + * @param {Response} response + * @returns {Promise<{action: String, file: String}>} + */ +async function parseHtml (response) { + const data = await response.text() + const $ = load(data) + const action = $('form.ajax-form').attr('action') + const file = $('form.ajax-form > input[type=hidden]').attr('value') + return { action, file } +} diff --git a/src/utils/sticker.js b/src/utils/sticker.js index 2503ca5..996ebbf 100644 --- a/src/utils/sticker.js +++ b/src/utils/sticker.js @@ -164,8 +164,8 @@ class Util { /** * Sticker metadata. * @typedef {Object} StickerMetadata - * @property {string} [name] * @property {string} [author] + * @property {string} [pack] * @property {string[]} [categories] */ @@ -182,22 +182,22 @@ class Util { if (media.mimetype.includes('image')) { webpMedia = await this.formatImageToWebpSticker(media, crop) } else if (media.mimetype.includes('video')) { webpMedia = await this.formatVideoToWebpSticker(media, crop) } else { throw new Error('Invalid media format') } - if (metadata.name || metadata.author) { - const img = new webp.Image() - const hash = this.generateHash(32) - const stickerPackId = hash - const packname = metadata.name - const author = metadata.author - const categories = metadata.categories || [''] - const json = { 'sticker-pack-id': stickerPackId, 'sticker-pack-name': packname, 'sticker-pack-publisher': author, emojis: categories } - const exifAttr = Buffer.from([0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x41, 0x57, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00]) - const jsonBuffer = Buffer.from(JSON.stringify(json), 'utf8') - const exif = Buffer.concat([exifAttr, jsonBuffer]) - exif.writeUIntLE(jsonBuffer.length, 14, 4) - await img.load(Buffer.from(webpMedia.data, 'base64')) - img.exif = exif - webpMedia.data = (await img.save(null)).toString('base64') - } + // if (metadata.author || metadata.pack) { + const img = new webp.Image() + const hash = this.generateHash(32) + const stickerPackId = hash + const packname = metadata.author // Yes, I know it is twisted + const author = metadata.pack // ¯\_(ツ)_/¯ + const categories = metadata.categories || [''] + const json = { 'sticker-pack-id': stickerPackId, 'sticker-pack-name': packname, 'sticker-pack-publisher': author, emojis: categories } + const exifAttr = Buffer.from([0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x41, 0x57, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00]) + const jsonBuffer = Buffer.from(JSON.stringify(json), 'utf8') + const exif = Buffer.concat([exifAttr, jsonBuffer]) + exif.writeUIntLE(jsonBuffer.length, 14, 4) + await img.load(Buffer.from(webpMedia.data, 'base64')) + img.exif = exif + webpMedia.data = (await img.save(null)).toString('base64') + // } return webpMedia } diff --git a/src/validators/message.js b/src/validators/message.js index d027fd3..769a670 100644 --- a/src/validators/message.js +++ b/src/validators/message.js @@ -2,6 +2,7 @@ import importFresh from '../utils/importFresh.js' import fs from 'fs/promises' import logger from '../logger.js' import reactions from '../config/reactions.js' +import { getSocket } from '../index.js' // // ================================ Variables ================================= // @@ -10,11 +11,13 @@ const commandless = (msg, aux) => { stickersFNstickerLyPack: msg.body && msg.body.startsWith('https://sticker.ly/s/'), stickersFNstickerCreator: ( (msg.hasMedia && ['image', 'video', 'document'].includes(msg.type)) || - (msg.hasQuotedMsg && (aux.quotedMsg.hasMedia && ['image', 'video', 'document'].includes(aux.quotedMsg.type)))), + (msg.hasQuotedMsg && (msg.quotedMsg.hasMedia && ['image', 'video', 'document'].includes(msg.quotedMsg.type)))), stickersFNtextSticker: msg.body && msg.type === 'chat', miscellaneousFNtranscribe: msg.hasMedia && ['audio', 'ptt'].includes(msg.type) } } + +const socket = getSocket() // // ================================ Main Functions ================================= // @@ -30,28 +33,15 @@ const commandless = (msg, aux) => { */ export default async (msg) => { const aux = {} // auxiliar variables - aux.client = (await import('../index.js')).getClient() - aux.chat = await msg.getChat() - aux.sender = await msg.getContact() aux.senderIsMe = msg.fromMe - aux.mentionedMe = msg.mentionedIds.includes(aux.client.info.wid._serialized) + aux.me = msg.bot.id.split(':')[0] + '@s.whatsapp.net' + aux.mentionedMe = msg.mentionedIds ? msg.mentionedIds.includes(aux.me) : false if (aux.mentionedMe) { - msg.body = msg.body.replace(new RegExp(`@${aux.client.info.wid.user}`, 'g'), '').trim() - } - - if (msg.hasQuotedMsg) { - aux.quotedMsg = await msg.getQuotedMessage() + msg.body = msg.body.replace(new RegExp(`@${aux.me.split('@')[0]}`, 'g'), '').trim() } - let msgCurrent = msg - const msgPrevious = [] - while (msgCurrent.hasQuotedMsg) { - msgPrevious.push(msgCurrent) - msgCurrent = await msgCurrent.getQuotedMessage() - } - aux.originalMsg = msgCurrent - msgPrevious.push(aux.originalMsg) - aux.history = msgPrevious.reverse() + aux.originalMsg = msg + aux.history = [aux.originalMsg] // Check if the message is a command const prefixes = await importFresh('config/bot.js').then(config => config.prefixes) @@ -74,21 +64,16 @@ export default async (msg) => { } } - aux.me = aux.client.info.wid._serialized ? aux.client.info.wid._serialized : aux.client.info.wid aux.mentions = msg.mentionedIds - if (typeof aux.mentions[0] !== 'string') { - // sometimes is an array of objects, sometimes is an array of strings - aux.mentions = aux.mentions.map((mention) => mention._serialized) // convert to array of strings - } - - aux.amIMentioned = aux.mentions.includes(aux.me) - aux.participants = aux.chat.isGroup ? aux.chat.participants : [] - aux.admins = aux.chat.isGroup ? aux.participants.filter((p) => p.isAdmin || p.isSuperAdmin).map((p) => p.id._serialized) : [] + aux.amIMentioned = aux.mentions ? aux.mentions.includes(aux.me) : false + aux.group = msg.isGroup ? await socket.groupMetadata(msg.from) : '' + aux.participants = msg.isGroup ? aux.group.participants : [] + aux.admins = msg.isGroup ? aux.participants.filter((p) => !!p.admin).map((p) => p.id) : [] aux.isSenderAdmin = aux.admins.includes(msg.author) aux.isBotAdmin = aux.admins.includes(aux.me) const stickerGroup = '120363187692992289@g.us' - aux.isStickerGroup = aux.chat.isGroup ? aux.chat.id._serialized === stickerGroup : false + aux.isStickerGroup = msg.isGroup ? msg.from === stickerGroup : false try { msg.aux = aux @@ -126,7 +111,7 @@ export default async (msg) => { } } const prefixesWithFallback = await importFresh('config/bot.js').then(config => config.prefixesWithFallback) - if (prefixesWithFallback.includes(aux.prefix) === false) { + if (prefixesWithFallback.includes(aux.prefix) === false && msg.aux.originalBody !== '...') { if (!aux.hasOriginalFunction) { await msg.react(reactions.confused) } @@ -144,8 +129,10 @@ export default async (msg) => { msg.body = aux.originalBody // Send incorrect function reaction - if (aux.isFunction) return false // if any function reach this point, it is an incorrect function - if (aux.chat.isGroup && !aux.mentionedMe) { + if (aux.isFunction) { + return false // if any function reach this point, it is an incorrect function + } + if (msg.isGroup && !aux.mentionedMe) { if (aux.isStickerGroup && msg.type === 'chat') { return false // ignore texts in sticker group } diff --git a/src/validators/messageType.js b/src/validators/messageType.js new file mode 100644 index 0000000..ec5478b --- /dev/null +++ b/src/validators/messageType.js @@ -0,0 +1,185 @@ +import logger from '../logger.js' +// +// ================================ Variables ================================= +// +const types = { + conversation: 'chat', + extendedTextMessage: 'chat', + audioMessage: 'audio', + pttMessage: 'ptt', + imageMessage: 'image', + videoMessage: 'video', + documentMessage: 'document', + stickerMessage: 'sticker', + locationMessage: 'location', + liveLocationMessage: 'live_location', + contactMessage: 'vcard', + contactsArrayMessage: 'multi_vcard', + orderMessage: 'order', + REVOKE: 'revoked', + productMessage: 'product', + UNKNOWN: 'unknown', + groupInviteMessage: 'groups_v4_invite', + listMessage: 'list', + listResponseMessage: 'list_response', + buttonsMessage: 'buttons', + buttonsResponseMessage: 'buttons_response', + sendPaymentMessage: 'payment', + requestPaymentMessage: 'payment', + declinePaymentRequestMessage: 'payment', + cancelPaymentRequestMessage: 'payment', + interactiveMessage: 'interactive', + interactiveResponseMessage: 'interactive_response', + protocolMessage: 'protocol', + reactionMessage: 'reaction', + templateButtonReplyMessage: 'template_button_reply', + pollCreationMessage: 'poll_creation', + pollUpdateMessage: 'poll_update', + editedMessage: 'edited' +} +// +// ================================ Main Functions ================================= +// +/** + * Detect message type using the same pattern as WWebJS + * @param {import('@whiskeysockets/baileys').proto.IWebMessageInfo} msg + */ +export default (msg) => { + if (!msg.message) { + logger.warn('Message has no message', msg) + return types.UNKNOWN + } + const keysToIgnore = ['messageContextInfo'] + const hasKeys = Object.keys(msg).length > 1 + let firstKey = Object.keys(msg)[0] + let incomingType = firstKey + if (hasKeys) { + const keys = Object.keys(msg.message) + .filter(key => !keysToIgnore.includes(key)) + if (keys.length === 0) return types.UNKNOWN + + firstKey = keys[0] + incomingType = firstKey + + if (keys.length > 1) { + // senderKeyDistributionMessage + if (keys.includes('senderKeyDistributionMessage')) { + // delete this key and continue from msg.message + delete msg.message.senderKeyDistributionMessage + // and make sure that the other key is the FIRST key of msg.message + const newMessage = {} + const leftOverKeys = Object.keys(msg.message) + const keyName = keys[1] + incomingType = keyName + newMessage[keyName] = msg.message[keyName] + leftOverKeys.forEach(key => { + if (key !== keyName) newMessage[key] = msg.message[key] + }) + msg.message = newMessage + } else { + logger.warn('Message has more than one key', msg) + } + } + } + + while (incomingType === 'ephemeralMessage') { + msg.message = msg.message[firstKey].message + firstKey = Object.keys(msg.message)[0] + if (!firstKey) throw new Error('firstKey is undefined') + incomingType = firstKey + } + // while (incomingType === 'ephemeralMessage') { + // const firstInside = msg[] + // } + // nometime msg.message comes with layers of "ephemeralMessage" + // loop digging until it finds something that is not ephemeralMessage + // if (incomingType === 'ephemeralMessage') { + // msg = msg[firstKey].message + // console.log(incomingType, msg) + // // firstKey = Object.keys(msg)[0] + // // incomingType = firstKey + // } + + /** + * Handle viewOnceMessage && groupMentionedMessage + */ + if ( + incomingType.startsWith('viewOnce') || + incomingType === 'groupMentionedMessage' + ) { + const keys = Object.keys((msg.message ? msg.message : msg)[firstKey].message).filter(key => !keysToIgnore.includes(key)) + if (keys.length) { + incomingType = keys[0] + if (msg.message) msg.message = msg.message[firstKey].message + else msg = msg[firstKey].message + } + } + + /** + * Check if audioMessage is ptt + */ + if (incomingType === 'audioMessage') { + incomingType = parseAudioTypes(msg) + } + + /** + * Parse protocolMessage + */ + if (incomingType === 'protocolMessage') { + incomingType = parseProtocolTypes(msg.message.protocolMessage.type) + } + + const typeName = types[incomingType] || types.UNKNOWN + logger.trace('messageType', { incomingType, typeName }) + return { type: typeName, updatedMsg: msg } +} + +// +// ================================== Helper Functions ================================== +// +/** + * Parse audio types + * @param {import('@whiskeysockets/baileys').proto.IWebMessageInfo} msg + */ +function parseAudioTypes (msg) { + const audioType = (msg.message ? msg.message : msg).audioMessage.ptt ? 'pttMessage' : 'audioMessage' + if (audioType === 'pttMessage') return audioType + + // if it is audio, and still can be a forwarded ppt + if ((msg.message ? msg.message : msg).audioMessage.contextInfo?.isForwarded) { + const hasWaveform = !!(msg.message ? msg.message : msg).audioMessage.waveform + if (hasWaveform) return 'pttMessage' + + const mimetype = (msg.message ? msg.message : msg).audioMessage.mimetype + const isOpus = mimetype === 'audio/ogg; codecs=opus' + if (isOpus) return 'pttMessage' + } + + return audioType +} + +/** + * Parse protocol types + * @param {number} type + */ +function parseProtocolTypes (type) { + const types = { + 0: 'REVOKE', + 3: 'EPHEMERAL_SETTING', + 4: 'EPHEMERAL_SYNC_RESPONSE', + 5: 'HISTORY_SYNC_NOTIFICATION', + 6: 'APP_STATE_SYNC_KEY_SHARE', + 7: 'APP_STATE_SYNC_KEY_REQUEST', + 8: 'MSG_FANOUT_BACKFILL_REQUEST', + 9: 'INITIAL_SECURITY_NOTIFICATION_SETTING_SYNC', + 10: 'APP_STATE_FATAL_EXCEPTION_NOTIFICATION', + 11: 'SHARE_PHONE_NUMBER', + 14: 'MESSAGE_EDIT', + 16: 'PEER_DATA_OPERATION_REQUEST_MESSAGE', + 17: 'PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE', + 18: 'REQUEST_WELCOME_MESSAGE', + 19: 'BOT_FEEDBACK_MESSAGE', + 20: 'MEDIA_NOTIFY_MESSAGE' + } + return types[type] || 'protocolMessage' +}