Skip to content

Commit 27f0cfa

Browse files
committed
new: adding custom plugins dir and hot module reload for plugins
1 parent aeed550 commit 27f0cfa

5 files changed

Lines changed: 86 additions & 48 deletions

File tree

src/Classes/client.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export class Client {
3232
logs: Logs;
3333

3434
middleware = new Middleware<any>();
35-
plugins = new Plugins();
35+
plugins: Plugins;
3636
health: HealthManager;
3737
cleanup: CleanUpManager;
3838

@@ -47,6 +47,8 @@ export class Client {
4747
new SignalCommunity(this),
4848
]);
4949

50+
this.plugins = new Plugins(this.options.pluginsDir, this.options.pluginsHmr);
51+
5052
this._ready = this.initialize(proxy);
5153
return proxy;
5254
}
@@ -65,6 +67,7 @@ export class Client {
6567
await registerAuthCreds(this);
6668

6769
await this.plugins.load();
70+
this.plugins.setupHmr();
6871

6972
new Listener(client || this);
7073

src/Classes/plugins.ts

Lines changed: 66 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,12 @@ export class Plugins {
2727
private pluginsDir: string;
2828
private globalEnabled = true;
2929
private disabledPlugins = new Set<string>();
30+
private hmr = false;
31+
private watcher: fs.FSWatcher | null = null;
3032

31-
constructor(pluginsDir: string = path.join(process.cwd(), 'plugins')) {
32-
this.pluginsDir = pluginsDir;
33+
constructor(pluginsDir: string = 'plugins', hmr = false) {
34+
this.pluginsDir = path.isAbsolute(pluginsDir) ? pluginsDir : path.join(process.cwd(), pluginsDir);
35+
this.hmr = hmr;
3336
}
3437

3538
private async getAllFiles(dir: string, baseDir: string = dir): Promise<FileInfo[]> {
@@ -49,21 +52,17 @@ export class Plugins {
4952
return this.getAllFiles(filePath, baseDir);
5053
}
5154

52-
const isValidFile =
53-
(entry.name.endsWith('.ts') || entry.name.endsWith('.js')) &&
54-
!entry.name.endsWith('.d.ts');
55+
const isValidFile = (entry.name.endsWith('.ts') || entry.name.endsWith('.js')) && !entry.name.endsWith('.d.ts');
5556

5657
if (isValidFile) {
5758
const relativePath = path.relative(baseDir, dir);
58-
const parent = relativePath === ''
59-
? null
60-
: relativePath.split(path.sep)[0];
61-
59+
const parent = relativePath === '' ? null : relativePath.split(path.sep)[0];
60+
6261
return [{ filePath, parent }];
6362
}
6463

6564
return [];
66-
})
65+
}),
6766
);
6867

6968
return results.flat();
@@ -81,7 +80,10 @@ export class Plugins {
8180
const loadResults = await Promise.all(
8281
files.map(async ({ filePath, parent }): Promise<PluginDefinition | null> => {
8382
try {
84-
const pluginModule = await import(pathToFileURL(filePath).href);
83+
const fileUrl = pathToFileURL(filePath).href;
84+
const finalUrl = this.hmr ? `${fileUrl}?t=${Date.now()}` : fileUrl;
85+
86+
const pluginModule = await import(finalUrl);
8587
let plugin = pluginModule.default;
8688

8789
if (plugin?.default) {
@@ -90,28 +92,48 @@ export class Plugins {
9092

9193
if (plugin?.handler && plugin?.config) {
9294
const pluginId = this.getPluginId(plugin.config.matcher);
93-
return {
94-
...plugin,
95-
parent,
96-
enabled: !this.disabledPlugins.has(pluginId)
95+
return {
96+
...plugin,
97+
parent,
98+
enabled: !this.disabledPlugins.has(pluginId),
9799
};
98100
}
99-
} catch {}
101+
} catch (error) {
102+
console.error(`[Plugins] Failed to load plugin ${filePath}:`, error);
103+
}
100104
return null;
101-
})
105+
}),
102106
);
103107

104108
this.plugins = loadResults.filter((p): p is PluginDefinition => p !== null);
105109
}
106110

