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