diff --git a/.env.example b/.env.example index 3418db9c..1e1f5d15 100644 --- a/.env.example +++ b/.env.example @@ -9,8 +9,8 @@ RATE_LIMIT_WINDOW_MS=1000 # OPTIONAL, TIME FRAME FOR WHICH REQUESTS ARE CHECKED ## Client ## MAX_ATTACHMENT_SIZE=10000000 # IF REACHED, MEDIA ATTACHMENT BODY WILL BE NULL SET_MESSAGES_AS_SEEN=TRUE # WILL MARK THE MESSAGES AS READ AUTOMATICALLY -# ALL CALLBACKS: auth_failure|authenticated|call|change_state|disconnected|group_join|group_leave|group_update|loading_screen|media_uploaded|message|message_ack|message_create|message_reaction|message_revoke_everyone|qr|ready|contact_changed|media -DISABLED_CALLBACKS=message_ack|message_reaction # PREVENT SENDING CERTAIN TYPES OF CALLBACKS BACK TO THE WEBHOOK +# ALL CALLBACKS: auth_failure|authenticated|call|change_state|disconnected|group_join|group_leave|group_update|loading_screen|media_uploaded|message|message_ack|message_create|message_reaction|message_revoke_everyone|qr|ready|contact_changed|unread_count|message_edit|message_ciphertext +DISABLED_CALLBACKS=message_ack|message_reaction|unread_count|message_edit|message_ciphertext # PREVENT SENDING CERTAIN TYPES OF CALLBACKS BACK TO THE WEBHOOK WEB_VERSION='2.2328.5' # OPTIONAL, THE VERSION OF WHATSAPP WEB TO USE WEB_VERSION_CACHE_TYPE=none # OPTIONAL, DETERMINTES WHERE TO GET THE WHATSAPP WEB VERSION(local, remote or none), DEFAULT 'none' RECOVER_SESSIONS=TRUE # OPTIONAL, SHOULD WE RECOVER THE SESSION IN CASE OF PAGE FAILURES diff --git a/src/controllers/sessionController.js b/src/controllers/sessionController.js index b0c90877..51186f94 100644 --- a/src/controllers/sessionController.js +++ b/src/controllers/sessionController.js @@ -1,6 +1,5 @@ - const qr = require('qr-image') -const { setupSession, deleteSession, validateSession, flushSessions, sessions } = require('../sessions') +const { setupSession, deleteSession, reloadSession, validateSession, flushSessions, sessions } = require('../sessions') const { sendErrorResponse, waitForNestedObject } = require('../utils') /** @@ -192,6 +191,52 @@ const sessionQrCodeImage = async (req, res) => { } } +/** + * Restarts the session with the given session ID. + * + * @function + * @async + * @param {Object} req - The HTTP request object. + * @param {Object} res - The HTTP response object. + * @param {string} req.params.sessionId - The session ID to terminate. + * @returns {Promise} + * @throws {Error} If there was an error terminating the session. + */ +const restartSession = async (req, res) => { + // #swagger.summary = 'Restart session' + // #swagger.description = 'Restarts the session with the given session ID.' + try { + const sessionId = req.params.sessionId + const validation = await validateSession(sessionId) + if (validation.message === 'session_not_found') { + return res.json(validation) + } + await reloadSession(sessionId) + /* #swagger.responses[200] = { + description: "Sessions restarted.", + content: { + "application/json": { + schema: { "$ref": "#/definitions/RestartSessionResponse" } + } + } + } + */ + res.json({ success: true, message: 'Restarted successfully' }) + } catch (error) { + /* #swagger.responses[500] = { + description: "Server Failure.", + content: { + "application/json": { + schema: { "$ref": "#/definitions/ErrorResponse" } + } + } + } + */ + console.log('restartSession ERROR', error) + sendErrorResponse(res, 500, error.message) + } +} + /** * Terminates the session with the given session ID. * @@ -323,6 +368,7 @@ module.exports = { statusSession, sessionQrCode, sessionQrCodeImage, + restartSession, terminateSession, terminateInactiveSessions, terminateAllSessions diff --git a/src/routes.js b/src/routes.js index 2cc39999..8f0ffdd6 100644 --- a/src/routes.js +++ b/src/routes.js @@ -40,6 +40,7 @@ sessionRouter.get('/start/:sessionId', middleware.sessionNameValidation, session sessionRouter.get('/status/:sessionId', middleware.sessionNameValidation, sessionController.statusSession) sessionRouter.get('/qr/:sessionId', middleware.sessionNameValidation, sessionController.sessionQrCode) sessionRouter.get('/qr/:sessionId/image', middleware.sessionNameValidation, sessionController.sessionQrCodeImage) +sessionRouter.get('/restart/:sessionId', middleware.sessionNameValidation, sessionController.restartSession) sessionRouter.get('/terminate/:sessionId', middleware.sessionNameValidation, sessionController.terminateSession) sessionRouter.get('/terminateInactive', sessionController.terminateInactiveSessions) sessionRouter.get('/terminateAll', sessionController.terminateAllSessions) diff --git a/src/sessions.js b/src/sessions.js index bf5712cc..45fccf5f 100644 --- a/src/sessions.js +++ b/src/sessions.js @@ -22,15 +22,22 @@ const validateSession = async (sessionId) => { .catch((err) => { return { success: false, state: null, message: err.message } }) // Wait for client.pupPage to be evaluable + let maxRetry = 0 while (true) { try { if (client.pupPage.isClosed()) { return { success: false, state: null, message: 'browser tab closed' } } - await client.pupPage.evaluate('1'); break + await Promise.race([ + client.pupPage.evaluate('1'), + new Promise(resolve => setTimeout(resolve, 1000)) + ]) + break } catch (error) { - // Ignore error and wait for a bit before trying again - await new Promise(resolve => setTimeout(resolve, 100)) + if (maxRetry === 2) { + return { success: false, state: null, message: 'session closed' } + } + maxRetry++ } } @@ -277,10 +284,33 @@ const initializeEvents = (client, sessionId) => { }) }) + checkIfEventisEnabled('message_edit') + .then(_ => { + client.on('message_edit', (message, newBody, prevBody) => { + triggerWebhook(sessionWebhook, sessionId, 'message_edit', { message, newBody, prevBody }) + }) + }) + + checkIfEventisEnabled('message_ciphertext') + .then(_ => { + client.on('message_ciphertext', (message) => { + triggerWebhook(sessionWebhook, sessionId, 'message_ciphertext', { message }) + }) + }) + checkIfEventisEnabled('message_revoke_everyone') .then(_ => { - client.on('message_revoke_everyone', async (after, before) => { - triggerWebhook(sessionWebhook, sessionId, 'message_revoke_everyone', { after, before }) + // eslint-disable-next-line camelcase + client.on('message_revoke_everyone', async (message) => { + // eslint-disable-next-line camelcase + triggerWebhook(sessionWebhook, sessionId, 'message_revoke_everyone', { message }) + }) + }) + + checkIfEventisEnabled('message_revoke_me') + .then(_ => { + client.on('message_revoke_me', async (message) => { + triggerWebhook(sessionWebhook, sessionId, 'message_revoke_me', { message }) }) }) @@ -306,8 +336,30 @@ const initializeEvents = (client, sessionId) => { triggerWebhook(sessionWebhook, sessionId, 'contact_changed', { message, oldId, newId, isContact }) }) }) + + checkIfEventisEnabled('chat_removed') + .then(_ => { + client.on('chat_removed', async (chat) => { + triggerWebhook(sessionWebhook, sessionId, 'chat_removed', { chat }) + }) + }) + + checkIfEventisEnabled('chat_archived') + .then(_ => { + client.on('chat_archived', async (chat, currState, prevState) => { + triggerWebhook(sessionWebhook, sessionId, 'chat_archived', { chat, currState, prevState }) + }) + }) + + checkIfEventisEnabled('unread_count') + .then(_ => { + client.on('unread_count', async (chat) => { + triggerWebhook(sessionWebhook, sessionId, 'unread_count', { chat }) + }) + }) } +// Function to delete client session folder const deleteSessionFolder = async (sessionId) => { try { const targetDirPath = path.join(sessionFolderPath, `session-${sessionId}`) @@ -328,7 +380,36 @@ const deleteSessionFolder = async (sessionId) => { } } -// Function to delete client session +// Function to reload client session without removing browser cache +const reloadSession = async (sessionId) => { + try { + const client = sessions.get(sessionId) + if (!client) { + return + } + client.pupPage.removeAllListeners('close') + client.pupPage.removeAllListeners('error') + try { + const pages = await client.pupBrowser.pages() + await Promise.all(pages.map((page) => page.close())) + await Promise.race([ + client.pupBrowser.close(), + new Promise(resolve => setTimeout(resolve, 5000)) + ]) + } catch (e) { + const childProcess = client.pupBrowser.process() + if (childProcess) { + childProcess.kill(9) + } + } + sessions.delete(sessionId) + setupSession(sessionId) + } catch (error) { + console.log(error) + throw error + } +} + const deleteSession = async (sessionId, validation) => { try { const client = sessions.get(sessionId) @@ -346,10 +427,11 @@ const deleteSession = async (sessionId, validation) => { console.log(`Destroying session ${sessionId}`) await client.destroy() } - - // Wait for client.pupBrowser to be disconnected before deleting the folder - while (client.pupBrowser.isConnected()) { - await new Promise(resolve => setTimeout(resolve, 100)) + // Wait 10 secs for client.pupBrowser to be disconnected before deleting the folder + let maxDelay = 0 + while (client.pupBrowser.isConnected() && (maxDelay < 10)) { + await new Promise(resolve => setTimeout(resolve, 1000)) + maxDelay++ } await deleteSessionFolder(sessionId) sessions.delete(sessionId) @@ -388,5 +470,6 @@ module.exports = { restoreSessions, validateSession, deleteSession, + reloadSession, flushSessions } diff --git a/swagger.js b/swagger.js index 2e7eeded..08a739c7 100644 --- a/swagger.js +++ b/swagger.js @@ -50,6 +50,10 @@ const doc = { state: 'CONNECTED', message: 'session_connected' }, + RestartSessionResponse: { + success: true, + message: 'Restarted successfully' + }, TerminateSessionResponse: { success: true, message: 'Logged out successfully' diff --git a/swagger.json b/swagger.json index 0c89a436..be3420e3 100644 --- a/swagger.json +++ b/swagger.json @@ -36,7 +36,6 @@ "Various" ], "description": "", - "parameters": [], "responses": { "200": { "description": "OK" @@ -342,6 +341,74 @@ ] } }, + "/session/restart/{sessionId}": { + "get": { + "tags": [ + "Session" + ], + "summary": "Restart session", + "description": "Restarts the session with the given session ID.", + "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": "Sessions restarted.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestartSessionResponse" + } + } + } + }, + "403": { + "description": "Forbidden.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenResponse" + } + } + } + }, + "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": [] + } + ] + } + }, "/session/terminate/{sessionId}": { "get": { "tags": [ @@ -417,7 +484,6 @@ ], "summary": "Terminate inactive sessions", "description": "Terminates all inactive sessions.", - "parameters": [], "responses": { "200": { "description": "Sessions terminated.", @@ -464,7 +530,6 @@ ], "summary": "Terminate all sessions", "description": "Terminates all sessions.", - "parameters": [], "responses": { "200": { "description": "Sessions terminated.", @@ -7470,6 +7535,22 @@ "name": "StatusSessionResponse" } }, + "RestartSessionResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Restarted successfully" + } + }, + "xml": { + "name": "RestartSessionResponse" + } + }, "TerminateSessionResponse": { "type": "object", "properties": {