signal-sdk 0.1.0 → 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 +21 -14
- 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 +244 -15
- package/dist/SignalCli.js +623 -25
- package/dist/__tests__/MultiAccountManager.test.d.ts +4 -0
- package/dist/__tests__/MultiAccountManager.test.js +209 -0
- package/dist/__tests__/SignalBot.additional.test.js +31 -11
- 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.js +9 -2
- package/dist/__tests__/SignalCli.methods.test.js +87 -1
- 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 +5 -13
- package/dist/config.d.ts +16 -1
- package/dist/config.js +6 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/interfaces.d.ts +64 -9
- package/dist/retry.js +25 -8
- package/package.json +1 -1
package/dist/SignalCli.js
CHANGED
|
@@ -88,6 +88,25 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
88
88
|
this.account = phoneNumber;
|
|
89
89
|
}
|
|
90
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() {
|
|
91
110
|
const args = this.account ? ['-a', this.account, 'jsonRpc'] : ['jsonRpc'];
|
|
92
111
|
if (process.platform === 'win32') {
|
|
93
112
|
// On Windows, use cmd.exe to run the batch file with proper quoting for paths with spaces
|
|
@@ -133,11 +152,120 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
133
152
|
});
|
|
134
153
|
});
|
|
135
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
|
+
}
|
|
136
253
|
disconnect() {
|
|
137
|
-
|
|
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) {
|
|
138
265
|
this.cliProcess.kill();
|
|
139
266
|
this.cliProcess = null;
|
|
140
267
|
}
|
|
268
|
+
// For HTTP mode, nothing to disconnect (stateless)
|
|
141
269
|
}
|
|
142
270
|
async gracefulShutdown() {
|
|
143
271
|
return new Promise((resolve) => {
|
|
@@ -153,13 +281,17 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
153
281
|
// Send SIGTERM for graceful shutdown
|
|
154
282
|
this.cliProcess.kill('SIGTERM');
|
|
155
283
|
// Force kill after 5 seconds if it doesn't close gracefully
|
|
156
|
-
setTimeout(() => {
|
|
284
|
+
const forceKillTimer = setTimeout(() => {
|
|
157
285
|
if (this.cliProcess) {
|
|
158
286
|
this.cliProcess.kill('SIGKILL');
|
|
159
287
|
this.cliProcess = null;
|
|
160
288
|
resolve();
|
|
161
289
|
}
|
|
162
290
|
}, 5000);
|
|
291
|
+
// Use unref() to prevent this timer from keeping the process alive
|
|
292
|
+
if (forceKillTimer.unref) {
|
|
293
|
+
forceKillTimer.unref();
|
|
294
|
+
}
|
|
163
295
|
});
|
|
164
296
|
}
|
|
165
297
|
handleRpcResponse(data) {
|
|
@@ -234,8 +366,40 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
234
366
|
}
|
|
235
367
|
}
|
|
236
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
|
|
237
401
|
if (!this.cliProcess || !this.cliProcess.stdin) {
|
|
238
|
-
throw new
|
|
402
|
+
throw new errors_1.ConnectionError('Not connected. Call connect() first.');
|
|
239
403
|
}
|
|
240
404
|
const id = (0, uuid_1.v4)();
|
|
241
405
|
const request = {
|
|
@@ -272,7 +436,7 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
272
436
|
else {
|
|
273
437
|
params.recipients = [recipient];
|
|
274
438
|
}
|
|
275
|
-
//
|
|
439
|
+
// Add well-known options
|
|
276
440
|
if (options.attachments && options.attachments.length > 0) {
|
|
277
441
|
params.attachments = options.attachments;
|
|
278
442
|
}
|
|
@@ -280,7 +444,64 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
280
444
|
params.expiresInSeconds = options.expiresInSeconds;
|
|
281
445
|
}
|
|
282
446
|
if (options.isViewOnce) {
|
|
283
|
-
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;
|
|
284
505
|
}
|
|
285
506
|
return this.sendJsonRpcRequest('send', params);
|
|
286
507
|
}
|
|
@@ -371,6 +592,79 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
371
592
|
async trustIdentity(number, safetyNumber, verified = true) {
|
|
372
593
|
await this.sendJsonRpcRequest('trust', { account: this.account, recipient: number, safetyNumber, verified });
|
|
373
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
|
+
}
|
|
374
668
|
async link(deviceName) {
|
|
375
669
|
const result = await this.sendJsonRpcRequest('link', { deviceName });
|
|
376
670
|
return result.uri;
|
|
@@ -505,6 +799,10 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
505
799
|
params.promoteAdmins = options.promoteAdmins;
|
|
506
800
|
if (options.demoteAdmins)
|
|
507
801
|
params.demoteAdmins = options.demoteAdmins;
|
|
802
|
+
if (options.banMembers)
|
|
803
|
+
params.banMembers = options.banMembers;
|
|
804
|
+
if (options.unbanMembers)
|
|
805
|
+
params.unbanMembers = options.unbanMembers;
|
|
508
806
|
if (options.resetInviteLink)
|
|
509
807
|
params.resetLink = true;
|
|
510
808
|
if (options.permissionAddMember)
|
|
@@ -520,6 +818,58 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
520
818
|
async listGroups() {
|
|
521
819
|
return this.sendJsonRpcRequest('listGroups', { account: this.account });
|
|
522
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
|
+
}
|
|
523
873
|
async listContacts() {
|
|
524
874
|
return this.sendJsonRpcRequest('listContacts', { account: this.account });
|
|
525
875
|
}
|
|
@@ -559,6 +909,101 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
559
909
|
console.warn("stopDaemon is deprecated. Use gracefulShutdown() or disconnect() instead.");
|
|
560
910
|
this.gracefulShutdown();
|
|
561
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
|
+
}
|
|
562
1007
|
// ############# NEW FEATURES - Missing signal-cli Commands #############
|
|
563
1008
|
/**
|
|
564
1009
|
* Remove a contact from the contact list.
|
|
@@ -604,12 +1049,29 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
604
1049
|
return statusResults;
|
|
605
1050
|
}
|
|
606
1051
|
/**
|
|
607
|
-
* Send a payment notification to a recipient.
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
* @
|
|
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
|
+
* ```
|
|
611
1068
|
*/
|
|
612
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
|
+
}
|
|
613
1075
|
const params = {
|
|
614
1076
|
receipt: paymentData.receipt,
|
|
615
1077
|
account: this.account
|
|
@@ -662,13 +1124,18 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
662
1124
|
};
|
|
663
1125
|
}
|
|
664
1126
|
/**
|
|
665
|
-
* Start the
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
* @
|
|
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
|
|
670
1135
|
*/
|
|
671
1136
|
async startChangeNumber(newNumber, voice = false, captcha) {
|
|
1137
|
+
this.logger.info(`Starting change number to ${newNumber} (voice: ${voice})`);
|
|
1138
|
+
(0, validators_1.validatePhoneNumber)(newNumber);
|
|
672
1139
|
const params = {
|
|
673
1140
|
account: this.account,
|
|
674
1141
|
number: newNumber,
|
|
@@ -676,22 +1143,28 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
676
1143
|
};
|
|
677
1144
|
if (captcha)
|
|
678
1145
|
params.captcha = captcha;
|
|
679
|
-
|
|
680
|
-
return {
|
|
681
|
-
session: result.session,
|
|
682
|
-
newNumber,
|
|
683
|
-
challenge: result.challenge
|
|
684
|
-
};
|
|
1146
|
+
await this.sendJsonRpcRequest('startChangeNumber', params);
|
|
685
1147
|
}
|
|
686
1148
|
/**
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
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
|
|
690
1157
|
*/
|
|
691
|
-
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
|
+
}
|
|
692
1164
|
const params = {
|
|
693
1165
|
account: this.account,
|
|
694
|
-
|
|
1166
|
+
number: newNumber,
|
|
1167
|
+
verificationCode
|
|
695
1168
|
};
|
|
696
1169
|
if (pin)
|
|
697
1170
|
params.pin = pin;
|
|
@@ -857,6 +1330,40 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
857
1330
|
};
|
|
858
1331
|
}
|
|
859
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
|
+
}
|
|
860
1367
|
/**
|
|
861
1368
|
* Get raw attachment data as base64 string.
|
|
862
1369
|
* @param options Attachment retrieval options
|
|
@@ -963,5 +1470,96 @@ class SignalCli extends events_1.EventEmitter {
|
|
|
963
1470
|
const result = await this.sendJsonRpcRequest('listAccounts');
|
|
964
1471
|
return result.accounts || [];
|
|
965
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
|
+
}
|
|
966
1564
|
}
|
|
967
1565
|
exports.SignalCli = SignalCli;
|