@shoru/kitten 0.0.6 → 0.1.0

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.
@@ -0,0 +1,4 @@
1
+ import { logger, getClient } from '../src/index.js';
2
+
3
+ logger.level = 'debug';
4
+ await getClient();
@@ -0,0 +1,33 @@
1
+ import { fileURLToPath } from 'url';
2
+ import path from 'path';
3
+
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = path.dirname(__filename);
6
+
7
+ const pluginsDir = path.resolve(__dirname, 'plugins');
8
+
9
+ const db = {
10
+ path: path.resolve(__dirname, '../db'),
11
+ compression: true,
12
+ mapSize: 2 * 1024 * 1024 * 1024, // 2 GB
13
+ maxReaders: 126,
14
+ noSync: false,
15
+ noMetaSync: false,
16
+ };
17
+
18
+ const plugins = {
19
+ dir: pluginsDir,
20
+ prefixes: ['.', '\\', '!'],
21
+ defaultEvent: 'messages.upsert',
22
+ hmr: {
23
+ enable: true,
24
+ debounce: 200,
25
+ debug: true
26
+ }
27
+ }
28
+
29
+ export default {
30
+ db,
31
+ plugins,
32
+ timeZone: 'Africa/Casablanca'
33
+ }
@@ -0,0 +1,7 @@
1
+ export const plugin = async (sock, ctx) => {
2
+ console.log(JSON.stringify({...ctx, raw: undefined}, null, 1));
3
+ sock.sendMessage(ctx.roomId, { text: ctx.body }, { quoted: ctx.raw });
4
+ }
5
+
6
+ plugin.events = ['messages.upsert'];
7
+ plugin.match = ['tst'];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shoru/kitten",
3
3
  "type": "module",
4
- "version": "0.0.6",
4
+ "version": "0.1.0",
5
5
  "description": "A powerful Node.js framework to simplify making WhatsApp Bots",
6
6
  "main": "src/index.js",
