signal-sdk 0.1.2 → 0.1.3

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 (53) hide show
  1. package/README.md +18 -8
  2. package/dist/MultiAccountManager.js +11 -19
  3. package/dist/SignalBot.js +40 -36
  4. package/dist/SignalCli.d.ts +23 -319
  5. package/dist/SignalCli.js +224 -998
  6. package/dist/__tests__/DeviceManager.test.d.ts +1 -0
  7. package/dist/__tests__/DeviceManager.test.js +135 -0
  8. package/dist/__tests__/MultiAccountManager.coverage.test.d.ts +1 -0
  9. package/dist/__tests__/MultiAccountManager.coverage.test.js +33 -0
  10. package/dist/__tests__/MultiAccountManager.test.js +3 -3
  11. package/dist/__tests__/SignalBot.additional.test.js +40 -37
  12. package/dist/__tests__/SignalBot.coverage.test.d.ts +1 -0
  13. package/dist/__tests__/SignalBot.coverage.test.js +385 -0
  14. package/dist/__tests__/SignalBot.test.js +8 -8
  15. package/dist/__tests__/SignalCli.advanced.test.js +47 -58
  16. package/dist/__tests__/SignalCli.connections.test.d.ts +1 -0
  17. package/dist/__tests__/SignalCli.connections.test.js +110 -0
  18. package/dist/__tests__/SignalCli.e2e.test.js +28 -32
  19. package/dist/__tests__/SignalCli.events.test.d.ts +1 -0
  20. package/dist/__tests__/SignalCli.events.test.js +113 -0
  21. package/dist/__tests__/SignalCli.integration.test.js +6 -5
  22. package/dist/__tests__/SignalCli.methods.test.js +77 -77
  23. package/dist/__tests__/SignalCli.parsing.test.js +4 -13
  24. package/dist/__tests__/SignalCli.simple.test.d.ts +1 -0
  25. package/dist/__tests__/SignalCli.simple.test.js +77 -0
  26. package/dist/__tests__/SignalCli.test.js +96 -82
  27. package/dist/__tests__/config.test.js +19 -29
  28. package/dist/__tests__/errors.test.js +2 -2
  29. package/dist/__tests__/retry.test.js +10 -8
  30. package/dist/__tests__/robustness.test.d.ts +1 -0
  31. package/dist/__tests__/robustness.test.js +59 -0
  32. package/dist/__tests__/security.test.d.ts +1 -0
  33. package/dist/__tests__/security.test.js +50 -0
  34. package/dist/config.js +3 -3
  35. package/dist/interfaces.d.ts +18 -0
  36. package/dist/managers/AccountManager.d.ts +27 -0
  37. package/dist/managers/AccountManager.js +147 -0
  38. package/dist/managers/BaseManager.d.ts +9 -0
  39. package/dist/managers/BaseManager.js +17 -0
  40. package/dist/managers/ContactManager.d.ts +15 -0
  41. package/dist/managers/ContactManager.js +123 -0
  42. package/dist/managers/DeviceManager.d.ts +11 -0
  43. package/dist/managers/DeviceManager.js +139 -0
  44. package/dist/managers/GroupManager.d.ts +12 -0
  45. package/dist/managers/GroupManager.js +78 -0
  46. package/dist/managers/MessageManager.d.ts +18 -0
  47. package/dist/managers/MessageManager.js +301 -0
  48. package/dist/managers/StickerManager.d.ts +8 -0
  49. package/dist/managers/StickerManager.js +39 -0
  50. package/dist/retry.js +3 -3
  51. package/dist/validators.d.ts +9 -0
  52. package/dist/validators.js +20 -0
  53. package/package.json +11 -4
package/dist/SignalCli.js CHANGED
@@ -43,6 +43,12 @@ const validators_1 = require("./validators");
43
43
  const retry_1 = require("./retry");
44
44
  const config_1 = require("./config");
45
45
  const errors_1 = require("./errors");
