signal-sdk 0.0.6 → 0.0.7

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/dist/SignalBot.js DELETED
@@ -1,811 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.SignalBot = void 0;
37
- const events_1 = require("events");
38
- const SignalCli_1 = require("./SignalCli");
39
- const fs = __importStar(require("fs"));
40
- const path = __importStar(require("path"));
41
- const https = __importStar(require("https"));
42
- const http = __importStar(require("http"));
43
- class SignalBot extends events_1.EventEmitter {
44
- constructor(config, signalCliPath) {
45
- super();
46
- this.commands = new Map();
47
- this.isRunning = false;
48
- this.botGroupId = null;
49
- this.userCooldowns = new Map();
50
- this.actionQueue = [];
51
- this.isProcessingQueue = false;
52
- this.incomingMessageBuffer = [];
53
- this.config = {
54
- phoneNumber: config.phoneNumber,
55
- admins: config.admins || [],
56
- settings: {
57
- commandPrefix: config.settings?.commandPrefix || '/',
58
- autoReact: config.settings?.autoReact ?? false,
59
- logMessages: config.settings?.logMessages ?? true,
60
- welcomeNewMembers: config.settings?.welcomeNewMembers ?? true,
61
- cooldownSeconds: config.settings?.cooldownSeconds || 2,
62
- maxMessageLength: config.settings?.maxMessageLength || 1000,
63
- },
64
- };
65
- if (config.group) {
66
- this.config.group = {
67
- name: config.group.name,
68
- description: config.group.description || '- Group managed by Signal Bot',
69
- createIfNotExists: config.group.createIfNotExists ?? true,
70
- initialMembers: config.group.initialMembers || [],
71
- avatar: config.group.avatar, // Add the missing avatar property
72
- };
73
- }
74
- this.signalCli = new SignalCli_1.SignalCli(signalCliPath, this.config.phoneNumber);
75
- this.stats = {
76
- messagesReceived: 0,
77
- commandsExecuted: 0,
78
- startTime: Date.now(),
79
- lastActivity: Date.now(),
80
- activeUsers: 0
81
- };
82
- this.setupDefaultCommands();
83
- }
84
- /**
85
- * Downloads an image from URL to a temporary file
86
- * @param imageUrl URL of the image to download
87
- * @returns Path to the temporary file
88
- */
89
- async downloadImage(imageUrl) {
90
- return new Promise((resolve, reject) => {
91
- const tempFileName = `bot_avatar_${Date.now()}.jpg`;
92
- const tempFilePath = path.join(process.cwd(), tempFileName);
93
- const file = fs.createWriteStream(tempFilePath);
94
- const client = imageUrl.startsWith('https:') ? https : http;
95
- client.get(imageUrl, (response) => {
96
- if (response.statusCode !== 200) {
97
- fs.unlink(tempFilePath, () => { }); // Clean up on error
98
- reject(new Error(`Failed to download image: ${response.statusCode}`));
99
- return;
100
- }
101
- response.pipe(file);
102
- file.on('finish', () => {
103
- file.close();
104
- resolve(tempFilePath);
105
- });
106
- file.on('error', (err) => {
107
- fs.unlink(tempFilePath, () => { }); // Clean up on error
108
- reject(err);
109
- });
110
- }).on('error', (err) => {
111
- fs.unlink(tempFilePath, () => { }); // Clean up on error
112
- reject(err);
113
- });
114
- });
115
- }
116
- /**
117
- * Downloads an image from URL for commands (like NASA images)
118
- * @param imageUrl URL of the image to download
119
- * @param prefix Optional prefix for the temp file name
120
- * @returns Path to the temporary file
121
- */
122
- async downloadImageFromUrl(imageUrl, prefix = 'bot_image') {
123
- return new Promise((resolve, reject) => {
124
- const downloadWithRedirect = (url, maxRedirects = 5) => {
125
- const client = url.startsWith('https:') ? https : http;
126
- client.get(url, (response) => {
127
- // Handle redirections (3xx status codes)
128
- if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400) {
129
- if (maxRedirects <= 0) {
130
- reject(new Error(`Too many redirects for image: ${url}`));
131
- return;
132
- }
133
- const redirectUrl = response.headers.location;
134
- if (!redirectUrl) {
135
- reject(new Error(`Redirect without location header: ${response.statusCode}`));
136
- return;
137
- }
138
- // Resolve relative URLs
139
- const finalUrl = redirectUrl.startsWith('http') ? redirectUrl : new URL(redirectUrl, url).href;
140
- this.log(`🔄 Following redirect to: ${finalUrl}`, 'DEBUG');
141
- downloadWithRedirect(finalUrl, maxRedirects - 1);
142
- return;
143
- }
144
- // Handle non-success status codes
145
- if (response.statusCode !== 200) {
146
- reject(new Error(`Failed to download image: ${response.statusCode}`));
147
- return;
148
- }
149
- // Success - create file and pipe response
150
- const urlObj = new URL(url);
151
- const extension = path.extname(urlObj.pathname) || '.jpg';
152
- const tempFileName = `${prefix}_${Date.now()}${extension}`;
153
- const tempFilePath = path.join(process.cwd(), tempFileName);
154
- const file = fs.createWriteStream(tempFilePath);
155
- response.pipe(file);
156
- file.on('finish', () => {
157
- file.close();
158
- resolve(tempFilePath);
159
- });
160
- file.on('error', (err) => {
161
- fs.unlink(tempFilePath, () => { }); // Clean up on error
162
- reject(err);
163
- });
164
- }).on('error', (err) => {
165
- reject(err);
166
- });
167
- };
168
- // Start the download with redirect handling
169
- downloadWithRedirect(imageUrl);
170
- });
171
- }
172
- /**
173
- * Sends a message with downloaded image attachment
174
- * @param recipient Recipient to send to
175
- * @param message Text message to send
176
- * @param imageUrl URL of the image to download and send
177
- * @param prefix Optional prefix for the temp file name
178
- */
179
- async sendMessageWithImage(recipient, message, imageUrl, prefix = 'bot_image') {
180
- let tempFilePath = null;
181
- try {
182
- this.log(`- Downloading image from ${imageUrl}...`, 'DEBUG');
183
- tempFilePath = await this.downloadImageFromUrl(imageUrl, prefix);
184
- this.log(`- Image downloaded to: ${tempFilePath}`, 'DEBUG');
185
- // Add to the action queue with cleanup info
186
- this.actionQueue.push({
187
- type: 'sendMessageWithAttachment',
188
- recipient,
189
- message,
190
- attachments: [tempFilePath],
191
- cleanup: [tempFilePath] // Mark files for cleanup after sending
192
- });
193
- this.processActionQueue();
194
- }
195
- catch (error) {
196
- this.log(`ERROR: Failed to download and send image: ${error?.message}`, 'ERROR');
197
- // Clean up on download error
198
- if (tempFilePath) {
199
- this.cleanupTempFile(tempFilePath);
200
- }
201
- // Fallback to text message with URL
202
- await this.sendMessage(recipient, `${message}\n\n- Image: ${imageUrl}`);
203
- }
204
- }
205
- /**
206
- * Cleans up a temporary file
207
- * @param filePath Path to the file to delete
208
- */
209
- cleanupTempFile(filePath) {
210
- try {
211
- if (fs.existsSync(filePath)) {
212
- fs.unlinkSync(filePath);
213
- this.log(`- Cleaned up temporary file: ${filePath}`, 'DEBUG');
214
- }
215
- }
216
- catch (error) {
217
- this.log(`WARNING: Could not cleanup temp file ${filePath}: ${error?.message}`, 'DEBUG');
218
- }
219
- }
220
- /**
221
- * Processes group avatar configuration
222
- * @param avatar Avatar configuration (URL, file path, or base64)
223
- * @returns Path to the avatar file or null if no avatar
224
- */
225
- async processGroupAvatar(avatar) {
226
- if (!avatar) {
227
- return null;
228
- }
229
- // Check if it's a URL
230
- if (avatar.startsWith('http://') || avatar.startsWith('https://')) {
231
- try {
232
- this.log(`- Downloading group avatar from URL...`, 'INFO');
233
- const tempPath = await this.downloadImage(avatar);
234
- this.log(`- Avatar downloaded to: ${tempPath}`, 'DEBUG');
235
- return tempPath;
236
- }
237
- catch (error) {
238
- this.log(`ERROR: Failed to download avatar: ${error?.message}`, 'ERROR');
239
- return null;
240
- }
241
- }
242
- // Check if it's a file path
243
- if (fs.existsSync(avatar)) {
244
- this.log(`- Using local avatar file: ${avatar}`, 'INFO');
245
- return avatar;
246
- }
247
- // If it's base64 or other format, save it as a temporary file
248
- if (avatar.startsWith('data:image/')) {
249
- try {
250
- const base64Data = avatar.split(',')[1];
251
- const tempFileName = `bot_avatar_${Date.now()}.jpg`;
252
- const tempFilePath = path.join(process.cwd(), tempFileName);
253
- fs.writeFileSync(tempFilePath, base64Data, 'base64');
254
- this.log(`- Saved base64 avatar to: ${tempFilePath}`, 'DEBUG');
255
- return tempFilePath;
256
- }
257
- catch (error) {
258
- this.log(`ERROR: Failed to process base64 avatar: ${error?.message}`, 'ERROR');
259
- return null;
260
- }
261
- }
262
- this.log(`WARNING: Unsupported avatar format: ${avatar.substring(0, 50)}...`, 'WARN');
263
- return null;
264
- }
265
- /**
266
- * Adds a custom command to the bot
267
- */
268
- addCommand(command) {
269
- this.commands.set(command.name.toLowerCase(), command);
270
- }
271
- /**
272
- * Removes a command from the bot
273
- */
274
- removeCommand(name) {
275
- return this.commands.delete(name.toLowerCase());
276
- }
277
- /**
278
- * Gets all available commands
279
- */
280
- getCommands() {
281
- return Array.from(this.commands.values());
282
- }
283
- /**
284
- * Starts the bot
285
- */
286
- async start() {
287
- try {
288
- this.log('- Starting Signal Bot...', 'INFO');
289
- await this.signalCli.connect();
290
- this.log('- Connected to signal-cli in JSON-RPC mode');
291
- // Check that the bot is linked
292
- const devices = await this.signalCli.listDevices();
293
- if (devices.length === 0) {
294
- throw new Error('No device found. Please link the bot first.');
295
- }
296
- this.log(`- Bot linked with ${devices.length} device(s)`);
297
- // Set up or find the bot group
298
- if (this.config.group && this.config.group.createIfNotExists) {
299
- await this.setupBotGroup();
300
- }
301
- // Set up event handlers
302
- this.setupEventHandlers();
303
- this.isRunning = true;
304
- this.log('- Signal Bot started successfully!');
305
- this.emit('ready');
306
- // Send welcome message to admins
307
- await this.sendWelcomeMessage();
308
- }
309
- catch (error) {
310
- this.log(`ERROR: Error during startup: ${error?.message || error}`, 'ERROR');
311
- throw error;
312
- }
313
- }
314
- /**
315
- * Stops the bot
316
- */
317
- async stop() {
318
- this.log('- Stopping Signal Bot...');
319
- this.isRunning = false;
320
- this.signalCli.disconnect();
321
- this.emit('stopped');
322
- this.log('- Bot stopped');
323
- }
324
- async gracefulShutdown() {
325
- this.log('- Gracefully shutting down Signal Bot...');
326
- this.isRunning = false;
327
- try {
328
- await this.signalCli.gracefulShutdown();
329
- this.log('- Signal Bot shutdown completed gracefully');
330
- }
331
- catch (error) {
332
- this.log(`ERROR: Error during graceful shutdown: ${error?.message || error}`, 'ERROR');
333
- }
334
- this.emit('stopped');
335
- }
336
- /**
337
- * Sends a message
338
- */
339
- async sendMessage(recipient, message) {
340
- if (message.length > (this.config.settings.maxMessageLength || 1000)) {
341
- message = message.substring(0, (this.config.settings.maxMessageLength || 1000) - 3) + '...';
342
- }
343
- // Add to the action queue
344
- this.actionQueue.push({ type: 'sendMessage', recipient, message });
345
- this.processActionQueue();
346
- }
347
- /**
348
- * Sends a reaction to a message
349
- */
350
- async sendReaction(recipient, targetAuthor, targetTimestamp, emoji) {
351
- // Add to the action queue
352
- this.actionQueue.push({ type: 'sendReaction', recipient, targetAuthor, targetTimestamp, emoji });
353
- this.processActionQueue();
354
- }
355
- /**
356
- * Sends a message with file attachments
357
- */
358
- async sendMessageWithAttachment(recipient, message, attachments, cleanup) {
359
- if (message.length > (this.config.settings.maxMessageLength || 1000)) {
360
- message = message.substring(0, (this.config.settings.maxMessageLength || 1000) - 3) + '...';
361
- }
362
- // Add to the action queue
363
- this.actionQueue.push({ type: 'sendMessageWithAttachment', recipient, message, attachments, cleanup });
364
- this.processActionQueue();
365
- }
366
- /**
367
- * Gets bot statistics
368
- */
369
- getStats() {
370
- return {
371
- ...this.stats,
372
- activeUsers: this.userCooldowns.size
373
- };
374
- }
375
- /**
376
- * Checks if a user is an admin
377
- */
378
- isAdmin(phoneNumber) {
379
- return this.config.admins.includes(phoneNumber);
380
- }
381
- /**
382
- * Gets the bot group ID
383
- */
384
- getBotGroupId() {
385
- return this.botGroupId;
386
- }
387
- /**
388
- * Gets the underlying SignalCli instance for advanced operations
389
- */
390
- getSignalCli() {
391
- return this.signalCli;
392
- }
393
- setupDefaultCommands() {
394
- // Help command
395
- this.addCommand({
396
- name: 'help',
397
- description: 'Displays available commands',
398
- handler: async (message, args) => {
399
- const userCommands = Array.from(this.commands.values())
400
- .filter(cmd => !cmd.adminOnly || message.isFromAdmin)
401
- .map(cmd => `${this.config.settings.commandPrefix}${cmd.name} - ${cmd.description}`)
402
- .join('\n');
403
- return `Signal Bot Commands\n\n${userCommands}\n\n${message.isFromAdmin ? '| You have admin privileges' : ''}`;
404
- }
405
- });
406
- // Stats command
407
- this.addCommand({
408
- name: 'stats',
409
- description: 'Displays bot statistics',
410
- handler: async () => {
411
- const stats = this.getStats();
412
- const uptime = this.formatUptime(Date.now() - stats.startTime);
413
- return `Bot Statistics\n\n` +
414
- `1. Messages Received: ${stats.messagesReceived}\n` +
415
- `2. Commands Executed: ${stats.commandsExecuted}\n` +
416
- `3. Uptime: ${uptime}\n` +
417
- `4. Active Users: ${stats.activeUsers}`;
418
- }
419
- });
420
- // Ping command
421
- this.addCommand({
422
- name: 'ping',
423
- description: 'Tests bot responsiveness',
424
- handler: async (message) => {
425
- const responseTime = Date.now() - message.timestamp;
426
- return `Pong! Response time: ${responseTime}ms`;
427
- }
428
- });
429
- // Info command (admin only)
430
- this.addCommand({
431
- name: 'info',
432
- description: 'Detailed bot information (admin)',
433
- adminOnly: true,
434
- handler: async () => {
435
- return `Bot Information\n\n` +
436
- `- Number: ${this.config.phoneNumber}\n` +
437
- `- Group: ${this.config.group ? this.config.group.name : 'N/A'}\n` +
438
- `- Admins: ${this.config.admins.length}\n` +
439
- `- Commands: ${this.commands.size}\n` +
440
- `- Prefix: ${this.config.settings.commandPrefix}`;
441
- }
442
- });
443
- }
444
- async setupBotGroup() {
445
- if (!this.config.group) {
446
- return;
447
- }
448
- let avatarPath = null;
449
- let isTemporaryAvatar = false;
450
- try {
451
- // Process avatar if configured
452
- if (this.config.group.avatar) {
453
- avatarPath = await this.processGroupAvatar(this.config.group.avatar);
454
- isTemporaryAvatar = avatarPath !== this.config.group.avatar; // It's temporary if it was processed
455
- }
456
- // Search for an existing group
457
- const groups = await this.signalCli.listGroups();
458
- const existingGroup = groups.find(group => group.name === this.config.group.name && group.isMember);
459
- if (existingGroup) {
460
- // Try different possible field names for the group ID
461
- const groupData = existingGroup;
462
- const possibleGroupId = existingGroup.groupId || groupData.id || groupData.Id;
463
- this.botGroupId = possibleGroupId;
464
- this.log(`- Existing group found: ${this.config.group.name} (${possibleGroupId})`);
465
- // Check if all admins are in the group and add them if not
466
- const currentMembers = existingGroup.members?.map((m) => m.number || m) || [];
467
- const missingAdmins = this.config.admins.filter(admin => !currentMembers.includes(admin));
468
- // Prepare update options
469
- const updateOptions = {};
470
- if (missingAdmins.length > 0) {
471
- updateOptions.addMembers = missingAdmins;
472
- }
473
- if (avatarPath) {
474
- updateOptions.avatar = avatarPath;
475
- }
476
- // Always apply avatar if configured, even for existing groups
477
- if (Object.keys(updateOptions).length > 0 && this.botGroupId) {
478
- try {
479
- if (missingAdmins.length > 0) {
480
- this.log(`- Adding missing admins to group: ${missingAdmins.join(', ')}`, 'INFO');
481
- }
482
- if (avatarPath) {
483
- this.log(`- Setting group avatar...`, 'INFO');
484
- }
485
- await this.signalCli.updateGroup(this.botGroupId, updateOptions);
486
- if (missingAdmins.length > 0) {
487
- this.log(`- Successfully added admins to the group`, 'INFO');
488
- }
489
- if (avatarPath) {
490
- this.log(`- Group avatar set successfully`, 'INFO');
491
- }
492
- // Send welcome message to the group only if admins were added
493
- if (missingAdmins.length > 0) {
494
- await this.sendMessage(this.botGroupId, `Welcome to ${this.config.group.name}!\n\n` +
495
- `This group is managed by Signal Bot.\n` +
496
- `Type ${this.config.settings.commandPrefix}help to see available commands.`);
497
- }
498
- }
499
- catch (updateError) {
500
- this.log(`ERROR: Error updating group: ${updateError?.message || updateError}`, 'ERROR');
501
- }
502
- }
503
- else {
504
- this.log(`- All admins are already in the group`, 'INFO');
505
- if (!avatarPath) {
506
- this.log(`INFO: No avatar configured for group`, 'INFO');
507
- }
508
- }
509
- return;
510
- }
511
- // Create a new group only if none exists
512
- this.log(`- Creating group: ${this.config.group.name}`);
513
- const initialMembers = [
514
- ...this.config.admins,
515
- ...(this.config.group.initialMembers || [])
516
- ].filter((member, index, array) => array.indexOf(member) === index);
517
- try {
518
- const newGroup = await this.signalCli.createGroup(this.config.group.name, initialMembers);
519
- const newGroupData = newGroup;
520
- this.botGroupId = newGroup.groupId || newGroupData.id || newGroupData.Id;
521
- if (this.botGroupId) {
522
- this.log(`- Group created: ${this.config.group.name} (${this.botGroupId})`);
523
- // Configure the group with description, permissions, and avatar
524
- const configOptions = {
525
- description: this.config.group.description,
526
- permissionAddMember: 'ONLY_ADMINS',
527
- permissionEditDetails: 'ONLY_ADMINS'
528
- };
529
- if (avatarPath) {
530
- configOptions.avatar = avatarPath;
531
- this.log(`- Setting avatar for new group...`, 'INFO');
532
- }
533
- await this.signalCli.updateGroup(this.botGroupId, configOptions);
534
- if (avatarPath) {
535
- this.log(`- Group avatar set successfully`, 'INFO');
536
- }
537
- // Send a welcome message to the group
538
- await this.sendMessage(this.botGroupId, `Welcome to ${this.config.group.name}!\n\n` +
539
- `This group is managed by Signal Bot.\n` +
540
- `Type ${this.config.settings.commandPrefix}help to see available commands.`);
541
- }
542
- }
543
- catch (createError) {
544
- if (createError.message && createError.message.includes('Method not implemented')) {
545
- // signal-cli doesn't support group creation via JSON-RPC
546
- this.log(`ERROR: Group creation not supported by signal-cli version.`, 'ERROR');
547
- this.log(`INSTRUCTIONS: To fix this issue:`, 'INFO');
548
- this.log(` 1. Create a group named "${this.config.group.name}" manually in Signal`, 'INFO');
549
- this.log(` 2. Add the bot number (${this.config.phoneNumber}) to the group`, 'INFO');
550
- this.log(` 3. Add all admins to the group: ${this.config.admins.join(', ')}`, 'INFO');
551
- this.log(` 4. Restart the bot`, 'INFO');
552
- this.log(` Available groups: ${groups.map(g => g.name).join(', ')}`, 'INFO');
553
- // Don't use a fallback group - this is misleading behavior
554
- throw new Error(`Group "${this.config.group.name}" does not exist and cannot be created automatically. Please create it manually as described above.`);
555
- }
556
- else {
557
- // Re-throw the original error for other types of failures
558
- this.log(`ERROR: Failed to create group: ${createError?.message || createError}`, 'ERROR');
559
- throw createError;
560
- }
561
- }
562
- }
563
- catch (error) {
564
- this.log(`ERROR: Error configuring group: ${error?.message || error}`, 'ERROR');
565
- }
566
- finally {
567
- // Clean up temporary avatar file if it was downloaded/created
568
- if (avatarPath && isTemporaryAvatar) {
569
- this.cleanupTempFile(avatarPath);
570
- }
571
- }
572
- }
573
- setupEventHandlers() {
574
- this.signalCli.on('message', (messageData) => {
575
- if (this.isProcessingQueue) {
576
- this.log('Queue is processing, buffering incoming message...', 'DEBUG');
577
- this.incomingMessageBuffer.push(messageData);
578
- }
579
- else {
580
- this.handleMessage(messageData);
581
- }
582
- });
583
- this.signalCli.on('close', (code) => {
584
- // Only log as warning if exit code indicates an error
585
- if (code === 0) {
586
- this.log(`Signal daemon closed gracefully`, 'INFO');
587
- }
588
- else {
589
- this.log(`Signal daemon closed with error code ${code}`, 'ERROR');
590
- }
591
- this.emit('daemon-closed', code);
592
- });
593
- this.signalCli.on('error', (error) => {
594
- this.log(`Daemon error: ${error.message}`, 'ERROR');
595
- this.emit('error', error);
596
- });
597
- this.signalCli.on('log', (logData) => {
598
- // Handle non-error stderr messages from signal-cli
599
- this.log(`[signal-cli ${logData.level.toUpperCase()}] ${logData.message}`, 'DEBUG');
600
- });
601
- }
602
- async handleMessage(messageData) {
603
- try {
604
- if (!messageData.envelope?.dataMessage) {
605
- return; // Ignore non-data messages (receipts, typing indicators, etc.)
606
- }
607
- const parsedMessage = {
608
- id: messageData.envelope.timestamp.toString(),
609
- source: messageData.envelope.sourceNumber || messageData.envelope.source,
610
- text: messageData.envelope.dataMessage.message || '',
611
- timestamp: messageData.envelope.timestamp,
612
- groupInfo: messageData.envelope.dataMessage.groupInfo,
613
- isFromAdmin: this.isAdmin(messageData.envelope.sourceNumber || messageData.envelope.source)
614
- };
615
- // Ignore own messages
616
- if (parsedMessage.source === this.config.phoneNumber) {
617
- return;
618
- }
619
- // Ignore empty messages (like reactions only, media without text, etc.)
620
- if (!parsedMessage.text && !messageData.envelope.dataMessage.attachments) {
621
- return;
622
- }
623
- // If bot is configured for group mode, ignore private messages
624
- if (this.botGroupId && !parsedMessage.groupInfo) {
625
- // Only log if there's actual text content to avoid spam
626
- if (parsedMessage.text.trim()) {
627
- this.log(`IGNORE: Ignoring private message from ${parsedMessage.source} (bot is in group mode)`, 'DEBUG');
628
- }
629
- return;
630
- }
631
- // Check if the group is authorized
632
- if (parsedMessage.groupInfo && this.botGroupId) {
633
- const groupId = parsedMessage.groupInfo.id || parsedMessage.groupInfo.groupId;
634
- if (groupId !== this.botGroupId) {
635
- return; // Ignore messages from other groups
636
- }
637
- }
638
- // Update statistics
639
- this.stats.messagesReceived++;
640
- this.stats.lastActivity = Date.now();
641
- this.log(`Message from ${parsedMessage.source}: ${parsedMessage.text.substring(0, 50)}...`);
642
- // Emit message event
643
- this.emit('message', parsedMessage);
644
- // Send read receipt automatically
645
- try {
646
- await this.signalCli.sendReceipt(parsedMessage.source, parsedMessage.timestamp, 'read');
647
- }
648
- catch (error) {
649
- this.log(`ERROR: Error sending read receipt: ${error?.message || error}`, 'DEBUG');
650
- }
651
- // Handle commands
652
- if (parsedMessage.text.startsWith(this.config.settings.commandPrefix || '/')) {
653
- await this.handleCommand(parsedMessage);
654
- }
655
- }
656
- catch (error) {
657
- this.log(`ERROR: Error processing message: ${error?.message || error}`, 'ERROR');
658
- }
659
- }
660
- async handleCommand(message) {
661
- const commandPrefix = this.config.settings.commandPrefix || '/';
662
- const commandText = message.text.substring(commandPrefix.length);
663
- const [commandName, ...args] = commandText.split(' ');
664
- const command = this.commands.get(commandName.toLowerCase());
665
- if (!command) {
666
- return; // Unknown command, ignore
667
- }
668
- // Check admin permissions
669
- if (command.adminOnly && !message.isFromAdmin) {
670
- await this.sendCommandResponse(message, 'ERROR: This command requires admin privileges');
671
- return;
672
- }
673
- if (this.isOnCooldown(message.source)) {
674
- this.log(`COOLDOWN: Cooldown active for ${message.source} on command "${commandName}"`);
675
- return;
676
- }
677
- this.stats.commandsExecuted++;
678
- this.emit('command', { command: commandName, user: message.source, args });
679
- this.userCooldowns.set(message.source, Date.now());
680
- this.log(`- Executing command: "${command.name}" by ${message.source}`);
681
- try {
682
- const response = await command.handler(message, args, this);
683
- if (response) {
684
- await this.sendCommandResponse(message, response);
685
- }
686
- }
687
- catch (error) {
688
- this.log(`ERROR: Error executing command "${command.name}": ${error?.message || error}`, 'ERROR');
689
- await this.sendCommandResponse(message, `ERROR: An error occurred while running the command: ${command.name}`);
690
- }
691
- }
692
- async sendCommandResponse(message, response) {
693
- const recipient = message.groupInfo ?
694
- (message.groupInfo.id || message.groupInfo.groupId || message.source) : message.source;
695
- await this.sendMessage(recipient, response);
696
- }
697
- isOnCooldown(userId) {
698
- const lastCommand = this.userCooldowns.get(userId);
699
- if (!lastCommand)
700
- return false;
701
- const now = Date.now();
702
- const cooldownMs = (this.config.settings.cooldownSeconds || 2) * 1000;
703
- return (now - lastCommand) < cooldownMs;
704
- }
705
- async processActionQueue() {
706
- if (this.isProcessingQueue || this.actionQueue.length === 0) {
707
- return;
708
- }
709
- this.isProcessingQueue = true;
710
- this.log(`Processing action queue with ${this.actionQueue.length} action(s)...`, 'DEBUG');
711
- try {
712
- while (this.actionQueue.length > 0) {
713
- const action = this.actionQueue.shift();
714
- try {
715
- switch (action.type) {
716
- case 'sendMessage':
717
- this.log(`Executing sendMessage to ${action.recipient}...`, 'DEBUG');
718
- await this.signalCli.sendMessage(action.recipient, action.message);
719
- break;
720
- case 'sendMessageWithAttachment':
721
- this.log(`Executing sendMessageWithAttachment to ${action.recipient} with ${action.attachments.length} file(s)...`, 'DEBUG');
722
- await this.signalCli.sendMessage(action.recipient, action.message, {
723
- attachments: action.attachments
724
- });
725
- // Wait a bit for signal-cli to finish processing the files before cleanup
726
- // signal-cli responds immediately but continues processing files in background
727
- if (action.cleanup && action.cleanup.length > 0) {
728
- setTimeout(() => {
729
- action.cleanup.forEach(filePath => {
730
- this.cleanupTempFile(filePath);
731
- });
732
- }, 2000); // Wait 2 seconds for signal-cli to upload files
733
- }
734
- break;
735
- case 'sendReaction':
736
- this.log(`Executing sendReaction to ${action.recipient}...`, 'DEBUG');
737
- await this.signalCli.sendReaction(action.recipient, action.targetAuthor, action.targetTimestamp, action.emoji);
738
- break;
739
- }
740
- // Wait a bit between actions to be safe
741
- await new Promise(resolve => setTimeout(resolve, 250));
742
- }
743
- catch (error) {
744
- this.log(`ERROR: Failed to execute action ${action.type}: ${error?.message || error}`, 'ERROR');
745
- // Clean up temporary files even on error
746
- if (action.type === 'sendMessageWithAttachment' && action.cleanup && action.cleanup.length > 0) {
747
- action.cleanup.forEach(filePath => {
748
- this.cleanupTempFile(filePath);
749
- });
750
- }
751
- }
752
- }
753
- }
754
- finally {
755
- this.isProcessingQueue = false;
756
- this.log('Action queue processed.', 'DEBUG');
757
- // Process any buffered messages
758
- while (this.incomingMessageBuffer.length > 0) {
759
- this.log('Processing buffered message...', 'DEBUG');
760
- const bufferedMessage = this.incomingMessageBuffer.shift();
761
- this.handleMessage(bufferedMessage);
762
- }
763
- }
764
- }
765
- async sendWelcomeMessage() {
766
- const welcomeText = `Signal Bot Started!
767
-
768
- ` +
769
- `Bot is now active
770
- ` +
771
- `Number: ${this.config.phoneNumber}
772
- ` +
773
- `Group: ${this.config.group ? this.config.group.name : 'None'}
774
- ` +
775
- `Use ${this.config.settings.commandPrefix}help to see commands
776
-
777
- ` +
778
- `Happy chatting!`;
779
- for (const admin of this.config.admins) {
780
- try {
781
- await this.sendMessage(admin, welcomeText);
782
- }
783
- catch (error) {
784
- this.log(`ERROR: Error sending welcome message to ${admin}: ${error?.message || error}`, 'ERROR');
785
- }
786
- }
787
- }
788
- formatUptime(ms) {
789
- const seconds = Math.floor(ms / 1000);
790
- const minutes = Math.floor(seconds / 60);
791
- const hours = Math.floor(minutes / 60);
792
- const days = Math.floor(hours / 24);
793
- if (days > 0)
794
- return `${days}d ${hours % 24}h ${minutes % 60}m`;
795
- if (hours > 0)
796
- return `${hours}h ${minutes % 60}m`;
797
- if (minutes > 0)
798
- return `${minutes}m ${seconds % 60}s`;
799
- return `${seconds}s`;
800
- }
801
- log(message, level = 'INFO') {
802
- const timestamp = new Date().toISOString();
803
- const logMessage = `[${timestamp}] [${level}] ${message}`;
804
- console.log(logMessage);
805
- if (this.config.settings.logMessages) {
806
- // In production, you might want to write to a file
807
- // fs.appendFileSync('bot.log', logMessage + '\n');
808
- }
809
- }
810
- }
811
- exports.SignalBot = SignalBot;