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>
61 lines
2.4 KiB
JavaScript
61 lines
2.4 KiB
JavaScript
import { mkdirSync } from 'node:fs';
|
||
import { readFile, writeFile } from 'node:fs/promises';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
import { config, mediaPath } from '../config.js';
|
||
import { encrypt } from './crypto.js';
|
||
import { logger } from './logger.js';
|
||
|
||
/** Определяем тип медиа по объекту сообщения GramJS. */
|
||
export function detectMedia(message) {
|
||
if (!message?.media) return null;
|
||
if (message.photo) return 'photo';
|
||
if (message.video) return 'video';
|
||
if (message.voice) return 'voice';
|
||
if (message.videoNote) return 'video_note';
|
||
if (message.gif) return 'gif';
|
||
if (message.audio) return 'audio';
|
||
if (message.sticker) return 'sticker';
|
||
if (message.document) return 'document';
|
||
return 'media';
|
||
}
|
||
|
||
/**
|
||
* Одноразовые медиа помечены ttlSeconds на самом медиа. Такие файлы Telegram
|
||
* стирает у клиента после просмотра — их важно скачать в момент прихода.
|
||
*/
|
||
export function isOneTime(message) {
|
||
const media = message?.media;
|
||
const ttl = media?.ttlSeconds ?? media?.photo?.ttlSeconds ?? media?.document?.ttlSeconds;
|
||
return Boolean(ttl);
|
||
}
|
||
|
||
/**
|
||
* Скачивает медиа сообщения, шифрует и кладёт на диск. Возвращает
|
||
* { kind, path } либо null. Имя файла не раскрывает содержимого.
|
||
*/
|
||
export async function downloadEncrypted(client, message, tag = 'm') {
|
||
const kind = detectMedia(message);
|
||
if (!kind) return null;
|
||
|
||
try {
|
||
const buffer = await client.downloadMedia(message, {});
|
||
if (!buffer || !buffer.length) return { kind, path: null };
|
||
|
||
mkdirSync(mediaPath(), { recursive: true });
|
||
const name = `${tag}_${message.id ?? 'x'}_${buffer.length}.enc`;
|
||
const full = fileURLToPath(new URL(name, config.mediaDir));
|
||
await writeFile(full, encrypt(buffer));
|
||
logger.debug(`media: сохранено ${kind} -> ${name} (${buffer.length} б)`);
|
||
return { kind, path: name };
|
||
} catch (err) {
|
||
logger.warn(`media: не удалось скачать ${kind}:`, err.message);
|
||
return { kind, path: null };
|
||
}
|
||
}
|
||
|
||
/** Читает зашифрованный медиа-файл с диска по имени (как хранится в БД). */
|
||
export function readMedia(name) {
|
||
return readFile(fileURLToPath(new URL(name, config.mediaDir)));
|
||
}
|