@aidulcandra/simple-wa-bot
Version:
A baileys wrapper module
612 lines (547 loc) • 22.9 kB
JavaScript
const vCard = require("vcard-parser")
const {extractMessageContent, decryptPollVote} = require("baileys")
const fs = require("fs")
const {randomUUID, createHash} = require("crypto")
const {createSticker} = require("./utils.js")
class Program {
constructor (options) {
this.output = null
this.options = options || {}
this.uid = randomUUID()
}
ifVarIs (name, value) {
this.options.checkVarIs ??= {}
this.options.checkVarIs[name] = value
return this
}
ifVarExists (name) {
this.options.checkVarExists ??= []
this.options.checkVarExists.push(name)
return this
}
ifVarNotExists (name) {
this.options.checkVarNotExists ??= []
this.options.checkVarNotExists.push(name)
return this
}
if (conditionFunction) {
this.options.conditionFunctions ??= []
this.options.conditionFunctions.push(conditionFunction)
return this
}
text (output) {
this.output = output
this.options.randomOutput = false
return this
}
textRandom(...output) {
this.output = [...output].flat()
this.options.randomOutput = true
return this
}
sendMenu () {
this.options.sendMenu = true
return this
}
image (link) {
this.options.image = link
return this
}
video (link) {
this.options.video = link
return this
}
gif (link) {
this.options.gif = link
return this
}
audio (link, mimetype) {
this.options.audio = link
this.options.audioType = mimetype
return this
}
sticker (link, options) {
this.options.sticker = {
link, ...options
}
return this
}
document (link, mimetype, fileName) {
this.options.document = link
this.options.documentType = mimetype
this.options.documentFileName = fileName
return this
}
contacts (contacts) {
this.options.contacts = contacts
return this
}
location (latitude=0, longitude=0) {
this.options.location = {latitude,longitude}
return this
}
poll (title, choices, multiple = false) {
this.options.poll = {title, choices, multiple}
return this
}
showTyping () {
this.options.showTyping = true
return this
}
showRecording () {
this.options.showRecording = true
return this
}
run (f, fname) {
this.options.run = f
this.options.runFunctionName = fname
return this
}
stopHere () {
this.options.stopHere = true
return this
}
setVar (name, value) {
this.options.setVar ??= {}
this.options.setVar[name] = value
return this
}
blacklist (ids) {
this.options.blacklist = ids
return this
}
whitelist (ids) {
this.options.whitelist = ids
return this
}
to (ids) {
this.options.to = ids
return this
}
}
class InputReaderProgram extends Program {
constructor (input, options) {
super(options)
this.input = input
}
privateOnly () {
this.options.privateOnly = true
this.options.groupOnly = false
this.options.adminOnly = false
return this
}
groupOnly () {
this.options.groupOnly = true
this.options.privateOnly = false
return this
}
adminOnly () {
this.options.privateOnly = false
this.options.groupOnly = true
this.options.adminOnly = true
return this
}
ownerOnly () {
this.options.ownerOnly = true
return this
}
mentionedOnly () {
this.options.mentionedOnly = true
return this
}
quotedOnly () {
this.options.quotedOnly = true
return this
}
in (...i) {
const ids = i.flat()
this.options.in = ids
return this
}
from (...i) {
const ids = i.flat()
this.options.from = ids
return this
}
notIn (...i) {
const ids = i.flat()
this.options.notIn = ids
return this
}
notFrom (...i) {
const ids = i.flat()
this.options.notFrom = ids
return this
}
includeBlacklist () {
this.options.includeBlacklist = true
return this
}
similarity (value) {
this.options.similarityThreshold = value
return this
}
ignoreCase () {
this.options.ignoreCase = true
return this
}
withImage () {
this.options.withImage = true
return this
}
withVideo () {
this.options.withVideo = true
return this
}
withGif () {
this.options.withGif = true
return this
}
withAudio () {
this.options.withAudio = true
return this
}
withSticker () {
this.options.withSticker = true
return this
}
withDocument () {
this.options.withDocument = true
return this
}
withContacts () {
this.options.withContacts = true
return this
}
withLocation () {
this.options.withLocation = true
return this
}
withPoll () {
this.options.withPoll = true
return this
}
reply (output) {
this.output = output
this.options.reply = true
this.options.randomOutput = false
return this
}
replyRandom (...output) {
this.output = [output].flat()
this.options.reply = true
this.options.randomOutput = true
return this
}
react (emoji) {
this.options.react = emoji
return this
}
forward () {
this.options.forward = true
return this
}
delete () {
this.options.delete = true
return this
}
read () {
this.options.read = true
return this
}
stripMentions () {
this.options.stripMentions = true
return this
}
hideCommand() {
this.options.hideCommand = true
return this
}
}
class StartupProgram extends Program {
constructor (options) {
super(options)
}
}
class ScheduledProgram extends Program {
constructor (time, options) {
super(options)
this.time = time
}
}
class Message {
constructor (message) {
const realMessage = extractMessageContent(
message.message?.protocolMessage?.editedMessage ||
message.message?.groupMentionedMessage?.message ||
message.message?.viewOnceMessage?.message ||
message.message?.viewOnceMessageV2?.message ||
message.message
)
this.id = message.key?.id
this.text = realMessage?.conversation
|| realMessage?.extendedTextMessage?.text
|| realMessage?.documentMessage?.caption
|| realMessage?.imageMessage?.caption
|| realMessage?.liveLocationMessage?.caption
|| realMessage?.videoMessage?.caption
|| realMessage?.reactionMessage?.text
|| realMessage?.groupMentionedMessage?.text
|| realMessage?.pollCreationMessage?.name
|| realMessage?.pollCreationMessageV2?.name
|| realMessage?.pollCreationMessageV3?.name
|| realMessage?.pollCreationMessageV4?.name
|| realMessage?.pollCreationMessageV5?.name
this.saveTextTo = path => fs.promises.writeFile(path, this.text||"")
this.isEdit = Boolean(message.message?.protocolMessage?.editedMessage)
const messageObjectNames = Object.keys(realMessage||{})
.filter(k=>k.includes("Message"))
for (const name of messageObjectNames) {
if (realMessage?.[name]?.contextInfo?.quotedMessage) {
const remoteJid = message.key.remoteJid
const participant = realMessage[name].contextInfo.participant || remoteJid
this.quoted = Message.from({
key: {
remoteJid,
fromMe: participant == classOptions.myId, //
id: realMessage[name].contextInfo.stanzaId,
participant,
},
messageTimestamp: 0, //
message:realMessage[name].contextInfo.quotedMessage,
broadcast: false, //
pushName: classOptions.helperFunctions.getPushname(realMessage[name].contextInfo.participant),
})
this.getQuoted = () => classOptions.helperFunctions.getMessageFromStore(realMessage[name].contextInfo.stanzaId, remoteJid) || this.quoted
}
if (realMessage?.[name]?.contextInfo?.expiration) this.disappearingTime = realMessage[name].contextInfo.expiration
if (realMessage?.protocolMessage) this.protocolData = {}
if (realMessage?.protocolMessage?.ephemeralExpiration) {
this.protocolData.chatDisappearingTime = realMessage.protocolMessage.ephemeralExpiration
}
}
this.room = message.key.remoteJid
const senderId = message.key.fromMe ? classOptions.myId : (message.key.participant || message.key.remoteJid)
this.sender = {
id: senderId,
isMe: message.key.fromMe,
name: message.key.fromMe ? classOptions.helperFunctions.getBotName() : message.pushName,
isBlacklisted: classOptions.helperFunctions.blacklistCheck(senderId),
isOwner: classOptions.helperFunctions.isOwner(senderId),
getPPUrl: ()=>classOptions.helperFunctions.getPPUrl(senderId)
}
this.type = realMessage?.conversation ? 'text'
: realMessage?.extendedTextMessage ? 'text'
: realMessage?.documentMessage ? 'document'
: realMessage?.imageMessage ? 'image'
: realMessage?.locationMessage ? 'location'
: realMessage?.liveLocationMessage ? 'livelocation'
: realMessage?.videoMessage?.gifPlayback ? 'gif'
: realMessage?.videoMessage ? 'video'
: realMessage?.audioMessage ? 'audio'
: realMessage?.stickerMessage ? 'sticker'
: realMessage?.contactMessage ? 'contacts'
: realMessage?.contactsArrayMessage ? 'contacts'
: realMessage?.reactionMessage ? 'reaction'
: realMessage?.pollCreationMessage ? 'poll'
: realMessage?.pollCreationMessageV2 ? 'poll'
: realMessage?.pollCreationMessageV3 ? 'poll'
: realMessage?.pollCreationMessageV4 ? 'poll'
: realMessage?.pollCreationMessageV5 ? 'poll'
: realMessage?.pollUpdateMessage ? 'vote'
: realMessage?.protocolMessage ? 'protocol'
: Object.keys(realMessage||{}).filter(k=>!['key'].includes(k))?.[0]
this.isMedia = ['image','video','gif','audio','sticker','document'].includes(this.type)
if (this.isMedia) {
const mediaObject = {
getBuffer:()=>classOptions.helperFunctions.downloadMediaMessage(message,'buffer',{},{reuploadRequest:classOptions.socket.updateMediaMessage}),
getStream:()=>classOptions.helperFunctions.downloadMediaMessage(message,'stream',{},{reuploadRequest:classOptions.socket.updateMediaMessage}),
saveTo: path=>classOptions.helperFunctions.saveMedia(message, path)
}
if (this.type == 'sticker') realMessage.stickerMessage.url = "https://mmg.whatsapp.net" + realMessage.stickerMessage.directPath + "&mms3=true"
if (this.type == 'audio') mediaObject.duration = realMessage?.audioMessage?.seconds
this[this.type] = mediaObject
}
if (this.type == 'contacts') {
const parseVcard = (vc) => {
const data = vCard.parse(vc)
const name = data.fn[0].value
const number = data.tel[0].value
return {name, number}
}
this.contacts = [realMessage.contactMessage?.vcard || realMessage.contactsArrayMessage?.contacts.map(c=>c.vcard)].flat().filter(x=>x).map(parseVcard)
}
if (this.type == 'location') {
this.location = {
latitude:realMessage.locationMessage?.degreesLatitude,
longitude:realMessage.locationMessage?.degreesLongitude
}
}
if (this.type == 'reaction') {
this.reactedId = realMessage?.reactionMessage?.key?.id
}
if (this.type == 'poll') {
const pm = realMessage?.pollCreationMessage
|| realMessage?.pollCreationMessageV2
|| realMessage?.pollCreationMessageV3
|| realMessage?.pollCreationMessageV4
|| realMessage?.pollCreationMessageV5
this.poll = {
title: pm?.name,
choices: pm?.options?.map(o=>o.optionName) || [],
multiple: pm?.selectableOptionsCount != 1,
votes: {}
}
for (const c of this.poll.choices) this.poll.votes[c] = []
}
if (this.type == 'vote') {
const pcmKey = realMessage?.pollUpdateMessage?.pollCreationMessageKey
const pollCreationMessage = classOptions.helperFunctions.getMessageFromStore(pcmKey.id, pcmKey.remoteJid)
if (!pollCreationMessage) {
console.log(`Poll message not found in store for this vote`)
this.vote = null
}
else {
const pcmBM = pollCreationMessage.getBaileysMessage()
const pollContext = {
pollCreatorJid: realMessage?.pollUpdateMessage?.pollCreationMessageKey?.participant,
pollMsgId: realMessage?.pollUpdateMessage?.pollCreationMessageKey?.id,
pollEncKey: pcmBM.message?.messageContextInfo?.messageSecret,
voterJid: message.key?.participant
}
const pcm = pcmBM.message?.pollCreationMessage
|| pcmBM.message?.pollCreationMessageV2
|| pcmBM.message?.pollCreationMessageV3
|| pcmBM.message?.pollCreationMessageV4
|| pcmBM.message?.pollCreationMessageV5
const voteMsg = decryptPollVote(realMessage?.pollUpdateMessage?.vote, pollContext)
const options = pcm?.options?.map(o=>o.optionName)
this.vote = {
pollId: pcmBM.key.id,
list: voteMsg?.selectedOptions?.map(o=>options?.find(opt=>hashPollOption(opt) == o.toString('base64')))
}
const votes = pollCreationMessage?.poll?.votes
const choices = pollCreationMessage?.poll?.choices
for (const c of choices) {
if (this.vote.list.includes(c)) {
if (!votes[c].includes(this.sender.id)) votes[c].push(this.sender.id)
} else {
if (votes[c].includes(this.sender.id)) votes[c].splice(votes[c].indexOf(this.sender.id), 1)
}
}
}
}
this.mentions = realMessage?.extendedTextMessage?.contextInfo?.mentionedJid || []
this.groupMentions = realMessage?.extendedTextMessage?.contextInfo?.groupMentions || []
// this.baileysMessage = message
this.getBaileysMessage = ()=>message
}
async reply (text) {
const ephemeralExpiration = await classOptions.helperFunctions.getEphemeralExpiration(this.room)
const mentions = classOptions.helperFunctions.getMentionsFromText(text)
return createMessage(await classOptions.helperFunctions.sendMessage(this.room, {text, mentions}, {quoted:this.getBaileysMessage(), ephemeralExpiration}))
}
async replyImage (source, caption) {
const ephemeralExpiration = await classOptions.helperFunctions.getEphemeralExpiration(this.room)
const media = Buffer.isBuffer(source) ? source : {url:source}
const mentions = classOptions.helperFunctions.getMentionsFromText(caption)
return createMessage(await classOptions.helperFunctions.sendMessage(this.room, {image:media, caption, mentions}, {quoted:this.getBaileysMessage(), ephemeralExpiration}))
}
async replyVideo (source, caption) {
const ephemeralExpiration = await classOptions.helperFunctions.getEphemeralExpiration(this.room)
const media = Buffer.isBuffer(source) ? source : {url:source}
const mentions = classOptions.helperFunctions.getMentionsFromText(caption)
return createMessage(await classOptions.helperFunctions.sendMessage(this.room, {video:media, caption, mentions}, {quoted:this.getBaileysMessage(), ephemeralExpiration}))
}
async replyGif (source, caption) {
const ephemeralExpiration = await classOptions.helperFunctions.getEphemeralExpiration(this.room)
const media = Buffer.isBuffer(source) ? source : {url:source}
const mentions = classOptions.helperFunctions.getMentionsFromText(caption)
return createMessage(await classOptions.helperFunctions.sendMessage(this.room, {video:media, caption, gifPlayback:true, mentions}, {quoted:this.getBaileysMessage(), ephemeralExpiration}))
}
async replyAudio (source, mimetype) {
const ephemeralExpiration = await classOptions.helperFunctions.getEphemeralExpiration(this.room)
const media = Buffer.isBuffer(source) ? source : {url:source}
return createMessage(await classOptions.helperFunctions.sendMessage(this.room, {audio:media, mimetype:mimetype||'audio/mp4'}, {quoted:this.getBaileysMessage(), ephemeralExpiration}))
}
async replySticker (source, options) {
const ephemeralExpiration = await classOptions.helperFunctions.getEphemeralExpiration(this.room)
// const buff = await sharp(source).webp().toBuffer()
const sticker = await createSticker(source, {
pack: options?.pack,
author: options?.author,
type: options?.type,
quality: options?.quality,
background: options?.background,
categories: options?.categories
})
return createMessage(await classOptions.helperFunctions.sendMessage(this.room, sticker, {quoted:this.getBaileysMessage(), ephemeralExpiration}))
}
async replyDocument(source, mimetype, fileName, caption) {
const ephemeralExpiration = await classOptions.helperFunctions.getEphemeralExpiration(this.room)
const media = Buffer.isBuffer(source) ? source : {url:source}
return createMessage(await classOptions.helperFunctions.sendMessage(this.room, {document:media, mimetype, fileName, caption}, {quoted:this.getBaileysMessage(), ephemeralExpiration}))
}
async replyContacts(contacts) {
const ephemeralExpiration = await classOptions.helperFunctions.getEphemeralExpiration(this.room)
const vcards = contacts.map(c => {
return {vcard:`BEGIN:VCARD\nVERSION:3.0\nFN:${c.name}\nTEL;type=CELL;type=VOICE;waid=${c.number.replace(/\D/g,"")}:${c.number}\nEND:VCARD`}
})
return createMessage(await classOptions.helperFunctions.sendMessage(this.room, {contacts:{contacts:vcards}}, {quoted:this.getBaileysMessage(), ephemeralExpiration}))
}
async replyLocation(latitude=0, longitude=0) {
const ephemeralExpiration = await classOptions.helperFunctions.getEphemeralExpiration(this.room)
const location = {degreesLatitude:latitude, degreesLongitude:longitude}
return createMessage(await classOptions.helperFunctions.sendMessage(this.room, {location}, {ephemeralExpiration}))
}
async replyPoll(title, choices, multiple = false) {
const ephemeralExpiration = await classOptions.helperFunctions.getEphemeralExpiration(this.room)
const poll = {name: title, values: choices, selectableCount: multiple ? 0 : 1}
return createMessage(
await classOptions.helperFunctions.sendMessage(this.room, {poll}, {quoted:this.getBaileysMessage(), ephemeralExpiration})
)
}
async edit (text) {
const mentions = await classOptions.helperFunctions.getMentionsFromText(text)
return createMessage(await classOptions.helperFunctions.sendMessage(this.room, {text, edit:this.getBaileysMessage().key, mentions}))
}
async forward (to) {
return createMessage(await classOptions.helperFunctions.sendMessage(to, {forward:this.getBaileysMessage()}))
}
async react (emoji) {
return createMessage(await classOptions.helperFunctions.sendMessage(this.room, {react:{text:emoji, key:this.getBaileysMessage().key}}))
}
async delete () {
return createMessage(await classOptions.helperFunctions.sendMessage(this.room, {delete:this.getBaileysMessage().key}))
}
async read () {
return classOptions.helperFunctions.readMessages([this.getBaileysMessage().key])
}
saveToStore () {
return classOptions.helperFunctions.saveMessageToStore(this)
}
removeFromStore () {
return classOptions.helperFunctions.removeMessageFromStore(this)
}
static from (message) {
return new Message(message)
}
}
const classOptions = {
socket: null,
helperFunctions: {}
}
async function createMessage(message) {
const m = new Message(message)
if (m.room.endsWith("@g.us") && m.sender.id.endsWith("@lid")) {
const gd = await classOptions.helperFunctions.getGroupData(m.room, true)
m.sender.lid = m.sender.id
m.sender.id = gd?.content?.[0]?.content?.filter(c=>c.tag=="participant")?.find(c=>c.attrs.jid==m.sender.id)?.attrs?.phone_number
}
if (m.type=="poll") m.saveToStore()
return m
}
function hashPollOption (name) {
return createHash('sha256').update(Buffer.from(name)).digest().toString('base64')
}
module.exports = {InputReaderProgram, StartupProgram, ScheduledProgram, Message, createMessage, classOptions}