@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.
Files changed (3) hide show
  1. package/README.MD +101 -0
  2. package/package.json +28 -0
  3. 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(/&quot;/g, '"');
373
+ message = message.replace(/&#39;/g, "'");
374
+ message = message.replace(/&amp;/g, '&');
375
+ message = message.replace(/&lt;/g, '<');
376
+ message = message.replace(/&gt;/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();