Init
This commit is contained in:
14
.env.example
Normal file
14
.env.example
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Server Configuration
|
||||||
|
PORT=3000
|
||||||
|
API_KEY=your-strong-api-key-here
|
||||||
|
|
||||||
|
# Database & Storage
|
||||||
|
DB_PATH=./data/whatsapp.db
|
||||||
|
MEDIA_PATH=./media
|
||||||
|
|
||||||
|
# WhatsApp Client
|
||||||
|
HEADLESS=false
|
||||||
|
REJECT_CALLS=true
|
||||||
|
|
||||||
|
# CORS (for WebSocket)
|
||||||
|
CORS_ORIGIN=*
|
||||||
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
.idea
|
||||||
|
.git
|
||||||
|
.vscode
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.env
|
||||||
|
.DS_Store
|
||||||
|
.wwebjs_cache
|
||||||
|
.wwebjs_auth
|
||||||
|
data/
|
||||||
|
media/
|
||||||
26
README.md
Normal file
26
README.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# WhatsApp Web.js Server
|
||||||
|
|
||||||
|
Complete WhatsApp automation server with REST API, WebSocket, and SQLite storage.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
✅ **REST API** - Send messages, retrieve contacts/chats/messages
|
||||||
|
✅ **WebSocket** - Real-time event streaming
|
||||||
|
✅ **SQLite** - Persistent storage for all data
|
||||||
|
✅ **Media Storage** - Automatic media file handling
|
||||||
|
✅ **Authentication** - API key protection
|
||||||
|
✅ **All Original Commands** - Full command handler support
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#FIRST WORKING VERSION!! Or manually sync later
|
||||||
|
curl -X POST http://localhost:3000/api/sync \
|
||||||
|
-H "X-API-Key: VERY_STRONG_API_KEY_12345" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"contacts":true,"chats":true}'
|
||||||
|
```
|
||||||
1
markmap.svg
Normal file
1
markmap.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="markmap mm-xo0pfw-6" style="width: 100%; height: 100%;"><style>.markmap{--markmap-max-width: 9999px;--markmap-a-color: #0097e6;--markmap-a-hover-color: #00a8ff;--markmap-code-bg: #f0f0f0;--markmap-code-color: #555;--markmap-highlight-bg: #ffeaa7;--markmap-table-border: 1px solid currentColor;--markmap-font: 300 16px/20px sans-serif;--markmap-circle-open-bg: #fff;--markmap-text-color: #333;--markmap-highlight-node-bg: #ff02;font:var(--markmap-font);color:var(--markmap-text-color)}.markmap-link{fill:none}.markmap-node>circle{cursor:pointer}.markmap-foreign{display:inline-block}.markmap-foreign p{margin:0}.markmap-foreign a{color:var(--markmap-a-color)}.markmap-foreign a:hover{color:var(--markmap-a-hover-color)}.markmap-foreign code{padding:.25em;font-size:calc(1em - 2px);color:var(--markmap-code-color);background-color:var(--markmap-code-bg);border-radius:2px}.markmap-foreign pre{margin:0}.markmap-foreign pre>code{display:block}.markmap-foreign del{text-decoration:line-through}.markmap-foreign em{font-style:italic}.markmap-foreign strong{font-weight:700}.markmap-foreign mark{background:var(--markmap-highlight-bg)}.markmap-foreign table,.markmap-foreign th,.markmap-foreign td{border-collapse:collapse;border:var(--markmap-table-border)}.markmap-foreign img{display:inline-block}.markmap-foreign svg{fill:currentColor}.markmap-foreign>div{width:var(--markmap-max-width);text-align:left}.markmap-foreign>div>div{display:inline-block}.markmap-highlight rect{fill:var(--markmap-highlight-node-bg)}.markmap-dark .markmap{--markmap-code-bg: #1a1b26;--markmap-code-color: #ddd;--markmap-circle-open-bg: #444;--markmap-text-color: #eee}</style><g transform="translate(20.000000000000057,297.5232774674115) scale(1.4152700186219738)"><path class="markmap-link" data-depth="3" data-path="1.3.4" d="M367,24.25C407,24.25,407,27.563,447,27.563" stroke-width="1.375" stroke="rgb(214, 39, 40)"/><path class="markmap-link" data-depth="2" data-path="1.2" d="M194,11.25C234,11.25,234,-2.5,274,-2.5" stroke-width="1.75" stroke="rgb(255, 127, 14)"/><path class="markmap-link" data-depth="2" data-path="1.3" d="M194,11.25C234,11.25,234,24.25,274,24.25" stroke-width="1.75" stroke="rgb(44, 160, 44)"/><g class="markmap-highlight"/><g data-depth="2" data-path="1.2" class="markmap-node" transform="translate(274, -23.375)"><line stroke="#ff7f0e" stroke-width="1.75" x1="-1" x2="81" y1="20.875" y2="20.875"/><foreignObject class="markmap-foreign" x="8" y="0" style="opacity: 1;" width="63" height="20"><div xmlns="http://www.w3.org/1999/xhtml"><div xmlns="http://www.w3.org/1999/xhtml">Features</div></div></foreignObject></g><g data-depth="3" data-path="1.3.4" class="markmap-node" transform="translate(447, -0.125)"><line stroke="#d62728" stroke-width="1.375" x1="-1" x2="92" y1="27.6875" y2="27.6875"/><foreignObject class="markmap-foreign" x="8" y="0" style="opacity: 1;" width="74" height="27"><div xmlns="http://www.w3.org/1999/xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><pre data-lines="15,17"><code class="language-bash">npm install</code></pre></div></div></foreignObject></g><g data-depth="2" data-path="1.3" class="markmap-node" transform="translate(274, 3.375)"><line stroke="#2ca02c" stroke-width="1.75" x1="-1" x2="95" y1="20.875" y2="20.875"/><circle stroke-width="1.5" r="6" stroke="#2ca02c" fill="var(--markmap-circle-open-bg)" cx="93" cy="20.875"/><foreignObject class="markmap-foreign" x="8" y="0" style="opacity: 1;" width="77" height="20"><div xmlns="http://www.w3.org/1999/xhtml"><div xmlns="http://www.w3.org/1999/xhtml">Installation</div></div></foreignObject></g><g data-depth="1" data-path="1" class="markmap-node" transform="translate(0, -10)"><line stroke="#1f77b4" stroke-width="2.5" x1="-1" x2="196" y1="21.25" y2="21.25"/><circle stroke-width="1.5" r="6" stroke="#1f77b4" fill="var(--markmap-circle-open-bg)" cx="194" cy="21.25"/><foreignObject class="markmap-foreign" x="8" y="0" style="opacity: 1;" width="178" height="20"><div xmlns="http://www.w3.org/1999/xhtml"><div xmlns="http://www.w3.org/1999/xhtml">WhatsApp Web.js Server</div></div></foreignObject></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 4.1 KiB |
4484
package-lock.json
generated
Normal file
4484
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
package.json
Normal file
25
package.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "whatsapp-web-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "WhatsApp automation server with API and database",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "nodemon server.js",
|
||||||
|
"postinstall": "patch-package"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"qrcode-terminal": "^0.12.0",
|
||||||
|
"sequelize": "^6.32.1",
|
||||||
|
"socket.io": "^4.7.2",
|
||||||
|
"sqlite3": "^5.1.6",
|
||||||
|
"whatsapp-web.js": "github:pedroslopez/whatsapp-web.js"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.0.1",
|
||||||
|
"patch-package": "^8.0.1",
|
||||||
|
"postinstall-postinstall": "^2.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
65
patches/whatsapp-web.js+1.21.0.patch
Normal file
65
patches/whatsapp-web.js+1.21.0.patch
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
diff --git a/node_modules/whatsapp-web.js/src/util/Injected.js b/node_modules/whatsapp-web.js/src/util/Injected.js
|
||||||
|
index xxxxxxx..yyyyyyy 100644
|
||||||
|
--- a/node_modules/whatsapp-web.js/src/util/Injected.js
|
||||||
|
+++ b/node_modules/whatsapp-web.js/src/util/Injected.js
|
||||||
|
@@ -628,16 +628,28 @@ exports.ExposeStore = () => {
|
||||||
|
return Object.defineProperty(window, 'WWebJS', {
|
||||||
|
get() {
|
||||||
|
return {
|
||||||
|
getContactModel: (contact) => {
|
||||||
|
+ // Safe method extraction with fallbacks
|
||||||
|
+ const getContactMethod = (method) => {
|
||||||
|
+ if (!window.Store.ContactMethods || !window.Store.ContactMethods[method]) {
|
||||||
|
+ return null;
|
||||||
|
+ }
|
||||||
|
+ return window.Store.ContactMethods[method];
|
||||||
|
+ };
|
||||||
|
+
|
||||||
|
return {
|
||||||
|
id: contact.id,
|
||||||
|
name: contact.name,
|
||||||
|
type: contact.type,
|
||||||
|
- isMe: contact.isMe,
|
||||||
|
- isUser: contact.isUser,
|
||||||
|
- isGroup: contact.isGroup,
|
||||||
|
- isBroadcast: contact.isBroadcast,
|
||||||
|
- isBusiness: contact.isBusiness,
|
||||||
|
- isEnterprise: contact.isEnterprise,
|
||||||
|
+ isMe: contact.isMe || false,
|
||||||
|
+ isUser: contact.isUser || false,
|
||||||
|
+ isGroup: contact.isGroup || false,
|
||||||
|
+ isBroadcast: contact.isBroadcast || false,
|
||||||
|
+ isBusiness: contact.isBusiness || false,
|
||||||
|
+ isEnterprise: contact.isEnterprise || false,
|
||||||
|
+ isContactSyncEnabled: contact.isContactSyncEnabled || false,
|
||||||
|
+ isContactLocked: contact.isContactLocked || false,
|
||||||
|
verifiedLevel: contact.verifiedLevel,
|
||||||
|
verifiedName: contact.verifiedName,
|
||||||
|
pushname: contact.pushname,
|
||||||
|
@@ -646,11 +658,23 @@ exports.ExposeStore = () => {
|
||||||
|
isMe: contact.isMe,
|
||||||
|
isUser: contact.isUser,
|
||||||
|
isWAContact: contact.isWAContact,
|
||||||
|
- isMyContact: contact.isMyContact,
|
||||||
|
+ // Fallback for changed API
|
||||||
|
+ isMyContact: contact.isMyContact !== undefined
|
||||||
|
+ ? contact.isMyContact
|
||||||
|
+ : contact.type === 'in' || false,
|
||||||
|
isBlocked: contact.isBlocked,
|
||||||
|
- labels: contact.labels ? contact.labels.map(l => l.id) : []
|
||||||
|
+ labels: contact.labels ? contact.labels.map(l => l.id) : [],
|
||||||
|
+ // Additional properties
|
||||||
|
+ displayName: contact.displayName || contact.pushname || contact.name,
|
||||||
|
+ formattedName: contact.formattedName || contact.name,
|
||||||
|
+ formattedShortName: contact.formattedShortName || contact.name,
|
||||||
|
+ shortName: contact.shortName || contact.name,
|
||||||
|
+ isSaved: contact.isSaved !== undefined ? contact.isSaved : false,
|
||||||
|
+ isMuted: contact.muteExpiration !== undefined ? contact.muteExpiration > 0 : false,
|
||||||
|
+ muteExpiration: contact.muteExpiration || 0,
|
||||||
|
+ disappearingModeDuration: contact.disappearingModeDuration || 0
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
485
server.js
Normal file
485
server.js
Normal file
@@ -0,0 +1,485 @@
|
|||||||
|
require('dotenv').config()
|
||||||
|
const express = require('express')
|
||||||
|
const http = require('http')
|
||||||
|
const socketIo = require('socket.io')
|
||||||
|
const { Sequelize, DataTypes, Op } = require('sequelize')
|
||||||
|
const { Client, LocalAuth, Location, Poll, List, Buttons } = require('whatsapp-web.js')
|
||||||
|
const path = require('path')
|
||||||
|
const fs = require('fs').promises
|
||||||
|
const crypto = require('crypto')
|
||||||
|
const qrcode = require('qrcode-terminal')
|
||||||
|
|
||||||
|
// --- CONFIGURATION ---
|
||||||
|
const config = {
|
||||||
|
port : process.env.PORT || 3000,
|
||||||
|
dbPath : process.env.DB_PATH || path.join(__dirname, 'data', 'whatsapp.db'),
|
||||||
|
mediaPath : process.env.MEDIA_PATH || path.join(__dirname, 'media'),
|
||||||
|
headless : process.env.HEADLESS !== 'false',
|
||||||
|
rejectCalls: process.env.REJECT_CALLS !== 'false',
|
||||||
|
apiKey : process.env.API_KEY || 'test-key'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure directories exist
|
||||||
|
async function ensureDirectories() {
|
||||||
|
await fs.mkdir(path.dirname(config.dbPath), { recursive: true })
|
||||||
|
await fs.mkdir(config.mediaPath, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple logger
|
||||||
|
const logger = {
|
||||||
|
info : (msg) => console.log(`[${ new Date().toISOString() }] INFO: ${ msg }`),
|
||||||
|
error: (msg, err) => console.error(`[${ new Date().toISOString() }] ERROR: ${ msg }`, err || ''),
|
||||||
|
warn : (msg) => console.warn(`[${ new Date().toISOString() }] WARN: ${ msg }`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- DATABASE SETUP ---
|
||||||
|
const sequelize = new Sequelize({
|
||||||
|
dialect: 'sqlite',
|
||||||
|
storage: config.dbPath,
|
||||||
|
logging: process.env.NODE_ENV === 'development' ? logger.info : false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Models
|
||||||
|
const Contact = sequelize.define('Contact', {
|
||||||
|
id : { type: DataTypes.STRING, primaryKey: true },
|
||||||
|
phoneNumber : { type: DataTypes.STRING, allowNull: true },
|
||||||
|
name : DataTypes.STRING,
|
||||||
|
pushname : DataTypes.STRING,
|
||||||
|
isBlocked : { type: DataTypes.BOOLEAN, defaultValue: false },
|
||||||
|
isMyContact : { type: DataTypes.BOOLEAN, defaultValue: false },
|
||||||
|
labels : { type: DataTypes.JSON, defaultValue: [] },
|
||||||
|
profilePicUrl: DataTypes.STRING,
|
||||||
|
isGroup: { type: DataTypes.BOOLEAN, defaultValue: false },
|
||||||
|
isNewsletter: { type: DataTypes.BOOLEAN, defaultValue: false },
|
||||||
|
isUser: { type: DataTypes.BOOLEAN, defaultValue: true }
|
||||||
|
}, { indexes: [{ fields: ['phoneNumber'] }] })
|
||||||
|
|
||||||
|
const Chat = sequelize.define('Chat', {
|
||||||
|
id : { type: DataTypes.STRING, primaryKey: true },
|
||||||
|
name : DataTypes.STRING,
|
||||||
|
isGroup : { type: DataTypes.BOOLEAN, defaultValue: false },
|
||||||
|
isMuted : { type: DataTypes.BOOLEAN, defaultValue: false },
|
||||||
|
unreadCount: { type: DataTypes.INTEGER, defaultValue: 0 },
|
||||||
|
timestamp : DataTypes.INTEGER,
|
||||||
|
pinned : { type: DataTypes.BOOLEAN, defaultValue: false },
|
||||||
|
description: DataTypes.TEXT
|
||||||
|
}, { indexes: [{ fields: ['isGroup'] }] })
|
||||||
|
|
||||||
|
const Message = sequelize.define('Message', {
|
||||||
|
id : { type: DataTypes.STRING, primaryKey: true },
|
||||||
|
from : { type: DataTypes.STRING, allowNull: false },
|
||||||
|
to : { type: DataTypes.STRING, allowNull: false },
|
||||||
|
body : DataTypes.TEXT,
|
||||||
|
type : DataTypes.STRING,
|
||||||
|
timestamp: { type: DataTypes.INTEGER, index: true },
|
||||||
|
hasMedia : { type: DataTypes.BOOLEAN, defaultValue: false },
|
||||||
|
mediaId : DataTypes.STRING,
|
||||||
|
fromMe : { type: DataTypes.BOOLEAN, defaultValue: false },
|
||||||
|
ack : { type: DataTypes.INTEGER, defaultValue: 0 }
|
||||||
|
}, { indexes: [{ fields: ['from', 'timestamp'] }] })
|
||||||
|
|
||||||
|
const Media = sequelize.define('Media', {
|
||||||
|
id : { type: DataTypes.STRING, primaryKey: true },
|
||||||
|
messageId: { type: DataTypes.STRING, references: { model: Message, key: 'id' } },
|
||||||
|
mimetype : DataTypes.STRING,
|
||||||
|
filename : DataTypes.STRING,
|
||||||
|
filePath : DataTypes.STRING
|
||||||
|
})
|
||||||
|
|
||||||
|
const Call = sequelize.define('Call', {
|
||||||
|
id : { type: DataTypes.STRING, primaryKey: true },
|
||||||
|
from : DataTypes.STRING,
|
||||||
|
isVideo : { type: DataTypes.BOOLEAN, defaultValue: false },
|
||||||
|
timestamp: DataTypes.INTEGER,
|
||||||
|
rejected : { type: DataTypes.BOOLEAN, defaultValue: false }
|
||||||
|
})
|
||||||
|
|
||||||
|
Message.hasOne(Media, { foreignKey: 'messageId', as: 'media' })
|
||||||
|
|
||||||
|
// --- EXPRESS & SOCKET.IO ---
|
||||||
|
const app = express()
|
||||||
|
const server = http.createServer(app)
|
||||||
|
const io = socketIo(server, {
|
||||||
|
cors: { origin: process.env.CORS_ORIGIN || '*', methods: ['GET', 'POST'] }
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use(express.json({ limit: '50mb' }))
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
function validateApiKey(req, res, next) {
|
||||||
|
const apiKey = req.headers['x-api-key'] || req.query.apiKey
|
||||||
|
if (!apiKey || apiKey !== config.apiKey) {
|
||||||
|
return res.status(401).json({ success: false, error: 'Unauthorized' })
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkWhatsAppReady(req, res, next) {
|
||||||
|
if (!client.info) return res.status(503).json({ success: false, error: 'Client not ready' })
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- REST API ---
|
||||||
|
app.post('/api/send', [validateApiKey, checkWhatsAppReady], async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { phoneNumber, message } = req.body
|
||||||
|
if (!phoneNumber || !message) {
|
||||||
|
return res.status(400).json({ success: false, error: 'Missing phoneNumber or message' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatId = phoneNumber.includes('@') ? phoneNumber : `${ phoneNumber }@c.us`
|
||||||
|
const sent = await client.sendMessage(chatId, message)
|
||||||
|
|
||||||
|
await Message.create({
|
||||||
|
id : sent.id._serialized,
|
||||||
|
from : client.info.wid._serialized,
|
||||||
|
to : chatId,
|
||||||
|
body : typeof message === 'string' ? message : JSON.stringify(message),
|
||||||
|
type : sent.type,
|
||||||
|
timestamp: sent.timestamp,
|
||||||
|
fromMe : true,
|
||||||
|
ack : sent.ack
|
||||||
|
})
|
||||||
|
|
||||||
|
io.emit('message_sent', { id: sent.id._serialized, phoneNumber, message })
|
||||||
|
res.json({ success: true, messageId: sent.id._serialized })
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error sending message', error)
|
||||||
|
res.status(500).json({ success: false, error: error.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/api/contacts', [validateApiKey], async (req, res) => {
|
||||||
|
try {
|
||||||
|
const contacts = await Contact.findAll({ order: [['name', 'ASC']], limit: 1000 })
|
||||||
|
res.json({ success: true, data: contacts })
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error fetching contacts', error)
|
||||||
|
res.status(500).json({ success: false, error: error.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/api/chats', [validateApiKey], async (req, res) => {
|
||||||
|
try {
|
||||||
|
const chats = await Chat.findAll({ order: [['timestamp', 'DESC']], limit: 1000 })
|
||||||
|
res.json({ success: true, data: chats })
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error fetching chats', error)
|
||||||
|
res.status(500).json({ success: false, error: error.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/api/messages/:chatId', [validateApiKey], async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { chatId } = req.params
|
||||||
|
const messages = await Message.findAll({
|
||||||
|
where : { [Op.or]: [{ from: chatId }, { to: chatId }] },
|
||||||
|
order : [['timestamp', 'DESC']],
|
||||||
|
limit : 100,
|
||||||
|
include: [{ model: Media, as: 'media' }]
|
||||||
|
})
|
||||||
|
res.json({ success: true, data: messages })
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error fetching messages', error)
|
||||||
|
res.status(500).json({ success: false, error: error.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/api/status', [validateApiKey], async (req, res) => {
|
||||||
|
try {
|
||||||
|
const status = {
|
||||||
|
connected : !!client.info,
|
||||||
|
user : client.info,
|
||||||
|
uptime : process.uptime(),
|
||||||
|
wwebVersion: client.info ? await client.getWWebVersion() : null
|
||||||
|
}
|
||||||
|
res.json({ success: true, data: status })
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ success: false, error: error.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/health', (req, res) => res.json({ status: 'OK', timestamp: Date.now() }))
|
||||||
|
|
||||||
|
// --- WEBSOCKET ---
|
||||||
|
io.on('connection', (socket) => {
|
||||||
|
logger.info(`WebSocket client connected: ${ socket.id }`)
|
||||||
|
socket.emit('status', { connected: !!client.info })
|
||||||
|
|
||||||
|
socket.on('disconnect', (reason) => {
|
||||||
|
logger.info(`WebSocket client disconnected: ${ socket.id }, reason: ${ reason }`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- WHATSAPP CLIENT ---
|
||||||
|
const client = new Client({
|
||||||
|
authStrategy: new LocalAuth({ clientId: 'whatsapp-server' }),
|
||||||
|
puppeteer : {
|
||||||
|
headless: config.headless,
|
||||||
|
args : ['--no-sandbox', '--disable-setuid-sandbox']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
client.on('qr', (qr) => {
|
||||||
|
logger.info('QR received - scan to authenticate')
|
||||||
|
qrcode.generate(qr, { small: true })
|
||||||
|
io.emit('qr', { qr, timestamp: Date.now() })
|
||||||
|
})
|
||||||
|
|
||||||
|
client.on('ready', async () => {
|
||||||
|
logger.info('WhatsApp client ready');
|
||||||
|
const wwebVersion = await client.getWWebVersion();
|
||||||
|
|
||||||
|
// Sync contacts with proper type detection
|
||||||
|
const contacts = await client.getContacts();
|
||||||
|
let contactsSynced = 0;
|
||||||
|
|
||||||
|
for (const contact of contacts) {
|
||||||
|
try {
|
||||||
|
const id = contact.id?._serialized || contact.id || '';
|
||||||
|
const name = contact.name || contact.pushname || 'Unknown';
|
||||||
|
|
||||||
|
// Determine contact type and extract identifier
|
||||||
|
let phoneNumber = null;
|
||||||
|
let isGroup = false;
|
||||||
|
let isNewsletter = false;
|
||||||
|
let isUser = false;
|
||||||
|
|
||||||
|
if (contact.id && typeof contact.id === 'object') {
|
||||||
|
if (contact.id.server === 'c.us') {
|
||||||
|
// Regular user contact
|
||||||
|
phoneNumber = contact.id.user || contact.number;
|
||||||
|
isUser = true;
|
||||||
|
} else if (contact.id.server === 'g.us') {
|
||||||
|
// Group chat
|
||||||
|
isGroup = true;
|
||||||
|
phoneNumber = contact.id._serialized; // Use group ID
|
||||||
|
} else if (contact.id.server === 'newsletter') {
|
||||||
|
// Newsletter
|
||||||
|
isNewsletter = true;
|
||||||
|
phoneNumber = contact.id._serialized; // Use newsletter ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback
|
||||||
|
if (!phoneNumber) {
|
||||||
|
phoneNumber = contact.number || contact.id?._serialized || null;
|
||||||
|
isUser = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Contact.upsert({
|
||||||
|
id: id,
|
||||||
|
phoneNumber: phoneNumber,
|
||||||
|
name: name,
|
||||||
|
pushname: contact.pushname || null,
|
||||||
|
isBlocked: contact.isBlocked || false,
|
||||||
|
isMyContact: contact.isMyContact || false,
|
||||||
|
labels: contact.labels || [],
|
||||||
|
profilePicUrl: contact.profilePicUrl || null,
|
||||||
|
isGroup: isGroup,
|
||||||
|
isNewsletter: isNewsletter,
|
||||||
|
isUser: isUser
|
||||||
|
});
|
||||||
|
contactsSynced++;
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(`Failed to sync contact ${contact.id?._serialized || 'unknown'}: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync chats
|
||||||
|
const chats = await client.getChats();
|
||||||
|
let chatsSynced = 0;
|
||||||
|
|
||||||
|
for (const chat of chats) {
|
||||||
|
try {
|
||||||
|
await Chat.upsert({
|
||||||
|
id: chat.id._serialized,
|
||||||
|
name: chat.name,
|
||||||
|
isGroup: chat.isGroup,
|
||||||
|
isMuted: chat.isMuted || false,
|
||||||
|
unreadCount: chat.unreadCount || 0,
|
||||||
|
timestamp: chat.timestamp || 0,
|
||||||
|
pinned: chat.pinned || false,
|
||||||
|
description: chat.description || null
|
||||||
|
});
|
||||||
|
chatsSynced++;
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(`Failed to sync chat ${chat.id._serialized}: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Synced ${contactsSynced}/${contacts.length} contacts and ${chatsSynced}/${chats.length} chats`);
|
||||||
|
io.emit('ready', { version: wwebVersion, user: client.info });
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('message', async (msg) => {
|
||||||
|
try {
|
||||||
|
const messageData = {
|
||||||
|
id : msg.id._serialized,
|
||||||
|
from : msg.from,
|
||||||
|
to : msg.to || msg.from,
|
||||||
|
body : msg.body || '',
|
||||||
|
type : msg.type,
|
||||||
|
timestamp: msg.timestamp,
|
||||||
|
hasMedia : msg.hasMedia,
|
||||||
|
fromMe : msg.fromMe,
|
||||||
|
ack : msg.ack || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.location) {
|
||||||
|
Object.assign(messageData, {
|
||||||
|
locationLatitude : msg.location.latitude,
|
||||||
|
locationLongitude: msg.location.longitude,
|
||||||
|
locationName : msg.location.name,
|
||||||
|
locationAddress : msg.location.address
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await Message.create(messageData)
|
||||||
|
|
||||||
|
if (msg.hasMedia) {
|
||||||
|
const media = await msg.downloadMedia()
|
||||||
|
const mediaId = crypto.randomUUID()
|
||||||
|
const fileExt = media.filename ? path.extname(media.filename) : media.mimetype.split('/')[1] || 'bin'
|
||||||
|
const filename = `${ mediaId }${ fileExt }`
|
||||||
|
const filePath = path.join(config.mediaPath, filename)
|
||||||
|
|
||||||
|
await fs.writeFile(filePath, media.data, 'base64')
|
||||||
|
await Media.create({
|
||||||
|
id : mediaId,
|
||||||
|
messageId: msg.id._serialized,
|
||||||
|
mimetype : media.mimetype,
|
||||||
|
filename : media.filename || filename,
|
||||||
|
filePath
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update contact info for incoming messages (safely)
|
||||||
|
if (!msg.fromMe && config.syncOnReady) {
|
||||||
|
try {
|
||||||
|
// Only attempt if SYNC_ON_READY is true (you trust the API)
|
||||||
|
const contact = await msg.getContact()
|
||||||
|
await Contact.upsert({
|
||||||
|
id : contact.id._serialized,
|
||||||
|
phoneNumber : contact.number || contact.id.user,
|
||||||
|
name : contact.name || contact.pushname,
|
||||||
|
pushname : contact.pushname,
|
||||||
|
isBlocked : contact.isBlocked || false,
|
||||||
|
isMyContact : contact.isMyContact || false,
|
||||||
|
labels : contact.labels || [],
|
||||||
|
profilePicUrl: contact.profilePicUrl || null
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
// Silently fail - this is a known issue with whatsapp-web.js
|
||||||
|
logger.warn(`Failed to update contact: ${ err.message }`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
io.emit('message', {
|
||||||
|
id : msg.id._serialized,
|
||||||
|
from : msg.from,
|
||||||
|
body : msg.body,
|
||||||
|
type : msg.type,
|
||||||
|
timestamp: msg.timestamp,
|
||||||
|
hasMedia : msg.hasMedia,
|
||||||
|
fromMe : msg.fromMe
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- ALL ORIGINAL COMMAND HANDLERS ---
|
||||||
|
if (msg.body === '!ping reply') {
|
||||||
|
await msg.reply('pong')
|
||||||
|
} else if (msg.body === '!ping') {
|
||||||
|
await client.sendMessage(msg.from, 'pong')
|
||||||
|
} else if (msg.body.startsWith('!sendto ')) {
|
||||||
|
const parts = msg.body.split(' ')
|
||||||
|
const number = parts[1].includes('@c.us') ? parts[1] : `${ parts[1] }@c.us`
|
||||||
|
await client.sendMessage(number, parts.slice(2).join(' '))
|
||||||
|
} else if (msg.body.startsWith('!subject ')) {
|
||||||
|
const chat = await msg.getChat()
|
||||||
|
if (chat.isGroup) await chat.setSubject(msg.body.slice(9))
|
||||||
|
} else if (msg.body.startsWith('!desc ')) {
|
||||||
|
const chat = await msg.getChat()
|
||||||
|
if (chat.isGroup) await chat.setDescription(msg.body.slice(6))
|
||||||
|
} else if (msg.body === '!leave') {
|
||||||
|
const chat = await msg.getChat()
|
||||||
|
if (chat.isGroup) await chat.leave()
|
||||||
|
} else if (msg.body.startsWith('!join ')) {
|
||||||
|
try {
|
||||||
|
await client.acceptInvite(msg.body.split(' ')[1])
|
||||||
|
await msg.reply('Joined the group!')
|
||||||
|
} catch (e) {
|
||||||
|
await msg.reply('Invalid invite code.')
|
||||||
|
}
|
||||||
|
} else if (msg.body === '!groupinfo') {
|
||||||
|
const chat = await msg.getChat()
|
||||||
|
if (chat.isGroup) {
|
||||||
|
await msg.reply(`*Group Details*\nName: ${ chat.name }\nDescription: ${ chat.description }\nParticipants: ${ chat.participants.length }`)
|
||||||
|
}
|
||||||
|
} else if (msg.body === '!chats') {
|
||||||
|
const chats = await client.getChats()
|
||||||
|
await client.sendMessage(msg.from, `The bot has ${ chats.length } chats open.`)
|
||||||
|
} else if (msg.body === '!info') {
|
||||||
|
const info = client.info
|
||||||
|
await client.sendMessage(msg.from, `*Connection Info*\nUser: ${ info.pushname }\nNumber: ${ info.wid.user }\nPlatform: ${ info.platform }`)
|
||||||
|
} else if (msg.body === '!mediainfo' && msg.hasMedia) {
|
||||||
|
const media = await msg.downloadMedia()
|
||||||
|
await msg.reply(`*Media Info*\nMime: ${ media.mimetype }\nSize: ${ media.data.length }`)
|
||||||
|
} else if (msg.body === '!location') {
|
||||||
|
await msg.reply(new Location(37.422, -122.084, { name: 'Googleplex' }))
|
||||||
|
} else if (msg.body === '!reaction') {
|
||||||
|
await msg.react('👍')
|
||||||
|
} else if (msg.body === '!sendpoll') {
|
||||||
|
await msg.reply(new Poll('Winter or Summer?', ['Winter', 'Summer']))
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error in message handler', error)
|
||||||
|
io.emit('error', { source: 'message_handler', error: error.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
client.on('call', async (call) => {
|
||||||
|
logger.info(`Call from ${ call.from }`)
|
||||||
|
await Call.create({
|
||||||
|
id : call.id,
|
||||||
|
from : call.from,
|
||||||
|
isVideo : call.isVideo,
|
||||||
|
timestamp: Math.floor(Date.now() / 1000),
|
||||||
|
rejected : config.rejectCalls
|
||||||
|
})
|
||||||
|
|
||||||
|
io.emit('call', { id: call.id, from: call.from, isVideo: call.isVideo })
|
||||||
|
|
||||||
|
if (config.rejectCalls) await call.reject()
|
||||||
|
await client.sendMessage(call.from, `[${ call.isVideo ? 'Video' : 'Audio' }] Call from ${ call.from } ${ config.rejectCalls ? '(Auto-rejected)' : '' }`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Graceful shutdown
|
||||||
|
process.on('SIGINT', async () => {
|
||||||
|
logger.info('Shutting down...')
|
||||||
|
await client.destroy()
|
||||||
|
await sequelize.close()
|
||||||
|
server.close(() => process.exit(0))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start
|
||||||
|
async function start() {
|
||||||
|
await ensureDirectories()
|
||||||
|
await sequelize.authenticate()
|
||||||
|
await sequelize.sync({ force: false })
|
||||||
|
|
||||||
|
server.listen(config.port, () => {
|
||||||
|
logger.info(`🚀 Server running on port ${ config.port }`)
|
||||||
|
logger.info(`🔑 API Key: ${ config.apiKey }`)
|
||||||
|
logger.info(`💾 Database: ${ config.dbPath }`)
|
||||||
|
logger.info(`📱 Initializing WhatsApp...`)
|
||||||
|
})
|
||||||
|
|
||||||
|
client.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { app, server, io, client, sequelize }
|
||||||
699
test/complete.js
Normal file
699
test/complete.js
Normal file
@@ -0,0 +1,699 @@
|
|||||||
|
const { Client, Location, Poll, List, Buttons, LocalAuth } = require('whatsapp-web.js');
|
||||||
|
|
||||||
|
const client = new Client({
|
||||||
|
authStrategy: new LocalAuth(),
|
||||||
|
// proxyAuthentication: { username: 'username', password: 'password' },
|
||||||
|
/**
|
||||||
|
* This option changes the browser name from defined in user agent to custom.
|
||||||
|
*/
|
||||||
|
// deviceName: 'Your custom name',
|
||||||
|
/**
|
||||||
|
* This option changes browser type from defined in user agent to yours. It affects the browser icon
|
||||||
|
* that is displayed in 'linked devices' section.
|
||||||
|
* Valid value are: 'Chrome' | 'Firefox' | 'IE' | 'Opera' | 'Safari' | 'Edge'.
|
||||||
|
* If another value is provided, the browser icon in 'linked devices' section will be gray.
|
||||||
|
*/
|
||||||
|
// browserName: 'Firefox',
|
||||||
|
puppeteer: {
|
||||||
|
// args: ['--proxy-server=proxy-server-that-requires-authentication.example.com'],
|
||||||
|
headless: false,
|
||||||
|
},
|
||||||
|
// pairWithPhoneNumber: {
|
||||||
|
// phoneNumber: '96170100100' // Pair with phone number (format: <COUNTRY_CODE><PHONE_NUMBER>)
|
||||||
|
// showNotification: true,
|
||||||
|
// intervalMs: 180000 // Time to renew pairing code in milliseconds, defaults to 3 minutes
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
|
||||||
|
// client initialize does not finish at ready now.
|
||||||
|
client.initialize();
|
||||||
|
|
||||||
|
client.on('loading_screen', (percent, message) => {
|
||||||
|
console.log('LOADING SCREEN', percent, message);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('qr', async (qr) => {
|
||||||
|
// NOTE: This event will not be fired if a session is specified.
|
||||||
|
console.log('QR RECEIVED', qr);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('code', (code) => {
|
||||||
|
console.log('Pairing code:',code);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('authenticated', () => {
|
||||||
|
console.log('AUTHENTICATED');
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('auth_failure', msg => {
|
||||||
|
// Fired if session restore was unsuccessful
|
||||||
|
console.error('AUTHENTICATION FAILURE', msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('ready', async () => {
|
||||||
|
console.log('READY');
|
||||||
|
const debugWWebVersion = await client.getWWebVersion();
|
||||||
|
console.log(`WWebVersion = ${debugWWebVersion}`);
|
||||||
|
|
||||||
|
client.pupPage.on('pageerror', function(err) {
|
||||||
|
console.log('Page error: ' + err.toString());
|
||||||
|
});
|
||||||
|
client.pupPage.on('error', function(err) {
|
||||||
|
console.log('Page error: ' + err.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('message', async msg => {
|
||||||
|
console.log('MESSAGE RECEIVED', msg);
|
||||||
|
|
||||||
|
if (msg.body === '!ping reply') {
|
||||||
|
// Send a new message as a reply to the current one
|
||||||
|
msg.reply('pong');
|
||||||
|
|
||||||
|
} else if (msg.body === '!ping') {
|
||||||
|
// Send a new message to the same chat
|
||||||
|
client.sendMessage(msg.from, 'pong');
|
||||||
|
|
||||||
|
} else if (msg.body.startsWith('!sendto ')) {
|
||||||
|
// Direct send a new message to specific id
|
||||||
|
let number = msg.body.split(' ')[1];
|
||||||
|
let messageIndex = msg.body.indexOf(number) + number.length;
|
||||||
|
let message = msg.body.slice(messageIndex, msg.body.length);
|
||||||
|
number = number.includes('@c.us') ? number : `${number}@c.us`;
|
||||||
|
let chat = await msg.getChat();
|
||||||
|
chat.sendSeen();
|
||||||
|
client.sendMessage(number, message);
|
||||||
|
|
||||||
|
} else if (msg.body.startsWith('!subject ')) {
|
||||||
|
// Change the group subject
|
||||||
|
let chat = await msg.getChat();
|
||||||
|
if (chat.isGroup) {
|
||||||
|
let newSubject = msg.body.slice(9);
|
||||||
|
chat.setSubject(newSubject);
|
||||||
|
} else {
|
||||||
|
msg.reply('This command can only be used in a group!');
|
||||||
|
}
|
||||||
|
} else if (msg.body.startsWith('!echo ')) {
|
||||||
|
// Replies with the same message
|
||||||
|
msg.reply(msg.body.slice(6));
|
||||||
|
} else if (msg.body.startsWith('!preview ')) {
|
||||||
|
const text = msg.body.slice(9);
|
||||||
|
msg.reply(text, null, { linkPreview: true });
|
||||||
|
} else if (msg.body.startsWith('!desc ')) {
|
||||||
|
// Change the group description
|
||||||
|
let chat = await msg.getChat();
|
||||||
|
if (chat.isGroup) {
|
||||||
|
let newDescription = msg.body.slice(6);
|
||||||
|
chat.setDescription(newDescription);
|
||||||
|
} else {
|
||||||
|
msg.reply('This command can only be used in a group!');
|
||||||
|
}
|
||||||
|
} else if (msg.body === '!leave') {
|
||||||
|
// Leave the group
|
||||||
|
let chat = await msg.getChat();
|
||||||
|
if (chat.isGroup) {
|
||||||
|
chat.leave();
|
||||||
|
} else {
|
||||||
|
msg.reply('This command can only be used in a group!');
|
||||||
|
}
|
||||||
|
} else if (msg.body.startsWith('!join ')) {
|
||||||
|
const inviteCode = msg.body.split(' ')[1];
|
||||||
|
try {
|
||||||
|
await client.acceptInvite(inviteCode);
|
||||||
|
msg.reply('Joined the group!');
|
||||||
|
} catch (e) {
|
||||||
|
msg.reply('That invite code seems to be invalid.');
|
||||||
|
}
|
||||||
|
} else if (msg.body.startsWith('!addmembers')) {
|
||||||
|
const group = await msg.getChat();
|
||||||
|
const result = await group.addParticipants(['number1@c.us', 'number2@c.us', 'number3@c.us']);
|
||||||
|
/**
|
||||||
|
* The example of the {@link result} output:
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* 'number1@c.us': {
|
||||||
|
* code: 200,
|
||||||
|
* message: 'The participant was added successfully',
|
||||||
|
* isInviteV4Sent: false
|
||||||
|
* },
|
||||||
|
* 'number2@c.us': {
|
||||||
|
* code: 403,
|
||||||
|
* message: 'The participant can be added by sending private invitation only',
|
||||||
|
* isInviteV4Sent: true
|
||||||
|
* },
|
||||||
|
* 'number3@c.us': {
|
||||||
|
* code: 404,
|
||||||
|
* message: 'The phone number is not registered on WhatsApp',
|
||||||
|
* isInviteV4Sent: false
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* For more usage examples:
|
||||||
|
* @see https://github.com/pedroslopez/whatsapp-web.js/pull/2344#usage-example1
|
||||||
|
*/
|
||||||
|
console.log(result);
|
||||||
|
} else if (msg.body === '!creategroup') {
|
||||||
|
const partitipantsToAdd = ['number1@c.us', 'number2@c.us', 'number3@c.us'];
|
||||||
|
const result = await client.createGroup('Group Title', partitipantsToAdd);
|
||||||
|
/**
|
||||||
|
* The example of the {@link result} output:
|
||||||
|
* {
|
||||||
|
* title: 'Group Title',
|
||||||
|
* gid: {
|
||||||
|
* server: 'g.us',
|
||||||
|
* user: '1111111111',
|
||||||
|
* _serialized: '1111111111@g.us'
|
||||||
|
* },
|
||||||
|
* participants: {
|
||||||
|
* 'botNumber@c.us': {
|
||||||
|
* statusCode: 200,
|
||||||
|
* message: 'The participant was added successfully',
|
||||||
|
* isGroupCreator: true,
|
||||||
|
* isInviteV4Sent: false
|
||||||
|
* },
|
||||||
|
* 'number1@c.us': {
|
||||||
|
* statusCode: 200,
|
||||||
|
* message: 'The participant was added successfully',
|
||||||
|
* isGroupCreator: false,
|
||||||
|
* isInviteV4Sent: false
|
||||||
|
* },
|
||||||
|
* 'number2@c.us': {
|
||||||
|
* statusCode: 403,
|
||||||
|
* message: 'The participant can be added by sending private invitation only',
|
||||||
|
* isGroupCreator: false,
|
||||||
|
* isInviteV4Sent: true
|
||||||
|
* },
|
||||||
|
* 'number3@c.us': {
|
||||||
|
* statusCode: 404,
|
||||||
|
* message: 'The phone number is not registered on WhatsApp',
|
||||||
|
* isGroupCreator: false,
|
||||||
|
* isInviteV4Sent: false
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* For more usage examples:
|
||||||
|
* @see https://github.com/pedroslopez/whatsapp-web.js/pull/2344#usage-example2
|
||||||
|
*/
|
||||||
|
console.log(result);
|
||||||
|
} else if (msg.body === '!groupinfo') {
|
||||||
|
let chat = await msg.getChat();
|
||||||
|
if (chat.isGroup) {
|
||||||
|
msg.reply(`
|
||||||
|
*Group Details*
|
||||||
|
Name: ${chat.name}
|
||||||
|
Description: ${chat.description}
|
||||||
|
Created At: ${chat.createdAt.toString()}
|
||||||
|
Created By: ${chat.owner.user}
|
||||||
|
Participant count: ${chat.participants.length}
|
||||||
|
`);
|
||||||
|
} else {
|
||||||
|
msg.reply('This command can only be used in a group!');
|
||||||
|
}
|
||||||
|
} else if (msg.body === '!chats') {
|
||||||
|
const chats = await client.getChats();
|
||||||
|
client.sendMessage(msg.from, `The bot has ${chats.length} chats open.`);
|
||||||
|
} else if (msg.body === '!info') {
|
||||||
|
let info = client.info;
|
||||||
|
client.sendMessage(msg.from, `
|
||||||
|
*Connection info*
|
||||||
|
User name: ${info.pushname}
|
||||||
|
My number: ${info.wid.user}
|
||||||
|
Platform: ${info.platform}
|
||||||
|
`);
|
||||||
|
} else if (msg.body === '!mediainfo' && msg.hasMedia) {
|
||||||
|
const attachmentData = await msg.downloadMedia();
|
||||||
|
msg.reply(`
|
||||||
|
*Media info*
|
||||||
|
MimeType: ${attachmentData.mimetype}
|
||||||
|
Filename: ${attachmentData.filename}
|
||||||
|
Data (length): ${attachmentData.data.length}
|
||||||
|
`);
|
||||||
|
} else if (msg.body === '!quoteinfo' && msg.hasQuotedMsg) {
|
||||||
|
const quotedMsg = await msg.getQuotedMessage();
|
||||||
|
|
||||||
|
quotedMsg.reply(`
|
||||||
|
ID: ${quotedMsg.id._serialized}
|
||||||
|
Type: ${quotedMsg.type}
|
||||||
|
Author: ${quotedMsg.author || quotedMsg.from}
|
||||||
|
Timestamp: ${quotedMsg.timestamp}
|
||||||
|
Has Media? ${quotedMsg.hasMedia}
|
||||||
|
`);
|
||||||
|
} else if (msg.body === '!resendmedia' && msg.hasQuotedMsg) {
|
||||||
|
const quotedMsg = await msg.getQuotedMessage();
|
||||||
|
if (quotedMsg.hasMedia) {
|
||||||
|
const attachmentData = await quotedMsg.downloadMedia();
|
||||||
|
client.sendMessage(msg.from, attachmentData, { caption: 'Here\'s your requested media.' });
|
||||||
|
}
|
||||||
|
if (quotedMsg.hasMedia && quotedMsg.type === 'audio') {
|
||||||
|
const audio = await quotedMsg.downloadMedia();
|
||||||
|
await client.sendMessage(msg.from, audio, { sendAudioAsVoice: true });
|
||||||
|
}
|
||||||
|
} else if (msg.body === '!isviewonce' && msg.hasQuotedMsg) {
|
||||||
|
const quotedMsg = await msg.getQuotedMessage();
|
||||||
|
if (quotedMsg.hasMedia) {
|
||||||
|
const media = await quotedMsg.downloadMedia();
|
||||||
|
await client.sendMessage(msg.from, media, { isViewOnce: true });
|
||||||
|
}
|
||||||
|
} else if (msg.body === '!location') {
|
||||||
|
// only latitude and longitude
|
||||||
|
await msg.reply(new Location(37.422, -122.084));
|
||||||
|
// location with name only
|
||||||
|
await msg.reply(new Location(37.422, -122.084, { name: 'Googleplex' }));
|
||||||
|
// location with address only
|
||||||
|
await msg.reply(new Location(37.422, -122.084, { address: '1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA' }));
|
||||||
|
// location with name, address and url
|
||||||
|
await msg.reply(new Location(37.422, -122.084, { name: 'Googleplex', address: '1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA', url: 'https://google.com' }));
|
||||||
|
} else if (msg.location) {
|
||||||
|
msg.reply(msg.location);
|
||||||
|
} else if (msg.body.startsWith('!status ')) {
|
||||||
|
const newStatus = msg.body.split(' ')[1];
|
||||||
|
await client.setStatus(newStatus);
|
||||||
|
msg.reply(`Status was updated to *${newStatus}*`);
|
||||||
|
} else if (msg.body === '!mentionUsers') {
|
||||||
|
const chat = await msg.getChat();
|
||||||
|
const userNumber = 'XXXXXXXXXX';
|
||||||
|
/**
|
||||||
|
* To mention one user you can pass user's ID to 'mentions' property as is,
|
||||||
|
* without wrapping it in Array, and a user's phone number to the message body:
|
||||||
|
*/
|
||||||
|
await chat.sendMessage(`Hi @${userNumber}`, {
|
||||||
|
mentions: userNumber + '@c.us'
|
||||||
|
});
|
||||||
|
// To mention a list of users:
|
||||||
|
await chat.sendMessage(`Hi @${userNumber}, @${userNumber}`, {
|
||||||
|
mentions: [userNumber + '@c.us', userNumber + '@c.us']
|
||||||
|
});
|
||||||
|
} else if (msg.body === '!mentionGroups') {
|
||||||
|
const chat = await msg.getChat();
|
||||||
|
const groupId = 'YYYYYYYYYY@g.us';
|
||||||
|
/**
|
||||||
|
* Sends clickable group mentions, the same as user mentions.
|
||||||
|
* When the mentions are clicked, it opens a chat with the mentioned group.
|
||||||
|
* The 'groupMentions.subject' can be custom
|
||||||
|
*
|
||||||
|
* @note The user that does not participate in the mentioned group,
|
||||||
|
* will not be able to click on that mentioned group, the same if the group does not exist
|
||||||
|
*
|
||||||
|
* To mention one group:
|
||||||
|
*/
|
||||||
|
await chat.sendMessage(`Check the last message here: @${groupId}`, {
|
||||||
|
groupMentions: { subject: 'GroupSubject', id: groupId }
|
||||||
|
});
|
||||||
|
// To mention a list of groups:
|
||||||
|
await chat.sendMessage(`Check the last message in these groups: @${groupId}, @${groupId}`, {
|
||||||
|
groupMentions: [
|
||||||
|
{ subject: 'FirstGroup', id: groupId },
|
||||||
|
{ subject: 'SecondGroup', id: groupId }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
} else if (msg.body === '!getGroupMentions') {
|
||||||
|
// To get group mentions from a message:
|
||||||
|
const groupId = 'ZZZZZZZZZZ@g.us';
|
||||||
|
const msg = await client.sendMessage('chatId', `Check the last message here: @${groupId}`, {
|
||||||
|
groupMentions: { subject: 'GroupSubject', id: groupId }
|
||||||
|
});
|
||||||
|
/** {@link groupMentions} is an array of `GroupChat` */
|
||||||
|
const groupMentions = await msg.getGroupMentions();
|
||||||
|
console.log(groupMentions);
|
||||||
|
} else if (msg.body === '!delete') {
|
||||||
|
if (msg.hasQuotedMsg) {
|
||||||
|
const quotedMsg = await msg.getQuotedMessage();
|
||||||
|
if (quotedMsg.fromMe) {
|
||||||
|
quotedMsg.delete(true);
|
||||||
|
} else {
|
||||||
|
msg.reply('I can only delete my own messages');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (msg.body === '!pin') {
|
||||||
|
const chat = await msg.getChat();
|
||||||
|
await chat.pin();
|
||||||
|
} else if (msg.body === '!archive') {
|
||||||
|
const chat = await msg.getChat();
|
||||||
|
await chat.archive();
|
||||||
|
} else if (msg.body === '!mute') {
|
||||||
|
const chat = await msg.getChat();
|
||||||
|
// mute the chat for 20 seconds
|
||||||
|
const unmuteDate = new Date();
|
||||||
|
unmuteDate.setSeconds(unmuteDate.getSeconds() + 20);
|
||||||
|
await chat.mute(unmuteDate);
|
||||||
|
} else if (msg.body === '!typing') {
|
||||||
|
const chat = await msg.getChat();
|
||||||
|
// simulates typing in the chat
|
||||||
|
chat.sendStateTyping();
|
||||||
|
} else if (msg.body === '!recording') {
|
||||||
|
const chat = await msg.getChat();
|
||||||
|
// simulates recording audio in the chat
|
||||||
|
chat.sendStateRecording();
|
||||||
|
} else if (msg.body === '!clearstate') {
|
||||||
|
const chat = await msg.getChat();
|
||||||
|
// stops typing or recording in the chat
|
||||||
|
chat.clearState();
|
||||||
|
} else if (msg.body === '!jumpto') {
|
||||||
|
if (msg.hasQuotedMsg) {
|
||||||
|
const quotedMsg = await msg.getQuotedMessage();
|
||||||
|
client.interface.openChatWindowAt(quotedMsg.id._serialized);
|
||||||
|
}
|
||||||
|
} else if (msg.body === '!buttons') {
|
||||||
|
let button = new Buttons('Button body', [{ body: 'bt1' }, { body: 'bt2' }, { body: 'bt3' }], 'title', 'footer');
|
||||||
|
client.sendMessage(msg.from, button);
|
||||||
|
} else if (msg.body === '!list') {
|
||||||
|
let sections = [
|
||||||
|
{ title: 'sectionTitle', rows: [{ title: 'ListItem1', description: 'desc' }, { title: 'ListItem2' }] }
|
||||||
|
];
|
||||||
|
let list = new List('List body', 'btnText', sections, 'Title', 'footer');
|
||||||
|
client.sendMessage(msg.from, list);
|
||||||
|
} else if (msg.body === '!reaction') {
|
||||||
|
await msg.react('👍');
|
||||||
|
} else if (msg.body === '!sendpoll') {
|
||||||
|
/** By default the poll is created as a single choice poll: */
|
||||||
|
await msg.reply(new Poll('Winter or Summer?', ['Winter', 'Summer']));
|
||||||
|
/** If you want to provide a multiple choice poll, add allowMultipleAnswers as true: */
|
||||||
|
await msg.reply(new Poll('Cats or Dogs?', ['Cats', 'Dogs'], { allowMultipleAnswers: true }));
|
||||||
|
/**
|
||||||
|
* You can provide a custom message secret, it can be used as a poll ID:
|
||||||
|
* @note It has to be a unique vector with a length of 32
|
||||||
|
*/
|
||||||
|
await msg.reply(
|
||||||
|
new Poll('Cats or Dogs?', ['Cats', 'Dogs'], {
|
||||||
|
messageSecret: [
|
||||||
|
1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||||
|
]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else if (msg.body === '!vote') {
|
||||||
|
if (msg.hasQuotedMsg) {
|
||||||
|
const quotedMsg = await msg.getQuotedMessage();
|
||||||
|
if (quotedMsg.type === 'poll_creation') {
|
||||||
|
await quotedMsg.vote(msg.body.replace('!vote', ''));
|
||||||
|
} else {
|
||||||
|
msg.reply('Can only be used on poll messages');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (msg.body === '!edit') {
|
||||||
|
if (msg.hasQuotedMsg) {
|
||||||
|
const quotedMsg = await msg.getQuotedMessage();
|
||||||
|
if (quotedMsg.fromMe) {
|
||||||
|
await quotedMsg.edit(msg.body.replace('!edit', ''));
|
||||||
|
} else {
|
||||||
|
msg.reply('I can only edit my own messages');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (msg.body === '!updatelabels') {
|
||||||
|
const chat = await msg.getChat();
|
||||||
|
await chat.changeLabels([0, 1]);
|
||||||
|
} else if (msg.body === '!addlabels') {
|
||||||
|
const chat = await msg.getChat();
|
||||||
|
let labels = (await chat.getLabels()).map((l) => l.id);
|
||||||
|
labels.push('0');
|
||||||
|
labels.push('1');
|
||||||
|
await chat.changeLabels(labels);
|
||||||
|
} else if (msg.body === '!removelabels') {
|
||||||
|
const chat = await msg.getChat();
|
||||||
|
await chat.changeLabels([]);
|
||||||
|
} else if (msg.body === '!approverequest') {
|
||||||
|
/**
|
||||||
|
* Presented an example for membership request approvals, the same examples are for the request rejections.
|
||||||
|
* To approve the membership request from a specific user:
|
||||||
|
*/
|
||||||
|
await client.approveGroupMembershipRequests(msg.from, { requesterIds: 'number@c.us' });
|
||||||
|
/** The same for execution on group object (no need to provide the group ID): */
|
||||||
|
const group = await msg.getChat();
|
||||||
|
await group.approveGroupMembershipRequests({ requesterIds: 'number@c.us' });
|
||||||
|
/** To approve several membership requests: */
|
||||||
|
const approval = await client.approveGroupMembershipRequests(msg.from, {
|
||||||
|
requesterIds: ['number1@c.us', 'number2@c.us']
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* The example of the {@link approval} output:
|
||||||
|
* [
|
||||||
|
* {
|
||||||
|
* requesterId: 'number1@c.us',
|
||||||
|
* message: 'Rejected successfully'
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* requesterId: 'number2@c.us',
|
||||||
|
* error: 404,
|
||||||
|
* message: 'ParticipantRequestNotFoundError'
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
console.log(approval);
|
||||||
|
/** To approve all the existing membership requests (simply don't provide any user IDs): */
|
||||||
|
await client.approveGroupMembershipRequests(msg.from);
|
||||||
|
/** To change the sleep value to 300 ms: */
|
||||||
|
await client.approveGroupMembershipRequests(msg.from, {
|
||||||
|
requesterIds: ['number1@c.us', 'number2@c.us'],
|
||||||
|
sleep: 300
|
||||||
|
});
|
||||||
|
/** To change the sleep value to random value between 100 and 300 ms: */
|
||||||
|
await client.approveGroupMembershipRequests(msg.from, {
|
||||||
|
requesterIds: ['number1@c.us', 'number2@c.us'],
|
||||||
|
sleep: [100, 300]
|
||||||
|
});
|
||||||
|
/** To explicitly disable the sleep: */
|
||||||
|
await client.approveGroupMembershipRequests(msg.from, {
|
||||||
|
requesterIds: ['number1@c.us', 'number2@c.us'],
|
||||||
|
sleep: null
|
||||||
|
});
|
||||||
|
} else if (msg.body === '!pinmsg') {
|
||||||
|
/**
|
||||||
|
* Pins a message in a chat, a method takes a number in seconds for the message to be pinned.
|
||||||
|
* WhatsApp default values for duration to pass to the method are:
|
||||||
|
* 1. 86400 for 24 hours
|
||||||
|
* 2. 604800 for 7 days
|
||||||
|
* 3. 2592000 for 30 days
|
||||||
|
* You can pass your own value:
|
||||||
|
*/
|
||||||
|
const result = await msg.pin(60); // Will pin a message for 1 minute
|
||||||
|
console.log(result); // True if the operation completed successfully, false otherwise
|
||||||
|
} else if (msg.body === '!howManyConnections') {
|
||||||
|
/**
|
||||||
|
* Get user device count by ID
|
||||||
|
* Each WaWeb Connection counts as one device, and the phone (if exists) counts as one
|
||||||
|
* So for a non-enterprise user with one WaWeb connection it should return "2"
|
||||||
|
*/
|
||||||
|
let deviceCount = await client.getContactDeviceCount(msg.from);
|
||||||
|
await msg.reply(`You have *${deviceCount}* devices connected`);
|
||||||
|
} else if (msg.body === '!syncHistory') {
|
||||||
|
const isSynced = await client.syncHistory(msg.from);
|
||||||
|
// Or through the Chat object:
|
||||||
|
// const chat = await client.getChatById(msg.from);
|
||||||
|
// const isSynced = await chat.syncHistory();
|
||||||
|
|
||||||
|
await msg.reply(isSynced ? 'Historical chat is syncing..' : 'There is no historical chat to sync.');
|
||||||
|
} else if (msg.body === '!statuses') {
|
||||||
|
const statuses = await client.getBroadcasts();
|
||||||
|
console.log(statuses);
|
||||||
|
const chat = await statuses[0]?.getChat(); // Get user chat of a first status
|
||||||
|
console.log(chat);
|
||||||
|
} else if (msg.body === '!sendMediaHD' && msg.hasQuotedMsg) {
|
||||||
|
const quotedMsg = await msg.getQuotedMessage();
|
||||||
|
if (quotedMsg.hasMedia) {
|
||||||
|
const media = await quotedMsg.downloadMedia();
|
||||||
|
await client.sendMessage(msg.from, media, { sendMediaAsHd: true });
|
||||||
|
}
|
||||||
|
} else if (msg.body === '!parseVCard') {
|
||||||
|
const vCard =
|
||||||
|
'BEGIN:VCARD\n' +
|
||||||
|
'VERSION:3.0\n' +
|
||||||
|
'FN:John Doe\n' +
|
||||||
|
'ORG:Microsoft;\n' +
|
||||||
|
'EMAIL;type=INTERNET:john.doe@gmail.com\n' +
|
||||||
|
'URL:www.johndoe.com\n' +
|
||||||
|
'TEL;type=CELL;type=VOICE;waid=18006427676:+1 (800) 642 7676\n' +
|
||||||
|
'END:VCARD';
|
||||||
|
const vCardExtended =
|
||||||
|
'BEGIN:VCARD\n' +
|
||||||
|
'VERSION:3.0\n' +
|
||||||
|
'FN:John Doe\n' +
|
||||||
|
'ORG:Microsoft;\n' +
|
||||||
|
'item1.TEL:+1 (800) 642 7676\n' +
|
||||||
|
'item1.X-ABLabel:USA Customer Service\n' +
|
||||||
|
'item2.TEL:+55 11 4706 0900\n' +
|
||||||
|
'item2.X-ABLabel:Brazil Customer Service\n' +
|
||||||
|
'PHOTO;BASE64:here you can paste a binary data of a contact photo in Base64 encoding\n' +
|
||||||
|
'END:VCARD';
|
||||||
|
const userId = 'XXXXXXXXXX@c.us';
|
||||||
|
await client.sendMessage(userId, vCard);
|
||||||
|
await client.sendMessage(userId, vCardExtended);
|
||||||
|
} else if (msg.body === '!changeSync') {
|
||||||
|
// NOTE: this action will take effect after you restart the client.
|
||||||
|
const backgroundSync = await client.setBackgroundSync(true);
|
||||||
|
console.log(backgroundSync);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('message_create', async (msg) => {
|
||||||
|
// Fired on all message creations, including your own
|
||||||
|
if (msg.fromMe) {
|
||||||
|
// do stuff here
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unpins a message
|
||||||
|
if (msg.fromMe && msg.body.startsWith('!unpin')) {
|
||||||
|
const pinnedMsg = await msg.getQuotedMessage();
|
||||||
|
if (pinnedMsg) {
|
||||||
|
// Will unpin a message
|
||||||
|
const result = await pinnedMsg.unpin();
|
||||||
|
console.log(result); // True if the operation completed successfully, false otherwise
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('message_ciphertext', (msg) => {
|
||||||
|
// Receiving new incoming messages that have been encrypted
|
||||||
|
// msg.type === 'ciphertext'
|
||||||
|
msg.body = 'Waiting for this message. Check your phone.';
|
||||||
|
|
||||||
|
// do stuff here
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('message_revoke_everyone', async (after, before) => {
|
||||||
|
// Fired whenever a message is deleted by anyone (including you)
|
||||||
|
console.log(after); // message after it was deleted.
|
||||||
|
if (before) {
|
||||||
|
console.log(before); // message before it was deleted.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('message_revoke_me', async (msg) => {
|
||||||
|
// Fired whenever a message is only deleted in your own view.
|
||||||
|
console.log(msg.body); // message before it was deleted.
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('message_ack', (msg, ack) => {
|
||||||
|
/*
|
||||||
|
== ACK VALUES ==
|
||||||
|
ACK_ERROR: -1
|
||||||
|
ACK_PENDING: 0
|
||||||
|
ACK_SERVER: 1
|
||||||
|
ACK_DEVICE: 2
|
||||||
|
ACK_READ: 3
|
||||||
|
ACK_PLAYED: 4
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (ack == 3) {
|
||||||
|
// The message was read
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('group_join', (notification) => {
|
||||||
|
// User has joined or been added to the group.
|
||||||
|
console.log('join', notification);
|
||||||
|
notification.reply('User joined.');
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('group_leave', (notification) => {
|
||||||
|
// User has left or been kicked from the group.
|
||||||
|
console.log('leave', notification);
|
||||||
|
notification.reply('User left.');
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('group_update', (notification) => {
|
||||||
|
// Group picture, subject or description has been updated.
|
||||||
|
console.log('update', notification);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('change_state', state => {
|
||||||
|
console.log('CHANGE STATE', state);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Change to false if you don't want to reject incoming calls
|
||||||
|
let rejectCalls = true;
|
||||||
|
|
||||||
|
client.on('call', async (call) => {
|
||||||
|
console.log('Call received, rejecting. GOTO Line 261 to disable', call);
|
||||||
|
if (rejectCalls) await call.reject();
|
||||||
|
await client.sendMessage(call.from, `[${call.fromMe ? 'Outgoing' : 'Incoming'}] Phone call from ${call.from}, type ${call.isGroup ? 'group' : ''} ${call.isVideo ? 'video' : 'audio'} call. ${rejectCalls ? 'This call was automatically rejected by the script.' : ''}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('disconnected', (reason) => {
|
||||||
|
console.log('Client was logged out', reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('contact_changed', async (message, oldId, newId, isContact) => {
|
||||||
|
/** The time the event occurred. */
|
||||||
|
const eventTime = (new Date(message.timestamp * 1000)).toLocaleString();
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`The contact ${oldId.slice(0, -5)}` +
|
||||||
|
`${!isContact ? ' that participates in group ' +
|
||||||
|
`${(await client.getChatById(message.to ?? message.from)).name} ` : ' '}` +
|
||||||
|
`changed their phone number\nat ${eventTime}.\n` +
|
||||||
|
`Their new phone number is ${newId.slice(0, -5)}.\n`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about the @param {message}:
|
||||||
|
*
|
||||||
|
* 1. If a notification was emitted due to a group participant changing their phone number:
|
||||||
|
* @param {message.author} is a participant's id before the change.
|
||||||
|
* @param {message.recipients[0]} is a participant's id after the change (a new one).
|
||||||
|
*
|
||||||
|
* 1.1 If the contact who changed their number WAS in the current user's contact list at the time of the change:
|
||||||
|
* @param {message.to} is a group chat id the event was emitted in.
|
||||||
|
* @param {message.from} is a current user's id that got an notification message in the group.
|
||||||
|
* Also the @param {message.fromMe} is TRUE.
|
||||||
|
*
|
||||||
|
* 1.2 Otherwise:
|
||||||
|
* @param {message.from} is a group chat id the event was emitted in.
|
||||||
|
* @param {message.to} is @type {undefined}.
|
||||||
|
* Also @param {message.fromMe} is FALSE.
|
||||||
|
*
|
||||||
|
* 2. If a notification was emitted due to a contact changing their phone number:
|
||||||
|
* @param {message.templateParams} is an array of two user's ids:
|
||||||
|
* the old (before the change) and a new one, stored in alphabetical order.
|
||||||
|
* @param {message.from} is a current user's id that has a chat with a user,
|
||||||
|
* whos phone number was changed.
|
||||||
|
* @param {message.to} is a user's id (after the change), the current user has a chat with.
|
||||||
|
*/
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('group_admin_changed', (notification) => {
|
||||||
|
if (notification.type === 'promote') {
|
||||||
|
/**
|
||||||
|
* Emitted when a current user is promoted to an admin.
|
||||||
|
* {@link notification.author} is a user who performs the action of promoting/demoting the current user.
|
||||||
|
*/
|
||||||
|
console.log(`You were promoted by ${notification.author}`);
|
||||||
|
} else if (notification.type === 'demote')
|
||||||
|
/** Emitted when a current user is demoted to a regular user. */
|
||||||
|
console.log(`You were demoted by ${notification.author}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('group_membership_request', async (notification) => {
|
||||||
|
/**
|
||||||
|
* The example of the {@link notification} output:
|
||||||
|
* {
|
||||||
|
* id: {
|
||||||
|
* fromMe: false,
|
||||||
|
* remote: 'groupId@g.us',
|
||||||
|
* id: '123123123132132132',
|
||||||
|
* participant: 'number@c.us',
|
||||||
|
* _serialized: 'false_groupId@g.us_123123123132132132_number@c.us'
|
||||||
|
* },
|
||||||
|
* body: '',
|
||||||
|
* type: 'created_membership_requests',
|
||||||
|
* timestamp: 1694456538,
|
||||||
|
* chatId: 'groupId@g.us',
|
||||||
|
* author: 'number@c.us',
|
||||||
|
* recipientIds: []
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
console.log(notification);
|
||||||
|
/** You can approve or reject the newly appeared membership request: */
|
||||||
|
await client.approveGroupMembershipRequestss(notification.chatId, notification.author);
|
||||||
|
await client.rejectGroupMembershipRequests(notification.chatId, notification.author);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('message_reaction', async (reaction) => {
|
||||||
|
console.log('REACTION RECEIVED', reaction);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('vote_update', (vote) => {
|
||||||
|
/** The vote that was affected: */
|
||||||
|
console.log(vote);
|
||||||
|
});
|
||||||
82
test/index.js
Normal file
82
test/index.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
const { Client, LocalAuth } = require('whatsapp-web.js')
|
||||||
|
const qrcode = require('qrcode-terminal')
|
||||||
|
|
||||||
|
// Initialize client with session persistence
|
||||||
|
const client = new Client({
|
||||||
|
authStrategy: new LocalAuth(), // Saves session so you don't need to scan QR every time
|
||||||
|
puppeteer : {
|
||||||
|
headless: true,
|
||||||
|
args : ['--no-sandbox', '--disable-setuid-sandbox']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Generate QR in terminal
|
||||||
|
client.on('qr', (qr) => {
|
||||||
|
console.log('\n📱 Scan this QR code with WhatsApp:')
|
||||||
|
qrcode.generate(qr, { small: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
// When client is ready
|
||||||
|
/*client.on('ready', () => {
|
||||||
|
console.log('\n✅ Client is ready!')
|
||||||
|
console.log('Listening for messages...')
|
||||||
|
console.log('Use Ctrl+C to exit\n')
|
||||||
|
})*/
|
||||||
|
client.on('ready', async () => {
|
||||||
|
console.log('\n✅ Client is ready!')
|
||||||
|
|
||||||
|
// Auto-reply to the last person who messaged you
|
||||||
|
let lastContact = null
|
||||||
|
var currentTimeAndDateTodayAsDutchh = new Date().toLocaleString('nl-NL', { timeZone: 'Europe/Amsterdam' })
|
||||||
|
await sendMessage('31639266516@c.us', currentTimeAndDateTodayAsDutchh + 'Yoo toppertje 🎉!')
|
||||||
|
|
||||||
|
client.on('message', (message) => {
|
||||||
|
console.log(`📥 [${ message.from }] ${ message.body }`)
|
||||||
|
lastContact = message.from.split('@')[0] // Extract number
|
||||||
|
// Send after 10 seconds
|
||||||
|
setTimeout(async () => {
|
||||||
|
if (lastContact) {
|
||||||
|
console.log(`\nSending test to ${ lastContact }...`)
|
||||||
|
await sendMessage(lastContact, 'Yooo toppertje🎉!')
|
||||||
|
} else
|
||||||
|
console.log('No last contact found')
|
||||||
|
}, 2000)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
// Listen for incoming messages
|
||||||
|
client.on('message', (message) => {
|
||||||
|
console.log(`📥 [${ message.from }] ${ message.body || '(media message)' }`)
|
||||||
|
|
||||||
|
// Auto-reply example
|
||||||
|
if (message.body.toLowerCase() === 'ping') {
|
||||||
|
message.reply('🏓 Pong!')
|
||||||
|
console.log(' Replied with "Pong!"')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Function to send messages
|
||||||
|
const sendMessage = async (number, text) => {
|
||||||
|
try {
|
||||||
|
const chatId = number.includes('@c.us') ? number : `${ number }@c.us`
|
||||||
|
await client.sendMessage(chatId, text)
|
||||||
|
console.log(`✅ Sent to ${ number }: "${ text }"`)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Error sending to ${ number }:`, error.message)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export for external use
|
||||||
|
module.exports = { client, sendMessage }
|
||||||
|
|
||||||
|
// Initialize client
|
||||||
|
client.initialize()
|
||||||
|
|
||||||
|
// Graceful shutdown
|
||||||
|
process.on('SIGINT', async () => {
|
||||||
|
console.log('\n🛑 Shutting down...')
|
||||||
|
await client.destroy()
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user