@shoru/kitten 0.0.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 ADDED
@@ -0,0 +1,24 @@
1
+ <div align="left">
2
+
3
+ <img src="https://files.catbox.moe/j7dpni.png" align="right" width="200" alt="Kitten Logo">
4
+
5
+ <a name="-kitten"></a>
6
+ # **Kitten Framework**
7
+
8
+ ### A powerful `Node.js` framework to simplify making WhatsApp Bots. built on top of the [Baileys](https://github.com/WhiskeySockets/Baileys) library. 😺
9
+
10
+ ![State](https://img.shields.io/badge/State-BETA-5a67d8?style=for-the-badge&logo=activity)
11
+ [![npm](https://img.shields.io/npm/v/@shoru/kitten.svg?style=for-the-badge)](https://www.npmjs.com/package/@shoru/kitten)
12
+ [![License](https://img.shields.io/github/license/Mangaka-bot/kitten-wa?style=for-the-badge)](https://github.com/Mangaka-bot/kitten-wa/blob/main/LICENSE)
13
+
14
+ </div>
15
+
16
+ ---
17
+
18
+ <br clear="left"/>
19
+
20
+ <div align="center">
21
+
22
+
23
+ **Made with ❤️ for Whatsapp community**
24
+ </div>
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@shoru/kitten",
3
+ "type": "module",
4
+ "version": "0.0.1",
5
+ "description": "A powerful Node.js framework to simplify making WhatsApp Bots",
6
+ "main": "src/index.js",
7
+ "repository": {
8
+ "url": "git+https://github.com/Mangaka-bot/kitten-wa.git"
9
+ },
10
+ "scripts": {
11
+ "start": "node --no-warnings src/index.js",
12
+ "test": "echo \"Error: no test specified\" && exit 1"
13
+ },
14
+ "imports": {
15
+ "#utils.js": "./src/utils/index.js",
16
+ "#config.js": "./src/config/default.js",
17
+ "#auth.js": "./src/auth/index.js",
18
+ "#internals.js": "./src/internals/index.js",
19
+ "#client.js": "./src/client/index.js"
20
+ },
21
+ "exports": {
22
+ ".": "./src/index.js"
23
+ },
24
+ "author": "Aymane Shoru",
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "@hapi/boom": "^10.0.1",
28
+ "@inquirer/prompts": "^8.1.0",
29
+ "@shoru/spindle": "^1.0.6",
30
+ "baileys": "^7.0.0-rc.9",
31
+ "chalk": "^5.6.2",
32
+ "cosmiconfig": "^9.0.0",
33
+ "defu": "^6.1.4",
34
+ "lmdb": "^3.4.4",
35
+ "pino": "^10.1.0",
36
+ "pino-pretty": "^13.1.3",
37
+ "qrcode-terminal": "^0.12.0"
38
+ }
39
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./lmdb-auth-state.js";
2
+ export * from "./init-session.js";
@@ -0,0 +1,21 @@
1
+ import { makeWASocket } from "baileys";
2
+ import { useLMDBAuthState } from "./lmdb-auth-state.js";
3
+ import { config } from "#internals.js";
4
+
5
+ export const initSession = async ({ socketConfig, id } = {}) => {
6
+ try {
7
+ const { state, saveCreds, session } = await useLMDBAuthState(id);
8
+
9
+ const sock = makeWASocket({
10
+ auth: state,
11
+ ...config.socket,
12
+ ...socketConfig
13
+ });
14
+
15
+ sock.ev.on("creds.update", saveCreds);
16
+
17
+ return { sock, session };
18
+ } catch (err) {
19
+ throw new Error(`[INIT_SESSION] Failed to initialize session: ${err.message}`, { cause: err });
20
+ }
21
+ };
@@ -0,0 +1,171 @@
1
+ import { proto, initAuthCreds } from "baileys";
2
+ import { LMDBManager, logger } from "#internals.js";
3
+ import { serialize, deserialize } from "#utils.js";
4
+
5
+ const KEY_PREFIX = "baileys";
6
+ const COUNTER_KEY = `${KEY_PREFIX}:__meta__:counter`;
7
+ const SESSION_PREFIX = `${KEY_PREFIX}:__sessions__:`;
8
+
9
+ const keyBuilder = (sessionId) => {
10
+ const prefix = `${KEY_PREFIX}:${sessionId}:`;
11
+ return {
12
+ sessionId,
13
+ sessionPrefix: prefix,
14
+ creds: `${prefix}creds`,
15
+ forKey: (type, id) => `${prefix}${type}:${id}`,
16
+ };
17
+ };
18
+
19
+ const genID = async (db) => {
20
+ return db.transaction(() => {
21
+ const id = (db.get(COUNTER_KEY) ?? 0) + 1;
22
+ db.put(COUNTER_KEY, id);
23
+ return id;
24
+ });
25
+ };
26
+
27
+ const getSessionId = async (db, input) => {
28
+ if (input == null) return genID(db);
29
+ if (Number.isInteger(input) && input >= 0) return input;
30
+ throw new TypeError(
31
+ 'Invalid sessionId: expected null/undefined or non-negative integer'
32
+ );
33
+ };
34
+
35
+ export async function useLMDBAuthState(inputSessionId) {
36
+ const { db } = LMDBManager;
37
+ const sessionId = await getSessionId(db, inputSessionId);
38
+ const keys = keyBuilder(sessionId);
39
+
40
+ const writeCreds = async (credsData) => {
41
+ await db.put(keys.creds, serialize(credsData));
42
+ };
43
+
44
+ const getKeys = (type, ids) => {
45
+ if (!ids.length) return {};
46
+
47
+ const keyList = ids.map((id) => keys.forKey(type, id));
48
+ const values = db.getMany(keyList);
49
+
50
+ const result = {};
51
+ for (let i = 0; i < ids.length; i++) {
52
+ const rawValue = values[i];
53
+
54
+ if (rawValue) {
55
+ try {
56
+ let parsed = deserialize(rawValue);
57
+ if (type === "app-state-sync-key" && parsed) {
58
+ parsed = proto.Message.AppStateSyncKeyData.fromObject(parsed);
59
+ }
60
+ result[ids[i]] = parsed;
61
+ } catch (err) {
62
+ logger.error(
63
+ err,
64
+ `[LMDBAuthState] Deserialize error: ${type}:${ids[i]}`
65
+ );
66
+ db.remove(keys.forKey(type, ids[i]));
67
+ }
68
+ }
69
+ }
70
+ return result;
71
+ };
72
+
73
+ const setKeys = async (data) => {
74
+ await db.batch(() => {
75
+ for (const [category, categoryData] of Object.entries(data)) {
76
+ if (!categoryData) continue;
77
+ for (const [id, value] of Object.entries(categoryData)) {
78
+ const key = keys.forKey(category, id);
79
+ if (value != null) {
80
+ db.put(key, serialize(value));
81
+ } else {
82
+ db.remove(key);
83
+ }
84
+ }
85
+ }
86
+ });
87
+ };
88
+
89
+ const clearKeys = async () => {
90
+ let count = 0;
91
+ await db.batch(() => {
92
+ for (const { key } of db.getRange({
93
+ start: keys.sessionPrefix,
94
+ end: `${keys.sessionPrefix}\xFF`,
95
+ })) {
96
+ if (key !== keys.creds) {
97
+ db.remove(key);
98
+ count++;
99
+ }
100
+ }
101
+ });
102
+ logger.debug(`[LMDBAuthState] Cleared ${count} keys`);
103
+ };
104
+
105
+ const deleteSession = async () => {
106
+ await db.batch(() => {
107
+ for (const { key } of db.getRange({
108
+ start: keys.sessionPrefix,
109
+ end: `${keys.sessionPrefix}\xFF`,
110
+ })) {
111
+ db.remove(key);
112
+ }
113
+ db.remove(`${SESSION_PREFIX}${sessionId}`);
114
+ });
115
+
116
+ logger.debug(`[LMDBAuthState] Deleted session ${sessionId}`);
117
+ };
118
+
119
+ const creds = await db.transaction(() => {
120
+ const existing = db.get(keys.creds);
121
+ if (existing != null) {
122
+ return deserialize(existing);
123
+ }
124
+
125
+ const newCreds = initAuthCreds();
126
+ db.put(keys.creds, serialize(newCreds));
127
+ db.put(`${SESSION_PREFIX}${sessionId}`, true);
128
+ return newCreds;
129
+ });
130
+
131
+ return {
132
+ state: {
133
+ creds,
134
+ keys: {
135
+ get: getKeys,
136
+ set: setKeys,
137
+ clear: clearKeys,
138
+ },
139
+ },
140
+ saveCreds: () => writeCreds(creds),
141
+ session: {
142
+ delete: deleteSession,
143
+ clear: clearKeys,
144
+ id: sessionId,
145
+ },
146
+ };
147
+ }
148
+
149
+ export function listSessions() {
150
+ if (!LMDBManager.isOpen) return [];
151
+
152
+ const { db } = LMDBManager;
153
+ const sessions = [];
154
+
155
+ for (const { key } of db.getRange({
156
+ start: SESSION_PREFIX,
157
+ end: `${SESSION_PREFIX}\xFF`,
158
+ })) {
159
+ const id = parseInt(key.slice(SESSION_PREFIX.length), 10);
160
+ if (!isNaN(id)) sessions.push(id);
161
+ }
162
+
163
+ return sessions.sort((a, b) => a - b);
164
+ }
165
+
166
+ export function sessionExists(sessionId) {
167
+ if (!Number.isInteger(sessionId) || sessionId < 0 || !LMDBManager.isOpen)
168
+ return false;
169
+ const { db } = LMDBManager;
170
+ return db.get(`${SESSION_PREFIX}${sessionId}`) != null;
171
+ }
@@ -0,0 +1,418 @@
1
+ import { DisconnectReason } from "baileys";
2
+ import { Boom } from "@hapi/boom";
3
+ import qrcode from "qrcode-terminal";
4
+ import chalk from "chalk";
5
+ import { logger } from "#internals.js";
6
+ import { initSession } from "#auth.js";
7
+ import { pauseSpinner } from "#utils.js";
8
+ import { getConnectionConfig } from "./getConnectionConfig.js";
9
+
10
+ export const ConnectionState = Object.freeze({
11
+ DISCONNECTED: 'disconnected',
12
+ CONNECTING: 'connecting',
13
+ CONNECTED: 'connected',
14
+ RECONNECTING: 'reconnecting',
15
+ });
16
+
17
+ class ConnectionError extends Error {
18
+ constructor(message, { statusCode, recoverable = true } = {}) {
19
+ super(message);
20
+ this.name = "ConnectionError";
21
+ this.statusCode = statusCode;
22
+ this.recoverable = recoverable;
23
+ }
24
+ }
25
+
26
+ const DISCONNECT_HANDLERS = new Map([
27
+ [DisconnectReason.connectionClosed, { message: "Connection closed", recoverable: true }],
28
+ [DisconnectReason.restartRequired, { message: "QR Scanned", recoverable: true }],
29
+ [DisconnectReason.timedOut, { message: "Connection timed out", recoverable: true }],
30
+ [DisconnectReason.connectionLost, { message: "Connection lost", recoverable: true }],
31
+ [DisconnectReason.unavailableService, { message: "Service unavailable", recoverable: true }],
32
+ [DisconnectReason.loggedOut, { message: "Session logged out", recoverable: true, deleteSession: true }],
33
+ [DisconnectReason.forbidden, { message: "Account banned", recoverable: false, deleteSession: true }],
34
+ [405, { message: "Not logged in", recoverable: true, deleteSession: true }],
35
+ ]);
36
+
37
+ export class Client {
38
+ sock = null;
39
+ session = null;
40
+ id = null;
41
+
42
+ #flag = "";
43
+ #qr = null;
44
+ #state = ConnectionState.DISCONNECTED;
45
+ #cancelWait
46
+ #isConfiguring = false;
47
+ #hasConnectedOnce = false;
48
+
49
+ #socketConfig = null;
50
+ #authConfig = null;
51
+
52
+ #reconnectAttempts = 0;
53
+ #reconnectTimer = null;
54
+ #isShuttingDown = false;
55
+
56
+ #pendingConnect = null;
57
+
58
+ #maxRetries;
59
+ #backoff;
60
+
61
+ #onPairing;
62
+ #onConnect;
63
+ #onReconnect;
64
+ #onDisconnect;
65
+ #onStateChange;
66
+
67
+ constructor(options = {}) {
68
+ const {
69
+ id,
70
+ maxRetries = 30,
71
+ backoff = (attempt) => Math.min(1000 * Math.pow(2, attempt - 1), 60_000),
72
+ onPairing = null,
73
+ onConnect = null,
74
+ onReconnect = null,
75
+ onDisconnect = null,
76
+ onStateChange = null,
77
+ socketConfig = {}
78
+ } = options;
79
+
80
+ this.id = id;
81
+ this.#socketConfig = socketConfig;
82
+ this.#maxRetries = maxRetries;
83
+ this.#backoff = backoff;
84
+ this.#onPairing = onPairing;
85
+ this.#onConnect = onConnect;
86
+ this.#onReconnect = onReconnect;
87
+ this.#onDisconnect = onDisconnect;
88
+ this.#onStateChange = onStateChange;
89
+ }
90
+
91
+ get state() {
92
+ return this.#state;
93
+ }
94
+
95
+ get isConnected() {
96
+ return this.#state === ConnectionState.CONNECTED;
97
+ }
98
+
99
+ get reconnectAttempts() {
100
+ return this.#reconnectAttempts;
101
+ }
102
+
103
+ // State Management
104
+
105
+ #setState(newState) {
106
+ const oldState = this.#state;
107
+ if (oldState === newState) return;
108
+
109
+ this.#state = newState;
110
+ this.#emit('stateChange', { oldState, newState });
111
+ }
112
+
113
+ #emit(event, data = {}) {
114
+ const callbacks = {
115
+ connect: this.#onConnect,
116
+ reconnect: this.#onReconnect,
117
+ disconnect: this.#onDisconnect,
118
+ stateChange: this.#onStateChange,
119
+ };
120
+
121
+ const callback = callbacks[event];
122
+ if (typeof callback !== 'function') return;
123
+
124
+ queueMicrotask(() => {
125
+ try {
126
+ callback({ ...data, client: this });
127
+ } catch (err) {
128
+ logger.error(err, `[${this.#flag}] Error in ${event} callback`);
129
+ }
130
+ });
131
+ }
132
+
133
+ // Connection Management
134
+
135
+ async connect() {
136
+ if (this.#isShuttingDown) {
137
+ throw new Error(`[${this.#flag}] Client is shutting down`);
138
+ }
139
+
140
+ if (this.#state === ConnectionState.CONNECTED) {
141
+ return { sock: this.sock, session: this.session, id: this.id };
142
+ }
143
+
144
+ if (this.#pendingConnect) {
145
+ return this.#pendingConnect.promise;
146
+ }
147
+
148
+ return this.#initConnection();
149
+ }
150
+
151
+ async #initConnection() {
152
+ this.#setState(ConnectionState.CONNECTING);
153
+ this.#reconnectAttempts = 0;
154
+
155
+ this.#pendingConnect = this.#createDeferred();
156
+
157
+ try {
158
+ await this.#createSocket();
159
+ } catch (err) {
160
+ this.#setState(ConnectionState.DISCONNECTED);
161
+ this.#resolvePending(null, err);
162
+ }
163
+
164
+ return this.#pendingConnect.promise;
165
+ }
166
+
167
+ #createDeferred() {
168
+ let resolve, reject;
169
+ const promise = new Promise((res, rej) => {
170
+ resolve = res;
171
+ reject = rej;
172
+ });
173
+ return { promise, resolve, reject };
174
+ }
175
+
176
+ #resolvePending(value, error = null) {
177
+ if (!this.#pendingConnect) return;
178
+
179
+ const { resolve, reject } = this.#pendingConnect;
180
+ this.#pendingConnect = null;
181
+
182
+ if (error) {
183
+ reject(error);
184
+ } else {
185
+ resolve(value);
186
+ }
187
+ }
188
+
189
+ async #createSocket() {
190
+ this.#cleanupSocket();
191
+
192
+ const { sock, session } = await initSession({
193
+ socketConfig: this.#socketConfig,
194
+ id: this.id,
195
+ });
196
+
197
+ this.sock = sock;
198
+ this.session = session;
199
+ this.id = session.id;
200
+ this.#flag = `CLIENT-${session.id}`;
201
+
202
+ this.sock.ev.on("connection.update", (update) => {
203
+ this.#handleConnectionUpdate(update);
204
+ });
205
+ }
206
+
207
+ async #handleConnectionUpdate({ connection, lastDisconnect, qr }) {
208
+ if (this.#isShuttingDown) return;
209
+
210
+ try {
211
+ if (qr) {
212
+ await this.#handleAuth(qr);
213
+ } else if (connection === "open") {
214
+ this.#onConnectionOpen();
215
+ } else if (connection === "close") {
216
+ await this.#onConnectionClose(lastDisconnect);
217
+ }
218
+ } catch (err) {
219
+ logger.error(err, `[${this.#flag}] Error in connection update handler`);
220
+ this.#resolvePending(null, err);
221
+ }
222
+ }
223
+
224
+ #onConnectionOpen() {
225
+ const wasReconnecting = this.#state === ConnectionState.RECONNECTING;
226
+ this.#setState(ConnectionState.CONNECTED);
227
+
228
+ const attempts = this.#reconnectAttempts;
229
+ this.#reconnectAttempts = 0;
230
+
231
+ if (wasReconnecting) {
232
+ this.#emit('reconnect', { attempts });
233
+ logger.debug(`[${this.#flag}] Reconnected after ${attempts} attempt(s)`);
234
+ } else {
235
+ this.#hasConnectedOnce = true;
236
+ this.#emit('connect');
237
+ logger.debug(`[${this.#flag}] Connected successfully`);
238
+ this.#resolvePending({ sock: this.sock, session: this.session });
239
+ }
240
+ }
241
+
242
+ async #onConnectionClose(lastDisconnect) {
243
+ const disconnectInfo = this.#parseDisconnectReason(lastDisconnect);
244
+ const { message, statusCode, recoverable, deleteSession } = disconnectInfo;
245
+
246
+ if (message === "QR Scanned" && !this.#onPairing) {
247
+ console.clear();
248
+ }
249
+
250
+ const level = recoverable ? 'debug' : 'warn';
251
+
252
+ logger[level](`[${this.#flag}] Disconnected: ${message} (code: ${statusCode})`);
253
+
254
+ if (this.#hasConnectedOnce) {
255
+ this.#emit('disconnect', { message, statusCode, recoverable });
256
+ }
257
+
258
+ if (deleteSession) {
259
+ await this.session?.delete().catch((err) => {
260
+ logger.error(err, `[${this.#flag}] Failed to delete session`);
261
+ });
262
+ }
263
+
264
+ if (!recoverable || this.#isShuttingDown) {
265
+ this.#setState(ConnectionState.DISCONNECTED);
266
+ this.#resolvePending(null, new ConnectionError(message, { statusCode, recoverable }));
267
+ return;
268
+ }
269
+
270
+ await this.#scheduleReconnect(message);
271
+ }
272
+
273
+ // Reconnection Logic
274
+
275
+ async #scheduleReconnect(reason) {
276
+ this.#reconnectAttempts++;
277
+
278
+ if (this.#reconnectAttempts > this.#maxRetries) {
279
+ const err = new ConnectionError(
280
+ `Max reconnection attempts (${this.#maxRetries}) exceeded`,
281
+ { recoverable: false }
282
+ );
283
+ this.#setState(ConnectionState.DISCONNECTED);
284
+ this.#resolvePending(null, err);
285
+ logger.error(err, `[${this.#flag}] ${err.message}`);
286
+ return;
287
+ }
288
+
289
+ if (this.#hasConnectedOnce) {
290
+ this.#setState(ConnectionState.RECONNECTING);
291
+ }
292
+
293
+ const delay = this.#backoff(this.#reconnectAttempts);
294
+
295
+ const retriesInfo = this.#maxRetries !== Infinity ? `(${this.#reconnectAttempts}/${this.#maxRetries})` : '';
296
+
297
+ logger.debug(`[${this.#flag}] ${reason}. Reconnecting in ${delay} ms`);
298
+
299
+ const cancelled = await this.#wait(delay);
300
+ if (cancelled || this.#isShuttingDown) return;
301
+
302
+ logger.debug(`[${this.#flag}] Executing reconnect attempt ${retriesInfo}`);
303
+
304
+ try {
305
+ await this.#createSocket();
306
+ } catch (err) {
307
+ logger.error(err, `[${this.#flag}] Socket creation failed during reconnect`);
308
+ }
309
+ }
310
+
311
+ #wait(ms) {
312
+ return new Promise((resolve) => {
313
+ this.#reconnectTimer = setTimeout(() => {
314
+ this.#reconnectTimer = null;
315
+ resolve(false);
316
+ }, ms);
317
+
318
+ this.#cancelWait = () => resolve(true);
319
+ });
320
+ }
321
+
322
+ #parseDisconnectReason(lastDisconnect) {
323
+ const boom = new Boom(lastDisconnect?.error);
324
+ const statusCode = boom?.output?.statusCode;
325
+ const handler = DISCONNECT_HANDLERS.get(statusCode);
326
+
327
+ if (!handler) {
328
+ return {
329
+ message: `Unknown disconnect reason (code: ${statusCode ?? 'unknown'})`,
330
+ statusCode,
331
+ recoverable: true,
332
+ deleteSession: false,
333
+ };
334
+ }
335
+
336
+ return { ...handler, statusCode };
337
+ }
338
+
339
+ // Authentication
340
+
341
+ async #handleAuth(qr) {
342
+ this.#qr = qr;
343
+ if (this.#isConfiguring) return;
344
+
345
+ if (typeof this.#onPairing === 'function') {
346
+ const requestPairingCode = this.sock?.requestPairingCode?.bind(this.sock);
347
+ await this.#onPairing({ qr: this.#qr, requestPairingCode });
348
+ return;
349
+ }
350
+
351
+ this.#isConfiguring = true;
352
+ this.#authConfig ??= await pauseSpinner(getConnectionConfig);
353
+ this.#isConfiguring = false;
354
+
355
+ if (this.#authConfig.type === "pn") {
356
+ const code = await this.sock.requestPairingCode(this.#authConfig.pn);
357
+ logger.prompt(this.#formatPairingCode(code));
358
+ } else {
359
+ qrcode.generate(this.#qr, { small: true });
360
+ process.stdout.write("\n");
361
+ }
362
+ }
363
+
364
+ #formatPairingCode(code) {
365
+ const formatted = code.match(/.{1,4}/g)?.join(" ") ?? code;
366
+ return `\n${chalk.green("> Your OTP Code: ")}${chalk.bold(formatted)}`;
367
+ }
368
+
369
+ // Cleanup & Shutdown
370
+
371
+ #cleanupSocket() {
372
+ if (!this.sock) return;
373
+
374
+ try {
375
+ this.sock.ev.removeAllListeners();
376
+ } catch { /* noop */ };
377
+
378
+ this.sock = null;
379
+ }
380
+
381
+ #clearReconnectTimer() {
382
+ if (this.#reconnectTimer) {
383
+ clearTimeout(this.#reconnectTimer);
384
+ this.#reconnectTimer = null;
385
+ this.#cancelWait?.();
386
+ }
387
+ }
388
+
389
+ async disconnect() {
390
+ if (this.#isShuttingDown) return;
391
+ this.#isShuttingDown = true;
392
+
393
+ this.#clearReconnectTimer();
394
+ this.#resolvePending(null, new Error('Client disconnected'));
395
+
396
+ this.#cleanupSocket();
397
+ this.#setState(ConnectionState.DISCONNECTED);
398
+
399
+ this.#isShuttingDown = false;
400
+ this.#hasConnectedOnce = false;
401
+ }
402
+
403
+ async logout() {
404
+ try {
405
+ await this.sock?.logout();
406
+ await this.disconnect();
407
+ await this.session?.delete();
408
+ } catch (err) {
409
+ logger.error(err, `[${this.#flag}] Logging out failed: ${err.message}`)
410
+ }
411
+ }
412
+ }
413
+
414
+ export const getClient = async (options) => {
415
+ const client = new Client(options);
416
+ await client.connect();
417
+ return client;
418
+ };
@@ -0,0 +1,48 @@
1
+ import { select, input } from "@inquirer/prompts";
2
+ import chalk from "chalk";
3
+ import { logger } from "#internals.js";
4
+
5
+ export const getConnectionConfig = async () => {
6
+ try {
7
+ process.stdout.write("\n");
8
+ const type = await select({
9
+ message: "How would you like to connect?",
10
+ choices: [
11
+ {
12
+ name: " ⛶ QR Code",
13
+ value: "qr",
14
+ description: "Generate a secure code to scan"
15
+ },
16
+ {
17
+ name: " 🔑 Phone Number",
18
+ value: "pn",
19
+ description: "Receive a One Time Password"
20
+ }
21
+ ]
22
+ });
23
+
24
+ if (type === "pn") {
25
+ console.log(chalk.yellow("\n 🔑 Phone Number Selected\n"));
26
+
27
+ const pn = await input({
28
+ message: "Enter your phone number:",
29
+ validate: (value) => {
30
+ const digitsOnly = /^\d+$/.test(value);
31
+ if (!digitsOnly) return "Digits only (+1 (234) 567-8901 → 12345678901)";
32
+ const correctLength = /^.{7,15}$/.test(value);
33
+ if (!correctLength) return "Phone number length should be from 7 to 15 digits";
34
+ return true;
35
+ },
36
+ transformer: (value) => chalk.cyan(value)
37
+ });
38
+
39
+ return { type, pn }
40
+ } else {
41
+ logger.prompt(chalk.cyan("\n ⛶ QR Code Selected\n"));
42
+ return { type };
43
+ }
44
+ } catch {
45
+ logger.prompt(chalk.red("\nOperation cancelled"));
46
+ process.exit(0);
47
+ }
48
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./getConnectionConfig.js"
2
+ export * from "./client.js"
@@ -0,0 +1,26 @@
1
+ import { Browsers } from "baileys";
2
+ import pino from "pino";
3
+
4
+ const socket = {
5
+ browser: Browsers.ubuntu('Chrome'),
6
+ markOnlineOnConnect: false,
7
+ syncFullHistory: false,
8
+ generateHighQualityLinkPreview: true,
9
+ shouldIgnoreJid: () => false,
10
+ shouldSyncHistoryMessage: () => false,
11
+ logger: pino({ level: "silent" }),
12
+ }
13
+
14
+ const db = {
15
+ path: './db',
16
+ compression: true,
17
+ mapSize: 2 * 1024 * 1024 * 1024, // 2 GB
18
+ maxReaders: 126,
19
+ noSync: false,
20
+ noMetaSync: false,
21
+ };
22
+
23
+ export default {
24
+ db,
25
+ socket
26
+ }
package/src/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./auth/index.js";
2
+ export * from "./client/index.js";
3
+ export * from "./internals/index.js";
4
+ export * from "./utils/index.js";
@@ -0,0 +1,40 @@
1
+ import { cosmiconfig } from 'cosmiconfig';
2
+ import { defu } from 'defu';
3
+
4
+ const loadDefaultConfig = async () => {
5
+ try {
6
+ const module = await import('../config/default.js');
7
+ const defaultConfig = module?.default ?? module;
8
+
9
+ if (typeof defaultConfig !== 'object' || defaultConfig === null || Array.isArray(defaultConfig)) {
10
+ throw new Error(`[INTERNAL_CONFIG] default config must export an object`);
11
+ }
12
+
13
+ return defaultConfig;
14
+ } catch (err) {
15
+ throw new Error(`[INTERNAL_CONFIG] Error loading default config: ${err.message}`, { cause: err });
16
+ }
17
+ };
18
+
19
+ const loadUserConfig = async () => {
20
+ try {
21
+ const explorer = cosmiconfig('kittenwa');
22
+ const result = await explorer.search();
23
+ return result?.config ?? {};
24
+ } catch (err) {
25
+ throw new Error(`[USER_CONFIG] Error loading user config: ${err.message}`, { cause: err });
26
+ }
27
+ };
28
+
29
+ export const loadConfig = async () => {
30
+ const [userConfig, defaultConfig] = await Promise.all([
31
+ loadUserConfig(),
32
+ loadDefaultConfig()
33
+ ]);
34
+
35
+ return defu(userConfig, defaultConfig);
36
+ };
37
+
38
+ const configObject = await loadConfig()
39
+
40
+ export const config = Object.freeze(configObject);
@@ -0,0 +1,4 @@
1
+ export * from "./logger.js";
2
+ export * from "./lmdb-manager.js";
3
+ export * from "./config.js"
4
+ export * from "./spinner.js"
@@ -0,0 +1,52 @@
1
+ import { open } from 'lmdb';
2
+ import { logger } from './logger.js';
3
+ import { config } from './config.js';
4
+
5
+ class LMDBDatabaseManager {
6
+ #db = null;
7
+ #config = config.db;
8
+ #isClosing = false;
9
+
10
+ constructor(config) {
11
+ this.#config = { ...this.#config, ...config };
12
+ }
13
+
14
+ get db() {
15
+ if (this.#isClosing) {
16
+ throw new Error('[LMDBManager] Database is closing');
17
+ }
18
+
19
+ if (!this.#db) {
20
+ this.#db = open(this.#config);
21
+ }
22
+
23
+ return this.#db;
24
+ }
25
+
26
+ get isOpen() {
27
+ return this.#db !== null && !this.#isClosing;
28
+ }
29
+
30
+ get config() {
31
+ return Object.freeze({ ...this.#config });
32
+ }
33
+
34
+ async close() {
35
+ if (!this.#db || this.#isClosing) return;
36
+
37
+ this.#isClosing = true;
38
+
39
+ try {
40
+ await this.#db.flushed;
41
+ await this.#db.close();
42
+ } catch (err) {
43
+ logger.error(err, '[LMDBManager] Error closing database');
44
+ throw err;
45
+ } finally {
46
+ this.#db = null;
47
+ this.#isClosing = false;
48
+ }
49
+ }
50
+ }
51
+
52
+ export const LMDBManager = new LMDBDatabaseManager();
@@ -0,0 +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 };
@@ -0,0 +1,3 @@
1
+ import { spindle } from "@shoru/spindle";
2
+
3
+ export const spinner = spindle();
@@ -0,0 +1,11 @@
1
+ import { BufferJSON } from 'baileys';
2
+
3
+ export const serialize = (data) => {
4
+ if (data == null) return null;
5
+ return JSON.stringify(data, BufferJSON.replacer);
6
+ }
7
+
8
+ export const deserialize = (json) => {
9
+ if (json == null) return null;
10
+ return JSON.parse(json, BufferJSON.reviver);
11
+ };
@@ -0,0 +1,3 @@
1
+ export * from "./buffer-json.js";
2
+ export * from "./retry.js";
3
+ export * from "./pause-spinner.js"
@@ -0,0 +1,12 @@
1
+ import { spinner } from "#internals.js";
2
+
3
+ export const pauseSpinner = async (action) => {
4
+ if (!spinner.isSpinning) return action();
5
+
6
+ spinner.stop();
7
+ try {
8
+ return await action();
9
+ } finally {
10
+ spinner.start();
11
+ }
12
+ };
@@ -0,0 +1,29 @@
1
+ export async function retry(fn, options = {}) {
2
+ const {
3
+ maxAttempts = 20,
4
+ backoff = (attempt) => Math.min(500 * attempt, 30_000),
5
+ shouldRetry = () => true,
6
+ onRetry = () => {},
7
+ } = options;
8
+
9
+ let lastError;
10
+
11
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
12
+ try {
13
+ return await fn(attempt);
14
+ } catch (err) {
15
+ lastError = err;
16
+
17
+ if (attempt === maxAttempts || !shouldRetry(err, attempt)) {
18
+ throw err;
19
+ }
20
+
21
+ onRetry(err, attempt);
22
+ await wait(backoff(attempt));
23
+ }
24
+ }
25
+
26
+ throw lastError;
27
+ }
28
+
29
+ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));