signal-sdk 0.0.9 → 0.1.1

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 (44) hide show
  1. package/README.md +184 -61
  2. package/dist/MultiAccountManager.d.ts +149 -0
  3. package/dist/MultiAccountManager.js +320 -0
  4. package/dist/SignalBot.d.ts +1 -0
  5. package/dist/SignalBot.js +20 -2
  6. package/dist/SignalCli.d.ts +315 -16
  7. package/dist/SignalCli.js +880 -26
  8. package/dist/__tests__/MultiAccountManager.test.d.ts +4 -0
  9. package/dist/__tests__/MultiAccountManager.test.js +209 -0
  10. package/dist/__tests__/SignalBot.additional.test.d.ts +5 -0
  11. package/dist/__tests__/SignalBot.additional.test.js +353 -0
  12. package/dist/__tests__/SignalBot.test.js +5 -0
  13. package/dist/__tests__/SignalCli.advanced.test.d.ts +5 -0
  14. package/dist/__tests__/SignalCli.advanced.test.js +295 -0
  15. package/dist/__tests__/SignalCli.e2e.test.d.ts +5 -0
  16. package/dist/__tests__/SignalCli.e2e.test.js +240 -0
  17. package/dist/__tests__/SignalCli.integration.test.d.ts +5 -0
  18. package/dist/__tests__/SignalCli.integration.test.js +225 -0
  19. package/dist/__tests__/SignalCli.methods.test.d.ts +5 -0
  20. package/dist/__tests__/SignalCli.methods.test.js +556 -0
  21. package/dist/__tests__/SignalCli.parsing.test.d.ts +5 -0
  22. package/dist/__tests__/SignalCli.parsing.test.js +258 -0
  23. package/dist/__tests__/SignalCli.test.js +249 -13
  24. package/dist/__tests__/config.test.d.ts +5 -0
  25. package/dist/__tests__/config.test.js +252 -0
  26. package/dist/__tests__/errors.test.d.ts +5 -0
  27. package/dist/__tests__/errors.test.js +276 -0
  28. package/dist/__tests__/retry.test.d.ts +4 -0
  29. package/dist/__tests__/retry.test.js +123 -0
  30. package/dist/__tests__/validators.test.d.ts +4 -0
  31. package/dist/__tests__/validators.test.js +147 -0
  32. package/dist/config.d.ts +82 -0
  33. package/dist/config.js +116 -0
  34. package/dist/errors.d.ts +32 -0
  35. package/dist/errors.js +75 -0
  36. package/dist/index.d.ts +5 -0
  37. package/dist/index.js +7 -1
  38. package/dist/interfaces.d.ts +200 -10
  39. package/dist/interfaces.js +1 -1
  40. package/dist/retry.d.ts +56 -0
  41. package/dist/retry.js +152 -0
  42. package/dist/validators.d.ts +59 -0
  43. package/dist/validators.js +170 -0
  44. package/package.json +1 -1
@@ -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');