111+
setupHmr(): void {
112+
if (!this.hmr || this.watcher) return;
113+
114+
try {
115+
if (!fs.existsSync(this.pluginsDir)) {
116+
return;
117+
}
118+
119+
this.watcher = fs.watch(this.pluginsDir, { recursive: true }, async (event, filename) => {
120+
if (filename && (filename.endsWith('.ts') || filename.endsWith('.js'))) {
121+
await this.reload();
122+
}
123+
});
124+
} catch (error) {
125+
console.error('[Plugins] Failed to setup HMR:', error);
126+
}
127+
}
128+
107129
private getPluginId(matcher: (string | RegExp)[]): string {
108-
return matcher.map(m => m.toString()).join('|');
130+
return matcher.map((m) => m.toString()).join('|');
109131
}
110132

111133
async execute(wa: Client, ctx: MiddlewareContextType): Promise<void> {
112134
if (!this.globalEnabled) return;
113135

114-
const messageText = ctx.messages.text || '';
136+
const messageText = ctx.messages?.text || '';
115137

116138
for (const plugin of this.plugins) {
117139
if (!plugin.enabled) continue;
@@ -122,7 +144,9 @@ export class Plugins {
122144
if (isMatch) {
123145
await plugin.handler(wa, ctx);
124146
}
125-
} catch {}
147+
} catch (error) {
148+
console.error(`[Plugins] Error executing plugin:`, error);
149+
}
126150
}
127151
}
128152

