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; /** Called when Discord client is ready */ onReady?(ctx: PluginContext): Promise; /** 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; /** Called on graceful shutdown */ destroy?(ctx: PluginContext): Promise; } export interface PluginContext { client: Client; dataDir: string; adminPwd: string; allowedGuildIds: string[]; } 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]; } // ── 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(); 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); } }