@shoru/kitten 0.0.1-beta → 0.0.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shoru/kitten",
3
3
  "type": "module",
4
- "version": "0.0.1-beta",
4
+ "version": "0.0.2",
5
5
  "description": "A powerful Node.js framework to simplify making WhatsApp Bots",
6
6
  "main": "src/index.js",
7
7
  "repository": {
@@ -1,6 +1,8 @@
1
1
  import { makeWASocket } from "baileys";
2
2
  import { useLMDBAuthState } from "./lmdb-auth-state.js";
3
- import { config } from "#internals.js";
3
+ import { getConfig } from "#internals.js";
4
+
5
+ const config = await getConfig();
4
6
 
5
7
  export const initSession = async ({ socketConfig, id } = {}) => {
6
8
  try {
@@ -17,11 +17,9 @@ const keyBuilder = (sessionId) => {
17
17
  };
18
18
 
19
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
- });
20
+ const id = (db.get(COUNTER_KEY) ?? 0) + 1;
21
+ await db.put(COUNTER_KEY, id);
22
+ return id;
25
23
  };
26
24
 
27
25
  const getSessionId = async (db, input) => {
@@ -37,6 +35,17 @@ export async function useLMDBAuthState(inputSessionId) {
37
35
  const sessionId = await getSessionId(db, inputSessionId);
38
36
  const keys = keyBuilder(sessionId);
39
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
+
40
49
  const writeCreds = async (credsData) => {
41
50
  await db.put(keys.creds, serialize(credsData));
42
51
  };
@@ -44,90 +53,87 @@ export async function useLMDBAuthState(inputSessionId) {
44
53
  const getKeys = (type, ids) => {
45
54
  if (!ids.length) return {};
46
55
 
47
- const keyList = ids.map((id) => keys.forKey(type, id));
48
- const values = db.getMany(keyList);
49
-
50
56
  const result = {};
51
- for (let i = 0; i < ids.length; i++) {
52
- const rawValue = values[i];
57
+ for (const id of ids) {
58
+ const dbKey = keys.forKey(type, id);
59
+ const rawValue = db.get(dbKey);
53
60
 
54
- if (rawValue) {
61
+ if (rawValue != null) {
55
62
  try {
56
63
  let parsed = deserialize(rawValue);
57
64
  if (type === "app-state-sync-key" && parsed) {
58
65
  parsed = proto.Message.AppStateSyncKeyData.fromObject(parsed);
59
66
  }
60
- result[ids[i]] = parsed;
67
+ result[id] = parsed;
61
68
  } catch (err) {
62
69
  logger.error(
63
70
  err,
64
- `[LMDBAuthState] Deserialize error: ${type}:${ids[i]}`
71
+ `[LMDBAuthState] Deserialize error: ${type}:${id}`
65
72
  );
66
- db.remove(keys.forKey(type, ids[i]));
73
+ result[id] = null;
67
74
  }
75
+ } else {
76
+ result[id] = null;
68
77
  }
69
78
  }
70
79
  return result;
71
80
  };
72
81
 
73
82
  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
- }
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));
84
93
  }
85
94
  }
86
- });
95
+ }
96
+
97
+ if (writes.length > 0) {
98
+ await Promise.all(writes);
99
+ }
87
100
  };
88
101
 
89
102
  const clearKeys = async () => {
90
103
  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
- }
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++;
100
113
  }
101
- });
114
+ }
115
+
116
+ if (writes.length > 0) {
117
+ await Promise.all(writes);
118
+ }
102
119
  logger.debug(`[LMDBAuthState] Cleared ${count} keys`);
103
120
  };
104
121
 
105
122
  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
-
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);
116
134
  logger.debug(`[LMDBAuthState] Deleted session ${sessionId}`);
117
135
  };
