Skip to content

Commit

Permalink
Merge pull request #222 from avoylenko/session-reload
Browse files Browse the repository at this point in the history
Session reload and support more events
  • Loading branch information
chrishubert authored Jun 2, 2024
2 parents a30b53b + 1ec984b commit c312345
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 17 deletions.
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 48 additions & 2 deletions src/controllers/sessionController.js
Original file line number Diff line number Diff line change
@@ -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')

/**
Expand Down Expand Up @@ -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<void>}
* @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.
*
Expand Down Expand Up @@ -323,6 +368,7 @@ module.exports = {
statusSession,
sessionQrCode,
sessionQrCodeImage,
restartSession,
terminateSession,
terminateInactiveSessions,
terminateAllSessions
Expand Down
1 change: 1 addition & 0 deletions src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
103 changes: 93 additions & 10 deletions src/sessions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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++
}
}

Expand Down Expand Up @@ -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 })
})
})

Expand All @@ -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}`)
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -388,5 +470,6 @@ module.exports = {
restoreSessions,
validateSession,
deleteSession,
reloadSession,
flushSessions
}
4 changes: 4 additions & 0 deletions swagger.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ const doc = {
state: 'CONNECTED',
message: 'session_connected'
},
RestartSessionResponse: {
success: true,
message: 'Restarted successfully'
},
TerminateSessionResponse: {
success: true,
message: 'Logged out successfully'
Expand Down
87 changes: 84 additions & 3 deletions swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
"Various"
],
"description": "",
"parameters": [],
"responses": {
"200": {
"description": "OK"
Expand Down Expand Up @@ -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": [
Expand Down Expand Up @@ -417,7 +484,6 @@
],
"summary": "Terminate inactive sessions",
"description": "Terminates all inactive sessions.",
"parameters": [],
"responses": {
"200": {
"description": "Sessions terminated.",
Expand Down Expand Up @@ -464,7 +530,6 @@
],
"summary": "Terminate all sessions",
"description": "Terminates all sessions.",
"parameters": [],
"responses": {
"200": {
"description": "Sessions terminated.",
Expand Down Expand Up @@ -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": {
Expand Down

0 comments on commit c312345

Please sign in to comment.