signal-sdk 0.0.9 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +184 -61
  2. package/dist/MultiAccountManager.d.ts +149 -0
  3. package/dist/MultiAccountManager.js +320 -0
  4. package/dist/SignalBot.d.ts +1 -0
  5. package/dist/SignalBot.js +20 -2
  6. package/dist/SignalCli.d.ts +315 -16
  7. package/dist/SignalCli.js +880 -26
  8. package/dist/__tests__/MultiAccountManager.test.d.ts +4 -0
  9. package/dist/__tests__/MultiAccountManager.test.js +209 -0
  10. package/dist/__tests__/SignalBot.additional.test.d.ts +5 -0
  11. package/dist/__tests__/SignalBot.additional.test.js +353 -0
  12. package/dist/__tests__/SignalBot.test.js +5 -0
  13. package/dist/__tests__/SignalCli.advanced.test.d.ts +5 -0
  14. package/dist/__tests__/SignalCli.advanced.test.js +295 -0
  15. package/dist/__tests__/SignalCli.e2e.test.d.ts +5 -0
  16. package/dist/__tests__/SignalCli.e2e.test.js +240 -0
  17. package/dist/__tests__/SignalCli.integration.test.d.ts +5 -0
  18. package/dist/__tests__/SignalCli.integration.test.js +225 -0
  19. package/dist/__tests__/SignalCli.methods.test.d.ts +5 -0
  20. package/dist/__tests__/SignalCli.methods.test.js +556 -0
  21. package/dist/__tests__/SignalCli.parsing.test.d.ts +5 -0
  22. package/dist/__tests__/SignalCli.parsing.test.js +258 -0
  23. package/dist/__tests__/SignalCli.test.js +249 -13
  24. package/dist/__tests__/config.test.d.ts +5 -0
  25. package/dist/__tests__/config.test.js +252 -0
  26. package/dist/__tests__/errors.test.d.ts +5 -0
  27. package/dist/__tests__/errors.test.js +276 -0
  28. package/dist/__tests__/retry.test.d.ts +4 -0
  29. package/dist/__tests__/retry.test.js +123 -0
  30. package/dist/__tests__/validators.test.d.ts +4 -0
  31. package/dist/__tests__/validators.test.js +147 -0
  32. package/dist/config.d.ts +82 -0
  33. package/dist/config.js +116 -0
  34. package/dist/errors.d.ts +32 -0
  35. package/dist/errors.js +75 -0
  36. package/dist/index.d.ts +5 -0
  37. package/dist/index.js +7 -1
  38. package/dist/interfaces.d.ts +200 -10
  39. package/dist/interfaces.js +1 -1
  40. package/dist/retry.d.ts +56 -0
  41. package/dist/retry.js +152 -0
  42. package/dist/validators.d.ts +59 -0
  43. package/dist/validators.js +170 -0
  44. package/package.json +1 -1
package/dist/SignalCli.js CHANGED
@@ -39,11 +39,27 @@ const uuid_1 = require("uuid");
39
39
  const qrcodeTerminal = __importStar(require("qrcode-terminal"));
40
40
  const events_1 = require("events");
41
41
  const path = __importStar(require("path"));
