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