Skip to content

Commit dd886a1

Browse files
committed
feat: ability to filter chats and messages
1 parent 62860a6 commit dd886a1

5 files changed

Lines changed: 274 additions & 29 deletions

File tree

src/controllers/clientController.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,56 @@ const getChats = async (req, res) => {
360360
}
361361
}
362362

363+
/**
364+
* Retrieve all chats for the given session ID with optional search parameters.
365+
*
366+
* @function
367+
* @async
368+
*
369+
* @param {Object} req - The request object.
370+
* @param {string} req.params.sessionId - The session ID.
371+
* @param {Object} res - The response object.
372+
*
373+
* @returns {Promise<void>} A Promise that resolves when the operation is complete.
374+
*
375+
* @throws {Error} If the operation fails, an error is thrown.
376+
*/
377+
const getChatsWithSearch = async (req, res) => {
378+
/*
379+
#swagger.summary = 'Get all current chats with optional search parameters'
380+
#swagger.requestBody = {
381+
required: true,
382+
schema: {
383+
type: 'object',
384+
properties: {
385+
searchOptions: {
386+
properties: {
387+
since: {
388+
type: 'number',
389+
description: 'Timestamp to filter chats since',
390+
example: 1746618541
391+
},
392+
unread: {
393+
type: 'boolean',
394+
description: 'Filter for unread chats',
395+
example: true
396+
},
397+
}
398+
}
399+
}
400+
},
401+
}
402+
*/
403+
try {
404+
const { searchOptions = {} } = req.body
405+
const client = sessions.get(req.params.sessionId)
406+
const chats = await client.getChats(searchOptions)
407+
res.json({ success: true, chats })
408+
} catch (error) {
409+
sendErrorResponse(res, 500, error.message)
410+
}
411+
}
412+
363413
/**
364414
* Returns the profile picture URL for a given contact ID.
365415
*
@@ -2170,6 +2220,7 @@ module.exports = {
21702220
getChatById,
21712221
getChatLabels,
21722222
getChats,
2223+
getChatsWithSearch,
21732224
getChatsByLabelId,
21742225
getCommonGroups,
21752226
getContactById,

src/routes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ clientRouter.post('/getBlockedContacts/:sessionId', [middleware.sessionNameValid
6969
clientRouter.post('/getChatById/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getChatById)
7070
clientRouter.post('/getChatLabels/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getChatLabels)
7171
clientRouter.get('/getChats/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getChats)
72+
clientRouter.post('/getChats/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getChatsWithSearch)
7273
clientRouter.post('/getChatsByLabelId/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getChatsByLabelId)
7374
clientRouter.post('/getCommonGroups/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getCommonGroups)
7475
clientRouter.post('/getContactById/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getContactById)

src/sessions.js

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const fs = require('fs')
33
const path = require('path')
44
const sessions = new Map()
55
const { baseWebhookURL, sessionFolderPath, maxAttachmentSize, setMessagesAsSeen, webVersion, webVersionCacheType, recoverSessions, chromeBin, headless, releaseBrowserLock } = require('./config')
6-
const { triggerWebhook, waitForNestedObject, isEventEnabled, sendMessageSeenStatus, sleep } = require('./utils')
6+
const { triggerWebhook, waitForNestedObject, isEventEnabled, sendMessageSeenStatus, sleep, patchWWebLibrary } = require('./utils')
77
const { logger } = require('./logger')
88
const { initWebSocketServer, terminateWebSocketServer, triggerWebSocket } = require('./websocket')
99

@@ -177,36 +177,12 @@ const setupSession = async (sessionId) => {
177177
}
178178

179179
try {
180-
await client.initialize()
181-
// hotfix for https://github.com/pedroslopez/whatsapp-web.js/pull/3703
182180
client.once('ready', () => {
183-
client.pupPage.evaluate(() => {
184-
window.Store.FindOrCreateChat = window.require('WAWebFindChatAction')
185-
window.WWebJS.getChat = async (chatId, { getAsModel = true } = {}) => {
186-
const isChannel = /@\w*newsletter\b/.test(chatId)
187-
const chatWid = window.Store.WidFactory.createWid(chatId)
188-
let chat
189-
190-
if (isChannel) {
191-
try {
192-
chat = window.Store.NewsletterCollection.get(chatId)
193-
if (!chat) {
194-
await window.Store.ChannelUtils.loadNewsletterPreviewChat(chatId)
195-
chat = await window.Store.NewsletterCollection.find(chatWid)
196-
}
197-
} catch (err) {
198-
chat = null
199-
}
200-
} else {
201-
chat = window.Store.Chat.get(chatWid) || (await window.Store.FindOrCreateChat.findOrCreateLatestChat(chatWid))?.chat
202-
}
203-
204-
return getAsModel && chat
205-
? await window.WWebJS.getChatModel(chat, { isChannel })
206-
: chat
207-
}
181+
patchWWebLibrary(client).catch((err) => {
182+
logger.error({ sessionId, err }, 'Failed to patch WWebJS library')
208183
})
209184
})
185+
await client.initialize()
210186
} catch (error) {
211187
logger.error({ sessionId, err: error }, 'Initialize error')
212188
throw error

src/utils.js

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
const axios = require('axios')
22
const { globalApiKey, disabledCallbacks, enableWebHook } = require('./config')
33
const { logger } = require('./logger')
4+
const ChatFactory = require('whatsapp-web.js/src/factories/ChatFactory')
5+
const Client = require('whatsapp-web.js').Client
6+
const { Chat, Message } = require('whatsapp-web.js/src/structures')
47

58
// Trigger webhook endpoint
69
const triggerWebhook = (webhookURL, sessionId, dataType, data) => {
@@ -73,6 +76,103 @@ const exposeFunctionIfAbsent = async (page, name, fn) => {
7376
await page.exposeFunction(name, fn)
7477
}
7578

79+
const patchWWebLibrary = async (client) => {
80+
// MUST be run after the 'ready' event fired
81+
Client.prototype.getChats = async function (searchOptions = {}) {
82+
const chats = await this.pupPage.evaluate(async (searchOptions) => {
83+
return await window.WWebJS.getChats({ ...searchOptions })
84+
}, searchOptions)
85+
86+
return chats.map(chat => ChatFactory.create(this, chat))
87+
}
88+
89+
Chat.prototype.fetchMessages = async function (searchOptions) {
90+
const messages = await this.client.pupPage.evaluate(async (chatId, searchOptions) => {
91+
const msgFilter = (m) => {
92+
if (m.isNotification) {
93+
return false
94+
}
95+
if (searchOptions && searchOptions.fromMe !== undefined && m.id.fromMe !== searchOptions.fromMe) {
96+
return false
97+
}
98+
if (searchOptions && searchOptions.since !== undefined && Number.isFinite(searchOptions.since) && m.t < searchOptions.since) {
99+
return false
100+
}
101+
return true
102+
}
103+
104+
const chat = await window.WWebJS.getChat(chatId, { getAsModel: false })
105+
let msgs = chat.msgs.getModelsArray().filter(msgFilter)
106+
107+
if (searchOptions && searchOptions.limit > 0) {
108+
while (msgs.length < searchOptions.limit) {
109+
const loadedMessages = await window.Store.ConversationMsgs.loadEarlierMsgs(chat)
110+
if (!loadedMessages || !loadedMessages.length) break
111+
msgs = [...loadedMessages.filter(msgFilter), ...msgs]
112+
}
113+
114+
if (msgs.length > searchOptions.limit) {
115+
msgs.sort((a, b) => (a.t > b.t) ? 1 : -1)
116+
msgs = msgs.splice(msgs.length - searchOptions.limit)
117+
}
118+
}
119+
120+
return msgs.map(m => window.WWebJS.getMessageModel(m))
121+
}, this.id._serialized, searchOptions)
122+
123+
return messages.map(m => new Message(this.client, m))
124+
}
125+
126+
await client.pupPage.evaluate(() => {
127+
// hotfix for https://github.com/pedroslopez/whatsapp-web.js/pull/3643
128+
window.WWebJS.getChats = async (searchOptions = {}) => {
129+
const chatFilter = (c) => {
130+
if (searchOptions && searchOptions.unread === true && c.unreadCount === 0) {
131+
return false
132+
}
133+
if (searchOptions && searchOptions.since !== undefined && Number.isFinite(searchOptions.since) && c.t < searchOptions.since) {
134+
return false
135+
}
136+
return true
137+
}
138+
139+
const allChats = window.Store.Chat.getModelsArray()
140+
141+
const filteredChats = allChats.filter(chatFilter)
142+
143+
return await Promise.all(
144+
filteredChats.map(chat => window.WWebJS.getChatModel(chat))
145+
)
146+
}
147+
148+
// hotfix for https://github.com/pedroslopez/whatsapp-web.js/pull/3703
149+
window.Store.FindOrCreateChat = window.require('WAWebFindChatAction')
150+
window.WWebJS.getChat = async (chatId, { getAsModel = true } = {}) => {
151+
const isChannel = /@\w*newsletter\b/.test(chatId)
152+
const chatWid = window.Store.WidFactory.createWid(chatId)
153+
let chat
154+
155+
if (isChannel) {
156+
try {
157+
chat = window.Store.NewsletterCollection.get(chatId)
158+
if (!chat) {
159+
await window.Store.ChannelUtils.loadNewsletterPreviewChat(chatId)
160+
chat = await window.Store.NewsletterCollection.find(chatWid)
161+
}
162+
} catch (err) {
163+
chat = null
164+
}
165+
} else {
166+
chat = window.Store.Chat.get(chatWid) || (await window.Store.FindOrCreateChat.findOrCreateLatestChat(chatWid))?.chat
167+
}
168+
169+
return getAsModel && chat
170+
? await window.WWebJS.getChatModel(chat, { isChannel })
171+
: chat
172+
}
173+
})
174+
}
175+
76176
module.exports = {
77177
triggerWebhook,
78178
sendErrorResponse,
@@ -81,5 +181,6 @@ module.exports = {
81181
sendMessageSeenStatus,
82182
decodeBase64,
83183
sleep,
84-
exposeFunctionIfAbsent
184+
exposeFunctionIfAbsent,
185+
patchWWebLibrary
85186
}

swagger.json

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1604,6 +1604,122 @@
16041604
"apiKeyAuth": []
16051605
}
16061606
]
1607+
},
1608+
"post": {
1609+
"tags": [
1610+
"Client"
1611+
],
1612+
"summary": "Get all current chats with optional search parameters",
1613+
"description": "",
1614+
"parameters": [
1615+
{
1616+
"name": "sessionId",
1617+
"in": "path",
1618+
"required": true,
1619+
"schema": {
1620+
"type": "string"
1621+
},
1622+
"description": "Unique identifier for the session (alphanumeric and - allowed)",
1623+
"example": "f8377d8d-a589-4242-9ba6-9486a04ef80c"
1624+
}
1625+
],
1626+
"responses": {
1627+
"200": {
1628+
"description": "OK"
1629+
},
1630+
"403": {
1631+
"description": "Forbidden.",
1632+
"content": {
1633+
"application/json": {
1634+
"schema": {
1635+
"$ref": "#/components/schemas/ForbiddenResponse"
1636+
}
1637+
}
1638+
}
1639+
},
1640+
"404": {
1641+
"description": "Not Found.",
1642+
"content": {
1643+
"application/json": {
1644+
"schema": {
1645+
"$ref": "#/components/schemas/NotFoundResponse"
1646+
}
1647+
}
1648+
}
1649+
},
1650+
"422": {
1651+
"description": "Unprocessable Entity.",
1652+
"content": {
1653+
"application/json": {
1654+
"schema": {
1655+
"$ref": "#/components/schemas/ErrorResponse"
1656+
}
1657+
}
1658+
}
1659+
},
1660+
"500": {
1661+
"description": "Server failure.",
1662+
"content": {
1663+
"application/json": {
1664+
"schema": {
1665+
"$ref": "#/components/schemas/ErrorResponse"
1666+
}
1667+
}
1668+
}
1669+
}
1670+
},
1671+
"security": [
1672+
{
1673+
"apiKeyAuth": []
1674+
}
1675+
],
1676+
"requestBody": {
1677+
"required": true,
1678+
"content": {
1679+
"application/json": {
1680+
"schema": {
1681+
"type": "object",
1682+
"properties": {
1683+
"searchOptions": {
1684+
"properties": {
1685+
"since": {
1686+
"type": "number",
1687+
"description": "Timestamp to filter chats since",
1688+
"example": 1746618541
1689+
},
1690+
"unread": {
1691+
"type": "boolean",
1692+
"description": "Filter for unread chats",
1693+
"example": true
1694+
}
1695+
}
1696+
}
1697+
}
1698+
}
1699+
},
1700+
"application/xml": {
1701+
"schema": {
1702+
"type": "object",
1703+
"properties": {
1704+
"searchOptions": {
1705+
"properties": {
1706+
"since": {
1707+
"type": "number",
1708+
"description": "Timestamp to filter chats since",
1709+
"example": 1746618541
1710+
},
1711+
"unread": {
1712+
"type": "boolean",
1713+
"description": "Filter for unread chats",
1714+
"example": true
1715+
}
1716+
}
1717+
}
1718+
}
1719+
}
1720+
}
1721+
}
1722+
}
16071723
}
16081724
},
16091725
"/client/getChatsByLabelId/{sessionId}": {

0 commit comments

Comments
 (0)