7
7
  "repository": {
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "scripts": {
11
11
  "start": "node --no-warnings src/index.js",
12
- "test": "echo \"Error: no test specified\" && exit 1"
12
+ "test": "node --no-warnings internal-test/index.js"
13
13
  },
14
14
  "imports": {
15
15
  "#utils.js": "./src/utils/index.js",
@@ -26,16 +26,16 @@
26
26
  "license": "MIT",
27
27
  "dependencies": {
28
28
  "@hapi/boom": "^10.0.1",
29
- "@inquirer/prompts": "^8.1.0",
30
- "@shoru/spindle": "^1.0.6",
29
+ "@inquirer/prompts": "^8.7.0",
30
+ "@shoru/spindle": "^1.0.7",
31
31
  "async-mutex": "^0.5.0",
32
- "baileys": "^7.0.0-rc.9",
33
- "chalk": "^5.6.2",
32
+ "baileys": "github:WhiskeySockets/Baileys",
33
+ "chalk": "^6.0.0",
34
34
  "chokidar": "^5.0.0",
35
- "cosmiconfig": "^9.0.0",
36
- "defu": "^6.1.4",
37
- "lmdb": "^3.4.4",
38
- "pino": "^10.1.0",
35
+ "cosmiconfig": "^10.0.0",
36
+ "defu": "^6.1.7",
37
+ "lmdb": "^3.5.6",
38
+ "pino": "^10.3.1",
39
39
  "pino-pretty": "^13.1.3",
40
40
  "qrcode-terminal": "^0.12.0"
41
41
  }
@@ -1,19 +1,32 @@
1
- import { makeWASocket } from "baileys";
1
+ import { makeWASocket, fetchLatestWaWebVersion } from "baileys";
2
2
  import { useLMDBAuthState } from "./lmdb-auth-state.js";
3
3
  import { getConfig } from "#internals.js";
4
4
 
5
5
  const config = await getConfig();
6
+ let cachedWaVersion = null;
7
+
8
+ const getWaVersion = async () => {
9
+ if (!cachedWaVersion) {
10
+ try {
11
+ const { version } = await fetchLatestWaWebVersion();
12
+ cachedWaVersion = version;
13
+ } catch {/* noop */}
14
+ }
15
+ return cachedWaVersion;
16
+ };
6
17
 
7
18
  export const initSession = async ({ socketConfig, id } = {}) => {
8
19
  try {
9
20
  const { state, saveCreds, session } = await useLMDBAuthState(id);
10
-
11
- const sock = makeWASocket({
21
+ const version = (await getWaVersion()) || config.socket?.version;
22
+
23
+ const sock = makeWASocket({
24
+ version: version || undefined,
12
25
  auth: state,
13
- ...config.socket,
26
+ ...config.socket,
14
27
  ...socketConfig
15
28
  });
16
-
29
+
17
30
  sock.ev.on("creds.update", saveCreds);
18
31
 
19
32
  return { sock, session };
@@ -1,177 +1,271 @@
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
- const id = (db.get(COUNTER_KEY) ?? 0) + 1;
21
- await db.put(COUNTER_KEY, id);
22
- return id;
23
- };
24
-
25
- const getSessionId = async (db, input) => {
26
- if (input == null) return genID(db);
27
- if (Number.isInteger(input) && input >= 0) return input;
28
- throw new TypeError(
29
- 'Invalid sessionId: expected null/undefined or non-negative integer'
30
- );
31
- };
32
-
33
- export async function useLMDBAuthState(inputSessionId) {
34
- const { db } = LMDBManager;
35
- const sessionId = await getSessionId(db, inputSessionId);
36
- const keys = keyBuilder(sessionId);
37
-
38
- const existingCreds = db.get(keys.creds);
39
- let creds;
40
-
41
- if (existingCreds != null) {
42
- creds = deserialize(existingCreds);
43
- } else {
44
- creds = initAuthCreds();
45
- await db.put(keys.creds, serialize(creds));
46
- await db.put(`${SESSION_PREFIX}${sessionId}`, true);
47
- }
48
-
49
- const writeCreds = async (credsData) => {
50
- await db.put(keys.creds, serialize(credsData));
51
- };
52
-
53
- const getKeys = (type, ids) => {
54
- if (!ids.length) return {};
55
-
56
- const result = {};
57
- for (const id of ids) {
58
- const dbKey = keys.forKey(type, id);
59
- const rawValue = db.get(dbKey);
60
-
61
- if (rawValue != null) {
62
- try {
63
- let parsed = deserialize(rawValue);
64
- if (type === "app-state-sync-key" && parsed) {
65
- parsed = proto.Message.AppStateSyncKeyData.fromObject(parsed);
66
- }
67
- result[id] = parsed;
68
- } catch (err) {
69
- logger.error(
70
- err,
71
- `[LMDBAuthState] Deserialize error: ${type}:${id}`
72
- );
73
- result[id] = null;
74
- }
75
- } else {
76
- result[id] = null;
77
- }
78
- }
79
- return result;
80
- };
81
-
82
- const setKeys = async (data) => {
83
- const writes = [];
84
-
85
- for (const [category, categoryData] of Object.entries(data)) {
86
- if (!categoryData) continue;
87
- for (const [id, value] of Object.entries(categoryData)) {
88
- const key = keys.forKey(category, id);
89
- if (value != null) {
90
- writes.push(db.put(key, serialize(value)));
91
- } else {
92
- writes.push(db.remove(key));
93
- }
94
- }
95
- }
96
-
97
- if (writes.length > 0) {
98
- await Promise.all(writes);
99
- }
100
- };
101
-
102
- const clearKeys = async () => {
103
- let count = 0;
104
- const writes = [];
105
-
106
- for (const { key } of db.getRange({
107
- start: keys.sessionPrefix,
108
- end: `${keys.sessionPrefix}\xFF`,
109
- })) {
110
- if (key !== keys.creds) {
111
- writes.push(db.remove(key));
112
- count++;
113
- }
114
- }
115
-
116
- if (writes.length > 0) {
117
- await Promise.all(writes);
118
- }
119
- logger.debug(`[LMDBAuthState] Cleared ${count} keys`);
120
- };
121
-
122
- const deleteSession = async () => {
123
- const writes = [];
124
-
125
- for (const { key } of db.getRange({
126
- start: keys.sessionPrefix,
127
- end: `${keys.sessionPrefix}\xFF`,
128
- })) {
129
- writes.push(db.remove(key));
130
- }
131
- writes.push(db.remove(`${SESSION_PREFIX}${sessionId}`));
132
-
133
- await Promise.all(writes);
134
- logger.debug(`[LMDBAuthState] Deleted session ${sessionId}`);
135
- };
136
-
137
- return {
138
- state: {
139
- creds,
140
- keys: {
141
- get: getKeys,
142
- set: setKeys,
143
- clear: clearKeys,
144
- },
145
- },
146
- saveCreds: () => writeCreds(creds),
147
- session: {
148
- delete: deleteSession,
149
- clear: clearKeys,
150
- id: sessionId,
151
- },
152
- };
153
- }
154
-
155
- export function listSessions() {
156
- if (!LMDBManager.isOpen) return [];
157
-
158
- const { db } = LMDBManager;
159
- const sessions = [];
160
-
161
- for (const { key } of db.getRange({
162
- start: SESSION_PREFIX,
163
- end: `${SESSION_PREFIX}\xFF`,
164
- })) {
165
- const id = parseInt(key.slice(SESSION_PREFIX.length), 10);
166
- if (!isNaN(id)) sessions.push(id);
167
- }
168
-
169
- return sessions.sort((a, b) => a - b);
170
- }
171
-
172
- export function sessionExists(sessionId) {
173
- if (!Number.isInteger(sessionId) || sessionId < 0 || !LMDBManager.isOpen)
174
- return false;
175
- const { db } = LMDBManager;
176
- return db.get(`${SESSION_PREFIX}${sessionId}`) != null;
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
+ const current = db.get(COUNTER_KEY);
21
+ let id;
22
+ if (current == null) {
23
+ const existing = listSessions();
24
+ id = existing.length > 0 ? Math.max(...existing) + 1 : 0;
25
+ } else {
26
+ id = current + 1;
27
+ }
28
+ await db.put(COUNTER_KEY, id);
29
+ return id;
30
+ };
31
+
32
+ const getSessionId = async (db, input) => {
33
+ if (input == null) {
34
+ const existing = listSessions();
35
+ if (existing.length > 0) {
36
+ return existing[0];
37
+ }
38
+ return genID(db);
39
+ }
40
+ if (Number.isInteger(input) && input >= 0) return input;
41
+ throw new TypeError(
42
+ 'Invalid sessionId: expected null/undefined or non-negative integer'
43
+ );
44
+ };
45
+
46
+ export async function useLMDBAuthState(inputSessionId) {
47
+ const { db } = LMDBManager;
48
+ const sessionId = await getSessionId(db, inputSessionId);
49
+ const keys = keyBuilder(sessionId);
50
+
51
+ const existingCreds = db.get(keys.creds);
52
+ let creds = null;
53
+ let isNew = false;
54
+
55
+ if (existingCreds != null) {
56
+ try {
57
+ creds = deserialize(existingCreds);
58
+ } catch {
59
+ logger.warn(`[LMDBAuthState] Corrupted credentials for session ${sessionId}, purging session`);
60
+ creds = null;
61
+ }
62
+
63
+ if (creds && (creds.registered || creds.me)) {
64
+ await db.put(`${SESSION_PREFIX}${sessionId}`, true);
65
+ } else {
66
+ // Credentials not registered or corrupted: clean up old session keys and start fresh
67
+ for (const { key } of db.getRange({
68
+ start: keys.sessionPrefix,
69
+ end: `${keys.sessionPrefix}\xFF`,
70
+ })) {
71
+ await db.remove(key);
72
+ }
73
+ await db.remove(`${SESSION_PREFIX}${sessionId}`);
74
+ creds = initAuthCreds();
75
+ await db.put(keys.creds, serialize(creds));
76
+ isNew = true;
77
+ }
78
+ } else {
79
+ creds = initAuthCreds();
80
+ await db.put(keys.creds, serialize(creds));
81
+ isNew = true;
82
+ }
83
+
84
+ const writeCreds = async (credsData) => {
85
+ await db.put(keys.creds, serialize(credsData));
86
+ if (credsData?.registered || credsData?.me) {
87
+ await db.put(`${SESSION_PREFIX}${sessionId}`, true);
88
+ }
89
+ };
90
+
91
+ const getKeys = (type, ids) => {
92
+ if (!ids.length) return {};
93
+
94
+ const result = {};
95
+ for (const id of ids) {
96
+ const dbKey = keys.forKey(type, id);
97
+ const rawValue = db.get(dbKey);
98
+
99
+ if (rawValue != null) {
100
+ try {
101
+ let parsed = deserialize(rawValue);
102
+ if (type === "app-state-sync-key" && parsed) {
103
+ parsed = proto.Message.AppStateSyncKeyData.fromObject(parsed);
104
+ }
105
+ result[id] = parsed;
106
+ } catch (err) {
107
+ logger.error(
108
+ err,
109
+ `[LMDBAuthState] Deserialize error: ${type}:${id}`
110
+ );
111
+ result[id] = null;
112
+ }
113
+ } else {
114
+ result[id] = null;
115
+ }
116
+ }
117
+ return result;
118
+ };
119
+
120
+ const setKeys = async (data) => {
121
+ const writes = [];
122
+
123
+ for (const [category, categoryData] of Object.entries(data)) {
124
+ if (!categoryData) continue;
125
+ for (const [id, value] of Object.entries(categoryData)) {
126
+ const key = keys.forKey(category, id);
127
+ if (value != null) {
128
+ writes.push(db.put(key, serialize(value)));
129
+ } else {
130
+ writes.push(db.remove(key));
131
+ }
132
+ }
133
+ }
134
+
135
+ if (writes.length > 0) {
136
+ await Promise.all(writes);
137
+ }
138
+ };
139
+
140
+ const clearKeys = async () => {
141
+ let count = 0;
142
+ const writes = [];
143
+
144
+ for (const { key } of db.getRange({
145
+ start: keys.sessionPrefix,
146
+ end: `${keys.sessionPrefix}\xFF`,
147
+ })) {
148
+ if (key !== keys.creds) {
149
+ writes.push(db.remove(key));
150
+ count++;
151
+ }
152
+ }
153
+
154
+ if (writes.length > 0) {
155
+ await Promise.all(writes);
156
+ }
157
+ logger.debug(`[LMDBAuthState] Cleared ${count} keys`);
158
+ };
159
+
160
+ const deleteSession = async () => {
161
+ const writes = [];
162
+
163
+ for (const { key } of db.getRange({
164
+ start: keys.sessionPrefix,
165
+ end: `${keys.sessionPrefix}\xFF`,
166
+ })) {
167
+ writes.push(db.remove(key));
168
+ }
169
+ writes.push(db.remove(`${SESSION_PREFIX}${sessionId}`));
170
+
171
+ await Promise.all(writes);
172
+ logger.debug(`[LMDBAuthState] Deleted session ${sessionId}`);
173
+ };
174
+
175
+ return {
176
+ state: {
177
+ creds,
178
+ keys: {
179
+ get: getKeys,
180
+ set: setKeys,
181
+ clear: clearKeys,
182
+ },
183
+ },
184
+ saveCreds: () => writeCreds(creds),
185
+ session: {
186
+ delete: deleteSession,
187
+ clear: clearKeys,
188
+ id: sessionId,
189
+ isNew,
190
+ },
191
+ };
192
+ }
193
+
194
+ export function listSessions() {
195
+ const { db } = LMDBManager;
196
+ if (!db) return [];
197
+
198
+ const sessions = new Set();
199
+
200
+ for (const { key } of db.getRange({
201
+ start: SESSION_PREFIX,
202
+ end: `${SESSION_PREFIX}\xFF`,
203
+ })) {
204
+ const id = parseInt(key.slice(SESSION_PREFIX.length), 10);
205
+ if (!isNaN(id)) {
206
+ const rawCreds = db.get(`${KEY_PREFIX}:${id}:creds`);
207
+ if (rawCreds != null) {
208
+ try {
209
+ const creds = deserialize(rawCreds);
210
+ if (creds?.registered || creds?.me) {
211
+ sessions.add(id);
212
+ } else {
213
+ db.remove(key);
214
+ }
215
+ } catch {
216
+ db.remove(key);
217
+ }
218
+ } else {
219
+ db.remove(key);
220
+ }
221
+ }
222
+ }
223
+
224
+ // Also check for existing credentials as fallback
225
+ for (const { key, value } of db.getRange({
226
+ start: `${KEY_PREFIX}:`,
227
+ end: `${KEY_PREFIX}:\xFF`,
228
+ })) {
229
+ if (key.endsWith(':creds')) {
230
+ const parts = key.split(':');
231
+ if (parts.length === 3 && parts[0] === KEY_PREFIX && parts[2] === 'creds') {
232
+ const id = parseInt(parts[1], 10);
233
+ if (!isNaN(id) && !sessions.has(id)) {
234
+ try {
235
+ const creds = deserialize(value);
236
+ if (creds?.registered || creds?.me) {
237
+ sessions.add(id);
238
+ db.put(`${SESSION_PREFIX}${id}`, true);
239
+ }
240
+ } catch {
241
+ /* ignore corrupted creds */
242
+ }
243
+ }
244
+ }
245
+ }
246
+ }
247
+
248
+ return [...sessions].sort((a, b) => a - b);
249
+ }
250
+
251
+ export function sessionExists(sessionId) {
252
+ if (!Number.isInteger(sessionId) || sessionId < 0) return false;
253
+ const { db } = LMDBManager;
254
+ if (!db) return false;
255
+
256
+ const rawCreds = db.get(`${KEY_PREFIX}:${sessionId}:creds`);
257
+ if (rawCreds != null) {
258
+ try {
259
+ const creds = deserialize(rawCreds);
260
+ if (creds?.registered || creds?.me) {
261
+ db.put(`${SESSION_PREFIX}${sessionId}`, true);
262
+ return true;
263
+ }
264
+ } catch {
265
+ db.remove(`${SESSION_PREFIX}${sessionId}`);
266
+ return false;
267
+ }
268
+ }
269
+ db.remove(`${SESSION_PREFIX}${sessionId}`);
270
+ return false;
177
271
  }
@@ -0,0 +1,97 @@
1
+ import qrcode from 'qrcode-terminal';
2
+ import chalk from 'chalk';
3
+ import { ConnectionError } from './errors.js';
4
+ import { ClientRegistry } from './registry.js';
5
+ import { getConnectionConfig } from './getConnectionConfig.js';
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
+ export function formatPairingCode(code) {
14
+ const formatted = code.match(/.{1,4}/g)?.join(' ') ?? code;
15
+ return `\n${chalk.green('> Your OTP Code: ')}${chalk.bold(formatted)}`;
16
+ }
17
+
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
+ export async function handleClientAuth({
36
+ qr,
37
+ rawSock,
38
+ flag,
39
+ isSync,
40
+ isSilent,
41
+ onPairing,
42
+ logger,
43
+ authConfig,
44
+ setAuthConfig,
45
+ pairingCodeRequested,
46
+ setPairingCodeRequested,
47
+ onSyncAuthAbort,
48
+ }) {
49
+ if (isSync) {
50
+ const err = new ConnectionError('Authentication required for sync connection', {
51
+ recoverable: false,
52
+ });
53
+ logger.debug(`[${flag}] Sync connection requires auth, aborting`);
54
+ onSyncAuthAbort(err);
55
+ return;
56
+ }
57
+
58
+ if (ClientRegistry.isConfiguring) return;
59
+
60
+ if (typeof onPairing === 'function') {
61
+ const requestPairingCode = rawSock?.requestPairingCode?.bind(rawSock);
62
+ await onPairing({ qr, requestPairingCode });
63
+ return;
64
+ }
65
+
66
+ if (isSilent) {
67
+ return;
68
+ }
69
+
70
+ let currentAuthConfig = authConfig;
71
+ if (!currentAuthConfig) {
72
+ ClientRegistry.isConfiguring = true;
73
+ try {
74
+ currentAuthConfig = await getConnectionConfig();
75
+ setAuthConfig(currentAuthConfig);
76
+ } finally {
77
+ ClientRegistry.isConfiguring = false;
78
+ }
79
+ }
80
+
81
+ if (currentAuthConfig.type === 'pn') {
82
+ if (!pairingCodeRequested && rawSock) {
83
+ try {
84
+ setPairingCodeRequested(true);
85
+ const code = await rawSock.requestPairingCode(currentAuthConfig.pn);
86
+ logger.prompt(formatPairingCode(code));
87
+ } catch (err) {
88
+ setPairingCodeRequested(false);
89
+ logger.error(err, `[${flag}] Failed to request pairing code`);
90
+ }
91
+ }
92
+ } else {
93
+ qrcode.generate(qr, { small: true });
94
+ process.stdout.write('\n');
95
+ }
96
+ }
97
+