signal-sdk 0.1.1 → 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 (54) hide show
  1. package/README.md +23 -11
  2. package/dist/MultiAccountManager.js +11 -19
  3. package/dist/SignalBot.js +40 -36
  4. package/dist/SignalCli.d.ts +25 -301
  5. package/dist/SignalCli.js +226 -971
  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 +150 -66
  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 +133 -74
  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 +27 -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
  54. package/scripts/install.js +1 -1
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,216 +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();
669
+ }
670
+ async updateDevice(options) {
671
+ return this.devices.updateDevice(options);
878
672
  }
879
673
  async listAccounts() {
880
- const result = await this.sendJsonRpcRequest('listAccounts');
881
- return result.accounts.map((acc) => acc.number);
674
+ return this.accounts.listAccounts();
882
675
  }
883
676
  // ############# Deprecated Methods (Kept for backward compatibility) #############
884
677
  /**
@@ -888,8 +681,8 @@ class SignalCli extends events_1.EventEmitter {
888
681
  async receiveMessages() {
889
682
  console.warn("receiveMessages is deprecated and will be removed in a future version. Use connect() and listen for 'message' events instead.");
890
683
  // Return empty array but log helpful migration info
891
- console.info("Migration guide: Replace receiveMessages() with:");
892
- console.info(" await signalCli.connect();");
684
+ console.info('Migration guide: Replace receiveMessages() with:');
685
+ console.info(' await signalCli.connect();');
893
686
  console.info(" signalCli.on('message', (msg) => { /* handle message */ });");
894
687
  return Promise.resolve([]);
895
688
  }
