feat: ability to filter chats and messages

This commit is contained in:
Anton Voylenko 2025-09-02 23:19:46 +03:00
commit dd886a1fea
5 changed files with 274 additions and 29 deletions

View file

@ -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<void>} 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,

View file

@ -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)

View file

@ -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

View file

@ -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
}

View file

@ -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}": {