@vielhuber/wahelper 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.MD +101 -0
- package/package.json +28 -0
- package/whatsapp.js +759 -0
package/README.MD
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# ๐ธ wahelper ๐ธ
|
|
2
|
+
|
|
3
|
+
wahelper is a lightweight whatsapp integration layer built on top of [baileys](https://github.com/WhiskeySockets/Baileys) that provides a simple cli, php wrapper, and mcp server for fetching messages, sending direct and group messages, and wiring whatsapp into existing tooling (wordpress, node, mcp clients) without having to deal with the full session lifecycle yourself.
|
|
4
|
+
|
|
5
|
+
## requirements
|
|
6
|
+
|
|
7
|
+
- node >= lts
|
|
8
|
+
- php >= 8.1
|
|
9
|
+
|
|
10
|
+
## installation
|
|
11
|
+
|
|
12
|
+
### js
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install @vielhuber/wahelper
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### php
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
composer require vielhuber/wahelper
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## usage
|
|
25
|
+
|
|
26
|
+
### cli
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npx wahelper --device="xxxxxxxxxxxx" --action "fetch_messages"
|
|
30
|
+
|
|
31
|
+
npx wahelper --device="xxxxxxxxxxxx" --action "send_user" \
|
|
32
|
+
--number "xxxxxxxxxxxx" \
|
|
33
|
+
--message "This is a test! ๐"
|
|
34
|
+
|
|
35
|
+
npx wahelper --device="xxxxxxxxxxxx" --action "send_group" \
|
|
36
|
+
--name "Group name" \
|
|
37
|
+
--message "This is a test! ๐"
|
|
38
|
+
|
|
39
|
+
npx wahelper --device="xxxxxxxxxxxx" --action "send_user" \
|
|
40
|
+
--number "xxxxxxxxxxxx" \
|
|
41
|
+
--message "This is a test! ๐" \
|
|
42
|
+
--attachments="/full/path/to/file.pdf,/full/path/to/image.png"
|
|
43
|
+
|
|
44
|
+
npx wahelper
|
|
45
|
+
--disable-warning=ExperimentalWarning
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
sqlite3 \
|
|
49
|
+
-header \
|
|
50
|
+
-column \
|
|
51
|
+
whatsapp.sqlite \
|
|
52
|
+
"SELECT text FROM messages ORDER BY timestamp ASC" | \
|
|
53
|
+
tail -10
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### php
|
|
57
|
+
|
|
58
|
+
```php
|
|
59
|
+
require_once 'whatsapp.php';
|
|
60
|
+
|
|
61
|
+
// fetch messages
|
|
62
|
+
WhatsApp::run([
|
|
63
|
+
'device' => 'xxxxxxxxxxxx',
|
|
64
|
+
'action' => 'fetch_messages'
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
// send messages
|
|
68
|
+
WhatsApp::run([
|
|
69
|
+
'device' => 'xxxxxxxxxxxx',
|
|
70
|
+
'action' => 'send_user',
|
|
71
|
+
'number' => 'xxxxxxxxxxxx',
|
|
72
|
+
'message' => 'This is a test! ๐'
|
|
73
|
+
]);
|
|
74
|
+
WhatsApp::run([
|
|
75
|
+
'device' => 'xxxxxxxxxxxx',
|
|
76
|
+
'action' => 'send_group',
|
|
77
|
+
'name' => 'Group name',
|
|
78
|
+
'message' => 'This is a test! ๐'
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
// send attachments
|
|
82
|
+
WhatsApp::run([
|
|
83
|
+
'device' => 'xxxxxxxxxxxx',
|
|
84
|
+
'action' => 'send_user', // or 'send_group'
|
|
85
|
+
/* ... */
|
|
86
|
+
'attachments' => ['/full/path/to/file.pdf', '/full/path/to/image.png']
|
|
87
|
+
]);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### mcp
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
{
|
|
94
|
+
"mcpServers": {
|
|
95
|
+
"whatsapp": {
|
|
96
|
+
"command": "npx",
|
|
97
|
+
"args": ["/var/www/project/path/to/subfolder/wahelper/node_modules/.bin/wahelper", "--mcp"]
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vielhuber/wahelper",
|
|
3
|
+
"version": "1.0.6",
|
|
4
|
+
"description": "Lightweight whatsapp integration layer.",
|
|
5
|
+
"main": "whatsapp.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"whatsapp.js"
|
|
8
|
+
],
|
|
9
|
+
"bin": {
|
|
10
|
+
"wahelper": "whatsapp.js"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"repository": "git@github.com:vielhuber/wahelper.git",
|
|
17
|
+
"author": "David Vielhuber <david@vielhuber.de>",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.20.2",
|
|
21
|
+
"baileys": "^7.0.0-rc.6",
|
|
22
|
+
"dotenv": "^17.2.3"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@prettier/plugin-php": "^0.24.0",
|
|
26
|
+
"prettier": "^3.6.2"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/whatsapp.js
ADDED
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import makeWASocket, { useMultiFileAuthState, DisconnectReason, downloadMediaMessage } from 'baileys';
|
|
6
|
+
import P from 'pino';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { dirname } from 'path';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
11
|
+
import dotenv from 'dotenv';
|
|
12
|
+
|
|
13
|
+
export default class WhatsApp {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.args = this.parseArgs();
|
|
16
|
+
this.dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
this.sock = null;
|
|
18
|
+
this.locks = { db: false };
|
|
19
|
+
this.db = null;
|
|
20
|
+
this.dbIsOpen = false;
|
|
21
|
+
this.shutdown = false;
|
|
22
|
+
this.isFirstRun = false;
|
|
23
|
+
this.isMcp = this.args.mcp === true;
|
|
24
|
+
if (this.args.device !== undefined && this.args.device !== null && this.args.device !== '') {
|
|
25
|
+
this.authFolder = 'auth_' + this.formatNumber(this.args.device);
|
|
26
|
+
this.dbPath = 'whatsapp_' + this.formatNumber(this.args.device) + '.sqlite';
|
|
27
|
+
this.logPath = 'whatsapp_' + this.formatNumber(this.args.device) + '.log';
|
|
28
|
+
this.dataPath = 'whatsapp_' + this.formatNumber(this.args.device) + '.json';
|
|
29
|
+
}
|
|
30
|
+
this.inactivityTimeMaxOrig = 10;
|
|
31
|
+
this.inactivityTimeMax = this.inactivityTimeMaxOrig;
|
|
32
|
+
this.inactivityTimeCur = 0;
|
|
33
|
+
this.inactivityTimeInterval = null;
|
|
34
|
+
this.inactivityTimeStatus = false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async init() {
|
|
38
|
+
await this.awaitLock('init', true);
|
|
39
|
+
this.setLock('init', true);
|
|
40
|
+
this.write({ success: false, message: 'loading_state', data: null });
|
|
41
|
+
|
|
42
|
+
if (this.isMcp === false) {
|
|
43
|
+
this.log('cli start');
|
|
44
|
+
this.log(this.args);
|
|
45
|
+
if (
|
|
46
|
+
this.args.device === undefined ||
|
|
47
|
+
(this.args.action === 'send_user' &&
|
|
48
|
+
(this.args.number === undefined || this.args.message === undefined)) ||
|
|
49
|
+
(this.args.action === 'send_group' &&
|
|
50
|
+
(this.args.name === undefined || this.args.message === undefined)) ||
|
|
51
|
+
!['fetch_messages', 'send_user', 'send_group'].includes(this.args.action)
|
|
52
|
+
) {
|
|
53
|
+
console.error('input missing or unknown action!');
|
|
54
|
+
this.log('โinput missing or unknown action!');
|
|
55
|
+
this.write({
|
|
56
|
+
success: false,
|
|
57
|
+
message: 'error',
|
|
58
|
+
public_message: 'input missing or unknown action!',
|
|
59
|
+
data: null
|
|
60
|
+
});
|
|
61
|
+
this.removeLocks();
|
|
62
|
+
} else {
|
|
63
|
+
this.initDatabase();
|
|
64
|
+
this.initInactivityTimer();
|
|
65
|
+
this.initExitHooks();
|
|
66
|
+
if (this.args.reset === true) {
|
|
67
|
+
this.resetFolder();
|
|
68
|
+
}
|
|
69
|
+
let response = null;
|
|
70
|
+
if (this.args.action === 'fetch_messages') {
|
|
71
|
+
response = await this.authAndRun(() => this.fetchMessages());
|
|
72
|
+
}
|
|
73
|
+
if (this.args.action === 'send_user') {
|
|
74
|
+
response = await this.authAndRun(() =>
|
|
75
|
+
this.sendMessageToUser(this.args.number, this.args.message, this.args.attachments)
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (this.args.action === 'send_group') {
|
|
79
|
+
response = await this.authAndRun(() =>
|
|
80
|
+
this.sendMessageToGroup(this.args.name, this.args.message, this.args.attachments)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
console.log(response);
|
|
84
|
+
await this.endSession();
|
|
85
|
+
}
|
|
86
|
+
this.log('cli stop');
|
|
87
|
+
//process.exit();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (this.isMcp === true) {
|
|
91
|
+
this.log('mcp start');
|
|
92
|
+
this.registerMcp();
|
|
93
|
+
let transport = new StdioServerTransport();
|
|
94
|
+
await server.connect(transport);
|
|
95
|
+
this.log('mcp stop');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async endSession() {
|
|
100
|
+
if (this.shutdown === true) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this.shutdown = true;
|
|
104
|
+
this.setInactivityTimeMax(0);
|
|
105
|
+
|
|
106
|
+
// this mainly closes the websocket connection
|
|
107
|
+
// be aware that the connecion cound be still active afterwards
|
|
108
|
+
this.log('โณsock.end');
|
|
109
|
+
this.sock.end();
|
|
110
|
+
this.log('โ
sock.end');
|
|
111
|
+
|
|
112
|
+
// we wait until the connection is really closed
|
|
113
|
+
//await new Promise(resolve => setTimeout(resolve, 1000));
|
|
114
|
+
|
|
115
|
+
if (this.db) {
|
|
116
|
+
this.log('โณdb.close');
|
|
117
|
+
this.db.close();
|
|
118
|
+
this.dbIsOpen = false;
|
|
119
|
+
this.log('โ
db.close');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return;
|
|
123
|
+
// this is too harsh
|
|
124
|
+
//process.exit(0);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async authAndRun(fn) {
|
|
128
|
+
return new Promise(async (resolve, reject) => {
|
|
129
|
+
let { state, saveCreds } = await useMultiFileAuthState(this.dirname + '/' + this.authFolder);
|
|
130
|
+
this.sock = makeWASocket({
|
|
131
|
+
auth: state,
|
|
132
|
+
logger: P(
|
|
133
|
+
{
|
|
134
|
+
level: 'silent' // or info
|
|
135
|
+
},
|
|
136
|
+
P.destination(2)
|
|
137
|
+
),
|
|
138
|
+
syncFullHistory: true
|
|
139
|
+
});
|
|
140
|
+
/* this syncs on pairing / initial connection */
|
|
141
|
+
this.sock.ev.on('messaging-history.set', async obj => {
|
|
142
|
+
this.restartInactivityTimer();
|
|
143
|
+
//this.log(obj);
|
|
144
|
+
this.log('messaging-history.set');
|
|
145
|
+
await this.storeDataToDatabase(obj);
|
|
146
|
+
});
|
|
147
|
+
/* this syncs the diff for every subsequent connection */
|
|
148
|
+
this.sock.ev.on('messages.upsert', async obj => {
|
|
149
|
+
this.restartInactivityTimer();
|
|
150
|
+
//this.log(obj);
|
|
151
|
+
this.log('messages.upsert');
|
|
152
|
+
await this.storeDataToDatabase(obj);
|
|
153
|
+
});
|
|
154
|
+
/* ??? */
|
|
155
|
+
this.sock.ev.on('chats.upsert', async obj => {
|
|
156
|
+
this.restartInactivityTimer();
|
|
157
|
+
//this.log(obj);
|
|
158
|
+
this.log('chats.upsert');
|
|
159
|
+
await this.storeDataToDatabase(obj);
|
|
160
|
+
});
|
|
161
|
+
this.sock.ev.on('creds.update', saveCreds);
|
|
162
|
+
this.sock.ev.on('connection.update', async update => {
|
|
163
|
+
let { connection, lastDisconnect, qr } = update;
|
|
164
|
+
let statusCode = lastDisconnect?.error?.output?.statusCode;
|
|
165
|
+
this.log(connection);
|
|
166
|
+
|
|
167
|
+
if (qr) {
|
|
168
|
+
this.isFirstRun = true;
|
|
169
|
+
// increase inactivity timer on pairing
|
|
170
|
+
this.restartInactivityTimer();
|
|
171
|
+
this.setInactivityTimeMax(60);
|
|
172
|
+
if (!this.isMcp) {
|
|
173
|
+
//let code = await QRCode.toString(qr, { type: 'utf8' });
|
|
174
|
+
//console.log(code);
|
|
175
|
+
let code = await this.sock.requestPairingCode(this.formatNumber(this.args.device));
|
|
176
|
+
// format code XXXXXXX => XXXX-XXXX
|
|
177
|
+
code = code.match(/.{1,4}/g).join('-');
|
|
178
|
+
console.log('Bitte verknรผpfe das neue Gerรคt und gib diesen Code ein:');
|
|
179
|
+
console.log(code);
|
|
180
|
+
this.write({ success: false, message: 'pairing_code_required', data: code });
|
|
181
|
+
}
|
|
182
|
+
if (this.isMcp) {
|
|
183
|
+
resolve({
|
|
184
|
+
content: [{ type: 'text', text: 'QR Code muss gescannt werden.' }]
|
|
185
|
+
});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
if (connection === 'close') {
|
|
190
|
+
if (this.isMcp === false) {
|
|
191
|
+
// reconnect after pairing (needed!)
|
|
192
|
+
if (statusCode === DisconnectReason.restartRequired) {
|
|
193
|
+
// again: reset inactivity timer after pairing
|
|
194
|
+
this.restartInactivityTimer();
|
|
195
|
+
this.setInactivityTimeMax(this.inactivityTimeMaxOrig);
|
|
196
|
+
this.log('โ ๏ธclose: reconnect');
|
|
197
|
+
resolve(await this.authAndRun(fn));
|
|
198
|
+
return;
|
|
199
|
+
} else if (statusCode === 401) {
|
|
200
|
+
this.log('โ ๏ธclose: 2');
|
|
201
|
+
if (this.resetFolder() === true) {
|
|
202
|
+
console.log('reset authentication. try again!');
|
|
203
|
+
}
|
|
204
|
+
resolve(await this.authAndRun(fn));
|
|
205
|
+
return;
|
|
206
|
+
} else {
|
|
207
|
+
this.log('โ ๏ธclose: 3');
|
|
208
|
+
resolve();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (connection === 'open') {
|
|
215
|
+
resolve(await fn());
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
initExitHooks() {
|
|
223
|
+
process.on('uncaughtException', async (error, origin) => {
|
|
224
|
+
this.log('uncaughtException');
|
|
225
|
+
await this.endSession();
|
|
226
|
+
process.exit(1);
|
|
227
|
+
});
|
|
228
|
+
process.on('unhandledRejection', async (reason, promise) => {
|
|
229
|
+
this.log('unhandledRejection');
|
|
230
|
+
this.log(JSON.stringify(reason, null, 2));
|
|
231
|
+
await this.endSession();
|
|
232
|
+
process.exit(1);
|
|
233
|
+
});
|
|
234
|
+
process.on('SIGINT', async () => {
|
|
235
|
+
this.log('SIGINT');
|
|
236
|
+
await this.endSession();
|
|
237
|
+
process.exit(0);
|
|
238
|
+
});
|
|
239
|
+
process.on('SIGTERM', async () => {
|
|
240
|
+
this.log('SIGTERM');
|
|
241
|
+
await this.endSession();
|
|
242
|
+
process.exit(0);
|
|
243
|
+
});
|
|
244
|
+
process.on('exit', code => {
|
|
245
|
+
this.removeLocks();
|
|
246
|
+
this.log('final exit');
|
|
247
|
+
console.log('final exit');
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async awaitLock(name = null, file_based = true) {
|
|
252
|
+
if (file_based === false) {
|
|
253
|
+
// check if object has property and property is true
|
|
254
|
+
while (this.locks[name] === true) {
|
|
255
|
+
this.log('lock is present!!! awaiting ' + name);
|
|
256
|
+
await new Promise(resolve => setTimeout(() => resolve(), 1000));
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
while (fs.existsSync(this.dirname + '/whatsapp' + (name !== null ? '-' + name : '') + '.lock')) {
|
|
260
|
+
await new Promise(resolve => setTimeout(() => resolve(), 1000));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
setLock(name = null, file_based = true) {
|
|
267
|
+
if (file_based === false) {
|
|
268
|
+
this.locks[name] = true;
|
|
269
|
+
this.log('set lock ' + name);
|
|
270
|
+
} else {
|
|
271
|
+
if (!fs.existsSync(this.dirname + '/whatsapp' + (name !== null ? '-' + name : '') + '.lock')) {
|
|
272
|
+
fs.writeFileSync(this.dirname + '/whatsapp' + (name !== null ? '-' + name : '') + '.lock', '');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
removeLock(name = null, file_based = true) {
|
|
278
|
+
if (file_based === false) {
|
|
279
|
+
this.locks[name] = false;
|
|
280
|
+
this.log('remove lock ' + name);
|
|
281
|
+
} else {
|
|
282
|
+
if (fs.existsSync(this.dirname + '/whatsapp' + (name !== null ? '-' + name : '') + '.lock')) {
|
|
283
|
+
fs.rmSync(this.dirname + '/whatsapp' + (name !== null ? '-' + name : '') + '.lock', { force: true });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
removeLocks() {
|
|
289
|
+
this.locks = {};
|
|
290
|
+
let files = fs.readdirSync(this.dirname);
|
|
291
|
+
for (let files__value of files) {
|
|
292
|
+
if (files__value.endsWith('.lock')) {
|
|
293
|
+
this.log('remove lock ' + files__value);
|
|
294
|
+
fs.rmSync(this.dirname + '/' + files__value, { force: true });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
initInactivityTimer() {
|
|
300
|
+
this.inactivityTimeCur = 0;
|
|
301
|
+
this.inactivityTimeInterval = setInterval(() => {
|
|
302
|
+
this.inactivityTimeCur++;
|
|
303
|
+
this.log(this.inactivityTimeCur + '/' + this.inactivityTimeMax);
|
|
304
|
+
if (this.inactivityTimeStatus === false && this.inactivityTimeCur >= this.inactivityTimeMax) {
|
|
305
|
+
if (this.inactivityTimeInterval) {
|
|
306
|
+
clearInterval(this.inactivityTimeInterval);
|
|
307
|
+
}
|
|
308
|
+
this.log('No new messages!');
|
|
309
|
+
this.inactivityTimeStatus = true;
|
|
310
|
+
}
|
|
311
|
+
}, 1000);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
restartInactivityTimer() {
|
|
315
|
+
this.inactivityTimeCur = 0;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
setInactivityTimeMax(s) {
|
|
319
|
+
this.inactivityTimeMax = s;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async awaitInactivityTimer() {
|
|
323
|
+
while (this.inactivityTimeStatus === false) {
|
|
324
|
+
await new Promise(resolve => setTimeout(() => resolve(), 1000));
|
|
325
|
+
}
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async fetchMessages() {
|
|
330
|
+
// wait for inactivity
|
|
331
|
+
await this.awaitInactivityTimer();
|
|
332
|
+
|
|
333
|
+
// fetch from database
|
|
334
|
+
let messages = this.db
|
|
335
|
+
.prepare(
|
|
336
|
+
`
|
|
337
|
+
SELECT id, \`from\`, \`to\`, content, media_filename, timestamp
|
|
338
|
+
FROM messages
|
|
339
|
+
ORDER BY timestamp DESC
|
|
340
|
+
`
|
|
341
|
+
)
|
|
342
|
+
.all();
|
|
343
|
+
this.write({ success: true, message: 'messages_fetched', data: messages });
|
|
344
|
+
return {
|
|
345
|
+
content: [
|
|
346
|
+
{
|
|
347
|
+
type: 'text',
|
|
348
|
+
text: 'Fetched ' + messages.length + ' messages from database'
|
|
349
|
+
}
|
|
350
|
+
],
|
|
351
|
+
structuredContent: messages
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
formatMessage(message) {
|
|
356
|
+
if (message === null || message === undefined || message === '') {
|
|
357
|
+
return message;
|
|
358
|
+
}
|
|
359
|
+
// replace <br> with real line breaks
|
|
360
|
+
message = message.replace(/<br\s*\/?>/gi, '\n');
|
|
361
|
+
// replace " </x>" with "</x> "
|
|
362
|
+
message = message.replace(/ (\<\/[a-z]+\>)/gi, '$1 ');
|
|
363
|
+
// replace "<x> " with " <x>"
|
|
364
|
+
message = message.replace(/(\<[a-z]+\>) /gi, ' $1');
|
|
365
|
+
// replace <strong></strong> with "*"
|
|
366
|
+
message = message.replace(/<strong>(.*?)<\/strong>/gi, '*$1*');
|
|
367
|
+
// replace <em></em> with "_"
|
|
368
|
+
message = message.replace(/<em>(.*?)<\/em>/gi, '_$1_');
|
|
369
|
+
// replace <i></i> with "_"
|
|
370
|
+
message = message.replace(/<i>(.*?)<\/i>/gi, '_$1_');
|
|
371
|
+
// replace html entities
|
|
372
|
+
message = message.replace(/"/g, '"');
|
|
373
|
+
message = message.replace(/'/g, "'");
|
|
374
|
+
message = message.replace(/&/g, '&');
|
|
375
|
+
message = message.replace(/</g, '<');
|
|
376
|
+
message = message.replace(/>/g, '>');
|
|
377
|
+
// strip all other tags
|
|
378
|
+
message = message.replace(/<\/?[^>]+(>|$)/g, '');
|
|
379
|
+
// remove all other html entities
|
|
380
|
+
message = message.replace(/&[^;]+;/g, '');
|
|
381
|
+
return message;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
getAttachmentObj(attachment) {
|
|
385
|
+
let ext = (attachment.split('.').pop() || '').toLowerCase();
|
|
386
|
+
if (ext === 'jpg' || ext === 'jpeg' || ext === 'png') {
|
|
387
|
+
return {
|
|
388
|
+
image: fs.readFileSync(attachment)
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
let map = {
|
|
392
|
+
pdf: 'application/pdf',
|
|
393
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
394
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
395
|
+
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
396
|
+
txt: 'text/plain'
|
|
397
|
+
},
|
|
398
|
+
mime_type = map[ext] || 'application/octet-stream';
|
|
399
|
+
return {
|
|
400
|
+
document: fs.readFileSync(attachment),
|
|
401
|
+
fileName: attachment.split('/').splice(-1),
|
|
402
|
+
mimetype: mime_type
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async sendMessageToUser(number = null, message = null, attachments = null) {
|
|
407
|
+
let jid = this.formatNumber(number) + '@s.whatsapp.net',
|
|
408
|
+
msgResponse = [];
|
|
409
|
+
msgResponse.push(await this.sock.sendMessage(jid, { text: this.formatMessage(message) }));
|
|
410
|
+
//this.log(attachments);
|
|
411
|
+
if (attachments !== null && attachments.length > 0) {
|
|
412
|
+
for (let attachments__value of attachments) {
|
|
413
|
+
msgResponse.push(await this.sock.sendMessage(jid, this.getAttachmentObj(attachments__value)));
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
this.write({ success: true, message: 'message_user_sent', data: msgResponse });
|
|
417
|
+
//this.log(msgResponse);
|
|
418
|
+
return {
|
|
419
|
+
content: [{ type: 'text', text: JSON.stringify(msgResponse, null, 2) }],
|
|
420
|
+
structuredContent: msgResponse
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async sendMessageToGroup(name = null, message = null, attachments = null) {
|
|
425
|
+
let jid = null,
|
|
426
|
+
msgResponse = [],
|
|
427
|
+
groups = await this.sock.groupFetchAllParticipating();
|
|
428
|
+
for (let groups__value of Object.values(groups)) {
|
|
429
|
+
if (groups__value.subject === name) {
|
|
430
|
+
jid = groups__value.id;
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (jid !== null) {
|
|
435
|
+
msgResponse.push(await this.sock.sendMessage(jid, { text: this.formatMessage(message) }));
|
|
436
|
+
if (attachments !== null && attachments.length > 0) {
|
|
437
|
+
for (let attachments__value of attachments) {
|
|
438
|
+
msgResponse.push(await this.sock.sendMessage(jid, this.getAttachmentObj(attachments__value)));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
this.write({ success: true, message: 'message_group_sent', data: msgResponse });
|
|
443
|
+
//this.log(msgResponse);
|
|
444
|
+
return {
|
|
445
|
+
content: [{ type: 'text', text: JSON.stringify(msgResponse, null, 2) }],
|
|
446
|
+
structuredContent: msgResponse
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
registerMcp() {
|
|
451
|
+
let server = new McpServer({
|
|
452
|
+
name: 'whatsapp-mcp',
|
|
453
|
+
version: '1.0.0'
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
server.registerTool(
|
|
457
|
+
'fetch_messages',
|
|
458
|
+
{
|
|
459
|
+
title: 'Fetch messages',
|
|
460
|
+
description: 'Fetch all messages',
|
|
461
|
+
inputSchema: {},
|
|
462
|
+
outputSchema: { result: z.string().describe('Result of fetching messages') }
|
|
463
|
+
},
|
|
464
|
+
async ({}) => {
|
|
465
|
+
let response = await this.authAndRun(() => fetchMessages());
|
|
466
|
+
return response;
|
|
467
|
+
}
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
server.registerTool(
|
|
471
|
+
'send_group',
|
|
472
|
+
{
|
|
473
|
+
title: 'Send message to group',
|
|
474
|
+
description: 'Send message to group',
|
|
475
|
+
inputSchema: { group: z.string().describe('Group name'), text: z.string().describe('Message text') },
|
|
476
|
+
outputSchema: { result: z.string().describe('Result of sending message') }
|
|
477
|
+
},
|
|
478
|
+
async ({ group, text }) => {
|
|
479
|
+
this.log([group, text]);
|
|
480
|
+
let response = await this.authAndRun(() => sendMessageToGroup(group, text));
|
|
481
|
+
return response;
|
|
482
|
+
}
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
server.registerTool(
|
|
486
|
+
'send_message',
|
|
487
|
+
{
|
|
488
|
+
title: 'Send message to person',
|
|
489
|
+
description: 'Send message to person',
|
|
490
|
+
inputSchema: {
|
|
491
|
+
number: z.string().describe('Person number'),
|
|
492
|
+
text: z.string().describe('Message text')
|
|
493
|
+
},
|
|
494
|
+
outputSchema: { result: z.string().describe('Result of sending message') }
|
|
495
|
+
},
|
|
496
|
+
async ({ number, text }) => {
|
|
497
|
+
this.log([number, text]);
|
|
498
|
+
let response = await this.authAndRun(() => sendMessageToPerson(number, text));
|
|
499
|
+
return response;
|
|
500
|
+
}
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
formatNumber(number) {
|
|
505
|
+
// replace leading zero with 49
|
|
506
|
+
number = number.replace(/^0+/, '49');
|
|
507
|
+
// remove non-digit characters
|
|
508
|
+
number = number.replace(/\D/g, '');
|
|
509
|
+
return number;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
parseArgs() {
|
|
513
|
+
let args = {};
|
|
514
|
+
let argv = process.argv.slice(2);
|
|
515
|
+
for (let i = 0; i < argv.length; i++) {
|
|
516
|
+
if (argv[i].startsWith('-')) {
|
|
517
|
+
let key = argv[i].replace(/^-+/, '').replace(/-/, '_'),
|
|
518
|
+
value = argv[i + 1] && !argv[i + 1].startsWith('-') ? argv[i + 1] : true;
|
|
519
|
+
if (key === 'attachments') {
|
|
520
|
+
value = value.split(',');
|
|
521
|
+
}
|
|
522
|
+
args[key] = value;
|
|
523
|
+
if (value !== true) i++;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return args;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
resetFolder() {
|
|
530
|
+
if (fs.existsSync(this.dirname + '/' + this.authFolder)) {
|
|
531
|
+
fs.rmSync(this.dirname + '/' + this.authFolder, { recursive: true, force: true });
|
|
532
|
+
}
|
|
533
|
+
if (fs.existsSync(this.dirname + '/' + this.dbPath)) {
|
|
534
|
+
if (this.db !== null) {
|
|
535
|
+
this.db.close();
|
|
536
|
+
this.dbIsOpen = false;
|
|
537
|
+
}
|
|
538
|
+
fs.rmSync(this.dirname + '/' + this.dbPath, { force: true });
|
|
539
|
+
this.initDatabase();
|
|
540
|
+
}
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
log(...args) {
|
|
545
|
+
if (this.logPath === undefined) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
let message = args.map(arg => (typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg)).join(' ');
|
|
549
|
+
let logLine = new Date().toISOString() + ' - ' + message + '\n';
|
|
550
|
+
fs.appendFileSync(this.dirname + '/' + this.logPath, logLine);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
write(msg) {
|
|
554
|
+
fs.writeFileSync(this.dirname + '/' + this.dataPath, JSON.stringify(msg));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
initDatabase() {
|
|
558
|
+
try {
|
|
559
|
+
this.db = new DatabaseSync(this.dirname + '/' + this.dbPath);
|
|
560
|
+
this.dbIsOpen = true;
|
|
561
|
+
this.db.exec(`
|
|
562
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
563
|
+
id TEXT PRIMARY KEY,
|
|
564
|
+
\`from\` TEXT,
|
|
565
|
+
\`to\` TEXT,
|
|
566
|
+
content TEXT,
|
|
567
|
+
media_data TEXT,
|
|
568
|
+
media_filename TEXT,
|
|
569
|
+
timestamp INTEGER
|
|
570
|
+
);
|
|
571
|
+
`);
|
|
572
|
+
} catch (error) {
|
|
573
|
+
this.log('โ Error initing database: ' + error.message + ' (code: ' + error.code + ')');
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async storeDataToDatabase(data) {
|
|
578
|
+
if (!data.messages || data.messages.length === 0) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (this.locks['db'] !== true) {
|
|
583
|
+
this.setLock('db', false);
|
|
584
|
+
} else {
|
|
585
|
+
await this.awaitLock('db', false);
|
|
586
|
+
this.setLock('db', false);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
//this.log(data.messages);
|
|
590
|
+
let count = 0,
|
|
591
|
+
length = data.messages.length;
|
|
592
|
+
|
|
593
|
+
// if db is closed in the meantime
|
|
594
|
+
if (this.dbIsOpen === false) {
|
|
595
|
+
this.initDatabase();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
try {
|
|
599
|
+
this.log('BEGIN TRANSACTION');
|
|
600
|
+
this.db.exec('BEGIN TRANSACTION');
|
|
601
|
+
let query = this.db.prepare(`
|
|
602
|
+
INSERT OR IGNORE INTO messages
|
|
603
|
+
(id, \`from\`, \`to\`, content, media_data, media_filename, timestamp)
|
|
604
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
605
|
+
`);
|
|
606
|
+
|
|
607
|
+
for (let messages__value of data.messages) {
|
|
608
|
+
let id = messages__value.key?.id,
|
|
609
|
+
chatId = messages__value.key?.remoteJid,
|
|
610
|
+
fromMe = messages__value.key?.fromMe ? 1 : 0,
|
|
611
|
+
timestamp = messages__value.messageTimestamp;
|
|
612
|
+
if (timestamp !== undefined && timestamp !== null) {
|
|
613
|
+
timestamp = Number(timestamp);
|
|
614
|
+
if (isNaN(timestamp)) {
|
|
615
|
+
timestamp = Math.floor(Date.now() / 1000);
|
|
616
|
+
}
|
|
617
|
+
} else {
|
|
618
|
+
timestamp = Math.floor(Date.now() / 1000);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
let from = null;
|
|
622
|
+
let to = null;
|
|
623
|
+
if (fromMe) {
|
|
624
|
+
from = this.args.device || 'me';
|
|
625
|
+
to = chatId;
|
|
626
|
+
} else if (chatId?.endsWith('@g.us')) {
|
|
627
|
+
if (messages__value?.participant) {
|
|
628
|
+
from = messages__value?.participant;
|
|
629
|
+
} else if (messages__value?.key?.participantAlt) {
|
|
630
|
+
from = messages__value?.key?.participantAlt;
|
|
631
|
+
}
|
|
632
|
+
to = chatId;
|
|
633
|
+
} else {
|
|
634
|
+
from = chatId;
|
|
635
|
+
to = this.args.device || 'me';
|
|
636
|
+
}
|
|
637
|
+
if (from) {
|
|
638
|
+
from = from.replace(/@.*$/, '');
|
|
639
|
+
}
|
|
640
|
+
if (to) {
|
|
641
|
+
to = to.replace(/@.*$/, '');
|
|
642
|
+
}
|
|
643
|
+
if (from === null || from === undefined || from === '') {
|
|
644
|
+
this.log('โmissing fromโ');
|
|
645
|
+
this.log(messages__value);
|
|
646
|
+
}
|
|
647
|
+
if (to === null || to === undefined || to === '') {
|
|
648
|
+
this.log('โmissing toโ');
|
|
649
|
+
this.log(messages__value);
|
|
650
|
+
}
|
|
651
|
+
// skip status messages
|
|
652
|
+
if (from === 'status') {
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
let content = null,
|
|
657
|
+
mediaFilename = null,
|
|
658
|
+
mediaData = null,
|
|
659
|
+
mediaBufferInput = null;
|
|
660
|
+
if (messages__value.message?.conversation) {
|
|
661
|
+
content = messages__value.message.conversation;
|
|
662
|
+
} else if (messages__value.message?.extendedTextMessage?.text) {
|
|
663
|
+
content = messages__value.message.extendedTextMessage.text;
|
|
664
|
+
} else if (messages__value.message?.imageMessage) {
|
|
665
|
+
content = messages__value.message.imageMessage.caption || null;
|
|
666
|
+
mediaFilename = id + '.jpg';
|
|
667
|
+
mediaBufferInput = messages__value;
|
|
668
|
+
} else if (messages__value.message?.stickerMessage) {
|
|
669
|
+
content = messages__value.message.stickerMessage.caption || null;
|
|
670
|
+
mediaFilename = id + '.webp';
|
|
671
|
+
mediaBufferInput = messages__value;
|
|
672
|
+
} else if (messages__value.message?.videoMessage) {
|
|
673
|
+
content = messages__value.message.videoMessage.caption || null;
|
|
674
|
+
mediaFilename = id + '.mp4';
|
|
675
|
+
mediaBufferInput = messages__value;
|
|
676
|
+
} else if (messages__value.message?.documentMessage) {
|
|
677
|
+
content = messages__value.message.documentMessage.caption || null;
|
|
678
|
+
mediaFilename = messages__value.message.documentMessage.fileName || id + '.bin';
|
|
679
|
+
mediaBufferInput = messages__value;
|
|
680
|
+
} else if (messages__value.message?.documentWithCaptionMessage) {
|
|
681
|
+
content =
|
|
682
|
+
messages__value.message.documentWithCaptionMessage.message.documentMessage.caption || null;
|
|
683
|
+
mediaFilename =
|
|
684
|
+
messages__value.message.documentWithCaptionMessage.message.documentMessage.fileName ||
|
|
685
|
+
id + '.bin';
|
|
686
|
+
mediaBufferInput = messages__value.message.documentWithCaptionMessage;
|
|
687
|
+
} else if (messages__value.message?.audioMessage) {
|
|
688
|
+
content = messages__value.message.audioMessage.caption || null;
|
|
689
|
+
mediaFilename = id + '.ogg';
|
|
690
|
+
mediaBufferInput = messages__value;
|
|
691
|
+
} else {
|
|
692
|
+
continue;
|
|
693
|
+
/*
|
|
694
|
+
content = '[Unsupported message type]';
|
|
695
|
+
this.log('[Unsupported message type]');
|
|
696
|
+
this.log(messages__value);
|
|
697
|
+
*/
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// don't sync media on first run
|
|
701
|
+
if (this.isFirstRun) {
|
|
702
|
+
if (content === null || content === '') {
|
|
703
|
+
content = '[Media message not downloaded on first run]';
|
|
704
|
+
}
|
|
705
|
+
mediaFilename = null;
|
|
706
|
+
mediaBufferInput = null;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (mediaBufferInput !== null) {
|
|
710
|
+
try {
|
|
711
|
+
let buffer = await downloadMediaMessage(
|
|
712
|
+
mediaBufferInput,
|
|
713
|
+
'buffer',
|
|
714
|
+
{},
|
|
715
|
+
{
|
|
716
|
+
logger: P({ level: 'silent' }),
|
|
717
|
+
reuploadRequest: this.sock.updateMediaMessage
|
|
718
|
+
}
|
|
719
|
+
);
|
|
720
|
+
mediaData = buffer.toString('base64');
|
|
721
|
+
this.log('โ
Downloaded media ' + mediaFilename);
|
|
722
|
+
} catch (error) {
|
|
723
|
+
mediaData = mediaBufferInput?.url || null;
|
|
724
|
+
this.log(
|
|
725
|
+
'โ
Failed to download media: ' + error.message + '. Store URL ' + mediaData + ' instead.'
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
this.restartInactivityTimer();
|
|
731
|
+
query.run(id, from, to, content, mediaData, mediaFilename, timestamp);
|
|
732
|
+
|
|
733
|
+
count++;
|
|
734
|
+
if (length < 100 || count % 100 === 0) {
|
|
735
|
+
this.log('syncing progress: ' + Math.round((count / length) * 100, 2) + '%');
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
this.db.exec('COMMIT');
|
|
739
|
+
this.log('END TRANSACTION');
|
|
740
|
+
if (count > 0) {
|
|
741
|
+
this.log('Stored ' + count + ' new messages to database (' + length + ' total received)');
|
|
742
|
+
}
|
|
743
|
+
} catch (error) {
|
|
744
|
+
this.log('โ Error storing message: ' + error.message + ' (code: ' + error.code + ')');
|
|
745
|
+
try {
|
|
746
|
+
this.db.exec('ROLLBACK');
|
|
747
|
+
this.log('END TRANSACTION');
|
|
748
|
+
this.log('โ
Transaction rolled back');
|
|
749
|
+
} catch (rollbackError) {
|
|
750
|
+
this.log('โ Rollback failed: ' + rollbackError.message);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
this.removeLock('db', false);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
let wa = new WhatsApp();
|
|
759
|
+
wa.init();
|