2026-03-05 22:52:13 +01:00
|
|
|
import { Client } from 'discord.js';
|
|
|
|
|
import express from 'express';
|
|
|
|
|
|
|
|
|
|
export interface Plugin {
|
|
|
|
|
name: string;
|
|
|
|
|
version: string;
|
|
|
|
|
description: string;
|
|
|
|
|
|
|
|
|
|
/** Called once when plugin is loaded */
|
|
|
|
|
init(ctx: PluginContext): Promise<void>;
|
|
|
|
|
|
|
|
|
|
/** Called when Discord client is ready */
|
|
|
|
|
onReady?(ctx: PluginContext): Promise<void>;
|
|
|
|
|
|
|
|
|
|
/** Called to register Express routes */
|
|
|
|
|
registerRoutes?(app: express.Application, ctx: PluginContext): void;
|
|
|
|
|
|
|
|
|
|
/** Called to build SSE snapshot data for new clients */
|
|
|
|
|
getSnapshot?(ctx: PluginContext): Record<string, any>;
|
|
|
|
|
|
|
|
|
|
/** Called on graceful shutdown */
|
|
|
|
|
destroy?(ctx: PluginContext): Promise<void>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface PluginContext {
|
|
|
|
|
client: Client;
|
|
|
|
|
dataDir: string;
|
2026-03-05 23:48:23 +01:00
|
|
|
adminPwd: string;
|
|
|
|
|
allowedGuildIds: string[];
|
2026-03-05 22:52:13 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const loadedPlugins: Plugin[] = [];
|
|
|
|
|
|
|
|
|
|
export function registerPlugin(plugin: Plugin): void {
|
|
|
|
|
loadedPlugins.push(plugin);
|
|
|
|
|
console.log(`[Plugin] Registered: ${plugin.name} v${plugin.version}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getPlugins(): Plugin[] {
|
|
|
|
|
return [...loadedPlugins];
|
|
|
|
|
}
|
2026-03-06 10:21:11 +01:00
|
|
|
|
|
|
|
|
// ── Voice claim system ──
|
|
|
|
|
// Only one plugin can use voice per guild. When a new plugin claims voice,
|
|
|
|
|
// the previous claimant's cleanup callback is invoked automatically.
|
|
|
|
|
type VoiceClaimCleanup = () => void;
|
|
|
|
|
const voiceClaims = new Map<string, { plugin: string; cleanup: VoiceClaimCleanup }>();
|
|
|
|
|
|
|
|
|
|
export function claimVoice(guildId: string, pluginName: string, cleanup: VoiceClaimCleanup): void {
|
|
|
|
|
const existing = voiceClaims.get(guildId);
|
|
|
|
|
if (existing && existing.plugin !== pluginName) {
|
|
|
|
|
console.log(`[Voice] ${pluginName} claims guild ${guildId}, releasing ${existing.plugin}`);
|
|
|
|
|
existing.cleanup();
|
|
|
|
|
}
|
|
|
|
|
voiceClaims.set(guildId, { plugin: pluginName, cleanup });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function releaseVoice(guildId: string, pluginName: string): void {
|
|
|
|
|
const claim = voiceClaims.get(guildId);
|
|
|
|
|
if (claim?.plugin === pluginName) {
|
|
|
|
|
voiceClaims.delete(guildId);
|
|
|
|
|
}
|
|
|
|
|
}
|