signal-sdk 0.0.8 → 0.1.0

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 (36) hide show
  1. package/README.md +175 -59
  2. package/dist/SignalBot.d.ts +108 -0
  3. package/dist/SignalBot.js +811 -0
  4. package/dist/SignalCli.d.ts +205 -0
  5. package/dist/SignalCli.js +967 -0
  6. package/dist/__tests__/SignalBot.additional.test.d.ts +5 -0
  7. package/dist/__tests__/SignalBot.additional.test.js +333 -0
  8. package/dist/__tests__/SignalBot.test.d.ts +1 -0
  9. package/dist/__tests__/SignalBot.test.js +102 -0
  10. package/dist/__tests__/SignalCli.integration.test.d.ts +5 -0
  11. package/dist/__tests__/SignalCli.integration.test.js +218 -0
  12. package/dist/__tests__/SignalCli.methods.test.d.ts +5 -0
  13. package/dist/__tests__/SignalCli.methods.test.js +470 -0
  14. package/dist/__tests__/SignalCli.test.d.ts +1 -0
  15. package/dist/__tests__/SignalCli.test.js +479 -0
  16. package/dist/__tests__/config.test.d.ts +5 -0
  17. package/dist/__tests__/config.test.js +252 -0
  18. package/dist/__tests__/errors.test.d.ts +5 -0
  19. package/dist/__tests__/errors.test.js +276 -0
  20. package/dist/__tests__/retry.test.d.ts +4 -0
  21. package/dist/__tests__/retry.test.js +123 -0
  22. package/dist/__tests__/validators.test.d.ts +4 -0
  23. package/dist/__tests__/validators.test.js +147 -0
  24. package/dist/config.d.ts +67 -0
  25. package/dist/config.js +111 -0
  26. package/dist/errors.d.ts +32 -0
  27. package/dist/errors.js +75 -0
  28. package/dist/index.d.ts +7 -0
  29. package/dist/index.js +26 -0
  30. package/dist/interfaces.d.ts +1073 -0
  31. package/dist/interfaces.js +11 -0
  32. package/dist/retry.d.ts +56 -0
  33. package/dist/retry.js +135 -0
  34. package/dist/validators.d.ts +59 -0
  35. package/dist/validators.js +170 -0
  36. package/package.json +5 -6
