UNPKG

@baileys-md/baileys

Version:

WhatsApp API Support Node 18

568 lines (567 loc) 24.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.waLabelAssociationKey = exports.waMessageID = exports.waChatKey = void 0; const cron_1 = require("cron"); const moment_timezone_1 = __importDefault(require("moment-timezone")); const WAProto_1 = require("../../WAProto"); const Defaults_1 = require("../Defaults"); const LabelAssociation_1 = require("../Types/LabelAssociation"); const Utils_1 = require("../Utils"); const WABinary_1 = require("../WABinary"); const make_ordered_dictionary_1 = __importDefault(require("./make-ordered-dictionary")); const object_repository_1 = require("./object-repository"); 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), }); exports.waChatKey = waChatKey; const waMessageID = (m) => m.key.id || ""; exports.waMessageID = waMessageID; exports.waLabelAssociationKey = { key: (la) => la.type === LabelAssociation_1.LabelAssociationType.Chat ? la.chatId + la.labelId : la.chatId + la.messageId + la.labelId, compare: (k1, k2) => k2.localeCompare(k1), }; const makeMessagesDictionary = () => (0, make_ordered_dictionary_1.default)(exports.waMessageID); const predefinedLabels = Object.freeze({ "0": { id: "0", name: "New customer", predefinedId: "0", color: 0, deleted: false, }, "1": { id: "1", name: "New order", predefinedId: "1", color: 1, deleted: false, }, "2": { id: "2", name: "Pending payment", predefinedId: "2", color: 2, deleted: false, }, "3": { id: "3", name: "Paid", predefinedId: "3", color: 3, deleted: false, }, "4": { id: "4", name: "Order completed", predefinedId: "4", color: 4, deleted: false, }, }); exports.default = ({ logger: _logger, socket, db, filterChats, autoDeleteStatusMessage, }) => { const isOlderThan24Hours = (timestamp) => { const currentTime = (0, moment_timezone_1.default)(new Date()).tz("Asia/Jakarta"); const hoursDifference = currentTime.diff((0, moment_timezone_1.default)(timestamp * 1000).tz("Asia/Jakarta"), "hours"); return hoursDifference > 24; }; if (autoDeleteStatusMessage) { if (typeof autoDeleteStatusMessage === "boolean") { autoDeleteStatusMessage = { cronTime: "0 0 * * *", timeZone: "Asia/Jakarta", }; } const update = { $set: { messages: { $filter: { input: "$messages", cond: { $not: { $or: [], }, }, }, }, }, }; new cron_1.CronJob(autoDeleteStatusMessage.cronTime, // cronTime async () => { var _a, _b, _c, _d, _e; const statusMesasges = await chats.findOne({ id: "status@broadcast" }, { projection: { _id: 0 } }); if (statusMesasges) { for (const m of statusMesasges === null || statusMesasges === void 0 ? void 0 : statusMesasges.messages) { if (isOlderThan24Hours(typeof ((_a = m.message) === null || _a === void 0 ? void 0 : _a.messageTimestamp) === "number" ? (_b = m.message) === null || _b === void 0 ? void 0 : _b.messageTimestamp : (_d = (_c = m.message) === null || _c === void 0 ? void 0 : _c.messageTimestamp) === null || _d === void 0 ? void 0 : _d.low)) { update.$set.messages.$filter.cond.$not.$or.push({ $eq: ["$$this.message.key.id", (_e = m.message) === null || _e === void 0 ? void 0 : _e.key.id], }); } } if (update.$set.messages.$filter.cond.$not.$or.length > 0) { const updateResult = await chats.updateOne({ id: "status@broadcast" }, [update]); logger === null || logger === void 0 ? void 0 : logger.debug(updateResult, "updated statusMessages"); } } }, () => { logger === null || logger === void 0 ? void 0 : logger.debug("cleared statusMessages"); }, true, // start autoDeleteStatusMessage === null || autoDeleteStatusMessage === void 0 ? void 0 : autoDeleteStatusMessage.timeZone); } const logger = _logger || Defaults_1.DEFAULT_CONNECTION_CONFIG.logger.child({ stream: "mongo-store" }); const chats = db.collection("chats"); const messages = {}; const contacts = db.collection("contacts"); const groupMetadata = {}; const presences = {}; const state = { connection: "close" }; const labels = new object_repository_1.ObjectRepository(predefinedLabels); const labelAssociations = db.collection("labelAssociations"); const assertMessageList = (jid) => { if (!messages[jid]) { messages[jid] = makeMessagesDictionary(); } return messages[jid]; }; const labelsUpsert = (newLabels) => { for (const label of newLabels) { labels.upsertById(label.id, label); } }; // const contactsUpsert = async (newContacts: Contact[]) => { // const oldContacts = new Set(await contacts // .find({}, { projection: { _id: 0 } }) // .toArray()); // for (const contact of newContacts) { // } // }; const bind = (ev) => { ev.on("connection.update", (update) => { Object.assign(state, update); }); ev.on("messaging-history.set", async ({ chats: newChats, contacts: newContacts, messages: newMessages, isLatest }) => { var _a; if (isLatest) { await chats.drop(); await contacts.drop(); for (const id in messages) { delete messages[id]; } } if (filterChats) { newChats = newChats .map((chat) => { var _a; if ((_a = chat.messages) === null || _a === void 0 ? void 0 : _a.some((m) => { var _a, _b; return !((_a = m.message) === null || _a === void 0 ? void 0 : _a.message) && ((_b = m.message) === null || _b === void 0 ? void 0 : _b.messageStubType); })) { return undefined; } return chat; }) .filter(Boolean); } if (newChats.length) { const chatsAdded = await chats.bulkWrite(newChats.map((chat) => { return { insertOne: { document: chat, }, }; })); logger.debug({ chatsAdded: chatsAdded.insertedCount }, "synced chats"); } else { logger.debug("no chats added"); } const oldContacts = await contacts.bulkWrite(newContacts.map((contact) => { return { insertOne: { document: contact, }, }; })); logger.debug({ insertedContacts: oldContacts.insertedCount }, "synced contacts"); if (!oldContacts.insertedCount) { throw new Error("no contacts added"); } for (const msg of newMessages) { const jid = msg.key.remoteJid; const list = assertMessageList(jid); list.upsert(msg, "prepend"); const chat = await chats.findOne({ id: jid }, { projection: { _id: 0 } }); if (chat) { ((_a = chat.messages) === null || _a === void 0 ? void 0 : _a.push({ message: msg })) || (chat.messages = [{ message: msg }]); await chats.findOneAndUpdate({ id: jid }, { $set: chat }, { upsert: true }); } else { logger.debug({ jid }, "chat not found"); } } logger.debug({ messages: newMessages.length }, "synced messages"); }); ev.on("contacts.upsert", async (Contacts) => { for (const contact of Contacts) { await contacts.updateOne({ id: contact.id }, { $set: contact }, { upsert: true }); } logger === null || logger === void 0 ? void 0 : logger.debug({ contactsUpserted: Contacts.length }, "contacts upserted"); }); ev.on("contacts.update", async (updates) => { for (const update of updates) { const contact = await contacts.findOne({ id: update.id }, { projection: { _id: 0 } }); if (contact) { Object.assign(contact, update); await contacts.updateOne({ id: update.id }, { $set: contact }, { upsert: true }); } else { logger.debug("got update for non-existent contact"); } } }); ev.on("chats.upsert", async (newChats) => { await chats.bulkWrite(newChats.map((chat) => { return { updateOne: { filter: { id: chat.id }, update: { $set: chat }, upsert: true, }, }; })); }); ev.on("chats.update", async (updates) => { // try { for (const update of updates) { const chat = await chats.findOneAndUpdate({ id: update.id }, { $set: update, }, { upsert: true }); if (!chat) { logger.debug("got update for non-existant chat"); } } }); ev.on("labels.edit", (label) => { if (label.deleted) { return labels.deleteById(label.id); } // WhatsApp can store only up to 20 labels if (labels.count() < 20) { return labels.upsertById(label.id, label); } logger.error("Labels count exceed"); }); ev.on("labels.association", async ({ type, association }) => { switch (type) { case "add": await labelAssociations.updateOne({ id: (association === null || association === void 0 ? void 0 : association.chatId) || (association === null || association === void 0 ? void 0 : association.labelId) }, { $set: association }, { upsert: true }); break; case "remove": await labelAssociations.deleteOne({ id: (association === null || association === void 0 ? void 0 : association.chatId) || (association === null || association === void 0 ? void 0 : association.labelId), }); break; default: logger.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", async (deletions) => { for (const item of deletions) { await chats.deleteOne({ id: item }); } }); ev.on("messages.upsert", async ({ messages: newMessages, type }) => { // try { switch (type) { case "append": case "notify": for (const msg of newMessages) { const jid = (0, WABinary_1.jidNormalizedUser)(msg.key.remoteJid); const list = assertMessageList(jid); list.upsert(msg, "append"); const chat = await chats.findOne({ id: jid }); if (type === "notify") { if (!chat) { ev.emit("chats.upsert", [ { id: jid, conversationTimestamp: (0, Utils_1.toNumber)(msg.messageTimestamp), unreadCount: 1, }, ]); } else { chat.messages ? chat.messages.push({ message: msg }) : (chat.messages = [{ message: msg }]); await chats.updateOne({ id: jid }, { $set: chat }, { upsert: true }); } } } break; } }); ev.on("messages.update", (updates) => { var _a; for (const { update, key } of updates) { const list = assertMessageList((0, WABinary_1.jidNormalizedUser)(key.remoteJid)); if (update === null || update === void 0 ? void 0 : update.status) { const listStatus = (_a = list.get(key.id)) === null || _a === void 0 ? void 0 : _a.status; if (listStatus && (update === null || update === void 0 ? void 0 : 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("got update for non-existent message"); } } }); ev.on("messages.delete", (item) => { if ("all" in item) { const list = messages[item.jid]; list === null || list === void 0 ? void 0 : list.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) { switch (action) { case "add": metadata.participants.push(...participants.map((id) => ({ id, isAdmin: false, isSuperAdmin: false, }))); break; case "demote": case "promote": 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 === null || obj === void 0 ? void 0 : obj.get(key.id); if (msg) { (0, Utils_1.updateMessageWithReceipt)(msg, receipt); } } }); ev.on("messages.reaction", (reactions) => { for (const { key, reaction } of reactions) { const obj = messages[key.remoteJid]; const msg = obj === null || obj === void 0 ? void 0 : obj.get(key.id); if (msg) { (0, Utils_1.updateMessageWithReaction)(msg, reaction); } } }); }; const toJSON = () => ({ chats, contacts, messages, labels, labelAssociations, }); // TODO: replace upsert logic by corresponding mongodb collection methods const fromJSON = async (json) => { await chats.updateMany({}, { $set: { ...json.chats } }, { upsert: true }); await labelAssociations.updateMany({}, { $set: { ...(json.labelAssociations || []) } }, { upsert: true }); const contactsCollection = db.collection("contacts"); await contactsCollection.updateMany({}, { $set: { ...Object.values(json.contacts) } }, { upsert: true }); // // 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(WAProto_1.proto.WebMessageInfo.fromObject(msg), "append"); } } }; /** * Retrieves a chat object by its ID. * * @param {string} jid - The ID of the chat. * @return {Promise<Chat|null>} A promise that resolves to the chat object if found, or null if not found. */ const getChatById = async (jid) => { return await chats.findOne({ id: jid }, { projection: { _id: 0 } }); }; return { chats, contacts, messages, groupMetadata, state, presences, labels, labelAssociations, bind, /** loads messages from the store, if not found -- uses the legacy connection */ 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 messages; if (list && mode === "before" && (!cursorKey || cursorValue)) { if (cursorValue) { const msgIdx = list.array.findIndex((m) => m.key.id === (cursorKey === null || cursorKey === void 0 ? void 0 : cursorKey.id)); messages = list.array.slice(0, msgIdx); } else { messages = list.array; } const diff = count - messages.length; if (diff < 0) { messages = messages.slice(-count); // get the last X messages } } else { messages = []; } return messages; }, /** * Get all available labels for profile * * Keep in mind that the list is formed from predefined tags and tags * that were "caught" during their editing. */ getLabels: () => { return labels; }, /** * Get labels for chat * * @returns Label IDs **/ getChatLabels: (chatId) => { return labelAssociations.findOne((la) => la.chatId === chatId); }, /** * Get labels for message * * @returns Label IDs **/ getMessageLabels: async (messageId) => { const associations = labelAssociations.find((la) => la.messageId === messageId); return associations === null || associations === void 0 ? void 0 : associations.map(({ labelId }) => labelId); }, loadMessage: async (jid, id) => { var _a, _b; if (messages[jid]) { return messages[jid].get(id); } const chat = await chats.findOne({ id: jid }, { projection: { _id: 0 } }); for (const m of (_a = chat === null || chat === void 0 ? void 0 : chat.messages) !== null && _a !== void 0 ? _a : []) { if (((_b = m === null || m === void 0 ? void 0 : m.message) === null || _b === void 0 ? void 0 : _b.key.id) === id) { return m.message; } } }, mostRecentMessage: async (jid) => { var _a, _b, _c; const message = ((_a = messages[jid]) === null || _a === void 0 ? void 0 : _a.array.slice(-1)[0]) || ((_c = (_b = (await chats.findOne({ id: jid }, { projection: { _id: 0 } }))) === null || _b === void 0 ? void 0 : _b.messages) === null || _c === void 0 ? void 0 : _c.slice(-1)[0].message) || undefined; return message; }, fetchImageUrl: async (jid, sock) => { const contact = await contacts.findOne({ id: jid }, { projection: { _id: 0 } }); if (!contact) { return sock === null || sock === void 0 ? void 0 : sock.profilePictureUrl(jid); } if (typeof contact.imgUrl === "undefined") { contact.imgUrl = await (sock === null || sock === void 0 ? void 0 : sock.profilePictureUrl(jid)); await contacts.updateOne({ id: jid }, { $set: contact }, { upsert: true }); } return contact.imgUrl; }, getContactInfo: async (jid, socket) => { const contact = await contacts.findOne({ id: jid }, { projection: { _id: 0 } }); if (!contact) { return { id: jid, imgUrl: await (socket === null || socket === void 0 ? void 0 : socket.profilePictureUrl(jid)), }; } // fetch image if required if (typeof contact.imgUrl === "undefined" || contact.imgUrl === "changed") { contact.imgUrl = await (socket === null || socket === void 0 ? void 0 : socket.profilePictureUrl(contact.id, "image")); await contacts.updateOne({ id: jid }, { $set: { ...contact } }, { upsert: true }); } return contact; }, fetchGroupMetadata: async (jid, sock) => { if (!groupMetadata[jid]) { const metadata = await (sock === null || sock === void 0 ? void 0 : sock.groupMetadata(jid)); if (metadata) { groupMetadata[jid] = metadata; } } return groupMetadata[jid]; }, fetchMessageReceipts: async ({ remoteJid, id }) => { const list = messages[remoteJid]; const msg = list === null || list === void 0 ? void 0 : list.get(id); return msg === null || msg === void 0 ? void 0 : msg.userReceipt; }, getChatById, toJSON, fromJSON, }; };