Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Session reload and support more events #222

Merged
merged 1 commit into from
Jun 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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