2026-08-09 19:16:52 +03:00
|
|
|
import { logger } from './logger.js';
|
2026-08-09 16:53:38 +03:00
|
|
|
|
2026-08-09 19:16:52 +03:00
|
|
|
/**
|
|
|
|
|
* Реестр модулей юзербота. Модуль — это файл в 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();
|
2026-08-09 16:53:38 +03:00
|
|
|
|
|
|
|
|
register(definition, source = '<inline>') {
|
2026-08-09 19:16:52 +03:00
|
|
|
const module = normalize(definition, source);
|
|
|
|
|
if (this.#byName.has(module.name)) {
|
|
|
|
|
throw new Error(`Модуль "${module.name}" уже зарегистрирован (${source}).`);
|
2026-08-09 16:53:38 +03:00
|
|
|
}
|
2026-08-09 19:16:52 +03:00
|
|
|
this.#byName.set(module.name, module);
|
|
|
|
|
this.#modules.push(module);
|
|
|
|
|
return module;
|
2026-08-09 16:53:38 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
list() {
|
2026-08-09 19:16:52 +03:00
|
|
|
return [...this.#modules];
|
2026-08-09 16:53:38 +03:00
|
|
|
}
|
|
|
|
|
|
2026-08-09 19:16:52 +03:00
|
|
|
get(name) {
|
|
|
|
|
return this.#byName.get(name);
|
2026-08-09 16:53:38 +03:00
|
|
|
}
|
|
|
|
|
|
2026-08-09 19:16:52 +03:00
|
|
|
/** Модули, у которых объявлен нужный хук — чтобы не дёргать пустышки. */
|
|
|
|
|
withHook(hook) {
|
|
|
|
|
return this.#modules.filter((m) => typeof m[hook] === 'function');
|
2026-08-09 16:53:38 +03:00
|
|
|
}
|
|
|
|
|
|
2026-08-09 19:16:52 +03:00
|
|
|
panels() {
|
|
|
|
|
return this.#modules.filter((m) => m.panel).map((m) => ({ ...m.panel, module: m.name }));
|
2026-08-09 16:53:38 +03:00
|
|
|
}
|
2026-08-09 19:16:52 +03:00
|
|
|
}
|
2026-08-09 16:53:38 +03:00
|
|
|
|
2026-08-09 19:16:52 +03:00
|
|
|
function normalize(def, source) {
|
|
|
|
|
if (!def || typeof def !== 'object') {
|
|
|
|
|
throw new Error(`${source}: модуль должен экспортировать объект по умолчанию.`);
|
2026-08-09 16:53:38 +03:00
|
|
|
}
|
2026-08-09 19:16:52 +03:00
|
|
|
if (typeof def.name !== 'string' || !def.name.trim()) {
|
|
|
|
|
throw new Error(`${source}: у модуля не задано поле "name".`);
|
2026-08-09 16:53:38 +03:00
|
|
|
}
|
2026-08-09 19:16:52 +03:00
|
|
|
for (const hook of ['setup', 'onMessage', 'onDeleted', 'onEdited']) {
|
|
|
|
|
if (def[hook] !== undefined && typeof def[hook] !== 'function') {
|
|
|
|
|
throw new Error(`${source}: "${hook}" должен быть функцией.`);
|
|
|
|
|
}
|
2026-08-09 16:53:38 +03:00
|
|
|
}
|
|
|
|
|
return {
|
2026-08-09 19:16:52 +03:00
|
|
|
name: def.name,
|
|
|
|
|
description: def.description ?? '',
|
|
|
|
|
setup: def.setup,
|
|
|
|
|
onMessage: def.onMessage,
|
|
|
|
|
onDeleted: def.onDeleted,
|
|
|
|
|
onEdited: def.onEdited,
|
|
|
|
|
panel: def.panel,
|
2026-08-09 16:53:38 +03:00
|
|
|
source,
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-08-09 19:16:52 +03:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Прогоняет хук по всем модулям, где он объявлен. Ошибка одного модуля
|
|
|
|
|
* не роняет остальные — логируем и идём дальше.
|
|
|
|
|
*/
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|