├── .editorconfig ├── .env.example ├── .eslintrc.json ├── .github └── FUNDING.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── Baileys API v0.4.postman_collection.json ├── README.md ├── app.js ├── controllers ├── chatsController.js ├── getMessages.js ├── groupsController.js ├── miscControlls.js └── sessionsController.js ├── dirname.js ├── examples ├── authenticating.js └── sending-message.js ├── middlewares ├── authenticationValidator.js ├── requestValidator.js └── sessionValidator.js ├── package.json ├── postman_collection.json ├── response.js ├── routes.js ├── routes ├── chatsRoute.js ├── groupsRoute.js ├── miscRoute.js └── sessionsRoute.js ├── sessions └── .gitignore ├── store └── memory-store.js ├── utils ├── download.js └── functions.js └── whatsapp.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Api host and port 2 | HOST=127.0.0.1 3 | PORT=8000 4 | 5 | # Number retry to connect to whatsapp -1 for infinite 6 | MAX_RETRIES=-1 7 | 8 | # Maximun time to connect to whatsapp 9 | RECONNECT_INTERVAL=5000 10 | 11 | # Authentication 12 | AUTHENTICATION_GLOBAL_AUTH_TOKEN=A4gx18YGxKAvR01ClcHpcR7TjZUNtwvE 13 | 14 | # WEBHOOK CONFIGURATION 15 | APP_WEBHOOK_URL="https://webhook.site/dbf387ed-a861-4ae9-9b30-9344c972cb74" 16 | APP_WEBHOOK_ALLOWED_EVENTS=MESSAGES_UPSERT,MESSAGES_DELETE,MESSAGES_UPDATE 17 | APP_WEBHOOK_FILE_IN_BASE64=false 18 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "extends": ["xo", "prettier"], 6 | "parserOptions": { 7 | "ecmaVersion": 2020, 8 | "sourceType": "module" 9 | }, 10 | "rules": { 11 | "prettier/prettier": "error", 12 | "one-var": "off", 13 | "max-params": ["error", 5], 14 | "arrow-body-style": ["error", "always"], 15 | "radix": "off", 16 | "no-unused-expressions": ["error", { "allowTernary": true }], 17 | "curly": "error", 18 | "new-cap": "off", 19 | "no-return-assign": "off", 20 | "no-await-in-loop": "off" 21 | }, 22 | "plugins": ["prettier"] 23 | } 24 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [andresayac]# Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://paypal.me/pelislatino24'] 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | yarn.lock 4 | .env 5 | bun.lock -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | sessions/ 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "endOfLine": "lf", 6 | "semi": false, 7 | "singleQuote": true, 8 | "overrides": [ 9 | { 10 | "files": "*.json", 11 | "options": { 12 | "tabWidth": 2 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Baileys API 3 | [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/andresayac/baileys-api) 4 | 5 | An implementation of [@whiskeysockets/Baileys](https://github.com/whiskeysockets/Baileys) as a simple RESTful API service with multiple device support. This project implements both **Multi-Device** client so that you can choose and use one of them easily. 6 | 7 | ## Requirements 8 | 9 | - **NodeJS** version **18.16.0** or higher. 10 | 11 | ## Installation 12 | 13 | 1. Download or clone this repo. 14 | 2. Enter to the project directory. 15 | 3. Install the dependencies. 16 | 17 | ## `.env` Configurations 18 | 19 | ```env 20 | # Api host and port 21 | HOST=127.0.0.1 22 | PORT=8000 23 | 24 | # Number retry to connect to whatsapp -1 for infinite 25 | MAX_RETRIES=-1 26 | 27 | # Maximun time to connect to whatsapp 28 | RECONNECT_INTERVAL=5000 29 | 30 | # Authentication 31 | AUTHENTICATION_GLOBAL_AUTH_TOKEN=A4gx18YGxKAvR01ClcHpcR7TjZUNtwvE 32 | 33 | # WEBHOOK CONFIGURATION 34 | APP_WEBHOOK_URL="" 35 | APP_WEBHOOK_ALLOWED_EVENTS=MESSAGES_UPSERT,MESSAGES_DELETE,MESSAGES_UPDATE 36 | APP_WEBHOOK_FILE_IN_BASE64=false 37 | 38 | ``` 39 | 40 | ## Usage 41 | 42 | 1. You can start the app by executing `npm run start` or `node .`. 43 | 2. Now the endpoint should be available according to your environment variable configurations. Default is at `http://localhost:8000`. 44 | 45 | Also check out the `examples` directory for the basic usage examples. 46 | 47 | ## API Docs 48 | 49 | The API documentation is available online [here](https://documenter.getpostman.com/view/9471522/2s8YehTwHJ). You can also import the **Postman Collection File** `(postman_collection.json)` into your Postman App alternatively. 50 | 51 | The server will respond in following JSON format: 52 | 53 | ```javascript 54 | { 55 | success: true|false, // bool 56 | message: "", // string 57 | data: {}|[] // object or array of object 58 | } 59 | ``` 60 | 61 | ## Available Features 62 | At this moment we are working to bring more functionalities 63 | ### Autentication 64 | * ApiKey (By default it is not active, change it in env by adding your custom key) 65 | ### Sessions 66 | * Find Session 67 | * Session Status 68 | * List Sessions 69 | * Create New Session 70 | => QR method (Default) 71 | => Pairing Code method 72 | * Delete Session 73 | ### Chats 74 | * Get Chat List 75 | * Get Conversation 76 | * Forward Message 77 | * Send Presence Update 78 | * Read Message 79 | * Send Bulk Message 80 | * Send Message Types 81 | => Send Message Text 82 | => Send Message Image 83 | => Send Message Audio 84 | => Send Message Video 85 | => Send Message Document 86 | => Send Message Gif 87 | => Send Message Sticker 88 | => Send Message Contact 89 | => Send Message Location 90 | => Send Message React 91 | => Send Message How To Forward 92 | ### Groups 93 | * Get Chat List 94 | * Get Conversation 95 | * Get Group Metadata 96 | * Create Group 97 | * Group Update Participants 98 | * Group Update Subject 99 | * Group Update Description 100 | * Group Update Settings 101 | * Group Get Invite Code 102 | * Group Join Invite Code 103 | * Group Revoke Invite Code 104 | * Group Update Picture 105 | * Group List Without Participants 106 | ### Misc 107 | * Update Profile Status 108 | * Update Profile Name 109 | * Update Progile Image 110 | * Get My Profile {name, phote, status} 111 | * Get Profile User 112 | * Block And Unblock User 113 | * Public Story Status (NEW) 114 | ### Webhook 115 | * Global webhook 116 | 117 | ## Webhook Events 118 | Configure in .env by default this `MESSAGES_UPSERT,MESSAGES_DELETE,MESSAGES_UPDATE` or use `ALL` 119 | If it is necessary to send multimedia message in base64 use `APP_WEBHOOK_FILE_IN_BASE64=true` 120 | 121 | | Name | Event | TypeData | Description | 122 | |------|-------|-----------|------------| 123 | | ALL | | | All event send to Webhook | 124 | | QRCODE_UPDATED | qrcode.updated | json | Sends the base64 of the qrcode for reading | 125 | | CONNECTION_UPDATE | connection.update | json | Informs the status of the connection with whatsapp | 126 | | MESSAGES_UPSERT | message.upsert | json | Notifies you when a message is received | 127 | | MESSAGES_UPDATE | message.update | json | Tells you when a message is updated | 128 | | MESSAGES_DELETE | messages.delete | JSON | Notifies when message is delete | 129 | | MESSAGING_HISTORY_SET | messaging-history.set | JSON | set chats (history sync), everything is reverse chronologically sorted| 130 | | MESSAGES_MEDIA_UPDATE | messages.media-update | JSON | Notifies when a message message media have update | 131 | | MESSAGES_REACTION | messages.reaction | JSON | message was reacted to. If reaction was removed -- then "reaction.text" will be falsey | 132 | | MESSAGES_RECEIPT_UPDATE | message-receipt.update | JSON | Notifies when a message have update | 133 | | MESSAGES_DELETE | messages.delete | JSON | Notifies when a message is delete | 134 | | CONTACTS_SET | contacts.set | json | Performs initial loading of all contacts
This event occurs only once | 135 | | CONTACTS_UPSERT | contacts.upsert | json | Reloads all contacts with additional information
This event occurs only once | 136 | | CONTACTS_UPDATE | contacts.update | json | Informs you when the chat is updated | 137 | | PRESENCE_UPDATE | presence.update | json | Informs if the user is online, if he is performing some action like writing or recording and his last seen
'unavailable' | 'available' | 'composing' | 'recording' | 'paused' | 138 | | CHATS_SET | chats.set | json | Send a list of all loaded chats | 139 | | CHATS_UPDATE | chats.update | json | Informs you when the chat is updated | 140 | | CHATS_UPSERT | chats.upsert | json | Sends any new chat information | 141 | | CHATS_DELETE | chats.delete | JSON | Notifies when chats is delete | 142 | | GROUPS_UPSERT | groups.upsert | JSON | Notifies when a group is created | 143 | | GROUPS_UPDATE | groups.update | JSON | Notifies when a group has its information updated | 144 | | GROUP_PARTICIPANTS_UPDATE | group-participants.update | JSON | Notifies when an action occurs involving a participant
'add' | 'remove' | 'promote' | 'demote' | 145 | | BLOCKLIST_SET | blocklist.set | JSON | Notifies when is set contact in blocklist | 146 | | BLOCKLIST_UPDATE | blocklist.update | JSON | event of add/remove contact in blocklist | 147 | | LABELS_EDIT | labels.edit | JSON | event edit label | 148 | | LABELS_ASSOCIATION | labels.association | JSON | add/remove chat label association action | 149 | 150 | 151 | ## Known Issue 152 | 153 | Currently there's no known issues. If you find any, please kindly open a new one. 154 | 155 | ## Notes 156 | 157 | - The app only provide a very simple validation, you may want to implement your own. 158 | - When sending message, your `message` property will not be validated, so make sure you sent the right data! 159 | - There's no authentication, you may want to implement your own. 160 | - The **Beta Multi-Device** client use provided Baileys's `makeInMemoryStore` method which will store your data in memory and a json file, you may want to use a better data management. 161 | - Automatically reading incoming messages is now disabled by default. Uncomment `whatsapp.js:91-105` to enable this behaviour. 162 | - If you have problems when deploying on **CPanel** or any other similar hosting, transpiling your code into **CommonJS** should fix the problems. 163 | 164 | ## Notice 165 | 166 | This project is intended for learning purpose only, don't use it for spamming or any activities that's prohibited by **WhatsApp**. 167 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import express from 'express' 3 | import nodeCleanup from 'node-cleanup' 4 | import routes from './routes.js' 5 | import { init, cleanup } from './whatsapp.js' 6 | import cors from 'cors' 7 | 8 | const app = express() 9 | 10 | const host = process.env.HOST || undefined 11 | const port = parseInt(process.env.PORT ?? 8000) 12 | 13 | app.use(cors()) 14 | app.use(express.urlencoded({ extended: true })) 15 | app.use(express.json()) 16 | app.use('/', routes) 17 | 18 | const listenerCallback = () => { 19 | init() 20 | console.log(`Server is listening on http://${host ? host : 'localhost'}:${port}`) 21 | } 22 | 23 | if (host) { 24 | app.listen(port, host, listenerCallback) 25 | } else { 26 | app.listen(port, listenerCallback) 27 | } 28 | 29 | nodeCleanup(cleanup) 30 | 31 | export default app 32 | -------------------------------------------------------------------------------- /controllers/chatsController.js: -------------------------------------------------------------------------------- 1 | import { 2 | getSession, 3 | getChatList, 4 | isExists, 5 | sendMessage, 6 | formatPhone, 7 | formatGroup, 8 | readMessage, 9 | getMessageMedia, 10 | getStoreMessage, 11 | } from './../whatsapp.js' 12 | import response from './../response.js' 13 | import { compareAndFilter, fileExists, isUrlValid } from './../utils/functions.js' 14 | 15 | const getList = (req, res) => { 16 | return response(res, 200, true, '', getChatList(res.locals.sessionId)) 17 | } 18 | 19 | const send = async (req, res) => { 20 | const session = getSession(res.locals.sessionId) 21 | const { message } = req.body 22 | const isGroup = req.body.isGroup ?? false 23 | const receiver = isGroup ? formatGroup(req.body.receiver) : formatPhone(req.body.receiver) 24 | 25 | const typesMessage = ['image', 'video', 'audio', 'document', 'sticker'] 26 | 27 | const filterTypeMessaje = compareAndFilter(Object.keys(message), typesMessage) 28 | try { 29 | const exists = await isExists(session, receiver, isGroup) 30 | 31 | if (!exists) { 32 | return response(res, 400, false, 'The receiver number is not exists.') 33 | } 34 | 35 | if (filterTypeMessaje.length > 0) { 36 | const url = message[filterTypeMessaje]?.url 37 | 38 | if (url.length === undefined || url.length === 0) { 39 | return response(res, 400, false, 'The URL is invalid or empty.') 40 | } 41 | 42 | if (!isUrlValid(url)) { 43 | if (!fileExists(url)) { 44 | return response(res, 400, false, 'The file or url does not exist.') 45 | } 46 | } 47 | } 48 | 49 | await sendMessage(session, receiver, message, {}, 0) 50 | 51 | response(res, 200, true, 'The message has been successfully sent.') 52 | } catch { 53 | response(res, 500, false, 'Failed to send the message.') 54 | } 55 | } 56 | 57 | const sendBulk = async (req, res) => { 58 | const session = getSession(res.locals.sessionId) 59 | const errors = [] 60 | 61 | for (const [key, data] of req.body.entries()) { 62 | let { receiver, message, delay } = data 63 | 64 | if (!receiver || !message) { 65 | errors.push({ key, message: 'The receiver number is not exists.' }) 66 | continue 67 | } 68 | 69 | if (!delay || isNaN(delay)) { 70 | delay = 1000 71 | } 72 | 73 | receiver = formatPhone(receiver) 74 | 75 | try { 76 | const exists = await isExists(session, receiver) 77 | 78 | if (!exists) { 79 | errors.push({ key, message: 'number not exists on whatsapp' }) 80 | continue 81 | } 82 | 83 | await sendMessage(session, receiver, message, {}, delay) 84 | } catch (err) { 85 | errors.push({ key, message: err.message }) 86 | } 87 | } 88 | 89 | if (errors.length === 0) { 90 | return response(res, 200, true, 'All messages has been successfully sent.') 91 | } 92 | 93 | const isAllFailed = errors.length === req.body.length 94 | 95 | response( 96 | res, 97 | isAllFailed ? 500 : 200, 98 | !isAllFailed, 99 | isAllFailed ? 'Failed to send all messages.' : 'Some messages has been successfully sent.', 100 | { errors }, 101 | ) 102 | } 103 | 104 | const deleteChat = async (req, res) => { 105 | const session = getSession(res.locals.sessionId) 106 | const { receiver, isGroup, message } = req.body 107 | 108 | try { 109 | const jidFormat = isGroup ? formatGroup(receiver) : formatPhone(receiver) 110 | 111 | await sendMessage(session, jidFormat, { delete: message }) 112 | response(res, 200, true, 'Message has been successfully deleted.') 113 | } catch { 114 | response(res, 500, false, 'Failed to delete message .') 115 | } 116 | } 117 | 118 | const forward = async (req, res) => { 119 | const session = getSession(res.locals.sessionId) 120 | const { forward, receiver, isGroup } = req.body 121 | 122 | const { id, remoteJid } = forward 123 | const jidFormat = isGroup ? formatGroup(receiver) : formatPhone(receiver) 124 | 125 | try { 126 | const messages = await session.store.loadMessages(remoteJid, 25, null) 127 | 128 | 129 | const key = [...messages.values()].filter((element) => { 130 | return element.key.id === id 131 | }) 132 | 133 | const queryForward = { 134 | forward: key[0], 135 | } 136 | 137 | await sendMessage(session, jidFormat, queryForward, {}, 0) 138 | 139 | response(res, 200, true, 'The message has been successfully forwarded.') 140 | } catch { 141 | response(res, 500, false, 'Failed to forward the message.') 142 | } 143 | } 144 | 145 | const read = async (req, res) => { 146 | const session = getSession(res.locals.sessionId) 147 | const { keys } = req.body 148 | 149 | try { 150 | await readMessage(session, keys) 151 | 152 | if (!keys[0].id) { 153 | throw new Error('Data not found') 154 | } 155 | 156 | response(res, 200, true, 'The message has been successfully marked as read.') 157 | } catch { 158 | response(res, 500, false, 'Failed to mark the message as read.') 159 | } 160 | } 161 | 162 | const sendPresence = async (req, res) => { 163 | const session = getSession(res.locals.sessionId) 164 | const { receiver, isGroup, presence } = req.body 165 | 166 | try { 167 | const jidFormat = isGroup ? formatGroup(receiver) : formatPhone(receiver) 168 | 169 | await session.sendPresenceUpdate(presence, jidFormat) 170 | 171 | response(res, 200, true, 'Presence has been successfully sent.') 172 | } catch { 173 | response(res, 500, false, 'Failed to send presence.') 174 | } 175 | } 176 | 177 | const downloadMedia = async (req, res) => { 178 | const session = getSession(res.locals.sessionId) 179 | const { remoteJid, messageId } = req.body 180 | 181 | try { 182 | const message = await getStoreMessage(session, messageId, remoteJid) 183 | const dataMessage = await getMessageMedia(session, message) 184 | 185 | response(res, 200, true, 'Message downloaded successfully', dataMessage) 186 | } catch { 187 | response( 188 | res, 189 | 500, 190 | false, 191 | 'Error downloading multimedia message: it may not exist or may not contain multimedia content.', 192 | ) 193 | } 194 | } 195 | 196 | export { getList, send, sendBulk, deleteChat, read, forward, sendPresence, downloadMedia } 197 | -------------------------------------------------------------------------------- /controllers/getMessages.js: -------------------------------------------------------------------------------- 1 | import { getSession, formatGroup, formatPhone } from '../whatsapp.js' 2 | import response from './../response.js' 3 | 4 | const getMessages = async (req, res) => { 5 | const session = getSession(res.locals.sessionId) 6 | 7 | const { jid } = req.params 8 | const { limit = 25, cursorId = null, cursorFromMe = null, isGroup = false } = req.query 9 | 10 | const isGroupBool = isGroup === 'true' 11 | const jidFormat = isGroupBool ? formatGroup(jid) : formatPhone(jid) 12 | 13 | const cursor = {} 14 | 15 | if (cursorId) { 16 | cursor.before = { 17 | id: cursorId, 18 | fromMe: Boolean(cursorFromMe && cursorFromMe === 'true'), 19 | } 20 | } 21 | 22 | try { 23 | const useCursor = 'before' in cursor ? cursor : null 24 | const messages = await session.store.loadMessages(jidFormat, limit, useCursor) 25 | 26 | response(res, 200, true, '', messages) 27 | } catch { 28 | response(res, 500, false, 'Failed to load messages.') 29 | } 30 | } 31 | 32 | export default getMessages 33 | -------------------------------------------------------------------------------- /controllers/groupsController.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable arrow-body-style */ 2 | import { 3 | getSession, 4 | getChatList, 5 | isExists, 6 | sendMessage, 7 | formatGroup, 8 | formatPhone, 9 | getGroupsWithParticipants, 10 | participantsUpdate, 11 | updateSubject, 12 | updateDescription, 13 | settingUpdate, 14 | leave, 15 | inviteCode, 16 | acceptInvite, 17 | revokeInvite, 18 | profilePicture, 19 | } from './../whatsapp.js' 20 | import response from './../response.js' 21 | 22 | const getList = (req, res) => { 23 | return response(res, 200, true, '', getChatList(res.locals.sessionId, true)) 24 | } 25 | 26 | const getListWithoutParticipants = async (req, res) => { 27 | const session = getSession(res.locals.sessionId) 28 | try { 29 | const groups = await getGroupsWithParticipants(session) 30 | return response(res, 200, true, '', groups) 31 | } catch { 32 | response(res, 500, false, 'Failed to get group list with participants.') 33 | } 34 | } 35 | 36 | const getGroupMetaData = async (req, res) => { 37 | const session = getSession(res.locals.sessionId) 38 | const { jid } = req.params 39 | 40 | try { 41 | const data = await session.groupMetadata(jid) 42 | 43 | if (!data.id) { 44 | return response(res, 400, false, 'The group is not exists.') 45 | } 46 | 47 | response(res, 200, true, '', data) 48 | } catch { 49 | response(res, 500, false, 'Failed to get group metadata.') 50 | } 51 | } 52 | 53 | const create = async (req, res) => { 54 | const session = getSession(res.locals.sessionId) 55 | const { groupName, participants } = req.body 56 | const participantsformat = participants.map((e) => formatPhone(e)) 57 | try { 58 | const group = await session.groupCreate(groupName, participantsformat) 59 | response(res, 200, true, 'The group has been successfully created.', group) 60 | } catch { 61 | response(res, 500, false, 'Failed to create the group.') 62 | } 63 | } 64 | 65 | const send = async (req, res) => { 66 | const session = getSession(res.locals.sessionId) 67 | 68 | try { 69 | const receiver = formatGroup(req.body.receiver) 70 | const { message } = req.body 71 | 72 | const exists = await isExists(session, receiver, true) 73 | 74 | if (!exists) { 75 | return response(res, 400, false, 'The receiver number is not exists.') 76 | } 77 | 78 | await sendMessage(session, receiver, message, {}, 0) 79 | 80 | response(res, 200, true, 'The message has been successfully sent.') 81 | } catch { 82 | response(res, 500, false, 'Failed to send the message.') 83 | } 84 | } 85 | 86 | const groupParticipantsUpdate = async (req, res) => { 87 | const session = getSession(res.locals.sessionId) 88 | try { 89 | const jid = formatGroup(req.params.jid) 90 | const participants = req?.body?.participants 91 | const action = req?.body?.action 92 | const participantsFormat = participants.map((e) => formatPhone(e)) 93 | 94 | const exists = await isExists(session, jid, true) 95 | if (!exists) { 96 | return response(res, 400, false, 'The group is not exists.') 97 | } 98 | 99 | await participantsUpdate(session, formatGroup(jid), participantsFormat, action) 100 | response(res, 200, true, 'Update participants successfully.') 101 | } catch { 102 | response(res, 500, false, 'Failed update participants.') 103 | } 104 | } 105 | 106 | const groupUpdateSubject = async (req, res) => { 107 | const session = getSession(res.locals.sessionId) 108 | try { 109 | const jid = formatGroup(req.params.jid) 110 | const subject = req?.body?.subject 111 | const exists = await isExists(session, jid, true) 112 | 113 | if (!exists) { 114 | return response(res, 400, false, 'The group is not exists.') 115 | } 116 | 117 | await updateSubject(session, formatGroup(jid), subject) 118 | 119 | response(res, 200, true, 'Update subject successfully.') 120 | } catch { 121 | response(res, 500, false, 'Failed update subject.') 122 | } 123 | } 124 | 125 | const groupUpdateDescription = async (req, res) => { 126 | const session = getSession(res.locals.sessionId) 127 | try { 128 | const jid = formatGroup(req.params.jid) 129 | const { description } = req.body 130 | const exists = await isExists(session, jid, true) 131 | 132 | if (!exists) { 133 | return response(res, 400, false, 'The group is not exists.') 134 | } 135 | 136 | await updateDescription(session, formatGroup(jid), description) 137 | 138 | response(res, 200, true, 'Update description successfully.') 139 | } catch { 140 | response(res, 500, false, 'Failed description subject.') 141 | } 142 | } 143 | 144 | const groupSettingUpdate = async (req, res) => { 145 | const session = getSession(res.locals.sessionId) 146 | try { 147 | const jid = formatGroup(req.params.jid) 148 | const settings = req?.body?.settings 149 | 150 | const exists = await isExists(session, jid, true) 151 | 152 | if (!exists) { 153 | return response(res, 400, false, 'The group is not exists.') 154 | } 155 | 156 | await settingUpdate(session, jid, settings) 157 | 158 | response(res, 200, true, 'Update setting successfully.') 159 | } catch { 160 | response(res, 500, false, 'Failed update setting.') 161 | } 162 | } 163 | 164 | const groupLeave = async (req, res) => { 165 | const session = getSession(res.locals.sessionId) 166 | try { 167 | const jid = formatGroup(req.params.jid) 168 | const exists = await isExists(session, jid, true) 169 | 170 | if (!exists) { 171 | return response(res, 400, false, 'The group is not exists.') 172 | } 173 | 174 | await leave(session, jid) 175 | 176 | response(res, 200, true, 'Leave group successfully.') 177 | } catch { 178 | response(res, 500, false, 'Failed leave group.') 179 | } 180 | } 181 | 182 | const groupInviteCode = async (req, res) => { 183 | const session = getSession(res.locals.sessionId) 184 | try { 185 | const jid = formatGroup(req.params.jid) 186 | const exists = await isExists(session, jid, true) 187 | 188 | if (!exists) { 189 | return response(res, 400, false, 'The group is not exists.') 190 | } 191 | 192 | const group = await inviteCode(session, jid) 193 | 194 | response(res, 200, true, 'Invite code successfully.', group) 195 | } catch { 196 | response(res, 500, false, 'Failed invite code.') 197 | } 198 | } 199 | 200 | const groupAcceptInvite = async (req, res) => { 201 | const session = getSession(res.locals.sessionId) 202 | try { 203 | const group = await acceptInvite(session, req.body) 204 | 205 | response(res, 200, true, 'Accept invite successfully.', group) 206 | } catch { 207 | response(res, 500, false, 'Failed accept invite.') 208 | } 209 | } 210 | 211 | const groupRevokeInvite = async (req, res) => { 212 | const session = getSession(res.locals.sessionId) 213 | try { 214 | const jid = formatGroup(req.params.jid) 215 | 216 | const exists = await isExists(session, jid, true) 217 | 218 | if (!exists) { 219 | return response(res, 400, false, 'The group is not exists.') 220 | } 221 | 222 | const group = await revokeInvite(session, jid) 223 | 224 | response(res, 200, true, 'Revoke code successfully.', group) 225 | } catch { 226 | response(res, 500, false, 'Failed rovoke code.') 227 | } 228 | } 229 | 230 | const updateProfilePicture = async (req, res) => { 231 | const session = getSession(res.locals.sessionId) 232 | try { 233 | const jid = formatGroup(req.params.jid) 234 | const { url } = req.body 235 | const exists = await isExists(session, jid, true) 236 | 237 | if (!exists) { 238 | return response(res, 400, false, 'The group is not exists.') 239 | } 240 | 241 | await profilePicture(session, jid, url) 242 | response(res, 200, true, 'Update profile picture successfully.') 243 | } catch { 244 | response(res, 500, false, 'Failed Update profile picture.') 245 | } 246 | } 247 | 248 | export { 249 | getList, 250 | getGroupMetaData, 251 | create, 252 | send, 253 | groupParticipantsUpdate, 254 | groupUpdateSubject, 255 | groupUpdateDescription, 256 | groupSettingUpdate, 257 | groupLeave, 258 | groupInviteCode, 259 | groupAcceptInvite, 260 | groupRevokeInvite, 261 | getListWithoutParticipants, 262 | updateProfilePicture, 263 | } 264 | -------------------------------------------------------------------------------- /controllers/miscControlls.js: -------------------------------------------------------------------------------- 1 | import { 2 | updateProfileStatus, 3 | updateProfileName, 4 | getSession, 5 | getProfilePicture, 6 | formatPhone, 7 | formatGroup, 8 | profilePicture, 9 | blockAndUnblockUser, 10 | sendMessage, 11 | } from './../whatsapp.js' 12 | import response from './../response.js' 13 | 14 | import { compareAndFilter, fileExists, isUrlValid } from './../utils/functions.js' 15 | 16 | const setProfileStatus = async (req, res) => { 17 | try { 18 | const session = getSession(res.locals.sessionId) 19 | await updateProfileStatus(session, req.body.status) 20 | response(res, 200, true, 'The status has been updated successfully') 21 | } catch { 22 | response(res, 500, false, 'Failed to update status') 23 | } 24 | } 25 | 26 | const setProfileName = async (req, res) => { 27 | try { 28 | const session = getSession(res.locals.sessionId) 29 | await updateProfileName(session, req.body.name) 30 | response(res, 200, true, 'The name has been updated successfully') 31 | } catch { 32 | response(res, 500, false, 'Failed to update name') 33 | } 34 | } 35 | 36 | const setProfilePicture = async (req, res) => { 37 | try { 38 | const session = getSession(res.locals.sessionId) 39 | const { url } = req.body 40 | session.user.phone = session.user.id.split(':')[0].split('@')[0] 41 | await profilePicture(session, session.user.phone + '@s.whatsapp.net', url) 42 | response(res, 200, true, 'Update profile picture successfully.') 43 | } catch { 44 | response(res, 500, false, 'Failed Update profile picture.') 45 | } 46 | } 47 | 48 | const getProfile = async (req, res) => { 49 | try { 50 | const session = getSession(res.locals.sessionId) 51 | 52 | session.user.phone = session.user.id.split(':')[0].split('@')[0] 53 | session.user.image = await session.profilePictureUrl(session.user.id, 'image') 54 | session.user.status = await session.fetchStatus(session.user.phone + '@s.whatsapp.net') 55 | 56 | response(res, 200, true, 'The information has been obtained successfully.', session.user) 57 | } catch { 58 | response(res, 500, false, 'Could not get the information') 59 | } 60 | } 61 | 62 | const getProfilePictureUser = async (req, res) => { 63 | try { 64 | const session = getSession(res.locals.sessionId) 65 | const isGroup = req.body.isGroup ?? false 66 | const jid = isGroup ? formatGroup(req.body.jid) : formatPhone(req.body.jid) 67 | 68 | const imagen = await getProfilePicture(session, jid, 'image') 69 | 70 | response(res, 200, true, 'The image has been obtained successfully.', imagen) 71 | } catch (err) { 72 | if (err === null) { 73 | return response(res, 404, false, 'the user or group not have image') 74 | } 75 | 76 | response(res, 500, false, 'Could not get the information') 77 | } 78 | } 79 | 80 | const blockAndUnblockContact = async (req, res) => { 81 | try { 82 | const session = getSession(res.locals.sessionId) 83 | const { jid, isBlock } = req.body 84 | const jidFormat = formatPhone(jid) 85 | const blockFormat = isBlock === true ? 'block' : 'unblock' 86 | await blockAndUnblockUser(session, jidFormat, blockFormat) 87 | response(res, 200, true, 'The contact has been blocked or unblocked successfully') 88 | } catch { 89 | response(res, 500, false, 'Failed to block or unblock contact') 90 | } 91 | } 92 | const shareStory = async (req, res) => { 93 | const session = getSession(res.locals.sessionId); 94 | const { 95 | receiver, 96 | message, 97 | options = { 98 | backgroundColor: "#103529", 99 | font: 12 100 | } 101 | } = req.body; 102 | 103 | const statusJid = 'status@broadcast'; 104 | const typesMessage = ['image', 'video', 'audio']; 105 | let finalReceivers = []; 106 | if (session?.user?.id) { 107 | finalReceivers.push(formatPhone(session.user.id)); 108 | } 109 | 110 | if (!receiver || (typeof receiver === 'string' && receiver.length === 0)) { 111 | return response(res, 400, false, 'The receiver number does not exist.'); 112 | } 113 | if (receiver === 'all_contacts') { 114 | const contacts = Object.keys(session.store.contacts) 115 | 116 | if (contacts.length === 0) { 117 | return response(res, 400, false, 'No contacts found.'); 118 | } 119 | 120 | finalReceivers.push(...contacts); 121 | } 122 | 123 | if (typeof receiver === 'string') { 124 | if (receiver === session.user.id) { 125 | return response(res, 400, false, 'You cannot send a message to yourself.'); 126 | } 127 | 128 | if (receiver !== 'all_contacts') { 129 | finalReceivers.push(formatPhone(receiver)); 130 | } 131 | 132 | 133 | } else if (Array.isArray(receiver)) { 134 | if (receiver.length === 0) { 135 | return response(res, 400, false, 'The receiver list is empty.'); 136 | } 137 | 138 | const invalidReceiver = receiver.find(r => typeof r !== 'string'); 139 | if (invalidReceiver) { 140 | return response(res, 400, false, 'All receivers must be strings.'); 141 | } 142 | 143 | finalReceivers.push(...receiver.map(r => formatPhone(r))); 144 | } 145 | 146 | const optionsBroadcast = { 147 | backgroundColor: options.backgroundColor || "#103529", 148 | font: options.font || 12, 149 | broadcast: true, 150 | statusJidList: finalReceivers, 151 | } 152 | 153 | const filterTypeMessage = compareAndFilter(Object.keys(message), typesMessage); 154 | 155 | try { 156 | if (filterTypeMessage.length > 0) { 157 | const mediaType = filterTypeMessage[0]; 158 | const url = message[mediaType]?.url; 159 | 160 | if (!url || url.length === 0) { 161 | return response(res, 400, false, 'The URL is invalid or empty.'); 162 | } 163 | 164 | if (!isUrlValid(url) && !fileExists(url)) { 165 | return response(res, 400, false, 'The file or URL does not exist.'); 166 | } 167 | } 168 | 169 | await sendMessage(session, statusJid, message, optionsBroadcast, 0); 170 | 171 | return response(res, 200, true, 'The story status has been successfully sent.'); 172 | } catch { 173 | return response(res, 500, false, 'Failed to send the story status.'); 174 | } 175 | }; 176 | 177 | 178 | 179 | export { 180 | setProfileStatus, 181 | setProfileName, 182 | setProfilePicture, 183 | getProfile, 184 | getProfilePictureUser, 185 | blockAndUnblockContact, 186 | shareStory, 187 | } 188 | -------------------------------------------------------------------------------- /controllers/sessionsController.js: -------------------------------------------------------------------------------- 1 | import { isSessionExists, createSession, getSession, getListSessions, deleteSession } from './../whatsapp.js' 2 | import response from './../response.js' 3 | 4 | const find = (req, res) => { 5 | response(res, 200, true, 'Session found.') 6 | } 7 | 8 | const status = (req, res) => { 9 | const states = ['connecting', 'connected', 'disconnecting', 'disconnected'] 10 | 11 | const session = getSession(res.locals.sessionId) 12 | let state = states[session.ws?.socket?.readyState] 13 | 14 | state = state === 'connected' && typeof session.user !== 'undefined' ? 'authenticated' : state 15 | 16 | response(res, 200, true, '', { status: state }) 17 | } 18 | 19 | const add = (req, res) => { 20 | const { id, typeAuth, phoneNumber } = req.body 21 | 22 | if (isSessionExists(id)) { 23 | return response(res, 409, false, 'Session already exists, please use another id.') 24 | } 25 | 26 | if (!['qr', 'code'].includes(typeAuth) && typeAuth !== undefined) { 27 | return response(res, 400, false, 'typeAuth must be qr or code.') 28 | } 29 | 30 | const usePairingCode = typeAuth === 'code' 31 | 32 | if (usePairingCode && !phoneNumber) { 33 | return response(res, 400, false, 'phoneNumber is required.') 34 | } 35 | 36 | createSession(id, res, { usePairingCode, phoneNumber }) 37 | } 38 | 39 | const del = async (req, res) => { 40 | const { id } = req.params 41 | const session = getSession(id) 42 | try { 43 | await session.logout() 44 | session.end() 45 | session.ws.close() 46 | } catch { 47 | } finally { 48 | deleteSession(id) 49 | } 50 | 51 | response(res, 200, true, 'The session has been successfully deleted.') 52 | } 53 | 54 | const list = (req, res) => { 55 | response(res, 200, true, 'Session list', getListSessions()) 56 | } 57 | 58 | export { find, status, add, del, list } 59 | -------------------------------------------------------------------------------- /dirname.js: -------------------------------------------------------------------------------- 1 | import { dirname } from 'path' 2 | import { fileURLToPath } from 'url' 3 | 4 | const __dirname = dirname(fileURLToPath(import.meta.url)) 5 | 6 | export default __dirname 7 | -------------------------------------------------------------------------------- /examples/authenticating.js: -------------------------------------------------------------------------------- 1 | const BASE_URI = 'http://localhost:8000/' 2 | const SESSION_ID = 'john' 3 | 4 | const sendRequest = async () => { 5 | // Here we are using fetch API to send the request 6 | const response = await fetch(`${BASE_URI}sessions/add`, { 7 | method: 'POST', 8 | body: JSON.stringify({ 9 | id: SESSION_ID, 10 | // A string value representing the client type. 11 | // Use 'false' for Multi-Device, 12 | // or 'true' for Legacy (Normal WhatsApp Web). 13 | // This is important since the generated QR is not compatible each other. 14 | // So make sure you generated the correct one. 15 | isLegacy: 'false', 16 | }), 17 | headers: { 18 | 'Content-Type': 'application/json', 19 | }, 20 | }) 21 | 22 | return response.json() 23 | } 24 | 25 | ;(async () => { 26 | const response = await sendRequest() 27 | 28 | if (response.success && 'qr' in response.data) { 29 | // Scan the QR 30 | } 31 | })() 32 | -------------------------------------------------------------------------------- /examples/sending-message.js: -------------------------------------------------------------------------------- 1 | const BASE_URI = 'http://localhost:8000/' 2 | const SESSION_ID = 'john' 3 | 4 | const sendMessage = async (endpoint, data) => { 5 | // Here we are using fetch API to send the request 6 | const response = await fetch(`${BASE_URI}${endpoint}?id=${SESSION_ID}`, { 7 | method: 'POST', 8 | body: JSON.stringify(data), 9 | headers: { 10 | 'Content-Type': 'application/json', 11 | }, 12 | }) 13 | 14 | return response.json() 15 | } 16 | 17 | ;(async () => { 18 | // Send Text Message to Someone 19 | await sendMessage('chats/send', { 20 | receiver: '628231xxxxx', 21 | message: { 22 | text: 'Hello there!', 23 | }, 24 | }) 25 | 26 | // Send Bulk Text Message to Multiple Person 27 | await sendMessage('chats/send-bulk', [ 28 | { 29 | receiver: '628231xxxxx', 30 | message: { 31 | text: 'Hello! How are you?', 32 | }, 33 | }, 34 | { 35 | receiver: '628951xxxxx', 36 | message: { 37 | text: "I'm fine, thank you.", 38 | }, 39 | }, 40 | ]) 41 | 42 | // Send Text Message to a Group 43 | await sendMessage('groups/send', { 44 | receiver: '628950xxxxx-1631xxxxx', 45 | message: { 46 | text: 'Hello guys!', 47 | }, 48 | }) 49 | })() 50 | -------------------------------------------------------------------------------- /middlewares/authenticationValidator.js: -------------------------------------------------------------------------------- 1 | import response from './../response.js' 2 | 3 | const validate = (req, res, next) => { 4 | const apiKey = req.get('apikey') ?? req.query.apikey 5 | 6 | if (!process.env.AUTHENTICATION_GLOBAL_AUTH_TOKEN) { 7 | return next() 8 | } 9 | 10 | if (apiKey !== process.env.AUTHENTICATION_GLOBAL_AUTH_TOKEN) { 11 | return response(res, 401, false, 'Authentication failed.') 12 | } 13 | 14 | next() 15 | } 16 | 17 | export default validate 18 | -------------------------------------------------------------------------------- /middlewares/requestValidator.js: -------------------------------------------------------------------------------- 1 | import { validationResult } from 'express-validator' 2 | import response from './../response.js' 3 | 4 | const validate = (req, res, next) => { 5 | const errors = validationResult(req) 6 | 7 | if (!errors.isEmpty()) { 8 | return response(res, 400, false, 'Please fill out all required input.') 9 | } 10 | 11 | next() 12 | } 13 | 14 | export default validate 15 | -------------------------------------------------------------------------------- /middlewares/sessionValidator.js: -------------------------------------------------------------------------------- 1 | import { isSessionExists, isSessionConnected } from '../whatsapp.js' 2 | import response from './../response.js' 3 | 4 | const validate = (req, res, next) => { 5 | const sessionId = req.query.id ?? req.params.id 6 | 7 | if (!isSessionExists(sessionId)) { 8 | return response(res, 404, false, 'Session not found.') 9 | } 10 | 11 | if (req.baseUrl !== '/sessions' && !isSessionConnected(sessionId)) { 12 | return response(res, 400, false, 'There is no connection with whatsapp at the moment, please try again') 13 | } 14 | 15 | res.locals.sessionId = sessionId 16 | next() 17 | } 18 | 19 | export default validate 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "baileys-api", 3 | "description": "Simple RESTful WhatsApp API", 4 | "private": true, 5 | "main": "app.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node ." 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/ookamiiixd/baileys-api.git" 13 | }, 14 | "author": "ookamiiixd ", 15 | "bugs": { 16 | "url": "https://github.com/ookamiiixd/baileys-api/issues" 17 | }, 18 | "homepage": "https://github.com/ookamiiixd/baileys-api#readme", 19 | "engines": { 20 | "node": ">=18.16.0" 21 | }, 22 | "dependencies": { 23 | "baileys": "^6.7.18", 24 | "axios": "^1.8.4", 25 | "cors": "^2.8.5", 26 | "dotenv": "^16.5.0", 27 | "express": "^4.21.2", 28 | "express-validator": "^7.2.1", 29 | "node-cache": "^5.1.2", 30 | "node-cleanup": "^2.1.2", 31 | "pino": "^9.6.0", 32 | "qrcode": "^1.5.4" 33 | }, 34 | "devDependencies": { 35 | "eslint": "^8.57.1", 36 | "eslint-plugin-prettier": "^5.2.6", 37 | "prettier": "^3.5.3" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /response.js: -------------------------------------------------------------------------------- 1 | const response = (res, statusCode = 200, success = false, message = '', data = {}) => { 2 | res.status(statusCode) 3 | res.json({ 4 | success, 5 | message, 6 | data, 7 | }) 8 | 9 | res.end() 10 | } 11 | 12 | export default response 13 | -------------------------------------------------------------------------------- /routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import sessionsRoute from './routes/sessionsRoute.js' 3 | import chatsRoute from './routes/chatsRoute.js' 4 | import groupsRoute from './routes/groupsRoute.js' 5 | import miscRoute from './routes/miscRoute.js' 6 | import response from './response.js' 7 | import authenticationValidator from './middlewares/authenticationValidator.js' 8 | 9 | const router = Router() 10 | 11 | // Use auth middleware for all routes 12 | router.use(authenticationValidator) 13 | 14 | router.use('/sessions', sessionsRoute) 15 | router.use('/chats', chatsRoute) 16 | router.use('/groups', groupsRoute) 17 | router.use('/misc', miscRoute) 18 | 19 | router.all('*', (req, res) => { 20 | response(res, 404, false, 'The requested url cannot be found.') 21 | }) 22 | 23 | export default router 24 | -------------------------------------------------------------------------------- /routes/chatsRoute.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import { body, query } from 'express-validator' 3 | import requestValidator from './../middlewares/requestValidator.js' 4 | import sessionValidator from './../middlewares/sessionValidator.js' 5 | import * as controller from './../controllers/chatsController.js' 6 | import getMessages from './../controllers/getMessages.js' 7 | 8 | const router = Router() 9 | 10 | router.get('/', query('id').notEmpty(), requestValidator, sessionValidator, controller.getList) 11 | 12 | router.get('/:jid', query('id').notEmpty(), requestValidator, sessionValidator, getMessages) 13 | 14 | router.post( 15 | '/delete', 16 | query('id').notEmpty(), 17 | body('receiver').notEmpty(), 18 | body('message').notEmpty(), 19 | requestValidator, 20 | sessionValidator, 21 | controller.deleteChat 22 | ) 23 | 24 | router.post( 25 | '/send', 26 | query('id').notEmpty(), 27 | body('receiver').notEmpty(), 28 | body('message').notEmpty(), 29 | requestValidator, 30 | sessionValidator, 31 | controller.send 32 | ) 33 | 34 | router.post('/send-bulk', query('id').notEmpty(), requestValidator, sessionValidator, controller.sendBulk) 35 | 36 | router.post( 37 | '/forward', 38 | query('id').notEmpty(), 39 | body('forward').notEmpty(), 40 | body('receiver').notEmpty(), 41 | body('isGroup').notEmpty(), 42 | requestValidator, 43 | sessionValidator, 44 | controller.forward 45 | ) 46 | 47 | router.post( 48 | '/read', 49 | query('id').notEmpty(), 50 | body('keys').notEmpty(), 51 | requestValidator, 52 | sessionValidator, 53 | controller.read 54 | ) 55 | 56 | router.post( 57 | '/send-presence', 58 | query('id').notEmpty(), 59 | body('receiver').notEmpty(), 60 | body('presence').notEmpty(), 61 | requestValidator, 62 | sessionValidator, 63 | controller.sendPresence 64 | ) 65 | 66 | router.post( 67 | '/download-media', 68 | query('id').notEmpty(), 69 | body('remoteJid').notEmpty(), 70 | body('messageId').notEmpty(), 71 | requestValidator, 72 | sessionValidator, 73 | controller.downloadMedia 74 | ) 75 | 76 | export default router 77 | -------------------------------------------------------------------------------- /routes/groupsRoute.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import { body, query } from 'express-validator' 3 | import requestValidator from './../middlewares/requestValidator.js' 4 | import sessionValidator from './../middlewares/sessionValidator.js' 5 | import * as controller from './../controllers/groupsController.js' 6 | import getMessages from './../controllers/getMessages.js' 7 | 8 | const router = Router() 9 | 10 | router.get('/', query('id').notEmpty(), requestValidator, sessionValidator, controller.getList) 11 | 12 | router.post( 13 | '/create', 14 | query('id').notEmpty(), 15 | body('groupName').notEmpty(), 16 | body('participants').notEmpty(), 17 | requestValidator, 18 | sessionValidator, 19 | controller.create 20 | ) 21 | 22 | router.post( 23 | '/send/:jid', 24 | query('id').notEmpty(), 25 | body('receiver').notEmpty(), 26 | body('message').notEmpty(), 27 | requestValidator, 28 | sessionValidator, 29 | controller.send 30 | ) 31 | 32 | router.get('/:jid', query('id').notEmpty(), requestValidator, sessionValidator, getMessages) 33 | 34 | router.get('/meta/:jid', query('id').notEmpty(), requestValidator, sessionValidator, controller.getGroupMetaData) 35 | 36 | router.post( 37 | '/participants-update/:jid', 38 | query('id').notEmpty(), 39 | body('action').notEmpty(), 40 | body('participants').notEmpty(), 41 | requestValidator, 42 | sessionValidator, 43 | controller.groupParticipantsUpdate 44 | ) 45 | 46 | router.post( 47 | '/subject-update/:jid', 48 | query('id').notEmpty(), 49 | body('subject').notEmpty(), 50 | requestValidator, 51 | sessionValidator, 52 | controller.groupUpdateSubject 53 | ) 54 | 55 | router.post( 56 | '/description-update/:jid', 57 | query('id').notEmpty(), 58 | body('description').notEmpty(), 59 | requestValidator, 60 | sessionValidator, 61 | controller.groupUpdateDescription 62 | ) 63 | 64 | router.post( 65 | '/setting-update/:jid', 66 | query('id').notEmpty(), 67 | body('settings').notEmpty(), 68 | requestValidator, 69 | sessionValidator, 70 | controller.groupSettingUpdate 71 | ) 72 | 73 | router.post('/leave/:jid', query('id').notEmpty(), requestValidator, sessionValidator, controller.groupLeave) 74 | 75 | router.get('/invite-code/:jid', query('id').notEmpty(), requestValidator, sessionValidator, controller.groupInviteCode) 76 | 77 | router.post( 78 | '/accept-invite', 79 | query('id').notEmpty(), 80 | body('invite').notEmpty(), 81 | requestValidator, 82 | sessionValidator, 83 | controller.groupAcceptInvite 84 | ) 85 | 86 | router.post( 87 | '/revoke-code/:jid', 88 | query('id').notEmpty(), 89 | requestValidator, 90 | sessionValidator, 91 | controller.groupRevokeInvite 92 | ) 93 | 94 | router.post( 95 | '/profile-picture/:jid', 96 | query('id').notEmpty(), 97 | body('url').notEmpty(), 98 | requestValidator, 99 | sessionValidator, 100 | controller.updateProfilePicture 101 | ) 102 | 103 | router.post( 104 | '/get-participants', 105 | query('id').notEmpty(), 106 | requestValidator, 107 | sessionValidator, 108 | controller.getListWithoutParticipants 109 | ) 110 | 111 | export default router 112 | -------------------------------------------------------------------------------- /routes/miscRoute.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import { body, query } from 'express-validator' 3 | import requestValidator from './../middlewares/requestValidator.js' 4 | import sessionValidator from './../middlewares/sessionValidator.js' 5 | import * as controller from './../controllers/miscControlls.js' 6 | 7 | const router = Router() 8 | 9 | router.post( 10 | '/update-profile-status', 11 | query('id').notEmpty(), 12 | body('status').notEmpty(), 13 | requestValidator, 14 | sessionValidator, 15 | controller.setProfileStatus, 16 | ) 17 | router.post( 18 | '/update-profile-name', 19 | query('id').notEmpty(), 20 | body('name').notEmpty(), 21 | requestValidator, 22 | sessionValidator, 23 | controller.setProfileName, 24 | ) 25 | router.post('/my-profile', query('id').notEmpty(), requestValidator, sessionValidator, controller.getProfile) 26 | 27 | router.post( 28 | '/profile-picture', 29 | query('id').notEmpty(), 30 | body('jid').notEmpty(), 31 | body('isGroup').notEmpty(), 32 | requestValidator, 33 | sessionValidator, 34 | controller.getProfilePictureUser, 35 | ) 36 | 37 | router.post( 38 | '/set-profile-picture', 39 | query('id').notEmpty(), 40 | body('url').notEmpty(), 41 | requestValidator, 42 | sessionValidator, 43 | controller.setProfilePicture, 44 | ) 45 | 46 | router.post( 47 | '/block-and-unblock', 48 | query('id').notEmpty(), 49 | body('jid').notEmpty(), 50 | body('isBlock').notEmpty(), 51 | requestValidator, 52 | sessionValidator, 53 | controller.blockAndUnblockContact, 54 | ) 55 | 56 | router.post( 57 | '/public-story-status', 58 | query('id').notEmpty(), 59 | body('receiver').notEmpty(), 60 | body('message').notEmpty(), 61 | requestValidator, 62 | sessionValidator, 63 | controller.shareStory, 64 | ) 65 | 66 | export default router 67 | -------------------------------------------------------------------------------- /routes/sessionsRoute.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import { body } from 'express-validator' 3 | import requestValidator from './../middlewares/requestValidator.js' 4 | import sessionValidator from './../middlewares/sessionValidator.js' 5 | import * as controller from './../controllers/sessionsController.js' 6 | 7 | const router = Router() 8 | 9 | router.get('/list', requestValidator, controller.list) 10 | 11 | router.get('/find/:id', sessionValidator, controller.find) 12 | 13 | router.get('/status/:id', sessionValidator, controller.status) 14 | 15 | router.post('/add', body('id').notEmpty(), requestValidator, controller.add) 16 | 17 | router.delete('/delete/:id', sessionValidator, controller.del) 18 | 19 | export default router 20 | -------------------------------------------------------------------------------- /sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /store/memory-store.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import proto from 'baileys' 4 | import { jidNormalizedUser, toNumber } from 'baileys'; 5 | 6 | function makeInMemoryStore() { 7 | const chats = new Map(); 8 | const messages = new Map(); // { jid: Map } 9 | const contacts = {}; 10 | const groupMetadata = new Map(); 11 | 12 | const bind = (ev) => { 13 | ev.on('messages.upsert', ({ messages: newMessages, type }) => { 14 | switch (type) { 15 | case 'append': 16 | case 'notify': 17 | for (const msg of newMessages) { 18 | const jid = jidNormalizedUser(msg.key.remoteJid); 19 | 20 | if (!messages.has(jid)) { 21 | messages.set(jid, new Map()); 22 | } 23 | const list = messages.get(jid); 24 | 25 | list.set(msg.key.id, msg); 26 | 27 | if (type === 'notify' && !chats.has(jid)) { 28 | ev.emit('chats.upsert', [ 29 | { 30 | id: jid, 31 | conversationTimestamp: toNumber(msg.messageTimestamp), 32 | unreadCount: 1 33 | } 34 | ]); 35 | } 36 | } 37 | break; 38 | } 39 | }); 40 | 41 | ev.on('messaging-history.set', ({ 42 | chats: newChats, 43 | contacts: newContacts, 44 | messages: newMessages, 45 | isLatest, 46 | syncType 47 | }) => { 48 | if (syncType === 6) { 49 | return; 50 | } 51 | 52 | if (isLatest) { 53 | chats.clear(); 54 | 55 | for (const jid of messages.keys()) { 56 | messages.delete(jid); 57 | } 58 | } 59 | 60 | let chatsAdded = 0; 61 | for (const chat of newChats) { 62 | if (!chats.has(chat.id)) { 63 | chats.set(chat.id, chat); 64 | chatsAdded++; 65 | } 66 | } 67 | 68 | const contactsUpsert = (newContacts) => { 69 | const oldContacts = []; 70 | for (const contact of newContacts) { 71 | const jid = jidNormalizedUser(contact.id); 72 | if (!contacts[jid]) { 73 | contacts[jid] = contact; 74 | oldContacts.push(jid); 75 | } else { 76 | Object.assign(contacts[jid], contact); 77 | } 78 | } 79 | return oldContacts; 80 | } 81 | 82 | const oldContacts = contactsUpsert(newContacts); 83 | if (isLatest) { 84 | for (const jid of oldContacts) { 85 | delete contacts[jid]; 86 | } 87 | } 88 | }); 89 | 90 | 91 | ev.on('chats.upsert', newChats => { 92 | for (const chat of newChats) { 93 | chats.set(chat.id, chat); 94 | } 95 | }); 96 | 97 | ev.on('chats.update', updates => { 98 | for (let update of updates) { 99 | const existing = chats.get(update.id); 100 | 101 | if (existing) { 102 | if (update.unreadCount > 0) { 103 | update = { ...update }; 104 | update.unreadCount = (existing.unreadCount || 0) + update.unreadCount; 105 | } 106 | 107 | Object.assign(existing, update); 108 | } 109 | } 110 | }); 111 | 112 | ev.on('chats.set', ({ chats: newChats }) => { 113 | for (const chat of newChats) { 114 | chats.set(chat.id, chat); 115 | } 116 | }); 117 | 118 | ev.on('chats.delete', deletions => { 119 | for (const item of deletions) { 120 | if (chats.has(item)) { 121 | chats.delete(item); 122 | } 123 | } 124 | }); 125 | 126 | ev.on('contacts.upsert', (newContacts) => { 127 | for (const contact of newContacts) { 128 | contacts[contact.id] = contact; 129 | } 130 | }); 131 | 132 | ev.on('groups.update', (updates) => { 133 | for (const update of updates) { 134 | if (groupMetadata.has(update.id)) { 135 | const current = groupMetadata.get(update.id); 136 | groupMetadata.set(update.id, { ...current, ...update }); 137 | } else { 138 | groupMetadata.set(update.id, update); 139 | } 140 | } 141 | }); 142 | }; 143 | 144 | const readFromFile = (file = path.resolve(__dirname, '../baileys_store.json')) => { 145 | if (!fs.existsSync(file)) { 146 | return; 147 | }; 148 | 149 | try { 150 | const raw = JSON.parse(fs.readFileSync(file, 'utf-8')); 151 | for (const [jid, chat] of raw.chats || []) chats.set(jid, chat); 152 | for (const [jid, msgs] of Object.entries(raw.messages || {})) { 153 | messages.set(jid, new Map(msgs)); 154 | } 155 | Object.assign(contacts, raw.contacts || {}); 156 | for (const [jid, meta] of raw.groupMetadata || []) groupMetadata.set(jid, meta); 157 | } catch (err) { 158 | console.error('Failed to read store:', err.message); 159 | } 160 | }; 161 | 162 | const writeToFile = (file = path.resolve(__dirname, '../baileys_store.json')) => { 163 | try { 164 | const data = { 165 | chats: [...chats.entries()], 166 | messages: Object.fromEntries( 167 | [...messages.entries()].map(([jid, msgs]) => [jid, [...msgs.entries()]]) 168 | ), 169 | contacts, 170 | groupMetadata: [...groupMetadata.entries()] 171 | }; 172 | fs.writeFileSync(file, JSON.stringify(data, null, 2)); 173 | } catch (err) { 174 | console.error('Failed to write store:', err.message); 175 | } 176 | }; 177 | 178 | const loadMessage = async (jid, id) => { 179 | const msgs = messages.get(jid); 180 | if (!msgs) return undefined; 181 | return msgs.get(id); 182 | }; 183 | 184 | const fetchGroupMetadata = async (jid, sock) => { 185 | try { 186 | const metadata = await sock.groupMetadata(jid); 187 | groupMetadata.set(jid, metadata); 188 | return metadata; 189 | } catch (err) { 190 | console.error('Failed to fetch group metadata:', err.message); 191 | return null; 192 | } 193 | }; 194 | 195 | return { 196 | chats, 197 | messages, 198 | contacts, 199 | groupMetadata, 200 | bind, 201 | readFromFile, 202 | writeToFile, 203 | loadMessage, 204 | fetchGroupMetadata, 205 | }; 206 | } 207 | 208 | export default makeInMemoryStore; -------------------------------------------------------------------------------- /utils/download.js: -------------------------------------------------------------------------------- 1 | import { createWriteStream } from 'fs' 2 | import axios from 'axios' 3 | 4 | const downloadImage = async (url) => { 5 | const name = Math.floor(Date.now() / 1000) 6 | const filepath = './uploads/profile/' + name + '.jpg' 7 | 8 | const response = await axios({ 9 | url, 10 | method: 'GET', 11 | responseType: 'stream', 12 | }) 13 | 14 | return new Promise((resolve, reject) => { 15 | response.data 16 | .pipe(createWriteStream(filepath)) 17 | .on('érror', reject) 18 | .once('close', () => { 19 | resolve(filepath) 20 | }) 21 | }) 22 | } 23 | 24 | export { downloadImage } 25 | -------------------------------------------------------------------------------- /utils/functions.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | 3 | const compareAndFilter = (array1, array2) => { 4 | return array1.filter((item) => { 5 | return array2.includes(item) 6 | }) 7 | } 8 | 9 | const isUrlValid = (url) => { 10 | return Boolean( 11 | /^(?:(?:(?:https?|ftp):)\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:[/?#]\S*)?$/i.test( 12 | url 13 | ) 14 | ) 15 | } 16 | 17 | const fileExists = (path) => { 18 | return Boolean(fs.existsSync(path)) 19 | } 20 | 21 | const deleteFile = async (path) => { 22 | return new Promise((resolve, reject) => { 23 | fs.unlink(path, (err) => { 24 | err ? reject(err) : resolve(true) 25 | }) 26 | }) 27 | } 28 | 29 | export { compareAndFilter, isUrlValid, fileExists, deleteFile } 30 | -------------------------------------------------------------------------------- /whatsapp.js: -------------------------------------------------------------------------------- 1 | import { rmSync, readdir, existsSync } from 'fs' 2 | import { join } from 'path' 3 | import pino from 'pino' 4 | import makeWASocketModule, { 5 | useMultiFileAuthState, 6 | makeCacheableSignalKeyStore, 7 | DisconnectReason, 8 | delay, 9 | downloadMediaMessage, 10 | getAggregateVotesInPollMessage, 11 | fetchLatestBaileysVersion, 12 | WAMessageStatus, 13 | } from 'baileys' 14 | 15 | import proto from 'baileys' 16 | 17 | import makeInMemoryStore from './store/memory-store.js' 18 | 19 | import { toDataURL } from 'qrcode' 20 | import __dirname from './dirname.js' 21 | import response from './response.js' 22 | import { downloadImage } from './utils/download.js' 23 | import axios from 'axios' 24 | import NodeCache from 'node-cache' 25 | 26 | const msgRetryCounterCache = new NodeCache() 27 | 28 | const sessions = new Map() 29 | const retries = new Map() 30 | 31 | const APP_WEBHOOK_ALLOWED_EVENTS = process.env.APP_WEBHOOK_ALLOWED_EVENTS.split(',') 32 | 33 | const sessionsDir = (sessionId = '') => { 34 | return join(__dirname, 'sessions', sessionId ? sessionId : '') 35 | } 36 | 37 | const isSessionExists = (sessionId) => { 38 | return sessions.has(sessionId) 39 | } 40 | 41 | const isSessionConnected = (sessionId) => { 42 | return sessions.get(sessionId)?.ws?.socket?.readyState === 1 43 | } 44 | 45 | const shouldReconnect = (sessionId) => { 46 | const maxRetries = parseInt(process.env.MAX_RETRIES ?? 0) 47 | let attempts = retries.get(sessionId) ?? 0 48 | 49 | // MaxRetries = maxRetries < 1 ? 1 : maxRetries 50 | if (attempts < maxRetries || maxRetries === -1) { 51 | ++attempts 52 | 53 | console.log('Reconnecting...', { attempts, sessionId }) 54 | retries.set(sessionId, attempts) 55 | 56 | return true 57 | } 58 | 59 | return false 60 | } 61 | 62 | const callWebhook = async (instance, eventType, eventData) => { 63 | if (APP_WEBHOOK_ALLOWED_EVENTS.includes('ALL') || APP_WEBHOOK_ALLOWED_EVENTS.includes(eventType)) { 64 | await webhook(instance, eventType, eventData) 65 | } 66 | } 67 | 68 | const webhook = async (instance, type, data) => { 69 | if (process.env.APP_WEBHOOK_URL) { 70 | axios 71 | .post(`${process.env.APP_WEBHOOK_URL}`, { 72 | instance, 73 | type, 74 | data, 75 | }) 76 | .then((success) => { 77 | return success 78 | }) 79 | .catch((error) => { 80 | return error 81 | }) 82 | } 83 | } 84 | 85 | const createSession = async (sessionId, res = null, options = { usePairingCode: false, phoneNumber: '' }) => { 86 | const sessionFile = 'md_' + sessionId 87 | 88 | const logger = pino({ level: 'silent' }) 89 | const store = makeInMemoryStore({ logger }) 90 | 91 | const { state, saveCreds } = await useMultiFileAuthState(sessionsDir(sessionFile)) 92 | 93 | // Fetch latest version of WA Web 94 | const { version, isLatest } = await fetchLatestBaileysVersion() 95 | console.log(`using WA v${version.join('.')}, isLatest: ${isLatest}`) 96 | 97 | // Load store 98 | store?.readFromFile(sessionsDir(`${sessionId}_store.json`)) 99 | 100 | // Save every 10s 101 | setInterval(() => { 102 | if (existsSync(sessionsDir(sessionFile))) { 103 | store?.writeToFile(sessionsDir(`${sessionId}_store.json`)) 104 | } 105 | }, 10000) 106 | 107 | // Make both Node and Bun compatible 108 | const makeWASocket = makeWASocketModule.default ?? makeWASocketModule; 109 | 110 | /** 111 | * @type {import('baileys').AnyWASocket} 112 | */ 113 | const wa = makeWASocket({ 114 | version, 115 | printQRInTerminal: false, 116 | mobile: false, 117 | auth: { 118 | creds: state.creds, 119 | keys: makeCacheableSignalKeyStore(state.keys, logger), 120 | }, 121 | logger, 122 | msgRetryCounterCache, 123 | generateHighQualityLinkPreview: true, 124 | getMessage, 125 | }) 126 | store?.bind(wa.ev) 127 | 128 | sessions.set(sessionId, { ...wa, store }) 129 | 130 | if (options.usePairingCode && !wa.authState.creds.registered) { 131 | if (!wa.authState.creds.account) { 132 | await wa.waitForConnectionUpdate((update) => { 133 | return Boolean(update.qr) 134 | }) 135 | const code = await wa.requestPairingCode(options.phoneNumber) 136 | if (res && !res.headersSent && code !== undefined) { 137 | response(res, 200, true, 'Verify on your phone and enter the provided code.', { code }) 138 | } else { 139 | response(res, 500, false, 'Unable to create session.') 140 | } 141 | } 142 | } 143 | 144 | wa.ev.on('creds.update', saveCreds) 145 | 146 | wa.ev.on('chats.set', ({ chats }) => { 147 | callWebhook(sessionId, 'CHATS_SET', chats) 148 | }) 149 | 150 | wa.ev.on('chats.upsert', (c) => { 151 | callWebhook(sessionId, 'CHATS_UPSERT', c) 152 | }) 153 | 154 | wa.ev.on('chats.delete', (c) => { 155 | callWebhook(sessionId, 'CHATS_DELETE', c) 156 | }) 157 | 158 | wa.ev.on('chats.update', (c) => { 159 | callWebhook(sessionId, 'CHATS_UPDATE', c) 160 | }) 161 | 162 | wa.ev.on('labels.association', (l) => { 163 | callWebhook(sessionId, 'LABELS_ASSOCIATION', l) 164 | }) 165 | 166 | wa.ev.on('labels.edit', (l) => { 167 | callWebhook(sessionId, 'LABELS_EDIT', l) 168 | }) 169 | 170 | // Automatically read incoming messages, uncomment below codes to enable this behaviour 171 | wa.ev.on('messages.upsert', async (m) => { 172 | const messages = m.messages.filter((m) => { 173 | return m.key.fromMe === false 174 | }) 175 | if (messages.length > 0) { 176 | const messageTmp = await Promise.all( 177 | messages.map(async (msg) => { 178 | try { 179 | const typeMessage = Object.keys(msg.message)[0] 180 | if (msg?.status) { 181 | msg.status = WAMessageStatus[msg?.status] ?? 'UNKNOWN' 182 | } 183 | 184 | if ( 185 | ['documentMessage', 'imageMessage', 'videoMessage', 'audioMessage'].includes(typeMessage) && 186 | process.env.APP_WEBHOOK_FILE_IN_BASE64 === 'true' 187 | ) { 188 | const mediaMessage = await getMessageMedia(wa, msg) 189 | 190 | const fieldsToConvert = [ 191 | 'fileEncSha256', 192 | 'mediaKey', 193 | 'fileSha256', 194 | 'jpegThumbnail', 195 | 'thumbnailSha256', 196 | 'thumbnailEncSha256', 197 | 'streamingSidecar', 198 | ] 199 | 200 | fieldsToConvert.forEach((field) => { 201 | if (msg.message[typeMessage]?.[field] !== undefined) { 202 | msg.message[typeMessage][field] = convertToBase64(msg.message[typeMessage][field]) 203 | } 204 | }) 205 | 206 | return { 207 | ...msg, 208 | message: { 209 | [typeMessage]: { 210 | ...msg.message[typeMessage], 211 | fileBase64: mediaMessage.base64, 212 | }, 213 | }, 214 | } 215 | } 216 | 217 | return msg 218 | } catch { 219 | return {} 220 | } 221 | }), 222 | ) 223 | 224 | callWebhook(sessionId, 'MESSAGES_UPSERT', messageTmp) 225 | } 226 | }) 227 | 228 | wa.ev.on('messages.delete', async (m) => { 229 | callWebhook(sessionId, 'MESSAGES_DELETE', m) 230 | }) 231 | 232 | wa.ev.on('messages.update', async (m) => { 233 | for (const { key, update } of m) { 234 | const msg = await getMessage(key) 235 | 236 | if (!msg) { 237 | continue 238 | } 239 | 240 | update.status = WAMessageStatus[update.status] 241 | const messagesUpdate = [ 242 | { 243 | key, 244 | update, 245 | message: msg, 246 | }, 247 | ] 248 | callWebhook(sessionId, 'MESSAGES_UPDATE', messagesUpdate) 249 | } 250 | }) 251 | 252 | wa.ev.on('message-receipt.update', async (m) => { 253 | for (const { key, messageTimestamp, pushName, broadcast, update } of m) { 254 | if (update?.pollUpdates) { 255 | const pollCreation = await getMessage(key) 256 | if (pollCreation) { 257 | const pollMessage = await getAggregateVotesInPollMessage({ 258 | message: pollCreation, 259 | pollUpdates: update.pollUpdates, 260 | }) 261 | update.pollUpdates[0].vote = pollMessage 262 | callWebhook(sessionId, 'MESSAGES_RECEIPT_UPDATE', [ 263 | { key, messageTimestamp, pushName, broadcast, update }, 264 | ]) 265 | return 266 | } 267 | } 268 | } 269 | 270 | callWebhook(sessionId, 'MESSAGES_RECEIPT_UPDATE', m) 271 | }) 272 | 273 | wa.ev.on('messages.reaction', async (m) => { 274 | callWebhook(sessionId, 'MESSAGES_REACTION', m) 275 | }) 276 | 277 | wa.ev.on('messages.media-update', async (m) => { 278 | callWebhook(sessionId, 'MESSAGES_MEDIA_UPDATE', m) 279 | }) 280 | 281 | wa.ev.on('messaging-history.set', async (m) => { 282 | callWebhook(sessionId, 'MESSAGING_HISTORY_SET', m) 283 | }) 284 | 285 | wa.ev.on('connection.update', async (update) => { 286 | const { connection, lastDisconnect, qr } = update 287 | const statusCode = lastDisconnect?.error?.output?.statusCode 288 | 289 | callWebhook(sessionId, 'CONNECTION_UPDATE', update) 290 | 291 | if (connection === 'open') { 292 | retries.delete(sessionId) 293 | } 294 | 295 | if (connection === 'close') { 296 | if (statusCode === DisconnectReason.loggedOut || !shouldReconnect(sessionId)) { 297 | if (res && !res.headersSent) { 298 | response(res, 500, false, 'Unable to create session.') 299 | } 300 | 301 | return deleteSession(sessionId) 302 | } 303 | 304 | setTimeout( 305 | () => { 306 | createSession(sessionId, res) 307 | }, 308 | statusCode === DisconnectReason.restartRequired ? 0 : parseInt(process.env.RECONNECT_INTERVAL ?? 0), 309 | ) 310 | } 311 | 312 | if (qr) { 313 | if (res && !res.headersSent) { 314 | callWebhook(sessionId, 'QRCODE_UPDATED', update) 315 | 316 | try { 317 | const qrcode = await toDataURL(qr) 318 | response(res, 200, true, 'QR code received, please scan the QR code.', { qrcode }) 319 | return 320 | } catch { 321 | response(res, 500, false, 'Unable to create QR code.') 322 | } 323 | } 324 | 325 | try { 326 | await wa.logout() 327 | } catch { 328 | } finally { 329 | deleteSession(sessionId) 330 | } 331 | } 332 | }) 333 | 334 | wa.ev.on('groups.upsert', async (m) => { 335 | callWebhook(sessionId, 'GROUPS_UPSERT', m) 336 | }) 337 | 338 | wa.ev.on('groups.update', async (m) => { 339 | callWebhook(sessionId, 'GROUPS_UPDATE', m) 340 | }) 341 | 342 | wa.ev.on('group-participants.update', async (m) => { 343 | callWebhook(sessionId, 'GROUP_PARTICIPANTS_UPDATE', m) 344 | }) 345 | 346 | wa.ev.on('blocklist.set', async (m) => { 347 | callWebhook(sessionId, 'BLOCKLIST_SET', m) 348 | }) 349 | 350 | wa.ev.on('blocklist.update', async (m) => { 351 | callWebhook(sessionId, 'BLOCKLIST_UPDATE', m) 352 | }) 353 | 354 | wa.ev.on('contacts.set', async (c) => { 355 | callWebhook(sessionId, 'CONTACTS_SET', c) 356 | }) 357 | 358 | wa.ev.on('contacts.upsert', async (c) => { 359 | callWebhook(sessionId, 'CONTACTS_UPSERT', c) 360 | }) 361 | 362 | wa.ev.on('contacts.update', async (c) => { 363 | callWebhook(sessionId, 'CONTACTS_UPDATE', c) 364 | }) 365 | 366 | wa.ev.on('presence.update', async (p) => { 367 | callWebhook(sessionId, 'PRESENCE_UPDATE', p) 368 | }) 369 | 370 | async function getMessage(key) { 371 | if (store) { 372 | const msg = await store.loadMessage(key.remoteJid, key.id) 373 | return msg?.message || undefined 374 | } 375 | 376 | // Only if store is present 377 | return proto.Message.fromObject({}) 378 | } 379 | } 380 | 381 | /** 382 | * @returns {(import('baileys').AnyWASocket|null)} 383 | */ 384 | const getSession = (sessionId) => { 385 | return sessions.get(sessionId) ?? null 386 | } 387 | 388 | const getListSessions = () => { 389 | return [...sessions.keys()] 390 | } 391 | 392 | const deleteSession = (sessionId) => { 393 | const sessionFile = 'md_' + sessionId 394 | const storeFile = `${sessionId}_store.json` 395 | const rmOptions = { force: true, recursive: true } 396 | 397 | rmSync(sessionsDir(sessionFile), rmOptions) 398 | rmSync(sessionsDir(storeFile), rmOptions) 399 | 400 | sessions.delete(sessionId) 401 | retries.delete(sessionId) 402 | } 403 | 404 | const getChatList = (sessionId, isGroup = false) => { 405 | const filter = isGroup ? '@g.us' : '@s.whatsapp.net' 406 | const chats = getSession(sessionId).store.chats 407 | return [...chats.values()].filter(chat => chat.id.endsWith(filter)) 408 | } 409 | 410 | /** 411 | * @param {import('baileys').AnyWASocket} session 412 | */ 413 | const isExists = async (session, jid, isGroup = false) => { 414 | try { 415 | let result 416 | 417 | if (isGroup) { 418 | result = await session.groupMetadata(jid) 419 | 420 | return Boolean(result.id) 421 | } 422 | 423 | ;[result] = await session.onWhatsApp(jid) 424 | 425 | return result.exists 426 | } catch { 427 | return false 428 | } 429 | } 430 | 431 | /** 432 | * @param {import('baileys').AnyWASocket} session 433 | */ 434 | const sendMessage = async (session, receiver, message, options = {}, delayMs = 1000) => { 435 | try { 436 | await delay(parseInt(delayMs)) 437 | return await session.sendMessage(receiver, message, options) 438 | } catch { 439 | return Promise.reject(null) // eslint-disable-line prefer-promise-reject-errors 440 | } 441 | } 442 | 443 | /** 444 | * @param {import('baileys').AnyWASocket} session 445 | */ 446 | const updateProfileStatus = async (session, status) => { 447 | try { 448 | return await session.updateProfileStatus(status) 449 | } catch { 450 | return Promise.reject(null) // eslint-disable-line prefer-promise-reject-errors 451 | } 452 | } 453 | 454 | const updateProfileName = async (session, name) => { 455 | try { 456 | return await session.updateProfileName(name) 457 | } catch { 458 | return Promise.reject(null) // eslint-disable-line prefer-promise-reject-errors 459 | } 460 | } 461 | 462 | const getProfilePicture = async (session, jid, type = 'image') => { 463 | try { 464 | return await session.profilePictureUrl(jid, type) 465 | } catch { 466 | return Promise.reject(null) // eslint-disable-line prefer-promise-reject-errors 467 | } 468 | } 469 | 470 | const blockAndUnblockUser = async (session, jid, block) => { 471 | try { 472 | return await session.updateBlockStatus(jid, block) 473 | } catch { 474 | return Promise.reject(null) // eslint-disable-line prefer-promise-reject-errors 475 | } 476 | } 477 | 478 | const formatPhone = (phone) => { 479 | if (phone.endsWith('@s.whatsapp.net')) { 480 | return phone 481 | } 482 | 483 | let formatted = phone.replace(/\D/g, '') 484 | 485 | return (formatted += '@s.whatsapp.net') 486 | } 487 | 488 | const formatGroup = (group) => { 489 | if (group.endsWith('@g.us')) { 490 | return group 491 | } 492 | 493 | let formatted = group.replace(/[^\d-]/g, '') 494 | 495 | return (formatted += '@g.us') 496 | } 497 | 498 | const cleanup = () => { 499 | console.log('Running cleanup before exit.') 500 | 501 | sessions.forEach((session, sessionId) => { 502 | session.store.writeToFile(sessionsDir(`${sessionId}_store.json`)) 503 | }) 504 | } 505 | 506 | const getGroupsWithParticipants = async (session) => { 507 | return session.groupFetchAllParticipating() 508 | } 509 | 510 | const participantsUpdate = async (session, jid, participants, action) => { 511 | return session.groupParticipantsUpdate(jid, participants, action) 512 | } 513 | 514 | const updateSubject = async (session, jid, subject) => { 515 | return session.groupUpdateSubject(jid, subject) 516 | } 517 | 518 | const updateDescription = async (session, jid, description) => { 519 | return session.groupUpdateDescription(jid, description) 520 | } 521 | 522 | const settingUpdate = async (session, jid, settings) => { 523 | return session.groupSettingUpdate(jid, settings) 524 | } 525 | 526 | const leave = async (session, jid) => { 527 | return session.groupLeave(jid) 528 | } 529 | 530 | const inviteCode = async (session, jid) => { 531 | return session.groupInviteCode(jid) 532 | } 533 | 534 | const revokeInvite = async (session, jid) => { 535 | return session.groupRevokeInvite(jid) 536 | } 537 | 538 | const metaData = async (session, req) => { 539 | return session.groupMetadata(req.groupId) 540 | } 541 | 542 | const acceptInvite = async (session, req) => { 543 | return session.groupAcceptInvite(req.invite) 544 | } 545 | 546 | const profilePicture = async (session, jid, urlImage) => { 547 | const image = await downloadImage(urlImage) 548 | return session.updateProfilePicture(jid, { url: image }) 549 | } 550 | 551 | const readMessage = async (session, keys) => { 552 | return session.readMessages(keys) 553 | } 554 | 555 | const getStoreMessage = async (session, messageId, remoteJid) => { 556 | try { 557 | return await session.store.loadMessage(remoteJid, messageId) 558 | } catch { 559 | // eslint-disable-next-line prefer-promise-reject-errors 560 | return Promise.reject(null) 561 | } 562 | } 563 | 564 | const getMessageMedia = async (session, message) => { 565 | try { 566 | const messageType = Object.keys(message.message)[0] 567 | const mediaMessage = message.message[messageType] 568 | const buffer = await downloadMediaMessage( 569 | message, 570 | 'buffer', 571 | {}, 572 | { reuploadRequest: session.updateMediaMessage }, 573 | ) 574 | 575 | return { 576 | messageType, 577 | fileName: mediaMessage.fileName ?? '', 578 | caption: mediaMessage.caption ?? '', 579 | size: { 580 | fileLength: mediaMessage.fileLength, 581 | height: mediaMessage.height ?? 0, 582 | width: mediaMessage.width ?? 0, 583 | }, 584 | mimetype: mediaMessage.mimetype, 585 | base64: buffer.toString('base64'), 586 | } 587 | } catch { 588 | // eslint-disable-next-line prefer-promise-reject-errors 589 | return Promise.reject(null) 590 | } 591 | } 592 | 593 | const convertToBase64 = (arrayBytes) => { 594 | const byteArray = new Uint8Array(arrayBytes) 595 | return Buffer.from(byteArray).toString('base64') 596 | } 597 | 598 | const init = () => { 599 | readdir(sessionsDir(), (err, files) => { 600 | if (err) { 601 | throw err 602 | } 603 | 604 | for (const file of files) { 605 | if ((!file.startsWith('md_') && !file.startsWith('legacy_')) || file.endsWith('_store')) { 606 | continue 607 | } 608 | 609 | const filename = file.replace('.json', '') 610 | const sessionId = filename.substring(3) 611 | console.log('Recovering session: ' + sessionId) 612 | createSession(sessionId) 613 | } 614 | }) 615 | } 616 | 617 | export { 618 | isSessionExists, 619 | createSession, 620 | getSession, 621 | getListSessions, 622 | deleteSession, 623 | getChatList, 624 | getGroupsWithParticipants, 625 | isExists, 626 | sendMessage, 627 | updateProfileStatus, 628 | updateProfileName, 629 | getProfilePicture, 630 | formatPhone, 631 | formatGroup, 632 | cleanup, 633 | participantsUpdate, 634 | updateSubject, 635 | updateDescription, 636 | settingUpdate, 637 | leave, 638 | inviteCode, 639 | revokeInvite, 640 | metaData, 641 | acceptInvite, 642 | profilePicture, 643 | readMessage, 644 | init, 645 | isSessionConnected, 646 | getMessageMedia, 647 | getStoreMessage, 648 | blockAndUnblockUser, 649 | } 650 | --------------------------------------------------------------------------------