@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.
- package/internal-test/index.js +4 -0
- package/internal-test/kittenwa.config.js +33 -0
- package/internal-test/plugins/exp.js +7 -0
- package/package.json +10 -10
- package/src/auth/init-session.js +18 -5
- package/src/auth/lmdb-auth-state.js +270 -176
- package/src/client/auth-handler.js +97 -0
- package/src/client/client.js +552 -603
- package/src/client/constants.js +41 -0
- package/src/client/errors.js +47 -0
- package/src/client/event-bus.js +69 -0
- package/src/client/index.js +9 -2
- package/src/client/registry.js +63 -0
- package/src/client/socket-proxy.js +57 -0
- package/src/client/sync-manager.js +79 -0
- package/src/config/default.js +39 -39
- package/src/formatter/format-message.js +189 -144
- package/src/index.js +2 -1
- package/src/internals/config.js +15 -3
- package/src/internals/lmdb-manager.js +79 -54
- package/src/plugins/config.js +1 -1
- package/src/plugins/handlers.js +121 -84
- package/src/plugins/loader.js +19 -13
- package/src/plugins/matcher.js +77 -58
- package/src/plugins/registry.js +184 -94
- package/src/plugins/watcher.js +13 -11
- package/src/utils/time-string.js +31 -19
- package/src/utils/type-conversions.js +11 -5
package/src/plugins/handlers.js
CHANGED
|
@@ -1,85 +1,122 @@
|
|
|
1
|
-
import { formatter } from '#formatter.js';
|
|
2
|
-
import { getBucket } from './registry.js';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
}
|
package/src/plugins/loader.js
CHANGED
|
@@ -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
|
-
|
|
14
|
+
const resolved = path.resolve(filePath);
|
|
15
|
+
let lock = fileLocks.get(resolved);
|
|
15
16
|
if (!lock) {
|
|
16
17
|
lock = new Mutex();
|
|
17
|
-
fileLocks.set(
|
|
18
|
+
fileLocks.set(resolved, lock);
|
|
18
19
|
}
|
|
19
20
|
return lock;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export function deleteLockIfUnused(filePath) {
|
|
23
|
-
const
|
|
24
|
+
const resolved = path.resolve(filePath);
|
|
25
|
+
const lock = fileLocks.get(resolved);
|
|
24
26
|
if (lock && !lock.isLocked()) {
|
|
25
|
-
fileLocks.delete(
|
|
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
|
-
|
|
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(
|
|
51
|
-
const mod = await import(`${pathToFileURL(
|
|
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,
|
|
62
|
+
const id = path.relative(PLUGIN_DIR, resolvedPath)
|
|
59
63
|
.replace(/\.[jt]s$/, '')
|
|
60
|
-
.replaceAll(path.sep, '/')
|
|
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(
|
|
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:
|
|
112
|
+
path: fullPath,
|
|
107
113
|
parent: getParentFolder(dirPath)
|
|
108
114
|
};
|
|
109
115
|
});
|
package/src/plugins/matcher.js
CHANGED
|
@@ -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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
return { match:
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
}
|