signal-sdk 0.0.7 → 0.0.9

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.
@@ -0,0 +1,711 @@
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
+ class SignalCli extends events_1.EventEmitter {
43
+ constructor(accountOrPath, account) {
44
+ super();
45
+ this.cliProcess = null;
46
+ this.requestPromises = new Map();
47
+ let signalCliPath;
48
+ let phoneNumber;
49
+ // Smart parameter detection
50
+ if (typeof accountOrPath === 'string') {
51
+ if (accountOrPath.startsWith('+')) {
52
+ // First parameter is a phone number
53
+ phoneNumber = accountOrPath;
54
+ signalCliPath = undefined;
55
+ }
56
+ else {
57
+ // First parameter is a path, second is phone number
58
+ signalCliPath = accountOrPath;
59
+ phoneNumber = account;
60
+ }
61
+ }
62
+ // Determine the correct signal-cli path based on platform
63
+ let defaultPath;
64
+ if (process.platform === 'win32') {
65
+ defaultPath = path.join(__dirname, '..', 'bin', 'signal-cli.bat');
66
+ }
67
+ else {
68
+ // For Unix/Linux systems, use the shell script
69
+ defaultPath = path.join(__dirname, '..', 'bin', 'signal-cli');
70
+ }
71
+ this.signalCliPath = signalCliPath || defaultPath;
72
+ this.account = phoneNumber;
73
+ }
74
+ async connect() {
75
+ const args = this.account ? ['-a', this.account, 'jsonRpc'] : ['jsonRpc'];
76
+ if (process.platform === 'win32') {
77
+ // On Windows, use cmd.exe to run the batch file with proper quoting for paths with spaces
78
+ this.cliProcess = (0, child_process_1.spawn)('cmd.exe', ['/c', `"${this.signalCliPath}"`, ...args]);
79
+ }
80
+ else {
81
+ this.cliProcess = (0, child_process_1.spawn)(this.signalCliPath, args);
82
+ }
83
+ this.cliProcess.stdout?.on('data', (data) => this.handleRpcResponse(data.toString('utf8')));
84
+ this.cliProcess.stderr?.on('data', (data) => this.handleStderrData(data.toString('utf8')));
85
+ this.cliProcess.on('close', (code) => {
86
+ this.emit('close', code);
87
+ this.cliProcess = null;
88
+ });
89
+ this.cliProcess.on('error', (err) => this.emit('error', err));
90
+ return new Promise((resolve, reject) => {
91
+ // Set a timeout to resolve the promise even if no data is received
92
+ const connectTimeout = setTimeout(() => {
93
+ // Test if the process is still alive and responsive
94
+ if (this.cliProcess && !this.cliProcess.killed) {
95
+ resolve();
96
+ }
97
+ else {
98
+ reject(new Error('signal-cli process failed to start'));
99
+ }
100
+ }, 2000); // Wait 2 seconds max
101
+ // If we get data quickly, resolve immediately
102
+ this.cliProcess?.stdout?.once('data', () => {
103
+ clearTimeout(connectTimeout);
104
+ resolve();
105
+ });
106
+ // If there's an error, reject
107
+ this.cliProcess?.once('error', (err) => {
108
+ clearTimeout(connectTimeout);
109
+ reject(err);
110
+ });
111
+ // If the process exits immediately, that's an error
112
+ this.cliProcess?.once('close', (code) => {
113
+ clearTimeout(connectTimeout);
114
+ if (code !== 0) {
115
+ reject(new Error(`signal-cli exited with code ${code}`));
116
+ }
117
+ });
118
+ });
119
+ }
120
+ disconnect() {
121
+ if (this.cliProcess) {
122
+ this.cliProcess.kill();
123
+ this.cliProcess = null;
124
+ }
125
+ }
126
+ async gracefulShutdown() {
127
+ return new Promise((resolve) => {
128
+ if (!this.cliProcess) {
129
+ resolve();
130
+ return;
131
+ }
132
+ // Listen for the process to close
133
+ this.cliProcess.once('close', () => {
134
+ this.cliProcess = null;
135
+ resolve();
136
+ });
137
+ // Send SIGTERM for graceful shutdown
138
+ this.cliProcess.kill('SIGTERM');
139
+ // Force kill after 5 seconds if it doesn't close gracefully
140
+ setTimeout(() => {
141
+ if (this.cliProcess) {
142
+ this.cliProcess.kill('SIGKILL');
143
+ this.cliProcess = null;
144
+ resolve();
145
+ }
146
+ }, 5000);
147
+ });
148
+ }
149
+ handleRpcResponse(data) {
150
+ const lines = data.trim().split('\n');
151
+ for (const line of lines) {
152
+ if (!line)
153
+ continue;
154
+ try {
155
+ const response = JSON.parse(line);
156
+ if ('id' in response && response.id) {
157
+ const promise = this.requestPromises.get(response.id);
158
+ if (promise) {
159
+ if (response.error) {
160
+ promise.reject(new Error(`[${response.error.code}] ${response.error.message}`));
161
+ }
162
+ else {
163
+ promise.resolve(response.result);
164
+ }
165
+ this.requestPromises.delete(response.id);
166
+ }
167
+ }
168
+ else if ('method' in response) {
169
+ this.emit('notification', response);
170
+ if (response.method === 'receive') {
171
+ this.emit('message', response.params);
172
+ }
173
+ }
174
+ }
175
+ catch (error) {
176
+ this.emit('error', new Error(`Failed to parse JSON-RPC response: ${line}`));
177
+ }
178
+ }
179
+ }
180
+ handleStderrData(data) {
181
+ const message = data.trim();
182
+ if (!message)
183
+ return;
184
+ // Parse log level from signal-cli stderr output
185
+ // Format: "LEVEL Component - Message"
186
+ const logLevelMatch = message.match(/^(ERROR|WARN|INFO|DEBUG)\s+/);
187
+ const logLevel = logLevelMatch ? logLevelMatch[1] : 'UNKNOWN';
188
+ // Only emit errors for actual ERROR level messages
189
+ // INFO, WARN, DEBUG messages are just logged without crashing
190
+ if (logLevel === 'ERROR') {
191
+ this.emit('error', new Error(`signal-cli error: ${message}`));
192
+ }
193
+ else {
194
+ // Emit as a warning event for non-error messages
195
+ this.emit('log', { level: logLevel.toLowerCase(), message });
196
+ // Filter out common informational WARN messages from signal-cli
197
+ const isInformationalWarn = logLevel === 'WARN' && (message.includes('Failed to get sender certificate') ||
198
+ message.includes('ignoring: java.lang.InterruptedException') ||
199
+ message.includes('Request was interrupted') ||
200
+ message.includes('Connection reset') ||
201
+ message.includes('Socket closed') ||
202
+ message.includes('gracefully closing'));
203
+ // Only log actual warning messages, not informational ones
204
+ if (logLevel === 'WARN' && !isInformationalWarn) {
205
+ console.warn(`[signal-cli] ${message}`);
206
+ }
207
+ else if (logLevel === 'INFO' || logLevel === 'DEBUG') {
208
+ // Silently ignore INFO and DEBUG messages to avoid spam
209
+ // Uncomment the line below if you want to see all signal-cli logs:
210
+ // console.log(`[signal-cli] ${message}`);
211
+ }
212
+ else if (logLevel === 'UNKNOWN') {
213
+ // Unknown format, log as warning only if it doesn't look informational
214
+ if (!message.includes('Daemon closed') && !message.includes('gracefully')) {
215
+ console.warn(`[signal-cli] ${message}`);
216
+ }
217
+ }
218
+ }
219
+ }
220
+ async sendJsonRpcRequest(method, params) {
221
+ if (!this.cliProcess || !this.cliProcess.stdin) {
222
+ throw new Error('Not connected. Call connect() first.');
223
+ }
224
+ const id = (0, uuid_1.v4)();
225
+ const request = {
226
+ jsonrpc: '2.0',
227
+ method,
228
+ params,
229
+ id,
230
+ };
231
+ const promise = new Promise((resolve, reject) => {
232
+ this.requestPromises.set(id, { resolve, reject });
233
+ });
234
+ this.cliProcess.stdin.write(JSON.stringify(request) + '\n');
235
+ return promise;
236
+ }
237
+ isGroupId(recipient) {
238
+ return recipient.includes('=') || recipient.includes('/') || (recipient.includes('+') && !recipient.startsWith('+'));
239
+ }
240
+ // ############# Refactored Methods #############
241
+ async register(number, voice, captcha) {
242
+ await this.sendJsonRpcRequest('register', { account: number, voice, captcha });
243
+ }
244
+ async verify(number, token, pin) {
245
+ await this.sendJsonRpcRequest('verify', { account: number, token, pin });
246
+ }
247
+ async sendMessage(recipient, message, options = {}) {
248
+ const params = {
249
+ message,
250
+ account: this.account
251
+ };
252
+ // Add recipient information
253
+ if (this.isGroupId(recipient)) {
254
+ params.groupId = recipient;
255
+ }
256
+ else {
257
+ params.recipients = [recipient];
258
+ }
259
+ // Only add safe, well-known options to avoid JSON parsing issues
260
+ if (options.attachments && options.attachments.length > 0) {
261
+ params.attachments = options.attachments;
262
+ }
263
+ if (options.expiresInSeconds) {
264
+ params.expiresInSeconds = options.expiresInSeconds;
265
+ }
266
+ if (options.isViewOnce) {
267
+ params.isViewOnce = options.isViewOnce;
268
+ }
269
+ return this.sendJsonRpcRequest('send', params);
270
+ }
271
+ async sendReaction(recipient, targetAuthor, targetTimestamp, emoji, remove = false) {
272
+ const params = {
273
+ emoji,
274
+ targetAuthor,
275
+ targetTimestamp,
276
+ remove,
277
+ account: this.account
278
+ };
279
+ if (this.isGroupId(recipient)) {
280
+ params.groupId = recipient;
281
+ }
282
+ else {
283
+ params.recipients = [recipient];
284
+ }
285
+ return this.sendJsonRpcRequest('sendReaction', params);
286
+ }
287
+ async sendTyping(recipient, stop = false) {
288
+ const params = { when: !stop, account: this.account };
289
+ if (this.isGroupId(recipient)) {
290
+ params.groupId = recipient;
291
+ }
292
+ else {
293
+ params.recipients = [recipient];
294
+ }
295
+ await this.sendJsonRpcRequest('sendTyping', params);
296
+ }
297
+ async remoteDeleteMessage(recipient, targetTimestamp) {
298
+ const params = { targetTimestamp, account: this.account };
299
+ if (this.isGroupId(recipient)) {
300
+ params.groupId = recipient;
301
+ }
302
+ else {
303
+ params.recipients = [recipient];
304
+ }
305
+ await this.sendJsonRpcRequest('remoteDelete', params);
306
+ }
307
+ async updateContact(number, name, options = {}) {
308
+ await this.sendJsonRpcRequest('updateContact', { account: this.account, recipient: number, name, ...options });
309
+ }
310
+ async block(recipients, groupId) {
311
+ await this.sendJsonRpcRequest('block', { account: this.account, recipient: recipients, groupId });
312
+ }
313
+ async unblock(recipients, groupId) {
314
+ await this.sendJsonRpcRequest('unblock', { account: this.account, recipient: recipients, groupId });
315
+ }
316
+ async quitGroup(groupId) {
317
+ await this.sendJsonRpcRequest('quitGroup', { account: this.account, groupId });
318
+ }
319
+ async joinGroup(uri) {
320
+ await this.sendJsonRpcRequest('joinGroup', { account: this.account, uri });
321
+ }
322
+ async updateProfile(name, about, aboutEmoji, avatar) {
323
+ await this.sendJsonRpcRequest('updateProfile', { account: this.account, name, about, aboutEmoji, avatar });
324
+ }
325
+ async sendReceipt(recipient, targetTimestamp, type = 'read') {
326
+ await this.sendJsonRpcRequest('sendReceipt', { account: this.account, recipient, targetTimestamp, type });
327
+ }
328
+ async listStickerPacks() {
329
+ return this.sendJsonRpcRequest('listStickerPacks', { account: this.account });
330
+ }
331
+ async addStickerPack(packId, packKey) {
332
+ await this.sendJsonRpcRequest('addStickerPack', { account: this.account, packId, packKey });
333
+ }
334
+ async unregister() {
335
+ await this.sendJsonRpcRequest('unregister', { account: this.account });
336
+ }
337
+ async deleteLocalAccountData() {
338
+ await this.sendJsonRpcRequest('deleteLocalAccountData', { account: this.account });
339
+ }
340
+ async updateAccountConfiguration(config) {
341
+ await this.sendJsonRpcRequest('updateConfiguration', { account: this.account, ...config });
342
+ }
343
+ async removeDevice(deviceId) {
344
+ await this.sendJsonRpcRequest('removeDevice', { account: this.account, deviceId });
345
+ }
346
+ async setPin(pin) {
347
+ await this.sendJsonRpcRequest('setPin', { account: this.account, pin });
348
+ }
349
+ async removePin() {
350
+ await this.sendJsonRpcRequest('removePin', { account: this.account });
351
+ }
352
+ async listIdentities(number) {
353
+ return this.sendJsonRpcRequest('listIdentities', { account: this.account, number });
354
+ }
355
+ async trustIdentity(number, safetyNumber, verified = true) {
356
+ await this.sendJsonRpcRequest('trust', { account: this.account, recipient: number, safetyNumber, verified });
357
+ }
358
+ async link(deviceName) {
359
+ const result = await this.sendJsonRpcRequest('link', { deviceName });
360
+ return result.uri;
361
+ }
362
+ /**
363
+ * Link a new device to an existing Signal account with QR code support.
364
+ * This method provides a complete device linking solution with QR code generation.
365
+ *
366
+ * @param options - Linking options including device name and QR code output preferences
367
+ * @returns Promise resolving to LinkingResult with QR code data and linking status
368
+ */
369
+ async deviceLink(options = {}) {
370
+ const { spawn } = await Promise.resolve().then(() => __importStar(require('child_process')));
371
+ return new Promise((resolve, reject) => {
372
+ const deviceName = options.name || 'Signal SDK Device';
373
+ // Spawn signal-cli link command
374
+ let linkProcess;
375
+ if (process.platform === 'win32') {
376
+ // On Windows, use cmd.exe to run the batch file with proper quoting for paths with spaces
377
+ linkProcess = spawn('cmd.exe', ['/c', `"${this.signalCliPath}"`, 'link', '--name', deviceName], {
378
+ stdio: ['pipe', 'pipe', 'pipe']
379
+ });
380
+ }
381
+ else {
382
+ linkProcess = spawn(this.signalCliPath, ['link', '--name', deviceName], {
383
+ stdio: ['pipe', 'pipe', 'pipe']
384
+ });
385
+ }
386
+ let qrCodeData;
387
+ let linkingComplete = false;
388
+ let hasError = false;
389
+ // Handle stdout (where QR code URI will be)
390
+ linkProcess.stdout.on('data', (data) => {
391
+ const output = data.toString('utf8').trim();
392
+ // Look for QR code URI (starts with sgnl://)
393
+ if (output.includes('sgnl://')) {
394
+ const uriMatch = output.match(/sgnl:\/\/[^\s]+/);
395
+ if (uriMatch && !qrCodeData) {
396
+ const uri = uriMatch[0];
397
+ qrCodeData = {
398
+ uri
399
+ };
400
+ // Auto-display QR code if requested
401
+ if (options.qrCodeOutput === 'console') {
402
+ console.log('\n- QR CODE - SCAN WITH YOUR PHONE:');
403
+ console.log('===================================');
404
+ this.displayQRCode(uri);
405
+ console.log('===================================\n');
406
+ }
407
+ }
408
+ }
409
+ // Check for successful linking
410
+ if (output.includes('Device registered') || output.includes('Successfully linked')) {
411
+ linkingComplete = true;
412
+ }
413
+ });
414
+ // Handle stderr for errors
415
+ linkProcess.stderr.on('data', (data) => {
416
+ const error = data.toString('utf8').trim();
417
+ // Filter out informational messages
418
+ if (!error.includes('INFO') && !error.includes('DEBUG') && error.length > 0) {
419
+ hasError = true;
420
+ }
421
+ });
422
+ // Handle process exit
423
+ linkProcess.on('close', (code) => {
424
+ if (code === 0 && linkingComplete) {
425
+ resolve({
426
+ success: true,
427
+ isLinked: true,
428
+ deviceName,
429
+ qrCode: qrCodeData
430
+ });
431
+ }
432
+ else if (code === 0 && qrCodeData) {
433
+ resolve({
434
+ success: true,
435
+ isLinked: false,
436
+ deviceName,
437
+ qrCode: qrCodeData
438
+ });
439
+ }
440
+ else {
441
+ resolve({
442
+ success: false,
443
+ error: hasError ? 'Device linking failed' : `signal-cli exited with code ${code}`,
444
+ qrCode: qrCodeData
445
+ });
446
+ }
447
+ });
448
+ // Handle process errors
449
+ linkProcess.on('error', (error) => {
450
+ reject(new Error(`Failed to start device linking: ${error.message}`));
451
+ });
452
+ });
453
+ }
454
+ /**
455
+ * Display ASCII QR code in console.
456
+ * Uses qrcode-terminal which is included as a dependency.
457
+ */
458
+ displayQRCode(uri) {
459
+ qrcodeTerminal.generate(uri, { small: true });
460
+ }
461
+ async addDevice(uri, deviceName) {
462
+ await this.sendJsonRpcRequest('addDevice', { account: this.account, uri, deviceName });
463
+ }
464
+ async sendSyncRequest() {
465
+ await this.sendJsonRpcRequest('sendSyncRequest', { account: this.account });
466
+ }
467
+ async sendMessageRequestResponse(recipient, response) {
468
+ await this.sendJsonRpcRequest('sendMessageRequestResponse', { account: this.account, recipient, type: response });
469
+ }
470
+ async getVersion() {
471
+ return this.sendJsonRpcRequest('version');
472
+ }
473
+ async createGroup(name, members) {
474
+ return this.sendJsonRpcRequest('updateGroup', { account: this.account, name, members });
475
+ }
476
+ async updateGroup(groupId, options) {
477
+ const params = { groupId, account: this.account };
478
+ if (options.name)
479
+ params.name = options.name;
480
+ if (options.description)
481
+ params.description = options.description;
482
+ if (options.avatar)
483
+ params.avatar = options.avatar;
484
+ if (options.addMembers)
485
+ params.addMembers = options.addMembers;
486
+ if (options.removeMembers)
487
+ params.removeMembers = options.removeMembers;
488
+ if (options.promoteAdmins)
489
+ params.promoteAdmins = options.promoteAdmins;
490
+ if (options.demoteAdmins)
491
+ params.demoteAdmins = options.demoteAdmins;
492
+ if (options.resetInviteLink)
493
+ params.resetLink = true;
494
+ if (options.permissionAddMember)
495
+ params.permissionAddMember = options.permissionAddMember;
496
+ if (options.permissionEditDetails)
497
+ params.permissionEditDetails = options.permissionEditDetails;
498
+ if (options.permissionSendMessage)
499
+ params.permissionSendMessage = options.permissionSendMessage;
500
+ if (options.expirationTimer)
501
+ params.expiration = options.expirationTimer;
502
+ await this.sendJsonRpcRequest('updateGroup', params);
503
+ }
504
+ async listGroups() {
505
+ return this.sendJsonRpcRequest('listGroups', { account: this.account });
506
+ }
507
+ async listContacts() {
508
+ return this.sendJsonRpcRequest('listContacts', { account: this.account });
509
+ }
510
+ async listDevices() {
511
+ return this.sendJsonRpcRequest('listDevices', { account: this.account });
512
+ }
513
+ async listAccounts() {
514
+ const result = await this.sendJsonRpcRequest('listAccounts');
515
+ return result.accounts.map((acc) => acc.number);
516
+ }
517
+ // ############# Deprecated Methods (Kept for backward compatibility) #############
518
+ /**
519
+ * @deprecated Use `connect` and listen for `message` events instead.
520
+ * This method now provides a compatibility layer by connecting and buffering messages.
521
+ */
522
+ async receiveMessages() {
523
+ console.warn("receiveMessages is deprecated and will be removed in a future version. Use connect() and listen for 'message' events instead.");
524
+ // Return empty array but log helpful migration info
525
+ console.info("Migration guide: Replace receiveMessages() with:");
526
+ console.info(" await signalCli.connect();");
527
+ console.info(" signalCli.on('message', (msg) => { /* handle message */ });");
528
+ return Promise.resolve([]);
529
+ }
530
+ /**
531
+ * @deprecated Use `connect` instead.
532
+ * This method now calls connect() for backward compatibility.
533
+ */
534
+ startDaemon() {
535
+ console.warn("startDaemon is deprecated. Use connect() instead.");
536
+ this.connect();
537
+ }
538
+ /**
539
+ * @deprecated Use `gracefulShutdown` or `disconnect` instead.
540
+ * This method now calls gracefulShutdown() for backward compatibility.
541
+ */
542
+ stopDaemon() {
543
+ console.warn("stopDaemon is deprecated. Use gracefulShutdown() or disconnect() instead.");
544
+ this.gracefulShutdown();
545
+ }
546
+ // ############# NEW FEATURES - Missing signal-cli Commands #############
547
+ /**
548
+ * Remove a contact from the contact list.
549
+ * @param number - The phone number of the contact to remove
550
+ * @param options - Options for how to remove the contact
551
+ */
552
+ async removeContact(number, options = {}) {
553
+ const params = {
554
+ account: this.account,
555
+ recipient: number
556
+ };
557
+ if (options.hide)
558
+ params.hide = true;
559
+ if (options.forget)
560
+ params.forget = true;
561
+ await this.sendJsonRpcRequest('removeContact', params);
562
+ }
563
+ /**
564
+ * Check if phone numbers are registered with Signal.
565
+ * @param numbers - Array of phone numbers to check
566
+ * @param usernames - Optional array of usernames to check
567
+ * @returns Array of user status results
568
+ */
569
+ async getUserStatus(numbers = [], usernames = []) {
570
+ const params = { account: this.account };
571
+ if (numbers.length > 0)
572
+ params.recipients = numbers;
573
+ if (usernames.length > 0)
574
+ params.usernames = usernames;
575
+ const result = await this.sendJsonRpcRequest('getUserStatus', params);
576
+ // Transform the result to match our interface
577
+ const statusResults = [];
578
+ if (result.recipients) {
579
+ result.recipients.forEach((recipient) => {
580
+ statusResults.push({
581
+ number: recipient.number,
582
+ isRegistered: recipient.isRegistered || false,
583
+ uuid: recipient.uuid,
584
+ username: recipient.username
585
+ });
586
+ });
587
+ }
588
+ return statusResults;
589
+ }
590
+ /**
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
595
+ */
596
+ async sendPaymentNotification(recipient, paymentData) {
597
+ const params = {
598
+ receipt: paymentData.receipt,
599
+ account: this.account
600
+ };
601
+ if (paymentData.note) {
602
+ params.note = paymentData.note;
603
+ }
604
+ if (this.isGroupId(recipient)) {
605
+ params.groupId = recipient;
606
+ }
607
+ else {
608
+ params.recipient = recipient;
609
+ }
610
+ return this.sendJsonRpcRequest('sendPaymentNotification', params);
611
+ }
612
+ /**
613
+ * Upload a custom sticker pack to Signal.
614
+ * @param manifest - Sticker pack manifest information
615
+ * @returns Upload result with pack ID and key
616
+ */
617
+ async uploadStickerPack(manifest) {
618
+ const params = {
619
+ account: this.account,
620
+ path: manifest.path
621
+ };
622
+ const result = await this.sendJsonRpcRequest('uploadStickerPack', params);
623
+ return {
624
+ packId: result.packId,
625
+ packKey: result.packKey,
626
+ installUrl: result.installUrl
627
+ };
628
+ }
629
+ /**
630
+ * Submit a rate limit challenge to lift rate limiting.
631
+ * @param challenge - Challenge token from the proof required error
632
+ * @param captcha - Captcha token from solved captcha
633
+ * @returns Challenge result indicating success/failure
634
+ */
635
+ async submitRateLimitChallenge(challenge, captcha) {
636
+ const params = {
637
+ account: this.account,
638
+ challenge,
639
+ captcha
640
+ };
641
+ const result = await this.sendJsonRpcRequest('submitRateLimitChallenge', params);
642
+ return {
643
+ success: result.success || false,
644
+ retryAfter: result.retryAfter,
645
+ message: result.message
646
+ };
647
+ }
648
+ /**
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
654
+ */
655
+ async startChangeNumber(newNumber, voice = false, captcha) {
656
+ const params = {
657
+ account: this.account,
658
+ number: newNumber,
659
+ voice
660
+ };
661
+ if (captcha)
662
+ params.captcha = captcha;
663
+ const result = await this.sendJsonRpcRequest('startChangeNumber', params);
664
+ return {
665
+ session: result.session,
666
+ newNumber,
667
+ challenge: result.challenge
668
+ };
669
+ }
670
+ /**
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
674
+ */
675
+ async finishChangeNumber(verificationCode, pin) {
676
+ const params = {
677
+ account: this.account,
678
+ code: verificationCode
679
+ };
680
+ if (pin)
681
+ params.pin = pin;
682
+ await this.sendJsonRpcRequest('finishChangeNumber', params);
683
+ }
684
+ /**
685
+ * Enhanced send message with progress callback support.
686
+ * @param recipient - Phone number or group ID
687
+ * @param message - Message text
688
+ * @param options - Send options including progress callback
689
+ * @returns Send response
690
+ */
691
+ async sendMessageWithProgress(recipient, message, options = {}) {
692
+ // For now, this is the same as sendMessage since signal-cli doesn't provide
693
+ // native progress callbacks. This is a placeholder for future enhancement.
694
+ const { onProgress, ...sendOptions } = options;
695
+ // Simulate progress for large attachments
696
+ if (onProgress && sendOptions.attachments && sendOptions.attachments.length > 0) {
697
+ // Simulate upload progress
698
+ for (let i = 0; i <= 100; i += 10) {
699
+ onProgress({
700
+ total: 100,
701
+ uploaded: i,
702
+ percentage: i
703
+ });
704
+ // Small delay to simulate upload
705
+ await new Promise(resolve => setTimeout(resolve, 50));
706
+ }
707
+ }
708
+ return this.sendMessage(recipient, message, sendOptions);
709
+ }
710
+ }
711
+ exports.SignalCli = SignalCli;