@baileys-md/baileys
Version:
Baileys WhatsApp API
290 lines (288 loc) • 11.9 kB
JavaScript
//===================================//
import { md5, toNumber, updateMessageWithReceipt, updateMessageWithReaction } from "../Utils/index.js"
import { DEFAULT_CONNECTION_CONFIG } from "../Defaults/index.js"
import { makeOrderedDictionary } from "./make-ordered-dictionary.js"
import { LabelAssociationType } from "../Types/LabelAssociation.js"
import { jidDecode, jidNormalizedUser } from "../WABinary/index.js"
import { proto } from "../../WAProto/index.js";
import { ObjectRepository } from "./object-repository.js"
import KeyedDB from "../KeyDB/KeyedDB.js"
//===================================//
export const waChatKey = (pin) => ({
key: (c) => (pin ? (c.pinned ? "1" : "0") : "") + (c.archived ? "0" : "1") + (c.conversationTimestamp ? c.conversationTimestamp.toString(16).padStart(8, "0") : "") + c.id,
compare: (k1, k2) => k2.localeCompare(k1)
})
//===================================//
export const waMessageID = (m) => m.key.id || ""
//===================================//
export const waLabelAssociationKey = {
key: (la) => (la.type === LabelAssociationType.Chat ? la.chatId + la.labelId : la.chatId + la.messageId + la.labelId),
compare: (k1, k2) => k2.localeCompare(k1)
}
//===================================//
const makeMessagesDictionary = () => makeOrderedDictionary(waMessageID)
//===================================//
export const makeInMemoryStore = (config) => {
const socket = config.socket
const chatKey = config.chatKey || waChatKey(true)
const labelAssociationKey = config.labelAssociationKey || waLabelAssociationKey
const logger = config.logger || DEFAULT_CONNECTION_CONFIG.logger.child({ stream: "in-mem-store" })
const chats = new KeyedDB(chatKey, c => c.id)
const messages = {}
const contacts = {}
const groupMetadata = {}
const presences = {}
const state = { connection: "close" }
const labels = new ObjectRepository()
const labelAssociations = new KeyedDB(labelAssociationKey, labelAssociationKey.key)
const assertMessageList = (jid) => {
if (!messages[jid]) messages[jid] = makeMessagesDictionary()
return messages[jid]
}
const contactsUpsert = (newContacts) => {
const oldContacts = new Set(Object.keys(contacts))
for (const contact of newContacts) {
oldContacts.delete(contact.id)
contacts[contact.id] = Object.assign(contacts[contact.id] || {}, contact)
}
return oldContacts
}
const labelsUpsert = (newLabels) => {
for (const label of newLabels) labels.upsertById(label.id, label)
}
const bind = (ev) => {
ev.on("connection.update", update => Object.assign(state, update))
ev.on("messaging-history.set", ({ chats: newChats, contacts: newContacts, messages: newMessages, isLatest, syncType }) => {
if (syncType === proto.HistorySync.HistorySyncType.ON_DEMAND) return
if (isLatest) {
chats.clear()
for (const id in messages) delete messages[id]
}
const chatsAdded = chats.insertIfAbsent(...newChats).length
logger.debug({ chatsAdded }, "synced chats")
const oldContacts = contactsUpsert(newContacts)
if (isLatest) for (const jid of oldContacts) delete contacts[jid]
logger.debug({ deletedContacts: isLatest ? oldContacts.size : 0, newContacts }, "synced contacts")
for (const msg of newMessages) {
const jid = msg.key.remoteJid
const list = assertMessageList(jid)
list.upsert(msg, "prepend")
}
logger.debug({ messages: newMessages.length }, "synced messages")
})
ev.on("contacts.upsert", contacts => contactsUpsert(contacts))
ev.on("contacts.update", async (updates) => {
for (const update of updates) {
let contact = contacts[update.id]
if (!contact) {
const contactHashes = await Promise.all(
Object.keys(contacts).map(async (contactId) => {
const { user } = jidDecode(contactId)
return [contactId, (await md5(Buffer.from(user + "WA_ADD_NOTIF", "utf8"))).toString("base64").slice(0, 3)]
})
)
contact = contacts[contactHashes.find(([, b]) => b === update.id?.[0]) || ""]
}
if (contact) {
if (update.imgUrl === "changed") contact.imgUrl = socket ? await socket.profilePictureUrl(contact.id) : undefined
else if (update.imgUrl === "removed") delete contact.imgUrl
} else return logger.debug({ update }, "got update for non-existant contact")
Object.assign(contacts[contact.id], contact)
}
})
ev.on("chats.upsert", newChats => chats.upsert(...newChats))
ev.on("chats.update", updates => {
for (let update of updates) {
const result = chats.update(update.id, chat => {
if (update.unreadCount > 0) {
update = { ...update }
update.unreadCount = (chat.unreadCount || 0) + update.unreadCount
}
Object.assign(chat, update)
})
if (!result) logger.debug({ update }, "got update for non-existant chat")
}
})
ev.on("labels.edit", (label) => {
if (label.deleted) return labels.deleteById(label.id)
if (labels.count() < 20) return labels.upsertById(label.id, label)
logger.error("Labels count exceed")
})
ev.on("labels.association", ({ type, association }) => {
switch (type) {
case "add": labelAssociations.upsert(association); break
case "remove": labelAssociations.delete(association); break
default: console.error(`unknown operation type [${type}]`)
}
})
ev.on("presence.update", ({ id, presences: update }) => {
presences[id] = presences[id] || {}
Object.assign(presences[id], update)
})
ev.on("chats.delete", deletions => {
for (const item of deletions) if (chats.get(item)) chats.deleteById(item)
})
ev.on("messages.upsert", ({ messages: newMessages, type }) => {
switch (type) {
case "append":
case "notify":
for (const msg of newMessages) {
const jid = jidNormalizedUser(msg.key.remoteJid)
const list = assertMessageList(jid)
list.upsert(msg, "append")
if (type === "notify" && !chats.get(jid)) {
ev.emit("chats.upsert", [{
id: jid,
conversationTimestamp: toNumber(msg.messageTimestamp),
unreadCount: 1
}])
}
}
break
}
})
ev.on("messages.update", updates => {
for (const { update, key } of updates) {
const list = assertMessageList(jidNormalizedUser(key.remoteJid))
if (update?.status) {
const listStatus = list.get(key.id)?.status
if (listStatus && update.status <= listStatus) {
logger.debug({ update, storedStatus: listStatus }, "status stored newer then update")
delete update.status
logger.debug({ update }, "new update object")
}
}
const result = list.updateAssign(key.id, update)
if (!result) logger.debug({ update }, "got update for non-existent message")
}
})
ev.on("messages.delete", item => {
if ("all" in item) messages[item.jid]?.clear()
else {
const jid = item.keys[0].remoteJid
const list = messages[jid]
if (list) {
const idSet = new Set(item.keys.map(k => k.id))
list.filter(m => !idSet.has(m.key.id))
}
}
})
ev.on("groups.update", updates => {
for (const update of updates) {
const id = update.id
if (groupMetadata[id]) Object.assign(groupMetadata[id], update)
else logger.debug({ update }, "got update for non-existant group metadata")
}
})
ev.on("group-participants.update", ({ id, participants, action }) => {
const metadata = groupMetadata[id]
if (!metadata) return
switch (action) {
case "add":
metadata.participants.push(...participants.map(id => ({ id, isAdmin: false, isSuperAdmin: false })))
break
case "promote":
case "demote":
for (const participant of metadata.participants) {
if (participants.includes(participant.id)) participant.isAdmin = action === "promote"
}
break
case "remove":
metadata.participants = metadata.participants.filter(p => !participants.includes(p.id))
break
}
})
ev.on("message-receipt.update", updates => {
for (const { key, receipt } of updates) {
const obj = messages[key.remoteJid]
const msg = obj?.get(key.id)
if (msg) updateMessageWithReceipt(msg, receipt)
}
})
ev.on("messages.reaction", reactions => {
for (const { key, reaction } of reactions) {
const obj = messages[key.remoteJid]
const msg = obj?.get(key.id)
if (msg) updateMessageWithReaction(msg, reaction)
}
})
}
const toJSON = () => ({ chats, contacts, messages, labels, labelAssociations })
const fromJSON = (json) => {
chats.upsert(...json.chats)
labelAssociations.upsert(...json.labelAssociations || [])
contactsUpsert(Object.values(json.contacts))
labelsUpsert(Object.values(json.labels || {}))
for (const jid in json.messages) {
const list = assertMessageList(jid)
for (const msg of json.messages[jid]) list.upsert(proto.WebMessageInfo.fromObject(msg), "append")
}
}
return {
chats,
contacts,
messages,
groupMetadata,
state,
presences,
labels,
labelAssociations,
bind,
loadMessages: async (jid, count, cursor) => {
const list = assertMessageList(jid)
const mode = !cursor || "before" in cursor ? "before" : "after"
const cursorKey = cursor ? ("before" in cursor ? cursor.before : cursor.after) : undefined
const cursorValue = cursorKey ? list.get(cursorKey.id) : undefined
let msgs
if (list && mode === "before" && (!cursorKey || cursorValue)) {
if (cursorValue) {
const idx = list.array.findIndex(m => m.key.id === cursorKey?.id)
msgs = list.array.slice(0, idx)
} else msgs = list.array
const diff = count - msgs.length
if (diff < 0) msgs = msgs.slice(-count)
} else msgs = []
return msgs
},
getLabels: () => labels,
getChatLabels: (chatId) => labelAssociations.filter(la => la.chatId === chatId).all(),
getMessageLabels: (messageId) => labelAssociations.filter(la => la.messageId === messageId).all().map(l => l.labelId),
loadMessage: async (jid, id) => messages[jid]?.get(id),
mostRecentMessage: async (jid) => messages[jid]?.array.slice(-1)[0],
fetchImageUrl: async (jid, baron) => {
const contact = contacts[jid]
if (!contact) return baron?.profilePictureUrl?.(jid)
if (typeof contact.imgUrl === "undefined") {
contact.imgUrl = await baron?.profilePictureUrl?.(jid)
}
return contact.imgUrl
},
fetchGroupMetadata: async (jid, baron) => {
if (!groupMetadata[jid]) {
const metadata = await baron?.groupMetadata(jid)
if (metadata) groupMetadata[jid] = metadata
}
return groupMetadata[jid]
},
fetchMessageReceipts: async ({ remoteJid, id }) => {
const list = messages[remoteJid]
const msg = list?.get(id)
return msg?.userReceipt
},
toJSON,
fromJSON,
writeToFile: (path) => {
import("fs").then(fs => fs.writeFileSync(path, JSON.stringify(toJSON())))
},
readFromFile: async (path) => {
const fs = await import("fs")
if (fs.existsSync(path)) {
logger.debug({ path }, "reading from file")
const jsonStr = fs.readFileSync(path, { encoding: "utf-8" })
const json = JSON.parse(jsonStr)
fromJSON(json)
}
}
}
}
//===================================//