@shoru/kitten 0.0.4 → 0.0.5
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 +42 -41
- package/src/client/client.js +2 -1
- package/src/plugins/config.js +25 -0
- package/src/plugins/handle-error.js +8 -0
- package/src/plugins/handlers.js +85 -0
- package/src/plugins/index.js +166 -0
- package/src/plugins/loader.js +132 -0
- package/src/plugins/matcher.js +59 -0
- package/src/plugins/registry.js +95 -0
- package/src/plugins/watcher.js +99 -0
- package/src/internals/plugin-manager.js +0 -477
package/package.json
CHANGED
|
@@ -1,41 +1,42 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@shoru/kitten",
|
|
3
|
-
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
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
|
-
"#auth.js": "./src/auth/index.js",
|
|
17
|
-
"#internals.js": "./src/internals/index.js",
|
|
18
|
-
"#client.js": "./src/client/index.js",
|
|
19
|
-
"#formatter.js": "./src/formatter/index.js"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
"@
|
|
29
|
-
"@
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"pino
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@shoru/kitten",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.5",
|
|
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
|
+
"#auth.js": "./src/auth/index.js",
|
|
17
|
+
"#internals.js": "./src/internals/index.js",
|
|
18
|
+
"#client.js": "./src/client/index.js",
|
|
19
|
+
"#formatter.js": "./src/formatter/index.js",
|
|
20
|
+
"#plugins.js": "./src/plugins/index.js"
|
|
21
|
+
},
|
|
22
|
+
"exports": {
|
|
23
|
+
".": "./src/index.js"
|
|
24
|
+
},
|
|
25
|
+
"author": "Aymane Shoru",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@hapi/boom": "^10.0.1",
|
|
29
|
+
"@inquirer/prompts": "^8.1.0",
|
|
30
|
+
"@shoru/spindle": "^1.0.6",
|
|
31
|
+
"async-mutex": "^0.5.0",
|
|
32
|
+
"baileys": "^7.0.0-rc.9",
|
|
33
|
+
"chalk": "^5.6.2",
|
|
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",
|
|
39
|
+
"pino-pretty": "^13.1.3",
|
|
40
|
+
"qrcode-terminal": "^0.12.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/client/client.js
CHANGED
|
@@ -2,7 +2,8 @@ import { DisconnectReason } from 'baileys';
|
|
|
2
2
|
import { Boom } from '@hapi/boom';
|
|
3
3
|
import qrcode from 'qrcode-terminal';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
|
-
import {
|
|
5
|
+
import { pluginManager } from '#plugins.js';
|
|
6
|
+
import { logger, pino } from '#internals.js';
|
|
6
7
|
import { initSession, listSessions } from '#auth.js';
|
|
7
8
|
import { getConnectionConfig } from './getConnectionConfig.js';
|
|
8
9
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getConfig } from '#internals.js';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const config = await getConfig();
|
|
5
|
+
|
|
6
|
+
export const {
|
|
7
|
+
dir,
|
|
8
|
+
defaultEvent,
|
|
9
|
+
prefixes: PREFIXES,
|
|
10
|
+
hmr: {
|
|
11
|
+
enable: HMREnabled,
|
|
12
|
+
debounce: debounceMs,
|
|
13
|
+
debug: isDebug
|
|
14
|
+
}
|
|
15
|
+
} = config.plugins;
|
|
16
|
+
|
|
17
|
+
export const PLUGIN_DIR = path.join(process.cwd(), dir);
|
|
18
|
+
|
|
19
|
+
export const EVENTS = Object.freeze(new Set([
|
|
20
|
+
'messaging-history.set', 'chats.upsert', 'chats.update', 'chats.delete',
|
|
21
|
+
'contacts.upsert', 'contacts.update', 'messages.upsert', 'messages.update',
|
|
22
|
+
'messages.delete', 'messages.reaction', 'message-receipt.update',
|
|
23
|
+
'groups.update', 'group-participants.update', 'connection.update',
|
|
24
|
+
'creds.update', 'presence.update', 'blocklist.set', 'blocklist.update', 'call',
|
|
25
|
+
]));
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { logger } from '#internals.js';
|
|
2
|
+
import { HMREnabled, isDebug } from './config.js';
|
|
3
|
+
import { handleError } from './handle-error.js';
|
|
4
|
+
import {
|
|
5
|
+
getPlugin,
|
|
6
|
+
getAllPlugins,
|
|
7
|
+
getPluginCount,
|
|
8
|
+
getEventCounts,
|
|
9
|
+
clear as clearRegistry
|
|
10
|
+
} from './registry.js';
|
|
11
|
+
import { loadAll, clearLocks } from './loader.js';
|
|
12
|
+
import { initWatcher, closeWatcher, clearTimers, onSync } from './watcher.js';
|
|
13
|
+
import { createHandler } from './handlers.js';
|
|
14
|
+
|
|
15
|
+
const instances = new Set();
|
|
16
|
+
|
|
17
|
+
let ready = false;
|
|
18
|
+
|
|
19
|
+
function syncAllInstances() {
|
|
20
|
+
for (const instance of instances) {
|
|
21
|
+
instance._syncListeners();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function cleanup() {
|
|
26
|
+
closeWatcher();
|
|
27
|
+
clearTimers();
|
|
28
|
+
clearLocks();
|
|
29
|
+
clearRegistry();
|
|
30
|
+
ready = false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class PluginManager {
|
|
34
|
+
#sock;
|
|
35
|
+
#handlers = new Map();
|
|
36
|
+
#destroyed = false;
|
|
37
|
+
|
|
38
|
+
constructor(sock) {
|
|
39
|
+
if (!sock?.ev) {
|
|
40
|
+
throw new TypeError('Invalid socket: missing ev property');
|
|
41
|
+
}
|
|
42
|
+
this.#sock = sock;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async init() {
|
|
46
|
+
if (this.#destroyed) {
|
|
47
|
+
throw new Error('Cannot reinitialize destroyed instance');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
instances.add(this);
|
|
51
|
+
|
|
52
|
+
if (!ready) {
|
|
53
|
+
await loadAll();
|
|
54
|
+
|
|
55
|
+
if (HMREnabled) {
|
|
56
|
+
initWatcher();
|
|
57
|
+
onSync(syncAllInstances);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
ready = true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this._syncListeners();
|
|
64
|
+
|
|
65
|
+
if (isDebug) {
|
|
66
|
+
logger.debug(
|
|
67
|
+
`[PluginManager] Init (sockets: ${instances.size}, plugins: ${getPluginCount()})`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
destroy() {
|
|
75
|
+
if (this.#destroyed) return;
|
|
76
|
+
this.#destroyed = true;
|
|
77
|
+
|
|
78
|
+
for (const [event, handler] of this.#handlers) {
|
|
79
|
+
this.#sock.ev.off(event, handler);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.#handlers.clear();
|
|
83
|
+
instances.delete(this);
|
|
84
|
+
|
|
85
|
+
if (instances.size === 0) {
|
|
86
|
+
cleanup();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_syncListeners() {
|
|
91
|
+
if (this.#destroyed) return;
|
|
92
|
+
|
|
93
|
+
const eventCounts = getEventCounts();
|
|
94
|
+
const activeEvents = new Set();
|
|
95
|
+
|
|
96
|
+
for (const [event, count] of eventCounts) {
|
|
97
|
+
if (count > 0) activeEvents.add(event);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Remove inactive events
|
|
101
|
+
for (const [event, handler] of this.#handlers) {
|
|
102
|
+
if (!activeEvents.has(event)) {
|
|
103
|
+
this.#sock.ev.off(event, handler);
|
|
104
|
+
this.#handlers.delete(event);
|
|
105
|
+
if (isDebug) logger.debug(`[Events] (-) ${event}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Add active events
|
|
110
|
+
for (const event of activeEvents) {
|
|
111
|
+
if (!this.#handlers.has(event)) {
|
|
112
|
+
const handler = createHandler(
|
|
113
|
+
event,
|
|
114
|
+
this.#sock,
|
|
115
|
+
() => this.#destroyed,
|
|
116
|
+
this.#execute.bind(this)
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
this.#sock.ev.on(event, handler);
|
|
120
|
+
this.#handlers.set(event, handler);
|
|
121
|
+
if (isDebug) logger.debug(`[Events] (+) ${event}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async #execute(id, plugin, sock, ctx, event, match) {
|
|
127
|
+
if (this.#destroyed) return;
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const context = match ? { ...ctx, _match: match } : ctx;
|
|
131
|
+
await plugin(sock, context, event);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
handleError(`[Plugin:${id}]`, err);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
get(id) {
|
|
138
|
+
return getPlugin(id);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
get all() {
|
|
142
|
+
return new Map(getAllPlugins());
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
get events() {
|
|
146
|
+
return [...this.#handlers.keys()];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
get destroyed() {
|
|
150
|
+
return this.#destroyed;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
static get instances() {
|
|
154
|
+
return instances.size;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
static get count() {
|
|
158
|
+
return getPluginCount();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function pluginManager(sock) {
|
|
163
|
+
const manager = new PluginManager(sock);
|
|
164
|
+
await manager.init();
|
|
165
|
+
return manager;
|
|
166
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Mutex } from 'async-mutex';
|
|
2
|
+
import { logger } from '#internals.js';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { pathToFileURL } from 'url';
|
|
6
|
+
import { PLUGIN_DIR, EVENTS, defaultEvent } from './config.js';
|
|
7
|
+
import { handleError } from './handle-error.js';
|
|
8
|
+
import { compile } from './matcher.js';
|
|
9
|
+
import { setPlugin, registerToBuckets } from './registry.js';
|
|
10
|
+
|
|
11
|
+
const fileLocks = new Map();
|
|
12
|
+
|
|
13
|
+
export function getLock(filePath) {
|
|
14
|
+
let lock = fileLocks.get(filePath);
|
|
15
|
+
if (!lock) {
|
|
16
|
+
lock = new Mutex();
|
|
17
|
+
fileLocks.set(filePath, lock);
|
|
18
|
+
}
|
|
19
|
+
return lock;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function deleteLockIfUnused(filePath) {
|
|
23
|
+
const lock = fileLocks.get(filePath);
|
|
24
|
+
if (lock && !lock.isLocked()) {
|
|
25
|
+
fileLocks.delete(filePath);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function clearLocks() {
|
|
30
|
+
fileLocks.clear();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getParentFolder(dirPath) {
|
|
34
|
+
return path.relative(PLUGIN_DIR, dirPath).split(path.sep)[0] || null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalize(value) {
|
|
38
|
+
if (typeof value === 'function') return value;
|
|
39
|
+
|
|
40
|
+
if (typeof value?.default === 'function') {
|
|
41
|
+
const { default: fn, ...rest } = value;
|
|
42
|
+
return Object.assign(fn, rest);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function loadFile(filePath, parent, shouldRegister = true) {
|
|
49
|
+
const execute = async () => {
|
|
50
|
+
const { mtimeMs } = await fs.stat(filePath);
|
|
51
|
+
const mod = await import(`${pathToFileURL(filePath)}?v=${Math.trunc(mtimeMs)}`);
|
|
52
|
+
const loaded = new Map();
|
|
53
|
+
|
|
54
|
+
for (const [name, value] of Object.entries(mod)) {
|
|
55
|
+
const plugin = normalize(value);
|
|
56
|
+
if (!plugin || plugin.enabled === false) continue;
|
|
57
|
+
|
|
58
|
+
const id = path.relative(PLUGIN_DIR, filePath)
|
|
59
|
+
.replace(/\.[jt]s$/, '')
|
|
60
|
+
.replaceAll(path.sep, '/') + ':' + name;
|
|
61
|
+
|
|
62
|
+
const events = (Array.isArray(plugin.events) ? plugin.events : [])
|
|
63
|
+
.filter(e => EVENTS.has(e));
|
|
64
|
+
|
|
65
|
+
plugin._meta = {
|
|
66
|
+
parent,
|
|
67
|
+
filePath,
|
|
68
|
+
id,
|
|
69
|
+
events: events.length ? events : [defaultEvent],
|
|
70
|
+
matchers: compile(plugin.match, plugin.prefix),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (shouldRegister) {
|
|
74
|
+
setPlugin(id, plugin);
|
|
75
|
+
registerToBuckets(id, plugin);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
loaded.set(id, plugin);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return loaded;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return shouldRegister
|
|
85
|
+
? getLock(filePath).runExclusive(execute)
|
|
86
|
+
: execute();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function loadAll() {
|
|
90
|
+
await fs.mkdir(PLUGIN_DIR, { recursive: true }).catch(() => {});
|
|
91
|
+
|
|
92
|
+
const entries = await fs.readdir(PLUGIN_DIR, {
|
|
93
|
+
withFileTypes: true,
|
|
94
|
+
recursive: true
|
|
95
|
+
}).catch(() => []);
|
|
96
|
+
|
|
97
|
+
const files = entries
|
|
98
|
+
.filter(e =>
|
|
99
|
+
e.isFile() &&
|
|
100
|
+
/(?<!\.d)\.[jt]s$/.test(e.name) &&
|
|
101
|
+
!e.name.startsWith('_')
|
|
102
|
+
)
|
|
103
|
+
.map(e => {
|
|
104
|
+
const dirPath = e.parentPath ?? e.path;
|
|
105
|
+
return {
|
|
106
|
+
path: path.join(dirPath, e.name),
|
|
107
|
+
parent: getParentFolder(dirPath)
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const results = await Promise.allSettled(
|
|
112
|
+
files.map(({ path: p, parent }) => loadFile(p, parent))
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
let loaded = 0;
|
|
116
|
+
let failed = 0;
|
|
117
|
+
|
|
118
|
+
for (let i = 0; i < results.length; i++) {
|
|
119
|
+
const result = results[i];
|
|
120
|
+
if (result.status === 'fulfilled') {
|
|
121
|
+
loaded += result.value?.size ?? 0;
|
|
122
|
+
} else {
|
|
123
|
+
failed++;
|
|
124
|
+
const rel = path.relative(PLUGIN_DIR, files[i].path);
|
|
125
|
+
handleError(`[PluginManager:${rel}] Failed to load:`, result.reason);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
logger.info(
|
|
130
|
+
`[PluginManager] Loaded ${loaded} plugins${failed ? ` (${failed} failed)` : ''}`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
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;
|
|
59
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { EVENTS } from './config.js';
|
|
2
|
+
|
|
3
|
+
const plugins = new Map();
|
|
4
|
+
|
|
5
|
+
const eventCounts = new Map([...EVENTS].map(e => [e, 0]));
|
|
6
|
+
|
|
7
|
+
const buckets = Object.fromEntries(
|
|
8
|
+
[...EVENTS].map(e => [e, { auto: new Map(), match: new Map() }])
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
// Plugin Map Operations
|
|
12
|
+
|
|
13
|
+
export function getPlugin(id) {
|
|
14
|
+
return plugins.get(id);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function setPlugin(id, plugin) {
|
|
18
|
+
plugins.set(id, plugin);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function deletePlugin(id) {
|
|
22
|
+
plugins.delete(id);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getAllPlugins() {
|
|
26
|
+
return plugins;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getPluginCount() {
|
|
30
|
+
return plugins.size;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Bucket Operations
|
|
34
|
+
|
|
35
|
+
export function getBucket(event) {
|
|
36
|
+
return buckets[event];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getEventCounts() {
|
|
40
|
+
return eventCounts;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function registerToBuckets(id, plugin) {
|
|
44
|
+
const key = plugin._meta.matchers ? 'match' : 'auto';
|
|
45
|
+
|
|
46
|
+
for (const event of plugin._meta.events) {
|
|
47
|
+
const bucket = buckets[event]?.[key];
|
|
48
|
+
if (bucket && !bucket.has(id)) {
|
|
49
|
+
bucket.set(id, plugin);
|
|
50
|
+
eventCounts.set(event, (eventCounts.get(event) ?? 0) + 1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function unregisterFromBuckets(id) {
|
|
56
|
+
const plugin = plugins.get(id);
|
|
57
|
+
const events = plugin?._meta?.events ?? [];
|
|
58
|
+
|
|
59
|
+
for (const event of events) {
|
|
60
|
+
const bucket = buckets[event];
|
|
61
|
+
if (bucket?.auto.delete(id) || bucket?.match.delete(id)) {
|
|
62
|
+
eventCounts.set(event, Math.max(0, (eventCounts.get(event) ?? 1) - 1));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function unloadByFilePath(filePath) {
|
|
68
|
+
const idsToUnload = [];
|
|
69
|
+
|
|
70
|
+
for (const [id, plugin] of plugins) {
|
|
71
|
+
if (plugin._meta?.filePath === filePath) {
|
|
72
|
+
idsToUnload.push(id);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const id of idsToUnload) {
|
|
77
|
+
unregisterFromBuckets(id);
|
|
78
|
+
plugins.delete(id);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return idsToUnload.length;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function clear() {
|
|
85
|
+
plugins.clear();
|
|
86
|
+
|
|
87
|
+
for (const bucket of Object.values(buckets)) {
|
|
88
|
+
bucket.auto.clear();
|
|
89
|
+
bucket.match.clear();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const event of EVENTS) {
|
|
93
|
+
eventCounts.set(event, 0);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { watch } from 'chokidar';
|
|
2
|
+
import { logger } from '#internals.js';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { PLUGIN_DIR, debounceMs } from './config.js';
|
|
5
|
+
import { handleError } from './handle-error.js';
|
|
6
|
+
import { loadFile, getLock, getParentFolder, deleteLockIfUnused } from './loader.js';
|
|
7
|
+
import { setPlugin, registerToBuckets, unloadByFilePath } from './registry.js';
|
|
8
|
+
|
|
9
|
+
let watcher = null;
|
|
10
|
+
|
|
11
|
+
const debounceTimers = new Map();
|
|
12
|
+
|
|
13
|
+
let syncCallback = null;
|
|
14
|
+
|
|
15
|
+
export function onSync(callback) {
|
|
16
|
+
syncCallback = callback;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function initWatcher() {
|
|
20
|
+
if (watcher) return;
|
|
21
|
+
|
|
22
|
+
watcher = watch(PLUGIN_DIR, {
|
|
23
|
+
persistent: true,
|
|
24
|
+
ignoreInitial: true,
|
|
25
|
+
ignored: [
|
|
26
|
+
/(^|[/\\])_/,
|
|
27
|
+
/\.d\.[jt]s$/,
|
|
28
|
+
/node_modules/,
|
|
29
|
+
/(^|[/\\])\../
|
|
30
|
+
],
|
|
31
|
+
awaitWriteFinish: {
|
|
32
|
+
stabilityThreshold: 100,
|
|
33
|
+
pollInterval: 20
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
.on('add', p => scheduleHMR(p, 'add'))
|
|
37
|
+
.on('change', p => scheduleHMR(p, 'change'))
|
|
38
|
+
.on('unlink', p => scheduleHMR(p, 'unlink'))
|
|
39
|
+
.on('error', err => handleError('[Watcher]', err));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function closeWatcher() {
|
|
43
|
+
watcher?.close();
|
|
44
|
+
watcher = null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function clearTimers() {
|
|
48
|
+
debounceTimers.forEach(clearTimeout);
|
|
49
|
+
debounceTimers.clear();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function scheduleHMR(filePath, type) {
|
|
53
|
+
clearTimeout(debounceTimers.get(filePath));
|
|
54
|
+
|
|
55
|
+
debounceTimers.set(
|
|
56
|
+
filePath,
|
|
57
|
+
setTimeout(() => {
|
|
58
|
+
debounceTimers.delete(filePath);
|
|
59
|
+
executeHMR(filePath, type);
|
|
60
|
+
}, debounceMs)
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function executeHMR(filePath, type) {
|
|
65
|
+
const rel = path.relative(PLUGIN_DIR, filePath);
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
await getLock(filePath).runExclusive(async () => {
|
|
69
|
+
if (type === 'unlink') {
|
|
70
|
+
const count = unloadByFilePath(filePath);
|
|
71
|
+
logger.info(`[HMR] Unloaded: ${rel} (${count})`);
|
|
72
|
+
} else {
|
|
73
|
+
// Load new plugins without registering
|
|
74
|
+
const parent = getParentFolder(path.dirname(filePath));
|
|
75
|
+
const loaded = await loadFile(filePath, parent, false);
|
|
76
|
+
|
|
77
|
+
// Remove old plugins for this file
|
|
78
|
+
unloadByFilePath(filePath);
|
|
79
|
+
|
|
80
|
+
// Register newly loaded plugins
|
|
81
|
+
for (const [id, plugin] of loaded) {
|
|
82
|
+
setPlugin(id, plugin);
|
|
83
|
+
registerToBuckets(id, plugin);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const action = type === 'add' ? 'Added' : 'Reloaded';
|
|
87
|
+
logger.info(`[HMR] ${action}: ${rel} (${loaded.size})`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
syncCallback?.();
|
|
91
|
+
});
|
|
92
|
+
} catch (err) {
|
|
93
|
+
handleError(`[HMR:${rel}] Failed:`, err);
|
|
94
|
+
} finally {
|
|
95
|
+
if (type === 'unlink') {
|
|
96
|
+
deleteLockIfUnused(filePath);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -1,477 +0,0 @@
|
|
|
1
|
-
import { Mutex } from 'async-mutex';
|
|
2
|
-
import { getConfig, logger } from '#internals.js';
|
|
3
|
-
import { watch } from 'chokidar';
|
|
4
|
-
import fs from 'fs/promises';
|
|
5
|
-
import path from 'path';
|
|
6
|
-
import { pathToFileURL } from 'url';
|
|
7
|
-
import { formatter } from '#formatter.js';
|
|
8
|
-
|
|
9
|
-
const config = await getConfig();
|
|
10
|
-
|
|
11
|
-
const {
|
|
12
|
-
dir,
|
|
13
|
-
defaultEvent,
|
|
14
|
-
prefixes: PREFIXES,
|
|
15
|
-
hmr: {
|
|
16
|
-
enable: HMREnabled,
|
|
17
|
-
debounce: debounceMs,
|
|
18
|
-
debug: isDebug
|
|
19
|
-
}
|
|
20
|
-
} = config.plugins;
|
|
21
|
-
|
|
22
|
-
const PLUGIN_DIR = path.join(process.cwd(), dir);
|
|
23
|
-
|
|
24
|
-
const EVENTS = new Set([
|
|
25
|
-
'messaging-history.set', 'chats.upsert', 'chats.update', 'chats.delete',
|
|
26
|
-
'contacts.upsert', 'contacts.update', 'messages.upsert', 'messages.update',
|
|
27
|
-
'messages.delete', 'messages.reaction', 'message-receipt.update',
|
|
28
|
-
'groups.update', 'group-participants.update', 'connection.update',
|
|
29
|
-
'creds.update', 'presence.update', 'blocklist.set', 'blocklist.update', 'call',
|
|
30
|
-
]);
|
|
31
|
-
|
|
32
|
-
const createBuckets = () => Object.fromEntries(
|
|
33
|
-
[...EVENTS].map(e => [e, { auto: new Map(), match: new Map() }])
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
export class PluginManager {
|
|
37
|
-
static #plugins = new Map();
|
|
38
|
-
static #watcher = null;
|
|
39
|
-
static #ready = false;
|
|
40
|
-
static #debounceTimers = new Map();
|
|
41
|
-
static #instances = new Set();
|
|
42
|
-
static #fileLocks = new Map();
|
|
43
|
-
static #buckets = createBuckets();
|
|
44
|
-
static #eventCounts = new Map([...EVENTS].map(e => [e, 0]));
|
|
45
|
-
|
|
46
|
-
#sock;
|
|
47
|
-
#handlers = new Map();
|
|
48
|
-
#destroyed = false;
|
|
49
|
-
|
|
50
|
-
constructor(sock) {
|
|
51
|
-
if (!sock?.ev) throw new TypeError('Invalid socket: missing ev property');
|
|
52
|
-
this.#sock = sock;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
static #getLock(filePath) {
|
|
56
|
-
return PluginManager.#fileLocks.get(filePath)
|
|
57
|
-
?? PluginManager.#fileLocks.set(filePath, new Mutex()).get(filePath);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
static #handleError(context, err) {
|
|
61
|
-
if (isDebug) {
|
|
62
|
-
logger.warn(`${context} ${err?.message ?? 'Unknown error'}`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async init() {
|
|
67
|
-
if (this.#destroyed) throw new Error('Cannot reinitialize destroyed instance');
|
|
68
|
-
|
|
69
|
-
PluginManager.#instances.add(this);
|
|
70
|
-
|
|
71
|
-
if (!PluginManager.#ready) {
|
|
72
|
-
await fs.mkdir(PLUGIN_DIR, { recursive: true }).catch(() => {});
|
|
73
|
-
await PluginManager.#loadAll();
|
|
74
|
-
if (HMREnabled) PluginManager.#initWatcher();
|
|
75
|
-
PluginManager.#ready = true;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
this.#syncListeners();
|
|
79
|
-
if (isDebug) {
|
|
80
|
-
logger.debug(`[PluginManager] Init (sockets: ${PluginManager.#instances.size}, plugins: ${PluginManager.#plugins.size})`);
|
|
81
|
-
}
|
|
82
|
-
return this;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
destroy() {
|
|
86
|
-
if (this.#destroyed) return;
|
|
87
|
-
this.#destroyed = true;
|
|
88
|
-
|
|
89
|
-
for (const [event, handler] of this.#handlers) {
|
|
90
|
-
this.#sock.ev.off(event, handler);
|
|
91
|
-
}
|
|
92
|
-
this.#handlers.clear();
|
|
93
|
-
PluginManager.#instances.delete(this);
|
|
94
|
-
|
|
95
|
-
if (PluginManager.#instances.size === 0) {
|
|
96
|
-
PluginManager.#cleanup();
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
static #cleanup() {
|
|
101
|
-
PluginManager.#watcher?.close();
|
|
102
|
-
PluginManager.#watcher = null;
|
|
103
|
-
PluginManager.#ready = false;
|
|
104
|
-
PluginManager.#debounceTimers.forEach(clearTimeout);
|
|
105
|
-
PluginManager.#debounceTimers.clear();
|
|
106
|
-
PluginManager.#fileLocks.clear();
|
|
107
|
-
PluginManager.#plugins.clear();
|
|
108
|
-
|
|
109
|
-
for (const bucket of Object.values(PluginManager.#buckets)) {
|
|
110
|
-
bucket.auto.clear();
|
|
111
|
-
bucket.match.clear();
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
for (const event of EVENTS) {
|
|
115
|
-
PluginManager.#eventCounts.set(event, 0);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
static #getParent(dirPath) {
|
|
120
|
-
return path.relative(PLUGIN_DIR, dirPath).split(path.sep)[0] || null;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
static async #loadAll() {
|
|
124
|
-
const entries = await fs.readdir(PLUGIN_DIR, { withFileTypes: true, recursive: true }).catch(() => []);
|
|
125
|
-
|
|
126
|
-
const files = entries
|
|
127
|
-
.filter(e => e.isFile() && /(?<!\.d)\.[jt]s$/.test(e.name) && !e.name.startsWith('_'))
|
|
128
|
-
.map(e => {
|
|
129
|
-
const dirPath = e.parentPath ?? e.path;
|
|
130
|
-
const parent = PluginManager.#getParent(dirPath);
|
|
131
|
-
const filePath = path.join(dirPath, e.name);
|
|
132
|
-
return { path: filePath, parent };
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
const results = await Promise.allSettled(
|
|
136
|
-
files.map(({ path: p, parent }) => PluginManager.#loadFile(p, parent))
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
let loaded = 0;
|
|
140
|
-
let failed = 0;
|
|
141
|
-
|
|
142
|
-
for (let i = 0; i < results.length; i++) {
|
|
143
|
-
const result = results[i];
|
|
144
|
-
if (result.status === 'fulfilled') {
|
|
145
|
-
loaded += result.value?.size ?? 0;
|
|
146
|
-
} else {
|
|
147
|
-
failed++;
|
|
148
|
-
const rel = path.relative(PLUGIN_DIR, files[i].path);
|
|
149
|
-
PluginManager.#handleError(`[PluginManager:${rel}] Failed to load:`, result.reason);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
logger.info(`[PluginManager] Loaded ${loaded} plugins${failed ? ` (${failed} failed)` : ''}`);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
static async #loadFile(filePath, parent, register = true) {
|
|
157
|
-
const exec = async () => {
|
|
158
|
-
const { mtimeMs } = await fs.stat(filePath);
|
|
159
|
-
const mod = await import(`${pathToFileURL(filePath)}?v=${Math.trunc(mtimeMs)}`);
|
|
160
|
-
const loaded = new Map();
|
|
161
|
-
|
|
162
|
-
for (const [name, value] of Object.entries(mod)) {
|
|
163
|
-
const plugin = PluginManager.#normalize(value);
|
|
164
|
-
if (!plugin || plugin.enabled === false) continue;
|
|
165
|
-
|
|
166
|
-
const id = path.relative(PLUGIN_DIR, filePath)
|
|
167
|
-
.replace(/\.[jt]s$/, '')
|
|
168
|
-
.replaceAll(path.sep, '/') + ':' + name;
|
|
169
|
-
|
|
170
|
-
const events = (Array.isArray(plugin.events) ? plugin.events : [])
|
|
171
|
-
.filter(e => EVENTS.has(e));
|
|
172
|
-
|
|
173
|
-
plugin._meta = {
|
|
174
|
-
parent,
|
|
175
|
-
filePath,
|
|
176
|
-
id,
|
|
177
|
-
events: events.length ? events : [defaultEvent],
|
|
178
|
-
matchers: PluginManager.#compile(plugin.match, plugin.prefix),
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
if (register) {
|
|
182
|
-
PluginManager.#plugins.set(id, plugin);
|
|
183
|
-
PluginManager.#register(id, plugin);
|
|
184
|
-
}
|
|
185
|
-
loaded.set(id, plugin);
|
|
186
|
-
}
|
|
187
|
-
return loaded;
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
return register ? PluginManager.#getLock(filePath).runExclusive(exec) : exec();
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
static #normalize(value) {
|
|
194
|
-
if (typeof value === 'function') return value;
|
|
195
|
-
if (typeof value?.default === 'function') {
|
|
196
|
-
const { default: fn, ...rest } = value;
|
|
197
|
-
return Object.assign(fn, rest);
|
|
198
|
-
}
|
|
199
|
-
return null;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
static #compile(match, prefixOpt = PREFIXES) {
|
|
203
|
-
if (!Array.isArray(match) || !match.length) return null;
|
|
204
|
-
|
|
205
|
-
const strings = match.filter(m => typeof m === 'string').map(s => s.toLowerCase());
|
|
206
|
-
const regexes = match.filter(m => m instanceof RegExp);
|
|
207
|
-
|
|
208
|
-
const prefixes = prefixOpt === false
|
|
209
|
-
? null
|
|
210
|
-
: new Set([prefixOpt ?? PREFIXES].flat());
|
|
211
|
-
|
|
212
|
-
return {
|
|
213
|
-
strings,
|
|
214
|
-
set: strings.length ? new Set(strings) : null,
|
|
215
|
-
regexes,
|
|
216
|
-
prefixes,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
static #test(matchers, body) {
|
|
221
|
-
if (!body || typeof body !== 'string') return null;
|
|
222
|
-
|
|
223
|
-
const text = body.toLowerCase();
|
|
224
|
-
|
|
225
|
-
if (matchers.set) {
|
|
226
|
-
const prefix = text[0];
|
|
227
|
-
const prefixValid = !matchers.prefixes || matchers.prefixes.has(prefix);
|
|
228
|
-
|
|
229
|
-
if (prefixValid) {
|
|
230
|
-
const rest = text.slice(1);
|
|
231
|
-
const idx = rest.indexOf(' ');
|
|
232
|
-
const cmd = idx < 0 ? rest : rest.slice(0, idx);
|
|
233
|
-
|
|
234
|
-
if (cmd) {
|
|
235
|
-
if (matchers.set.has(cmd)) {
|
|
236
|
-
return { match: cmd, prefix };
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
for (const s of matchers.strings) {
|
|
240
|
-
if (cmd.length > s.length && cmd.startsWith(s)) {
|
|
241
|
-
return { match: s, prefix };
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
for (const re of matchers.regexes) {
|
|
249
|
-
re.lastIndex = 0;
|
|
250
|
-
const m = re.exec(body);
|
|
251
|
-
if (m) return { match: m, prefix: null };
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return null;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
static #register(id, plugin) {
|
|
258
|
-
const key = plugin._meta.matchers ? 'match' : 'auto';
|
|
259
|
-
for (const e of plugin._meta.events) {
|
|
260
|
-
const bucket = PluginManager.#buckets[e]?.[key];
|
|
261
|
-
if (bucket && !bucket.has(id)) {
|
|
262
|
-
bucket.set(id, plugin);
|
|
263
|
-
PluginManager.#eventCounts.set(e, (PluginManager.#eventCounts.get(e) ?? 0) + 1);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
static #unregister(id) {
|
|
269
|
-
const events = PluginManager.#plugins.get(id)?._meta?.events ?? [];
|
|
270
|
-
for (const e of events) {
|
|
271
|
-
const bucket = PluginManager.#buckets[e];
|
|
272
|
-
if (bucket?.auto.delete(id) || bucket?.match.delete(id)) {
|
|
273
|
-
PluginManager.#eventCounts.set(e, Math.max(0, (PluginManager.#eventCounts.get(e) ?? 1) - 1));
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
#syncListeners() {
|
|
279
|
-
if (this.#destroyed) return;
|
|
280
|
-
|
|
281
|
-
const active = new Set();
|
|
282
|
-
for (const [event, count] of PluginManager.#eventCounts) {
|
|
283
|
-
if (count > 0) active.add(event);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
for (const [event, handler] of this.#handlers) {
|
|
287
|
-
if (!active.has(event)) {
|
|
288
|
-
this.#sock.ev.off(event, handler);
|
|
289
|
-
this.#handlers.delete(event);
|
|
290
|
-
if (isDebug) logger.debug(`[Events] (-) ${event}`);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
for (const event of active) {
|
|
295
|
-
if (!this.#handlers.has(event)) {
|
|
296
|
-
const handler = this.#createHandler(event);
|
|
297
|
-
this.#sock.ev.on(event, handler);
|
|
298
|
-
this.#handlers.set(event, handler);
|
|
299
|
-
if (isDebug) logger.debug(`[Events] (+) ${event}`);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
static #syncAll() {
|
|
305
|
-
for (const instance of PluginManager.#instances) {
|
|
306
|
-
instance.#syncListeners();
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
#createHandler(event) {
|
|
311
|
-
const bucket = PluginManager.#buckets[event];
|
|
312
|
-
const sock = this.#sock;
|
|
313
|
-
const dispatch = (ctx, event) => this.#dispatch(sock, ctx, bucket, event);
|
|
314
|
-
|
|
315
|
-
const handlers = {
|
|
316
|
-
'messages.upsert': ({ messages, type }) => {
|
|
317
|
-
if (type !== 'notify') return;
|
|
318
|
-
for (const msg of messages) {
|
|
319
|
-
if (!msg?.key?.remoteJid || msg.key.remoteJid === 'status@broadcast') continue;
|
|
320
|
-
try {
|
|
321
|
-
dispatch(formatter(sock, msg, event));
|
|
322
|
-
} catch (err) {
|
|
323
|
-
PluginManager.#handleError('[PluginManager] Format error:', err);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
},
|
|
327
|
-
|
|
328
|
-
'messages.update': (updates) => {
|
|
329
|
-
for (const { key, update } of updates) {
|
|
330
|
-
if (key?.remoteJid) dispatch({ key, update, jid: key.remoteJid }, event);
|
|
331
|
-
}
|
|
332
|
-
},
|
|
333
|
-
|
|
334
|
-
'messages.reaction': (reactions) => {
|
|
335
|
-
for (const { key, reaction } of reactions) {
|
|
336
|
-
if (key?.remoteJid) dispatch({ key, reaction, jid: key.remoteJid, emoji: reaction?.text }, event);
|
|
337
|
-
}
|
|
338
|
-
},
|
|
339
|
-
|
|
340
|
-
'group-participants.update': (u) => dispatch(u, event),
|
|
341
|
-
'connection.update': (u) => dispatch(u, event),
|
|
342
|
-
'creds.update': (creds) => dispatch({ creds }, event),
|
|
343
|
-
'call': (calls) => calls.forEach(c => dispatch(c, event)),
|
|
344
|
-
};
|
|
345
|
-
|
|
346
|
-
return handlers[event] ?? ((data) => dispatch({ data }, event));
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
#dispatch(sock, ctx, bucket, event) {
|
|
350
|
-
if (this.#destroyed || !ctx) return;
|
|
351
|
-
|
|
352
|
-
for (const [id, plugin] of bucket.auto) {
|
|
353
|
-
this.#execute(id, plugin, sock, ctx, event);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
if (bucket.match.size && ctx.body) {
|
|
357
|
-
for (const [id, plugin] of bucket.match) {
|
|
358
|
-
const matchers = plugin._meta?.matchers;
|
|
359
|
-
if (!matchers) continue;
|
|
360
|
-
|
|
361
|
-
const result = PluginManager.#test(matchers, ctx.body);
|
|
362
|
-
if (result) this.#execute(id, plugin, sock, ctx, event, result);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
async #execute(id, plugin, sock, ctx, event, match) {
|
|
368
|
-
if (this.#destroyed) return;
|
|
369
|
-
try {
|
|
370
|
-
await plugin(sock, match ? { ...ctx, _match: match } : ctx, event);
|
|
371
|
-
} catch (err) {
|
|
372
|
-
PluginManager.#handleError(`[Plugin:${id}]`, err);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
static #initWatcher() {
|
|
377
|
-
if (PluginManager.#watcher) return;
|
|
378
|
-
|
|
379
|
-
PluginManager.#watcher = watch(PLUGIN_DIR, {
|
|
380
|
-
persistent: true,
|
|
381
|
-
ignoreInitial: true,
|
|
382
|
-
ignored: [/(^|[/\\])_/, /\.d\.[jt]s$/, /node_modules/, /(^|[/\\])\../],
|
|
383
|
-
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 20 },
|
|
384
|
-
})
|
|
385
|
-
.on('add', p => PluginManager.#debounce(p, 'add'))
|
|
386
|
-
.on('change', p => PluginManager.#debounce(p, 'change'))
|
|
387
|
-
.on('unlink', p => PluginManager.#debounce(p, 'unlink'))
|
|
388
|
-
.on('error', e => PluginManager.#handleError('[Watcher]', e));
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
static #debounce(filePath, type) {
|
|
392
|
-
clearTimeout(PluginManager.#debounceTimers.get(filePath));
|
|
393
|
-
PluginManager.#debounceTimers.set(
|
|
394
|
-
filePath,
|
|
395
|
-
setTimeout(() => {
|
|
396
|
-
PluginManager.#debounceTimers.delete(filePath);
|
|
397
|
-
PluginManager.#hmr(filePath, type);
|
|
398
|
-
}, debounceMs)
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
static async #hmr(filePath, type) {
|
|
403
|
-
const rel = path.relative(PLUGIN_DIR, filePath);
|
|
404
|
-
|
|
405
|
-
try {
|
|
406
|
-
await PluginManager.#getLock(filePath).runExclusive(async () => {
|
|
407
|
-
if (type === 'unlink') {
|
|
408
|
-
const n = PluginManager.#unloadFile(filePath);
|
|
409
|
-
logger.info(`[HMR] Unloaded: ${rel} (${n})`);
|
|
410
|
-
} else {
|
|
411
|
-
const parent = PluginManager.#getParent(path.dirname(filePath));
|
|
412
|
-
const plugins = await PluginManager.#loadFile(filePath, parent, false);
|
|
413
|
-
|
|
414
|
-
PluginManager.#unloadFile(filePath);
|
|
415
|
-
|
|
416
|
-
for (const [id, plugin] of plugins) {
|
|
417
|
-
PluginManager.#plugins.set(id, plugin);
|
|
418
|
-
PluginManager.#register(id, plugin);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
logger.info(`[HMR] ${type === 'add' ? 'Added' : 'Reloaded'}: ${rel} (${plugins.size})`);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
PluginManager.#syncAll();
|
|
425
|
-
});
|
|
426
|
-
} catch (err) {
|
|
427
|
-
PluginManager.#handleError(`[HMR:${rel}] Failed:`, err);
|
|
428
|
-
} finally {
|
|
429
|
-
if (type === 'unlink') {
|
|
430
|
-
const lock = PluginManager.#fileLocks.get(filePath);
|
|
431
|
-
if (lock && !lock.isLocked()) PluginManager.#fileLocks.delete(filePath);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
static #unloadFile(filePath) {
|
|
437
|
-
let count = 0;
|
|
438
|
-
for (const [id, plugin] of PluginManager.#plugins) {
|
|
439
|
-
if (plugin._meta?.filePath === filePath) {
|
|
440
|
-
PluginManager.#unregister(id);
|
|
441
|
-
PluginManager.#plugins.delete(id);
|
|
442
|
-
count++;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
return count;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
get(id) {
|
|
449
|
-
return PluginManager.#plugins.get(id);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
get all() {
|
|
453
|
-
return new Map(PluginManager.#plugins);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
get events() {
|
|
457
|
-
return [...this.#handlers.keys()];
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
get destroyed() {
|
|
461
|
-
return this.#destroyed;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
static get instances() {
|
|
465
|
-
return PluginManager.#instances.size;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
static get count() {
|
|
469
|
-
return PluginManager.#plugins.size;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
export const pluginManager = async (sock) => {
|
|
474
|
-
const manager = new PluginManager(sock);
|
|
475
|
-
await manager.init();
|
|
476
|
-
return manager;
|
|
477
|
-
}
|