@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 +24 -0
- package/package.json +39 -0
- package/src/auth/index.js +2 -0
- package/src/auth/init-session.js +21 -0
- package/src/auth/lmdb-auth-state.js +171 -0
- package/src/client/client.js +418 -0
- package/src/client/getConnectionConfig.js +48 -0
- package/src/client/index.js +2 -0
- package/src/config/default.js +26 -0
- package/src/index.js +4 -0
- package/src/internals/config.js +40 -0
- package/src/internals/index.js +4 -0
- package/src/internals/lmdb-manager.js +52 -0
- package/src/internals/logger.js +15 -0
- package/src/internals/spinner.js +3 -0
- package/src/utils/buffer-json.js +11 -0
- package/src/utils/index.js +3 -0
- package/src/utils/pause-spinner.js +12 -0
- package/src/utils/retry.js +29 -0
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
|
+

|
|
11
|
+
[](https://www.npmjs.com/package/@shoru/kitten)
|
|
12
|
+
[](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,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,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,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,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,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,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));
|