signal-sdk 0.0.9 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +184 -61
- package/dist/MultiAccountManager.d.ts +149 -0
- package/dist/MultiAccountManager.js +320 -0
- package/dist/SignalBot.d.ts +1 -0
- package/dist/SignalBot.js +20 -2
- package/dist/SignalCli.d.ts +315 -16
- package/dist/SignalCli.js +880 -26
- package/dist/__tests__/MultiAccountManager.test.d.ts +4 -0
- package/dist/__tests__/MultiAccountManager.test.js +209 -0
- package/dist/__tests__/SignalBot.additional.test.d.ts +5 -0
- package/dist/__tests__/SignalBot.additional.test.js +353 -0
- package/dist/__tests__/SignalBot.test.js +5 -0
- package/dist/__tests__/SignalCli.advanced.test.d.ts +5 -0
- package/dist/__tests__/SignalCli.advanced.test.js +295 -0
- package/dist/__tests__/SignalCli.e2e.test.d.ts +5 -0
- package/dist/__tests__/SignalCli.e2e.test.js +240 -0
- package/dist/__tests__/SignalCli.integration.test.d.ts +5 -0
- package/dist/__tests__/SignalCli.integration.test.js +225 -0
- package/dist/__tests__/SignalCli.methods.test.d.ts +5 -0
- package/dist/__tests__/SignalCli.methods.test.js +556 -0
- package/dist/__tests__/SignalCli.parsing.test.d.ts +5 -0
- package/dist/__tests__/SignalCli.parsing.test.js +258 -0
- package/dist/__tests__/SignalCli.test.js +249 -13
- package/dist/__tests__/config.test.d.ts +5 -0
- package/dist/__tests__/config.test.js +252 -0
- package/dist/__tests__/errors.test.d.ts +5 -0
- package/dist/__tests__/errors.test.js +276 -0
- package/dist/__tests__/retry.test.d.ts +4 -0
- package/dist/__tests__/retry.test.js +123 -0
- package/dist/__tests__/validators.test.d.ts +4 -0
- package/dist/__tests__/validators.test.js +147 -0
- package/dist/config.d.ts +82 -0
- package/dist/config.js +116 -0
- package/dist/errors.d.ts +32 -0
- package/dist/errors.js +75 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +7 -1
- package/dist/interfaces.d.ts +200 -10
- package/dist/interfaces.js +1 -1
- package/dist/retry.d.ts +56 -0
- package/dist/retry.js +152 -0
- package/dist/validators.d.ts +59 -0
- package/dist/validators.js +170 -0
- package/package.json +1 -1
package/dist/SignalCli.js
CHANGED
|
@@ -39,11 +39,27 @@ const uuid_1 = require("uuid");
|
|
|
39
39
|
const qrcodeTerminal = __importStar(require("qrcode-terminal"));
|
|
40
40
|
const events_1 = require("events");
|
|
41
41
|
const path = __importStar(require("path"));
|
|
42
|
+
const validators_1 = require("./validators");
|
|
43
|
+
const retry_1 = require("./retry");
|
|
44
|
+
const config_1 = require("./config");
|
|
45
|
+
const errors_1 = require("./errors");
|
|
42
46
|
class SignalCli extends events_1.EventEmitter {
|
|
43
|
-
constructor(accountOrPath, account) {
|
|
47
|
+
constructor(accountOrPath, account, config = {}) {
|
|
44
48
|
super();
|
|
45
49
|
this.cliProcess = null;
|
|
46
50
|
this.requestPromises = new Map();
|
|
51
|
+
this.reconnectAttempts = 0;
|
|
52
|
+
this.maxReconnectAttempts = 5;
|
|
53
|
+
// Validate and merge configuration
|
|
54
|
+
this.config = (0, config_1.validateConfig)(config);
|
|
55
|
+
// Initialize logger
|
|
56
|
+
this.logger = new config_1.Logger({
|
|
57
|
+
level: this.config.verbose ? 'debug' : 'info',
|
|
58
|
+
enableFile: !!this.config.logFile,
|
|
59
|
+
filePath: this.config.logFile
|
|
60
|
+
});
|
|
61
|
+
// Initialize rate limiter
|
|
62
|
+
this.rateLimiter = new retry_1.RateLimiter(this.config.maxConcurrentRequests, this.config.minRequestInterval);
|
|
47
63
|
let signalCliPath;
|
|
48
64
|
let phoneNumber;
|
|
49
65
|
// Smart parameter detection
|
|
@@ -72,6 +88,25 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
72
88
|
this.account = phoneNumber;
|
|
73
89
|
}
|
|
74
90
|
async connect() {
|
|
91
|
+
const daemonMode = this.config.daemonMode || 'json-rpc';
|
|
92
|
+
switch (daemonMode) {
|
|
93
|
+
case 'json-rpc':
|
|
94
|
+
await this.connectJsonRpc();
|
|
95
|
+
break;
|
|
96
|
+
case 'unix-socket':
|
|
97
|
+
await this.connectUnixSocket();
|
|
98
|
+
break;
|
|
99
|
+
case 'tcp':
|
|
100
|
+
await this.connectTcp();
|
|
101
|
+
break;
|
|
102
|
+
case 'http':
|
|
103
|
+
await this.connectHttp();
|
|
104
|
+
break;
|
|
105
|
+
default:
|
|
106
|
+
throw new Error(`Invalid daemon mode: ${daemonMode}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async connectJsonRpc() {
|
|
75
110
|
const args = this.account ? ['-a', this.account, 'jsonRpc'] : ['jsonRpc'];
|
|
76
111
|
if (process.platform === 'win32') {
|
|
77
112
|
// On Windows, use cmd.exe to run the batch file with proper quoting for paths with spaces
|
|
@@ -117,11 +152,120 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
117
152
|
});
|
|
118
153
|
});
|
|
119
154
|
}
|
|
155
|
+
async connectUnixSocket() {
|
|
156
|
+
const net = await Promise.resolve().then(() => __importStar(require('net')));
|
|
157
|
+
const socketPath = this.config.socketPath || '/tmp/signal-cli.sock';
|
|
158
|
+
return new Promise((resolve, reject) => {
|
|
159
|
+
const socket = net.createConnection(socketPath);
|
|
160
|
+
socket.on('connect', () => {
|
|
161
|
+
this.logger.debug('Connected to Unix socket:', socketPath);
|
|
162
|
+
socket.on('data', (data) => this.handleRpcResponse(data.toString('utf8')));
|
|
163
|
+
socket.on('error', (err) => this.emit('error', err));
|
|
164
|
+
socket.on('close', () => {
|
|
165
|
+
this.emit('close', 0);
|
|
166
|
+
});
|
|
167
|
+
this.socket = socket;
|
|
168
|
+
resolve();
|
|
169
|
+
});
|
|
170
|
+
socket.on('error', (err) => {
|
|
171
|
+
reject(new errors_1.ConnectionError(`Failed to connect to Unix socket: ${err.message}`));
|
|
172
|
+
});
|
|
173
|
+
setTimeout(() => reject(new errors_1.ConnectionError('Unix socket connection timeout')), this.config.connectionTimeout);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
async connectTcp() {
|
|
177
|
+
const net = await Promise.resolve().then(() => __importStar(require('net')));
|
|
178
|
+
const host = this.config.tcpHost || 'localhost';
|
|
179
|
+
const port = this.config.tcpPort || 7583;
|
|
180
|
+
return new Promise((resolve, reject) => {
|
|
181
|
+
const socket = net.createConnection(port, host);
|
|
182
|
+
socket.on('connect', () => {
|
|
183
|
+
this.logger.debug(`Connected to TCP: ${host}:${port}`);
|
|
184
|
+
socket.on('data', (data) => this.handleRpcResponse(data.toString('utf8')));
|
|
185
|
+
socket.on('error', (err) => this.emit('error', err));
|
|
186
|
+
socket.on('close', () => {
|
|
187
|
+
this.emit('close', 0);
|
|
188
|
+
});
|
|
189
|
+
this.socket = socket;
|
|
190
|
+
resolve();
|
|
191
|
+
});
|
|
192
|
+
socket.on('error', (err) => {
|
|
193
|
+
reject(new errors_1.ConnectionError(`Failed to connect to TCP: ${err.message}`));
|
|
194
|
+
});
|
|
195
|
+
setTimeout(() => reject(new errors_1.ConnectionError('TCP connection timeout')), this.config.connectionTimeout);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
async connectHttp() {
|
|
199
|
+
const baseUrl = this.config.httpBaseUrl || 'http://localhost:8080';
|
|
200
|
+
// For HTTP mode, we don't maintain a persistent connection
|
|
201
|
+
// Instead, we'll use the httpRequest method for each operation
|
|
202
|
+
this.logger.debug('HTTP mode configured:', baseUrl);
|
|
203
|
+
this.httpBaseUrl = baseUrl;
|
|
204
|
+
// Test connection by sending a simple request
|
|
205
|
+
try {
|
|
206
|
+
await this.httpRequest({ jsonrpc: '2.0', method: 'version', params: {}, id: (0, uuid_1.v4)() });
|
|
207
|
+
this.logger.debug('HTTP connection verified');
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
throw new errors_1.ConnectionError(`Failed to connect to HTTP endpoint: ${error instanceof Error ? error.message : String(error)}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async httpRequest(request) {
|
|
214
|
+
const https = await Promise.resolve(`${this.config.httpBaseUrl?.startsWith('https:') ? 'https' : 'http'}`).then(s => __importStar(require(s)));
|
|
215
|
+
const baseUrl = this.config.httpBaseUrl || 'http://localhost:8080';
|
|
216
|
+
const url = new URL('/api/v1/rpc', baseUrl);
|
|
217
|
+
return new Promise((resolve, reject) => {
|
|
218
|
+
const data = JSON.stringify(request);
|
|
219
|
+
const options = {
|
|
220
|
+
method: 'POST',
|
|
221
|
+
headers: {
|
|
222
|
+
'Content-Type': 'application/json',
|
|
223
|
+
'Content-Length': Buffer.byteLength(data)
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
const req = https.request(url, options, (res) => {
|
|
227
|
+
let body = '';
|
|
228
|
+
res.on('data', (chunk) => body += chunk);
|
|
229
|
+
res.on('end', () => {
|
|
230
|
+
try {
|
|
231
|
+
const response = JSON.parse(body);
|
|
232
|
+
if (response.error) {
|
|
233
|
+
reject(new Error(`[${response.error.code}] ${response.error.message}`));
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
resolve(response.result);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
reject(new Error(`Failed to parse HTTP response: ${body}`));
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
req.on('error', (err) => reject(new errors_1.ConnectionError(`HTTP request failed: ${err.message}`)));
|
|
245
|
+
req.setTimeout(this.config.requestTimeout, () => {
|
|
246
|
+
req.destroy();
|
|
247
|
+
reject(new errors_1.ConnectionError('HTTP request timeout'));
|
|
248
|
+
});
|
|
249
|
+
req.write(data);
|
|
250
|
+
req.end();
|
|
251
|
+
});
|
|
252
|
+
}
|
|
120
253
|
disconnect() {
|
|
121
|
-
|
|
254
|
+
const daemonMode = this.config.daemonMode || 'json-rpc';
|
|
255
|
+
// Close socket connections
|
|
256
|
+
if (daemonMode === 'unix-socket' || daemonMode === 'tcp') {
|
|
257
|
+
const socket = this.socket;
|
|
258
|
+
if (socket && !socket.destroyed) {
|
|
259
|
+
socket.destroy();
|
|
260
|
+
this.socket = null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Close process for json-rpc mode
|
|
264
|
+
if (daemonMode === 'json-rpc' && this.cliProcess) {
|
|
122
265
|
this.cliProcess.kill();
|
|
123
266
|
this.cliProcess = null;
|
|
124
267
|
}
|
|
268
|
+
// For HTTP mode, nothing to disconnect (stateless)
|
|
125
269
|
}
|
|
126
270
|
async gracefulShutdown() {
|
|
127
271
|
return new Promise((resolve) => {
|
|
@@ -137,13 +281,17 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
137
281
|
// Send SIGTERM for graceful shutdown
|
|
138
282
|
this.cliProcess.kill('SIGTERM');
|
|
139
283
|
// Force kill after 5 seconds if it doesn't close gracefully
|
|
140
|
-
setTimeout(() => {
|
|
284
|
+
const forceKillTimer = setTimeout(() => {
|
|
141
285
|
if (this.cliProcess) {
|
|
142
286
|
this.cliProcess.kill('SIGKILL');
|
|
143
287
|
this.cliProcess = null;
|
|
144
288
|
resolve();
|
|
145
289
|
}
|
|
146
290
|
}, 5000);
|
|
291
|
+
// Use unref() to prevent this timer from keeping the process alive
|
|
292
|
+
if (forceKillTimer.unref) {
|
|
293
|
+
forceKillTimer.unref();
|
|
294
|
+
}
|
|
147
295
|
});
|
|
148
296
|
}
|
|
149
297
|
handleRpcResponse(data) {
|
|
@@ -218,8 +366,40 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
218
366
|
}
|
|
219
367
|
}
|
|
220
368
|
async sendJsonRpcRequest(method, params) {
|
|
369
|
+
const daemonMode = this.config.daemonMode || 'json-rpc';
|
|
370
|
+
// For HTTP mode, use HTTP requests
|
|
371
|
+
if (daemonMode === 'http') {
|
|
372
|
+
const id = (0, uuid_1.v4)();
|
|
373
|
+
const request = {
|
|
374
|
+
jsonrpc: '2.0',
|
|
375
|
+
method,
|
|
376
|
+
params,
|
|
377
|
+
id,
|
|
378
|
+
};
|
|
379
|
+
return await this.httpRequest(request);
|
|
380
|
+
}
|
|
381
|
+
// For socket modes (Unix socket, TCP), write to socket
|
|
382
|
+
if (daemonMode === 'unix-socket' || daemonMode === 'tcp') {
|
|
383
|
+
const socket = this.socket;
|
|
384
|
+
if (!socket || socket.destroyed) {
|
|
385
|
+
throw new errors_1.ConnectionError('Not connected. Call connect() first.');
|
|
386
|
+
}
|
|
387
|
+
const id = (0, uuid_1.v4)();
|
|
388
|
+
const request = {
|
|
389
|
+
jsonrpc: '2.0',
|
|
390
|
+
method,
|
|
391
|
+
params,
|
|
392
|
+
id,
|
|
393
|
+
};
|
|
394
|
+
const promise = new Promise((resolve, reject) => {
|
|
395
|
+
this.requestPromises.set(id, { resolve, reject });
|
|
396
|
+
});
|
|
397
|
+
socket.write(JSON.stringify(request) + '\n');
|
|
398
|
+
return promise;
|
|
399
|
+
}
|
|
400
|
+
// Default JSON-RPC mode with stdin/stdout
|
|
221
401
|
if (!this.cliProcess || !this.cliProcess.stdin) {
|
|
222
|
-
throw new
|
|
402
|
+
throw new errors_1.ConnectionError('Not connected. Call connect() first.');
|
|
223
403
|
}
|
|
224
404
|
const id = (0, uuid_1.v4)();
|
|
225
405
|
const request = {
|
|
@@ -256,7 +436,7 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
256
436
|
else {
|
|
257
437
|
params.recipients = [recipient];
|
|
258
438
|
}
|
|
259
|
-
//
|
|
439
|
+
// Add well-known options
|
|
260
440
|
if (options.attachments && options.attachments.length > 0) {
|
|
261
441
|
params.attachments = options.attachments;
|
|
262
442
|
}
|
|
@@ -264,7 +444,64 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
264
444
|
params.expiresInSeconds = options.expiresInSeconds;
|
|
265
445
|
}
|
|
266
446
|
if (options.isViewOnce) {
|
|
267
|
-
params.
|
|
447
|
+
params.viewOnce = options.isViewOnce;
|
|
448
|
+
}
|
|
449
|
+
// Add advanced text formatting options
|
|
450
|
+
if (options.mentions && options.mentions.length > 0) {
|
|
451
|
+
params.mentions = options.mentions.map(m => ({
|
|
452
|
+
start: m.start,
|
|
453
|
+
length: m.length,
|
|
454
|
+
number: m.recipient || m.number
|
|
455
|
+
}));
|
|
456
|
+
}
|
|
457
|
+
if (options.textStyles && options.textStyles.length > 0) {
|
|
458
|
+
params.textStyles = options.textStyles.map(ts => ({
|
|
459
|
+
start: ts.start,
|
|
460
|
+
length: ts.length,
|
|
461
|
+
style: ts.style
|
|
462
|
+
}));
|
|
463
|
+
}
|
|
464
|
+
// Add quote/reply information
|
|
465
|
+
if (options.quote) {
|
|
466
|
+
params.quoteTimestamp = options.quote.timestamp;
|
|
467
|
+
params.quoteAuthor = options.quote.author;
|
|
468
|
+
if (options.quote.text) {
|
|
469
|
+
params.quoteMessage = options.quote.text;
|
|
470
|
+
}
|
|
471
|
+
if (options.quote.mentions && options.quote.mentions.length > 0) {
|
|
472
|
+
params.quoteMentions = options.quote.mentions.map(m => ({
|
|
473
|
+
start: m.start,
|
|
474
|
+
length: m.length,
|
|
475
|
+
number: m.recipient || m.number
|
|
476
|
+
}));
|
|
477
|
+
}
|
|
478
|
+
if (options.quote.textStyles && options.quote.textStyles.length > 0) {
|
|
479
|
+
params.quoteTextStyles = options.quote.textStyles.map(ts => ({
|
|
480
|
+
start: ts.start,
|
|
481
|
+
length: ts.length,
|
|
482
|
+
style: ts.style
|
|
483
|
+
}));
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// Add preview URL
|
|
487
|
+
if (options.previewUrl) {
|
|
488
|
+
params.previewUrl = options.previewUrl;
|
|
489
|
+
}
|
|
490
|
+
// Add edit timestamp for editing existing messages
|
|
491
|
+
if (options.editTimestamp) {
|
|
492
|
+
params.editTimestamp = options.editTimestamp;
|
|
493
|
+
}
|
|
494
|
+
// Add story reply information
|
|
495
|
+
if (options.storyTimestamp && options.storyAuthor) {
|
|
496
|
+
params.storyTimestamp = options.storyTimestamp;
|
|
497
|
+
params.storyAuthor = options.storyAuthor;
|
|
498
|
+
}
|
|
499
|
+
// Add special flags
|
|
500
|
+
if (options.noteToSelf) {
|
|
501
|
+
params.noteToSelf = options.noteToSelf;
|
|
502
|
+
}
|
|
503
|
+
if (options.endSession) {
|
|
504
|
+
params.endSession = options.endSession;
|
|
268
505
|
}
|
|
269
506
|
return this.sendJsonRpcRequest('send', params);
|
|
270
507
|
}
|
|
@@ -355,6 +592,79 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
355
592
|
async trustIdentity(number, safetyNumber, verified = true) {
|
|
356
593
|
await this.sendJsonRpcRequest('trust', { account: this.account, recipient: number, safetyNumber, verified });
|
|
357
594
|
}
|
|
595
|
+
/**
|
|
596
|
+
* Get the safety number for a specific contact.
|
|
597
|
+
* This is a helper method that extracts just the safety number from identity information.
|
|
598
|
+
*
|
|
599
|
+
* @param number - The phone number of the contact
|
|
600
|
+
* @returns The safety number string, or null if not found
|
|
601
|
+
*
|
|
602
|
+
* @example
|
|
603
|
+
* ```typescript
|
|
604
|
+
* const safetyNumber = await signal.getSafetyNumber('+33123456789');
|
|
605
|
+
* console.log(`Safety number: ${safetyNumber}`);
|
|
606
|
+
* ```
|
|
607
|
+
*/
|
|
608
|
+
async getSafetyNumber(number) {
|
|
609
|
+
const identities = await this.listIdentities(number);
|
|
610
|
+
if (identities.length > 0 && identities[0].safetyNumber) {
|
|
611
|
+
return identities[0].safetyNumber;
|
|
612
|
+
}
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Verify a safety number for a contact.
|
|
617
|
+
* Checks if the provided safety number matches the stored one and marks it as trusted if it does.
|
|
618
|
+
*
|
|
619
|
+
* @param number - The phone number of the contact
|
|
620
|
+
* @param safetyNumber - The safety number to verify
|
|
621
|
+
* @returns True if the safety number matches and was trusted, false otherwise
|
|
622
|
+
*
|
|
623
|
+
* @example
|
|
624
|
+
* ```typescript
|
|
625
|
+
* const verified = await signal.verifySafetyNumber('+33123456789', '123456 78901...');
|
|
626
|
+
* if (verified) {
|
|
627
|
+
* console.log('Safety number verified and trusted');
|
|
628
|
+
* } else {
|
|
629
|
+
* console.log('Safety number does not match!');
|
|
630
|
+
* }
|
|
631
|
+
* ```
|
|
632
|
+
*/
|
|
633
|
+
async verifySafetyNumber(number, safetyNumber) {
|
|
634
|
+
const storedSafetyNumber = await this.getSafetyNumber(number);
|
|
635
|
+
if (!storedSafetyNumber) {
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
// Compare safety numbers (remove spaces for comparison)
|
|
639
|
+
const normalizedStored = storedSafetyNumber.replace(/\s/g, '');
|
|
640
|
+
const normalizedProvided = safetyNumber.replace(/\s/g, '');
|
|
641
|
+
if (normalizedStored === normalizedProvided) {
|
|
642
|
+
await this.trustIdentity(number, safetyNumber, true);
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* List all untrusted identities.
|
|
649
|
+
* Returns identities that have not been explicitly trusted.
|
|
650
|
+
*
|
|
651
|
+
* @returns Array of untrusted identity keys
|
|
652
|
+
*
|
|
653
|
+
* @example
|
|
654
|
+
* ```typescript
|
|
655
|
+
* const untrusted = await signal.listUntrustedIdentities();
|
|
656
|
+
* console.log(`Found ${untrusted.length} untrusted identities`);
|
|
657
|
+
* untrusted.forEach(id => {
|
|
658
|
+
* console.log(`${id.number}: ${id.safetyNumber}`);
|
|
659
|
+
* });
|
|
660
|
+
* ```
|
|
661
|
+
*/
|
|
662
|
+
async listUntrustedIdentities() {
|
|
663
|
+
const allIdentities = await this.listIdentities();
|
|
664
|
+
return allIdentities.filter(identity => identity.trustLevel === 'UNTRUSTED' ||
|
|
665
|
+
identity.trustLevel === 'TRUST_ON_FIRST_USE' ||
|
|
666
|
+
!identity.trustLevel);
|
|
667
|
+
}
|
|
358
668
|
async link(deviceName) {
|
|
359
669
|
const result = await this.sendJsonRpcRequest('link', { deviceName });
|
|
360
670
|
return result.uri;
|
|
@@ -489,6 +799,10 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
489
799
|
params.promoteAdmins = options.promoteAdmins;
|
|
490
800
|
if (options.demoteAdmins)
|
|
491
801
|
params.demoteAdmins = options.demoteAdmins;
|
|
802
|
+
if (options.banMembers)
|
|
803
|
+
params.banMembers = options.banMembers;
|
|
804
|
+
if (options.unbanMembers)
|
|
805
|
+
params.unbanMembers = options.unbanMembers;
|
|
492
806
|
if (options.resetInviteLink)
|
|
493
807
|
params.resetLink = true;
|
|
494
808
|
if (options.permissionAddMember)
|
|
@@ -504,6 +818,58 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
504
818
|
async listGroups() {
|
|
505
819
|
return this.sendJsonRpcRequest('listGroups', { account: this.account });
|
|
506
820
|
}
|
|
821
|
+
/**
|
|
822
|
+
* Send group invite link to a recipient.
|
|
823
|
+
* Retrieves and sends the invitation link for a group.
|
|
824
|
+
*
|
|
825
|
+
* @param groupId - The group ID
|
|
826
|
+
* @param recipient - The recipient to send the invite link to
|
|
827
|
+
* @returns Send response
|
|
828
|
+
*
|
|
829
|
+
* @example
|
|
830
|
+
* ```typescript
|
|
831
|
+
* await signal.sendGroupInviteLink('groupId123==', '+33123456789');
|
|
832
|
+
* ```
|
|
833
|
+
*/
|
|
834
|
+
async sendGroupInviteLink(groupId, recipient) {
|
|
835
|
+
// Get group info to retrieve invite link
|
|
836
|
+
const groups = await this.listGroups();
|
|
837
|
+
const group = groups.find(g => g.groupId === groupId);
|
|
838
|
+
const inviteLink = group?.groupInviteLink || group?.inviteLink;
|
|
839
|
+
if (!group || !inviteLink) {
|
|
840
|
+
throw new Error('Group not found or does not have an invite link');
|
|
841
|
+
}
|
|
842
|
+
return this.sendMessage(recipient, `Join our group: ${inviteLink}`);
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Set banned members for a group.
|
|
846
|
+
* Ban specific members from the group.
|
|
847
|
+
*
|
|
848
|
+
* @param groupId - The group ID
|
|
849
|
+
* @param members - Array of phone numbers to ban
|
|
850
|
+
*
|
|
851
|
+
* @example
|
|
852
|
+
* ```typescript
|
|
853
|
+
* await signal.setBannedMembers('groupId123==', ['+33111111111', '+33222222222']);
|
|
854
|
+
* ```
|
|
855
|
+
*/
|
|
856
|
+
async setBannedMembers(groupId, members) {
|
|
857
|
+
await this.updateGroup(groupId, { banMembers: members });
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Reset group invite link.
|
|
861
|
+
* Invalidates the current group invite link and generates a new one.
|
|
862
|
+
*
|
|
863
|
+
* @param groupId - The group ID
|
|
864
|
+
*
|
|
865
|
+
* @example
|
|
866
|
+
* ```typescript
|
|
867
|
+
* await signal.resetGroupLink('groupId123==');
|
|
868
|
+
* ```
|
|
869
|
+
*/
|
|
870
|
+
async resetGroupLink(groupId) {
|
|
871
|
+
await this.updateGroup(groupId, { resetInviteLink: true });
|
|
872
|
+
}
|
|
507
873
|
async listContacts() {
|
|
508
874
|
return this.sendJsonRpcRequest('listContacts', { account: this.account });
|
|
509
875
|
}
|
|
@@ -543,6 +909,101 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
543
909
|
console.warn("stopDaemon is deprecated. Use gracefulShutdown() or disconnect() instead.");
|
|
544
910
|
this.gracefulShutdown();
|
|
545
911
|
}
|
|
912
|
+
// ############# MESSAGE RECEIVING #############
|
|
913
|
+
/**
|
|
914
|
+
* Receive messages from Signal with configurable options.
|
|
915
|
+
* This is the modern replacement for the deprecated receiveMessages().
|
|
916
|
+
*
|
|
917
|
+
* @param options - Options for receiving messages
|
|
918
|
+
* @returns Array of received messages
|
|
919
|
+
*
|
|
920
|
+
* @example
|
|
921
|
+
* ```typescript
|
|
922
|
+
* // Receive with default timeout
|
|
923
|
+
* const messages = await signal.receive();
|
|
924
|
+
*
|
|
925
|
+
* // Receive with custom options
|
|
926
|
+
* const messages = await signal.receive({
|
|
927
|
+
* timeout: 10,
|
|
928
|
+
* maxMessages: 5,
|
|
929
|
+
* ignoreAttachments: true,
|
|
930
|
+
* sendReadReceipts: true
|
|
931
|
+
* });
|
|
932
|
+
* ```
|
|
933
|
+
*/
|
|
934
|
+
async receive(options = {}) {
|
|
935
|
+
const params = { account: this.account };
|
|
936
|
+
// Set timeout (default: 5 seconds)
|
|
937
|
+
if (options.timeout !== undefined) {
|
|
938
|
+
params.timeout = options.timeout;
|
|
939
|
+
}
|
|
940
|
+
// Set maximum number of messages
|
|
941
|
+
if (options.maxMessages !== undefined) {
|
|
942
|
+
params.maxMessages = options.maxMessages;
|
|
943
|
+
}
|
|
944
|
+
// Skip attachment downloads
|
|
945
|
+
if (options.ignoreAttachments) {
|
|
946
|
+
params.ignoreAttachments = true;
|
|
947
|
+
}
|
|
948
|
+
// Skip stories
|
|
949
|
+
if (options.ignoreStories) {
|
|
950
|
+
params.ignoreStories = true;
|
|
951
|
+
}
|
|
952
|
+
// Send read receipts automatically
|
|
953
|
+
if (options.sendReadReceipts) {
|
|
954
|
+
params.sendReadReceipts = true;
|
|
955
|
+
}
|
|
956
|
+
try {
|
|
957
|
+
const result = await this.sendJsonRpcRequest('receive', params);
|
|
958
|
+
// Parse and return messages
|
|
959
|
+
if (Array.isArray(result)) {
|
|
960
|
+
return result.map(envelope => this.parseEnvelope(envelope));
|
|
961
|
+
}
|
|
962
|
+
return [];
|
|
963
|
+
}
|
|
964
|
+
catch (error) {
|
|
965
|
+
this.logger.error('Failed to receive messages:', error);
|
|
966
|
+
throw error;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Parse a message envelope from signal-cli into a Message object.
|
|
971
|
+
* @private
|
|
972
|
+
*/
|
|
973
|
+
parseEnvelope(envelope) {
|
|
974
|
+
const message = {
|
|
975
|
+
timestamp: envelope.timestamp || Date.now(),
|
|
976
|
+
source: envelope.source || envelope.sourceNumber,
|
|
977
|
+
sourceUuid: envelope.sourceUuid,
|
|
978
|
+
sourceDevice: envelope.sourceDevice,
|
|
979
|
+
};
|
|
980
|
+
// Parse data message
|
|
981
|
+
if (envelope.dataMessage) {
|
|
982
|
+
const data = envelope.dataMessage;
|
|
983
|
+
message.text = data.message || data.body;
|
|
984
|
+
message.groupId = data.groupInfo?.groupId;
|
|
985
|
+
message.attachments = data.attachments;
|
|
986
|
+
message.mentions = data.mentions;
|
|
987
|
+
message.quote = data.quote;
|
|
988
|
+
message.reaction = data.reaction;
|
|
989
|
+
message.sticker = data.sticker;
|
|
990
|
+
message.expiresInSeconds = data.expiresInSeconds;
|
|
991
|
+
message.viewOnce = data.viewOnce;
|
|
992
|
+
}
|
|
993
|
+
// Parse sync message
|
|
994
|
+
if (envelope.syncMessage) {
|
|
995
|
+
message.syncMessage = envelope.syncMessage;
|
|
996
|
+
}
|
|
997
|
+
// Parse receipt message
|
|
998
|
+
if (envelope.receiptMessage) {
|
|
999
|
+
message.receipt = envelope.receiptMessage;
|
|
1000
|
+
}
|
|
1001
|
+
// Parse typing message
|
|
1002
|
+
if (envelope.typingMessage) {
|
|
1003
|
+
message.typing = envelope.typingMessage;
|
|
1004
|
+
}
|
|
1005
|
+
return message;
|
|
1006
|
+
}
|
|
546
1007
|
// ############# NEW FEATURES - Missing signal-cli Commands #############
|
|
547
1008
|
/**
|
|
548
1009
|
* Remove a contact from the contact list.
|
|
@@ -588,12 +1049,29 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
588
1049
|
return statusResults;
|
|
589
1050
|
}
|
|
590
1051
|
/**
|
|
591
|
-
* Send a payment notification to a recipient.
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
* @
|
|
1052
|
+
* Send a payment notification to a recipient (MobileCoin).
|
|
1053
|
+
* Sends a notification about a cryptocurrency payment made through Signal's MobileCoin integration.
|
|
1054
|
+
*
|
|
1055
|
+
* @param recipient - The phone number or group ID of the recipient
|
|
1056
|
+
* @param paymentData - Payment notification data including receipt and optional note
|
|
1057
|
+
* @returns Send result with timestamp
|
|
1058
|
+
* @throws {Error} If receipt is invalid or sending fails
|
|
1059
|
+
*
|
|
1060
|
+
* @example
|
|
1061
|
+
* ```typescript
|
|
1062
|
+
* const receiptBlob = 'base64EncodedReceiptData...';
|
|
1063
|
+
* await signal.sendPaymentNotification('+33612345678', {
|
|
1064
|
+
* receipt: receiptBlob,
|
|
1065
|
+
* note: 'Thanks for dinner!'
|
|
1066
|
+
* });
|
|
1067
|
+
* ```
|
|
595
1068
|
*/
|
|
596
1069
|
async sendPaymentNotification(recipient, paymentData) {
|
|
1070
|
+
this.logger.info(`Sending payment notification to ${recipient}`);
|
|
1071
|
+
(0, validators_1.validateRecipient)(recipient);
|
|
1072
|
+
if (!paymentData.receipt || paymentData.receipt.trim().length === 0) {
|
|
1073
|
+
throw new Error('Payment receipt is required');
|
|
1074
|
+
}
|
|
597
1075
|
const params = {
|
|
598
1076
|
receipt: paymentData.receipt,
|
|
599
1077
|
account: this.account
|
|
@@ -646,13 +1124,18 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
646
1124
|
};
|
|
647
1125
|
}
|
|
648
1126
|
/**
|
|
649
|
-
* Start the
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
653
|
-
* @
|
|
1127
|
+
* Start the phone number change process.
|
|
1128
|
+
* Initiates SMS or voice verification for changing your account to a new phone number.
|
|
1129
|
+
* After calling this, you must verify the new number with finishChangeNumber().
|
|
1130
|
+
*
|
|
1131
|
+
* @param newNumber - The new phone number in E164 format (e.g., "+33612345678")
|
|
1132
|
+
* @param voice - Use voice verification instead of SMS (default: false)
|
|
1133
|
+
* @param captcha - Optional captcha token if required
|
|
1134
|
+
* @throws {Error} If not a primary device or rate limited
|
|
654
1135
|
*/
|
|
655
1136
|
async startChangeNumber(newNumber, voice = false, captcha) {
|
|
1137
|
+
this.logger.info(`Starting change number to ${newNumber} (voice: ${voice})`);
|
|
1138
|
+
(0, validators_1.validatePhoneNumber)(newNumber);
|
|
656
1139
|
const params = {
|
|
657
1140
|
account: this.account,
|
|
658
1141
|
number: newNumber,
|
|
@@ -660,22 +1143,28 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
660
1143
|
};
|
|
661
1144
|
if (captcha)
|
|
662
1145
|
params.captcha = captcha;
|
|
663
|
-
|
|
664
|
-
return {
|
|
665
|
-
session: result.session,
|
|
666
|
-
newNumber,
|
|
667
|
-
challenge: result.challenge
|
|
668
|
-
};
|
|
1146
|
+
await this.sendJsonRpcRequest('startChangeNumber', params);
|
|
669
1147
|
}
|
|
670
1148
|
/**
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
1149
|
+
* Complete the phone number change process.
|
|
1150
|
+
* Verifies the code received via SMS or voice and changes your account to the new number.
|
|
1151
|
+
* Must be called after startChangeNumber().
|
|
1152
|
+
*
|
|
1153
|
+
* @param newNumber - The new phone number (same as startChangeNumber)
|
|
1154
|
+
* @param verificationCode - The verification code received via SMS or voice
|
|
1155
|
+
* @param pin - Optional registration lock PIN if one was set
|
|
1156
|
+
* @throws {Error} If verification fails or incorrect PIN
|
|
674
1157
|
*/
|
|
675
|
-
async finishChangeNumber(verificationCode, pin) {
|
|
1158
|
+
async finishChangeNumber(newNumber, verificationCode, pin) {
|
|
1159
|
+
this.logger.info(`Finishing change number to ${newNumber}`);
|
|
1160
|
+
(0, validators_1.validatePhoneNumber)(newNumber);
|
|
1161
|
+
if (!verificationCode || verificationCode.trim().length === 0) {
|
|
1162
|
+
throw new Error('Verification code is required');
|
|
1163
|
+
}
|
|
676
1164
|
const params = {
|
|
677
1165
|
account: this.account,
|
|
678
|
-
|
|
1166
|
+
number: newNumber,
|
|
1167
|
+
verificationCode
|
|
679
1168
|
};
|
|
680
1169
|
if (pin)
|
|
681
1170
|
params.pin = pin;
|
|
@@ -707,5 +1196,370 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
707
1196
|
}
|
|
708
1197
|
return this.sendMessage(recipient, message, sendOptions);
|
|
709
1198
|
}
|
|
1199
|
+
// ========== NEW METHODS FOR 100% signal-cli COMPATIBILITY ==========
|
|
1200
|
+
/**
|
|
1201
|
+
* Send a poll create message to a recipient or group.
|
|
1202
|
+
* @param options Poll creation options
|
|
1203
|
+
* @returns Send response with timestamp
|
|
1204
|
+
*/
|
|
1205
|
+
async sendPollCreate(options) {
|
|
1206
|
+
this.logger.debug('Sending poll create', options);
|
|
1207
|
+
(0, validators_1.validateMessage)(options.question, 500);
|
|
1208
|
+
if (!options.options || options.options.length < 2) {
|
|
1209
|
+
throw new errors_1.MessageError('Poll must have at least 2 options');
|
|
1210
|
+
}
|
|
1211
|
+
if (options.options.length > 10) {
|
|
1212
|
+
throw new errors_1.MessageError('Poll cannot have more than 10 options');
|
|
1213
|
+
}
|
|
1214
|
+
const params = {
|
|
1215
|
+
question: options.question,
|
|
1216
|
+
options: options.options,
|
|
1217
|
+
account: this.account
|
|
1218
|
+
};
|
|
1219
|
+
if (options.multiSelect !== undefined) {
|
|
1220
|
+
params.multiSelect = options.multiSelect;
|
|
1221
|
+
}
|
|
1222
|
+
if (options.groupId) {
|
|
1223
|
+
(0, validators_1.validateGroupId)(options.groupId);
|
|
1224
|
+
params.groupId = options.groupId;
|
|
1225
|
+
}
|
|
1226
|
+
else if (options.recipients) {
|
|
1227
|
+
params.recipients = options.recipients.map(r => {
|
|
1228
|
+
(0, validators_1.validateRecipient)(r);
|
|
1229
|
+
return r;
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
else {
|
|
1233
|
+
throw new errors_1.MessageError('Must specify either recipients or groupId');
|
|
1234
|
+
}
|
|
1235
|
+
return this.sendJsonRpcRequest('sendPollCreate', params);
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Send a poll vote message to vote on a poll.
|
|
1239
|
+
* @param recipient Recipient or group ID
|
|
1240
|
+
* @param options Poll vote options
|
|
1241
|
+
* @returns Send response with timestamp
|
|
1242
|
+
*/
|
|
1243
|
+
async sendPollVote(recipient, options) {
|
|
1244
|
+
this.logger.debug('Sending poll vote', { recipient, options });
|
|
1245
|
+
(0, validators_1.validateRecipient)(options.pollAuthor);
|
|
1246
|
+
(0, validators_1.validateTimestamp)(options.pollTimestamp);
|
|
1247
|
+
if (!options.optionIndexes || options.optionIndexes.length === 0) {
|
|
1248
|
+
throw new errors_1.MessageError('Must specify at least one option to vote for');
|
|
1249
|
+
}
|
|
1250
|
+
const params = {
|
|
1251
|
+
pollAuthor: options.pollAuthor,
|
|
1252
|
+
pollTimestamp: options.pollTimestamp,
|
|
1253
|
+
options: options.optionIndexes,
|
|
1254
|
+
account: this.account
|
|
1255
|
+
};
|
|
1256
|
+
if (options.voteCount !== undefined) {
|
|
1257
|
+
params.voteCount = options.voteCount;
|
|
1258
|
+
}
|
|
1259
|
+
if (this.isGroupId(recipient)) {
|
|
1260
|
+
(0, validators_1.validateGroupId)(recipient);
|
|
1261
|
+
params.groupId = recipient;
|
|
1262
|
+
}
|
|
1263
|
+
else {
|
|
1264
|
+
(0, validators_1.validateRecipient)(recipient);
|
|
1265
|
+
params.recipient = recipient;
|
|
1266
|
+
}
|
|
1267
|
+
return this.sendJsonRpcRequest('sendPollVote', params);
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Send a poll terminate message to close a poll.
|
|
1271
|
+
* @param recipient Recipient or group ID
|
|
1272
|
+
* @param options Poll terminate options
|
|
1273
|
+
* @returns Send response with timestamp
|
|
1274
|
+
*/
|
|
1275
|
+
async sendPollTerminate(recipient, options) {
|
|
1276
|
+
this.logger.debug('Sending poll terminate', { recipient, options });
|
|
1277
|
+
(0, validators_1.validateTimestamp)(options.pollTimestamp);
|
|
1278
|
+
const params = {
|
|
1279
|
+
pollTimestamp: options.pollTimestamp,
|
|
1280
|
+
account: this.account
|
|
1281
|
+
};
|
|
1282
|
+
if (this.isGroupId(recipient)) {
|
|
1283
|
+
(0, validators_1.validateGroupId)(recipient);
|
|
1284
|
+
params.groupId = recipient;
|
|
1285
|
+
}
|
|
1286
|
+
else {
|
|
1287
|
+
(0, validators_1.validateRecipient)(recipient);
|
|
1288
|
+
params.recipient = recipient;
|
|
1289
|
+
}
|
|
1290
|
+
return this.sendJsonRpcRequest('sendPollTerminate', params);
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Update account information (device name, username, privacy settings).
|
|
1294
|
+
* @param options Account update options
|
|
1295
|
+
* @returns Account update result
|
|
1296
|
+
*/
|
|
1297
|
+
async updateAccount(options) {
|
|
1298
|
+
this.logger.debug('Updating account', options);
|
|
1299
|
+
const params = { account: this.account };
|
|
1300
|
+
if (options.deviceName) {
|
|
1301
|
+
params.deviceName = options.deviceName;
|
|
1302
|
+
}
|
|
1303
|
+
if (options.username) {
|
|
1304
|
+
params.username = options.username;
|
|
1305
|
+
}
|
|
1306
|
+
if (options.deleteUsername) {
|
|
1307
|
+
params.deleteUsername = true;
|
|
1308
|
+
}
|
|
1309
|
+
if (options.unrestrictedUnidentifiedSender !== undefined) {
|
|
1310
|
+
params.unrestrictedUnidentifiedSender = options.unrestrictedUnidentifiedSender;
|
|
1311
|
+
}
|
|
1312
|
+
if (options.discoverableByNumber !== undefined) {
|
|
1313
|
+
params.discoverableByNumber = options.discoverableByNumber;
|
|
1314
|
+
}
|
|
1315
|
+
if (options.numberSharing !== undefined) {
|
|
1316
|
+
params.numberSharing = options.numberSharing;
|
|
1317
|
+
}
|
|
1318
|
+
try {
|
|
1319
|
+
const result = await this.sendJsonRpcRequest('updateAccount', params);
|
|
1320
|
+
return {
|
|
1321
|
+
success: true,
|
|
1322
|
+
username: result.username,
|
|
1323
|
+
usernameLink: result.usernameLink
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
catch (error) {
|
|
1327
|
+
return {
|
|
1328
|
+
success: false,
|
|
1329
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Set or update the username for this account.
|
|
1335
|
+
* Helper method that wraps updateAccount() for simpler username management.
|
|
1336
|
+
*
|
|
1337
|
+
* @param username - The username to set (without discriminator)
|
|
1338
|
+
* @returns Account update result with username and link
|
|
1339
|
+
*
|
|
1340
|
+
* @example
|
|
1341
|
+
* ```typescript
|
|
1342
|
+
* const result = await signal.setUsername('myusername');
|
|
1343
|
+
* console.log(`Username: ${result.username}`);
|
|
1344
|
+
* console.log(`Link: ${result.usernameLink}`);
|
|
1345
|
+
* ```
|
|
1346
|
+
*/
|
|
1347
|
+
async setUsername(username) {
|
|
1348
|
+
return this.updateAccount({ username });
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* Delete the current username from this account.
|
|
1352
|
+
* Helper method that wraps updateAccount() for simpler username deletion.
|
|
1353
|
+
*
|
|
1354
|
+
* @returns Account update result
|
|
1355
|
+
*
|
|
1356
|
+
* @example
|
|
1357
|
+
* ```typescript
|
|
1358
|
+
* const result = await signal.deleteUsername();
|
|
1359
|
+
* if (result.success) {
|
|
1360
|
+
* console.log('Username deleted successfully');
|
|
1361
|
+
* }
|
|
1362
|
+
* ```
|
|
1363
|
+
*/
|
|
1364
|
+
async deleteUsername() {
|
|
1365
|
+
return this.updateAccount({ deleteUsername: true });
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Get raw attachment data as base64 string.
|
|
1369
|
+
* @param options Attachment retrieval options
|
|
1370
|
+
* @returns Base64 encoded attachment data
|
|
1371
|
+
*/
|
|
1372
|
+
async getAttachment(options) {
|
|
1373
|
+
this.logger.debug('Getting attachment', options);
|
|
1374
|
+
if (!options.id) {
|
|
1375
|
+
throw new errors_1.MessageError('Attachment ID is required');
|
|
1376
|
+
}
|
|
1377
|
+
const params = {
|
|
1378
|
+
id: options.id,
|
|
1379
|
+
account: this.account
|
|
1380
|
+
};
|
|
1381
|
+
if (options.groupId) {
|
|
1382
|
+
(0, validators_1.validateGroupId)(options.groupId);
|
|
1383
|
+
params.groupId = options.groupId;
|
|
1384
|
+
}
|
|
1385
|
+
else if (options.recipient) {
|
|
1386
|
+
(0, validators_1.validateRecipient)(options.recipient);
|
|
1387
|
+
params.recipient = options.recipient;
|
|
1388
|
+
}
|
|
1389
|
+
const result = await this.sendJsonRpcRequest('getAttachment', params);
|
|
1390
|
+
return result.data || result;
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Get raw avatar data as base64 string.
|
|
1394
|
+
* @param options Avatar retrieval options
|
|
1395
|
+
* @returns Base64 encoded avatar data
|
|
1396
|
+
*/
|
|
1397
|
+
async getAvatar(options) {
|
|
1398
|
+
this.logger.debug('Getting avatar', options);
|
|
1399
|
+
const params = { account: this.account };
|
|
1400
|
+
if (options.contact) {
|
|
1401
|
+
(0, validators_1.validateRecipient)(options.contact);
|
|
1402
|
+
params.contact = options.contact;
|
|
1403
|
+
}
|
|
1404
|
+
else if (options.profile) {
|
|
1405
|
+
(0, validators_1.validateRecipient)(options.profile);
|
|
1406
|
+
params.profile = options.profile;
|
|
1407
|
+
}
|
|
1408
|
+
else if (options.groupId) {
|
|
1409
|
+
(0, validators_1.validateGroupId)(options.groupId);
|
|
1410
|
+
params.groupId = options.groupId;
|
|
1411
|
+
}
|
|
1412
|
+
else {
|
|
1413
|
+
throw new errors_1.MessageError('Must specify contact, profile, or groupId');
|
|
1414
|
+
}
|
|
1415
|
+
const result = await this.sendJsonRpcRequest('getAvatar', params);
|
|
1416
|
+
return result.data || result;
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Get raw sticker data as base64 string.
|
|
1420
|
+
* @param options Sticker retrieval options
|
|
1421
|
+
* @returns Base64 encoded sticker data
|
|
1422
|
+
*/
|
|
1423
|
+
async getSticker(options) {
|
|
1424
|
+
this.logger.debug('Getting sticker', options);
|
|
1425
|
+
if (!options.packId || !options.stickerId) {
|
|
1426
|
+
throw new errors_1.MessageError('Pack ID and sticker ID are required');
|
|
1427
|
+
}
|
|
1428
|
+
const params = {
|
|
1429
|
+
packId: options.packId,
|
|
1430
|
+
stickerId: options.stickerId,
|
|
1431
|
+
account: this.account
|
|
1432
|
+
};
|
|
1433
|
+
const result = await this.sendJsonRpcRequest('getSticker', params);
|
|
1434
|
+
return result.data || result;
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Send contacts synchronization message to linked devices.
|
|
1438
|
+
* @param options Contacts sync options
|
|
1439
|
+
*/
|
|
1440
|
+
async sendContacts(options = {}) {
|
|
1441
|
+
this.logger.debug('Sending contacts sync');
|
|
1442
|
+
const params = { account: this.account };
|
|
1443
|
+
if (options.includeAllRecipients) {
|
|
1444
|
+
params.allRecipients = true;
|
|
1445
|
+
}
|
|
1446
|
+
await this.sendJsonRpcRequest('sendContacts', params);
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* List groups with optional filtering and details.
|
|
1450
|
+
* @param options List groups options
|
|
1451
|
+
* @returns Array of group information
|
|
1452
|
+
*/
|
|
1453
|
+
async listGroupsDetailed(options = {}) {
|
|
1454
|
+
this.logger.debug('Listing groups with options', options);
|
|
1455
|
+
const params = { account: this.account };
|
|
1456
|
+
if (options.detailed) {
|
|
1457
|
+
params.detailed = true;
|
|
1458
|
+
}
|
|
1459
|
+
if (options.groupIds && options.groupIds.length > 0) {
|
|
1460
|
+
params.groupId = options.groupIds;
|
|
1461
|
+
}
|
|
1462
|
+
return this.sendJsonRpcRequest('listGroups', params);
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* List all local accounts.
|
|
1466
|
+
* @returns Array of account phone numbers
|
|
1467
|
+
*/
|
|
1468
|
+
async listAccountsDetailed() {
|
|
1469
|
+
this.logger.debug('Listing all accounts');
|
|
1470
|
+
const result = await this.sendJsonRpcRequest('listAccounts');
|
|
1471
|
+
return result.accounts || [];
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Extract profile information from a contact.
|
|
1475
|
+
* Parses givenName, familyName, mobileCoinAddress from profile data.
|
|
1476
|
+
*
|
|
1477
|
+
* @param contact - The contact object to parse
|
|
1478
|
+
* @returns Enhanced contact with extracted profile fields
|
|
1479
|
+
*
|
|
1480
|
+
* @example
|
|
1481
|
+
* ```typescript
|
|
1482
|
+
* const contacts = await signal.listContacts();
|
|
1483
|
+
* const enriched = signal.parseContactProfile(contacts[0]);
|
|
1484
|
+
* console.log(enriched.givenName, enriched.familyName);
|
|
1485
|
+
* ```
|
|
1486
|
+
*/
|
|
1487
|
+
parseContactProfile(contact) {
|
|
1488
|
+
// signal-cli already provides these fields if available
|
|
1489
|
+
// This method normalizes and validates the data
|
|
1490
|
+
return {
|
|
1491
|
+
...contact,
|
|
1492
|
+
givenName: contact.givenName || undefined,
|
|
1493
|
+
familyName: contact.familyName || undefined,
|
|
1494
|
+
mobileCoinAddress: contact.mobileCoinAddress || undefined,
|
|
1495
|
+
profileName: contact.profileName ||
|
|
1496
|
+
(contact.givenName && contact.familyName
|
|
1497
|
+
? `${contact.givenName} ${contact.familyName}`
|
|
1498
|
+
: contact.givenName || contact.familyName),
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* Extract group membership details.
|
|
1503
|
+
* Parses pendingMembers, bannedMembers, inviteLink from group data.
|
|
1504
|
+
*
|
|
1505
|
+
* @param group - The group info to parse
|
|
1506
|
+
* @returns Enhanced group with extracted membership fields
|
|
1507
|
+
*
|
|
1508
|
+
* @example
|
|
1509
|
+
* ```typescript
|
|
1510
|
+
* const groups = await signal.listGroups();
|
|
1511
|
+
* const enriched = signal.parseGroupDetails(groups[0]);
|
|
1512
|
+
* console.log(enriched.pendingMembers, enriched.bannedMembers);
|
|
1513
|
+
* ```
|
|
1514
|
+
*/
|
|
1515
|
+
parseGroupDetails(group) {
|
|
1516
|
+
return {
|
|
1517
|
+
...group,
|
|
1518
|
+
// Normalize inviteLink field
|
|
1519
|
+
inviteLink: group.groupInviteLink || group.inviteLink,
|
|
1520
|
+
groupInviteLink: group.groupInviteLink || group.inviteLink,
|
|
1521
|
+
// Ensure arrays exist
|
|
1522
|
+
pendingMembers: group.pendingMembers || [],
|
|
1523
|
+
banned: group.banned || [],
|
|
1524
|
+
requestingMembers: group.requestingMembers || [],
|
|
1525
|
+
admins: group.admins || [],
|
|
1526
|
+
members: group.members || [],
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Get enriched contacts list with parsed profile information.
|
|
1531
|
+
*
|
|
1532
|
+
* @returns Array of contacts with full profile data
|
|
1533
|
+
*
|
|
1534
|
+
* @example
|
|
1535
|
+
* ```typescript
|
|
1536
|
+
* const contacts = await signal.getContactsWithProfiles();
|
|
1537
|
+
* contacts.forEach(c => {
|
|
1538
|
+
* console.log(`${c.givenName} ${c.familyName} - ${c.mobileCoinAddress}`);
|
|
1539
|
+
* });
|
|
1540
|
+
* ```
|
|
1541
|
+
*/
|
|
1542
|
+
async getContactsWithProfiles() {
|
|
1543
|
+
const contacts = await this.listContacts();
|
|
1544
|
+
return contacts.map(c => this.parseContactProfile(c));
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Get enriched groups list with parsed membership details.
|
|
1548
|
+
*
|
|
1549
|
+
* @param options - List groups options
|
|
1550
|
+
* @returns Array of groups with full membership data
|
|
1551
|
+
*
|
|
1552
|
+
* @example
|
|
1553
|
+
* ```typescript
|
|
1554
|
+
* const groups = await signal.getGroupsWithDetails();
|
|
1555
|
+
* groups.forEach(g => {
|
|
1556
|
+
* console.log(`${g.name}: ${g.members.length} members, ${g.pendingMembers.length} pending`);
|
|
1557
|
+
* });
|
|
1558
|
+
* ```
|
|
1559
|
+
*/
|
|
1560
|
+
async getGroupsWithDetails(options = {}) {
|
|
1561
|
+
const groups = await this.listGroupsDetailed(options);
|
|
1562
|
+
return groups.map(g => this.parseGroupDetails(g));
|
|
1563
|
+
}
|
|
710
1564
|
}
|
|
711
1565
|
exports.SignalCli = SignalCli;
|