@@ -898,7 +691,7 @@ class SignalCli extends events_1.EventEmitter {
898
691
  * This method now calls connect() for backward compatibility.
899
692
  */
900
693
  startDaemon() {
901
- console.warn("startDaemon is deprecated. Use connect() instead.");
694
+ console.warn('startDaemon is deprecated. Use connect() instead.');
902
695
  this.connect();
903
696
  }
904
697
  /**
@@ -906,7 +699,7 @@ class SignalCli extends events_1.EventEmitter {
906
699
  * This method now calls gracefulShutdown() for backward compatibility.
907
700
  */
908
701
  stopDaemon() {
909
- console.warn("stopDaemon is deprecated. Use gracefulShutdown() or disconnect() instead.");
702
+ console.warn('stopDaemon is deprecated. Use gracefulShutdown() or disconnect() instead.');
910
703
  this.gracefulShutdown();
911
704
  }
912
705
  // ############# MESSAGE RECEIVING #############
@@ -932,634 +725,96 @@ class SignalCli extends events_1.EventEmitter {
932
725
  * ```
933
726
  */
934
727
  async receive(options = {}) {
935
- const params = { account: this.account };
936
- // Set timeout (default: 5 seconds)
937
- if (options.timeout !== undefined) {
938
- params.timeout = options.timeout;
939
- }
940
- // Set maximum number of messages
941
- if (options.maxMessages !== undefined) {
942
- params.maxMessages = options.maxMessages;
943
- }
944
- // Skip attachment downloads
945
- if (options.ignoreAttachments) {
946
- params.ignoreAttachments = true;
947
- }
948
- // Skip stories
949
- if (options.ignoreStories) {
950
- params.ignoreStories = true;
951
- }
952
- // Send read receipts automatically
953
- if (options.sendReadReceipts) {
954
- params.sendReadReceipts = true;
955
- }
956
- try {
957
- const result = await this.sendJsonRpcRequest('receive', params);
958
- // Parse and return messages
959
- if (Array.isArray(result)) {
960
- return result.map(envelope => this.parseEnvelope(envelope));
961
- }
962
- return [];
963
- }
964
- catch (error) {
965
- this.logger.error('Failed to receive messages:', error);
966
- throw error;
967
- }
728
+ return this.messages.receive(options);
968
729
  }
969
- /**
970
- * Parse a message envelope from signal-cli into a Message object.
971
- * @private
972
- */
973
730
  parseEnvelope(envelope) {
974
- const message = {
975
- timestamp: envelope.timestamp || Date.now(),
976
- source: envelope.source || envelope.sourceNumber,
977
- sourceUuid: envelope.sourceUuid,
978
- sourceDevice: envelope.sourceDevice,
979
- };
980
- // Parse data message
981
- if (envelope.dataMessage) {
982
- const data = envelope.dataMessage;
983
- message.text = data.message || data.body;
984
- message.groupId = data.groupInfo?.groupId;
985
- message.attachments = data.attachments;
986
- message.mentions = data.mentions;
987
- message.quote = data.quote;
988
- message.reaction = data.reaction;
989
- message.sticker = data.sticker;
990
- message.expiresInSeconds = data.expiresInSeconds;
991
- message.viewOnce = data.viewOnce;
992
- }
993
- // Parse sync message
994
- if (envelope.syncMessage) {
995
- message.syncMessage = envelope.syncMessage;
996
- }
997
- // Parse receipt message
998
- if (envelope.receiptMessage) {
999
- message.receipt = envelope.receiptMessage;
1000
- }
1001
- // Parse typing message
1002
- if (envelope.typingMessage) {
1003
- message.typing = envelope.typingMessage;
1004
- }
1005
- return message;
731
+ // Kept for backward compatibility if needed internally, but should use MessageManager.parseEnvelope
732
+ return this.messages.parseEnvelope(envelope);
1006
733
  }
1007
- // ############# NEW FEATURES - Missing signal-cli Commands #############
1008
- /**
1009
- * Remove a contact from the contact list.
1010
- * @param number - The phone number of the contact to remove
1011
- * @param options - Options for how to remove the contact
1012
- */
1013
734
  async removeContact(number, options = {}) {
1014
- const params = {
1015
- account: this.account,
1016
- recipient: number
1017
- };
1018
- if (options.hide)
1019
- params.hide = true;
1020
- if (options.forget)
1021
- params.forget = true;
1022
- await this.sendJsonRpcRequest('removeContact', params);
735
+ return this.contacts.removeContact(number, options);
1023
736
  }
1024
- /**
1025
- * Check if phone numbers are registered with Signal.
1026
- * @param numbers - Array of phone numbers to check
1027
- * @param usernames - Optional array of usernames to check
1028
- * @returns Array of user status results
1029
- */
1030
737
  async getUserStatus(numbers = [], usernames = []) {
1031
- const params = { account: this.account };
1032
- if (numbers.length > 0)
1033
- params.recipients = numbers;
1034
- if (usernames.length > 0)
1035
- params.usernames = usernames;
1036
- const result = await this.sendJsonRpcRequest('getUserStatus', params);
1037
- // Transform the result to match our interface
1038
- const statusResults = [];
1039
- if (result.recipients) {
1040
- result.recipients.forEach((recipient) => {
1041
- statusResults.push({
1042
- number: recipient.number,
1043
- isRegistered: recipient.isRegistered || false,
1044
- uuid: recipient.uuid,
1045
- username: recipient.username
1046
- });
1047
- });
1048
- }
1049
- return statusResults;
738
+ return this.contacts.getUserStatus(numbers, usernames);
1050
739
  }
1051
- /**
1052
- * Send a payment notification to a recipient (MobileCoin).
1053
- * Sends a notification about a cryptocurrency payment made through Signal's MobileCoin integration.
1054
- *
1055
- * @param recipient - The phone number or group ID of the recipient
1056
- * @param paymentData - Payment notification data including receipt and optional note
1057
- * @returns Send result with timestamp
1058
- * @throws {Error} If receipt is invalid or sending fails
1059
- *
1060
- * @example
1061
- * ```typescript
1062
- * const receiptBlob = 'base64EncodedReceiptData...';
1063
- * await signal.sendPaymentNotification('+33612345678', {
1064
- * receipt: receiptBlob,
1065
- * note: 'Thanks for dinner!'
1066
- * });
1067
- * ```
1068
- */
1069
740
  async sendPaymentNotification(recipient, paymentData) {
1070
- this.logger.info(`Sending payment notification to ${recipient}`);
1071
- (0, validators_1.validateRecipient)(recipient);
1072
- if (!paymentData.receipt || paymentData.receipt.trim().length === 0) {
1073
- throw new Error('Payment receipt is required');
1074
- }
1075
- const params = {
1076
- receipt: paymentData.receipt,
1077
- account: this.account
1078
- };
1079
- if (paymentData.note) {
1080
- params.note = paymentData.note;
1081
- }
1082
- if (this.isGroupId(recipient)) {
1083
- params.groupId = recipient;
1084
- }
1085
- else {
1086
- params.recipient = recipient;
1087
- }
1088
- return this.sendJsonRpcRequest('sendPaymentNotification', params);
741
+ return this.accounts.sendPaymentNotification(recipient, paymentData);
1089
742
  }
1090
- /**
1091
- * Upload a custom sticker pack to Signal.
1092
- * @param manifest - Sticker pack manifest information
1093
- * @returns Upload result with pack ID and key
1094
- */
1095
743
  async uploadStickerPack(manifest) {
1096
- const params = {
1097
- account: this.account,
1098
- path: manifest.path
1099
- };
1100
- const result = await this.sendJsonRpcRequest('uploadStickerPack', params);
1101
- return {
1102
- packId: result.packId,
1103
- packKey: result.packKey,
1104
- installUrl: result.installUrl
1105
- };
744
+ return this.stickers.uploadStickerPack(manifest);
1106
745
  }
1107
- /**
1108
- * Submit a rate limit challenge to lift rate limiting.
1109
- * @param challenge - Challenge token from the proof required error
1110
- * @param captcha - Captcha token from solved captcha
1111
- * @returns Challenge result indicating success/failure
1112
- */
1113
746
  async submitRateLimitChallenge(challenge, captcha) {
1114
- const params = {
1115
- account: this.account,
1116
- challenge,
1117
- captcha
1118
- };
1119
- const result = await this.sendJsonRpcRequest('submitRateLimitChallenge', params);
1120
- return {
1121
- success: result.success || false,
1122
- retryAfter: result.retryAfter,
1123
- message: result.message
1124
- };
747
+ return this.accounts.submitRateLimitChallenge(challenge, captcha);
1125
748
  }
1126
- /**
1127
- * Start the phone number change process.
1128
- * Initiates SMS or voice verification for changing your account to a new phone number.
1129
- * After calling this, you must verify the new number with finishChangeNumber().
1130
- *
1131
- * @param newNumber - The new phone number in E164 format (e.g., "+33612345678")
1132
- * @param voice - Use voice verification instead of SMS (default: false)
1133
- * @param captcha - Optional captcha token if required
1134
- * @throws {Error} If not a primary device or rate limited
1135
- */
1136
749
  async startChangeNumber(newNumber, voice = false, captcha) {
1137
- this.logger.info(`Starting change number to ${newNumber} (voice: ${voice})`);
1138
- (0, validators_1.validatePhoneNumber)(newNumber);
1139
- const params = {
1140
- account: this.account,
1141
- number: newNumber,
1142
- voice
1143
- };
1144
- if (captcha)
1145
- params.captcha = captcha;
1146
- await this.sendJsonRpcRequest('startChangeNumber', params);
750
+ return this.accounts.startChangeNumber(newNumber, voice, captcha);
1147
751
  }
1148
- /**
1149
- * Complete the phone number change process.
1150
- * Verifies the code received via SMS or voice and changes your account to the new number.
1151
- * Must be called after startChangeNumber().
1152
- *
1153
- * @param newNumber - The new phone number (same as startChangeNumber)
1154
- * @param verificationCode - The verification code received via SMS or voice
1155
- * @param pin - Optional registration lock PIN if one was set
1156
- * @throws {Error} If verification fails or incorrect PIN
1157
- */
1158
752
  async finishChangeNumber(newNumber, verificationCode, pin) {
1159
- this.logger.info(`Finishing change number to ${newNumber}`);
1160
- (0, validators_1.validatePhoneNumber)(newNumber);
1161
- if (!verificationCode || verificationCode.trim().length === 0) {
1162
- throw new Error('Verification code is required');
1163
- }
1164
- const params = {
1165
- account: this.account,
1166
- number: newNumber,
1167
- verificationCode
1168
- };
1169
- if (pin)
1170
- params.pin = pin;
1171
- await this.sendJsonRpcRequest('finishChangeNumber', params);
753
+ return this.accounts.finishChangeNumber(newNumber, verificationCode, pin);
1172
754
  }
1173
- /**
1174
- * Enhanced send message with progress callback support.
1175
- * @param recipient - Phone number or group ID
1176
- * @param message - Message text
1177
- * @param options - Send options including progress callback
1178
- * @returns Send response
1179
- */
1180
755
  async sendMessageWithProgress(recipient, message, options = {}) {
1181
- // For now, this is the same as sendMessage since signal-cli doesn't provide
1182
- // native progress callbacks. This is a placeholder for future enhancement.
1183
- const { onProgress, ...sendOptions } = options;
1184
- // Simulate progress for large attachments
1185
- if (onProgress && sendOptions.attachments && sendOptions.attachments.length > 0) {
1186
- // Simulate upload progress
1187
- for (let i = 0; i <= 100; i += 10) {
1188
- onProgress({
1189
- total: 100,
1190
- uploaded: i,
1191
- percentage: i
1192
- });
1193
- // Small delay to simulate upload
1194
- await new Promise(resolve => setTimeout(resolve, 50));
1195
- }
1196
- }
1197
- return this.sendMessage(recipient, message, sendOptions);
756
+ return this.messages.sendMessageWithProgress(recipient, message, options);
1198
757
  }
1199
- // ========== NEW METHODS FOR 100% signal-cli COMPATIBILITY ==========
1200
- /**
1201
- * Send a poll create message to a recipient or group.
1202
- * @param options Poll creation options
1203
- * @returns Send response with timestamp
1204
- */
1205
758
  async sendPollCreate(options) {
1206
- this.logger.debug('Sending poll create', options);
1207
- (0, validators_1.validateMessage)(options.question, 500);
1208
- if (!options.options || options.options.length < 2) {
1209
- throw new errors_1.MessageError('Poll must have at least 2 options');
1210
- }
1211
- if (options.options.length > 10) {
1212
- throw new errors_1.MessageError('Poll cannot have more than 10 options');
1213
- }
1214
- const params = {
1215
- question: options.question,
1216
- options: options.options,
1217
- account: this.account
1218
- };
1219
- if (options.multiSelect !== undefined) {
1220
- params.multiSelect = options.multiSelect;
1221
- }
1222
- if (options.groupId) {
1223
- (0, validators_1.validateGroupId)(options.groupId);
1224
- params.groupId = options.groupId;
1225
- }
1226
- else if (options.recipients) {
1227
- params.recipients = options.recipients.map(r => {
1228
- (0, validators_1.validateRecipient)(r);
1229
- return r;
1230
- });
1231
- }
1232
- else {
1233
- throw new errors_1.MessageError('Must specify either recipients or groupId');
1234
- }
1235
- return this.sendJsonRpcRequest('sendPollCreate', params);
759
+ return this.messages.sendPollCreate(options);
1236
760
  }
1237
- /**
1238
- * Send a poll vote message to vote on a poll.
1239
- * @param recipient Recipient or group ID
1240
- * @param options Poll vote options
1241
- * @returns Send response with timestamp
1242
- */
1243
761
  async sendPollVote(recipient, options) {
1244
- this.logger.debug('Sending poll vote', { recipient, options });
1245
- (0, validators_1.validateRecipient)(options.pollAuthor);
1246
- (0, validators_1.validateTimestamp)(options.pollTimestamp);
1247
- if (!options.optionIndexes || options.optionIndexes.length === 0) {
1248
- throw new errors_1.MessageError('Must specify at least one option to vote for');
1249
- }
1250
- const params = {
1251
- pollAuthor: options.pollAuthor,
1252
- pollTimestamp: options.pollTimestamp,
1253
- options: options.optionIndexes,
1254
- account: this.account
1255
- };
1256
- if (options.voteCount !== undefined) {
1257
- params.voteCount = options.voteCount;
1258
- }
1259
- if (this.isGroupId(recipient)) {
1260
- (0, validators_1.validateGroupId)(recipient);
1261
- params.groupId = recipient;
1262
- }
1263
- else {
1264
- (0, validators_1.validateRecipient)(recipient);
1265
- params.recipient = recipient;
1266
- }
1267
- return this.sendJsonRpcRequest('sendPollVote', params);
762
+ return this.messages.sendPollVote(recipient, options);
1268
763
  }
1269
- /**
1270
- * Send a poll terminate message to close a poll.
1271
- * @param recipient Recipient or group ID
1272
- * @param options Poll terminate options
1273
- * @returns Send response with timestamp
1274
- */
1275
764
  async sendPollTerminate(recipient, options) {
1276
- this.logger.debug('Sending poll terminate', { recipient, options });
1277
- (0, validators_1.validateTimestamp)(options.pollTimestamp);
1278
- const params = {
1279
- pollTimestamp: options.pollTimestamp,
1280
- account: this.account
1281
- };
1282
- if (this.isGroupId(recipient)) {
1283
- (0, validators_1.validateGroupId)(recipient);
1284
- params.groupId = recipient;
1285
- }
1286
- else {
1287
- (0, validators_1.validateRecipient)(recipient);
1288
- params.recipient = recipient;
1289
- }
1290
- return this.sendJsonRpcRequest('sendPollTerminate', params);
765
+ return this.messages.sendPollTerminate(recipient, options);
1291
766
  }
1292
- /**
1293
- * Update account information (device name, username, privacy settings).
1294
- * @param options Account update options
1295
- * @returns Account update result
1296
- */
1297
767
  async updateAccount(options) {
1298
- this.logger.debug('Updating account', options);
1299
- const params = { account: this.account };
1300
- if (options.deviceName) {
1301
- params.deviceName = options.deviceName;
1302
- }
1303
- if (options.username) {
1304
- params.username = options.username;
1305
- }
1306
- if (options.deleteUsername) {
1307
- params.deleteUsername = true;
1308
- }
1309
- if (options.unrestrictedUnidentifiedSender !== undefined) {
1310
- params.unrestrictedUnidentifiedSender = options.unrestrictedUnidentifiedSender;
1311
- }
1312
- if (options.discoverableByNumber !== undefined) {
1313
- params.discoverableByNumber = options.discoverableByNumber;
1314
- }
1315
- if (options.numberSharing !== undefined) {
1316
- params.numberSharing = options.numberSharing;
1317
- }
1318
- try {
1319
- const result = await this.sendJsonRpcRequest('updateAccount', params);
1320
- return {
1321
- success: true,
1322
- username: result.username,
1323
- usernameLink: result.usernameLink
1324
- };
1325
- }
1326
- catch (error) {
1327
- return {
1328
- success: false,
1329
- error: error instanceof Error ? error.message : 'Unknown error'
1330
- };
1331
- }
768
+ return this.accounts.updateAccount(options);
1332
769
  }
1333
- /**
1334
- * Set or update the username for this account.
1335
- * Helper method that wraps updateAccount() for simpler username management.
1336
- *
1337
- * @param username - The username to set (without discriminator)
1338
- * @returns Account update result with username and link
1339
- *
1340
- * @example
1341
- * ```typescript
1342
- * const result = await signal.setUsername('myusername');
1343
- * console.log(`Username: ${result.username}`);
1344
- * console.log(`Link: ${result.usernameLink}`);
1345
- * ```
1346
- */
1347
770
  async setUsername(username) {
1348
- return this.updateAccount({ username });
771
+ return this.accounts.updateAccount({ username });
1349
772
  }
1350
- /**
1351
- * Delete the current username from this account.
1352
- * Helper method that wraps updateAccount() for simpler username deletion.
1353
- *
1354
- * @returns Account update result
1355
- *
1356
- * @example
1357
- * ```typescript
1358
- * const result = await signal.deleteUsername();
1359
- * if (result.success) {
1360
- * console.log('Username deleted successfully');
1361
- * }
1362
- * ```
1363
- */
1364
773
  async deleteUsername() {
1365
- return this.updateAccount({ deleteUsername: true });
774
+ return this.accounts.updateAccount({ deleteUsername: true });
1366
775
  }
1367
- /**
1368
- * Get raw attachment data as base64 string.
1369
- * @param options Attachment retrieval options
1370
- * @returns Base64 encoded attachment data
1371
- */
1372
776
  async getAttachment(options) {
1373
- this.logger.debug('Getting attachment', options);
1374
- if (!options.id) {
1375
- throw new errors_1.MessageError('Attachment ID is required');
1376
- }
1377
- const params = {
1378
- id: options.id,
1379
- account: this.account
1380
- };
1381
- if (options.groupId) {
1382
- (0, validators_1.validateGroupId)(options.groupId);
1383
- params.groupId = options.groupId;
1384
- }
1385
- else if (options.recipient) {
1386
- (0, validators_1.validateRecipient)(options.recipient);
1387
- params.recipient = options.recipient;
1388
- }
1389
- const result = await this.sendJsonRpcRequest('getAttachment', params);
1390
- return result.data || result;
777
+ return this.messages.getAttachment(options);
1391
778
  }
1392
- /**
1393
- * Get raw avatar data as base64 string.
1394
- * @param options Avatar retrieval options
1395
- * @returns Base64 encoded avatar data
1396
- */
1397
779
  async getAvatar(options) {
1398
- this.logger.debug('Getting avatar', options);
1399
- const params = { account: this.account };
1400
- if (options.contact) {
1401
- (0, validators_1.validateRecipient)(options.contact);
1402
- params.contact = options.contact;
1403
- }
1404
- else if (options.profile) {
1405
- (0, validators_1.validateRecipient)(options.profile);
1406
- params.profile = options.profile;
1407
- }
1408
- else if (options.groupId) {
1409
- (0, validators_1.validateGroupId)(options.groupId);
1410
- params.groupId = options.groupId;
1411
- }
1412
- else {
1413
- throw new errors_1.MessageError('Must specify contact, profile, or groupId');
1414
- }
1415
- const result = await this.sendJsonRpcRequest('getAvatar', params);
1416
- return result.data || result;
780
+ return this.contacts.getAvatar(options);
1417
781
  }
1418
- /**
1419
- * Get raw sticker data as base64 string.
1420
- * @param options Sticker retrieval options
1421
- * @returns Base64 encoded sticker data
1422
- */
1423
782
  async getSticker(options) {
1424
- this.logger.debug('Getting sticker', options);
1425
- if (!options.packId || !options.stickerId) {
1426
- throw new errors_1.MessageError('Pack ID and sticker ID are required');
1427
- }
1428
- const params = {
1429
- packId: options.packId,
1430
- stickerId: options.stickerId,
1431
- account: this.account
1432
- };
1433
- const result = await this.sendJsonRpcRequest('getSticker', params);
1434
- return result.data || result;
783
+ return this.stickers.getSticker(options);
1435
784
  }
1436
- /**
1437
- * Send contacts synchronization message to linked devices.
1438
- * @param options Contacts sync options
1439
- */
1440
785
  async sendContacts(options = {}) {
1441
- this.logger.debug('Sending contacts sync');
1442
786
  const params = { account: this.account };
1443
- if (options.includeAllRecipients) {
787
+ if (options.includeAllRecipients)
1444
788
  params.allRecipients = true;
1445
- }
1446
789
  await this.sendJsonRpcRequest('sendContacts', params);
1447
790
  }
1448
- /**
1449
- * List groups with optional filtering and details.
1450
- * @param options List groups options
1451
- * @returns Array of group information
1452
- */
1453
791
  async listGroupsDetailed(options = {}) {
1454
- this.logger.debug('Listing groups with options', options);
1455
- const params = { account: this.account };
1456
- if (options.detailed) {
1457
- params.detailed = true;
1458
- }
1459
- if (options.groupIds && options.groupIds.length > 0) {
1460
- params.groupId = options.groupIds;
1461
- }
1462
- return this.sendJsonRpcRequest('listGroups', params);
792
+ return this.groups.listGroupsDetailed(options);
1463
793
  }
1464
- /**
1465
- * List all local accounts.
1466
- * @returns Array of account phone numbers
1467
- */
1468
794
  async listAccountsDetailed() {
1469
- this.logger.debug('Listing all accounts');
1470
- const result = await this.sendJsonRpcRequest('listAccounts');
1471
- return result.accounts || [];
795
+ return this.accounts.listAccountsDetailed();
1472
796
  }
1473
- /**
1474
- * Extract profile information from a contact.
1475
- * Parses givenName, familyName, mobileCoinAddress from profile data.
1476
- *
1477
- * @param contact - The contact object to parse
1478
- * @returns Enhanced contact with extracted profile fields
1479
- *
1480
- * @example
1481
- * ```typescript
1482
- * const contacts = await signal.listContacts();
1483
- * const enriched = signal.parseContactProfile(contacts[0]);
1484
- * console.log(enriched.givenName, enriched.familyName);
1485
- * ```
1486
- */
1487
797
  parseContactProfile(contact) {
1488
- // signal-cli already provides these fields if available
1489
- // This method normalizes and validates the data
1490
- return {
1491
- ...contact,
1492
- givenName: contact.givenName || undefined,
1493
- familyName: contact.familyName || undefined,
1494
- mobileCoinAddress: contact.mobileCoinAddress || undefined,
1495
- profileName: contact.profileName ||
1496
- (contact.givenName && contact.familyName
1497
- ? `${contact.givenName} ${contact.familyName}`
1498
- : contact.givenName || contact.familyName),
1499
- };
798
+ return this.contacts.parseContactProfile(contact);
1500
799
  }
1501
- /**
1502
- * Extract group membership details.
1503
- * Parses pendingMembers, bannedMembers, inviteLink from group data.
1504
- *
1505
- * @param group - The group info to parse
1506
- * @returns Enhanced group with extracted membership fields
1507
- *
1508
- * @example
1509
- * ```typescript
1510
- * const groups = await signal.listGroups();
1511
- * const enriched = signal.parseGroupDetails(groups[0]);
1512
- * console.log(enriched.pendingMembers, enriched.bannedMembers);
1513
- * ```
1514
- */
1515
800
  parseGroupDetails(group) {
1516
- return {
1517
- ...group,
1518
- // Normalize inviteLink field
1519
- inviteLink: group.groupInviteLink || group.inviteLink,
1520
- groupInviteLink: group.groupInviteLink || group.inviteLink,
1521
- // Ensure arrays exist
1522
- pendingMembers: group.pendingMembers || [],
1523
- banned: group.banned || [],
1524
- requestingMembers: group.requestingMembers || [],
1525
- admins: group.admins || [],
1526
- members: group.members || [],
1527
- };
801
+ return this.groups.parseGroupDetails(group);
1528
802
  }
1529
- /**
1530
- * Get enriched contacts list with parsed profile information.
1531
- *
1532
- * @returns Array of contacts with full profile data
1533
- *
1534
- * @example
1535
- * ```typescript
1536
- * const contacts = await signal.getContactsWithProfiles();
1537
- * contacts.forEach(c => {
1538
- * console.log(`${c.givenName} ${c.familyName} - ${c.mobileCoinAddress}`);
1539
- * });
1540
- * ```
1541
- */
1542
803
  async getContactsWithProfiles() {
1543
- const contacts = await this.listContacts();
1544
- return contacts.map(c => this.parseContactProfile(c));
804
+ return this.contacts.getContactsWithProfiles();
1545
805
  }
1546
- /**
1547
- * Get enriched groups list with parsed membership details.
1548
- *
1549
- * @param options - List groups options
1550
- * @returns Array of groups with full membership data
1551
- *
1552
- * @example
1553
- * ```typescript
1554
- * const groups = await signal.getGroupsWithDetails();
1555
- * groups.forEach(g => {
1556
- * console.log(`${g.name}: ${g.members.length} members, ${g.pendingMembers.length} pending`);
1557
- * });
1558
- * ```
1559
- */
1560
806
  async getGroupsWithDetails(options = {}) {
1561
- const groups = await this.listGroupsDetailed(options);
1562
- 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 });
1563
818
  }
1564
819
  }
1565
820
  exports.SignalCli = SignalCli;