signal-sdk 0.1.0 → 0.1.2

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 CHANGED
@@ -14,11 +14,10 @@
14
14
 
15
15
  [![npm version](https://badge.fury.io/js/signal-sdk.svg)](https://badge.fury.io/js/signal-sdk)
16
16
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
17
- [![signal-cli](https://img.shields.io/badge/signal--cli-v0.13.22-blue.svg)](https://github.com/AsamK/signal-cli)
17
+ [![signal-cli](https://img.shields.io/badge/signal--cli-v0.13.23-blue.svg)](https://github.com/AsamK/signal-cli)
18
18
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.8+-blue.svg)](https://www.typescriptlang.org/)
19
19
  [![Node.js](https://img.shields.io/badge/Node.js-18+-green.svg)](https://nodejs.org/)
20
- [![Tests](https://img.shields.io/badge/tests-225%20passing-brightgreen.svg)](./src/__tests__)
21
- [![Coverage](https://img.shields.io/badge/coverage-57.52%25-yellow.svg)](./src/__tests__)
20
+ [![Tests](https://img.shields.io/badge/tests-310%20passing-brightgreen.svg)](./src/__tests__)
22
21
  [![Donate on Liberapay](https://img.shields.io/badge/Liberapay-Donate-yellow.svg)](https://liberapay.com/devbyben/donate)
23
22
 
24
23
  </div>
@@ -48,6 +47,14 @@
48
47
 
49
48
  ### Advanced Features
50
49
 
50
+ - **Multi-Account Support** - Manage multiple Signal accounts simultaneously with `MultiAccountManager`
51
+ - **Advanced Messaging** - Text styles (bold, italic), mentions, quotes, URL previews, message editing
52
+ - **Enhanced Receive** - Modern `receive()` method with options (timeout, ignore filters)
53
+ - **Username Management** - Set and delete Signal usernames
54
+ - **Identity Verification** - Get and verify safety numbers, manage untrusted identities
55
+ - **Advanced Groups** - Send invite links, manage banned members, reset group links
56
+ - **Enhanced Parsing** - Extract givenName, familyName, mobileCoinAddress from profiles and contacts
57
+ - **Daemon Modes** - Support for Unix socket, TCP, and HTTP daemon connections
51
58
  - **File Attachments** - Send and receive files, images, and media
52
59
  - **Group Operations** - Create and manage groups with detailed information
53
60
  - **Contact Management** - Manage contacts with export/import capabilities
@@ -55,11 +62,11 @@
55
62
  - **Typing Indicators** - Send and receive typing notifications
56
63
  - **Read Receipts** - Track message delivery and read status
57
64
  - **Profile Management** - Update profile information and avatars
58
- - **Payment Notifications** - Send payment notifications
65
+ - **Payment Notifications** - Send MobileCoin payment notifications with receipts
59
66
  - **Sticker Packs** - Upload and manage custom sticker packs
60
67
  - **User Status** - Verify Signal registration status
61
68
  - **Rate Limiting** - Handle and recover from rate limits
62
- - **Phone Number Changes** - Change registered phone numbers
69
+ - **Phone Number Changes** - Change registered phone numbers with verification
63
70
  - **Progress Tracking** - Monitor upload progress
64
71
  - **Polls** - Create, vote, and terminate polls
65
72
  - **Attachment Retrieval** - Retrieve attachments, avatars, and stickers
@@ -357,7 +364,7 @@ await bot.start();
357
364
  - **TypeScript**: 5.8+ with strict mode
358
365
  - **Test Coverage**: 225 passing tests across 9 suites
359
366
  - **Code Coverage**: 57.52% overall, critical modules at 96-100%
360
- - **signal-cli**: Compatible with v0.13.22
367
+ - **signal-cli**: Compatible with v0.13.23
361
368
 
362
369
  ## Testing
363
370
 
@@ -371,14 +378,14 @@ The SDK has comprehensive test coverage to ensure reliability and quality.
371
378
 
372
379
  ### Coverage by Module
373
380
 
374
- | Module | Statements | Branches | Functions | Lines | Status |
375
- |--------|-----------|----------|-----------|-------|--------|
376
- | **validators.ts** | 100% | 100% | 100% | 100% | ✅ Perfect |
377
- | **config.ts** | 100% | 97.22% | 100% | 100% | ✅ Excellent |
378
- | **errors.ts** | 100% | 100% | 100% | 100% | ✅ Perfect |
379
- | **retry.ts** | 96.15% | 85.71% | 100% | 97.95% | ✅ Excellent |
380
- | **SignalCli.ts** | 68.68% | 55.46% | 65.9% | 72.7% | ✅ Good |
381
- | **SignalBot.ts** | 24.33% | 16.94% | 29.68% | 24.59% | ⚠️ In Progress |
381
+ | Module | Statements | Branches | Functions | Lines | Status |
382
+ | ----------------- | ---------- | -------- | --------- | ------ | -------------- |
383
+ | **validators.ts** | 100% | 100% | 100% | 100% | ✅ Perfect |
384
+ | **config.ts** | 100% | 97.22% | 100% | 100% | ✅ Excellent |
385
+ | **errors.ts** | 100% | 100% | 100% | 100% | ✅ Perfect |
386
+ | **retry.ts** | 96.15% | 85.71% | 100% | 97.95% | ✅ Excellent |
387
+ | **SignalCli.ts** | 68.68% | 55.46% | 65.9% | 72.7% | ✅ Good |
388
+ | **SignalBot.ts** | 24.33% | 16.94% | 29.68% | 24.59% | ⚠️ In Progress |
382
389
 
383
390
  ### Running Tests
384
391
 
@@ -408,7 +415,7 @@ npm test -- --watch
408
415
  8. **SignalBot.test.ts** - Bot framework
409
416
  9. **SignalBot.additional.test.ts** - Extended bot features
410
417
 
411
- ```
418
+ ````
412
419
 
413
420
  ## Development
414
421
 
@@ -428,7 +435,7 @@ npm run build && node examples/sdk/01-basic-usage.js
428
435
 
429
436
  # Run tests
430
437
  npm test
431
- ```
438
+ ````
432
439
 
433
440
  ## Configuration
434
441
 
@@ -563,7 +570,7 @@ This project is licensed under the MIT License - see the [LICENSE](./LICENSE) fi
563
570
 
564
571
  ## API Methods
565
572
 
566
- Compatible with signal-cli v0.13.22 - **100% Feature Coverage**
573
+ Compatible with signal-cli v0.13.23 - **100% Feature Coverage**
567
574
 
568
575
  | Category | Method | Description | Status |
569
576
  | --------------- | -------------------------- | ---------------------------------- | ------ |
@@ -588,6 +595,8 @@ Compatible with signal-cli v0.13.22 - **100% Feature Coverage**
588
595
  | | `getUserStatus` | Check registration status | ✅ |
589
596
  | **Account** | `updateAccount` | Update account settings | ✅ |
590
597
  | | `listAccountsDetailed` | List accounts with detailed info | ✅ |
598
+ | **Devices** | `listDevices` | List linked devices | ✅ |
599
+ | | `updateDevice` | Update device name (v0.13.23+) | ✅ |
591
600
  | **Attachments** | `getAttachment` | Retrieve attachment by ID | ✅ |
592
601
  | | `getAvatar` | Retrieve avatar by ID | ✅ |
593
602
  | | `getSticker` | Retrieve sticker by ID | ✅ |
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Multi-Account Manager for Signal SDK
3
+ *
4
+ * Manages multiple Signal accounts simultaneously with event routing
5
+ * and isolated process management.
6
+ */
7
+ import { SignalCli } from './SignalCli';
8
+ import { EventEmitter } from 'events';
9
+ import { SignalCliConfig } from './config';
10
+ /**
11
+ * Configuration for a managed account
12
+ */
13
+ export interface ManagedAccount {
14
+ /** Account phone number */
15
+ account: string;
16
+ /** SignalCli instance */
17
+ instance: SignalCli;
18
+ /** Whether the account is connected */
19
+ connected: boolean;
20
+ /** Last activity timestamp */
21
+ lastActivity: number;
22
+ }
23
+ /**
24
+ * Options for MultiAccountManager
25
+ */
26
+ export interface MultiAccountOptions {
27
+ /** Path to signal-cli executable */
28
+ signalCliPath?: string;
29
+ /** Data directory for all accounts */
30
+ dataPath?: string;
31
+ /** Enable verbose logging */
32
+ verbose?: boolean;
33
+ /** Auto-reconnect on failure */
34
+ autoReconnect?: boolean;
35
+ }
36
+ /**
37
+ * Multi-Account Manager
38
+ *
39
+ * Manages multiple Signal accounts with event routing and lifecycle management.
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * const manager = new MultiAccountManager({
44
+ * dataPath: '/path/to/data',
45
+ * autoReconnect: true
46
+ * });
47
+ *
48
+ * // Add accounts
49
+ * await manager.addAccount('+33123456789');
50
+ * await manager.addAccount('+33987654321');
51
+ *
52
+ * // Listen to events from all accounts
53
+ * manager.on('message', (account, message) => {
54
+ * console.log(`Message from ${account}: ${message.text}`);
55
+ * });
56
+ *
57
+ * // Connect all accounts
58
+ * await manager.connectAll();
59
+ *
60
+ * // Send from specific account
61
+ * await manager.sendMessage('+33123456789', '+33111111111', 'Hello!');
62
+ * ```
63
+ */
64
+ export declare class MultiAccountManager extends EventEmitter {
65
+ private accounts;
66
+ private options;
67
+ private logger;
68
+ constructor(options?: MultiAccountOptions);
69
+ /**
70
+ * Add an account to the manager
71
+ *
72
+ * @param account - Phone number of the account
73
+ * @param config - Optional SignalCli configuration
74
+ * @returns The SignalCli instance
75
+ */
76
+ addAccount(account: string, config?: Partial<SignalCliConfig>): Promise<SignalCli>;
77
+ /**
78
+ * Remove an account from the manager
79
+ *
80
+ * @param account - Phone number of the account
81
+ */
82
+ removeAccount(account: string): Promise<void>;
83
+ /**
84
+ * Get a specific account instance
85
+ *
86
+ * @param account - Phone number of the account
87
+ * @returns The SignalCli instance
88
+ */
89
+ getAccount(account: string): SignalCli | undefined;
90
+ /**
91
+ * Get all managed accounts
92
+ *
93
+ * @returns Array of account phone numbers
94
+ */
95
+ getAccounts(): string[];
96
+ /**
97
+ * Check if an account exists
98
+ *
99
+ * @param account - Phone number of the account
100
+ * @returns True if the account exists
101
+ */
102
+ hasAccount(account: string): boolean;
103
+ /**
104
+ * Connect a specific account
105
+ *
106
+ * @param account - Phone number of the account
107
+ */
108
+ connect(account: string): Promise<void>;
109
+ /**
110
+ * Disconnect a specific account
111
+ *
112
+ * @param account - Phone number of the account
113
+ */
114
+ disconnect(account: string): Promise<void>;
115
+ /**
116
+ * Connect all accounts
117
+ */
118
+ connectAll(): Promise<void>;
119
+ /**
120
+ * Disconnect all accounts
121
+ */
122
+ disconnectAll(): Promise<void>;
123
+ /**
124
+ * Send a message from a specific account
125
+ *
126
+ * @param fromAccount - Account to send from
127
+ * @param recipient - Recipient phone number or group ID
128
+ * @param message - Message text
129
+ * @param options - Send options
130
+ */
131
+ sendMessage(fromAccount: string, recipient: string, message: string, options?: any): Promise<any>;
132
+ /**
133
+ * Get account status information
134
+ *
135
+ * @param account - Phone number of the account (optional)
136
+ * @returns Status information for all or specific account
137
+ */
138
+ getStatus(account?: string): any;
139
+ /**
140
+ * Setup event forwarding from an account instance
141
+ *
142
+ * @private
143
+ */
144
+ private setupEventForwarding;
145
+ /**
146
+ * Shutdown the manager and cleanup all accounts
147
+ */
148
+ shutdown(): Promise<void>;
149
+ }
@@ -0,0 +1,320 @@
1
+ "use strict";
2
+ /**
3
+ * Multi-Account Manager for Signal SDK
4
+ *
5
+ * Manages multiple Signal accounts simultaneously with event routing
6
+ * and isolated process management.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.MultiAccountManager = void 0;
10
+ const SignalCli_1 = require("./SignalCli");
11
+ const events_1 = require("events");
12
+ const config_1 = require("./config");
13
+ /**
14
+ * Multi-Account Manager
15
+ *
16
+ * Manages multiple Signal accounts with event routing and lifecycle management.
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * const manager = new MultiAccountManager({
21
+ * dataPath: '/path/to/data',
22
+ * autoReconnect: true
23
+ * });
24
+ *
25
+ * // Add accounts
26
+ * await manager.addAccount('+33123456789');
27
+ * await manager.addAccount('+33987654321');
28
+ *
29
+ * // Listen to events from all accounts
30
+ * manager.on('message', (account, message) => {
31
+ * console.log(`Message from ${account}: ${message.text}`);
32
+ * });
33
+ *
34
+ * // Connect all accounts
35
+ * await manager.connectAll();
36
+ *
37
+ * // Send from specific account
38
+ * await manager.sendMessage('+33123456789', '+33111111111', 'Hello!');
39
+ * ```
40
+ */
41
+ class MultiAccountManager extends events_1.EventEmitter {
42
+ constructor(options = {}) {
43
+ super();
44
+ this.accounts = new Map();
45
+ this.options = options;
46
+ this.logger = new config_1.Logger({
47
+ level: options.verbose ? 'debug' : 'info',
48
+ enableFile: false
49
+ });
50
+ this.logger.info('MultiAccountManager initialized');
51
+ }
52
+ /**
53
+ * Add an account to the manager
54
+ *
55
+ * @param account - Phone number of the account
56
+ * @param config - Optional SignalCli configuration
57
+ * @returns The SignalCli instance
58
+ */
59
+ async addAccount(account, config = {}) {
60
+ if (this.accounts.has(account)) {
61
+ throw new Error(`Account ${account} already exists`);
62
+ }
63
+ this.logger.info(`Adding account: ${account}`);
64
+ // Create SignalCli instance with merged config
65
+ const signalConfig = {
66
+ signalCliPath: this.options.signalCliPath,
67
+ verbose: this.options.verbose,
68
+ ...config
69
+ };
70
+ const instance = new SignalCli_1.SignalCli(account, undefined, signalConfig);
71
+ // Forward events from this instance
72
+ this.setupEventForwarding(account, instance);
73
+ // Store the managed account
74
+ const managedAccount = {
75
+ account,
76
+ instance,
77
+ connected: false,
78
+ lastActivity: Date.now()
79
+ };
80
+ this.accounts.set(account, managedAccount);
81
+ this.emit('accountAdded', account);
82
+ this.logger.info(`Account ${account} added successfully`);
83
+ return instance;
84
+ }
85
+ /**
86
+ * Remove an account from the manager
87
+ *
88
+ * @param account - Phone number of the account
89
+ */
90
+ async removeAccount(account) {
91
+ const managedAccount = this.accounts.get(account);
92
+ if (!managedAccount) {
93
+ throw new Error(`Account ${account} not found`);
94
+ }
95
+ this.logger.info(`Removing account: ${account}`);
96
+ // Disconnect if connected
97
+ if (managedAccount.connected) {
98
+ await managedAccount.instance.disconnect();
99
+ }
100
+ // Remove all listeners
101
+ managedAccount.instance.removeAllListeners();
102
+ // Remove from map
103
+ this.accounts.delete(account);
104
+ this.emit('accountRemoved', account);
105
+ this.logger.info(`Account ${account} removed successfully`);
106
+ }
107
+ /**
108
+ * Get a specific account instance
109
+ *
110
+ * @param account - Phone number of the account
111
+ * @returns The SignalCli instance
112
+ */
113
+ getAccount(account) {
114
+ return this.accounts.get(account)?.instance;
115
+ }
116
+ /**
117
+ * Get all managed accounts
118
+ *
119
+ * @returns Array of account phone numbers
120
+ */
121
+ getAccounts() {
122
+ return Array.from(this.accounts.keys());
123
+ }
124
+ /**
125
+ * Check if an account exists
126
+ *
127
+ * @param account - Phone number of the account
128
+ * @returns True if the account exists
129
+ */
130
+ hasAccount(account) {
131
+ return this.accounts.has(account);
132
+ }
133
+ /**
134
+ * Connect a specific account
135
+ *
136
+ * @param account - Phone number of the account
137
+ */
138
+ async connect(account) {
139
+ const managedAccount = this.accounts.get(account);
140
+ if (!managedAccount) {
141
+ throw new Error(`Account ${account} not found`);
142
+ }
143
+ if (managedAccount.connected) {
144
+ this.logger.warn(`Account ${account} already connected`);
145
+ return;
146
+ }
147
+ this.logger.info(`Connecting account: ${account}`);
148
+ try {
149
+ await managedAccount.instance.connect();
150
+ managedAccount.connected = true;
151
+ managedAccount.lastActivity = Date.now();
152
+ this.emit('accountConnected', account);
153
+ this.logger.info(`Account ${account} connected successfully`);
154
+ }
155
+ catch (error) {
156
+ this.logger.error(`Failed to connect account ${account}:`, error);
157
+ if (this.options.autoReconnect) {
158
+ this.logger.info(`Will retry connection for ${account}`);
159
+ setTimeout(() => this.connect(account), 5000);
160
+ }
161
+ throw error;
162
+ }
163
+ }
164
+ /**
165
+ * Disconnect a specific account
166
+ *
167
+ * @param account - Phone number of the account
168
+ */
169
+ async disconnect(account) {
170
+ const managedAccount = this.accounts.get(account);
171
+ if (!managedAccount) {
172
+ throw new Error(`Account ${account} not found`);
173
+ }
174
+ if (!managedAccount.connected) {
175
+ this.logger.warn(`Account ${account} not connected`);
176
+ return;
177
+ }
178
+ this.logger.info(`Disconnecting account: ${account}`);
179
+ await managedAccount.instance.disconnect();
180
+ managedAccount.connected = false;
181
+ this.emit('accountDisconnected', account);
182
+ this.logger.info(`Account ${account} disconnected`);
183
+ }
184
+ /**
185
+ * Connect all accounts
186
+ */
187
+ async connectAll() {
188
+ this.logger.info('Connecting all accounts');
189
+ const promises = Array.from(this.accounts.keys()).map(account => this.connect(account).catch(error => {
190
+ this.logger.error(`Failed to connect ${account}:`, error);
191
+ }));
192
+ await Promise.all(promises);
193
+ this.logger.info('All accounts connected');
194
+ }
195
+ /**
196
+ * Disconnect all accounts
197
+ */
198
+ async disconnectAll() {
199
+ this.logger.info('Disconnecting all accounts');
200
+ const promises = Array.from(this.accounts.keys()).map(account => this.disconnect(account).catch(error => {
201
+ this.logger.error(`Failed to disconnect ${account}:`, error);
202
+ }));
203
+ await Promise.all(promises);
204
+ this.logger.info('All accounts disconnected');
205
+ }
206
+ /**
207
+ * Send a message from a specific account
208
+ *
209
+ * @param fromAccount - Account to send from
210
+ * @param recipient - Recipient phone number or group ID
211
+ * @param message - Message text
212
+ * @param options - Send options
213
+ */
214
+ async sendMessage(fromAccount, recipient, message, options = {}) {
215
+ const managedAccount = this.accounts.get(fromAccount);
216
+ if (!managedAccount) {
217
+ throw new Error(`Account ${fromAccount} not found`);
218
+ }
219
+ managedAccount.lastActivity = Date.now();
220
+ return managedAccount.instance.sendMessage(recipient, message, options);
221
+ }
222
+ /**
223
+ * Get account status information
224
+ *
225
+ * @param account - Phone number of the account (optional)
226
+ * @returns Status information for all or specific account
227
+ */
228
+ getStatus(account) {
229
+ if (account) {
230
+ const managedAccount = this.accounts.get(account);
231
+ if (!managedAccount) {
232
+ return null;
233
+ }
234
+ return {
235
+ account: managedAccount.account,
236
+ connected: managedAccount.connected,
237
+ lastActivity: managedAccount.lastActivity,
238
+ uptime: Date.now() - managedAccount.lastActivity
239
+ };
240
+ }
241
+ // Return status for all accounts
242
+ const status = {
243
+ totalAccounts: this.accounts.size,
244
+ connectedAccounts: 0,
245
+ accounts: []
246
+ };
247
+ for (const [account, managed] of this.accounts) {
248
+ if (managed.connected) {
249
+ status.connectedAccounts++;
250
+ }
251
+ status.accounts.push({
252
+ account,
253
+ connected: managed.connected,
254
+ lastActivity: managed.lastActivity,
255
+ uptime: Date.now() - managed.lastActivity
256
+ });
257
+ }
258
+ return status;
259
+ }
260
+ /**
261
+ * Setup event forwarding from an account instance
262
+ *
263
+ * @private
264
+ */
265
+ setupEventForwarding(account, instance) {
266
+ // Forward all events with account prefix
267
+ const events = [
268
+ 'message',
269
+ 'receipt',
270
+ 'typing',
271
+ 'reaction',
272
+ 'error',
273
+ 'connected',
274
+ 'disconnected'
275
+ ];
276
+ events.forEach(event => {
277
+ instance.on(event, (...args) => {
278
+ // Emit with account information
279
+ this.emit(event, account, ...args);
280
+ // Also emit a generic event with account
281
+ this.emit('accountEvent', {
282
+ account,
283
+ event,
284
+ data: args
285
+ });
286
+ // Update last activity
287
+ const managedAccount = this.accounts.get(account);
288
+ if (managedAccount) {
289
+ managedAccount.lastActivity = Date.now();
290
+ }
291
+ });
292
+ });
293
+ // Handle disconnection
294
+ instance.on('disconnected', () => {
295
+ const managedAccount = this.accounts.get(account);
296
+ if (managedAccount) {
297
+ managedAccount.connected = false;
298
+ }
299
+ // Auto-reconnect if enabled
300
+ if (this.options.autoReconnect) {
301
+ this.logger.info(`Auto-reconnecting account ${account}`);
302
+ setTimeout(() => this.connect(account), 5000);
303
+ }
304
+ });
305
+ }
306
+ /**
307
+ * Shutdown the manager and cleanup all accounts
308
+ */
309
+ async shutdown() {
310
+ this.logger.info('Shutting down MultiAccountManager');
311
+ await this.disconnectAll();
312
+ // Remove all accounts
313
+ for (const account of this.accounts.keys()) {
314
+ await this.removeAccount(account);
315
+ }
316
+ this.removeAllListeners();
317
+ this.logger.info('MultiAccountManager shutdown complete');
318
+ }
319
+ }
320
+ exports.MultiAccountManager = MultiAccountManager;
@@ -12,6 +12,7 @@ export declare class SignalBot extends EventEmitter {
12
12
  private actionQueue;
13
13
  private isProcessingQueue;
14
14
  private incomingMessageBuffer;
15
+ private activeTimers;
15
16
  constructor(config: BotConfig, signalCliPath?: string);
16
17
  /**
17
18
  * Downloads an image from URL to a temporary file
package/dist/SignalBot.js CHANGED
@@ -50,6 +50,7 @@ class SignalBot extends events_1.EventEmitter {
50
50
  this.actionQueue = [];
51
51
  this.isProcessingQueue = false;
52
52
  this.incomingMessageBuffer = [];
53
+ this.activeTimers = [];
53
54
  this.config = {
54
55
  phoneNumber: config.phoneNumber,
55
56
  admins: config.admins || [],
@@ -317,6 +318,9 @@ class SignalBot extends events_1.EventEmitter {
317
318
  async stop() {
318
319
  this.log('- Stopping Signal Bot...');
319
320
  this.isRunning = false;
321
+ // Clear all active timers
322
+ this.activeTimers.forEach(timer => clearTimeout(timer));
323
+ this.activeTimers = [];
320
324
  this.signalCli.disconnect();
321
325
  this.emit('stopped');
322
326
  this.log('- Bot stopped');
@@ -324,6 +328,9 @@ class SignalBot extends events_1.EventEmitter {
324
328
  async gracefulShutdown() {
325
329
  this.log('- Gracefully shutting down Signal Bot...');
326
330
  this.isRunning = false;
331
+ // Clear all active timers
332
+ this.activeTimers.forEach(timer => clearTimeout(timer));
333
+ this.activeTimers = [];
327
334
  try {
328
335
  await this.signalCli.gracefulShutdown();
329
336
  this.log('- Signal Bot shutdown completed gracefully');
@@ -725,11 +732,18 @@ class SignalBot extends events_1.EventEmitter {
725
732
  // Wait a bit for signal-cli to finish processing the files before cleanup
726
733
  // signal-cli responds immediately but continues processing files in background
727
734
  if (action.cleanup && action.cleanup.length > 0) {
728
- setTimeout(() => {
735
+ const cleanupTimer = setTimeout(() => {
729
736
  action.cleanup.forEach(filePath => {
730
737
  this.cleanupTempFile(filePath);
731
738
  });
739
+ // Remove timer from active list
740
+ const index = this.activeTimers.indexOf(cleanupTimer);
741
+ if (index > -1)
742
+ this.activeTimers.splice(index, 1);
732
743
  }, 2000); // Wait 2 seconds for signal-cli to upload files
744
+ if (cleanupTimer.unref)
745
+ cleanupTimer.unref();
746
+ this.activeTimers.push(cleanupTimer);
733
747
  }
734
748
  break;
735
749
  case 'sendReaction':
@@ -738,7 +752,11 @@ class SignalBot extends events_1.EventEmitter {
738
752
  break;
739
753
  }
740
754
  // Wait a bit between actions to be safe
741
- await new Promise(resolve => setTimeout(resolve, 250));
755
+ await new Promise(resolve => {
756
+ const timer = setTimeout(resolve, 250);
757
+ if (timer.unref)
758
+ timer.unref();
759
+ });
742
760
  }
743
761
  catch (error) {
744
762
  this.log(`ERROR: Failed to execute action ${action.type}: ${error?.message || error}`, 'ERROR');