@@ -0,0 +1,967 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SignalCli = void 0;
37
+ const child_process_1 = require("child_process");
38
+ const uuid_1 = require("uuid");
39
+ const qrcodeTerminal = __importStar(require("qrcode-terminal"));
40
+ const events_1 = require("events");
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");
46
+ class SignalCli extends events_1.EventEmitter {
47
+ constructor(accountOrPath, account, config = {}) {
48
+ super();
49
+ this.cliProcess = null;
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);
63
+ let signalCliPath;
64
+ let phoneNumber;
65
+ // Smart parameter detection
66
+ if (typeof accountOrPath === 'string') {
67
+ if (accountOrPath.startsWith('+')) {
68
+ // First parameter is a phone number
69
+ phoneNumber = accountOrPath;
70
+ signalCliPath = undefined;
71
+ }
72
+ else {
73
+ // First parameter is a path, second is phone number
74
+ signalCliPath = accountOrPath;
75
+ phoneNumber = account;
76
+ }
77
+ }
78
+ // Determine the correct signal-cli path based on platform
79
+ let defaultPath;
80
+ if (process.platform === 'win32') {
81
+ defaultPath = path.join(__dirname, '..', 'bin', 'signal-cli.bat');
82
+ }
83
+ else {
84
+ // For Unix/Linux systems, use the shell script
85
+ defaultPath = path.join(__dirname, '..', 'bin', 'signal-cli');
86
+ }
87
+ this.signalCliPath = signalCliPath || defaultPath;
88
+ this.account = phoneNumber;
89
+ }
90
+ async connect() {
91
+ const args = this.account ? ['-a', this.account, 'jsonRpc'] : ['jsonRpc'];
92
+ if (process.platform === 'win32') {
93
+ // On Windows, use cmd.exe to run the batch file with proper quoting for paths with spaces
94
+ this.cliProcess = (0, child_process_1.spawn)('cmd.exe', ['/c', `"${this.signalCliPath}"`, ...args]);
95
+ }
96
+ else {
97
+ this.cliProcess = (0, child_process_1.spawn)(this.signalCliPath, args);
98
+ }
99
+ this.cliProcess.stdout?.on('data', (data) => this.handleRpcResponse(data.toString('utf8')));
100
+ this.cliProcess.stderr?.on('data', (data) => this.handleStderrData(data.toString('utf8')));
101
+ this.cliProcess.on('close', (code) => {
102
+ this.emit('close', code);
103
+ this.cliProcess = null;
104
+ });
105
+ this.cliProcess.on('error', (err) => this.emit('error', err));
106
+ return new Promise((resolve, reject) => {
107
+ // Set a timeout to resolve the promise even if no data is received
108
+ const connectTimeout = setTimeout(() => {
109
+ // Test if the process is still alive and responsive
110
+ if (this.cliProcess && !this.cliProcess.killed) {
111
+ resolve();
112
+ }
113
+ else {
114
+ reject(new Error('signal-cli process failed to start'));
115
+ }
116
+ }, 2000); // Wait 2 seconds max
117
+ // If we get data quickly, resolve immediately
118
+ this.cliProcess?.stdout?.once('data', () => {
119
+ clearTimeout(connectTimeout);
120
+ resolve();
121
+ });
122
+ // If there's an error, reject
123
+ this.cliProcess?.once('error', (err) => {
124
+ clearTimeout(connectTimeout);
125
+ reject(err);
126
+ });
127
+ // If the process exits immediately, that's an error
128
+ this.cliProcess?.once('close', (code) => {
129
+ clearTimeout(connectTimeout);
130
+ if (code !== 0) {
131
+ reject(new Error(`signal-cli exited with code ${code}`));
132
+ }
133
+ });
134
+ });
135
+ }
136
+ disconnect() {
137
+ if (this.cliProcess) {
138
+ this.cliProcess.kill();
139
+ this.cliProcess = null;
140
+ }
141
+ }
142
+ async gracefulShutdown() {
143
+ return new Promise((resolve) => {
144
+ if (!this.cliProcess) {
145
+ resolve();
146
+ return;
147
+ }
148
+ // Listen for the process to close
149
+ this.cliProcess.once('close', () => {
150
+ this.cliProcess = null;
151
+ resolve();
152
+ });
153
+ // Send SIGTERM for graceful shutdown
154
+ this.cliProcess.kill('SIGTERM');
155
+ // Force kill after 5 seconds if it doesn't close gracefully
156
+ setTimeout(() => {
157
+ if (this.cliProcess) {
158
+ this.cliProcess.kill('SIGKILL');
159
+ this.cliProcess = null;
160
+ resolve();
161
+ }
162
+ }, 5000);
163
+ });
164
+ }
165
+ handleRpcResponse(data) {
166
+ const lines = data.trim().split('\n');
167
+ for (const line of lines) {
168
+ if (!line)
169
+ continue;
170
+ try {
171
+ const response = JSON.parse(line);
172
+ if ('id' in response && response.id) {
173
+ const promise = this.requestPromises.get(response.id);
174
+ if (promise) {
175
+ if (response.error) {
176
+ promise.reject(new Error(`[${response.error.code}] ${response.error.message}`));
177
+ }
178
+ else {
179
+ promise.resolve(response.result);
180
+ }
181
+ this.requestPromises.delete(response.id);
182
+ }
183
+ }
184
+ else if ('method' in response) {
185
+ this.emit('notification', response);
186
+ if (response.method === 'receive') {
187
+ this.emit('message', response.params);
188
+ }
189
+ }
190
+ }
191
+ catch (error) {
192
+ this.emit('error', new Error(`Failed to parse JSON-RPC response: ${line}`));
193
+ }
194
+ }
195
+ }
196
+ handleStderrData(data) {
197
+ const message = data.trim();
198
+ if (!message)
199
+ return;
200
+ // Parse log level from signal-cli stderr output
201
+ // Format: "LEVEL Component - Message"
202
+ const logLevelMatch = message.match(/^(ERROR|WARN|INFO|DEBUG)\s+/);
203
+ const logLevel = logLevelMatch ? logLevelMatch[1] : 'UNKNOWN';
204
+ // Only emit errors for actual ERROR level messages
205
+ // INFO, WARN, DEBUG messages are just logged without crashing
206
+ if (logLevel === 'ERROR') {
207
+ this.emit('error', new Error(`signal-cli error: ${message}`));
208
+ }
209
+ else {
210
+ // Emit as a warning event for non-error messages
211
+ this.emit('log', { level: logLevel.toLowerCase(), message });
212
+ // Filter out common informational WARN messages from signal-cli
213
+ const isInformationalWarn = logLevel === 'WARN' && (message.includes('Failed to get sender certificate') ||
214
+ message.includes('ignoring: java.lang.InterruptedException') ||
215
+ message.includes('Request was interrupted') ||
216
+ message.includes('Connection reset') ||
217
+ message.includes('Socket closed') ||
218
+ message.includes('gracefully closing'));
219
+ // Only log actual warning messages, not informational ones
220
+ if (logLevel === 'WARN' && !isInformationalWarn) {
221
+ console.warn(`[signal-cli] ${message}`);
222
+ }
223
+ else if (logLevel === 'INFO' || logLevel === 'DEBUG') {
224
+ // Silently ignore INFO and DEBUG messages to avoid spam
225
+ // Uncomment the line below if you want to see all signal-cli logs:
226
+ // console.log(`[signal-cli] ${message}`);
227
+ }
228
+ else if (logLevel === 'UNKNOWN') {
229
+ // Unknown format, log as warning only if it doesn't look informational
230
+ if (!message.includes('Daemon closed') && !message.includes('gracefully')) {
231
+ console.warn(`[signal-cli] ${message}`);
232
+ }
233
+ }
234
+ }
235
+ }
236
+ async sendJsonRpcRequest(method, params) {
237
+ if (!this.cliProcess || !this.cliProcess.stdin) {
238
+ throw new Error('Not connected. Call connect() first.');
239
+ }
240
+ const id = (0, uuid_1.v4)();
241
+ const request = {
242
+ jsonrpc: '2.0',
243
+ method,
244
+ params,
245
+ id,
246
+ };
247
+ const promise = new Promise((resolve, reject) => {
248
+ this.requestPromises.set(id, { resolve, reject });
249
+ });
250
+ this.cliProcess.stdin.write(JSON.stringify(request) + '\n');
251
+ return promise;
252
+ }
253
+ isGroupId(recipient) {
254
+ return recipient.includes('=') || recipient.includes('/') || (recipient.includes('+') && !recipient.startsWith('+'));
255
+ }
256
+ // ############# Refactored Methods #############
257
+ async register(number, voice, captcha) {
258
+ await this.sendJsonRpcRequest('register', { account: number, voice, captcha });
259
+ }
260
+ async verify(number, token, pin) {
261
+ await this.sendJsonRpcRequest('verify', { account: number, token, pin });
262
+ }
263
+ async sendMessage(recipient, message, options = {}) {
264
+ const params = {
265
+ message,
266
+ account: this.account
267
+ };
268
+ // Add recipient information
269
+ if (this.isGroupId(recipient)) {
270
+ params.groupId = recipient;
271
+ }
272
+ else {
273
+ params.recipients = [recipient];
274
+ }
275
+ // Only add safe, well-known options to avoid JSON parsing issues
276
+ if (options.attachments && options.attachments.length > 0) {
277
+ params.attachments = options.attachments;
278
+ }
279
+ if (options.expiresInSeconds) {
280
+ params.expiresInSeconds = options.expiresInSeconds;
281
+ }
282
+ if (options.isViewOnce) {
283
+ params.isViewOnce = options.isViewOnce;
284
+ }
285
+ return this.sendJsonRpcRequest('send', params);
286
+ }
287
+ async sendReaction(recipient, targetAuthor, targetTimestamp, emoji, remove = false) {
288
+ const params = {
289
+ emoji,
290
+ targetAuthor,
291
+ targetTimestamp,
292
+ remove,
293
+ account: this.account
294
+ };
295
+ if (this.isGroupId(recipient)) {
296
+ params.groupId = recipient;
297
+ }
298
+ else {
299
+ params.recipients = [recipient];
300
+ }
301
+ return this.sendJsonRpcRequest('sendReaction', params);
302
+ }
303
+ async sendTyping(recipient, stop = false) {
304
+ const params = { when: !stop, account: this.account };
305
+ if (this.isGroupId(recipient)) {
306
+ params.groupId = recipient;
307
+ }
308
+ else {
309
+ params.recipients = [recipient];
310
+ }
311
+ await this.sendJsonRpcRequest('sendTyping', params);
312
+ }
313
+ async remoteDeleteMessage(recipient, targetTimestamp) {
314
+ const params = { targetTimestamp, account: this.account };
315
+ if (this.isGroupId(recipient)) {
316
+ params.groupId = recipient;
317
+ }
318
+ else {
319
+ params.recipients = [recipient];
320
+ }
321
+ await this.sendJsonRpcRequest('remoteDelete', params);
322
+ }
323
+ async updateContact(number, name, options = {}) {
324
+ await this.sendJsonRpcRequest('updateContact', { account: this.account, recipient: number, name, ...options });
325
+ }
326
+ async block(recipients, groupId) {
327
+ await this.sendJsonRpcRequest('block', { account: this.account, recipient: recipients, groupId });
328
+ }
329
+ async unblock(recipients, groupId) {
330
+ await this.sendJsonRpcRequest('unblock', { account: this.account, recipient: recipients, groupId });
331
+ }
332
+ async quitGroup(groupId) {
333
+ await this.sendJsonRpcRequest('quitGroup', { account: this.account, groupId });
334
+ }
335
+ async joinGroup(uri) {
336
+ await this.sendJsonRpcRequest('joinGroup', { account: this.account, uri });
337
+ }
338
+ async updateProfile(name, about, aboutEmoji, avatar) {
339
+ await this.sendJsonRpcRequest('updateProfile', { account: this.account, name, about, aboutEmoji, avatar });
340
+ }
341
+ async sendReceipt(recipient, targetTimestamp, type = 'read') {
342
+ await this.sendJsonRpcRequest('sendReceipt', { account: this.account, recipient, targetTimestamp, type });
343
+ }
344
+ async listStickerPacks() {
345
+ return this.sendJsonRpcRequest('listStickerPacks', { account: this.account });
346
+ }
347
+ async addStickerPack(packId, packKey) {
348
+ await this.sendJsonRpcRequest('addStickerPack', { account: this.account, packId, packKey });
349
+ }
350
+ async unregister() {
351
+ await this.sendJsonRpcRequest('unregister', { account: this.account });
352
+ }
353
+ async deleteLocalAccountData() {
354
+ await this.sendJsonRpcRequest('deleteLocalAccountData', { account: this.account });
355
+ }
356
+ async updateAccountConfiguration(config) {
357
+ await this.sendJsonRpcRequest('updateConfiguration', { account: this.account, ...config });
358
+ }
359
+ async removeDevice(deviceId) {
360
+ await this.sendJsonRpcRequest('removeDevice', { account: this.account, deviceId });
361
+ }
362
+ async setPin(pin) {
363
+ await this.sendJsonRpcRequest('setPin', { account: this.account, pin });
364
+ }
365
+ async removePin() {
366
+ await this.sendJsonRpcRequest('removePin', { account: this.account });
367
+ }
368
+ async listIdentities(number) {
369
+ return this.sendJsonRpcRequest('listIdentities', { account: this.account, number });
370
+ }
371
+ async trustIdentity(number, safetyNumber, verified = true) {
372
+ await this.sendJsonRpcRequest('trust', { account: this.account, recipient: number, safetyNumber, verified });
373
+ }
374
+ async link(deviceName) {
375
+ const result = await this.sendJsonRpcRequest('link', { deviceName });
376
+ return result.uri;
377
+ }
378
+ /**
379
+ * Link a new device to an existing Signal account with QR code support.
380
+ * This method provides a complete device linking solution with QR code generation.
381
+ *
382
+ * @param options - Linking options including device name and QR code output preferences
383
+ * @returns Promise resolving to LinkingResult with QR code data and linking status
384
+ */
385
+ async deviceLink(options = {}) {
386
+ const { spawn } = await Promise.resolve().then(() => __importStar(require('child_process')));
387
+ return new Promise((resolve, reject) => {
388
+ const deviceName = options.name || 'Signal SDK Device';
389
+ // Spawn signal-cli link command
390
+ let linkProcess;
391
+ if (process.platform === 'win32') {
392
+ // On Windows, use cmd.exe to run the batch file with proper quoting for paths with spaces
393
+ linkProcess = spawn('cmd.exe', ['/c', `"${this.signalCliPath}"`, 'link', '--name', deviceName], {
394
+ stdio: ['pipe', 'pipe', 'pipe']
395
+ });
396
+ }
397
+ else {
398
+ linkProcess = spawn(this.signalCliPath, ['link', '--name', deviceName], {
399
+ stdio: ['pipe', 'pipe', 'pipe']
400
+ });
401
+ }
402
+ let qrCodeData;
403
+ let linkingComplete = false;
404
+ let hasError = false;
405
+ // Handle stdout (where QR code URI will be)
406
+ linkProcess.stdout.on('data', (data) => {
407
+ const output = data.toString('utf8').trim();
408
+ // Look for QR code URI (starts with sgnl://)
409
+ if (output.includes('sgnl://')) {
410
+ const uriMatch = output.match(/sgnl:\/\/[^\s]+/);
411
+ if (uriMatch && !qrCodeData) {
412
+ const uri = uriMatch[0];
413
+ qrCodeData = {
414
+ uri
415
+ };
416
+ // Auto-display QR code if requested
417
+ if (options.qrCodeOutput === 'console') {
418
+ console.log('\n- QR CODE - SCAN WITH YOUR PHONE:');
419
+ console.log('===================================');
420
+ this.displayQRCode(uri);
421
+ console.log('===================================\n');
422
+ }
423
+ }
424
+ }
425
+ // Check for successful linking
426
+ if (output.includes('Device registered') || output.includes('Successfully linked')) {
427
+ linkingComplete = true;
428
+ }
429
+ });
430
+ // Handle stderr for errors
431
+ linkProcess.stderr.on('data', (data) => {
432
+ const error = data.toString('utf8').trim();
433
+ // Filter out informational messages
434
+ if (!error.includes('INFO') && !error.includes('DEBUG') && error.length > 0) {
435
+ hasError = true;
436
+ }
437
+ });
438
+ // Handle process exit
439
+ linkProcess.on('close', (code) => {
440
+ if (code === 0 && linkingComplete) {
441
+ resolve({
442
+ success: true,
443
+ isLinked: true,
444
+ deviceName,
445
+ qrCode: qrCodeData
446
+ });
447
+ }
448
+ else if (code === 0 && qrCodeData) {
449
+ resolve({
450
+ success: true,
451
+ isLinked: false,
452
+ deviceName,
453
+ qrCode: qrCodeData
454
+ });
455
+ }
456
+ else {
457
+ resolve({
458
+ success: false,
459
+ error: hasError ? 'Device linking failed' : `signal-cli exited with code ${code}`,
460
+ qrCode: qrCodeData
461
+ });
462
+ }
463
+ });
464
+ // Handle process errors
465
+ linkProcess.on('error', (error) => {
466
+ reject(new Error(`Failed to start device linking: ${error.message}`));
467
+ });
468
+ });
469
+ }
470
+ /**
471
+ * Display ASCII QR code in console.
472
+ * Uses qrcode-terminal which is included as a dependency.
473
+ */
474
+ displayQRCode(uri) {
475
+ qrcodeTerminal.generate(uri, { small: true });
476
+ }
477
+ async addDevice(uri, deviceName) {
478
+ await this.sendJsonRpcRequest('addDevice', { account: this.account, uri, deviceName });
479
+ }
480
+ async sendSyncRequest() {
481
+ await this.sendJsonRpcRequest('sendSyncRequest', { account: this.account });
482
+ }
483
+ async sendMessageRequestResponse(recipient, response) {
484
+ await this.sendJsonRpcRequest('sendMessageRequestResponse', { account: this.account, recipient, type: response });
485
+ }
486
+ async getVersion() {
487
+ return this.sendJsonRpcRequest('version');
488
+ }
489
+ async createGroup(name, members) {
490
+ return this.sendJsonRpcRequest('updateGroup', { account: this.account, name, members });
491
+ }
492
+ async updateGroup(groupId, options) {
493
+ const params = { groupId, account: this.account };
494
+ if (options.name)
495
+ params.name = options.name;
496
+ if (options.description)
497
+ params.description = options.description;
498
+ if (options.avatar)
499
+ params.avatar = options.avatar;
500
+ if (options.addMembers)
501
+ params.addMembers = options.addMembers;
502
+ if (options.removeMembers)
503
+ params.removeMembers = options.removeMembers;
504
+ if (options.promoteAdmins)
505
+ params.promoteAdmins = options.promoteAdmins;
506
+ if (options.demoteAdmins)
507
+ params.demoteAdmins = options.demoteAdmins;
508
+ if (options.resetInviteLink)
509
+ params.resetLink = true;
510
+ if (options.permissionAddMember)
511
+ params.permissionAddMember = options.permissionAddMember;
512
+ if (options.permissionEditDetails)
513
+ params.permissionEditDetails = options.permissionEditDetails;
514
+ if (options.permissionSendMessage)
515
+ params.permissionSendMessage = options.permissionSendMessage;
516
+ if (options.expirationTimer)
517
+ params.expiration = options.expirationTimer;
518
+ await this.sendJsonRpcRequest('updateGroup', params);
519
+ }
520
+ async listGroups() {
521
+ return this.sendJsonRpcRequest('listGroups', { account: this.account });
522
+ }
523
+ async listContacts() {
524
+ return this.sendJsonRpcRequest('listContacts', { account: this.account });
525
+ }
526
+ async listDevices() {
527
+ return this.sendJsonRpcRequest('listDevices', { account: this.account });
528
+ }
529
+ async listAccounts() {
530
+ const result = await this.sendJsonRpcRequest('listAccounts');
531
+ return result.accounts.map((acc) => acc.number);
532
+ }
533
+ // ############# Deprecated Methods (Kept for backward compatibility) #############
534
+ /**
535
+ * @deprecated Use `connect` and listen for `message` events instead.
536
+ * This method now provides a compatibility layer by connecting and buffering messages.
537
+ */
538
+ async receiveMessages() {
539
+ console.warn("receiveMessages is deprecated and will be removed in a future version. Use connect() and listen for 'message' events instead.");
540
+ // Return empty array but log helpful migration info
541
+ console.info("Migration guide: Replace receiveMessages() with:");
542
+ console.info(" await signalCli.connect();");
543
+ console.info(" signalCli.on('message', (msg) => { /* handle message */ });");
544
+ return Promise.resolve([]);
545
+ }
546
+ /**
547
+ * @deprecated Use `connect` instead.
548
+ * This method now calls connect() for backward compatibility.
549
+ */
550
+ startDaemon() {
551
+ console.warn("startDaemon is deprecated. Use connect() instead.");
552
+ this.connect();
553
+ }
554
+ /**
555
+ * @deprecated Use `gracefulShutdown` or `disconnect` instead.
556
+ * This method now calls gracefulShutdown() for backward compatibility.
557
+ */
558
+ stopDaemon() {
559
+ console.warn("stopDaemon is deprecated. Use gracefulShutdown() or disconnect() instead.");
560
+ this.gracefulShutdown();
561
+ }
562
+ // ############# NEW FEATURES - Missing signal-cli Commands #############
563
+ /**
564
+ * Remove a contact from the contact list.
565
+ * @param number - The phone number of the contact to remove
566
+ * @param options - Options for how to remove the contact
567
+ */
568
+ async removeContact(number, options = {}) {
569
+ const params = {
570
+ account: this.account,
571
+ recipient: number
572
+ };
573
+ if (options.hide)
574
+ params.hide = true;
575
+ if (options.forget)
576
+ params.forget = true;
577
+ await this.sendJsonRpcRequest('removeContact', params);
578
+ }
579
+ /**
580
+ * Check if phone numbers are registered with Signal.
581
+ * @param numbers - Array of phone numbers to check
582
+ * @param usernames - Optional array of usernames to check
583
+ * @returns Array of user status results
584
+ */
585
+ async getUserStatus(numbers = [], usernames = []) {
586
+ const params = { account: this.account };
587
+ if (numbers.length > 0)
588
+ params.recipients = numbers;
589
+ if (usernames.length > 0)
590
+ params.usernames = usernames;
591
+ const result = await this.sendJsonRpcRequest('getUserStatus', params);
592
+ // Transform the result to match our interface
593
+ const statusResults = [];
594
+ if (result.recipients) {
595
+ result.recipients.forEach((recipient) => {
596
+ statusResults.push({
597
+ number: recipient.number,
598
+ isRegistered: recipient.isRegistered || false,
599
+ uuid: recipient.uuid,
600
+ username: recipient.username
601
+ });
602
+ });
603
+ }
604
+ return statusResults;
605
+ }
606
+ /**
607
+ * Send a payment notification to a recipient.
608
+ * @param recipient - Phone number or group ID to send the notification to
609
+ * @param paymentData - Payment notification data including receipt
610
+ * @returns Send response with timestamp and other details
611
+ */
612
+ async sendPaymentNotification(recipient, paymentData) {
613
+ const params = {
614
+ receipt: paymentData.receipt,
615
+ account: this.account
616
+ };
617
+ if (paymentData.note) {
618
+ params.note = paymentData.note;
619
+ }
620
+ if (this.isGroupId(recipient)) {
621
+ params.groupId = recipient;
622
+ }
623
+ else {
624
+ params.recipient = recipient;
625
+ }
626
+ return this.sendJsonRpcRequest('sendPaymentNotification', params);
627
+ }
628
+ /**
629
+ * Upload a custom sticker pack to Signal.
630
+ * @param manifest - Sticker pack manifest information
631
+ * @returns Upload result with pack ID and key
632
+ */
633
+ async uploadStickerPack(manifest) {
634
+ const params = {
635
+ account: this.account,
636
+ path: manifest.path
637
+ };
638
+ const result = await this.sendJsonRpcRequest('uploadStickerPack', params);
639
+ return {
640
+ packId: result.packId,
641
+ packKey: result.packKey,
642
+ installUrl: result.installUrl
643
+ };
644
+ }
645
+ /**
646
+ * Submit a rate limit challenge to lift rate limiting.
647
+ * @param challenge - Challenge token from the proof required error
648
+ * @param captcha - Captcha token from solved captcha
649
+ * @returns Challenge result indicating success/failure
650
+ */
651
+ async submitRateLimitChallenge(challenge, captcha) {
652
+ const params = {
653
+ account: this.account,
654
+ challenge,
655
+ captcha
656
+ };
657
+ const result = await this.sendJsonRpcRequest('submitRateLimitChallenge', params);
658
+ return {
659
+ success: result.success || false,
660
+ retryAfter: result.retryAfter,
661
+ message: result.message
662
+ };
663
+ }
664
+ /**
665
+ * Start the process of changing phone number.
666
+ * @param newNumber - The new phone number to change to
667
+ * @param voice - Whether to use voice verification instead of SMS
668
+ * @param captcha - Captcha token if required
669
+ * @returns Change number session information
670
+ */
671
+ async startChangeNumber(newNumber, voice = false, captcha) {
672
+ const params = {
673
+ account: this.account,
674
+ number: newNumber,
675
+ voice
676
+ };
677
+ if (captcha)
678
+ params.captcha = captcha;
679
+ const result = await this.sendJsonRpcRequest('startChangeNumber', params);
680
+ return {
681
+ session: result.session,
682
+ newNumber,
683
+ challenge: result.challenge
684
+ };
685
+ }
686
+ /**
687
+ * Finish the phone number change process with verification code.
688
+ * @param verificationCode - The verification code received via SMS/voice
689
+ * @param pin - Registration lock PIN if enabled
690
+ */
691
+ async finishChangeNumber(verificationCode, pin) {
692
+ const params = {
693
+ account: this.account,
694
+ code: verificationCode
695
+ };
696
+ if (pin)
697
+ params.pin = pin;
698
+ await this.sendJsonRpcRequest('finishChangeNumber', params);
699
+ }
700
+ /**
701
+ * Enhanced send message with progress callback support.
702
+ * @param recipient - Phone number or group ID
703
+ * @param message - Message text
704
+ * @param options - Send options including progress callback
705
+ * @returns Send response
706
+ */
707
+ async sendMessageWithProgress(recipient, message, options = {}) {
708
+ // For now, this is the same as sendMessage since signal-cli doesn't provide
709
+ // native progress callbacks. This is a placeholder for future enhancement.
710
+ const { onProgress, ...sendOptions } = options;
711
+ // Simulate progress for large attachments
712
+ if (onProgress && sendOptions.attachments && sendOptions.attachments.length > 0) {
713
+ // Simulate upload progress
714
+ for (let i = 0; i <= 100; i += 10) {
715
+ onProgress({
716
+ total: 100,
717
+ uploaded: i,
718
+ percentage: i
719
+ });
720
+ // Small delay to simulate upload
721
+ await new Promise(resolve => setTimeout(resolve, 50));
722
+ }
723
+ }
724
+ return this.sendMessage(recipient, message, sendOptions);
725
+ }
726
+ // ========== NEW METHODS FOR 100% signal-cli COMPATIBILITY ==========
727
+ /**
728
+ * Send a poll create message to a recipient or group.
729
+ * @param options Poll creation options
730
+ * @returns Send response with timestamp
731
+ */
732
+ async sendPollCreate(options) {
733
+ this.logger.debug('Sending poll create', options);
734
+ (0, validators_1.validateMessage)(options.question, 500);
735
+ if (!options.options || options.options.length < 2) {
736
+ throw new errors_1.MessageError('Poll must have at least 2 options');
737
+ }
738
+ if (options.options.length > 10) {
739
+ throw new errors_1.MessageError('Poll cannot have more than 10 options');
740
+ }
741
+ const params = {
742
+ question: options.question,
743
+ options: options.options,
744
+ account: this.account
745
+ };
746
+ if (options.multiSelect !== undefined) {
747
+ params.multiSelect = options.multiSelect;
748
+ }
749
+ if (options.groupId) {
750
+ (0, validators_1.validateGroupId)(options.groupId);
751
+ params.groupId = options.groupId;
752
+ }
753
+ else if (options.recipients) {
754
+ params.recipients = options.recipients.map(r => {
755
+ (0, validators_1.validateRecipient)(r);
756
+ return r;
757
+ });
758
+ }
759
+ else {
760
+ throw new errors_1.MessageError('Must specify either recipients or groupId');
761
+ }
762
+ return this.sendJsonRpcRequest('sendPollCreate', params);
763
+ }
764
+ /**
765
+ * Send a poll vote message to vote on a poll.
766
+ * @param recipient Recipient or group ID
767
+ * @param options Poll vote options
768
+ * @returns Send response with timestamp
769
+ */
770
+ async sendPollVote(recipient, options) {
771
+ this.logger.debug('Sending poll vote', { recipient, options });
772
+ (0, validators_1.validateRecipient)(options.pollAuthor);
773
+ (0, validators_1.validateTimestamp)(options.pollTimestamp);
774
+ if (!options.optionIndexes || options.optionIndexes.length === 0) {
775
+ throw new errors_1.MessageError('Must specify at least one option to vote for');
776
+ }
777
+ const params = {
778
+ pollAuthor: options.pollAuthor,
779
+ pollTimestamp: options.pollTimestamp,
780
+ options: options.optionIndexes,
781
+ account: this.account
782
+ };
783
+ if (options.voteCount !== undefined) {
784
+ params.voteCount = options.voteCount;
785
+ }
786
+ if (this.isGroupId(recipient)) {
787
+ (0, validators_1.validateGroupId)(recipient);
788
+ params.groupId = recipient;
789
+ }
790
+ else {
791
+ (0, validators_1.validateRecipient)(recipient);
792
+ params.recipient = recipient;
793
+ }
794
+ return this.sendJsonRpcRequest('sendPollVote', params);
795
+ }
796
+ /**
797
+ * Send a poll terminate message to close a poll.
798
+ * @param recipient Recipient or group ID
799
+ * @param options Poll terminate options
800
+ * @returns Send response with timestamp
801
+ */
802
+ async sendPollTerminate(recipient, options) {
803
+ this.logger.debug('Sending poll terminate', { recipient, options });
804
+ (0, validators_1.validateTimestamp)(options.pollTimestamp);
805
+ const params = {
806
+ pollTimestamp: options.pollTimestamp,
807
+ account: this.account
808
+ };
809
+ if (this.isGroupId(recipient)) {
810
+ (0, validators_1.validateGroupId)(recipient);
811
+ params.groupId = recipient;
812
+ }
813
+ else {
814
+ (0, validators_1.validateRecipient)(recipient);
815
+ params.recipient = recipient;
816
+ }
817
+ return this.sendJsonRpcRequest('sendPollTerminate', params);
818
+ }
819
+ /**
820
+ * Update account information (device name, username, privacy settings).
821
+ * @param options Account update options
822
+ * @returns Account update result
823
+ */
824
+ async updateAccount(options) {
825
+ this.logger.debug('Updating account', options);
826
+ const params = { account: this.account };
827
+ if (options.deviceName) {
828
+ params.deviceName = options.deviceName;
829
+ }
830
+ if (options.username) {
831
+ params.username = options.username;
832
+ }
833
+ if (options.deleteUsername) {
834
+ params.deleteUsername = true;
835
+ }
836
+ if (options.unrestrictedUnidentifiedSender !== undefined) {
837
+ params.unrestrictedUnidentifiedSender = options.unrestrictedUnidentifiedSender;
838
+ }
839
+ if (options.discoverableByNumber !== undefined) {
840
+ params.discoverableByNumber = options.discoverableByNumber;
841
+ }
842
+ if (options.numberSharing !== undefined) {
843
+ params.numberSharing = options.numberSharing;
844
+ }
845
+ try {
846
+ const result = await this.sendJsonRpcRequest('updateAccount', params);
847
+ return {
848
+ success: true,
849
+ username: result.username,
850
+ usernameLink: result.usernameLink
851
+ };
852
+ }
853
+ catch (error) {
854
+ return {
855
+ success: false,
856
+ error: error instanceof Error ? error.message : 'Unknown error'
857
+ };
858
+ }
859
+ }
860
+ /**
861
+ * Get raw attachment data as base64 string.
862
+ * @param options Attachment retrieval options
863
+ * @returns Base64 encoded attachment data
864
+ */
865
+ async getAttachment(options) {
866
+ this.logger.debug('Getting attachment', options);
867
+ if (!options.id) {
868
+ throw new errors_1.MessageError('Attachment ID is required');
869
+ }
870
+ const params = {
871
+ id: options.id,
872
+ account: this.account
873
+ };
874
+ if (options.groupId) {
875
+ (0, validators_1.validateGroupId)(options.groupId);
876
+ params.groupId = options.groupId;
877
+ }
878
+ else if (options.recipient) {
879
+ (0, validators_1.validateRecipient)(options.recipient);
880
+ params.recipient = options.recipient;
881
+ }
882
+ const result = await this.sendJsonRpcRequest('getAttachment', params);
883
+ return result.data || result;
884
+ }
885
+ /**
886
+ * Get raw avatar data as base64 string.
887
+ * @param options Avatar retrieval options
888
+ * @returns Base64 encoded avatar data
889
+ */
890
+ async getAvatar(options) {
891
+ this.logger.debug('Getting avatar', options);
892
+ const params = { account: this.account };
893
+ if (options.contact) {
894
+ (0, validators_1.validateRecipient)(options.contact);
895
+ params.contact = options.contact;
896
+ }
897
+ else if (options.profile) {
898
+ (0, validators_1.validateRecipient)(options.profile);
899
+ params.profile = options.profile;
900
+ }
901
+ else if (options.groupId) {
902
+ (0, validators_1.validateGroupId)(options.groupId);
903
+ params.groupId = options.groupId;
904
+ }
905
+ else {
906
+ throw new errors_1.MessageError('Must specify contact, profile, or groupId');
907
+ }
908
+ const result = await this.sendJsonRpcRequest('getAvatar', params);
909
+ return result.data || result;
910
+ }
911
+ /**
912
+ * Get raw sticker data as base64 string.
913
+ * @param options Sticker retrieval options
914
+ * @returns Base64 encoded sticker data
915
+ */
916
+ async getSticker(options) {
917
+ this.logger.debug('Getting sticker', options);
918
+ if (!options.packId || !options.stickerId) {
919
+ throw new errors_1.MessageError('Pack ID and sticker ID are required');
920
+ }
921
+ const params = {
922
+ packId: options.packId,
923
+ stickerId: options.stickerId,
924
+ account: this.account
925
+ };
926
+ const result = await this.sendJsonRpcRequest('getSticker', params);
927
+ return result.data || result;
928
+ }
929
+ /**
930
+ * Send contacts synchronization message to linked devices.
931
+ * @param options Contacts sync options
932
+ */
933
+ async sendContacts(options = {}) {
934
+ this.logger.debug('Sending contacts sync');
935
+ const params = { account: this.account };
936
+ if (options.includeAllRecipients) {
937
+ params.allRecipients = true;
938
+ }
939
+ await this.sendJsonRpcRequest('sendContacts', params);
940
+ }
941
+ /**
942
+ * List groups with optional filtering and details.
943
+ * @param options List groups options
944
+ * @returns Array of group information
945
+ */
946
+ async listGroupsDetailed(options = {}) {
947
+ this.logger.debug('Listing groups with options', options);
948
+ const params = { account: this.account };
949
+ if (options.detailed) {
950
+ params.detailed = true;
951
+ }
952
+ if (options.groupIds && options.groupIds.length > 0) {
953
+ params.groupId = options.groupIds;
954
+ }
955
+ return this.sendJsonRpcRequest('listGroups', params);
956
+ }
957
+ /**
958
+ * List all local accounts.
959
+ * @returns Array of account phone numbers
960
+ */
961
+ async listAccountsDetailed() {
962
+ this.logger.debug('Listing all accounts');
963
+ const result = await this.sendJsonRpcRequest('listAccounts');
964
+ return result.accounts || [];
965
+ }
966
+ }
967
+ exports.SignalCli = SignalCli;