Files

88 lines
3.0 KiB
JavaScript
Raw Permalink Normal View History

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