import { logger } from './logger.js'; /** * Реестр модулей юзербота. Модуль — это файл в src/modules, который экспортирует * по умолчанию объект вида: * * export default { * name: 'antidelete', * description: 'Ловит удалённые сообщения', * setup(ctx) { ... }, // однократная инициализация * onMessage(message, ctx) { ... }, // новое входящее (личные чаты) * onDeleted(event, ctx) { ... }, // событие удаления * onEdited(message, ctx) { ... }, // правка сообщения * panel: { title, id, render, ... }, // раздел в компаньон-боте (опц.) * } * * Обработчики опциональны — модуль объявляет только то, что ему нужно. */ export class ModuleRegistry { #modules = []; #byName = new Map(); register(definition, source = '') { const module = normalize(definition, source); if (this.#byName.has(module.name)) { throw new Error(`Модуль "${module.name}" уже зарегистрирован (${source}).`); } this.#byName.set(module.name, module); this.#modules.push(module); return module; } list() { return [...this.#modules]; } get(name) { return this.#byName.get(name); } /** Модули, у которых объявлен нужный хук — чтобы не дёргать пустышки. */ withHook(hook) { return this.#modules.filter((m) => typeof m[hook] === 'function'); } panels() { return this.#modules.filter((m) => m.panel).map((m) => ({ ...m.panel, module: m.name })); } } function normalize(def, source) { if (!def || typeof def !== 'object') { throw new Error(`${source}: модуль должен экспортировать объект по умолчанию.`); } if (typeof def.name !== 'string' || !def.name.trim()) { throw new Error(`${source}: у модуля не задано поле "name".`); } for (const hook of ['setup', 'onMessage', 'onDeleted', 'onEdited']) { if (def[hook] !== undefined && typeof def[hook] !== 'function') { throw new Error(`${source}: "${hook}" должен быть функцией.`); } } return { name: def.name, description: def.description ?? '', setup: def.setup, onMessage: def.onMessage, onDeleted: def.onDeleted, onEdited: def.onEdited, panel: def.panel, source, }; } /** * Прогоняет хук по всем модулям, где он объявлен. Ошибка одного модуля * не роняет остальные — логируем и идём дальше. */ export async function runHook(registry, hook, payload, ctx) { for (const module of registry.withHook(hook)) { try { await module[hook](payload, ctx); } catch (err) { logger.error(`Модуль "${module.name}", хук ${hook}:`, err); } } }