Перевести проект на Telegram Business (без хранения сессии)
Смена архитектуры с MTProto-юзербота (GramJS + строка сессии) на официальное бизнес-подключение Telegram (Bot API). Проект больше НЕ хранит сессию — работает только через бота с включённым Business Mode, подключённого в Настройках → Telegram для бизнеса → Чат-боты. Это соответствует ToS. Изменения: - удалено: scripts/login.js, src/userbot/client.js, модуль onetime - src/userbot/ → src/business/: events.js подписывает business_message / edited / deleted / connection и нормализует сообщение; context.js даёт ctx.api = bot.api - core/media.js: скачивание через getFile + fetch вместо GramJS - core/db.js: составной ключ PRIMARY KEY (chat_id, msg_id) — в Bot API message_id уникален только внутри чата - bot/panel.js: барьер владельца пропускает бизнес-апдейты (у них ctx.from это собеседник); убран UI одноразовых - index.js: сборка только на grammY, явный allowed_updates с бизнес-типами - config.js: убраны API_ID/API_HASH/SESSION, проверки на старте почищены - package.json: убраны зависимости telegram и input, скрипт login - README и .env.example переписаны под бизнес-подключение Границы: onetime невозможен через Business API (сервер отбрасывает self-destruct до доставки боту), медиа >~20 МБ не выгружаются (лимит getFile), перехват — только личные 1-на-1 чаты. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,21 +2,20 @@ import { getCachedMessage } from '../core/db.js';
|
||||
import { logger } from '../core/logger.js';
|
||||
|
||||
/**
|
||||
* Ловит удаления. MTProto в событии удаления присылает ТОЛЬКО id сообщений —
|
||||
* без содержимого и (в личных чатах) без чата. Поэтому восстанавливаем из кэша,
|
||||
* который наполняет модуль [cache]. Нашли в кэше — значит это было входящее
|
||||
* в личке: сохраняем как перехват и уведомляем владельца.
|
||||
* Ловит удаления. Telegram в событии deleted_business_messages присылает ТОЛЬКО
|
||||
* id сообщений и чат — без содержимого. Поэтому восстанавливаем из кэша, который
|
||||
* наполняет модуль [cache], по паре (chatId, msgId). Нашли — значит это было
|
||||
* входящее в личке: сохраняем как перехват и уведомляем владельца.
|
||||
*/
|
||||
export default {
|
||||
name: 'antidelete',
|
||||
description: 'Сохраняет удалённые собеседником сообщения и медиа',
|
||||
|
||||
async onDeleted(event, ctx) {
|
||||
const ids = event.deletedIds ?? [];
|
||||
if (!ids.length) return;
|
||||
async onDeleted({ chatId, msgIds }, ctx) {
|
||||
if (!msgIds?.length) return;
|
||||
|
||||
for (const id of ids) {
|
||||
const cached = getCachedMessage(id);
|
||||
for (const id of msgIds) {
|
||||
const cached = getCachedMessage(chatId, id);
|
||||
if (!cached) continue; // не наше/не кэшировали — пропускаем
|
||||
|
||||
await ctx.capture('deleted', {
|
||||
|
||||
+16
-29
@@ -1,50 +1,37 @@
|
||||
import { config } from '../config.js';
|
||||
import { cacheMessage } from '../core/db.js';
|
||||
import { downloadEncrypted, isOneTime } from '../core/media.js';
|
||||
import { isPrivateIncoming } from '../userbot/events.js';
|
||||
import { downloadEncrypted } from '../core/media.js';
|
||||
|
||||
/**
|
||||
* Фундамент для antidelete: кладёт каждое входящее из личных диалогов в кэш.
|
||||
* При удалении сообщение уже недоступно у Telegram — восстановить его можно
|
||||
* только из этого кэша. Поэтому текст сохраняем всегда, а медиа — сразу качаем
|
||||
* (если включён CACHE_MEDIA), потому что после удаления ссылка на файл мертва.
|
||||
* При удалении Telegram присылает только id (без содержимого) — восстановить
|
||||
* сообщение можно лишь из этого кэша. Поэтому текст сохраняем всегда, а медиа —
|
||||
* сразу качаем (если включён CACHE_MEDIA): при удалении file_id уже не приходит.
|
||||
*/
|
||||
export default {
|
||||
name: 'cache',
|
||||
description: 'Кэширует входящие личные сообщения — основа для antidelete',
|
||||
|
||||
async onMessage(message, ctx) {
|
||||
if (!isPrivateIncoming(message, ctx.me)) return;
|
||||
|
||||
// Одноразовые ведёт отдельный модуль onetime — тут их не трогаем,
|
||||
// чтобы не качать дважды.
|
||||
if (isOneTime(message)) return;
|
||||
async onMessage(msg, ctx) {
|
||||
if (!msg.isPrivateIncoming) return;
|
||||
|
||||
let mediaKind = null;
|
||||
let mediaPath = null;
|
||||
if (message.media && config.cacheMedia) {
|
||||
const saved = await downloadEncrypted(ctx.client, message, 'cache');
|
||||
mediaKind = saved?.kind ?? null;
|
||||
mediaPath = saved?.path ?? null;
|
||||
} else if (message.media) {
|
||||
mediaKind = 'media'; // знаем, что медиа было, но файл не кэшируем
|
||||
if (msg.media && config.cacheMedia) {
|
||||
mediaKind = msg.media.kind;
|
||||
mediaPath = await downloadEncrypted(ctx.api, msg.media.fileId, 'cache', msg.msgId);
|
||||
} else if (msg.media) {
|
||||
mediaKind = msg.media.kind; // знаем, что медиа было, но файл не кэшируем
|
||||
}
|
||||
|
||||
const sender = await message.getSender().catch(() => null);
|
||||
cacheMessage({
|
||||
msgId: message.id,
|
||||
chatId: message.chatId,
|
||||
senderId: message.senderId,
|
||||
sender: senderLabel(sender),
|
||||
text: message.text ?? '',
|
||||
msgId: msg.msgId,
|
||||
chatId: msg.chatId,
|
||||
senderId: msg.senderId,
|
||||
sender: msg.sender,
|
||||
text: msg.text,
|
||||
mediaKind,
|
||||
mediaPath,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
function senderLabel(sender) {
|
||||
if (!sender) return '';
|
||||
const name = [sender.firstName, sender.lastName].filter(Boolean).join(' ');
|
||||
return sender.username ? `${name} (@${sender.username})`.trim() : name;
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { downloadEncrypted, isOneTime } from '../core/media.js';
|
||||
import { logger } from '../core/logger.js';
|
||||
import { isPrivateIncoming } from '../userbot/events.js';
|
||||
|
||||
/**
|
||||
* Одноразовые фото/видео (ttl-медиа) Telegram стирает сразу после просмотра.
|
||||
* Ловим их в момент прихода: качаем файл, шифруем, сохраняем как перехват.
|
||||
* Работает только по входящим в личке — свои и групповые не трогаем.
|
||||
*/
|
||||
export default {
|
||||
name: 'onetime',
|
||||
description: 'Сохраняет одноразовые фото и видео',
|
||||
|
||||
async onMessage(message, ctx) {
|
||||
if (!isPrivateIncoming(message, ctx.me)) return;
|
||||
if (!isOneTime(message)) return;
|
||||
|
||||
const saved = await downloadEncrypted(ctx.client, message, 'onetime');
|
||||
const sender = await message.getSender().catch(() => null);
|
||||
|
||||
await ctx.capture('onetime', {
|
||||
chatId: message.chatId,
|
||||
senderId: message.senderId,
|
||||
sender: senderLabel(sender),
|
||||
text: message.text ?? '',
|
||||
mediaKind: saved?.kind ?? 'media',
|
||||
mediaPath: saved?.path ?? null,
|
||||
});
|
||||
logger.info(`onetime: сохранено одноразовое ${saved?.kind ?? 'медиа'} от ${senderLabel(sender) || '?'}`);
|
||||
},
|
||||
};
|
||||
|
||||
function senderLabel(sender) {
|
||||
if (!sender) return '';
|
||||
const name = [sender.firstName, sender.lastName].filter(Boolean).join(' ');
|
||||
return sender.username ? `${name} (@${sender.username})`.trim() : name;
|
||||
}
|
||||
Reference in New Issue
Block a user