@@ -137,7 +161,7 @@ export class Plugins {
137161

138162
enableAll(): void {
139163
this.globalEnabled = true;
140-
this.plugins.forEach(p => p.enabled = true);
164+
this.plugins.forEach((p) => (p.enabled = true));
141165
this.disabledPlugins.clear();
142166
}
143167

@@ -147,10 +171,8 @@ export class Plugins {
147171

148172
enable(matcher: string | RegExp): boolean {
149173
const matcherStr = matcher.toString();
150-
const plugin = this.plugins.find(p =>
151-
p.config.matcher.some(m => m.toString() === matcherStr)
152-
);
153-
174+
const plugin = this.plugins.find((p) => p.config.matcher.some((m) => m.toString() === matcherStr));
175+
154176
if (plugin) {
155177
plugin.enabled = true;
156178
this.disabledPlugins.delete(this.getPluginId(plugin.config.matcher));
@@ -161,10 +183,8 @@ export class Plugins {
161183

162184
disable(matcher: string | RegExp): boolean {
163185
const matcherStr = matcher.toString();
164-
const plugin = this.plugins.find(p =>
165-
p.config.matcher.some(m => m.toString() === matcherStr)
166-
);
167-
186+
const plugin = this.plugins.find((p) => p.config.matcher.some((m) => m.toString() === matcherStr));
187+
168188
if (plugin) {
169189
plugin.enabled = false;
170190
this.disabledPlugins.add(this.getPluginId(plugin.config.matcher));
@@ -175,7 +195,8 @@ export class Plugins {
175195

176196
enableByParent(parent: string): number {
177197
let count = 0;
178-
this.plugins.forEach(p => {
198+
199+
this.plugins.forEach((p) => {
179200
if (p.parent === parent) {
180201
p.enabled = true;
181202
this.disabledPlugins.delete(this.getPluginId(p.config.matcher));
@@ -187,7 +208,8 @@ export class Plugins {
187208

188209
disableByParent(parent: string): number {
189210
let count = 0;
190-
this.plugins.forEach(p => {
211+
212+
this.plugins.forEach((p) => {
191213
if (p.parent === parent) {
192214
p.enabled = false;
193215
this.disabledPlugins.add(this.getPluginId(p.config.matcher));
@@ -205,9 +227,9 @@ export class Plugins {
205227
return this.plugins;
206228
}
207229

208-
getPluginsInfo(): {
209-
matcher: (string | RegExp)[];
210-
metadata?: Record<string, any>;
230+
getPluginsInfo(): {
231+
matcher: (string | RegExp)[];
232+
metadata?: Record<string, any>;
211233
parent: string | null;
212234
enabled: boolean;
213235
}[] {
@@ -222,15 +244,20 @@ export class Plugins {
222244
async reload(): Promise<void> {
223245
this.plugins = [];
224246
await this.load();
247+
console.log(`[Plugins] Successfully reloaded ${this.plugins.length} plugins.`);
248+
}
249+
250+
stopHmr(): void {
251+
if (this.watcher) {
252+
this.watcher.close();
253+
this.watcher = null;
254+
}
225255
}
226256
}
227257

228-
export const definePlugins = (
229-
handler: PluginsHandlerType,
230-
config: PluginsConfigType
231-
): Omit<PluginDefinition, 'parent' | 'enabled'> => {
258+
export const definePlugins = (handler: PluginsHandlerType, config: PluginsConfigType): Omit<PluginDefinition, 'parent' | 'enabled'> => {
232259
return {
233260
handler,
234261
config,
235262
};
236-
};
263+
};

src/Library/ffmpeg/image.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { Jimp } from 'jimp';
21
import { fileTypeFromBuffer } from 'file-type';
2+
import { Jimp } from 'jimp';
33
import { BufferConverter, FFMPEG_CONSTANTS, FileManager, type MediaInput } from './core';
44

55
let sharp: any;
6+
67
try {
78
sharp = require('sharp');
89
} catch {

src/Library/fire-forget.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export class FireAndForget {
4747
closeResolve: ((value: void | PromiseLike<void>) => void) | null;
4848

4949
constructor(options?: FireAndForgetOptions) {
50-
this.concurrency = options?.concurrency || 30;
50+
this.concurrency = options?.concurrency || 20;
5151
this.timeout = options?.timeout || 30000; // 30s default
5252
this.onError = options?.onError || this._defaultErrorHandler;
5353

@@ -173,9 +173,7 @@ export class FireAndForget {
173173
}
174174
}
175175

176-
_defaultErrorHandler(err: Error, task: Task) {
177-
console.error(`[FireAndForget] Task ${task.id} failed:`, err);
178-
}
176+
_defaultErrorHandler(err: Error, task: Task) {}
179177

180178
_generateId() {
181179
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;

src/Types/client.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,15 @@ export const StickerMetadataType = z
3232
export const autoCleanUp = z
3333
.object({
3434
enabled: z.boolean().default(false).optional(),
35-
intervalMs: z.number().default(60 * 60 * 1000).optional(), // How often to run cleanup (default 1 hour)
36-
maxAgeMs: z.number().default(24 * 60 * 60 * 1000).optional(), // Max age of messages before deletion (default 24 hours)
37-
scopes: z.array(z.string()).default(['messages']).optional(), // Database scopes to clean up
35+
intervalMs: z
36+
.number()
37+
.default(60 * 60 * 1000)
38+
.optional(),
39+
maxAgeMs: z
40+
.number()
41+
.default(24 * 60 * 60 * 1000)
42+
.optional(),
43+
scopes: z.array(z.string()).default(['messages']).optional(),
3844
})
3945
.optional();
4046

@@ -56,6 +62,9 @@ export const ClientBaseType = z.object({
5662
autoPresence: z.boolean().default(true).optional(),
5763
autoRejectCall: z.boolean().default(true).optional(),
5864

65+
pluginsDir: z.string().default('plugins').optional(),
66+
pluginsHmr: z.boolean().default(true).optional(),
67+
5968
autoCleanUp,
6069

6170
limiter: LimiterType,

0 commit comments

Comments
 (0)