42
+ const validators_1 = require("./validators");
43
+ const retry_1 = require("./retry");
44
+ const config_1 = require("./config");
45
+ const errors_1 = require("./errors");
42
46
  class SignalCli extends events_1.EventEmitter {
43
- constructor(accountOrPath, account) {
47
+ constructor(accountOrPath, account, config = {}) {
44
48
  super();
45
49
  this.cliProcess = null;
46
50
  this.requestPromises = new Map();
51
+ this.reconnectAttempts = 0;
52
+ this.maxReconnectAttempts = 5;
53
+ // Validate and merge configuration
54
+ this.config = (0, config_1.validateConfig)(config);
55
+ // Initialize logger
56
+ this.logger = new config_1.Logger({
57
+ level: this.config.verbose ? 'debug' : 'info',
58
+ enableFile: !!this.config.logFile,
59
+ filePath: this.config.logFile
60
+ });
61
+ // Initialize rate limiter
62
+ this.rateLimiter = new retry_1.RateLimiter(this.config.maxConcurrentRequests, this.config.minRequestInterval);
47
63
  let signalCliPath;
48
64
  let phoneNumber;
49
65
  // Smart parameter detection
@@ -72,6 +88,25 @@ class SignalCli extends events_1.EventEmitter {
72
88
  this.account = phoneNumber;
73
89
  }
74
90
  async connect() {
91
+ const daemonMode = this.config.daemonMode || 'json-rpc';
92
+ switch (daemonMode) {
93
+ case 'json-rpc':
94
+ await this.connectJsonRpc();
95
+ break;
96
+ case 'unix-socket':
97
+ await this.connectUnixSocket();
98
+ break;
99
+ case 'tcp':
100
+ await this.connectTcp();
101
+ break;
102
+ case 'http':
103
+ await this.connectHttp();
104
+ break;
105
+ default:
106
+ throw new Error(`Invalid daemon mode: ${daemonMode}`);
107
+ }
108
+ }
109
+ async connectJsonRpc() {
75
110
  const args = this.account ? ['-a', this.account, 'jsonRpc'] : ['jsonRpc'];
76
111
  if (process.platform === 'win32') {
77
112
  // On Windows, use cmd.exe to run the batch file with proper quoting for paths with spaces
@@ -117,11 +152,120 @@ class SignalCli extends events_1.EventEmitter {
117
152
  });
118
153
  });
119
154
  }
155
+ async connectUnixSocket() {
156
+ const net = await Promise.resolve().then(() => __importStar(require('net')));
157
+ const socketPath = this.config.socketPath || '/tmp/signal-cli.sock';
158
+ return new Promise((resolve, reject) => {
159
+ const socket = net.createConnection(socketPath);
160
+ socket.on('connect', () => {
161
+ this.logger.debug('Connected to Unix socket:', socketPath);
162
+ socket.on('data', (data) => this.handleRpcResponse(data.toString('utf8')));
163
+ socket.on('error', (err) => this.emit('error', err));
164
+ socket.on('close', () => {
165
+ this.emit('close', 0);
166
+ });
167
+ this.socket = socket;
168
+ resolve();
169
+ });
170
+ socket.on('error', (err) => {
171
+ reject(new errors_1.ConnectionError(`Failed to connect to Unix socket: ${err.message}`));
172
+ });
173
+ setTimeout(() => reject(new errors_1.ConnectionError('Unix socket connection timeout')), this.config.connectionTimeout);
174
+ });
175
+ }
176
+ async connectTcp() {
177
+ const net = await Promise.resolve().then(() => __importStar(require('net')));
178
+ const host = this.config.tcpHost || 'localhost';
179
+ const port = this.config.tcpPort || 7583;
180
+ return new Promise((resolve, reject) => {
181
+ const socket = net.createConnection(port, host);
182
+ socket.on('connect', () => {
183
+ this.logger.debug(`Connected to TCP: ${host}:${port}`);
184
+ socket.on('data', (data) => this.handleRpcResponse(data.toString('utf8')));
185
+ socket.on('error', (err) => this.emit('error', err));
186
+ socket.on('close', () => {
187
+ this.emit('close', 0);
188
+ });
189
+ this.socket = socket;
190
+ resolve();
191
+ });
192
+ socket.on('error', (err) => {
193
+ reject(new errors_1.ConnectionError(`Failed to connect to TCP: ${err.message}`));
194
+ });
195
+ setTimeout(() => reject(new errors_1.ConnectionError('TCP connection timeout')), this.config.connectionTimeout);
196
+ });
197
+ }
198
+ async connectHttp() {
199
+ const baseUrl = this.config.httpBaseUrl || 'http://localhost:8080';
200
+ // For HTTP mode, we don't maintain a persistent connection
201
+ // Instead, we'll use the httpRequest method for each operation
202
+ this.logger.debug('HTTP mode configured:', baseUrl);
203
+ this.httpBaseUrl = baseUrl;
204
+ // Test connection by sending a simple request
205
+ try {
206
+ await this.httpRequest({ jsonrpc: '2.0', method: 'version', params: {}, id: (0, uuid_1.v4)() });
207
+ this.logger.debug('HTTP connection verified');
208
+ }
209
+ catch (error) {
210
+ throw new errors_1.ConnectionError(`Failed to connect to HTTP endpoint: ${error instanceof Error ? error.message : String(error)}`);
211
+ }
212
+ }
213
+ async httpRequest(request) {
214
+ const https = await Promise.resolve(`${this.config.httpBaseUrl?.startsWith('https:') ? 'https' : 'http'}`).then(s => __importStar(require(s)));
215
+ const baseUrl = this.config.httpBaseUrl || 'http://localhost:8080';
216
+ const url = new URL('/api/v1/rpc', baseUrl);
217
+ return new Promise((resolve, reject) => {
218
+ const data = JSON.stringify(request);
219
+ const options = {
220
+ method: 'POST',
221
+ headers: {
222
+ 'Content-Type': 'application/json',
223
+ 'Content-Length': Buffer.byteLength(data)
224
+ }
225
+ };
226
+ const req = https.request(url, options, (res) => {
227
+ let body = '';
228
+ res.on('data', (chunk) => body += chunk);
229
+ res.on('end', () => {
230
+ try {
231
+ const response = JSON.parse(body);
232
+ if (response.error) {
233
+ reject(new Error(`[${response.error.code}] ${response.error.message}`));
234
+ }
235
+ else {
236
+ resolve(response.result);
237
+ }
238
+ }
239
+ catch (error) {
240
+ reject(new Error(`Failed to parse HTTP response: ${body}`));
241
+ }
242
+ });
243
+ });
244
+ req.on('error', (err) => reject(new errors_1.ConnectionError(`HTTP request failed: ${err.message}`)));
245
+ req.setTimeout(this.config.requestTimeout, () => {
246
+ req.destroy();
247
+ reject(new errors_1.ConnectionError('HTTP request timeout'));
248
+ });
249
+ req.write(data);
250
+ req.end();
251
+ });
252
+ }
120
253
  disconnect() {
121
- if (this.cliProcess) {
254
+ const daemonMode = this.config.daemonMode || 'json-rpc';
255
+ // Close socket connections
256
+ if (daemonMode === 'unix-socket' || daemonMode === 'tcp') {
257
+ const socket = this.socket;
258
+ if (socket && !socket.destroyed) {
259
+ socket.destroy();
260
+ this.socket = null;
261
+ }
262
+ }
263
+ // Close process for json-rpc mode
264
+ if (daemonMode === 'json-rpc' && this.cliProcess) {
122
265
  this.cliProcess.kill();
123
266
  this.cliProcess = null;
124
267
  }
268
+ // For HTTP mode, nothing to disconnect (stateless)
125
269
  }
126
270
  async gracefulShutdown() {
127
271
  return new Promise((resolve) => {
@@ -137,13 +281,17 @@ class SignalCli extends events_1.EventEmitter {
137
281
  // Send SIGTERM for graceful shutdown
138
282
  this.cliProcess.kill('SIGTERM');
139
283
  // Force kill after 5 seconds if it doesn't close gracefully
140
- setTimeout(() => {
284
+ const forceKillTimer = setTimeout(() => {
141
285
  if (this.cliProcess) {
142
286
  this.cliProcess.kill('SIGKILL');
143
287
  this.cliProcess = null;
144
288
  resolve();
145
289
  }
146
290
  }, 5000);
291
+ // Use unref() to prevent this timer from keeping the process alive
292
+ if (forceKillTimer.unref) {
293
+ forceKillTimer.unref();
294
+ }
147
295
  });
148
296
  }
149
297
  handleRpcResponse(data) {
@@ -218,8 +366,40 @@ class SignalCli extends events_1.EventEmitter {
218
366
  }
219
367
  }
220
368
  async sendJsonRpcRequest(method, params) {
369
+ const daemonMode = this.config.daemonMode || 'json-rpc';
370
+ // For HTTP mode, use HTTP requests
371
+ if (daemonMode === 'http') {
372
+ const id = (0, uuid_1.v4)();
373
+ const request = {
374
+ jsonrpc: '2.0',
375
+ method,
376
+ params,
377
+ id,
378
+ };
379
+ return await this.httpRequest(request);
380
+ }
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
221
401
  if (!this.cliProcess || !this.cliProcess.stdin) {
222
- throw new Error('Not connected. Call connect() first.');
402
+ throw new errors_1.ConnectionError('Not connected. Call connect() first.');
223
403
  }
224
404
  const id = (0, uuid_1.v4)();
225
405
  const request = {
@@ -256,7 +436,7 @@ class SignalCli extends events_1.EventEmitter {
256
436
  else {
257
437
  params.recipients = [recipient];
258
438
  }
259
- // Only add safe, well-known options to avoid JSON parsing issues
439
+ // Add well-known options
260
440
  if (options.attachments && options.attachments.length > 0) {
261
441
  params.attachments = options.attachments;
262
442
  }
@@ -264,7 +444,64 @@ class SignalCli extends events_1.EventEmitter {
264
444
  params.expiresInSeconds = options.expiresInSeconds;
265
445
  }
266
446
  if (options.isViewOnce) {
267
- params.isViewOnce = 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;
268
505
  }
269
506
  return this.sendJsonRpcRequest('send', params);
270
507
  }
@@ -355,6 +592,79 @@ class SignalCli extends events_1.EventEmitter {
355
592
  async trustIdentity(number, safetyNumber, verified = true) {
356
593
  await this.sendJsonRpcRequest('trust', { account: this.account, recipient: number, safetyNumber, verified });
357
594
  }
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
+ async getSafetyNumber(number) {
609
+ const identities = await this.listIdentities(number);
610
+ if (identities.length > 0 && identities[0].safetyNumber) {
611
+ return identities[0].safetyNumber;
612
+ }
613
+ return null;
614
+ }
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
+ async verifySafetyNumber(number, safetyNumber) {
634
+ const storedSafetyNumber = await this.getSafetyNumber(number);
635
+ if (!storedSafetyNumber) {
636
+ return false;
637
+ }
638
+ // Compare safety numbers (remove spaces for comparison)
639
+ const normalizedStored = storedSafetyNumber.replace(/\s/g, '');
640
+ const normalizedProvided = safetyNumber.replace(/\s/g, '');
641
+ if (normalizedStored === normalizedProvided) {
642
+ await this.trustIdentity(number, safetyNumber, true);
643
+ return true;
644
+ }
645
+ return false;
646
+ }
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
+ async listUntrustedIdentities() {
663
+ const allIdentities = await this.listIdentities();
664
+ return allIdentities.filter(identity => identity.trustLevel === 'UNTRUSTED' ||
665
+ identity.trustLevel === 'TRUST_ON_FIRST_USE' ||
666
+ !identity.trustLevel);
667
+ }
358
668
  async link(deviceName) {
359
669
  const result = await this.sendJsonRpcRequest('link', { deviceName });
360
670
  return result.uri;
@@ -489,6 +799,10 @@ class SignalCli extends events_1.EventEmitter {
489
799
  params.promoteAdmins = options.promoteAdmins;
490
800
  if (options.demoteAdmins)
491
801
  params.demoteAdmins = options.demoteAdmins;
802
+ if (options.banMembers)
803
+ params.banMembers = options.banMembers;
804
+ if (options.unbanMembers)
805
+ params.unbanMembers = options.unbanMembers;
492
806
  if (options.resetInviteLink)
493
807
  params.resetLink = true;
494
808
  if (options.permissionAddMember)
@@ -504,6 +818,58 @@ class SignalCli extends events_1.EventEmitter {
504
818
  async listGroups() {
505
819
  return this.sendJsonRpcRequest('listGroups', { account: this.account });
506
820
  }
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
+ async sendGroupInviteLink(groupId, recipient) {
835
+ // Get group info to retrieve invite link
836
+ const groups = await this.listGroups();
837
+ const group = groups.find(g => g.groupId === groupId);
838
+ const inviteLink = group?.groupInviteLink || group?.inviteLink;
839
+ if (!group || !inviteLink) {
840
+ throw new Error('Group not found or does not have an invite link');
841
+ }
842
+ return this.sendMessage(recipient, `Join our group: ${inviteLink}`);
843
+ }
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
+ async setBannedMembers(groupId, members) {
857
+ await this.updateGroup(groupId, { banMembers: members });
858
+ }
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
+ async resetGroupLink(groupId) {
871
+ await this.updateGroup(groupId, { resetInviteLink: true });
872
+ }
507
873
  async listContacts() {
508
874
  return this.sendJsonRpcRequest('listContacts', { account: this.account });
509
875
  }
@@ -543,6 +909,101 @@ class SignalCli extends events_1.EventEmitter {
543
909
  console.warn("stopDaemon is deprecated. Use gracefulShutdown() or disconnect() instead.");
544
910
  this.gracefulShutdown();
545
911
  }
912
+ // ############# MESSAGE RECEIVING #############
913
+ /**
914
+ * Receive messages from Signal with configurable options.
915
+ * This is the modern replacement for the deprecated receiveMessages().
916
+ *
917
+ * @param options - Options for receiving messages
918
+ * @returns Array of received messages
919
+ *
920
+ * @example
921
+ * ```typescript
922
+ * // Receive with default timeout
923
+ * const messages = await signal.receive();
924
+ *
925
+ * // Receive with custom options
926
+ * const messages = await signal.receive({
927
+ * timeout: 10,
928
+ * maxMessages: 5,
929
+ * ignoreAttachments: true,
930
+ * sendReadReceipts: true
931
+ * });
932
+ * ```
933
+ */
934
+ 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
+ }
968
+ }
969
+ /**
970
+ * Parse a message envelope from signal-cli into a Message object.
971
+ * @private
972
+ */
973
+ 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;
1006
+ }
546
1007
  // ############# NEW FEATURES - Missing signal-cli Commands #############
547
1008
  /**
548
1009
  * Remove a contact from the contact list.
@@ -588,12 +1049,29 @@ class SignalCli extends events_1.EventEmitter {
588
1049
  return statusResults;
589
1050
  }
590
1051
  /**
591
- * Send a payment notification to a recipient.
592
- * @param recipient - Phone number or group ID to send the notification to
593
- * @param paymentData - Payment notification data including receipt
594
- * @returns Send response with timestamp and other details
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
+ * ```
595
1068
  */
596
1069
  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
+ }
597
1075
  const params = {
598
1076
  receipt: paymentData.receipt,
599
1077
  account: this.account
@@ -646,13 +1124,18 @@ class SignalCli extends events_1.EventEmitter {
646
1124
  };
647
1125
  }
648
1126
  /**
649
- * Start the process of changing phone number.
650
- * @param newNumber - The new phone number to change to
651
- * @param voice - Whether to use voice verification instead of SMS
652
- * @param captcha - Captcha token if required
653
- * @returns Change number session information
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
654
1135
  */
655
1136
  async startChangeNumber(newNumber, voice = false, captcha) {
1137
+ this.logger.info(`Starting change number to ${newNumber} (voice: ${voice})`);
1138
+ (0, validators_1.validatePhoneNumber)(newNumber);
656
1139
  const params = {
657
1140
  account: this.account,
658
1141
  number: newNumber,
@@ -660,22 +1143,28 @@ class SignalCli extends events_1.EventEmitter {
660
1143
  };
661
1144
  if (captcha)
662
1145
  params.captcha = captcha;
663
- const result = await this.sendJsonRpcRequest('startChangeNumber', params);
664
- return {
665
- session: result.session,
666
- newNumber,
667
- challenge: result.challenge
668
- };
1146
+ await this.sendJsonRpcRequest('startChangeNumber', params);
669
1147
  }
670
1148
  /**
671
- * Finish the phone number change process with verification code.
672
- * @param verificationCode - The verification code received via SMS/voice
673
- * @param pin - Registration lock PIN if enabled
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
674
1157
  */
675
- async finishChangeNumber(verificationCode, pin) {
1158
+ 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
+ }
676
1164
  const params = {
677
1165
  account: this.account,
678
- code: verificationCode
1166
+ number: newNumber,
1167
+ verificationCode
679
1168
  };
680
1169
  if (pin)
681
1170
  params.pin = pin;
@@ -707,5 +1196,370 @@ class SignalCli extends events_1.EventEmitter {
707
1196
  }
708
1197
  return this.sendMessage(recipient, message, sendOptions);
709
1198
  }
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
+ 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);
1236
+ }
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
+ 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);
1268
+ }
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
+ 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);
1291
+ }
1292
+ /**
1293
+ * Update account information (device name, username, privacy settings).
1294
+ * @param options Account update options
1295
+ * @returns Account update result
1296
+ */
1297
+ 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
+ }
1332
+ }
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
+ async setUsername(username) {
1348
+ return this.updateAccount({ username });
1349
+ }
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
+ async deleteUsername() {
1365
+ return this.updateAccount({ deleteUsername: true });
1366
+ }
1367
+ /**
1368
+ * Get raw attachment data as base64 string.
1369
+ * @param options Attachment retrieval options
1370
+ * @returns Base64 encoded attachment data
1371
+ */
1372
+ 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;
1391
+ }
1392
+ /**
1393
+ * Get raw avatar data as base64 string.
1394
+ * @param options Avatar retrieval options
1395
+ * @returns Base64 encoded avatar data
1396
+ */
1397
+ 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;
1417
+ }
1418
+ /**
1419
+ * Get raw sticker data as base64 string.
1420
+ * @param options Sticker retrieval options
1421
+ * @returns Base64 encoded sticker data
1422
+ */
1423
+ 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;
1435
+ }
1436
+ /**
1437
+ * Send contacts synchronization message to linked devices.
1438
+ * @param options Contacts sync options
1439
+ */
1440
+ async sendContacts(options = {}) {
1441
+ this.logger.debug('Sending contacts sync');
1442
+ const params = { account: this.account };
1443
+ if (options.includeAllRecipients) {
1444
+ params.allRecipients = true;
1445
+ }
1446
+ await this.sendJsonRpcRequest('sendContacts', params);
1447
+ }
1448
+ /**
1449
+ * List groups with optional filtering and details.
1450
+ * @param options List groups options
1451
+ * @returns Array of group information
1452
+ */
1453
+ 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);
1463
+ }
1464
+ /**
1465
+ * List all local accounts.
1466
+ * @returns Array of account phone numbers
1467
+ */
1468
+ async listAccountsDetailed() {
1469
+ this.logger.debug('Listing all accounts');
1470
+ const result = await this.sendJsonRpcRequest('listAccounts');
1471
+ return result.accounts || [];
1472
+ }
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
+ 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
+ };
1500
+ }
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
+ 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
+ };
1528
+ }
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
+ async getContactsWithProfiles() {
1543
+ const contacts = await this.listContacts();
1544
+ return contacts.map(c => this.parseContactProfile(c));
1545
+ }
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
+ async getGroupsWithDetails(options = {}) {
1561
+ const groups = await this.listGroupsDetailed(options);
1562
+ return groups.map(g => this.parseGroupDetails(g));
1563
+ }
710
1564
  }
711
1565
  exports.SignalCli = SignalCli;