46
+ const MessageManager_1 = require("./managers/MessageManager");
47
+ const GroupManager_1 = require("./managers/GroupManager");
48
+ const ContactManager_1 = require("./managers/ContactManager");
49
+ const DeviceManager_1 = require("./managers/DeviceManager");
50
+ const AccountManager_1 = require("./managers/AccountManager");
51
+ const StickerManager_1 = require("./managers/StickerManager");
46
52
  class SignalCli extends events_1.EventEmitter {
47
53
  constructor(accountOrPath, account, config = {}) {
48
54
  super();
@@ -56,7 +62,7 @@ class SignalCli extends events_1.EventEmitter {
56
62
  this.logger = new config_1.Logger({
57
63
  level: this.config.verbose ? 'debug' : 'info',
58
64
  enableFile: !!this.config.logFile,
59
- filePath: this.config.logFile
65
+ filePath: this.config.logFile,
60
66
  });
61
67
  // Initialize rate limiter
62
68
  this.rateLimiter = new retry_1.RateLimiter(this.config.maxConcurrentRequests, this.config.minRequestInterval);
@@ -86,6 +92,25 @@ class SignalCli extends events_1.EventEmitter {
86
92
  }
87
93
  this.signalCliPath = signalCliPath || defaultPath;
88
94
  this.account = phoneNumber;
95
+ // Validate account if provided
96
+ if (this.account) {
97
+ (0, validators_1.validatePhoneNumber)(this.account);
98
+ }
99
+ // Validate CLI path
100
+ (0, validators_1.validateSanitizedString)(this.signalCliPath, 'signalCliPath');
101
+ // Initialize Managers
102
+ const rpcCall = (method, params) => {
103
+ if (params === undefined) {
104
+ return this.sendJsonRpcRequest(method);
105
+ }
106
+ return this.sendJsonRpcRequest(method, params);
107
+ };
108
+ this.messages = new MessageManager_1.MessageManager(rpcCall, this.account, this.logger, this.config);
109
+ this.groups = new GroupManager_1.GroupManager(rpcCall, this.account, this.logger, this.config);
110
+ this.contacts = new ContactManager_1.ContactManager(rpcCall, this.account, this.logger, this.config);
111
+ this.devices = new DeviceManager_1.DeviceManager(rpcCall, this.account, this.logger, this.config, this.signalCliPath);
112
+ this.accounts = new AccountManager_1.AccountManager(rpcCall, this.account, this.logger, this.config);
113
+ this.stickers = new StickerManager_1.StickerManager(rpcCall, this.account, this.logger, this.config);
89
114
  }
90
115
  async connect() {
91
116
  const daemonMode = this.config.daemonMode || 'json-rpc';
@@ -117,10 +142,7 @@ class SignalCli extends events_1.EventEmitter {
117
142
  }
118
143
  this.cliProcess.stdout?.on('data', (data) => this.handleRpcResponse(data.toString('utf8')));
119
144
  this.cliProcess.stderr?.on('data', (data) => this.handleStderrData(data.toString('utf8')));
120
- this.cliProcess.on('close', (code) => {
121
- this.emit('close', code);
122
- this.cliProcess = null;
123
- });
145
+ this.cliProcess.on('close', (code) => this.handleProcessClose(code));
124
146
  this.cliProcess.on('error', (err) => this.emit('error', err));
125
147
  return new Promise((resolve, reject) => {
126
148
  // Set a timeout to resolve the promise even if no data is received
@@ -146,7 +168,8 @@ class SignalCli extends events_1.EventEmitter {
146
168
  // If the process exits immediately, that's an error
147
169
  this.cliProcess?.once('close', (code) => {
148
170
  clearTimeout(connectTimeout);
149
- if (code !== 0) {
171
+ if (code !== 0 && !this.cliProcess) {
172
+ // Only reject if not trying to reconnect or already null
150
173
  reject(new Error(`signal-cli exited with code ${code}`));
151
174
  }
152
175
  });
@@ -220,12 +243,12 @@ class SignalCli extends events_1.EventEmitter {
220
243
  method: 'POST',
221
244
  headers: {
222
245
  'Content-Type': 'application/json',
223
- 'Content-Length': Buffer.byteLength(data)
224
- }
246
+ 'Content-Length': Buffer.byteLength(data),
247
+ },
225
248
  };
226
249
  const req = https.request(url, options, (res) => {
227
250
  let body = '';
228
- res.on('data', (chunk) => body += chunk);
251
+ res.on('data', (chunk) => (body += chunk));
229
252
  res.on('end', () => {
230
253
  try {
231
254
  const response = JSON.parse(body);
@@ -317,6 +340,9 @@ class SignalCli extends events_1.EventEmitter {
317
340
  this.emit('notification', response);
318
341
  if (response.method === 'receive') {
319
342
  this.emit('message', response.params);
343
+ if (response.params && response.params.envelope) {
344
+ this.emitDetailedEvents(response.params.envelope);
345
+ }
320
346
  }
321
347
  }
322
348
  }
@@ -325,6 +351,72 @@ class SignalCli extends events_1.EventEmitter {
325
351
  }
326
352
  }
327
353
  }
354
+ emitDetailedEvents(envelope) {
355
+ const source = envelope.source || envelope.sourceNumber;
356
+ const timestamp = envelope.timestamp;
357
+ // 1. Reaction
358
+ if (envelope.dataMessage?.reaction) {
359
+ this.emit('reaction', {
360
+ emoji: envelope.dataMessage.reaction.emoji,
361
+ sender: source,
362
+ timestamp: timestamp,
363
+ targetAuthor: envelope.dataMessage.reaction.targetAuthor,
364
+ targetTimestamp: envelope.dataMessage.reaction.targetSentTimestamp,
365
+ isRemove: envelope.dataMessage.reaction.isRemove,
366
+ });
367
+ }
368
+ // 2. Receipt
369
+ if (envelope.receiptMessage) {
370
+ this.emit('receipt', {
371
+ sender: source,
372
+ timestamp: timestamp,
373
+ type: envelope.receiptMessage.type,
374
+ timestamps: envelope.receiptMessage.timestamps,
375
+ when: envelope.receiptMessage.when,
376
+ });
377
+ }
378
+ // 3. Typing
379
+ if (envelope.typingMessage) {
380
+ this.emit('typing', {
381
+ sender: source,
382
+ timestamp: timestamp,
383
+ action: envelope.typingMessage.action,
384
+ groupId: envelope.typingMessage.groupId,
385
+ });
386
+ }
387
+ // 4. Story (if supported in future or present)
388
+ if (envelope.storyMessage) {
389
+ this.emit('story', {
390
+ sender: source,
391
+ timestamp: timestamp,
392
+ ...envelope.storyMessage,
393
+ });
394
+ }
395
+ }
396
+ async handleProcessClose(code) {
397
+ this.cliProcess = null;
398
+ this.emit('close', code);
399
+ // Auto-reconnect logic if not explicitly disconnected
400
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
401
+ this.reconnectAttempts++;
402
+ // Exponential backoff: 1s, 2s, 4s, 8s, 16s...
403
+ const delay = Math.pow(2, this.reconnectAttempts - 1) * 1000;
404
+ this.logger.warn(`signal-cli process closed (code ${code}). Reconnecting in ${delay}ms... (Attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
405
+ setTimeout(async () => {
406
+ try {
407
+ await this.connect();
408
+ this.reconnectAttempts = 0; // Reset on success
409
+ this.logger.info('Reconnected to signal-cli successfully');
410
+ }
411
+ catch (error) {
412
+ this.logger.error('Reconnection attempt failed:', error);
413
+ }
414
+ }, delay);
415
+ }
416
+ else {
417
+ this.logger.error(`Max reconnection attempts reached (${this.maxReconnectAttempts}). Manual intervention required.`);
418
+ }
419
+ }
328
420
  handleStderrData(data) {
329
421
  const message = data.trim();
330
422
  if (!message)
@@ -342,12 +434,13 @@ class SignalCli extends events_1.EventEmitter {
342
434
  // Emit as a warning event for non-error messages
343
435
  this.emit('log', { level: logLevel.toLowerCase(), message });
344
436
  // Filter out common informational WARN messages from signal-cli
345
- const isInformationalWarn = logLevel === 'WARN' && (message.includes('Failed to get sender certificate') ||
346
- message.includes('ignoring: java.lang.InterruptedException') ||
347
- message.includes('Request was interrupted') ||
348
- message.includes('Connection reset') ||
349
- message.includes('Socket closed') ||
350
- message.includes('gracefully closing'));
437
+ const isInformationalWarn = logLevel === 'WARN' &&
438
+ (message.includes('Failed to get sender certificate') ||
439
+ message.includes('ignoring: java.lang.InterruptedException') ||
440
+ message.includes('Request was interrupted') ||
441
+ message.includes('Connection reset') ||
442
+ message.includes('Socket closed') ||
443
+ message.includes('gracefully closing'));
351
444
  // Only log actual warning messages, not informational ones
352
445
  if (logLevel === 'WARN' && !isInformationalWarn) {
353
446
  console.warn(`[signal-cli] ${message}`);
@@ -378,29 +471,6 @@ class SignalCli extends events_1.EventEmitter {
378
471
  };
379
472
  return await this.httpRequest(request);
380
473
  }
381
- // For socket modes (Unix socket, TCP), write to socket
382
- if (daemonMode === 'unix-socket' || daemonMode === 'tcp') {
383
- const socket = this.socket;
384
- if (!socket || socket.destroyed) {
385
- throw new errors_1.ConnectionError('Not connected. Call connect() first.');
386
- }
387
- const id = (0, uuid_1.v4)();
388
- const request = {
389
- jsonrpc: '2.0',
390
- method,
391
- params,
392
- id,
393
- };
394
- const promise = new Promise((resolve, reject) => {
395
- this.requestPromises.set(id, { resolve, reject });
396
- });
397
- socket.write(JSON.stringify(request) + '\n');
398
- return promise;
399
- }
400
- // Default JSON-RPC mode with stdin/stdout
401
- if (!this.cliProcess || !this.cliProcess.stdin) {
402
- throw new errors_1.ConnectionError('Not connected. Call connect() first.');
403
- }
404
474
  const id = (0, uuid_1.v4)();
405
475
  const request = {
406
476
  jsonrpc: '2.0',
@@ -408,203 +478,113 @@ class SignalCli extends events_1.EventEmitter {
408
478
  params,
409
479
  id,
410
480
  };
411
- const promise = new Promise((resolve, reject) => {
412
- this.requestPromises.set(id, { resolve, reject });
481
+ const executeRequest = () => {
482
+ // For socket modes (Unix socket, TCP), write to socket
483
+ if (daemonMode === 'unix-socket' || daemonMode === 'tcp') {
484
+ const socket = this.socket;
485
+ if (!socket || socket.destroyed) {
486
+ throw new errors_1.ConnectionError('Not connected. Call connect() first.');
487
+ }
488
+ const promise = new Promise((resolve, reject) => {
489
+ this.requestPromises.set(id, { resolve, reject });
490
+ });
491
+ socket.write(JSON.stringify(request) + '\n');
492
+ return promise;
493
+ }
494
+ // Default JSON-RPC mode with stdin/stdout
495
+ if (!this.cliProcess || !this.cliProcess.stdin) {
496
+ throw new errors_1.ConnectionError('Not connected. Call connect() first.');
497
+ }
498
+ const promise = new Promise((resolve, reject) => {
499
+ this.requestPromises.set(id, { resolve, reject });
500
+ });
501
+ this.cliProcess.stdin.write(JSON.stringify(request) + '\n');
502
+ return promise;
503
+ };
504
+ // Standardized timeout for all RPC requests
505
+ const timeoutPromise = new Promise((_, reject) => {
506
+ setTimeout(() => {
507
+ this.requestPromises.delete(id);
508
+ reject(new errors_1.ConnectionError(`RPC request timeout: ${method} (${this.config.requestTimeout}ms)`));
509
+ }, this.config.requestTimeout);
413
510
  });
414
- this.cliProcess.stdin.write(JSON.stringify(request) + '\n');
415
- return promise;
511
+ return Promise.race([executeRequest(), timeoutPromise]);
416
512
  }
417
513
  isGroupId(recipient) {
418
- return recipient.includes('=') || recipient.includes('/') || (recipient.includes('+') && !recipient.startsWith('+'));
514
+ return (recipient.includes('=') ||
515
+ recipient.includes('/') ||
516
+ (recipient.includes('+') && !recipient.startsWith('+')));
419
517
  }
420
518
  // ############# Refactored Methods #############
421
519
  async register(number, voice, captcha) {
422
- await this.sendJsonRpcRequest('register', { account: number, voice, captcha });
520
+ return this.accounts.register(number, voice, captcha);
423
521
  }
424
522
  async verify(number, token, pin) {
425
- await this.sendJsonRpcRequest('verify', { account: number, token, pin });
523
+ return this.accounts.verify(number, token, pin);
426
524
  }
427
525
  async sendMessage(recipient, message, options = {}) {
428
- const params = {
429
- message,
430
- account: this.account
431
- };
432
- // Add recipient information
433
- if (this.isGroupId(recipient)) {
434
- params.groupId = recipient;
435
- }
436
- else {
437
- params.recipients = [recipient];
438
- }
439
- // Add well-known options
440
- if (options.attachments && options.attachments.length > 0) {
441
- params.attachments = options.attachments;
442
- }
443
- if (options.expiresInSeconds) {
444
- params.expiresInSeconds = options.expiresInSeconds;
445
- }
446
- if (options.isViewOnce) {
447
- params.viewOnce = options.isViewOnce;
448
- }
449
- // Add advanced text formatting options
450
- if (options.mentions && options.mentions.length > 0) {
451
- params.mentions = options.mentions.map(m => ({
452
- start: m.start,
453
- length: m.length,
454
- number: m.recipient || m.number
455
- }));
456
- }
457
- if (options.textStyles && options.textStyles.length > 0) {
458
- params.textStyles = options.textStyles.map(ts => ({
459
- start: ts.start,
460
- length: ts.length,
461
- style: ts.style
462
- }));
463
- }
464
- // Add quote/reply information
465
- if (options.quote) {
466
- params.quoteTimestamp = options.quote.timestamp;
467
- params.quoteAuthor = options.quote.author;
468
- if (options.quote.text) {
469
- params.quoteMessage = options.quote.text;
470
- }
471
- if (options.quote.mentions && options.quote.mentions.length > 0) {
472
- params.quoteMentions = options.quote.mentions.map(m => ({
473
- start: m.start,
474
- length: m.length,
475
- number: m.recipient || m.number
476
- }));
477
- }
478
- if (options.quote.textStyles && options.quote.textStyles.length > 0) {
479
- params.quoteTextStyles = options.quote.textStyles.map(ts => ({
480
- start: ts.start,
481
- length: ts.length,
482
- style: ts.style
483
- }));
484
- }
485
- }
486
- // Add preview URL
487
- if (options.previewUrl) {
488
- params.previewUrl = options.previewUrl;
489
- }
490
- // Add edit timestamp for editing existing messages
491
- if (options.editTimestamp) {
492
- params.editTimestamp = options.editTimestamp;
493
- }
494
- // Add story reply information
495
- if (options.storyTimestamp && options.storyAuthor) {
496
- params.storyTimestamp = options.storyTimestamp;
497
- params.storyAuthor = options.storyAuthor;
498
- }
499
- // Add special flags
500
- if (options.noteToSelf) {
501
- params.noteToSelf = options.noteToSelf;
502
- }
503
- if (options.endSession) {
504
- params.endSession = options.endSession;
505
- }
506
- return this.sendJsonRpcRequest('send', params);
507
- }
508
- async sendReaction(recipient, targetAuthor, targetTimestamp, emoji, remove = false) {
509
- const params = {
510
- emoji,
511
- targetAuthor,
512
- targetTimestamp,
513
- remove,
514
- account: this.account
515
- };
516
- if (this.isGroupId(recipient)) {
517
- params.groupId = recipient;
518
- }
519
- else {
520
- params.recipients = [recipient];
521
- }
522
- return this.sendJsonRpcRequest('sendReaction', params);
526
+ return this.messages.sendMessage(recipient, message, options);
527
+ }
528
+ async sendReaction(recipient, targetAuthor, targetTimestamp, emoji, remove = false, isStory = false) {
529
+ return this.messages.sendReaction(recipient, targetAuthor, targetTimestamp, emoji, remove, isStory);
523
530
  }
524
531
  async sendTyping(recipient, stop = false) {
525
- const params = { when: !stop, account: this.account };
526
- if (this.isGroupId(recipient)) {
527
- params.groupId = recipient;
528
- }
529
- else {
530
- params.recipients = [recipient];
531
- }
532
- await this.sendJsonRpcRequest('sendTyping', params);
532
+ return this.messages.sendTyping(recipient, stop);
533
533
  }
534
534
  async remoteDeleteMessage(recipient, targetTimestamp) {
535
- const params = { targetTimestamp, account: this.account };
536
- if (this.isGroupId(recipient)) {
537
- params.groupId = recipient;
538
- }
539
- else {
540
- params.recipients = [recipient];
541
- }
542
- await this.sendJsonRpcRequest('remoteDelete', params);
535
+ return this.messages.remoteDeleteMessage(recipient, targetTimestamp);
543
536
  }
544
537
  async updateContact(number, name, options = {}) {
545
- await this.sendJsonRpcRequest('updateContact', { account: this.account, recipient: number, name, ...options });
538
+ return this.contacts.updateContact(number, name, options);
546
539
  }
547
540
  async block(recipients, groupId) {
548
- await this.sendJsonRpcRequest('block', { account: this.account, recipient: recipients, groupId });
541
+ return this.contacts.block(recipients, groupId);
549
542
  }
550
543
  async unblock(recipients, groupId) {
551
- await this.sendJsonRpcRequest('unblock', { account: this.account, recipient: recipients, groupId });
544
+ return this.contacts.unblock(recipients, groupId);
552
545
  }
553
546
  async quitGroup(groupId) {
554
- await this.sendJsonRpcRequest('quitGroup', { account: this.account, groupId });
547
+ return this.groups.quitGroup(groupId);
555
548
  }
556
549
  async joinGroup(uri) {
557
- await this.sendJsonRpcRequest('joinGroup', { account: this.account, uri });
550
+ return this.groups.joinGroup(uri);
558
551
  }
559
- async updateProfile(name, about, aboutEmoji, avatar) {
560
- await this.sendJsonRpcRequest('updateProfile', { account: this.account, name, about, aboutEmoji, avatar });
552
+ async updateProfile(name, about, aboutEmoji, avatar, options = {}) {
553
+ return this.accounts.updateProfile(name, about, aboutEmoji, avatar, options);
561
554
  }
562
555
  async sendReceipt(recipient, targetTimestamp, type = 'read') {
563
- await this.sendJsonRpcRequest('sendReceipt', { account: this.account, recipient, targetTimestamp, type });
556
+ return this.messages.sendReceipt(recipient, targetTimestamp, type);
564
557
  }
565
558
  async listStickerPacks() {
566
- return this.sendJsonRpcRequest('listStickerPacks', { account: this.account });
559
+ return this.stickers.listStickerPacks();
567
560
  }
568
561
  async addStickerPack(packId, packKey) {
569
- await this.sendJsonRpcRequest('addStickerPack', { account: this.account, packId, packKey });
562
+ return this.stickers.addStickerPack(packId, packKey);
570
563
  }
571
564
  async unregister() {
572
- await this.sendJsonRpcRequest('unregister', { account: this.account });
565
+ return this.accounts.unregister();
573
566
  }
574
567
  async deleteLocalAccountData() {
575
- await this.sendJsonRpcRequest('deleteLocalAccountData', { account: this.account });
568
+ return this.accounts.deleteLocalAccountData();
576
569
  }
577
570
  async updateAccountConfiguration(config) {
578
- await this.sendJsonRpcRequest('updateConfiguration', { account: this.account, ...config });
571
+ return this.accounts.updateAccountConfiguration(config);
579
572
  }
580
573
  async removeDevice(deviceId) {
581
- await this.sendJsonRpcRequest('removeDevice', { account: this.account, deviceId });
574
+ return this.devices.removeDevice(deviceId);
582
575
  }
583
576
  async setPin(pin) {
584
- await this.sendJsonRpcRequest('setPin', { account: this.account, pin });
577
+ return this.accounts.setPin(pin);
585
578
  }
586
579
  async removePin() {
587
- await this.sendJsonRpcRequest('removePin', { account: this.account });
580
+ return this.accounts.removePin();
588
581
  }
589
582
  async listIdentities(number) {
590
- return this.sendJsonRpcRequest('listIdentities', { account: this.account, number });
583
+ return this.contacts.listIdentities(number);
591
584
  }
592
585
  async trustIdentity(number, safetyNumber, verified = true) {
593
- await this.sendJsonRpcRequest('trust', { account: this.account, recipient: number, safetyNumber, verified });
586
+ return this.contacts.trustIdentity(number, safetyNumber, verified);
594
587
  }
595
- /**
596
- * Get the safety number for a specific contact.
597
- * This is a helper method that extracts just the safety number from identity information.
598
- *
599
- * @param number - The phone number of the contact
600
- * @returns The safety number string, or null if not found
601
- *
602
- * @example
603
- * ```typescript
604
- * const safetyNumber = await signal.getSafetyNumber('+33123456789');
605
- * console.log(`Safety number: ${safetyNumber}`);
606
- * ```
607
- */
608
588
  async getSafetyNumber(number) {
609
589
  const identities = await this.listIdentities(number);
610
590
  if (identities.length > 0 && identities[0].safetyNumber) {
@@ -612,30 +592,11 @@ class SignalCli extends events_1.EventEmitter {
612
592
  }
613
593
  return null;
614
594
  }
615
- /**
616
- * Verify a safety number for a contact.
617
- * Checks if the provided safety number matches the stored one and marks it as trusted if it does.
618
- *
619
- * @param number - The phone number of the contact
620
- * @param safetyNumber - The safety number to verify
621
- * @returns True if the safety number matches and was trusted, false otherwise
622
- *
623
- * @example
624
- * ```typescript
625
- * const verified = await signal.verifySafetyNumber('+33123456789', '123456 78901...');
626
- * if (verified) {
627
- * console.log('Safety number verified and trusted');
628
- * } else {
629
- * console.log('Safety number does not match!');
630
- * }
631
- * ```
632
- */
633
595
  async verifySafetyNumber(number, safetyNumber) {
634
596
  const storedSafetyNumber = await this.getSafetyNumber(number);
635
597
  if (!storedSafetyNumber) {
636
598
  return false;
637
599
  }
638
- // Compare safety numbers (remove spaces for comparison)
639
600
  const normalizedStored = storedSafetyNumber.replace(/\s/g, '');
640
601
  const normalizedProvided = safetyNumber.replace(/\s/g, '');
641
602
  if (normalizedStored === normalizedProvided) {
@@ -644,24 +605,9 @@ class SignalCli extends events_1.EventEmitter {
644
605
  }
645
606
  return false;
646
607
  }
647
- /**
648
- * List all untrusted identities.
649
- * Returns identities that have not been explicitly trusted.
650
- *
651
- * @returns Array of untrusted identity keys
652
- *
653
- * @example
654
- * ```typescript
655
- * const untrusted = await signal.listUntrustedIdentities();
656
- * console.log(`Found ${untrusted.length} untrusted identities`);
657
- * untrusted.forEach(id => {
658
- * console.log(`${id.number}: ${id.safetyNumber}`);
659
- * });
660
- * ```
661
- */
662
608
  async listUntrustedIdentities() {
663
609
  const allIdentities = await this.listIdentities();
664
- return allIdentities.filter(identity => identity.trustLevel === 'UNTRUSTED' ||
610
+ return allIdentities.filter((identity) => identity.trustLevel === 'UNTRUSTED' ||
665
611
  identity.trustLevel === 'TRUST_ON_FIRST_USE' ||
666
612
  !identity.trustLevel);
667
613
  }
@@ -669,245 +615,63 @@ class SignalCli extends events_1.EventEmitter {
669
615
  const result = await this.sendJsonRpcRequest('link', { deviceName });
670
616
  return result.uri;
671
617
  }
672
- /**
673
- * Link a new device to an existing Signal account with QR code support.
674
- * This method provides a complete device linking solution with QR code generation.
675
- *
676
- * @param options - Linking options including device name and QR code output preferences
677
- * @returns Promise resolving to LinkingResult with QR code data and linking status
678
- */
679
618
  async deviceLink(options = {}) {
680
- const { spawn } = await Promise.resolve().then(() => __importStar(require('child_process')));
681
- return new Promise((resolve, reject) => {
682
- const deviceName = options.name || 'Signal SDK Device';
683
- // Spawn signal-cli link command
684
- let linkProcess;
685
- if (process.platform === 'win32') {
686
- // On Windows, use cmd.exe to run the batch file with proper quoting for paths with spaces
687
- linkProcess = spawn('cmd.exe', ['/c', `"${this.signalCliPath}"`, 'link', '--name', deviceName], {
688
- stdio: ['pipe', 'pipe', 'pipe']
689
- });
690
- }
691
- else {
692
- linkProcess = spawn(this.signalCliPath, ['link', '--name', deviceName], {
693
- stdio: ['pipe', 'pipe', 'pipe']
694
- });
695
- }
696
- let qrCodeData;
697
- let linkingComplete = false;
698
- let hasError = false;
699
- // Handle stdout (where QR code URI will be)
700
- linkProcess.stdout.on('data', (data) => {
701
- const output = data.toString('utf8').trim();
702
- // Look for QR code URI (starts with sgnl://)
703
- if (output.includes('sgnl://')) {
704
- const uriMatch = output.match(/sgnl:\/\/[^\s]+/);
705
- if (uriMatch && !qrCodeData) {
706
- const uri = uriMatch[0];
707
- qrCodeData = {
708
- uri
709
- };
710
- // Auto-display QR code if requested
711
- if (options.qrCodeOutput === 'console') {
712
- console.log('\n- QR CODE - SCAN WITH YOUR PHONE:');
713
- console.log('===================================');
714
- this.displayQRCode(uri);
715
- console.log('===================================\n');
716
- }
717
- }
718
- }
719
- // Check for successful linking
720
- if (output.includes('Device registered') || output.includes('Successfully linked')) {
721
- linkingComplete = true;
722
- }
723
- });
724
- // Handle stderr for errors
725
- linkProcess.stderr.on('data', (data) => {
726
- const error = data.toString('utf8').trim();
727
- // Filter out informational messages
728
- if (!error.includes('INFO') && !error.includes('DEBUG') && error.length > 0) {
729
- hasError = true;
730
- }
731
- });
732
- // Handle process exit
733
- linkProcess.on('close', (code) => {
734
- if (code === 0 && linkingComplete) {
735
- resolve({
736
- success: true,
737
- isLinked: true,
738
- deviceName,
739
- qrCode: qrCodeData
740
- });
741
- }
742
- else if (code === 0 && qrCodeData) {
743
- resolve({
744
- success: true,
745
- isLinked: false,
746
- deviceName,
747
- qrCode: qrCodeData
748
- });
749
- }
750
- else {
751
- resolve({
752
- success: false,
753
- error: hasError ? 'Device linking failed' : `signal-cli exited with code ${code}`,
754
- qrCode: qrCodeData
755
- });
756
- }
757
- });
758
- // Handle process errors
759
- linkProcess.on('error', (error) => {
760
- reject(new Error(`Failed to start device linking: ${error.message}`));
761
- });
762
- });
619
+ return this.devices.deviceLink(options);
763
620
  }
764
- /**
765
- * Display ASCII QR code in console.
766
- * Uses qrcode-terminal which is included as a dependency.
767
- */
768
621
  displayQRCode(uri) {
769
622
  qrcodeTerminal.generate(uri, { small: true });
770
623
  }
771
624
  async addDevice(uri, deviceName) {
772
- await this.sendJsonRpcRequest('addDevice', { account: this.account, uri, deviceName });
625
+ return this.devices.addDevice(uri, deviceName);
773
626
  }
774
627
  async sendSyncRequest() {
775
628
  await this.sendJsonRpcRequest('sendSyncRequest', { account: this.account });
776
629
  }
777
630
  async sendMessageRequestResponse(recipient, response) {
778
- await this.sendJsonRpcRequest('sendMessageRequestResponse', { account: this.account, recipient, type: response });
631
+ await this.sendJsonRpcRequest('sendMessageRequestResponse', {
632
+ account: this.account,
633
+ recipient,
634
+ type: response,
635
+ });
779
636
  }
780
637
  async getVersion() {
781
638
  return this.sendJsonRpcRequest('version');
782
639
  }
783
640
  async createGroup(name, members) {
784
- return this.sendJsonRpcRequest('updateGroup', { account: this.account, name, members });
641
+ return this.groups.createGroup(name, members);
785
642
  }
786
643
  async updateGroup(groupId, options) {
787
- const params = { groupId, account: this.account };
788
- if (options.name)
789
- params.name = options.name;
790
- if (options.description)
791
- params.description = options.description;
792
- if (options.avatar)
793
- params.avatar = options.avatar;
794
- if (options.addMembers)
795
- params.addMembers = options.addMembers;
796
- if (options.removeMembers)
797
- params.removeMembers = options.removeMembers;
798
- if (options.promoteAdmins)
799
- params.promoteAdmins = options.promoteAdmins;
800
- if (options.demoteAdmins)
801
- params.demoteAdmins = options.demoteAdmins;
802
- if (options.banMembers)
803
- params.banMembers = options.banMembers;
804
- if (options.unbanMembers)
805
- params.unbanMembers = options.unbanMembers;
806
- if (options.resetInviteLink)
807
- params.resetLink = true;
808
- if (options.permissionAddMember)
809
- params.permissionAddMember = options.permissionAddMember;
810
- if (options.permissionEditDetails)
811
- params.permissionEditDetails = options.permissionEditDetails;
812
- if (options.permissionSendMessage)
813
- params.permissionSendMessage = options.permissionSendMessage;
814
- if (options.expirationTimer)
815
- params.expiration = options.expirationTimer;
816
- await this.sendJsonRpcRequest('updateGroup', params);
644
+ return this.groups.updateGroup(groupId, options);
817
645
  }
818
646
  async listGroups() {
819
- return this.sendJsonRpcRequest('listGroups', { account: this.account });
647
+ return this.groups.listGroups();
820
648
  }
821
- /**
822
- * Send group invite link to a recipient.
823
- * Retrieves and sends the invitation link for a group.
824
- *
825
- * @param groupId - The group ID
826
- * @param recipient - The recipient to send the invite link to
827
- * @returns Send response
828
- *
829
- * @example
830
- * ```typescript
831
- * await signal.sendGroupInviteLink('groupId123==', '+33123456789');
832
- * ```
833
- */
834
649
  async sendGroupInviteLink(groupId, recipient) {
835
- // Get group info to retrieve invite link
836
650
  const groups = await this.listGroups();
837
- const group = groups.find(g => g.groupId === groupId);
651
+ const group = groups.find((g) => g.groupId === groupId);
838
652
  const inviteLink = group?.groupInviteLink || group?.inviteLink;
839
653
  if (!group || !inviteLink) {
840
654
  throw new Error('Group not found or does not have an invite link');
841
655
  }
842
656
  return this.sendMessage(recipient, `Join our group: ${inviteLink}`);
843
657
  }
844
- /**
845
- * Set banned members for a group.
846
- * Ban specific members from the group.
847
- *
848
- * @param groupId - The group ID
849
- * @param members - Array of phone numbers to ban
850
- *
851
- * @example
852
- * ```typescript
853
- * await signal.setBannedMembers('groupId123==', ['+33111111111', '+33222222222']);
854
- * ```
855
- */
856
658
  async setBannedMembers(groupId, members) {
857
659
  await this.updateGroup(groupId, { banMembers: members });
858
660
  }
859
- /**
860
- * Reset group invite link.
861
- * Invalidates the current group invite link and generates a new one.
862
- *
863
- * @param groupId - The group ID
864
- *
865
- * @example
866
- * ```typescript
867
- * await signal.resetGroupLink('groupId123==');
868
- * ```
869
- */
870
661
  async resetGroupLink(groupId) {
871
662
  await this.updateGroup(groupId, { resetInviteLink: true });
872
663
  }
873
664
  async listContacts() {
874
- return this.sendJsonRpcRequest('listContacts', { account: this.account });
665
+ return this.contacts.listContacts();
875
666
  }
876
667
  async listDevices() {
877
- return this.sendJsonRpcRequest('listDevices', { account: this.account });
668
+ return this.devices.listDevices();
878
669
  }
879
- /**
880
- * Update a linked device name (signal-cli v0.13.23+).
881
- * Allows changing the display name of a linked device.
882
- *
883
- * @param options - Device update options
884
- * @returns Promise that resolves when device is updated
885
- *
886
- * @example
887
- * ```typescript
888
- * // List devices to get device IDs
889
- * const devices = await signal.listDevices();
890
- *
891
- * // Update device name
892
- * await signal.updateDevice({
893
- * deviceId: 2,
894
- * deviceName: 'My New Device Name'
895
- * });
896
- * ```
897
- */
898
670
  async updateDevice(options) {
899
- this.logger.debug('Updating device', options);
900
- (0, validators_1.validateDeviceId)(options.deviceId);
901
- (0, validators_1.validateMessage)(options.deviceName, 200);
902
- await this.sendJsonRpcRequest('updateDevice', {
903
- deviceId: options.deviceId,
904
- deviceName: options.deviceName,
905
- account: this.account
906
- });
671
+ return this.devices.updateDevice(options);
907
672
  }
908
673
  async listAccounts() {
909
- const result = await this.sendJsonRpcRequest('listAccounts');
910
- return result.accounts.map((acc) => acc.number);
674
+ return this.accounts.listAccounts();
911
675
  }
912
676
  // ############# Deprecated Methods (Kept for backward compatibility) #############
913
677
  /**
@@ -917,8 +681,8 @@ class SignalCli extends events_1.EventEmitter {
917
681
  async receiveMessages() {
918
682
  console.warn("receiveMessages is deprecated and will be removed in a future version. Use connect() and listen for 'message' events instead.");
919
683
  // Return empty array but log helpful migration info
920
- console.info("Migration guide: Replace receiveMessages() with:");
921
- console.info(" await signalCli.connect();");
684
+ console.info('Migration guide: Replace receiveMessages() with:');
685
+ console.info(' await signalCli.connect();');
922
686
  console.info(" signalCli.on('message', (msg) => { /* handle message */ });");
923
687
  return Promise.resolve([]);
924
688
  }
@@ -927,7 +691,7 @@ class SignalCli extends events_1.EventEmitter {
927
691
  * This method now calls connect() for backward compatibility.
928
692
  */
929
693
  startDaemon() {
930
- console.warn("startDaemon is deprecated. Use connect() instead.");
694
+ console.warn('startDaemon is deprecated. Use connect() instead.');
931
695
  this.connect();
932
696
  }
933
697
  /**
@@ -935,7 +699,7 @@ class SignalCli extends events_1.EventEmitter {
935
699
  * This method now calls gracefulShutdown() for backward compatibility.
936
700
  */
937
701
  stopDaemon() {
938
- console.warn("stopDaemon is deprecated. Use gracefulShutdown() or disconnect() instead.");
702
+ console.warn('stopDaemon is deprecated. Use gracefulShutdown() or disconnect() instead.');
939
703
  this.gracefulShutdown();
940
704
  }
941
705
  // ############# MESSAGE RECEIVING #############
@@ -961,634 +725,96 @@ class SignalCli extends events_1.EventEmitter {
961
725
  * ```
962
726
  */
963
727
  async receive(options = {}) {
964
- const params = { account: this.account };
965
- // Set timeout (default: 5 seconds)
966
- if (options.timeout !== undefined) {
967
- params.timeout = options.timeout;
968
- }
969
- // Set maximum number of messages
970
- if (options.maxMessages !== undefined) {
971
- params.maxMessages = options.maxMessages;
972
- }
973
- // Skip attachment downloads
974
- if (options.ignoreAttachments) {
975
- params.ignoreAttachments = true;
976
- }
977
- // Skip stories
978
- if (options.ignoreStories) {
979
- params.ignoreStories = true;
980
- }
981
- // Send read receipts automatically
982
- if (options.sendReadReceipts) {
983
- params.sendReadReceipts = true;
984
- }
985
- try {
986
- const result = await this.sendJsonRpcRequest('receive', params);
987
- // Parse and return messages
988
- if (Array.isArray(result)) {
989
- return result.map(envelope => this.parseEnvelope(envelope));
990
- }
991
- return [];
992
- }
993
- catch (error) {
994
- this.logger.error('Failed to receive messages:', error);
995
- throw error;
996
- }
728
+ return this.messages.receive(options);
997
729
  }
998
- /**
999
- * Parse a message envelope from signal-cli into a Message object.
1000
- * @private
1001
- */
1002
730
  parseEnvelope(envelope) {
1003
- const message = {
1004
- timestamp: envelope.timestamp || Date.now(),
1005
- source: envelope.source || envelope.sourceNumber,
1006
- sourceUuid: envelope.sourceUuid,
1007
- sourceDevice: envelope.sourceDevice,
1008
- };
1009
- // Parse data message
1010
- if (envelope.dataMessage) {
1011
- const data = envelope.dataMessage;
1012
- message.text = data.message || data.body;
1013
- message.groupId = data.groupInfo?.groupId;
1014
- message.attachments = data.attachments;
1015
- message.mentions = data.mentions;
1016
- message.quote = data.quote;
1017
- message.reaction = data.reaction;
1018
- message.sticker = data.sticker;
1019
- message.expiresInSeconds = data.expiresInSeconds;
1020
- message.viewOnce = data.viewOnce;
1021
- }
1022
- // Parse sync message
1023
- if (envelope.syncMessage) {
1024
- message.syncMessage = envelope.syncMessage;
1025
- }
1026
- // Parse receipt message
1027
- if (envelope.receiptMessage) {
1028
- message.receipt = envelope.receiptMessage;
1029
- }
1030
- // Parse typing message
1031
- if (envelope.typingMessage) {
1032
- message.typing = envelope.typingMessage;
1033
- }
1034
- return message;
731
+ // Kept for backward compatibility if needed internally, but should use MessageManager.parseEnvelope
732
+ return this.messages.parseEnvelope(envelope);
1035
733
  }
1036
- // ############# NEW FEATURES - Missing signal-cli Commands #############
1037
- /**
1038
- * Remove a contact from the contact list.
1039
- * @param number - The phone number of the contact to remove
1040
- * @param options - Options for how to remove the contact
1041
- */
1042
734
  async removeContact(number, options = {}) {
1043
- const params = {
1044
- account: this.account,
1045
- recipient: number
1046
- };
1047
- if (options.hide)
1048
- params.hide = true;
1049
- if (options.forget)
1050
- params.forget = true;
1051
- await this.sendJsonRpcRequest('removeContact', params);
735
+ return this.contacts.removeContact(number, options);
1052
736
  }
1053
- /**
1054
- * Check if phone numbers are registered with Signal.
1055
- * @param numbers - Array of phone numbers to check
1056
- * @param usernames - Optional array of usernames to check
1057
- * @returns Array of user status results
1058
- */
1059
737
  async getUserStatus(numbers = [], usernames = []) {
1060
- const params = { account: this.account };
1061
- if (numbers.length > 0)
1062
- params.recipients = numbers;
1063
- if (usernames.length > 0)
1064
- params.usernames = usernames;
1065
- const result = await this.sendJsonRpcRequest('getUserStatus', params);
1066
- // Transform the result to match our interface
1067
- const statusResults = [];
1068
- if (result.recipients) {
1069
- result.recipients.forEach((recipient) => {
1070
- statusResults.push({
1071
- number: recipient.number,
1072
- isRegistered: recipient.isRegistered || false,
1073
- uuid: recipient.uuid,
1074
- username: recipient.username
1075
- });
1076
- });
1077
- }
1078
- return statusResults;
738
+ return this.contacts.getUserStatus(numbers, usernames);
1079
739
  }
1080
- /**
1081
- * Send a payment notification to a recipient (MobileCoin).
1082
- * Sends a notification about a cryptocurrency payment made through Signal's MobileCoin integration.
1083
- *
1084
- * @param recipient - The phone number or group ID of the recipient
1085
- * @param paymentData - Payment notification data including receipt and optional note
1086
- * @returns Send result with timestamp
1087
- * @throws {Error} If receipt is invalid or sending fails
1088
- *
1089
- * @example
1090
- * ```typescript
1091
- * const receiptBlob = 'base64EncodedReceiptData...';
1092
- * await signal.sendPaymentNotification('+33612345678', {
1093
- * receipt: receiptBlob,
1094
- * note: 'Thanks for dinner!'
1095
- * });
1096
- * ```
1097
- */
1098
740
  async sendPaymentNotification(recipient, paymentData) {
1099
- this.logger.info(`Sending payment notification to ${recipient}`);
1100
- (0, validators_1.validateRecipient)(recipient);
1101
- if (!paymentData.receipt || paymentData.receipt.trim().length === 0) {
1102
- throw new Error('Payment receipt is required');
1103
- }
1104
- const params = {
1105
- receipt: paymentData.receipt,
1106
- account: this.account
1107
- };
1108
- if (paymentData.note) {
1109
- params.note = paymentData.note;
1110
- }
1111
- if (this.isGroupId(recipient)) {
1112
- params.groupId = recipient;
1113
- }
1114
- else {
1115
- params.recipient = recipient;
1116
- }
1117
- return this.sendJsonRpcRequest('sendPaymentNotification', params);
741
+ return this.accounts.sendPaymentNotification(recipient, paymentData);
1118
742
  }
1119
- /**
1120
- * Upload a custom sticker pack to Signal.
1121
- * @param manifest - Sticker pack manifest information
1122
- * @returns Upload result with pack ID and key
1123
- */
1124
743
  async uploadStickerPack(manifest) {
1125
- const params = {
1126
- account: this.account,
1127
- path: manifest.path
1128
- };
1129
- const result = await this.sendJsonRpcRequest('uploadStickerPack', params);
1130
- return {
1131
- packId: result.packId,
1132
- packKey: result.packKey,
1133
- installUrl: result.installUrl
1134
- };
744
+ return this.stickers.uploadStickerPack(manifest);
1135
745
  }
1136
- /**
1137
- * Submit a rate limit challenge to lift rate limiting.
1138
- * @param challenge - Challenge token from the proof required error
1139
- * @param captcha - Captcha token from solved captcha
1140
- * @returns Challenge result indicating success/failure
1141
- */
1142
746
  async submitRateLimitChallenge(challenge, captcha) {
1143
- const params = {
1144
- account: this.account,
1145
- challenge,
1146
- captcha
1147
- };
1148
- const result = await this.sendJsonRpcRequest('submitRateLimitChallenge', params);
1149
- return {
1150
- success: result.success || false,
1151
- retryAfter: result.retryAfter,
1152
- message: result.message
1153
- };
747
+ return this.accounts.submitRateLimitChallenge(challenge, captcha);
1154
748
  }
1155
- /**
1156
- * Start the phone number change process.
1157
- * Initiates SMS or voice verification for changing your account to a new phone number.
1158
- * After calling this, you must verify the new number with finishChangeNumber().
1159
- *
1160
- * @param newNumber - The new phone number in E164 format (e.g., "+33612345678")
1161
- * @param voice - Use voice verification instead of SMS (default: false)
1162
- * @param captcha - Optional captcha token if required
1163
- * @throws {Error} If not a primary device or rate limited
1164
- */
1165
749
  async startChangeNumber(newNumber, voice = false, captcha) {
1166
- this.logger.info(`Starting change number to ${newNumber} (voice: ${voice})`);
1167
- (0, validators_1.validatePhoneNumber)(newNumber);
1168
- const params = {
1169
- account: this.account,
1170
- number: newNumber,
1171
- voice
1172
- };
1173
- if (captcha)
1174
- params.captcha = captcha;
1175
- await this.sendJsonRpcRequest('startChangeNumber', params);
750
+ return this.accounts.startChangeNumber(newNumber, voice, captcha);
1176
751
  }
1177
- /**
1178
- * Complete the phone number change process.
1179
- * Verifies the code received via SMS or voice and changes your account to the new number.
1180
- * Must be called after startChangeNumber().
1181
- *
1182
- * @param newNumber - The new phone number (same as startChangeNumber)
1183
- * @param verificationCode - The verification code received via SMS or voice
1184
- * @param pin - Optional registration lock PIN if one was set
1185
- * @throws {Error} If verification fails or incorrect PIN
1186
- */
1187
752
  async finishChangeNumber(newNumber, verificationCode, pin) {
1188
- this.logger.info(`Finishing change number to ${newNumber}`);
1189
- (0, validators_1.validatePhoneNumber)(newNumber);
1190
- if (!verificationCode || verificationCode.trim().length === 0) {
1191
- throw new Error('Verification code is required');
1192
- }
1193
- const params = {
1194
- account: this.account,
1195
- number: newNumber,
1196
- verificationCode
1197
- };
1198
- if (pin)
1199
- params.pin = pin;
1200
- await this.sendJsonRpcRequest('finishChangeNumber', params);
753
+ return this.accounts.finishChangeNumber(newNumber, verificationCode, pin);
1201
754
  }
1202
- /**
1203
- * Enhanced send message with progress callback support.
1204
- * @param recipient - Phone number or group ID
1205
- * @param message - Message text
1206
- * @param options - Send options including progress callback
1207
- * @returns Send response
1208
- */
1209
755
  async sendMessageWithProgress(recipient, message, options = {}) {
1210
- // For now, this is the same as sendMessage since signal-cli doesn't provide
1211
- // native progress callbacks. This is a placeholder for future enhancement.
1212
- const { onProgress, ...sendOptions } = options;
1213
- // Simulate progress for large attachments
1214
- if (onProgress && sendOptions.attachments && sendOptions.attachments.length > 0) {
1215
- // Simulate upload progress
1216
- for (let i = 0; i <= 100; i += 10) {
1217
- onProgress({
1218
- total: 100,
1219
- uploaded: i,
1220
- percentage: i
1221
- });
1222
- // Small delay to simulate upload
1223
- await new Promise(resolve => setTimeout(resolve, 50));
1224
- }
1225
- }
1226
- return this.sendMessage(recipient, message, sendOptions);
756
+ return this.messages.sendMessageWithProgress(recipient, message, options);
1227
757
  }
1228
- // ========== NEW METHODS FOR 100% signal-cli COMPATIBILITY ==========
1229
- /**
1230
- * Send a poll create message to a recipient or group.
1231
- * @param options Poll creation options
1232
- * @returns Send response with timestamp
1233
- */
1234
758
  async sendPollCreate(options) {
1235
- this.logger.debug('Sending poll create', options);
1236
- (0, validators_1.validateMessage)(options.question, 500);
1237
- if (!options.options || options.options.length < 2) {
1238
- throw new errors_1.MessageError('Poll must have at least 2 options');
1239
- }
1240
- if (options.options.length > 10) {
1241
- throw new errors_1.MessageError('Poll cannot have more than 10 options');
1242
- }
1243
- const params = {
1244
- question: options.question,
1245
- options: options.options,
1246
- account: this.account
1247
- };
1248
- if (options.multiSelect !== undefined) {
1249
- params.multiSelect = options.multiSelect;
1250
- }
1251
- if (options.groupId) {
1252
- (0, validators_1.validateGroupId)(options.groupId);
1253
- params.groupId = options.groupId;
1254
- }
1255
- else if (options.recipients) {
1256
- params.recipients = options.recipients.map(r => {
1257
- (0, validators_1.validateRecipient)(r);
1258
- return r;
1259
- });
1260
- }
1261
- else {
1262
- throw new errors_1.MessageError('Must specify either recipients or groupId');
1263
- }
1264
- return this.sendJsonRpcRequest('sendPollCreate', params);
759
+ return this.messages.sendPollCreate(options);
1265
760
  }
1266
- /**
1267
- * Send a poll vote message to vote on a poll.
1268
- * @param recipient Recipient or group ID
1269
- * @param options Poll vote options
1270
- * @returns Send response with timestamp
1271
- */
1272
761
  async sendPollVote(recipient, options) {
1273
- this.logger.debug('Sending poll vote', { recipient, options });
1274
- (0, validators_1.validateRecipient)(options.pollAuthor);
1275
- (0, validators_1.validateTimestamp)(options.pollTimestamp);
1276
- if (!options.optionIndexes || options.optionIndexes.length === 0) {
1277
- throw new errors_1.MessageError('Must specify at least one option to vote for');
1278
- }
1279
- const params = {
1280
- pollAuthor: options.pollAuthor,
1281
- pollTimestamp: options.pollTimestamp,
1282
- options: options.optionIndexes,
1283
- account: this.account
1284
- };
1285
- if (options.voteCount !== undefined) {
1286
- params.voteCount = options.voteCount;
1287
- }
1288
- if (this.isGroupId(recipient)) {
1289
- (0, validators_1.validateGroupId)(recipient);
1290
- params.groupId = recipient;
1291
- }
1292
- else {
1293
- (0, validators_1.validateRecipient)(recipient);
1294
- params.recipient = recipient;
1295
- }
1296
- return this.sendJsonRpcRequest('sendPollVote', params);
762
+ return this.messages.sendPollVote(recipient, options);
1297
763
  }
1298
- /**
1299
- * Send a poll terminate message to close a poll.
1300
- * @param recipient Recipient or group ID
1301
- * @param options Poll terminate options
1302
- * @returns Send response with timestamp
1303
- */
1304
764
  async sendPollTerminate(recipient, options) {
1305
- this.logger.debug('Sending poll terminate', { recipient, options });
1306
- (0, validators_1.validateTimestamp)(options.pollTimestamp);
1307
- const params = {
1308
- pollTimestamp: options.pollTimestamp,
1309
- account: this.account
1310
- };
1311
- if (this.isGroupId(recipient)) {
1312
- (0, validators_1.validateGroupId)(recipient);
1313
- params.groupId = recipient;
1314
- }
1315
- else {
1316
- (0, validators_1.validateRecipient)(recipient);
1317
- params.recipient = recipient;
1318
- }
1319
- return this.sendJsonRpcRequest('sendPollTerminate', params);
765
+ return this.messages.sendPollTerminate(recipient, options);
1320
766
  }
1321
- /**
1322
- * Update account information (device name, username, privacy settings).
1323
- * @param options Account update options
1324
- * @returns Account update result
1325
- */
1326
767
  async updateAccount(options) {
1327
- this.logger.debug('Updating account', options);
1328
- const params = { account: this.account };
1329
- if (options.deviceName) {
1330
- params.deviceName = options.deviceName;
1331
- }
1332
- if (options.username) {
1333
- params.username = options.username;
1334
- }
1335
- if (options.deleteUsername) {
1336
- params.deleteUsername = true;
1337
- }
1338
- if (options.unrestrictedUnidentifiedSender !== undefined) {
1339
- params.unrestrictedUnidentifiedSender = options.unrestrictedUnidentifiedSender;
1340
- }
1341
- if (options.discoverableByNumber !== undefined) {
1342
- params.discoverableByNumber = options.discoverableByNumber;
1343
- }
1344
- if (options.numberSharing !== undefined) {
1345
- params.numberSharing = options.numberSharing;
1346
- }
1347
- try {
1348
- const result = await this.sendJsonRpcRequest('updateAccount', params);
1349
- return {
1350
- success: true,
1351
- username: result.username,
1352
- usernameLink: result.usernameLink
1353
- };
1354
- }
1355
- catch (error) {
1356
- return {
1357
- success: false,
1358
- error: error instanceof Error ? error.message : 'Unknown error'
1359
- };
1360
- }
768
+ return this.accounts.updateAccount(options);
1361
769
  }
1362
- /**
1363
- * Set or update the username for this account.
1364
- * Helper method that wraps updateAccount() for simpler username management.
1365
- *
1366
- * @param username - The username to set (without discriminator)
1367
- * @returns Account update result with username and link
1368
- *
1369
- * @example
1370
- * ```typescript
1371
- * const result = await signal.setUsername('myusername');
1372
- * console.log(`Username: ${result.username}`);
1373
- * console.log(`Link: ${result.usernameLink}`);
1374
- * ```
1375
- */
1376
770
  async setUsername(username) {
1377
- return this.updateAccount({ username });
771
+ return this.accounts.updateAccount({ username });
1378
772
  }
1379
- /**
1380
- * Delete the current username from this account.
1381
- * Helper method that wraps updateAccount() for simpler username deletion.
1382
- *
1383
- * @returns Account update result
1384
- *
1385
- * @example
1386
- * ```typescript
1387
- * const result = await signal.deleteUsername();
1388
- * if (result.success) {
1389
- * console.log('Username deleted successfully');
1390
- * }
1391
- * ```
1392
- */
1393
773
  async deleteUsername() {
1394
- return this.updateAccount({ deleteUsername: true });
774
+ return this.accounts.updateAccount({ deleteUsername: true });
1395
775
  }
1396
- /**
1397
- * Get raw attachment data as base64 string.
1398
- * @param options Attachment retrieval options
1399
- * @returns Base64 encoded attachment data
1400
- */
1401
776
  async getAttachment(options) {
1402
- this.logger.debug('Getting attachment', options);
1403
- if (!options.id) {
1404
- throw new errors_1.MessageError('Attachment ID is required');
1405
- }
1406
- const params = {
1407
- id: options.id,
1408
- account: this.account
1409
- };
1410
- if (options.groupId) {
1411
- (0, validators_1.validateGroupId)(options.groupId);
1412
- params.groupId = options.groupId;
1413
- }
1414
- else if (options.recipient) {
1415
- (0, validators_1.validateRecipient)(options.recipient);
1416
- params.recipient = options.recipient;
1417
- }
1418
- const result = await this.sendJsonRpcRequest('getAttachment', params);
1419
- return result.data || result;
777
+ return this.messages.getAttachment(options);
1420
778
  }
1421
- /**
1422
- * Get raw avatar data as base64 string.
1423
- * @param options Avatar retrieval options
1424
- * @returns Base64 encoded avatar data
1425
- */
1426
779
  async getAvatar(options) {
1427
- this.logger.debug('Getting avatar', options);
1428
- const params = { account: this.account };
1429
- if (options.contact) {
1430
- (0, validators_1.validateRecipient)(options.contact);
1431
- params.contact = options.contact;
1432
- }
1433
- else if (options.profile) {
1434
- (0, validators_1.validateRecipient)(options.profile);
1435
- params.profile = options.profile;
1436
- }
1437
- else if (options.groupId) {
1438
- (0, validators_1.validateGroupId)(options.groupId);
1439
- params.groupId = options.groupId;
1440
- }
1441
- else {
1442
- throw new errors_1.MessageError('Must specify contact, profile, or groupId');
1443
- }
1444
- const result = await this.sendJsonRpcRequest('getAvatar', params);
1445
- return result.data || result;
780
+ return this.contacts.getAvatar(options);
1446
781
  }
1447
- /**
1448
- * Get raw sticker data as base64 string.
1449
- * @param options Sticker retrieval options
1450
- * @returns Base64 encoded sticker data
1451
- */
1452
782
  async getSticker(options) {
1453
- this.logger.debug('Getting sticker', options);
1454
- if (!options.packId || !options.stickerId) {
1455
- throw new errors_1.MessageError('Pack ID and sticker ID are required');
1456
- }
1457
- const params = {
1458
- packId: options.packId,
1459
- stickerId: options.stickerId,
1460
- account: this.account
1461
- };
1462
- const result = await this.sendJsonRpcRequest('getSticker', params);
1463
- return result.data || result;
783
+ return this.stickers.getSticker(options);
1464
784
  }
1465
- /**
1466
- * Send contacts synchronization message to linked devices.
1467
- * @param options Contacts sync options
1468
- */
1469
785
  async sendContacts(options = {}) {
1470
- this.logger.debug('Sending contacts sync');
1471
786
  const params = { account: this.account };
1472
- if (options.includeAllRecipients) {
787
+ if (options.includeAllRecipients)
1473
788
  params.allRecipients = true;
1474
- }
1475
789
  await this.sendJsonRpcRequest('sendContacts', params);
1476
790
  }
1477
- /**
1478
- * List groups with optional filtering and details.
1479
- * @param options List groups options
1480
- * @returns Array of group information
1481
- */
1482
791
  async listGroupsDetailed(options = {}) {
1483
- this.logger.debug('Listing groups with options', options);
1484
- const params = { account: this.account };
1485
- if (options.detailed) {
1486
- params.detailed = true;
1487
- }
1488
- if (options.groupIds && options.groupIds.length > 0) {
1489
- params.groupId = options.groupIds;
1490
- }
1491
- return this.sendJsonRpcRequest('listGroups', params);
792
+ return this.groups.listGroupsDetailed(options);
1492
793
  }
1493
- /**
1494
- * List all local accounts.
1495
- * @returns Array of account phone numbers
1496
- */
1497
794
  async listAccountsDetailed() {
1498
- this.logger.debug('Listing all accounts');
1499
- const result = await this.sendJsonRpcRequest('listAccounts');
1500
- return result.accounts || [];
795
+ return this.accounts.listAccountsDetailed();
1501
796
  }
1502
- /**
1503
- * Extract profile information from a contact.
1504
- * Parses givenName, familyName, mobileCoinAddress from profile data.
1505
- *
1506
- * @param contact - The contact object to parse
1507
- * @returns Enhanced contact with extracted profile fields
1508
- *
1509
- * @example
1510
- * ```typescript
1511
- * const contacts = await signal.listContacts();
1512
- * const enriched = signal.parseContactProfile(contacts[0]);
1513
- * console.log(enriched.givenName, enriched.familyName);
1514
- * ```
1515
- */
1516
797
  parseContactProfile(contact) {
1517
- // signal-cli already provides these fields if available
1518
- // This method normalizes and validates the data
1519
- return {
1520
- ...contact,
1521
- givenName: contact.givenName || undefined,
1522
- familyName: contact.familyName || undefined,
1523
- mobileCoinAddress: contact.mobileCoinAddress || undefined,
1524
- profileName: contact.profileName ||
1525
- (contact.givenName && contact.familyName
1526
- ? `${contact.givenName} ${contact.familyName}`
1527
- : contact.givenName || contact.familyName),
1528
- };
798
+ return this.contacts.parseContactProfile(contact);
1529
799
  }
1530
- /**
1531
- * Extract group membership details.
1532
- * Parses pendingMembers, bannedMembers, inviteLink from group data.
1533
- *
1534
- * @param group - The group info to parse
1535
- * @returns Enhanced group with extracted membership fields
1536
- *
1537
- * @example
1538
- * ```typescript
1539
- * const groups = await signal.listGroups();
1540
- * const enriched = signal.parseGroupDetails(groups[0]);
1541
- * console.log(enriched.pendingMembers, enriched.bannedMembers);
1542
- * ```
1543
- */
1544
800
  parseGroupDetails(group) {
1545
- return {
1546
- ...group,
1547
- // Normalize inviteLink field
1548
- inviteLink: group.groupInviteLink || group.inviteLink,
1549
- groupInviteLink: group.groupInviteLink || group.inviteLink,
1550
- // Ensure arrays exist
1551
- pendingMembers: group.pendingMembers || [],
1552
- banned: group.banned || [],
1553
- requestingMembers: group.requestingMembers || [],
1554
- admins: group.admins || [],
1555
- members: group.members || [],
1556
- };
801
+ return this.groups.parseGroupDetails(group);
1557
802
  }
1558
- /**
1559
- * Get enriched contacts list with parsed profile information.
1560
- *
1561
- * @returns Array of contacts with full profile data
1562
- *
1563
- * @example
1564
- * ```typescript
1565
- * const contacts = await signal.getContactsWithProfiles();
1566
- * contacts.forEach(c => {
1567
- * console.log(`${c.givenName} ${c.familyName} - ${c.mobileCoinAddress}`);
1568
- * });
1569
- * ```
1570
- */
1571
803
  async getContactsWithProfiles() {
1572
- const contacts = await this.listContacts();
1573
- return contacts.map(c => this.parseContactProfile(c));
804
+ return this.contacts.getContactsWithProfiles();
1574
805
  }
1575
- /**
1576
- * Get enriched groups list with parsed membership details.
1577
- *
1578
- * @param options - List groups options
1579
- * @returns Array of groups with full membership data
1580
- *
1581
- * @example
1582
- * ```typescript
1583
- * const groups = await signal.getGroupsWithDetails();
1584
- * groups.forEach(g => {
1585
- * console.log(`${g.name}: ${g.members.length} members, ${g.pendingMembers.length} pending`);
1586
- * });
1587
- * ```
1588
- */
1589
806
  async getGroupsWithDetails(options = {}) {
1590
- const groups = await this.listGroupsDetailed(options);
1591
- return groups.map(g => this.parseGroupDetails(g));
807
+ return this.groups.getGroupsWithDetails(options);
808
+ }
809
+ async isRegistered(number) {
810
+ const results = await this.getUserStatus([number]);
811
+ return results.length > 0 && results[0].isRegistered;
812
+ }
813
+ async sendNoteToSelf(message, options = {}) {
814
+ if (!this.account) {
815
+ throw new Error('Account must be configured to send Note to Self');
816
+ }
817
+ return this.sendMessage(this.account, message, { ...options, noteToSelf: true });
1592
818
  }
1593
819
  }
1594
820
  exports.SignalCli = SignalCli;