@shoru/kitten 0.1.1 → 0.1.2
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 +19 -5
- package/internal-test/plugins/exp.js +1 -1
- package/internal-test/plugins/pairing.js +96 -0
- package/package.json +3 -3
- package/src/auth/lmdb-auth-state.js +0 -4
- package/src/client/auth-handler.js +8 -28
- package/src/client/client.js +60 -11
- package/src/client/constants.js +1 -0
- package/src/client/qr.js +35 -0
- package/src/internals/index.js +5 -3
- package/src/internals/libsignal-patch.js +80 -0
- package/src/internals/logger.js +15 -15
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div align="left">
|
|
2
2
|
|
|
3
|
-
<img src="https://files.catbox.moe/j7dpni.png"
|
|
3
|
+
<img src="https://files.catbox.moe/j7dpni.png" alt="Kitten Logo" style="width: 30%; max-width: 220px; height: auto; float: right; margin-left: 15px;">
|
|
4
4
|
|
|
5
5
|
<a name="-kitten"></a>
|
|
6
6
|
# **Kitten Framework**
|
|
@@ -73,6 +73,7 @@ import { getClient } from '@shoru/kitten';
|
|
|
73
73
|
const { sock, session, id } = await getClient({
|
|
74
74
|
id: 0, // Session ID (auto-generated if omitted)
|
|
75
75
|
maxRetries: 30, // Reconnection attempts
|
|
76
|
+
maxPairingAttempts: 20, // Max pairing attempts before timeout (default: 20)
|
|
76
77
|
silent: false, // Suppress output
|
|
77
78
|
socketConfig: {}, // Baileys socket overrides
|
|
78
79
|
|
|
@@ -88,6 +89,9 @@ const { sock, session, id } = await getClient({
|
|
|
88
89
|
},
|
|
89
90
|
onStateChange: ({ oldState, newState }) => {
|
|
90
91
|
console.log(`State: ${oldState} → ${newState}`);
|
|
92
|
+
},
|
|
93
|
+
onPairingAttemptsExceeded: ({ client, maxAttempts }) => {
|
|
94
|
+
console.log(`Pairing attempts exceeded limit of ${maxAttempts}`);
|
|
91
95
|
}
|
|
92
96
|
});
|
|
93
97
|
```
|
|
@@ -95,17 +99,27 @@ const { sock, session, id } = await getClient({
|
|
|
95
99
|
### Custom Authentication
|
|
96
100
|
|
|
97
101
|
```javascript
|
|
98
|
-
import qrcode from 'qrcode-terminal';
|
|
99
|
-
|
|
100
102
|
const { sock, id } = await getClient({
|
|
101
|
-
|
|
103
|
+
maxPairingAttempts: 5,
|
|
104
|
+
onPairing: async ({ qr, requestPairingCode, attempts, maxAttempts }) => {
|
|
105
|
+
console.log(`Pairing attempt ${attempts}/${maxAttempts}`);
|
|
106
|
+
|
|
102
107
|
// Option 1: Display QR code
|
|
103
108
|
console.log('Scan QR:');
|
|
104
|
-
|
|
109
|
+
await qr.print();
|
|
105
110
|
|
|
111
|
+
/*
|
|
112
|
+
you can use:
|
|
113
|
+
const qrImg = await qr.buffer()
|
|
114
|
+
to send|save the QR code as a png image
|
|
115
|
+
*/
|
|
116
|
+
|
|
106
117
|
// Option 2: Use pairing code
|
|
107
118
|
const code = await requestPairingCode('1234567890');
|
|
108
119
|
console.log('Enter this code in WhatsApp:', code);
|
|
120
|
+
},
|
|
121
|
+
onPairingAttemptsExceeded: async ({ client, maxAttempts }) => {
|
|
122
|
+
console.log(`Pairing expired after ${maxAttempts} attempts!`);
|
|
109
123
|
}
|
|
110
124
|
});
|
|
111
125
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const plugin = async (sock, ctx) => {
|
|
2
2
|
console.log(JSON.stringify({...ctx, raw: undefined}, null, 1));
|
|
3
|
-
sock.sendMessage(ctx.roomId, { text:
|
|
3
|
+
await sock.sendMessage(ctx.roomId, { text: "tst" }, { quoted: ctx.raw });
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
plugin.events = ['messages.upsert'];
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { getClient } from '../../src/index.js';
|
|
2
|
+
|
|
3
|
+
export const plugin = async (sock, ctx) => {
|
|
4
|
+
const args = ctx.body?.trim().split(/\s+/) ?? [];
|
|
5
|
+
const type = args[1]?.toLowerCase();
|
|
6
|
+
|
|
7
|
+
if (!type || (type !== 'qr' && type !== 'otp')) {
|
|
8
|
+
return await sock.sendMessage(
|
|
9
|
+
ctx.roomId,
|
|
10
|
+
{ text: 'Please specify the pairing type: `qr` or `otp`.\n\n*Usage:*\n- `!pairing qr`\n- `!pairing otp`' },
|
|
11
|
+
{ quoted: ctx.raw }
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const options = {
|
|
16
|
+
silent: true,
|
|
17
|
+
maxRetries: 2,
|
|
18
|
+
maxPairingAttempts: 1,
|
|
19
|
+
onPairingAttemptsExceeded: async () => {
|
|
20
|
+
await sock.sendMessage(
|
|
21
|
+
ctx.roomId,
|
|
22
|
+
{ text: 'Pairing code expired. Please try again' },
|
|
23
|
+
{ quoted: ctx.raw }
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (type === 'qr') {
|
|
29
|
+
await getClient({
|
|
30
|
+
...options,
|
|
31
|
+
onPairing: async ({ qr }) => {
|
|
32
|
+
const qrImg = await qr.buffer();
|
|
33
|
+
await sock.sendMessage(
|
|
34
|
+
ctx.roomId,
|
|
35
|
+
{ image: qrImg, caption: 'Scan this QR code to pair your device' },
|
|
36
|
+
{ quoted: ctx.raw }
|
|
37
|
+
);
|
|
38
|
+
},
|
|
39
|
+
onPairingAttemptsExceeded: async () => {
|
|
40
|
+
await sock.sendMessage(
|
|
41
|
+
ctx.roomId,
|
|
42
|
+
{ text: 'Pairing attempts exceeded. Please try again' },
|
|
43
|
+
{ quoted: ctx.raw }
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
onConnect: async ({ client }) => {
|
|
47
|
+
await sock.sendMessage(
|
|
48
|
+
ctx.roomId,
|
|
49
|
+
{ text: `Session *${client.id}* paired and connected successfully!` },
|
|
50
|
+
{ quoted: ctx.raw }
|
|
51
|
+
);
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
} else if (type === 'otp') {
|
|
55
|
+
const phone = await ctx.pn();
|
|
56
|
+
|
|
57
|
+
if (!phone) {
|
|
58
|
+
return await sock.sendMessage(
|
|
59
|
+
ctx.roomId,
|
|
60
|
+
{ text: 'Failed to extract phone number' },
|
|
61
|
+
{ quoted: ctx.raw }
|
|
62
|
+
);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await getClient({
|
|
67
|
+
...options,
|
|
68
|
+
onPairing: async ({ requestPairingCode }) => {
|
|
69
|
+
const code = await requestPairingCode(phone);
|
|
70
|
+
const formattedCode = code?.match(/.{1,4}/g)?.join('-') ?? code;
|
|
71
|
+
await sock.sendMessage(
|
|
72
|
+
ctx.roomId,
|
|
73
|
+
{ text: `Your OTP pairing code: *${formattedCode}*` },
|
|
74
|
+
{ quoted: ctx.raw }
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
onPairingAttemptsExceeded: async () => {
|
|
78
|
+
await sock.sendMessage(
|
|
79
|
+
ctx.roomId,
|
|
80
|
+
{ text: 'Pairing attempts exceeded. Please try again' },
|
|
81
|
+
{ quoted: ctx.raw }
|
|
82
|
+
);
|
|
83
|
+
},
|
|
84
|
+
onConnect: async ({ client }) => {
|
|
85
|
+
await sock.sendMessage(
|
|
86
|
+
ctx.roomId,
|
|
87
|
+
{ text: `Session *${client.id}* paired and connected successfully!` },
|
|
88
|
+
{ quoted: ctx.raw }
|
|
89
|
+
);
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
plugin.events = ['messages.upsert'];
|
|
96
|
+
plugin.match = ['pairing'];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shoru/kitten",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.2",
|
|
5
5
|
"description": "A powerful Node.js framework to simplify making WhatsApp Bots",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"repository": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"@inquirer/prompts": "^8.7.0",
|
|
30
30
|
"@shoru/spindle": "^1.0.7",
|
|
31
31
|
"async-mutex": "^0.5.0",
|
|
32
|
-
"baileys": "
|
|
32
|
+
"baileys": "7.0.0-rc14",
|
|
33
33
|
"chalk": "^6.0.0",
|
|
34
34
|
"chokidar": "^5.0.0",
|
|
35
35
|
"cosmiconfig": "^10.0.0",
|
|
@@ -37,6 +37,6 @@
|
|
|
37
37
|
"lmdb": "^3.5.6",
|
|
38
38
|
"pino": "^10.3.1",
|
|
39
39
|
"pino-pretty": "^13.1.3",
|
|
40
|
-
"qrcode
|
|
40
|
+
"qrcode": "^1.5.4"
|
|
41
41
|
}
|
|
42
42
|
}
|
|
@@ -31,10 +31,6 @@ const genID = async (db) => {
|
|
|
31
31
|
|
|
32
32
|
const getSessionId = async (db, input) => {
|
|
33
33
|
if (input == null) {
|
|
34
|
-
const existing = listSessions();
|
|
35
|
-
if (existing.length > 0) {
|
|
36
|
-
return existing[0];
|
|
37
|
-
}
|
|
38
34
|
return genID(db);
|
|
39
35
|
}
|
|
40
36
|
if (Number.isInteger(input) && input >= 0) return input;
|
|
@@ -1,37 +1,14 @@
|
|
|
1
|
-
import qrcode from 'qrcode-terminal';
|
|
2
1
|
import chalk from 'chalk';
|
|
3
2
|
import { ConnectionError } from './errors.js';
|
|
4
3
|
import { ClientRegistry } from './registry.js';
|
|
5
4
|
import { getConnectionConfig } from './getConnectionConfig.js';
|
|
5
|
+
import { QR } from './qr.js';
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* Formats a pairing code into readable 4-character groups with styling.
|
|
9
|
-
*
|
|
10
|
-
* @param {string} code - The raw pairing code string
|
|
11
|
-
* @returns {string} The styled formatted pairing code
|
|
12
|
-
*/
|
|
13
7
|
export function formatPairingCode(code) {
|
|
14
8
|
const formatted = code.match(/.{1,4}/g)?.join(' ') ?? code;
|
|
15
9
|
return `\n${chalk.green('> Your OTP Code: ')}${chalk.bold(formatted)}`;
|
|
16
10
|
}
|
|
17
11
|
|
|
18
|
-
/**
|
|
19
|
-
* Handles authentication for a client instance when a QR code or pairing challenge is received.
|
|
20
|
-
*
|
|
21
|
-
* @param {object} params
|
|
22
|
-
* @param {string} params.qr - The QR code string
|
|
23
|
-
* @param {any} params.rawSock - The raw WASocket instance
|
|
24
|
-
* @param {string} params.flag - Identifier string for logging
|
|
25
|
-
* @param {boolean} params.isSync - Whether the client is a background sync instance
|
|
26
|
-
* @param {boolean} params.isSilent - Whether logging/prompts are suppressed
|
|
27
|
-
* @param {Function|null} params.onPairing - Optional custom pairing callback
|
|
28
|
-
* @param {object} params.logger - Logger instance
|
|
29
|
-
* @param {object|null} params.authConfig - Cached auth config
|
|
30
|
-
* @param {(config: object|null) => void} params.setAuthConfig - Setter for auth config
|
|
31
|
-
* @param {boolean} params.pairingCodeRequested - Whether pairing code has already been requested
|
|
32
|
-
* @param {(requested: boolean) => void} params.setPairingCodeRequested - Setter for pairingCodeRequested
|
|
33
|
-
* @param {(err: Error) => void} params.onSyncAuthAbort - Callback when sync connection requires auth
|
|
34
|
-
*/
|
|
35
12
|
export async function handleClientAuth({
|
|
36
13
|
qr,
|
|
37
14
|
rawSock,
|
|
@@ -39,6 +16,8 @@ export async function handleClientAuth({
|
|
|
39
16
|
isSync,
|
|
40
17
|
isSilent,
|
|
41
18
|
onPairing,
|
|
19
|
+
attempts,
|
|
20
|
+
maxAttempts,
|
|
42
21
|
logger,
|
|
43
22
|
authConfig,
|
|
44
23
|
setAuthConfig,
|
|
@@ -55,11 +34,13 @@ export async function handleClientAuth({
|
|
|
55
34
|
return;
|
|
56
35
|
}
|
|
57
36
|
|
|
37
|
+
const qrObj = qr instanceof QR ? qr : new QR(qr);
|
|
38
|
+
|
|
58
39
|
if (ClientRegistry.isConfiguring) return;
|
|
59
40
|
|
|
60
41
|
if (typeof onPairing === 'function') {
|
|
61
42
|
const requestPairingCode = rawSock?.requestPairingCode?.bind(rawSock);
|
|
62
|
-
await onPairing({ qr, requestPairingCode });
|
|
43
|
+
await onPairing({ qr: qrObj, requestPairingCode, attempts, maxAttempts });
|
|
63
44
|
return;
|
|
64
45
|
}
|
|
65
46
|
|
|
@@ -90,8 +71,7 @@ export async function handleClientAuth({
|
|
|
90
71
|
}
|
|
91
72
|
}
|
|
92
73
|
} else {
|
|
93
|
-
|
|
74
|
+
await qrObj.print();
|
|
94
75
|
process.stdout.write('\n');
|
|
95
76
|
}
|
|
96
|
-
}
|
|
97
|
-
|
|
77
|
+
}
|
package/src/client/client.js
CHANGED
|
@@ -4,6 +4,7 @@ import { initSession, listSessions } from '#auth.js';
|
|
|
4
4
|
import {
|
|
5
5
|
ConnectionState,
|
|
6
6
|
DEFAULT_MAX_RETRIES,
|
|
7
|
+
DEFAULT_MAX_PAIRING_ATTEMPTS,
|
|
7
8
|
defaultBackoff,
|
|
8
9
|
silentLogger,
|
|
9
10
|
silentPino,
|
|
@@ -73,12 +74,14 @@ export class Client {
|
|
|
73
74
|
#pairingCodeRequested = false;
|
|
74
75
|
|
|
75
76
|
#reconnectAttempts = 0;
|
|
77
|
+
#pairingAttempts = 0;
|
|
76
78
|
#reconnectTimer = null;
|
|
77
79
|
#isShuttingDown = false;
|
|
78
80
|
|
|
79
81
|
#pendingConnect = null;
|
|
80
82
|
|
|
81
83
|
#maxRetries;
|
|
84
|
+
#maxPairingAttempts;
|
|
82
85
|
#backoff;
|
|
83
86
|
|
|
84
87
|
// Options
|
|
@@ -88,6 +91,7 @@ export class Client {
|
|
|
88
91
|
|
|
89
92
|
// Callbacks
|
|
90
93
|
#onPairing;
|
|
94
|
+
#onPairingAttemptsExceeded;
|
|
91
95
|
#onConnect;
|
|
92
96
|
#onReconnect;
|
|
93
97
|
#onDisconnect;
|
|
@@ -97,10 +101,12 @@ export class Client {
|
|
|
97
101
|
const {
|
|
98
102
|
id,
|
|
99
103
|
maxRetries = DEFAULT_MAX_RETRIES,
|
|
104
|
+
maxPairingAttempts = DEFAULT_MAX_PAIRING_ATTEMPTS,
|
|
100
105
|
backoff = defaultBackoff,
|
|
101
106
|
silent = false,
|
|
102
107
|
sync = false,
|
|
103
108
|
onPairing = null,
|
|
109
|
+
onPairingAttemptsExceeded = null,
|
|
104
110
|
onConnect = null,
|
|
105
111
|
onReconnect = null,
|
|
106
112
|
onDisconnect = null,
|
|
@@ -111,11 +117,13 @@ export class Client {
|
|
|
111
117
|
this.id = id;
|
|
112
118
|
this.#socketConfig = socketConfig;
|
|
113
119
|
this.#maxRetries = maxRetries;
|
|
120
|
+
this.#maxPairingAttempts = maxPairingAttempts;
|
|
114
121
|
this.#backoff = backoff;
|
|
115
122
|
this.#silent = silent;
|
|
116
123
|
this.#sync = sync;
|
|
117
124
|
this.#logger = silent ? silentLogger : logger;
|
|
118
125
|
this.#onPairing = onPairing;
|
|
126
|
+
this.#onPairingAttemptsExceeded = onPairingAttemptsExceeded;
|
|
119
127
|
this.#onConnect = onConnect;
|
|
120
128
|
this.#onReconnect = onReconnect;
|
|
121
129
|
this.#onDisconnect = onDisconnect;
|
|
@@ -145,6 +153,14 @@ export class Client {
|
|
|
145
153
|
return this.#reconnectAttempts;
|
|
146
154
|
}
|
|
147
155
|
|
|
156
|
+
get pairingAttempts() {
|
|
157
|
+
return this.#pairingAttempts;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
get maxPairingAttempts() {
|
|
161
|
+
return this.#maxPairingAttempts;
|
|
162
|
+
}
|
|
163
|
+
|
|
148
164
|
get ev() {
|
|
149
165
|
return this.#ev;
|
|
150
166
|
}
|
|
@@ -188,11 +204,15 @@ export class Client {
|
|
|
188
204
|
const callback = callbacks[event];
|
|
189
205
|
if (typeof callback !== 'function') return;
|
|
190
206
|
|
|
191
|
-
queueMicrotask(() => {
|
|
207
|
+
queueMicrotask(async () => {
|
|
192
208
|
try {
|
|
193
|
-
callback({ ...data, client: this });
|
|
209
|
+
await callback({ ...data, client: this });
|
|
194
210
|
} catch (err) {
|
|
195
|
-
|
|
211
|
+
try {
|
|
212
|
+
this.#logger.error(err, `[${this.#flag}] Error in ${event} callback`);
|
|
213
|
+
} catch {
|
|
214
|
+
// Prevent logger errors from becoming unhandled rejections
|
|
215
|
+
}
|
|
196
216
|
}
|
|
197
217
|
});
|
|
198
218
|
}
|
|
@@ -218,6 +238,7 @@ export class Client {
|
|
|
218
238
|
async #initConnection() {
|
|
219
239
|
this.#setState(ConnectionState.CONNECTING);
|
|
220
240
|
this.#reconnectAttempts = 0;
|
|
241
|
+
this.#pairingAttempts = 0;
|
|
221
242
|
this.#pendingConnect = this.#createDeferred();
|
|
222
243
|
|
|
223
244
|
try {
|
|
@@ -296,9 +317,36 @@ export class Client {
|
|
|
296
317
|
|
|
297
318
|
async #handleConnectionUpdate({ connection, lastDisconnect, qr }) {
|
|
298
319
|
if (this.#isShuttingDown) return;
|
|
299
|
-
|
|
320
|
+
|
|
300
321
|
try {
|
|
301
322
|
if (qr) {
|
|
323
|
+
this.#pairingAttempts++;
|
|
324
|
+
|
|
325
|
+
if (this.#pairingAttempts > this.#maxPairingAttempts) {
|
|
326
|
+
this.#logger.warn(`[${this.#flag}] Max pairing attempts (${this.#maxPairingAttempts}) exceeded`);
|
|
327
|
+
|
|
328
|
+
if (typeof this.#onPairingAttemptsExceeded === 'function') {
|
|
329
|
+
try {
|
|
330
|
+
await this.#onPairingAttemptsExceeded({
|
|
331
|
+
client: this,
|
|
332
|
+
maxAttempts: this.#maxPairingAttempts,
|
|
333
|
+
});
|
|
334
|
+
} catch (cbErr) {
|
|
335
|
+
this.#logger.error(
|
|
336
|
+
cbErr,
|
|
337
|
+
`[${this.#flag}] Error in onPairingAttemptsExceeded callback`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
this.#cleanupSocket();
|
|
343
|
+
this.#clearReconnectTimer();
|
|
344
|
+
this.#unregister();
|
|
345
|
+
this.#pairingAttempts = 0;
|
|
346
|
+
this.#setState(ConnectionState.DISCONNECTED);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
302
350
|
this.#qr = qr;
|
|
303
351
|
await handleClientAuth({
|
|
304
352
|
qr: this.#qr,
|
|
@@ -307,6 +355,8 @@ export class Client {
|
|
|
307
355
|
isSync: this.#sync,
|
|
308
356
|
isSilent: this.#silent,
|
|
309
357
|
onPairing: this.#onPairing,
|
|
358
|
+
attempts: this.#pairingAttempts,
|
|
359
|
+
maxAttempts: this.#maxPairingAttempts,
|
|
310
360
|
logger: this.#logger,
|
|
311
361
|
authConfig: this.#authConfig,
|
|
312
362
|
setAuthConfig: (cfg) => {
|
|
@@ -341,6 +391,7 @@ export class Client {
|
|
|
341
391
|
|
|
342
392
|
this.#register();
|
|
343
393
|
this.#pairingCodeRequested = false;
|
|
394
|
+
this.#pairingAttempts = 0;
|
|
344
395
|
|
|
345
396
|
if (!this.#plugins || this.#plugins.destroyed) {
|
|
346
397
|
try {
|
|
@@ -398,6 +449,7 @@ export class Client {
|
|
|
398
449
|
});
|
|
399
450
|
this.#authConfig = null;
|
|
400
451
|
this.#pairingCodeRequested = false;
|
|
452
|
+
this.#pairingAttempts = 0;
|
|
401
453
|
this.#isNewSession = true;
|
|
402
454
|
this.#hasEverConnected = false;
|
|
403
455
|
this.#isFirstConnection = false;
|
|
@@ -437,14 +489,9 @@ export class Client {
|
|
|
437
489
|
async #scheduleReconnect(reason) {
|
|
438
490
|
this.#reconnectAttempts++;
|
|
439
491
|
|
|
440
|
-
if (this.#reconnectAttempts > this.#maxRetries) {
|
|
441
|
-
const err = new ConnectionError(
|
|
442
|
-
`Max reconnection attempts (${this.#maxRetries}) exceeded`,
|
|
443
|
-
{ recoverable: false }
|
|
444
|
-
);
|
|
492
|
+
if (this.#reconnectAttempts > this.#maxRetries) {
|
|
445
493
|
this.#setState(ConnectionState.DISCONNECTED);
|
|
446
|
-
this.#
|
|
447
|
-
this.#logger.error(err, `[${this.#flag}] ${err.message}`);
|
|
494
|
+
this.#logger.warn(`[${this.#flag}] Max reconnection attempts (${this.#maxRetries}) exceeded`);
|
|
448
495
|
return;
|
|
449
496
|
}
|
|
450
497
|
|
|
@@ -530,6 +577,7 @@ export class Client {
|
|
|
530
577
|
this.#isShuttingDown = false;
|
|
531
578
|
this.#hasConnectedOnce = false;
|
|
532
579
|
this.#pairingCodeRequested = false;
|
|
580
|
+
this.#pairingAttempts = 0;
|
|
533
581
|
}
|
|
534
582
|
|
|
535
583
|
async logout() {
|
|
@@ -540,6 +588,7 @@ export class Client {
|
|
|
540
588
|
this.#isNewSession = null;
|
|
541
589
|
this.#hasEverConnected = false;
|
|
542
590
|
this.#isFirstConnection = false;
|
|
591
|
+
this.#pairingAttempts = 0;
|
|
543
592
|
} catch (err) {
|
|
544
593
|
this.#logger.error(err, `[${this.#flag}] Logging out failed`);
|
|
545
594
|
}
|
package/src/client/constants.js
CHANGED
|
@@ -36,6 +36,7 @@ export const silentLogger = Object.freeze({
|
|
|
36
36
|
export const silentPino = pino({ level: 'silent' });
|
|
37
37
|
|
|
38
38
|
export const DEFAULT_MAX_RETRIES = 30;
|
|
39
|
+
export const DEFAULT_MAX_PAIRING_ATTEMPTS = 20;
|
|
39
40
|
|
|
40
41
|
export const defaultBackoff = (attempt) => Math.min(1000 * 2 ** (attempt - 1), 60_000);
|
|
41
42
|
|
package/src/client/qr.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import QRCode from 'qrcode';
|
|
2
|
+
|
|
3
|
+
export class QR {
|
|
4
|
+
#raw;
|
|
5
|
+
constructor(raw) {
|
|
6
|
+
this.#raw = typeof raw === 'string' ? raw : (raw?.raw ?? String(raw ?? ''));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
get raw() {
|
|
10
|
+
return this.#raw;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async print(options = {}) {
|
|
14
|
+
const terminalQR = await QRCode.toString(this.#raw, {
|
|
15
|
+
type: 'terminal',
|
|
16
|
+
small: true,
|
|
17
|
+
...options,
|
|
18
|
+
});
|
|
19
|
+
console.log(terminalQR);
|
|
20
|
+
return terminalQR;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async buffer(options = {}) {
|
|
24
|
+
return QRCode.toBuffer(this.#raw, {
|
|
25
|
+
type: 'png',
|
|
26
|
+
margin: 2,
|
|
27
|
+
scale: 4,
|
|
28
|
+
...options,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
toString() {
|
|
33
|
+
return this.#raw;
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/internals/index.js
CHANGED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as libsignalModule from 'libsignal';
|
|
2
|
+
import { logger } from './logger.js';
|
|
3
|
+
|
|
4
|
+
const libsignal = libsignalModule.default || libsignalModule;
|
|
5
|
+
|
|
6
|
+
if (libsignal?.SessionRecord?.prototype) {
|
|
7
|
+
libsignal.SessionRecord.prototype.closeSession = function (session) {
|
|
8
|
+
if (this.isClosed(session)) return;
|
|
9
|
+
session.indexInfo.closed = Date.now();
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
libsignal.SessionRecord.prototype.openSession = function (session) {
|
|
13
|
+
session.indexInfo.closed = -1;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
libsignal.SessionRecord.prototype.removeOldSessions = function () {
|
|
17
|
+
while (Object.keys(this.sessions).length > 40) {
|
|
18
|
+
let oldestKey;
|
|
19
|
+
let oldestSession;
|
|
20
|
+
for (const [key, session] of Object.entries(this.sessions)) {
|
|
21
|
+
if (
|
|
22
|
+
session.indexInfo.closed !== -1 &&
|
|
23
|
+
(!oldestSession || session.indexInfo.closed < oldestSession.indexInfo.closed)
|
|
24
|
+
) {
|
|
25
|
+
oldestKey = key;
|
|
26
|
+
oldestSession = session;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (oldestKey) {
|
|
30
|
+
delete this.sessions[oldestKey];
|
|
31
|
+
} else {
|
|
32
|
+
throw new Error('Corrupt sessions object');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
logger.debug('libsignal SessionRecord patches applied');
|
|
38
|
+
} else {
|
|
39
|
+
logger.warn('libsignal SessionRecord patches skipped — target prototypes not found');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (libsignal?.SessionBuilder?.prototype) {
|
|
43
|
+
libsignal.SessionBuilder.prototype.initIncoming = async function (record, message) {
|
|
44
|
+
const fqAddr = this.addr.toString();
|
|
45
|
+
if (!(await this.storage.isTrustedIdentity(fqAddr, message.identityKey))) {
|
|
46
|
+
throw new libsignal.UntrustedIdentityKeyError(this.addr.id, message.identityKey);
|
|
47
|
+
}
|
|
48
|
+
if (record.getSession(message.baseKey)) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const preKeyPair = await this.storage.loadPreKey(message.preKeyId);
|
|
52
|
+
if (message.preKeyId && !preKeyPair) {
|
|
53
|
+
throw new libsignal.PreKeyError('Invalid PreKey ID');
|
|
54
|
+
}
|
|
55
|
+
const signedPreKeyPair = await this.storage.loadSignedPreKey(message.signedPreKeyId);
|
|
56
|
+
if (!signedPreKeyPair) {
|
|
57
|
+
throw new libsignal.PreKeyError('Missing SignedPreKey');
|
|
58
|
+
}
|
|
59
|
+
const existingOpenSession = record.getOpenSession();
|
|
60
|
+
if (existingOpenSession) {
|
|
61
|
+
record.closeSession(existingOpenSession);
|
|
62
|
+
}
|
|
63
|
+
record.setSession(
|
|
64
|
+
await this.initSession(
|
|
65
|
+
false,
|
|
66
|
+
preKeyPair,
|
|
67
|
+
signedPreKeyPair,
|
|
68
|
+
message.identityKey,
|
|
69
|
+
message.baseKey,
|
|
70
|
+
undefined,
|
|
71
|
+
message.registrationId
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
return message.preKeyId;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
logger.debug('libsignal SessionBuilder patches applied');
|
|
78
|
+
} else {
|
|
79
|
+
logger.warn('libsignal SessionBuilder patches skipped — target prototypes not found');
|
|
80
|
+
}
|
package/src/internals/logger.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import pino from "pino";
|
|
2
|
-
|
|
3
|
-
const logger = pino({
|
|
4
|
-
transport: {
|
|
5
|
-
target: "pino-pretty",
|
|
6
|
-
options: {
|
|
7
|
-
colorize: true,
|
|
8
|
-
ignore: "pid,hostname",
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
logger.prompt = console.log.bind(console);
|
|
14
|
-
|
|
15
|
-
export { logger, pino };
|
|
1
|
+
import pino from "pino";
|
|
2
|
+
|
|
3
|
+
const logger = pino({
|
|
4
|
+
transport: {
|
|
5
|
+
target: "pino-pretty",
|
|
6
|
+
options: {
|
|
7
|
+
colorize: true,
|
|
8
|
+
ignore: "pid,hostname",
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
logger.prompt = console.log.bind(console);
|
|
14
|
+
|
|
15
|
+
export { logger, pino };
|