From dd886a1feac637161711656faad099080ccbc9d5 Mon Sep 17 00:00:00 2001 From: Anton Voylenko Date: Tue, 2 Sep 2025 23:19:46 +0300 Subject: [PATCH] feat: ability to filter chats and messages --- src/controllers/clientController.js | 51 ++++++++++++ src/routes.js | 1 + src/sessions.js | 32 +------- src/utils.js | 103 +++++++++++++++++++++++- swagger.json | 116 ++++++++++++++++++++++++++++ 5 files changed, 274 insertions(+), 29 deletions(-) diff --git a/src/controllers/clientController.js b/src/controllers/clientController.js index 4fd25ae..7ccbaba 100644 --- a/src/controllers/clientController.js +++ b/src/controllers/clientController.js @@ -360,6 +360,56 @@ const getChats = async (req, res) => { } } +/** + * Retrieve all chats for the given session ID with optional search parameters. + * + * @function + * @async + * + * @param {Object} req - The request object. + * @param {string} req.params.sessionId - The session ID. + * @param {Object} res - The response object. + * + * @returns {Promise} A Promise that resolves when the operation is complete. + * + * @throws {Error} If the operation fails, an error is thrown. + */ +const getChatsWithSearch = async (req, res) => { + /* + #swagger.summary = 'Get all current chats with optional search parameters' + #swagger.requestBody = { + required: true, + schema: { + type: 'object', + properties: { + searchOptions: { + properties: { + since: { + type: 'number', + description: 'Timestamp to filter chats since', + example: 1746618541 + }, + unread: { + type: 'boolean', + description: 'Filter for unread chats', + example: true + }, + } + } + } + }, + } + */ + try { + const { searchOptions = {} } = req.body + const client = sessions.get(req.params.sessionId) + const chats = await client.getChats(searchOptions) + res.json({ success: true, chats }) + } catch (error) { + sendErrorResponse(res, 500, error.message) + } +} + /** * Returns the profile picture URL for a given contact ID. * @@ -2170,6 +2220,7 @@ module.exports = { getChatById, getChatLabels, getChats, + getChatsWithSearch, getChatsByLabelId, getCommonGroups, getContactById, diff --git a/src/routes.js b/src/routes.js index 51539c5..a9d228d 100644 --- a/src/routes.js +++ b/src/routes.js @@ -69,6 +69,7 @@ clientRouter.post('/getBlockedContacts/:sessionId', [middleware.sessionNameValid clientRouter.post('/getChatById/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getChatById) clientRouter.post('/getChatLabels/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getChatLabels) clientRouter.get('/getChats/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getChats) +clientRouter.post('/getChats/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getChatsWithSearch) clientRouter.post('/getChatsByLabelId/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getChatsByLabelId) clientRouter.post('/getCommonGroups/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getCommonGroups) clientRouter.post('/getContactById/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getContactById) diff --git a/src/sessions.js b/src/sessions.js index 336cc4b..ae89b42 100644 --- a/src/sessions.js +++ b/src/sessions.js @@ -3,7 +3,7 @@ const fs = require('fs') const path = require('path') const sessions = new Map() const { baseWebhookURL, sessionFolderPath, maxAttachmentSize, setMessagesAsSeen, webVersion, webVersionCacheType, recoverSessions, chromeBin, headless, releaseBrowserLock } = require('./config') -const { triggerWebhook, waitForNestedObject, isEventEnabled, sendMessageSeenStatus, sleep } = require('./utils') +const { triggerWebhook, waitForNestedObject, isEventEnabled, sendMessageSeenStatus, sleep, patchWWebLibrary } = require('./utils') const { logger } = require('./logger') const { initWebSocketServer, terminateWebSocketServer, triggerWebSocket } = require('./websocket') @@ -177,36 +177,12 @@ const setupSession = async (sessionId) => { } try { - await client.initialize() - // hotfix for https://github.com/pedroslopez/whatsapp-web.js/pull/3703 client.once('ready', () => { - client.pupPage.evaluate(() => { - window.Store.FindOrCreateChat = window.require('WAWebFindChatAction') - window.WWebJS.getChat = async (chatId, { getAsModel = true } = {}) => { - const isChannel = /@\w*newsletter\b/.test(chatId) - const chatWid = window.Store.WidFactory.createWid(chatId) - let chat - - if (isChannel) { - try { - chat = window.Store.NewsletterCollection.get(chatId) - if (!chat) { - await window.Store.ChannelUtils.loadNewsletterPreviewChat(chatId) - chat = await window.Store.NewsletterCollection.find(chatWid) - } - } catch (err) { - chat = null - } - } else { - chat = window.Store.Chat.get(chatWid) || (await window.Store.FindOrCreateChat.findOrCreateLatestChat(chatWid))?.chat - } - - return getAsModel && chat - ? await window.WWebJS.getChatModel(chat, { isChannel }) - : chat - } + patchWWebLibrary(client).catch((err) => { + logger.error({ sessionId, err }, 'Failed to patch WWebJS library') }) }) + await client.initialize() } catch (error) { logger.error({ sessionId, err: error }, 'Initialize error') throw error diff --git a/src/utils.js b/src/utils.js index dae931b..bc16f86 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,9 @@ const axios = require('axios') const { globalApiKey, disabledCallbacks, enableWebHook } = require('./config') const { logger } = require('./logger') +const ChatFactory = require('whatsapp-web.js/src/factories/ChatFactory') +const Client = require('whatsapp-web.js').Client +const { Chat, Message } = require('whatsapp-web.js/src/structures') // Trigger webhook endpoint const triggerWebhook = (webhookURL, sessionId, dataType, data) => { @@ -73,6 +76,103 @@ const exposeFunctionIfAbsent = async (page, name, fn) => { await page.exposeFunction(name, fn) } +const patchWWebLibrary = async (client) => { + // MUST be run after the 'ready' event fired + Client.prototype.getChats = async function (searchOptions = {}) { + const chats = await this.pupPage.evaluate(async (searchOptions) => { + return await window.WWebJS.getChats({ ...searchOptions }) + }, searchOptions) + + return chats.map(chat => ChatFactory.create(this, chat)) + } + + Chat.prototype.fetchMessages = async function (searchOptions) { + const messages = await this.client.pupPage.evaluate(async (chatId, searchOptions) => { + const msgFilter = (m) => { + if (m.isNotification) { + return false + } + if (searchOptions && searchOptions.fromMe !== undefined && m.id.fromMe !== searchOptions.fromMe) { + return false + } + if (searchOptions && searchOptions.since !== undefined && Number.isFinite(searchOptions.since) && m.t < searchOptions.since) { + return false + } + return true + } + + const chat = await window.WWebJS.getChat(chatId, { getAsModel: false }) + let msgs = chat.msgs.getModelsArray().filter(msgFilter) + + if (searchOptions && searchOptions.limit > 0) { + while (msgs.length < searchOptions.limit) { + const loadedMessages = await window.Store.ConversationMsgs.loadEarlierMsgs(chat) + if (!loadedMessages || !loadedMessages.length) break + msgs = [...loadedMessages.filter(msgFilter), ...msgs] + } + + if (msgs.length > searchOptions.limit) { + msgs.sort((a, b) => (a.t > b.t) ? 1 : -1) + msgs = msgs.splice(msgs.length - searchOptions.limit) + } + } + + return msgs.map(m => window.WWebJS.getMessageModel(m)) + }, this.id._serialized, searchOptions) + + return messages.map(m => new Message(this.client, m)) + } + + await client.pupPage.evaluate(() => { + // hotfix for https://github.com/pedroslopez/whatsapp-web.js/pull/3643 + window.WWebJS.getChats = async (searchOptions = {}) => { + const chatFilter = (c) => { + if (searchOptions && searchOptions.unread === true && c.unreadCount === 0) { + return false + } + if (searchOptions && searchOptions.since !== undefined && Number.isFinite(searchOptions.since) && c.t < searchOptions.since) { + return false + } + return true + } + + const allChats = window.Store.Chat.getModelsArray() + + const filteredChats = allChats.filter(chatFilter) + + return await Promise.all( + filteredChats.map(chat => window.WWebJS.getChatModel(chat)) + ) + } + + // hotfix for https://github.com/pedroslopez/whatsapp-web.js/pull/3703 + window.Store.FindOrCreateChat = window.require('WAWebFindChatAction') + window.WWebJS.getChat = async (chatId, { getAsModel = true } = {}) => { + const isChannel = /@\w*newsletter\b/.test(chatId) + const chatWid = window.Store.WidFactory.createWid(chatId) + let chat + + if (isChannel) { + try { + chat = window.Store.NewsletterCollection.get(chatId) + if (!chat) { + await window.Store.ChannelUtils.loadNewsletterPreviewChat(chatId) + chat = await window.Store.NewsletterCollection.find(chatWid) + } + } catch (err) { + chat = null + } + } else { + chat = window.Store.Chat.get(chatWid) || (await window.Store.FindOrCreateChat.findOrCreateLatestChat(chatWid))?.chat + } + + return getAsModel && chat + ? await window.WWebJS.getChatModel(chat, { isChannel }) + : chat + } + }) +} + module.exports = { triggerWebhook, sendErrorResponse, @@ -81,5 +181,6 @@ module.exports = { sendMessageSeenStatus, decodeBase64, sleep, - exposeFunctionIfAbsent + exposeFunctionIfAbsent, + patchWWebLibrary } diff --git a/swagger.json b/swagger.json index d8053fc..ce4b594 100644 --- a/swagger.json +++ b/swagger.json @@ -1604,6 +1604,122 @@ "apiKeyAuth": [] } ] + }, + "post": { + "tags": [ + "Client" + ], + "summary": "Get all current chats with optional search parameters", + "description": "", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the session (alphanumeric and - allowed)", + "example": "f8377d8d-a589-4242-9ba6-9486a04ef80c" + } + ], + "responses": { + "200": { + "description": "OK" + }, + "403": { + "description": "Forbidden.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenResponse" + } + } + } + }, + "404": { + "description": "Not Found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Entity.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server failure.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "searchOptions": { + "properties": { + "since": { + "type": "number", + "description": "Timestamp to filter chats since", + "example": 1746618541 + }, + "unread": { + "type": "boolean", + "description": "Filter for unread chats", + "example": true + } + } + } + } + } + }, + "application/xml": { + "schema": { + "type": "object", + "properties": { + "searchOptions": { + "properties": { + "since": { + "type": "number", + "description": "Timestamp to filter chats since", + "example": 1746618541 + }, + "unread": { + "type": "boolean", + "description": "Filter for unread chats", + "example": true + } + } + } + } + } + } + } + } } }, "/client/getChatsByLabelId/{sessionId}": {