15a03a77fd
Смена архитектуры с бизнес-бота на Bot API (команды со слешем) на личный юзербот на GramJS/MTProto с управлением через компаньон-бота. Возможности: - antidelete — кэш входящих в личке, восстановление удалённых «у всех» - onetime — перехват «просмотр один раз» фото/видео до истечения ссылки - локальное хранилище node:sqlite + шифрование AES-256-GCM, чистка по сроку - компаньон-бот (grammY): инлайн-панель, лента с пагинацией, выдача медиа, доступ только владельцу по OWNER_ID Устройство: - core/ — crypto, db, media, loader, registry, logger - userbot/ — MTProto-клиент из строки сессии, подписка на события - modules/ — фичи как модули с хуками setup/onMessage/onEdited/onDeleted - bot/ — компаньон-бот (единственный UI) - scripts/login.js — генерация строки сессии Удалён старый код Bot API (commands/, handlers/, middlewares/, dispatcher, store). Обновлены README и .env.example. Hcrgram/ (сторонний референс) добавлен в .gitignore. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
88 lines
3.0 KiB
JavaScript
88 lines
3.0 KiB
JavaScript
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 = '<inline>') {
|
|
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);
|
|
}
|
|
}
|
|
}
|