@shoru/kitten 0.0.6 → 0.1.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.
@@ -1,85 +1,122 @@
1
- import { formatter } from '#formatter.js';
2
- import { getBucket } from './registry.js';
3
- import { test } from './matcher.js';
4
- import { handleError } from './handle-error.js';
5
-
6
- export function createHandler(event, sock, isDestroyed, execute) {
7
- const bucket = getBucket(event);
8
-
9
- const dispatch = (ctx) => {
10
- if (isDestroyed() || !ctx) return;
11
-
12
- // Execute auto-triggered plugins
13
- for (const [id, plugin] of bucket.auto) {
14
- execute(id, plugin, sock, ctx, event, null);
15
- }
16
-
17
- // Execute pattern-matched plugins
18
- if (bucket.match.size && ctx.body) {
19
- for (const [id, plugin] of bucket.match) {
20
- const matchers = plugin._meta?.matchers;
21
- if (!matchers) continue;
22
-
23
- const result = test(matchers, ctx.body);
24
- if (result) {
25
- execute(id, plugin, sock, ctx, event, result);
26
- }
27
- }
28
- }
29
- };
30
-
31
- switch (event) {
32
- case 'messages.upsert':
33
- return ({ messages, type }) => {
34
- if (type !== 'notify') return;
35
-
36
- for (const msg of messages) {
37
- if (!msg?.key?.remoteJid || msg.key.remoteJid === 'status@broadcast') {
38
- continue;
39
- }
40
-
41
- try {
42
- dispatch(formatter(sock, msg, event));
43
- } catch (err) {
44
- handleError('[PluginManager] Format error:', err);
45
- }
46
- }
47
- };
48
-
49
- case 'messages.update':
50
- return (updates) => {
51
- for (const { key, update } of updates) {
52
- if (key?.remoteJid) {
53
- dispatch({ key, update, jid: key.remoteJid });
54
- }
55
- }
56
- };
57
-
58
- case 'messages.reaction':
59
- return (reactions) => {
60
- for (const { key, reaction } of reactions) {
61
- if (key?.remoteJid) {
62
- dispatch({
63
- key,
64
- reaction,
65
- jid: key.remoteJid,
66
- emoji: reaction?.text
67
- });
68
- }
69
- }
70
- };
71
-
72
- case 'group-participants.update':
73
- case 'connection.update':
74
- return (update) => dispatch(update);
75
-
76
- case 'creds.update':
77
- return (creds) => dispatch({ creds });
78
-
79
- case 'call':
80
- return (calls) => calls.forEach(c => dispatch(c));
81
-
82
- default:
83
- return (data) => dispatch({ data });
84
- }
1
+ import { formatter } from '#formatter.js';
2
+ import { getBucket } from './registry.js';
3
+ import { handleError } from './handle-error.js';
4
+
5
+ export function createHandler(event, sock, isDestroyed, execute) {
6
+ const bucket = getBucket(event);
7
+
8
+ const dispatch = (ctx) => {
9
+ if (isDestroyed() || !ctx) return;
10
+
11
+ // 1. Execute auto-triggered plugins
12
+ if (bucket.auto.size > 0) {
13
+ for (const [id, plugin] of bucket.auto) {
14
+ execute(id, plugin, sock, ctx, event, null);
15
+ }
16
+ }
17
+
18
+ // 2. Execute pattern-matched plugins
19
+ if (bucket.allMatch.size > 0 && ctx.body) {
20
+ const trimmed = ctx.body.trim();
21
+ if (!trimmed) return;
22
+ const lower = trimmed.toLowerCase();
23
+ const spaceIdx = lower.indexOf(' ');
24
+ const firstToken = spaceIdx < 0 ? lower : lower.slice(0, spaceIdx);
25
+
26
+ const executedIds = new Set();
27
+
28
+ // Fast O(1) command lookup (e.g. "!tst")
29
+ const matchedCommands = bucket.commandMap.get(firstToken);
30
+ if (matchedCommands) {
31
+ for (const item of matchedCommands) {
32
+ executedIds.add(item.id);
33
+ execute(item.id, item.plugin, sock, ctx, event, { match: item.match, prefix: item.prefix });
34
+ }
35
+ }
36
+
37
+ // Fast prefixless command lookup (plugins with prefix: false)
38
+ if (bucket.prefixlessMap.size > 0) {
39
+ const matchedPrefixless = bucket.prefixlessMap.get(firstToken);
40
+ if (matchedPrefixless) {
41
+ for (const item of matchedPrefixless) {
42
+ if (!executedIds.has(item.id)) {
43
+ executedIds.add(item.id);
44
+ execute(item.id, item.plugin, sock, ctx, event, { match: item.match, prefix: null });
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ // Execute regex plugins if any exist
51
+ if (bucket.regexList.size > 0) {
52
+ for (const [id, item] of bucket.regexList) {
53
+ if (executedIds.has(id)) continue;
54
+ for (const re of item.regexes) {
55
+ re.lastIndex = 0;
56
+ const m = re.exec(ctx.body);
57
+ if (m) {
58
+ executedIds.add(id);
59
+ execute(id, item.plugin, sock, ctx, event, { match: m, prefix: null });
60
+ break;
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+ };
67
+
68
+ switch (event) {
69
+ case 'messages.upsert':
70
+ return ({ messages, type }) => {
71
+ if (type !== 'notify') return;
72
+
73
+ for (const msg of messages) {
74
+ if (!msg?.key?.remoteJid || msg.key.remoteJid === 'status@broadcast') {
75
+ continue;
76
+ }
77
+
78
+ try {
79
+ dispatch(formatter(sock, msg, event));
80
+ } catch (err) {
81
+ handleError('[PluginManager] Format error:', err);
82
+ }
83
+ }
84
+ };
85
+
86
+ case 'messages.update':
87
+ return (updates) => {
88
+ for (const { key, update } of updates) {
89
+ if (key?.remoteJid) {
90
+ dispatch({ key, update, jid: key.remoteJid });
91
+ }
92
+ }
93
+ };
94
+
95
+ case 'messages.reaction':
96
+ return (reactions) => {
97
+ for (const { key, reaction } of reactions) {
98
+ if (key?.remoteJid) {
99
+ dispatch({
100
+ key,
101
+ reaction,
102
+ jid: key.remoteJid,
103
+ emoji: reaction?.text,
104
+ });
105
+ }
106
+ }
107
+ };
108
+
109
+ case 'group-participants.update':
110
+ case 'connection.update':
111
+ return (update) => dispatch(update);
112
+
113
+ case 'creds.update':
114
+ return (creds) => dispatch({ creds });
115
+
116
+ case 'call':
117
+ return (calls) => calls.forEach((c) => dispatch(c));
118
+
119
+ default:
120
+ return (data) => dispatch({ data });
121
+ }
85
122
  }
@@ -11,18 +11,20 @@ import { setPlugin, registerToBuckets } from './registry.js';
11
11
  const fileLocks = new Map();
12
12
 
13
13
  export function getLock(filePath) {
14
- let lock = fileLocks.get(filePath);
14
+ const resolved = path.resolve(filePath);
15
+ let lock = fileLocks.get(resolved);
15
16
  if (!lock) {
16
17
  lock = new Mutex();
17
- fileLocks.set(filePath, lock);
18
+ fileLocks.set(resolved, lock);
18
19
  }
19
20
  return lock;
20
21
  }
21
22
 
22
23
  export function deleteLockIfUnused(filePath) {
23
- const lock = fileLocks.get(filePath);
24
+ const resolved = path.resolve(filePath);
25
+ const lock = fileLocks.get(resolved);
24
26
  if (lock && !lock.isLocked()) {
25
- fileLocks.delete(filePath);
27
+ fileLocks.delete(resolved);
26
28
  }
27
29
  }
28
30
 
@@ -31,7 +33,8 @@ export function clearLocks() {
31
33
  }
32
34
 
33
35
  export function getParentFolder(dirPath) {
34
- return path.relative(PLUGIN_DIR, dirPath).split(path.sep)[0] || null;
36
+ const rel = path.relative(PLUGIN_DIR, path.resolve(dirPath));
37
+ return rel ? rel.split(/[\\/]/)[0] : null;
35
38
  }
36
39
 
37
40
  function normalize(value) {
@@ -46,25 +49,27 @@ function normalize(value) {
46
49
  }
47
50
 
48
51
  export async function loadFile(filePath, parent, shouldRegister = true) {
52
+ const resolvedPath = path.resolve(filePath);
49
53
  const execute = async () => {
50
- const { mtimeMs } = await fs.stat(filePath);
51
- const mod = await import(`${pathToFileURL(filePath)}?v=${Math.trunc(mtimeMs)}`);
54
+ const { mtimeMs } = await fs.stat(resolvedPath);
55
+ const mod = await import(`${pathToFileURL(resolvedPath)}?v=${Math.trunc(mtimeMs)}`);
52
56
  const loaded = new Map();
53
57
 
54
58
  for (const [name, value] of Object.entries(mod)) {
55
59
  const plugin = normalize(value);
56
60
  if (!plugin || plugin.enabled === false) continue;
57
61
 
58
- const id = path.relative(PLUGIN_DIR, filePath)
62
+ const id = path.relative(PLUGIN_DIR, resolvedPath)
59
63
  .replace(/\.[jt]s$/, '')
60
- .replaceAll(path.sep, '/') + ':' + name;
64
+ .replaceAll(path.sep, '/')
65
+ .replaceAll('\\', '/') + ':' + name;
61
66
 
62
67
  const events = (Array.isArray(plugin.events) ? plugin.events : [])
63
68
  .filter(e => EVENTS.has(e));
64
69
 
65
70
  plugin._meta = {
66
71
  parent,
67
- filePath,
72
+ filePath: resolvedPath,
68
73
  id,
69
74
  events: events.length ? events : [defaultEvent],
70
75
  matchers: compile(plugin.match, plugin.prefix),
@@ -82,7 +87,7 @@ export async function loadFile(filePath, parent, shouldRegister = true) {
82
87
  };
83
88
 
84
89
  return shouldRegister
85
- ? getLock(filePath).runExclusive(execute)
90
+ ? getLock(resolvedPath).runExclusive(execute)
86
91
  : execute();
87
92
  }
88
93
 
@@ -101,9 +106,10 @@ export async function loadAll() {
101
106
  !e.name.startsWith('_')
102
107
  )
103
108
  .map(e => {
104
- const dirPath = e.parentPath ?? e.path;
109
+ const dirPath = e.parentPath ?? e.path ?? PLUGIN_DIR;
110
+ const fullPath = path.resolve(dirPath, e.name);
105
111
  return {
106
- path: path.join(dirPath, e.name),
112
+ path: fullPath,
107
113
  parent: getParentFolder(dirPath)
108
114
  };
109
115
  });
@@ -1,59 +1,78 @@
1
- import { PREFIXES } from './config.js';
2
-
3
- export function compile(match, prefixOpt = PREFIXES) {
4
- if (!Array.isArray(match) || !match.length) return null;
5
-
6
- const strings = match
7
- .filter(m => typeof m === 'string')
8
- .map(s => s.toLowerCase());
9
-
10
- const regexes = match.filter(m => m instanceof RegExp);
11
-
12
- const prefixes = prefixOpt === false
13
- ? null
14
- : new Set([prefixOpt ?? PREFIXES].flat());
15
-
16
- return {
17
- strings,
18
- set: strings.length ? new Set(strings) : null,
19
- regexes,
20
- prefixes,
21
- };
22
- }
23
-
24
- export function test(matchers, body) {
25
- if (!body || typeof body !== 'string') return null;
26
-
27
- const text = body.toLowerCase();
28
-
29
- if (matchers.set) {
30
- const prefix = text[0];
31
- const prefixValid = !matchers.prefixes || matchers.prefixes.has(prefix);
32
-
33
- if (prefixValid) {
34
- const rest = text.slice(1);
35
- const idx = rest.indexOf(' ');
36
- const cmd = idx < 0 ? rest : rest.slice(0, idx);
37
-
38
- if (cmd) {
39
- if (matchers.set.has(cmd)) {
40
- return { match: cmd, prefix };
41
- }
42
-
43
- for (const s of matchers.strings) {
44
- if (cmd.length > s.length && cmd.startsWith(s)) {
45
- return { match: s, prefix };
46
- }
47
- }
48
- }
49
- }
50
- }
51
-
52
- for (const re of matchers.regexes) {
53
- re.lastIndex = 0;
54
- const m = re.exec(body);
55
- if (m) return { match: m, prefix: null };
56
- }
57
-
58
- return null;
1
+ import { PREFIXES } from './config.js';
2
+
3
+ export function compile(match, prefixOpt = PREFIXES) {
4
+ if (!Array.isArray(match) || !match.length) return null;
5
+
6
+ const strings = match
7
+ .filter(m => typeof m === 'string')
8
+ .map(s => s.trim().toLowerCase())
9
+ .filter(Boolean);
10
+
11
+ const regexes = match.filter(m => m instanceof RegExp);
12
+
13
+ if (!strings.length && !regexes.length) return null;
14
+
15
+ const prefixes = prefixOpt === false
16
+ ? null
17
+ : new Set([prefixOpt ?? PREFIXES].flat().map(p => (typeof p === 'string' ? p.trim().toLowerCase() : p)));
18
+
19
+ return {
20
+ strings,
21
+ set: strings.length ? new Set(strings) : null,
22
+ regexes,
23
+ prefixes,
24
+ hasPrefix: prefixOpt !== false,
25
+ };
26
+ }
27
+
28
+ export function test(matchers, body) {
29
+ if (!body || typeof body !== 'string') return null;
30
+
31
+ const trimmed = body.trim();
32
+ if (!trimmed) return null;
33
+ const text = trimmed.toLowerCase();
34
+
35
+ if (matchers.set) {
36
+ if (matchers.prefixes) {
37
+ const prefix = text[0];
38
+ if (matchers.prefixes.has(prefix)) {
39
+ const rest = text.slice(1);
40
+ const idx = rest.indexOf(' ');
41
+ const cmd = idx < 0 ? rest : rest.slice(0, idx);
42
+
43
+ if (cmd) {
44
+ if (matchers.set.has(cmd)) {
45
+ return { match: cmd, prefix };
46
+ }
47
+
48
+ for (const s of matchers.strings) {
49
+ if (cmd.length > s.length && cmd.startsWith(s)) {
50
+ return { match: s, prefix };
51
+ }
52
+ }
53
+ }
54
+ }
55
+ } else {
56
+ const idx = text.indexOf(' ');
57
+ const cmd = idx < 0 ? text : text.slice(0, idx);
58
+ if (cmd) {
59
+ if (matchers.set.has(cmd)) {
60
+ return { match: cmd, prefix: null };
61
+ }
62
+ for (const s of matchers.strings) {
63
+ if (cmd.length > s.length && cmd.startsWith(s)) {
64
+ return { match: s, prefix: null };
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ for (const re of matchers.regexes) {
72
+ re.lastIndex = 0;
73
+ const m = re.exec(body);
74
+ if (m) return { match: m, prefix: null };
75
+ }
76
+
77
+ return null;
59
78
  }