|
|
|
|
|
import { readFileSync, writeFileSync, existsSync, promises } from 'fs' |
|
import { join } from 'path' |
|
|
|
|
|
const { |
|
initAuthCreds, |
|
BufferJSON, |
|
proto, |
|
isJidBroadcast, |
|
isJidGroup, |
|
WAMessageStubType, |
|
updateMessageWithReceipt, |
|
updateMessageWithReaction, |
|
|
|
useMultiFileAuthState: baileysMultiFileAuthState |
|
} = (await import('@adiwajshing/baileys')).default |
|
|
|
const TIME_TO_DATA_STALE = 5 * 60 * 1000 |
|
|
|
|
|
|
|
function makeInMemoryStore() { |
|
|
|
let chats = {} |
|
|
|
let messages = {} |
|
|
|
let state = { connection: 'close' } |
|
|
|
|
|
|
|
|
|
|
|
|
|
function loadMessage(jid, id = null) { |
|
let message = null |
|
|
|
if (jid && !id) { |
|
id = jid |
|
|
|
const filter = (m) => m.key?.id == id |
|
const messageFind = Object.entries(messages) |
|
.find(([, msgs]) => { |
|
return msgs.find(filter) |
|
}) |
|
message = messageFind?.[1]?.find(filter) |
|
} else { |
|
|
|
jid = jid?.decodeJid?.() |
|
if (!(jid in messages)) return null; |
|
message = messages[jid].find(m => m.key.id == id) |
|
} |
|
return message ? message : null |
|
} |
|
|
|
|
|
|
|
|
|
|
|
async function fetchGroupMetadata(jid, groupMetadata) { |
|
|
|
jid = jid?.decodeJid?.() |
|
if (!isJidGroup(jid)) return |
|
if (!(jid in chats)) return chats[jid] = { id: jid } |
|
const isRequiredToUpdate = !chats[jid].metadata || Date.now() - (chats[jid].lastfetch || 0) > TIME_TO_DATA_STALE |
|
if (isRequiredToUpdate) { |
|
const metadata = await groupMetadata?.(jid) |
|
if (metadata) Object.assign(chats[jid], { |
|
subject: metadata.subject, |
|
lastfetch: Date.now(), |
|
metadata |
|
}) |
|
} |
|
return chats[jid].metadata |
|
} |
|
|
|
|
|
function fetchMessageReceipts(id) { |
|
const msg = loadMessage(id) |
|
if (!msg) return null |
|
return msg.userReceipt |
|
} |
|
|
|
|
|
|
|
|
|
|
|
async function fetchImageUrl(jid, profilePictureUrl) { |
|
|
|
jid = jid?.decodeJid?.() |
|
if (!(jid in chats)) return chats[jid] = { id: jid } |
|
if (!chats[jid].imgUrl) { |
|
const url = await profilePictureUrl(jid, 'image').catch(() => './src/avatar_contact.png') |
|
if (url) chats[jid].imgUrl = url |
|
} |
|
return chats[jid].imgUrl |
|
} |
|
|
|
|
|
|
|
|
|
function getContact(jid) { |
|
|
|
jid = jid?.decodeJid?.() |
|
if (!(jid in chats)) return null |
|
return chats[jid] |
|
} |
|
|
|
|
|
|
|
|
|
|
|
const upsertMessage = (jid, message, type = 'append') => { |
|
|
|
jid = jid?.decodeJid?.() |
|
if (!(jid in messages)) messages[jid] = [] |
|
|
|
|
|
delete message.message?.messageContextInfo |
|
delete message.message?.senderKeyDistributionMessage |
|
|
|
const msg = loadMessage(jid, message.key.id) |
|
if (msg) { |
|
Object.assign(msg, message) |
|
} else { |
|
if (type == 'append') messages[jid].push(message) |
|
else messages[jid].splice(0, 0, message) |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function bind(ev, opts = { groupMetadata: () => null }) { |
|
ev.on('connection.update', update => { |
|
Object.assign(state, update) |
|
}) |
|
|
|
ev.on('chats.set', function store(chatsSet) { |
|
|
|
|
|
for (const chat of chatsSet.chats) { |
|
|
|
const id = chat.id?.decodeJid?.() |
|
if (!id) continue |
|
|
|
if (!(id in chats)) chats[id] = { ...chat, isChats: true, ...(chat.name ? { name: (chat.name) } : {}) } |
|
if (chat.name) chats[id].name = chat.name |
|
} |
|
}) |
|
|
|
ev.on('contacts.set', function store(contactsSet) { |
|
for (const contact of contactsSet.contacts) { |
|
|
|
const id = contact.id?.decodeJid?.() |
|
if (!id) continue |
|
chats[id] = Object.assign(chats[id] || {}, { ...contact, isContact: true }) |
|
} |
|
}) |
|
|
|
ev.on('messages.set', function store(messagesSet) { |
|
|
|
|
|
for (const message of messagesSet.messages) { |
|
|
|
const jid = message.key.remoteJid?.decodeJid?.() |
|
if (!jid) continue |
|
if (!jid || isJidBroadcast(jid)) continue |
|
if (!(jid in messages)) messages[jid] = [] |
|
const id = message.key.id |
|
const msg = loadMessage(jid, id) |
|
|
|
upsertMessage(jid, proto.WebMessageInfo.fromObject(message), 'prepend') |
|
} |
|
}) |
|
|
|
ev.on('contacts.update', function store(contactsUpdate) { |
|
for (const contact of contactsUpdate) { |
|
|
|
const id = contact.id?.decodeJid?.() |
|
if (!id) continue |
|
chats[id] = Object.assign(chats[id] || {}, { id, ...contact, isContact: true }) |
|
} |
|
}) |
|
|
|
ev.on('chats.upsert', async function store(chatsUpsert) { |
|
await Promise.all(chatsUpsert.map(async (chat) => { |
|
|
|
const id = chat.id?.decodeJid?.() |
|
if (!id || isJidBroadcast(id)) return |
|
|
|
if (!(id in chats)) chats[id] = { id, ...chat, isChats: true } |
|
const isGroup = isJidGroup(id) |
|
Object.assign(chats[id], { ...chat, isChats: true }) |
|
if (isGroup && !chats[id].metadata) Object.assign(chats[id], { metadata: await fetchGroupMetadata(id, opts.groupMetadata) }) |
|
})) |
|
}) |
|
|
|
ev.on('chats.update', function store(chatsUpdate) { |
|
for (const chat of chatsUpdate) { |
|
|
|
const id = chat.id?.decodeJid?.() |
|
if (!id) continue |
|
|
|
if (!(id in chats)) chats[id] = { id, ...chat, isChats: true } |
|
if (chat.unreadCount) chat.unreadCount += chats[id].unreadCount || 0 |
|
Object.assign(chats[id], { id, ...chat, isChats: true }) |
|
} |
|
}) |
|
|
|
ev.on('presence.update', function store(presenceUpdate) { |
|
|
|
const id = presenceUpdate.id?.decodeJid?.() |
|
if (!id) return |
|
if (!(id in chats)) chats[id] = { id, isContact: true } |
|
Object.assign(chats[id], presenceUpdate) |
|
}) |
|
|
|
ev.on('messages.upsert', function store(messagesUpsert) { |
|
const { messages: newMessages, type } = messagesUpsert |
|
switch (type) { |
|
case 'append': |
|
case 'notify': |
|
for (const msg of newMessages) { |
|
|
|
const jid = msg.key.remoteJid?.decodeJid?.() |
|
if (!jid || isJidBroadcast(jid)) continue |
|
|
|
if (msg.messageStubType == WAMessageStubType.CIPHERTEXT) continue |
|
if (!(jid in messages)) messages[jid] = [] |
|
const message = loadMessage(jid, msg.key.id) |
|
|
|
upsertMessage(jid, proto.WebMessageInfo.fromObject(msg)) |
|
|
|
if (type === 'notify' && !(jid in chats)) |
|
ev.emit('chats.upsert', [{ |
|
id: jid, |
|
conversationTimestamp: msg.messageTimestamp, |
|
unreadCount: 1, |
|
name: msg.pushName || msg.verifiedBizName, |
|
}]) |
|
} |
|
break |
|
} |
|
}) |
|
|
|
ev.on('messages.update', function store(messagesUpdate) { |
|
for (const message of messagesUpdate) { |
|
|
|
const jid = message.key.remoteJid?.decodeJid?.() |
|
if (!jid) continue |
|
const id = message.key.id |
|
if (!jid || isJidBroadcast(jid)) continue |
|
if (!(jid in messages)) messages[jid] = [] |
|
const msg = loadMessage(jid, id) |
|
if (!msg) return |
|
if (message.update.messageStubType == WAMessageStubType.REVOKE) { |
|
|
|
|
|
continue |
|
} |
|
|
|
const msgIndex = messages[jid].findIndex(m => m.key.id === id) |
|
Object.assign(messages[jid][msgIndex], message.update) |
|
|
|
} |
|
}) |
|
|
|
ev.on('groups.update', async function store(groupsUpdate) { |
|
await Promise.all(groupsUpdate.map(async (group) => { |
|
|
|
const id = group.id?.decodeJid?.() |
|
if (!id) return |
|
const isGroup = isJidGroup(id) |
|
if (!isGroup) return |
|
if (!(id in chats)) chats[id] = { id, ...group, isChats: true } |
|
if (!chats[id].metadata) Object.assign(chats[id], { metadata: await fetchGroupMetadata(id, opts.groupMetadata) }) |
|
|
|
Object.assign(chats[id].metadata, group) |
|
})) |
|
}) |
|
|
|
ev.on('group-participants.update', async function store(groupParticipantsUpdate) { |
|
|
|
const id = groupParticipantsUpdate.id?.decodeJid?.() |
|
if (!id || !isJidGroup(id)) return |
|
if (!(id in chats)) chats[id] = { id } |
|
if (!(id in chats) || !chats[id].metadata) Object.assign(chats[id], { metadata: await fetchGroupMetadata(id, opts.groupMetadata) }) |
|
const metadata = chats[id].metadata |
|
if (!metadata) return console.log(`Try to update group ${id} but metadata not found in 'group-participants.update'`) |
|
switch (groupParticipantsUpdate.action) { |
|
case 'add': |
|
metadata.participants.push(...groupParticipantsUpdate.participants.map(id => ({ id, admin: null }))) |
|
break |
|
case 'demote': |
|
case 'promote': |
|
for (const participant of metadata.participants) |
|
if (groupParticipantsUpdate.participants.includes(participant.id)) |
|
participant.admin = groupParticipantsUpdate.action === 'promote' ? 'admin' : null |
|
|
|
break |
|
case 'remove': |
|
metadata.participants = metadata.participants.filter(p => !groupParticipantsUpdate.participants.includes(p.id)) |
|
break |
|
} |
|
|
|
Object.assign(chats[id], { metadata }) |
|
}) |
|
|
|
ev.on('message-receipt.update', function store(messageReceiptUpdate) { |
|
for (const { key, receipt } of messageReceiptUpdate) { |
|
|
|
const jid = key.remoteJid?.decodeJid?.() |
|
if (!jid) continue |
|
const id = key.id |
|
if (!(jid in messages)) messages[jid] = [] |
|
const msg = loadMessage(jid, id) |
|
if (!msg) return |
|
updateMessageWithReceipt(msg, receipt) |
|
} |
|
}) |
|
|
|
ev.on('messages.reaction', function store(reactions) { |
|
for (const { key, reaction } of reactions) { |
|
|
|
const jid = key.remoteJid?.decodeJid?.() |
|
if (!jid) continue |
|
const msg = loadMessage(jid, key.id) |
|
if (!msg) return |
|
updateMessageWithReaction(msg, reaction) |
|
} |
|
}) |
|
|
|
} |
|
|
|
function toJSON() { |
|
return { chats, messages } |
|
} |
|
|
|
function fromJSON(json) { |
|
Object.assign(chats, json.chats) |
|
for (const jid in json.messages) |
|
messages[jid] = json.messages[jid].map(m => m && proto.WebMessageInfo.fromObject(m)).filter(m => m && m.messageStubType != WAMessageStubType.CIPHERTEXT) |
|
|
|
} |
|
|
|
|
|
function writeToFile(path) { |
|
writeFileSync(path, JSON.stringify(toJSON(), (key, value) => key == 'isChats' ? undefined : value, 2)) |
|
} |
|
|
|
|
|
function readFromFile(path) { |
|
if (existsSync(path)) { |
|
const result = JSON.parse(readFileSync(path, { encoding: 'utf-8' })) |
|
fromJSON(result) |
|
} |
|
} |
|
|
|
return { |
|
chats, |
|
messages, |
|
state, |
|
|
|
loadMessage, |
|
fetchGroupMetadata, |
|
fetchMessageReceipts, |
|
fetchImageUrl, |
|
|
|
getContact, |
|
|
|
bind, |
|
writeToFile, |
|
readFromFile |
|
} |
|
} |
|
|
|
function JSONreplacer(key, value) { |
|
if (value == null) return |
|
const baileysJSON = BufferJSON.replacer(key, value) |
|
return baileysJSON |
|
} |
|
|
|
const fixFileName = (file) => file?.replace(/\//g, '__')?.replace(/:/g, '-') |
|
|
|
|
|
|
|
|
|
|
|
const useMultiFileAuthState = baileysMultiFileAuthState || |
|
|
|
|
|
|
|
|
|
async function useMultiFileAuthState(folder) { |
|
|
|
const writeData = (data, file) => { |
|
return promises.writeFile(join(folder, fixFileName(file)), JSON.stringify(data, JSONreplacer)) |
|
} |
|
|
|
const readData = async (file) => { |
|
try { |
|
const data = await promises.readFile(join(folder, fixFileName(file)), { encoding: 'utf-8' }) |
|
return JSON.parse(data, BufferJSON.reviver) |
|
} catch (error) { |
|
return null |
|
} |
|
} |
|
|
|
const removeData = async (file) => { |
|
try { |
|
await promises.unlink(fixFileName(file)) |
|
} catch { |
|
|
|
} |
|
} |
|
|
|
const folderInfo = await promises.stat(folder).catch(() => { }) |
|
if (folderInfo) { |
|
if (!folderInfo.isDirectory()) { |
|
throw new Error(`found something that is not a directory at ${folder}, either delete it or specify a different location`) |
|
} |
|
} else { |
|
await promises.mkdir(folder, { recursive: true }) |
|
} |
|
|
|
const creds = await readData('creds.json') || initAuthCreds() |
|
|
|
return { |
|
state: { |
|
creds, |
|
keys: { |
|
|
|
get: async (type, ids) => { |
|
const data = {} |
|
await Promise.all( |
|
ids.map( |
|
async id => { |
|
let value = await readData(`${type}-${id}.json`) |
|
if (type === 'app-state-sync-key') { |
|
value = proto.AppStateSyncKeyData.fromObject(value) |
|
} |
|
|
|
data[id] = value |
|
} |
|
) |
|
) |
|
|
|
return data |
|
}, |
|
set: async (data) => { |
|
const tasks = [] |
|
for (const category in data) { |
|
for (const id in data[category]) { |
|
const value = data[category][id] |
|
const file = `${category}-${id}.json` |
|
tasks.push(value ? writeData(value, file) : removeData(file)) |
|
} |
|
} |
|
|
|
await Promise.all(tasks) |
|
} |
|
} |
|
}, |
|
saveCreds: () => { |
|
return writeData(creds, 'creds.json') |
|
} |
|
} |
|
} |
|
|
|
const KEY_MAP = { |
|
'pre-key': 'preKeys', |
|
'session': 'sessions', |
|
'sender-key': 'senderKeys', |
|
'app-state-sync-key': 'appStateSyncKeys', |
|
'app-state-sync-version': 'appStateVersions', |
|
'sender-key-memory': 'senderKeyMemory' |
|
} |
|
|
|
|
|
|
|
|
|
|
|
const useMemoryAuthState = function useMemoryAuthState() { |
|
const creds = initAuthCreds() |
|
const keys = {} |
|
|
|
const saveCreds = () => undefined |
|
return { |
|
state: { |
|
creds, |
|
keys: { |
|
get: (type, ids) => { |
|
const key = KEY_MAP[type] |
|
return ids.reduce( |
|
(dict, id) => { |
|
|
|
let value = keys[key]?.[id] |
|
if (value) { |
|
if (type === 'app-state-sync-key') { |
|
value = proto.AppStateSyncKeyData.fromObject(value) |
|
} |
|
|
|
dict[id] = value |
|
} |
|
|
|
return dict |
|
}, {} |
|
) |
|
}, |
|
set: (data) => { |
|
for (const _key in data) { |
|
const key = KEY_MAP[_key] |
|
keys[key] = keys[key] || {} |
|
Object.assign(keys[key], data[_key]) |
|
} |
|
} |
|
} |
|
}, |
|
|
|
saveCreds |
|
} |
|
} |
|
export default { |
|
makeInMemoryStore, |
|
useMultiFileAuthState, |
|
useMemoryAuthState, |
|
|
|
|
|
fixFileName, |
|
JSONreplacer, |
|
|
|
KEY_MAP |
|
} |