118
136
 
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
137
  return {
132
138
  state: {
133
139
  creds,
@@ -276,12 +276,6 @@ export class Client {
276
276
  this.id = session.id;
277
277
  this.#flag = `CLIENT-${session.id}`;
278
278
 
279
- try {
280
- this.#plugins = await pluginManager(this.sock);
281
- } catch (err) {
282
- this.#logger.error(err, `[${this.#flag}] Failed to initialize plugins`);
283
- }
284
-
285
279
  this.sock.ev.on('connection.update', (update) => {
286
280
  this.#handleConnectionUpdate(update);
287
281
  });
@@ -310,6 +304,14 @@ export class Client {
310
304
 
311
305
  this.#register();
312
306
 
307
+ if (!this.#plugins || this.#plugins.destroyed) {
308
+ try {
309
+ this.#plugins = await pluginManager(this.sock);
310
+ } catch (err) {
311
+ this.#logger.error(err, `[${this.#flag}] Failed to initialize plugins`);
312
+ }
313
+ }
314
+
313
315
  const attempts = this.#reconnectAttempts;
314
316
  this.#reconnectAttempts = 0;
315
317
 
@@ -22,6 +22,7 @@ const db = {
22
22
 
23
23
  const plugins = {
24
24
  dir: 'plugins',
25
+ prefixes: ['.', '\\', '!'],
25
26
  defaultEvent: 'messages.upsert',
26
27
  hmr: {
27
28
  enable: false,
@@ -34,6 +35,5 @@ export default {
34
35
  db,
35
36
  socket,
36
37
  plugins,
37
- timeZone: 'Africa/Casablanca',
38
- prefixes: ['!', '.']
38
+ timeZone: 'Africa/Casablanca'
39
39
  }
@@ -1,11 +1,14 @@
1
1
  import { formatMessage } from './format-message.js';
2
2
 
3
- export const formatter = async (wa, event, eventName) => {
4
- eventName ??= 'massages.upsert';
3
+ export const formatter = (wa, event, eventName) => {
4
+ eventName ??= 'messages.upsert';
5
5
 
6
6
  switch (eventName) {
7
- case 'massages.upsert':
8
- return formatMessage(wa, event);
7
+ case 'messages.upsert':
8
+ return {
9
+ ...formatMessage(wa, event),
10
+ event: eventName
11
+ };
9
12
  default: return event;
10
13
  }
11
14
  }
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
+ export * from "#internals.js";
2
+ export * from "#utils.js";
1
3
  export * from "#auth.js";
2
4
  export * from "#client.js";
3
- export * from "#internals.js";
4
- export * from "#utils.js";
5
+ export * from "#formatter.js"
@@ -35,6 +35,11 @@ export const loadConfig = async () => {
35
35
  return defu(userConfig, defaultConfig);
36
36
  };
37
37
 
38
- const configObject = await loadConfig()
38
+ let cachedConfig = null;
39
39
 
40
- export const config = Object.freeze(configObject);
40
+ export const getConfig = async () => {
41
+ if (!cachedConfig) {
42
+ cachedConfig = Object.freeze(await loadConfig());
43
+ }
44
+ return cachedConfig;
45
+ };
@@ -1,5 +1,5 @@
1
- export * from './logger.js';
2
- export * from './lmdb-manager.js';
3
1
  export * from './config.js';
2
+ export * from './logger.js';
4
3
  export * from './spinner.js';
4
+ export * from './lmdb-manager.js';
5
5
  export * from './plugin-manager.js'
@@ -1,6 +1,8 @@
1
1
  import { open } from 'lmdb';
2
2
  import { logger } from './logger.js';
3
- import { config } from './config.js';
3
+ import { getConfig } from './config.js';
4
+
5
+ const config = await getConfig();
4
6
 
5
7
  class LMDBDatabaseManager {
6
8
  #db = null;
@@ -1,11 +1,13 @@
1
1
  import { Mutex } from 'async-mutex';
2
- import { config, logger } from '#internals.js';
2
+ import { getConfig, logger } from '#internals.js';
3
3
  import { watch } from 'chokidar';
4
4
  import fs from 'fs/promises';
5
5
  import path from 'path';
6
6
  import { pathToFileURL } from 'url';
7
7
  import { formatter } from '#formatter.js';
8
8
 
9
+ const config = await getConfig();
10
+
9
11
  const {
10
12
  dir,
11
13
  defaultEvent,
@@ -17,7 +19,7 @@ const {
17
19
  }
18
20
  } = config.plugins;
19
21
 
20
- const PLUGIN_DIR = path.resolve(import.meta.dirname ?? '.', dir);
22
+ const PLUGIN_DIR = path.join(process.cwd(), dir);
21
23
 
22
24
  const EVENTS = new Set([
23
25
  'messaging-history.set', 'chats.upsert', 'chats.update', 'chats.delete',
@@ -55,10 +57,8 @@ export class PluginManager {
55
57
  ?? PluginManager.#fileLocks.set(filePath, new Mutex()).get(filePath);
56
58
  }
57
59
 
58
- static #handleError(context, err, level = 'error') {
60
+ static #handleError(context, err) {
59
61
  if (isDebug) {
60
- logger.error(err, context);
61
- } else if (level === 'error') {
62
62
  logger.warn(`${context} ${err?.message ?? 'Unknown error'}`);
63
63
  }
64
64
  }
@@ -138,14 +138,15 @@ export class PluginManager {
138
138
 
139
139
  let loaded = 0;
140
140
  let failed = 0;
141
-
141
+
142
142
  for (let i = 0; i < results.length; i++) {
143
143
  const result = results[i];
144
144
  if (result.status === 'fulfilled') {
145
145
  loaded += result.value?.size ?? 0;
146
146
  } else {
147
147
  failed++;
148
- PluginManager.#handleError(`[PluginManager] Failed to load ${files[i].path}:`, result.reason, 'warn');
148
+ const rel = path.relative(PLUGIN_DIR, files[i].path);
149
+ PluginManager.#handleError(`[PluginManager:${rel}] Failed to load:`, result.reason);
149
150
  }
150
151
  }
151
152
 
@@ -198,7 +199,7 @@ export class PluginManager {
198
199
  return null;
199
200
  }
200
201
 
201
- static #compile(match, prefixOpt) {
202
+ static #compile(match, prefixOpt = PREFIXES) {
202
203
  if (!Array.isArray(match) || !match.length) return null;
203
204
 
204
205
  const strings = match.filter(m => typeof m === 'string').map(s => s.toLowerCase());
@@ -234,12 +235,12 @@ export class PluginManager {
234
235
 
235
236
  if (cmd) {
236
237
  if (matchers.set.has(cmd)) {
237
- return { value: cmd, prefix, captures: null };
238
+ return { match: cmd, prefix };
238
239
  }
239
240
 
240
241
  for (const s of matchers.strings) {
241
242
  if (cmd.length > s.length && cmd.startsWith(s)) {
242
- return { value: s, prefix, captures: null };
243
+ return { match: s, prefix };
243
244
  }
244
245
  }
245
246
  }
@@ -249,7 +250,7 @@ export class PluginManager {
249
250
  for (const re of matchers.regexes) {
250
251
  re.lastIndex = 0;
251
252
  const m = re.exec(body);
252
- if (m) return { value: re, prefix: null, captures: m };
253
+ if (m) return { match: m, prefix: null };
253
254
  }
254
255
 
255
256
  return null;
@@ -288,7 +289,7 @@ export class PluginManager {
288
289
  if (!active.has(event)) {
289
290
  this.#sock.ev.off(event, handler);
290
291
  this.#handlers.delete(event);
291
- if (isDebug) logger.debug(`[Events] ${event}`);
292
+ if (isDebug) logger.debug(`[Events] (-) ${event}`);
292
293
  }
293
294
  }
294
295
 
@@ -297,7 +298,7 @@ export class PluginManager {
297
298
  const handler = this.#createHandler(event);
298
299
  this.#sock.ev.on(event, handler);
299
300
  this.#handlers.set(event, handler);
300
- if (isDebug) logger.debug(`[Events] ${event}`);
301
+ if (isDebug) logger.debug(`[Events] (+) ${event}`);
301
302
  }
302
303
  }
303
304
  }
@@ -317,11 +318,11 @@ export class PluginManager {
317
318
  'messages.upsert': ({ messages, type }) => {
318
319
  if (type !== 'notify') return;
319
320
  for (const msg of messages) {
320
- if (!msg?.key?.remoteJid || msg.key.remoteJid === 'status@broadcast' || msg.key.fromMe) continue;
321
+ if (!msg?.key?.remoteJid || msg.key.remoteJid === 'status@broadcast') continue;
321
322
  try {
322
323
  dispatch(formatter(sock, msg, event));
323
324
  } catch (err) {
324
- PluginManager.#handleError('[PluginManager] Format error:', err, 'warn');
325
+ PluginManager.#handleError('[PluginManager] Format error:', err);
325
326
  }
326
327
  }
327
328
  },
@@ -386,7 +387,7 @@ export class PluginManager {
386
387
  .on('add', p => PluginManager.#debounce(p, 'add'))
387
388
  .on('change', p => PluginManager.#debounce(p, 'change'))
388
389
  .on('unlink', p => PluginManager.#debounce(p, 'unlink'))
389
- .on('error', e => PluginManager.#handleError('[Watcher]', e, 'warn'));
390
+ .on('error', e => PluginManager.#handleError('[Watcher]', e));
390
391
  }
391
392
 
392
393
  static #debounce(filePath, type) {
@@ -402,14 +403,12 @@ export class PluginManager {
402
403
 
403
404
  static async #hmr(filePath, type) {
404
405
  const rel = path.relative(PLUGIN_DIR, filePath);
405
- const ts = isDebug ? new Date().toISOString().slice(11, 19) : '';
406
- const tag = isDebug ? `[HMR:${ts}]` : '[HMR]';
407
406
 
408
407
  try {
409
408
  await PluginManager.#getLock(filePath).runExclusive(async () => {
410
409
  if (type === 'unlink') {
411
410
  const n = PluginManager.#unloadFile(filePath);
412
- logger.info(`${tag} Unloaded: ${rel} (${n})`);
411
+ logger.info(`[HMR] Unloaded: ${rel} (${n})`);
413
412
  } else {
414
413
  const parent = PluginManager.#getParent(path.dirname(filePath));
415
414
  const plugins = await PluginManager.#loadFile(filePath, parent, false);
@@ -421,13 +420,13 @@ export class PluginManager {
421
420
  PluginManager.#register(id, plugin);
422
421
  }
423
422
 
424
- logger.info(`${tag} ${type === 'add' ? 'Added' : 'Reloaded'}: ${rel} (${plugins.size})`);
423
+ logger.info(`[HMR] ${type === 'add' ? 'Added' : 'Reloaded'}: ${rel} (${plugins.size})`);
425
424
  }
426
425
 
427
426
  PluginManager.#syncAll();
428
427
  });
429
428
  } catch (err) {
430
- PluginManager.#handleError(`${tag} Failed: ${rel}`, err);
429
+ PluginManager.#handleError(`[HMR:${rel}] Failed:`, err);
431
430
  } finally {
432
431
  if (type === 'unlink') {
433
432
  const lock = PluginManager.#fileLocks.get(filePath);
@@ -2,10 +2,25 @@ import { BufferJSON } from 'baileys';
2
2
 
3
3
  export const serialize = (data) => {
4
4
  if (data == null) return null;
5
- return JSON.stringify(data, BufferJSON.replacer);
6
- }
5
+ try {
6
+ return JSON.stringify(data, BufferJSON.replacer);
7
+ } catch (err) {
8
+ throw new Error(`Serialization failed: ${err.message}`, { cause: err });
9
+ }
10
+ };
7
11
 
8
12
  export const deserialize = (json) => {
9
13
  if (json == null) return null;
10
- return JSON.parse(json, BufferJSON.reviver);
14
+
15
+ const str = typeof json === 'string'
16
+ ? json
17
+ : Buffer.isBuffer(json)
18
+ ? json.toString('utf8')
19
+ : String(json);
20
+
21
+ try {
22
+ return JSON.parse(str, BufferJSON.reviver);
23
+ } catch (err) {
24
+ throw new Error(`Deserialization failed: ${err.message}`, { cause: err });
25
+ }
11
26
  };
@@ -1,6 +1,6 @@
1
- import { loadConfig } from '#internals.js';
1
+ import { getConfig } from '#internals.js';
2
2
 
3
- const { timeZone } = await loadConfig();
3
+ const { timeZone } = await getConfig();
4
4
 
5
5
  export const getTimeString = (timestamp, TIME_ZONE = timeZone) => {
6
6
  const date = new Date(timestamp * 1000);